From f30c6e01f9e7deeb398b1be350014ebce5890982 Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Mon, 22 Jul 2024 02:56:48 -0400 Subject: [PATCH] Bump aiorussound to 2.0.6 (#122354) bump aiorussound to 2.0.6 --- .../components/russound_rio/config_flow.py | 11 +-- .../components/russound_rio/manifest.json | 2 +- .../components/russound_rio/media_player.py | 98 +++++++------------ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/russound_rio/conftest.py | 4 +- tests/components/russound_rio/const.py | 5 + .../russound_rio/test_config_flow.py | 8 +- 8 files changed, 57 insertions(+), 75 deletions(-) diff --git a/homeassistant/components/russound_rio/config_flow.py b/homeassistant/components/russound_rio/config_flow.py index 9ad0d25ff94..e25ac7dde2e 100644 --- a/homeassistant/components/russound_rio/config_flow.py +++ b/homeassistant/components/russound_rio/config_flow.py @@ -6,7 +6,7 @@ import asyncio import logging from typing import Any -from aiorussound import Russound +from aiorussound import Controller, Russound import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -31,13 +31,12 @@ _LOGGER = logging.getLogger(__name__) def find_primary_controller_metadata( - controllers: list[tuple[int, str, str]], + controllers: dict[int, Controller], ) -> tuple[str, str]: """Find the mac address of the primary Russound controller.""" - for controller_id, mac_address, controller_type in controllers: - # The integration only cares about the primary controller linked by IP and not any downstream controllers - if controller_id == 1: - return (mac_address, controller_type) + if 1 in controllers: + c = controllers[1] + return c.mac_address, c.controller_type raise NoPrimaryControllerException diff --git a/homeassistant/components/russound_rio/manifest.json b/homeassistant/components/russound_rio/manifest.json index 43cf8e7850f..7dcdf228244 100644 --- a/homeassistant/components/russound_rio/manifest.json +++ b/homeassistant/components/russound_rio/manifest.json @@ -6,5 +6,5 @@ "documentation": "https://www.home-assistant.io/integrations/russound_rio", "iot_class": "local_push", "loggers": ["aiorussound"], - "requirements": ["aiorussound==1.1.2"] + "requirements": ["aiorussound==2.0.6"] } diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index e3eae51eb9e..a96269ab906 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -4,6 +4,8 @@ from __future__ import annotations import logging +from aiorussound import Source, Zone + from homeassistant.components.media_player import ( MediaPlayerEntity, MediaPlayerEntityFeature, @@ -80,21 +82,18 @@ async def async_setup_entry( """Set up the Russound RIO platform.""" russ = entry.runtime_data - # Discover sources and zones - sources = await russ.enumerate_sources() - valid_zones = await russ.enumerate_zones() + # Discover controllers + controllers = await russ.enumerate_controllers() entities = [] - for zone_id, name in valid_zones: - if zone_id.controller > 6: - _LOGGER.debug( - "Zone ID %s exceeds RIO controller maximum, skipping", - zone_id.device_str(), - ) - continue - await russ.watch_zone(zone_id) - zone = RussoundZoneDevice(russ, zone_id, name, sources) - entities.append(zone) + for controller in controllers: + sources = controller.sources + for source in sources.values(): + await source.watch() + for zone in controller.zones.values(): + await zone.watch() + mp = RussoundZoneDevice(zone, sources) + entities.append(mp) @callback def on_stop(event): @@ -119,56 +118,35 @@ class RussoundZoneDevice(MediaPlayerEntity): | MediaPlayerEntityFeature.SELECT_SOURCE ) - def __init__(self, russ, zone_id, name, sources) -> None: + def __init__(self, zone: Zone, sources: dict[int, Source]) -> None: """Initialize the zone device.""" super().__init__() - self._name = name - self._russ = russ - self._zone_id = zone_id + self._zone = zone self._sources = sources - def _zone_var(self, name, default=None): - return self._russ.get_cached_zone_variable(self._zone_id, name, default) - - def _source_var(self, name, default=None): - current = int(self._zone_var("currentsource", 0)) - if current: - return self._russ.get_cached_source_variable(current, name, default) - return default - - def _source_na_var(self, name): - """Will replace invalid values with None.""" - current = int(self._zone_var("currentsource", 0)) - if current: - value = self._russ.get_cached_source_variable(current, name, None) - if value in (None, "", "------"): - return None - return value - return None - - def _zone_callback_handler(self, zone_id, *args): - if zone_id == self._zone_id: - self.schedule_update_ha_state() - - def _source_callback_handler(self, source_id, *args): - current = int(self._zone_var("currentsource", 0)) - if source_id == current: + def _callback_handler(self, device_str, *args): + if ( + device_str == self._zone.device_str() + or device_str == self._current_source().device_str() + ): self.schedule_update_ha_state() async def async_added_to_hass(self) -> None: """Register callback handlers.""" - self._russ.add_zone_callback(self._zone_callback_handler) - self._russ.add_source_callback(self._source_callback_handler) + self._zone.add_callback(self._callback_handler) + + def _current_source(self) -> Source: + return self._zone.fetch_current_source() @property def name(self): """Return the name of the zone.""" - return self._zone_var("name", self._name) + return self._zone.name @property def state(self) -> MediaPlayerState | None: """Return the state of the device.""" - status = self._zone_var("status", "OFF") + status = self._zone.status if status == "ON": return MediaPlayerState.ON if status == "OFF": @@ -178,32 +156,32 @@ class RussoundZoneDevice(MediaPlayerEntity): @property def source(self): """Get the currently selected source.""" - return self._source_na_var("name") + return self._current_source().name @property def source_list(self): """Return a list of available input sources.""" - return [x[1] for x in self._sources] + return [x.name for x in self._sources.values()] @property def media_title(self): """Title of current playing media.""" - return self._source_na_var("songname") + return self._current_source().song_name @property def media_artist(self): """Artist of current playing media, music track only.""" - return self._source_na_var("artistname") + return self._current_source().artist_name @property def media_album_name(self): """Album name of current playing media, music track only.""" - return self._source_na_var("albumname") + return self._current_source().album_name @property def media_image_url(self): """Image url of current playing media.""" - return self._source_na_var("coverarturl") + return self._current_source().cover_art_url @property def volume_level(self): @@ -212,25 +190,25 @@ class RussoundZoneDevice(MediaPlayerEntity): Value is returned based on a range (0..50). Therefore float divide by 50 to get to the required range. """ - return float(self._zone_var("volume", 0)) / 50.0 + return float(self._zone.volume or "0") / 50.0 async def async_turn_off(self) -> None: """Turn off the zone.""" - await self._russ.send_zone_event(self._zone_id, "ZoneOff") + await self._zone.send_event("ZoneOff") async def async_turn_on(self) -> None: """Turn on the zone.""" - await self._russ.send_zone_event(self._zone_id, "ZoneOn") + await self._zone.send_event("ZoneOn") async def async_set_volume_level(self, volume: float) -> None: """Set the volume level.""" rvol = int(volume * 50.0) - await self._russ.send_zone_event(self._zone_id, "KeyPress", "Volume", rvol) + await self._zone.send_event("KeyPress", "Volume", rvol) async def async_select_source(self, source: str) -> None: """Select the source input for this zone.""" - for source_id, name in self._sources: - if name.lower() != source.lower(): + for source_id, src in self._sources.items(): + if src.name.lower() != source.lower(): continue - await self._russ.send_zone_event(self._zone_id, "SelectSource", source_id) + await self._zone.send_event("SelectSource", source_id) break diff --git a/requirements_all.txt b/requirements_all.txt index 08511d1b191..b348e734c8b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -350,7 +350,7 @@ aioridwell==2024.01.0 aioruckus==0.34 # homeassistant.components.russound_rio -aiorussound==1.1.2 +aiorussound==2.0.6 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a4d8964da6d..0fb54bc922d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -329,7 +329,7 @@ aioridwell==2024.01.0 aioruckus==0.34 # homeassistant.components.russound_rio -aiorussound==1.1.2 +aiorussound==2.0.6 # homeassistant.components.ruuvi_gateway aioruuvigateway==0.1.0 diff --git a/tests/components/russound_rio/conftest.py b/tests/components/russound_rio/conftest.py index 49cb719dfc2..a87d0a74fa8 100644 --- a/tests/components/russound_rio/conftest.py +++ b/tests/components/russound_rio/conftest.py @@ -8,7 +8,7 @@ import pytest from homeassistant.components.russound_rio.const import DOMAIN from homeassistant.core import HomeAssistant -from .const import HARDWARE_MAC, MOCK_CONFIG, MODEL +from .const import HARDWARE_MAC, MOCK_CONFIG, MOCK_CONTROLLERS, MODEL from tests.common import MockConfigEntry @@ -44,5 +44,5 @@ def mock_russound() -> Generator[AsyncMock]: return_value=mock_client, ), ): - mock_client.enumerate_controllers.return_value = [(1, HARDWARE_MAC, MODEL)] + mock_client.enumerate_controllers.return_value = MOCK_CONTROLLERS yield mock_client diff --git a/tests/components/russound_rio/const.py b/tests/components/russound_rio/const.py index 92aed6494d9..d1f6aa7eead 100644 --- a/tests/components/russound_rio/const.py +++ b/tests/components/russound_rio/const.py @@ -1,5 +1,7 @@ """Constants for russound_rio tests.""" +from collections import namedtuple + HOST = "127.0.0.1" PORT = 9621 MODEL = "MCA-C5" @@ -9,3 +11,6 @@ MOCK_CONFIG = { "host": HOST, "port": PORT, } + +_CONTROLLER = namedtuple("Controller", ["mac_address", "controller_type"]) +MOCK_CONTROLLERS = {1: _CONTROLLER(mac_address=HARDWARE_MAC, controller_type=MODEL)} diff --git a/tests/components/russound_rio/test_config_flow.py b/tests/components/russound_rio/test_config_flow.py index 195e4af9b11..8bc7bd738a1 100644 --- a/tests/components/russound_rio/test_config_flow.py +++ b/tests/components/russound_rio/test_config_flow.py @@ -7,7 +7,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from .const import HARDWARE_MAC, MOCK_CONFIG, MODEL +from .const import MOCK_CONFIG, MOCK_CONTROLLERS, MODEL async def test_form( @@ -64,7 +64,7 @@ async def test_no_primary_controller( hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_russound: AsyncMock ) -> None: """Test we handle no primary controller error.""" - mock_russound.enumerate_controllers.return_value = [] + mock_russound.enumerate_controllers.return_value = {} result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -79,7 +79,7 @@ async def test_no_primary_controller( assert result["errors"] == {"base": "no_primary_controller"} # Recover with correct information - mock_russound.enumerate_controllers.return_value = [(1, HARDWARE_MAC, MODEL)] + mock_russound.enumerate_controllers.return_value = MOCK_CONTROLLERS result = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_CONFIG, @@ -125,7 +125,7 @@ async def test_import_no_primary_controller( hass: HomeAssistant, mock_russound: AsyncMock ) -> None: """Test import with no primary controller error.""" - mock_russound.enumerate_controllers.return_value = [] + mock_russound.enumerate_controllers.return_value = {} result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_CONFIG