parent
db9fc27a5c
commit
f30c6e01f9
8 changed files with 57 additions and 75 deletions
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue