From: Marcel van der Veldt Date: Thu, 19 Feb 2026 10:03:46 +0000 (+0100) Subject: Fix for protocol matching with locally administered mac X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=956cc0d3afb47828aa5d6c78c8b74ba3e6239e56;p=music-assistant-server.git Fix for protocol matching with locally administered mac --- diff --git a/music_assistant/controllers/players/protocol_linking.py b/music_assistant/controllers/players/protocol_linking.py index 5b0c1ed3..b8758575 100644 --- a/music_assistant/controllers/players/protocol_linking.py +++ b/music_assistant/controllers/players/protocol_linking.py @@ -36,6 +36,7 @@ from music_assistant.helpers.util import ( is_locally_administered_mac, is_valid_mac_address, normalize_ip_address, + normalize_mac_for_matching, resolve_real_mac_address, ) from music_assistant.models.player import Player @@ -812,8 +813,15 @@ class ProtocolLinkingMixin: continue # Normalize values for comparison - val_a_norm = val_a.lower().replace(":", "").replace("-", "") - val_b_norm = val_b.lower().replace(":", "").replace("-", "") + if conn_type == IdentifierType.MAC_ADDRESS: + # Use MAC normalization that handles locally-administered bit differences + # Some protocols (like AirPlay) report a locally-administered MAC variant + # where bit 1 of the first octet is set (e.g., 54:78:... vs 56:78:...) + val_a_norm = normalize_mac_for_matching(val_a) + val_b_norm = normalize_mac_for_matching(val_b) + else: + val_a_norm = val_a.lower().replace(":", "").replace("-", "") + val_b_norm = val_b.lower().replace(":", "").replace("-", "") # Direct match if val_a_norm == val_b_norm: diff --git a/music_assistant/helpers/util.py b/music_assistant/helpers/util.py index ea5b7423..2b8f5424 100644 --- a/music_assistant/helpers/util.py +++ b/music_assistant/helpers/util.py @@ -770,6 +770,40 @@ def is_locally_administered_mac(mac_address: str) -> bool: return False +def normalize_mac_for_matching(mac_address: str) -> str: + """ + Normalize a MAC address for device matching by masking out the locally-administered bit. + + Some protocols (like AirPlay) report a locally-administered MAC address variant where + bit 1 of the first octet is set. For example: + - Real hardware MAC: 54:78:C9:E6:0D:A0 (first byte 0x54 = 01010100) + - AirPlay reports: 56:78:C9:E6:0D:A0 (first byte 0x56 = 01010110) + + These represent the same device but differ only in the locally-administered bit. + This function normalizes the MAC by clearing bit 1 of the first octet, allowing + both variants to match the same device. + + :param mac_address: MAC address in any common format (with :, -, or no separator). + :return: Normalized MAC address in lowercase without separators, with the + locally-administered bit cleared. + """ + # Normalize MAC address (remove separators, lowercase) + mac_clean = mac_address.lower().replace(":", "").replace("-", "") + if len(mac_clean) != 12: + # Invalid MAC length, return as-is + return mac_clean + + try: + # Parse first octet and clear bit 1 (the locally-administered bit) + first_octet = int(mac_clean[:2], 16) + first_octet_normalized = first_octet & ~0x02 # Clear bit 1 + # Reconstruct the MAC with the normalized first octet + return f"{first_octet_normalized:02x}{mac_clean[2:]}" + except ValueError: + # Invalid hex, return as-is + return mac_clean + + def is_valid_mac_address(mac_address: str | None) -> bool: """ Check if a MAC address is valid and usable for device identification. diff --git a/music_assistant/providers/universal_player/provider.py b/music_assistant/providers/universal_player/provider.py index 618b76b1..de120010 100644 --- a/music_assistant/providers/universal_player/provider.py +++ b/music_assistant/providers/universal_player/provider.py @@ -16,6 +16,7 @@ from typing import TYPE_CHECKING from music_assistant_models.enums import IdentifierType, PlayerType from music_assistant.constants import CONF_PLAYERS +from music_assistant.helpers.util import normalize_mac_for_matching from music_assistant.models.player import DeviceInfo from music_assistant.models.player_provider import PlayerProvider @@ -367,8 +368,11 @@ class UniversalPlayerProvider(PlayerProvider): for player in protocol_players: identifiers = player.device_info.identifiers # Prefer MAC address (most reliable) + # Use normalize_mac_for_matching to handle locally-administered MAC variants + # Some protocols (like AirPlay) report a variant where bit 1 of the first octet + # is set (e.g., 54:78:... vs 56:78:...), but they represent the same device if mac := identifiers.get(IdentifierType.MAC_ADDRESS): - return mac.replace(":", "").replace("-", "").lower() + return normalize_mac_for_matching(mac) # Fall back to UUID (reliable for DLNA, Chromecast) if not uuid_key and (uuid := identifiers.get(IdentifierType.UUID)): # Normalize UUID: remove special characters, lowercase