CONF_PASSWORD,
CONF_RAOP_CREDENTIALS,
FALLBACK_VOLUME,
+ LEGACY_PAIRING_BIT,
+ PIN_REQUIRED,
RAOP_DISCOVERY_TYPE,
StreamingProtocol,
)
initial_volume: int = FALLBACK_VOLUME,
) -> None:
"""Initialize AirPlayPlayer."""
- super().__init__(provider, player_id)
self.raop_discovery_info = raop_discovery_info
self.airplay_discovery_info = airplay_discovery_info
+ super().__init__(provider, player_id)
self.address = address
self.stream: RaopStream | AirPlay2Stream | None = None
self.last_command_sent = 0.0
return base_entries
+ def _get_flags(self) -> int:
+ # Flags are either present via "sf" or "flags. Taken from pyatv.protocols.airplay.utils"
+ if self.airplay_discovery_info:
+ properties = self.airplay_discovery_info.properties
+ elif self.raop_discovery_info:
+ properties = self.raop_discovery_info.properties
+ else:
+ return 0
+
+ flags = properties.get(b"sf") or properties.get(b"flags") or "0x0"
+ return int(flags, 16)
+
def _requires_pairing(self) -> bool:
- """Check if this device requires pairing (Apple TV or macOS)."""
- if self.device_info.manufacturer.lower() != "apple":
- return False
-
- model = self.device_info.model
- # Apple TV devices
- if "appletv" in model.lower() or "apple tv" in model.lower():
- return True
- # Mac devices (including iMac, MacBook, Mac mini, Mac Pro, Mac Studio)
- return model.startswith(("Mac", "iMac"))
+ """Check if this device requires pairing.
+
+ Adapted from pyatv.protocols.airplay.utils.get_pairing_requirement.
+ """
+ return bool(self._get_flags() & (LEGACY_PAIRING_BIT | PIN_REQUIRED))
def _get_credentials_key(self, protocol: StreamingProtocol) -> str:
"""Get the config key for credentials for given protocol."""
--- /dev/null
+"""Unit tests for AirPlay player."""
+
+from unittest.mock import MagicMock
+
+import pytest
+
+from music_assistant.providers.airplay.player import AirPlayPlayer
+
+
+@pytest.mark.parametrize(
+ ("aiplay_properties", "raop_properties", "expected"),
+ [
+ ({b"flags": b"0x200"}, None, True),
+ ({b"sf": b"0x201"}, None, True),
+ ({b"flags": b"0x4"}, None, False),
+ ({b"sf": b"0x8"}, None, True),
+ ({b"flags": b"0x9"}, None, True),
+ (None, {b"flags": "0x200"}, True),
+ (None, {b"sf": b"0x201"}, True),
+ (None, {b"flags": b"0x4"}, False),
+ (None, {b"sf": b"0x8"}, True),
+ (None, {b"flags": b"0x9"}, True),
+ ({}, {}, False),
+ ],
+)
+def test_requires_pairing(
+ aiplay_properties: dict[bytes, bytes] | None,
+ raop_properties: dict[bytes, bytes] | None,
+ expected: bool,
+) -> None:
+ """Test the _requires_pairing method of AirPlayPlayer."""
+ if aiplay_properties is not None:
+ aiplay_discovery_info = MagicMock()
+ aiplay_discovery_info.properties = aiplay_properties
+ else:
+ aiplay_discovery_info = None
+ if raop_properties is not None:
+ raop_discovery_info = MagicMock()
+ raop_discovery_info.properties = raop_properties
+ else:
+ raop_discovery_info = None
+ player = AirPlayPlayer(
+ provider=MagicMock(),
+ player_id="test_player",
+ display_name="Test Player",
+ address="127.0.0.1",
+ manufacturer="Test Manufacturer",
+ model="Test Model",
+ raop_discovery_info=raop_discovery_info,
+ airplay_discovery_info=aiplay_discovery_info,
+ )
+ assert player._requires_pairing() == expected