Bump aiorussound to 2.0.6 (#122354)

bump aiorussound to 2.0.6
This commit is contained in:
Noah Husby 2024-07-22 02:56:48 -04:00 committed by GitHub
parent db9fc27a5c
commit f30c6e01f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 57 additions and 75 deletions

View file

@ -6,7 +6,7 @@ import asyncio
import logging import logging
from typing import Any from typing import Any
from aiorussound import Russound from aiorussound import Controller, Russound
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
@ -31,13 +31,12 @@ _LOGGER = logging.getLogger(__name__)
def find_primary_controller_metadata( def find_primary_controller_metadata(
controllers: list[tuple[int, str, str]], controllers: dict[int, Controller],
) -> tuple[str, str]: ) -> tuple[str, str]:
"""Find the mac address of the primary Russound controller.""" """Find the mac address of the primary Russound controller."""
for controller_id, mac_address, controller_type in controllers: if 1 in controllers:
# The integration only cares about the primary controller linked by IP and not any downstream controllers c = controllers[1]
if controller_id == 1: return c.mac_address, c.controller_type
return (mac_address, controller_type)
raise NoPrimaryControllerException raise NoPrimaryControllerException

View file

@ -6,5 +6,5 @@
"documentation": "https://www.home-assistant.io/integrations/russound_rio", "documentation": "https://www.home-assistant.io/integrations/russound_rio",
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aiorussound"], "loggers": ["aiorussound"],
"requirements": ["aiorussound==1.1.2"] "requirements": ["aiorussound==2.0.6"]
} }

View file

@ -4,6 +4,8 @@ from __future__ import annotations
import logging import logging
from aiorussound import Source, Zone
from homeassistant.components.media_player import ( from homeassistant.components.media_player import (
MediaPlayerEntity, MediaPlayerEntity,
MediaPlayerEntityFeature, MediaPlayerEntityFeature,
@ -80,21 +82,18 @@ async def async_setup_entry(
"""Set up the Russound RIO platform.""" """Set up the Russound RIO platform."""
russ = entry.runtime_data russ = entry.runtime_data
# Discover sources and zones # Discover controllers
sources = await russ.enumerate_sources() controllers = await russ.enumerate_controllers()
valid_zones = await russ.enumerate_zones()
entities = [] entities = []
for zone_id, name in valid_zones: for controller in controllers:
if zone_id.controller > 6: sources = controller.sources
_LOGGER.debug( for source in sources.values():
"Zone ID %s exceeds RIO controller maximum, skipping", await source.watch()
zone_id.device_str(), for zone in controller.zones.values():
) await zone.watch()
continue mp = RussoundZoneDevice(zone, sources)
await russ.watch_zone(zone_id) entities.append(mp)
zone = RussoundZoneDevice(russ, zone_id, name, sources)
entities.append(zone)
@callback @callback
def on_stop(event): def on_stop(event):
@ -119,56 +118,35 @@ class RussoundZoneDevice(MediaPlayerEntity):
| MediaPlayerEntityFeature.SELECT_SOURCE | 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.""" """Initialize the zone device."""
super().__init__() super().__init__()
self._name = name self._zone = zone
self._russ = russ
self._zone_id = zone_id
self._sources = sources self._sources = sources
def _zone_var(self, name, default=None): def _callback_handler(self, device_str, *args):
return self._russ.get_cached_zone_variable(self._zone_id, name, default) if (
device_str == self._zone.device_str()
def _source_var(self, name, default=None): or device_str == self._current_source().device_str()
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:
self.schedule_update_ha_state() self.schedule_update_ha_state()
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Register callback handlers.""" """Register callback handlers."""
self._russ.add_zone_callback(self._zone_callback_handler) self._zone.add_callback(self._callback_handler)
self._russ.add_source_callback(self._source_callback_handler)
def _current_source(self) -> Source:
return self._zone.fetch_current_source()
@property @property
def name(self): def name(self):
"""Return the name of the zone.""" """Return the name of the zone."""
return self._zone_var("name", self._name) return self._zone.name
@property @property
def state(self) -> MediaPlayerState | None: def state(self) -> MediaPlayerState | None:
"""Return the state of the device.""" """Return the state of the device."""
status = self._zone_var("status", "OFF") status = self._zone.status
if status == "ON": if status == "ON":
return MediaPlayerState.ON return MediaPlayerState.ON
if status == "OFF": if status == "OFF":
@ -178,32 +156,32 @@ class RussoundZoneDevice(MediaPlayerEntity):
@property @property
def source(self): def source(self):
"""Get the currently selected source.""" """Get the currently selected source."""
return self._source_na_var("name") return self._current_source().name
@property @property
def source_list(self): def source_list(self):
"""Return a list of available input sources.""" """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 @property
def media_title(self): def media_title(self):
"""Title of current playing media.""" """Title of current playing media."""
return self._source_na_var("songname") return self._current_source().song_name
@property @property
def media_artist(self): def media_artist(self):
"""Artist of current playing media, music track only.""" """Artist of current playing media, music track only."""
return self._source_na_var("artistname") return self._current_source().artist_name
@property @property
def media_album_name(self): def media_album_name(self):
"""Album name of current playing media, music track only.""" """Album name of current playing media, music track only."""
return self._source_na_var("albumname") return self._current_source().album_name
@property @property
def media_image_url(self): def media_image_url(self):
"""Image url of current playing media.""" """Image url of current playing media."""
return self._source_na_var("coverarturl") return self._current_source().cover_art_url
@property @property
def volume_level(self): def volume_level(self):
@ -212,25 +190,25 @@ class RussoundZoneDevice(MediaPlayerEntity):
Value is returned based on a range (0..50). Value is returned based on a range (0..50).
Therefore float divide by 50 to get to the required range. 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: async def async_turn_off(self) -> None:
"""Turn off the zone.""" """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: async def async_turn_on(self) -> None:
"""Turn on the zone.""" """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: async def async_set_volume_level(self, volume: float) -> None:
"""Set the volume level.""" """Set the volume level."""
rvol = int(volume * 50.0) 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: async def async_select_source(self, source: str) -> None:
"""Select the source input for this zone.""" """Select the source input for this zone."""
for source_id, name in self._sources: for source_id, src in self._sources.items():
if name.lower() != source.lower(): if src.name.lower() != source.lower():
continue continue
await self._russ.send_zone_event(self._zone_id, "SelectSource", source_id) await self._zone.send_event("SelectSource", source_id)
break break

View file

@ -350,7 +350,7 @@ aioridwell==2024.01.0
aioruckus==0.34 aioruckus==0.34
# homeassistant.components.russound_rio # homeassistant.components.russound_rio
aiorussound==1.1.2 aiorussound==2.0.6
# homeassistant.components.ruuvi_gateway # homeassistant.components.ruuvi_gateway
aioruuvigateway==0.1.0 aioruuvigateway==0.1.0

View file

@ -329,7 +329,7 @@ aioridwell==2024.01.0
aioruckus==0.34 aioruckus==0.34
# homeassistant.components.russound_rio # homeassistant.components.russound_rio
aiorussound==1.1.2 aiorussound==2.0.6
# homeassistant.components.ruuvi_gateway # homeassistant.components.ruuvi_gateway
aioruuvigateway==0.1.0 aioruuvigateway==0.1.0

View file

@ -8,7 +8,7 @@ import pytest
from homeassistant.components.russound_rio.const import DOMAIN from homeassistant.components.russound_rio.const import DOMAIN
from homeassistant.core import HomeAssistant 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 from tests.common import MockConfigEntry
@ -44,5 +44,5 @@ def mock_russound() -> Generator[AsyncMock]:
return_value=mock_client, 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 yield mock_client

View file

@ -1,5 +1,7 @@
"""Constants for russound_rio tests.""" """Constants for russound_rio tests."""
from collections import namedtuple
HOST = "127.0.0.1" HOST = "127.0.0.1"
PORT = 9621 PORT = 9621
MODEL = "MCA-C5" MODEL = "MCA-C5"
@ -9,3 +11,6 @@ MOCK_CONFIG = {
"host": HOST, "host": HOST,
"port": PORT, "port": PORT,
} }
_CONTROLLER = namedtuple("Controller", ["mac_address", "controller_type"])
MOCK_CONTROLLERS = {1: _CONTROLLER(mac_address=HARDWARE_MAC, controller_type=MODEL)}

View file

@ -7,7 +7,7 @@ from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType 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( async def test_form(
@ -64,7 +64,7 @@ async def test_no_primary_controller(
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_russound: AsyncMock hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_russound: AsyncMock
) -> None: ) -> None:
"""Test we handle no primary controller error.""" """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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_USER} DOMAIN, context={"source": SOURCE_USER}
) )
@ -79,7 +79,7 @@ async def test_no_primary_controller(
assert result["errors"] == {"base": "no_primary_controller"} assert result["errors"] == {"base": "no_primary_controller"}
# Recover with correct information # 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 = await hass.config_entries.flow.async_configure(
result["flow_id"], result["flow_id"],
MOCK_CONFIG, MOCK_CONFIG,
@ -125,7 +125,7 @@ async def test_import_no_primary_controller(
hass: HomeAssistant, mock_russound: AsyncMock hass: HomeAssistant, mock_russound: AsyncMock
) -> None: ) -> None:
"""Test import with no primary controller error.""" """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( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_CONFIG DOMAIN, context={"source": SOURCE_IMPORT}, data=MOCK_CONFIG