Add sub-device support to Russound RIO (#146763)

This commit is contained in:
Noah Husby 2025-06-20 08:52:34 -04:00 committed by GitHub
parent e28965770e
commit 1b73acc025
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 52 additions and 25 deletions

View File

@ -9,6 +9,8 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from .const import DOMAIN, RUSSOUND_RIO_EXCEPTIONS from .const import DOMAIN, RUSSOUND_RIO_EXCEPTIONS
@ -52,6 +54,39 @@ async def async_setup_entry(hass: HomeAssistant, entry: RussoundConfigEntry) ->
) from err ) from err
entry.runtime_data = client entry.runtime_data = client
device_registry = dr.async_get(hass)
for controller_id, controller in client.controllers.items():
_device_identifier = (
controller.mac_address
or f"{client.controllers[1].mac_address}-{controller_id}"
)
connections = None
via_device = None
configuration_url = None
if controller_id != 1:
assert client.controllers[1].mac_address
via_device = (
DOMAIN,
client.controllers[1].mac_address,
)
else:
assert controller.mac_address
connections = {(CONNECTION_NETWORK_MAC, controller.mac_address)}
if isinstance(client.connection_handler, RussoundTcpConnectionHandler):
configuration_url = f"http://{client.connection_handler.host}"
device_registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, _device_identifier)},
manufacturer="Russound",
name=controller.controller_type,
model=controller.controller_type,
sw_version=controller.firmware_version,
connections=connections,
via_device=via_device,
configuration_url=configuration_url,
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True return True

View File

@ -4,11 +4,11 @@ from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps from functools import wraps
from typing import Any, Concatenate from typing import Any, Concatenate
from aiorussound import Controller, RussoundClient, RussoundTcpConnectionHandler from aiorussound import Controller, RussoundClient
from aiorussound.models import CallbackType from aiorussound.models import CallbackType
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from .const import DOMAIN, RUSSOUND_RIO_EXCEPTIONS from .const import DOMAIN, RUSSOUND_RIO_EXCEPTIONS
@ -46,6 +46,7 @@ class RussoundBaseEntity(Entity):
def __init__( def __init__(
self, self,
controller: Controller, controller: Controller,
zone_id: int | None = None,
) -> None: ) -> None:
"""Initialize the entity.""" """Initialize the entity."""
self._client = controller.client self._client = controller.client
@ -57,29 +58,21 @@ class RussoundBaseEntity(Entity):
self._controller.mac_address self._controller.mac_address
or f"{self._primary_mac_address}-{self._controller.controller_id}" or f"{self._primary_mac_address}-{self._controller.controller_id}"
) )
if not zone_id:
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, self._device_identifier)},
)
return
zone = controller.zones[zone_id]
self._attr_device_info = DeviceInfo( self._attr_device_info = DeviceInfo(
# Use MAC address of Russound device as identifier identifiers={(DOMAIN, f"{self._device_identifier}-{zone_id}")},
identifiers={(DOMAIN, self._device_identifier)}, name=zone.name,
manufacturer="Russound", manufacturer="Russound",
name=controller.controller_type,
model=controller.controller_type, model=controller.controller_type,
sw_version=controller.firmware_version, sw_version=controller.firmware_version,
suggested_area=zone.name,
via_device=(DOMAIN, self._device_identifier),
) )
if isinstance(self._client.connection_handler, RussoundTcpConnectionHandler):
self._attr_device_info["configuration_url"] = (
f"http://{self._client.connection_handler.host}"
)
if controller.controller_id != 1:
assert self._client.controllers[1].mac_address
self._attr_device_info["via_device"] = (
DOMAIN,
self._client.controllers[1].mac_address,
)
else:
assert controller.mac_address
self._attr_device_info["connections"] = {
(CONNECTION_NETWORK_MAC, controller.mac_address)
}
async def _state_update_callback( async def _state_update_callback(
self, _client: RussoundClient, _callback_type: CallbackType self, _client: RussoundClient, _callback_type: CallbackType

View File

@ -60,16 +60,16 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity):
| MediaPlayerEntityFeature.SELECT_SOURCE | MediaPlayerEntityFeature.SELECT_SOURCE
| MediaPlayerEntityFeature.SEEK | MediaPlayerEntityFeature.SEEK
) )
_attr_name = None
def __init__( def __init__(
self, controller: Controller, zone_id: int, sources: dict[int, Source] self, controller: Controller, zone_id: int, sources: dict[int, Source]
) -> None: ) -> None:
"""Initialize the zone device.""" """Initialize the zone device."""
super().__init__(controller) super().__init__(controller, zone_id)
self._zone_id = zone_id self._zone_id = zone_id
_zone = self._zone _zone = self._zone
self._sources = sources self._sources = sources
self._attr_name = _zone.name
self._attr_unique_id = f"{self._primary_mac_address}-{_zone.device_str}" self._attr_unique_id = f"{self._primary_mac_address}-{_zone.device_str}"
@property @property

View File

@ -17,6 +17,5 @@ MOCK_RECONFIGURATION_CONFIG = {
CONF_PORT: 9622, CONF_PORT: 9622,
} }
DEVICE_NAME = "mca_c5"
NAME_ZONE_1 = "backyard" NAME_ZONE_1 = "backyard"
ENTITY_ID_ZONE_1 = f"{MP_DOMAIN}.{DEVICE_NAME}_{NAME_ZONE_1}" ENTITY_ID_ZONE_1 = f"{MP_DOMAIN}.{NAME_ZONE_1}"

View File

@ -207,7 +207,7 @@ async def test_invalid_source_service(
with pytest.raises( with pytest.raises(
HomeAssistantError, HomeAssistantError,
match="Error executing async_select_source on entity media_player.mca_c5_backyard", match="Error executing async_select_source on entity media_player.backyard",
): ):
await hass.services.async_call( await hass.services.async_call(
MP_DOMAIN, MP_DOMAIN,