Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for favorites #37

Open
wants to merge 26 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
de3404d
Add support for favorites and json response type needed for Russound …
Sep 22, 2024
2ffa647
Delete .vs directory
archiconda1976 Sep 23, 2024
470f9af
Update models.py
archiconda1976 Sep 23, 2024
7bc7672
Update connection.py
archiconda1976 Sep 23, 2024
a0e75a3
Update rio.py
archiconda1976 Sep 23, 2024
b894ed2
Update rio.py
archiconda1976 Sep 23, 2024
d39612b
Update rio.py
archiconda1976 Sep 23, 2024
d3b95d6
Update connection.py
archiconda1976 Sep 23, 2024
b980176
Resolve PR comments
Sep 23, 2024
1b6e3af
Merge
Sep 23, 2024
eb6bfef
Update connection.py
archiconda1976 Sep 23, 2024
a20294d
Moved definition of enemerate_zone_favorites to Zone class. Updated …
Sep 24, 2024
7460c20
Merge branch 'master' of https://github.com/archiconda1976/aiorussound
Sep 24, 2024
fd6270b
Merge branch 'RebaseToParent' into Feature-Favorites
archiconda1976 Sep 25, 2024
10ac12d
Merge pull request #1 from archiconda1976/Feature-Favorites
archiconda1976 Sep 25, 2024
d58cfd6
Fix missing ')' on import
archiconda1976 Sep 25, 2024
2d53545
Update rio.py
archiconda1976 Sep 26, 2024
f68a588
Merge pull request #2 from archiconda1976/RebaseToParent
archiconda1976 Sep 26, 2024
50b6a80
Merge branch 'noahhusby:master' into master
archiconda1976 Sep 26, 2024
847d6ce
Clean-up misc. whitespace deltas
archiconda1976 Sep 26, 2024
71f444d
Clean-up misc. whitespace deltas
archiconda1976 Sep 26, 2024
49862ba
Clean-up misc. whitespace deltas
archiconda1976 Sep 26, 2024
2aa49d1
Clean-up merge issue with imports
archiconda1976 Sep 26, 2024
4490bdb
Clean-up misc. whitespace deltas
archiconda1976 Sep 26, 2024
070a719
Update from running pre-commit
Sep 26, 2024
a47c144
Added methods to save,delete and restore zone and system favorites
Oct 3, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions aiorussound/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,17 @@ def _process_response(res: bytes) -> Optional[RussoundMessage]:
if tag == "E":
_LOGGER.debug("Device responded with error: %s", payload)
raise CommandError(payload)

m = RESPONSE_REGEX.match(payload.strip())

if not m:
return RussoundMessage(tag, None, None, None, None, None)

p = m.groupdict()
value = p["value"] or p["value_only"]
return RussoundMessage(tag, p["variable"], value, p["zone"], p["controller"], p["source"])
variable = p["variable"] or p["variable_only"]
noahhusby marked this conversation as resolved.
Show resolved Hide resolved

return RussoundMessage(tag, variable, value, p["zone"], p["controller"], p["source"])


class RussoundConnectionHandler:
Expand Down Expand Up @@ -129,7 +134,7 @@ async def close(self):
self._set_connected(False)

async def _ioloop(
self, reader: StreamReader, writer: StreamWriter, reconnect: bool
self, reader: StreamReader, writer: StreamWriter, reconnect: bool
) -> None:
queue_future = ensure_future(self._cmd_queue.get())
net_future = ensure_future(reader.readline())
Expand Down
16 changes: 15 additions & 1 deletion aiorussound/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

MAX_SOURCE = 17

SYSTEM_VARIABLES = "System"
noahhusby marked this conversation as resolved.
Show resolved Hide resolved

RESPONSE_REGEX = re.compile(
r"^(?:C\[(?P<controller>\d+)](?:\.Z\[(?P<zone>\d+)])?|S\[(?P<source>\d+)])?\."
r"(?P<variable>\S+)=\s*\"(?P<value>.*)\"$|^(?P<variable_only>\S+)=\s*\"(?P<value_only>.*)\"$"
Expand Down Expand Up @@ -154,7 +156,19 @@ class FeatureFlag(Enum):

VERSIONS_BY_FLAGS = defaultdict(list)

ZONE_PROPERTIES: list[str] = ["currentSource"]
ZONE_PROPERTIES: list[str] = [
"currentSource",
"favorite[1].valid",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we loading these values into the cache on connection? The enumerate command will typically fetch these values directly instead of grabbing them from the cache.

How we handle this is completely up for debate, just trying to understand it better.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No compelling reason other than it would happen during startup regardless. Since this is dependent on controller type, it doesn't make sense to pre-fetch these.

"favorite[1].name",
"favorite[1].providerMode",
"favorite[1].albumCoverURL",
"favorite[1].source",
"favorite[2].valid",
"favorite[2].name",
"favorite[2].providerMode",
"favorite[2].albumCoverURL",
"favorite[2].source",
]

SOURCE_PROPERTIES: list[str] = []

Expand Down
14 changes: 13 additions & 1 deletion aiorussound/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,18 @@
from mashumaro.mixins.orjson import DataClassORJSONMixin


@dataclass
class RussoundFavorite:
noahhusby marked this conversation as resolved.
Show resolved Hide resolved
"""Russound Favorite."""

favorite_id: int
issystemfavorite: bool
noahhusby marked this conversation as resolved.
Show resolved Hide resolved
name: str
providermode: str
albumcoverurl: str
source_id: int


@dataclass
class RussoundMessage:
"""Incoming russound message."""
Expand Down Expand Up @@ -63,4 +75,4 @@ class SourceProperties(DataClassORJSONMixin):
bit_rate: str = field(metadata=field_options(alias="bitrate"), default=None)
bit_depth: str = field(metadata=field_options(alias="bitdepth"), default=None)
play_time: str = field(metadata=field_options(alias="playtime"), default=None)
track_time: str = field(metadata=field_options(alias="tracktime"), default=None)
track_time: str = field(metadata=field_options(alias="tracktime"), default=None)
130 changes: 110 additions & 20 deletions aiorussound/rio.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@
SOURCE_PROPERTIES,
ZONE_PROPERTIES,
FeatureFlag,
SYSTEM_VARIABLES,
)
from aiorussound.exceptions import (
CommandError,
UncachedVariableError,
UnsupportedFeatureError,
)
from aiorussound.models import RussoundMessage, ZoneProperties, SourceProperties
from aiorussound.models import RussoundMessage, ZoneProperties, SourceProperties, RussoundFavorite
from aiorussound.util import (
controller_device_str,
get_max_zones,
Expand All @@ -36,7 +37,7 @@ class RussoundClient:
"""Manages the RIO connection to a Russound device."""

def __init__(
self, connection_handler: RussoundConnectionHandler
self, connection_handler: RussoundConnectionHandler
) -> None:
"""Initialize the Russound object using the event loop, host and port
provided.
Expand Down Expand Up @@ -94,6 +95,8 @@ def _on_msg_recv(self, msg: RussoundMessage) -> None:
self._store_cached_variable(
zone_device_str(controller_id, zone_id), msg.variable, msg.value
)
elif msg.variable:
self._store_cached_variable(SYSTEM_VARIABLES, msg.variable, msg.value)

def add_callback(self, device_str: str, callback) -> None:
"""Register a callback to be called whenever a device variable changes.
Expand Down Expand Up @@ -126,7 +129,7 @@ async def close(self) -> None:
await self.connection_handler.close()

async def set_variable(
self, device_str: str, key: str, value: str
self, device_str: str, key: str, value: str
) -> Coroutine[Any, Any, str]:
"""Set a zone variable to a new value."""
return self.connection_handler.send(f'SET {device_str}.{key}="{value}"')
Expand Down Expand Up @@ -173,7 +176,7 @@ async def enumerate_controllers(self) -> dict[int, Controller]:
pass
firmware_version = None
if is_feature_supported(
self.rio_version, FeatureFlag.PROPERTY_FIRMWARE_VERSION
self.rio_version, FeatureFlag.PROPERTY_FIRMWARE_VERSION
):
firmware_version = await self.get_variable(
device_str, "firmwareVersion"
Expand Down Expand Up @@ -232,18 +235,105 @@ async def init_sources(self) -> None:
except CommandError:
break

async def enumerate_zone_favorites(self, zone: Zone) -> list[RussoundFavorite]:
"""Return a list of RussoundFavorite for this zone."""
favorites = []

for favorite_id in range(1, 2):
noahhusby marked this conversation as resolved.
Show resolved Hide resolved
try:
valid = await self.get_variable(
zone.device_str(), f"favorite[{favorite_id}].valid"
)
if valid == "TRUE":
try:
name = await self.get_variable(
zone.device_str(), f"favorite[{favorite_id}].name"
)
providerMode = await self.get_variable(
zone.device_str(), f"favorite[{favorite_id}].providerMode"
)
albumCoverURL = await self.get_variable(
zone.device_str(), f"favorite[{favorite_id}].albumCoverURL"
)
source_id = await self.get_variable(
zone.device_str(), f"favorite[{favorite_id}].source"
)

favorites.append(
RussoundFavorite(
favorite_id,
False,
name,
providerMode,
albumCoverURL,
source_id,
)
)
except CommandError:
break
except CommandError:
continue
return favorites

async def enumerate_system_favorites(self) -> list[RussoundFavorite]:
"""Return a list of RussoundFavorite for this system."""
favorites = []

for favorite_id in range(1, 32):
noahhusby marked this conversation as resolved.
Show resolved Hide resolved
try:
valid = await self._get_system_favorite_variable(favorite_id, "valid")
if valid == "TRUE":
try:
name = await self._get_system_favorite_variable(
favorite_id, "name"
)
providerMode = await self._get_system_favorite_variable(
favorite_id, "providerMode"
)
albumCoverURL = await self._get_system_favorite_variable(
favorite_id, "albumCoverURL"
)
source_id = await self._get_system_favorite_variable(
favorite_id, "source"
)

favorites.append(
RussoundFavorite(
favorite_id,
True,
name,
providerMode,
albumCoverURL,
source_id,
)
)
except CommandError:
break
except CommandError:
continue
return favorites

async def _get_system_favorite_variable(self, favorite_id, variable) -> str:
"""Return a system favorite variable."""
try:
return await self.get_variable(
SYSTEM_VARIABLES, f"favorite[{favorite_id}].{variable}"
)
except UncachedVariableError:
return "False"


class Controller:
"""Uniquely identifies a controller."""

def __init__(
self,
instance: RussoundClient,
parent_controller: Controller,
controller_id: int,
mac_address: str,
controller_type: str,
firmware_version: str,
self,
instance: RussoundClient,
parent_controller: Controller,
controller_id: int,
mac_address: str,
controller_type: str,
firmware_version: str,
) -> None:
"""Initialize the controller."""
self.instance = instance
Expand All @@ -266,8 +356,8 @@ def __str__(self) -> str:
def __eq__(self, other: object) -> bool:
"""Equality check."""
return (
hasattr(other, "controller_id")
and other.controller_id == self.controller_id
hasattr(other, "controller_id")
and other.controller_id == self.controller_id
)

def __hash__(self) -> int:
Expand Down Expand Up @@ -307,7 +397,7 @@ class Zone:
"""

def __init__(
self, instance: RussoundClient, controller: Controller, zone_id: int, name: str
self, instance: RussoundClient, controller: Controller, zone_id: int, name: str
) -> None:
"""Initialize a zone object."""
self.instance = instance
Expand All @@ -330,10 +420,10 @@ def __str__(self) -> str:
def __eq__(self, other: object) -> bool:
"""Equality check."""
return (
hasattr(other, "zone_id")
and hasattr(other, "controller")
and other.zone_id == self.zone_id
and other.controller == self.controller
hasattr(other, "zone_id")
and hasattr(other, "controller")
and other.zone_id == self.zone_id
and other.controller == self.controller
)

def __hash__(self) -> int:
Expand Down Expand Up @@ -440,7 +530,7 @@ class Source:
"""Uniquely identifies a Source."""

def __init__(
self, instance: RussoundClient, source_id: int, name: str
self, instance: RussoundClient, source_id: int, name: str
) -> None:
"""Initialize a Source."""
self.instance = instance
Expand All @@ -466,7 +556,7 @@ def __eq__(self, other: object) -> bool:
and other.source_id == self.source_id
)

def __hash__(self) -> int:
def __hash__(self) -> int:
"""Hash the current configuration of the source."""
return hash(str(self))

Expand Down