Skip to content

Commit

Permalink
Support older players with incomplete data (#102)
Browse files Browse the repository at this point in the history
* Support older versions

* Test get system info with unsupported

* Add load players test
  • Loading branch information
andrewsayre authored Feb 10, 2025
1 parent e3f2772 commit 1a2fa30
Show file tree
Hide file tree
Showing 17 changed files with 698 additions and 54 deletions.
8 changes: 6 additions & 2 deletions pyheos/command/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ async def load_players(self) -> PlayerUpdateResult:
for player_data in payload:
player_id = int(player_data[c.ATTR_PLAYER_ID])
name = player_data[c.ATTR_NAME]
version = player_data[c.ATTR_VERSION]
version = player_data.get(c.ATTR_VERSION)
serial = player_data.get(c.ATTR_SERIAL)
# Try matching by serial (if available), then try matching by player_id
# and fallback to matching name when firmware version is different
Expand All @@ -111,7 +111,11 @@ async def load_players(self) -> PlayerUpdateResult:
for player in existing
if (player.serial == serial and serial is not None)
or player.player_id == player_id
or (player.name == name and player.version != version)
or (
player.name == name
and player.version != version
and version is not None
)
),
None,
)
Expand Down
4 changes: 3 additions & 1 deletion pyheos/command/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,5 +134,7 @@ async def get_system_info(self) -> HeosSystem:
response = await self._connection.command(HeosCommand(c.COMMAND_GET_PLAYERS))
payload = cast(Sequence[dict[str, Any]], response.payload)
hosts = list([HeosHost._from_data(item) for item in payload])
host = next(host for host in hosts if host.ip_address == self._options.host)
host = next(
(host for host in hosts if host.ip_address == self._options.host), None
)
return HeosSystem(self._signed_in_username, host, hosts)
44 changes: 44 additions & 0 deletions pyheos/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Define the common module."""

from typing import cast

from pyheos.const import TARGET_VERSION


def is_supported_version(
version: str | None, min_version: str = TARGET_VERSION
) -> bool:
"""Check if a version is supported.
Args:
version (str): The version to check.
min_version (str): The minimum version.
Returns:
bool: True if the version is supported, False otherwise.
"""
if version is None:
return False
try:
sem_ver = get_sem_ver(version)
except ValueError:
return False
min_sem_ver = get_sem_ver(min_version)
for i, ver in enumerate(sem_ver):
if ver < min_sem_ver[i]:
return False
if ver > min_sem_ver[i]:
return True
return True


def get_sem_ver(version: str) -> tuple[int, int, int]:
"""Get the semantic version from a string.
Args:
version (str): The version string.
Returns:
tuple: The semantic version as a tuple.
"""
return cast(tuple[int, int, int], tuple(map(int, version.split(".", 3))))
2 changes: 2 additions & 0 deletions pyheos/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

from typing import Final

TARGET_VERSION: Final = "3.34.0"

DEFAULT_TIMEOUT: Final = 10.0
DEFAULT_RECONNECT_DELAY: Final = 10.0
DEFAULT_RECONNECT_ATTEMPTS: Final = 0 # Unlimited
Expand Down
18 changes: 11 additions & 7 deletions pyheos/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from pyheos.abc import RemoveHeosFieldABC
from pyheos.command import optional_int, parse_enum, parse_optional_enum
from pyheos.common import is_supported_version
from pyheos.dispatch import DisconnectType, EventCallbackType, callback_wrapper
from pyheos.media import MediaItem, QueueItem, ServiceOption
from pyheos.message import HeosMessage
Expand Down Expand Up @@ -192,8 +193,9 @@ class HeosPlayer(RemoveHeosFieldABC):
player_id: int = field(repr=True, hash=True, compare=True)
model: str = field(repr=True, hash=False, compare=False)
serial: str | None = field(repr=False, hash=False, compare=False)
version: str = field(repr=True, hash=False, compare=False)
ip_address: str = field(repr=True, hash=False, compare=False)
version: str | None = field(repr=True, hash=False, compare=False)
supported_version: bool = field(repr=True, hash=False, compare=False)
ip_address: str | None = field(repr=True, hash=False, compare=False)
network: NetworkType = field(repr=False, hash=False, compare=False)
line_out: LineOutLevelType = field(repr=False, hash=False, compare=False)
control: VolumeControlType = field(
Expand All @@ -220,14 +222,15 @@ def _from_data(
heos: Optional["Heos"] = None,
) -> "HeosPlayer":
"""Create a new instance from the provided data."""

version = data.get(c.ATTR_VERSION)
return HeosPlayer(
name=data[c.ATTR_NAME],
player_id=int(data[c.ATTR_PLAYER_ID]),
model=data[c.ATTR_MODEL],
serial=data.get(c.ATTR_SERIAL),
version=data[c.ATTR_VERSION],
ip_address=data[c.ATTR_IP_ADDRESS],
version=version,
supported_version=is_supported_version(version),
ip_address=data.get(c.ATTR_IP_ADDRESS),
network=parse_enum(c.ATTR_NETWORK, data, NetworkType, NetworkType.UNKNOWN),
line_out=parse_enum(
c.ATTR_LINE_OUT, data, LineOutLevelType, LineOutLevelType.UNKNOWN
Expand All @@ -245,8 +248,9 @@ def _update_from_data(self, data: dict[str, Any]) -> None:
self.player_id = int(data[c.ATTR_PLAYER_ID])
self.model = data[c.ATTR_MODEL]
self.serial = data.get(c.ATTR_SERIAL)
self.version = data[c.ATTR_VERSION]
self.ip_address = data[c.ATTR_IP_ADDRESS]
self.version = data.get(c.ATTR_VERSION)
self.supported_version = is_supported_version(self.version)
self.ip_address = data.get(c.ATTR_IP_ADDRESS)
self.network = parse_enum(
c.ATTR_NETWORK, data, NetworkType, NetworkType.UNKNOWN
)
Expand Down
26 changes: 19 additions & 7 deletions pyheos/system.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from typing import Any

from pyheos import command as c
from pyheos.common import is_supported_version
from pyheos.types import NetworkType


Expand All @@ -17,9 +18,10 @@ class HeosHost:
name: str
model: str
serial: str | None
version: str
ip_address: str
version: str | None
ip_address: str | None
network: NetworkType
supported_version: bool

@staticmethod
def _from_data(data: dict[str, Any]) -> "HeosHost":
Expand All @@ -31,13 +33,15 @@ def _from_data(data: dict[str, Any]) -> "HeosHost":
Returns:
HeosHost: The created HeosHost object.
"""
version = data.get(c.ATTR_VERSION)
return HeosHost(
data[c.ATTR_NAME],
data[c.ATTR_MODEL],
data.get(c.ATTR_SERIAL),
data[c.ATTR_VERSION],
data[c.ATTR_IP_ADDRESS],
version,
data.get(c.ATTR_IP_ADDRESS),
c.parse_enum(c.ATTR_NETWORK, data, NetworkType, NetworkType.UNKNOWN),
is_supported_version(version),
)


Expand All @@ -49,7 +53,7 @@ class HeosSystem:
"""

signed_in_username: str | None
host: HeosHost
host: HeosHost | None
hosts: list[HeosHost]
is_signed_in: bool = field(init=False)
preferred_hosts: list[HeosHost] = field(init=False)
Expand All @@ -59,6 +63,14 @@ def __post_init__(self) -> None:
"""Post initialize the system."""
self.is_signed_in = self.signed_in_username is not None
self.preferred_hosts = list(
[host for host in self.hosts if host.network == NetworkType.WIRED]
[
host
for host in self.hosts
if host.network == NetworkType.WIRED
and host.supported_version
and host.ip_address is not None
]
)
self.connected_to_preferred_host = (
self.host is not None and self.host in self.preferred_hosts
)
self.connected_to_preferred_host = self.host in self.preferred_hosts
16 changes: 9 additions & 7 deletions tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ async def wrapped(*args: Any, **kwargs: Any) -> Any:
)

# Register commands
assert_list: list[Callable[..., None]] = []
assert_list: list[CommandMatcher] = []

for command in matched_commands:
# Get the fixture command
Expand All @@ -142,14 +142,15 @@ async def wrapped(*args: Any, **kwargs: Any) -> Any:

# Store item to assert later (so we don't need to keep a copy of the resolved args)
if command.assert_called:
assert_list.append(matcher.assert_called)
assert_list.append(matcher)

# Call the wrapped method
result = await func(*args, **kwargs)

# Assert the commands were called
for callable in assert_list:
callable()
for matcher in assert_list:
if matcher in mock_device._matchers:
matcher.assert_called()

return result

Expand Down Expand Up @@ -514,9 +515,10 @@ async def _get_response(self, response: str, query: dict[str, str]) -> str:

def assert_called(self) -> None:
"""Assert that the command was called."""
assert self.match_count, (
f"Command {self.command} was not called with arguments {self._args}."
)
if self.match_count == 0:
raise AssertionError(
f"Command {self.command} was not called with arguments {self._args}."
)


class ConnectionLog:
Expand Down
6 changes: 4 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ async def player_fixture(heos: MockHeos) -> HeosPlayer:
player_id=1,
model="HEOS Drive",
serial="B1A2C3K",
version="1.493.180",
version="3.34.240",
supported_version=True,
ip_address="127.0.0.1",
network=NetworkType.WIRED,
line_out=LineOutLevelType.FIXED,
Expand All @@ -147,7 +148,8 @@ async def player_front_porch_fixture(heos: MockHeos) -> HeosPlayer:
player_id=2,
model="HEOS Drive",
serial=None,
version="1.493.180",
version="3.34.240",
supported_version=True,
ip_address="127.0.0.2",
network=NetworkType.WIFI,
line_out=LineOutLevelType.FIXED,
Expand Down
4 changes: 2 additions & 2 deletions tests/fixtures/player.get_players.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"pid": 1,
"gid": 2,
"model": "HEOS Drive",
"version": "1.493.180",
"version": "3.34.240",
"ip": "127.0.0.1",
"network": "wired",
"lineout": 2,
Expand All @@ -20,7 +20,7 @@
"pid": 2,
"gid": 2,
"model": "HEOS Drive",
"version": "1.493.180",
"version": "3.34.240",
"ip": "127.0.0.2",
"network": "wifi",
"lineout": 1
Expand Down
4 changes: 2 additions & 2 deletions tests/fixtures/player.get_players_changed.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"name": "Backyard",
"pid": 1,
"model": "HEOS Drive",
"version": "1.493.180",
"version": "3.34.240",
"ip": "192.168.0.1",
"network": "wired",
"lineout": 1,
Expand All @@ -17,7 +17,7 @@
"name": "Basement",
"pid": 3,
"model": "HEOS Drive",
"version": "1.493.180",
"version": "3.34.240",
"ip": "127.0.0.3",
"network": "wifi",
"lineout": 1
Expand Down
4 changes: 2 additions & 2 deletions tests/fixtures/player.get_players_firmware_update.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"name": "Back Patio",
"pid": 101,
"model": "HEOS Drive",
"version": "1.500.000",
"version": "3.34.610",
"ip": "127.0.0.1",
"network": "wired",
"lineout": 1,
Expand All @@ -17,7 +17,7 @@
"name": "Front Porch",
"pid": 102,
"model": "HEOS Drive",
"version": "1.500.000",
"version": "3.34.610",
"ip": "127.0.0.2",
"network": "wifi",
"lineout": 1
Expand Down
4 changes: 2 additions & 2 deletions tests/fixtures/player.get_players_no_groups.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"name": "Back Patio",
"pid": 1,
"model": "HEOS Drive",
"version": "1.493.180",
"version": "3.34.240",
"ip": "127.0.0.1",
"network": "wired",
"lineout": 2,
Expand All @@ -18,7 +18,7 @@
"name": "Front Porch",
"pid": 2,
"model": "HEOS Drive",
"version": "1.493.180",
"version": "3.34.240",
"ip": "127.0.0.2",
"network": "wifi",
"lineout": 1
Expand Down
21 changes: 21 additions & 0 deletions tests/fixtures/player.get_players_unsupported.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"heos": {
"command": "player/get_players",
"result": "success",
"message": ""
},
"payload": [{
"name": "Back Patio",
"pid": 1,
"model": "HEOS Link"
}, {
"name": "Front Porch",
"pid": 2,
"model": "HEOS 5",
"version": "1.583.147",
"ip": "127.0.0.2",
"network": "wifi",
"lineout": 0
}
]
}
Loading

0 comments on commit 1a2fa30

Please sign in to comment.