From a7290f92cfd29c43b6d81ae0ccbc782667b81b4b Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Sun, 22 Jun 2025 18:02:16 -0400 Subject: [PATCH] Add number entity to Russound RIO (#147228) * Add number entity to Russound RIO * Fixes * Fix tests * Change entity name --- .../components/russound_rio/__init__.py | 2 +- .../components/russound_rio/entity.py | 7 + .../components/russound_rio/media_player.py | 6 - .../components/russound_rio/number.py | 112 +++ .../components/russound_rio/strings.json | 16 + tests/components/russound_rio/conftest.py | 4 + .../russound_rio/snapshots/test_number.ambr | 913 ++++++++++++++++++ tests/components/russound_rio/test_number.py | 70 ++ 8 files changed, 1123 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/russound_rio/number.py create mode 100644 tests/components/russound_rio/snapshots/test_number.ambr create mode 100644 tests/components/russound_rio/test_number.py diff --git a/homeassistant/components/russound_rio/__init__.py b/homeassistant/components/russound_rio/__init__.py index f35a476bbb3..51ca9a9b1ea 100644 --- a/homeassistant/components/russound_rio/__init__.py +++ b/homeassistant/components/russound_rio/__init__.py @@ -14,7 +14,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from .const import DOMAIN, RUSSOUND_RIO_EXCEPTIONS -PLATFORMS = [Platform.MEDIA_PLAYER] +PLATFORMS = [Platform.MEDIA_PLAYER, Platform.NUMBER] _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/russound_rio/entity.py b/homeassistant/components/russound_rio/entity.py index d7b4e412831..1fe6a7876d1 100644 --- a/homeassistant/components/russound_rio/entity.py +++ b/homeassistant/components/russound_rio/entity.py @@ -6,6 +6,7 @@ from typing import Any, Concatenate from aiorussound import Controller, RussoundClient from aiorussound.models import CallbackType +from aiorussound.rio import ZoneControlSurface from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo @@ -58,6 +59,7 @@ class RussoundBaseEntity(Entity): self._controller.mac_address or f"{self._primary_mac_address}-{self._controller.controller_id}" ) + self._zone_id = zone_id if not zone_id: self._attr_device_info = DeviceInfo( identifiers={(DOMAIN, self._device_identifier)}, @@ -74,6 +76,11 @@ class RussoundBaseEntity(Entity): via_device=(DOMAIN, self._device_identifier), ) + @property + def _zone(self) -> ZoneControlSurface: + assert self._zone_id + return self._controller.zones[self._zone_id] + async def _state_update_callback( self, _client: RussoundClient, _callback_type: CallbackType ) -> None: diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index 7dbc3ae34be..aaaad05a2bc 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -9,7 +9,6 @@ from typing import TYPE_CHECKING from aiorussound import Controller from aiorussound.const import FeatureFlag from aiorussound.models import PlayStatus, Source -from aiorussound.rio import ZoneControlSurface from aiorussound.util import is_feature_supported from homeassistant.components.media_player import ( @@ -67,15 +66,10 @@ class RussoundZoneDevice(RussoundBaseEntity, MediaPlayerEntity): ) -> None: """Initialize the zone device.""" super().__init__(controller, zone_id) - self._zone_id = zone_id _zone = self._zone self._sources = sources self._attr_unique_id = f"{self._primary_mac_address}-{_zone.device_str}" - @property - def _zone(self) -> ZoneControlSurface: - return self._controller.zones[self._zone_id] - @property def _source(self) -> Source: return self._zone.fetch_current_source() diff --git a/homeassistant/components/russound_rio/number.py b/homeassistant/components/russound_rio/number.py new file mode 100644 index 00000000000..ae13815fa0a --- /dev/null +++ b/homeassistant/components/russound_rio/number.py @@ -0,0 +1,112 @@ +"""Support for Russound number entities.""" + +from collections.abc import Awaitable, Callable +from dataclasses import dataclass + +from aiorussound.rio import Controller, ZoneControlSurface + +from homeassistant.components.number import NumberEntity, NumberEntityDescription +from homeassistant.const import EntityCategory +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from . import RussoundConfigEntry +from .entity import RussoundBaseEntity, command + +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class RussoundZoneNumberEntityDescription(NumberEntityDescription): + """Describes Russound number entities.""" + + value_fn: Callable[[ZoneControlSurface], float] + set_value_fn: Callable[[ZoneControlSurface, float], Awaitable[None]] + + +CONTROL_ENTITIES: tuple[RussoundZoneNumberEntityDescription, ...] = ( + RussoundZoneNumberEntityDescription( + key="balance", + translation_key="balance", + native_min_value=-10, + native_max_value=10, + native_step=1, + entity_category=EntityCategory.CONFIG, + value_fn=lambda zone: zone.balance, + set_value_fn=lambda zone, value: zone.set_balance(int(value)), + ), + RussoundZoneNumberEntityDescription( + key="bass", + translation_key="bass", + native_min_value=-10, + native_max_value=10, + native_step=1, + entity_category=EntityCategory.CONFIG, + value_fn=lambda zone: zone.bass, + set_value_fn=lambda zone, value: zone.set_bass(int(value)), + ), + RussoundZoneNumberEntityDescription( + key="treble", + translation_key="treble", + native_min_value=-10, + native_max_value=10, + native_step=1, + entity_category=EntityCategory.CONFIG, + value_fn=lambda zone: zone.treble, + set_value_fn=lambda zone, value: zone.set_treble(int(value)), + ), + RussoundZoneNumberEntityDescription( + key="turn_on_volume", + translation_key="turn_on_volume", + native_min_value=0, + native_max_value=100, + native_step=2, + entity_category=EntityCategory.CONFIG, + value_fn=lambda zone: zone.turn_on_volume * 2, + set_value_fn=lambda zone, value: zone.set_turn_on_volume(int(value / 2)), + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: RussoundConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Russound number entities based on a config entry.""" + client = entry.runtime_data + async_add_entities( + RussoundNumberEntity(controller, zone_id, description) + for controller in client.controllers.values() + for zone_id in controller.zones + for description in CONTROL_ENTITIES + ) + + +class RussoundNumberEntity(RussoundBaseEntity, NumberEntity): + """Defines a Russound number entity.""" + + entity_description: RussoundZoneNumberEntityDescription + + def __init__( + self, + controller: Controller, + zone_id: int, + description: RussoundZoneNumberEntityDescription, + ) -> None: + """Initialize a Russound number entity.""" + super().__init__(controller, zone_id) + self.entity_description = description + self._attr_unique_id = ( + f"{self._primary_mac_address}-{self._zone.device_str}-{description.key}" + ) + + @property + def native_value(self) -> float: + """Return the native value of the entity.""" + return float(self.entity_description.value_fn(self._zone)) + + @command + async def async_set_native_value(self, value: float) -> None: + """Set the value.""" + await self.entity_description.set_value_fn(self._zone, value) diff --git a/homeassistant/components/russound_rio/strings.json b/homeassistant/components/russound_rio/strings.json index eba66856302..37f78bfa75d 100644 --- a/homeassistant/components/russound_rio/strings.json +++ b/homeassistant/components/russound_rio/strings.json @@ -40,6 +40,22 @@ "wrong_device": "This Russound controller does not match the existing device ID. Please make sure you entered the correct IP address." } }, + "entity": { + "number": { + "balance": { + "name": "Balance" + }, + "bass": { + "name": "Bass" + }, + "treble": { + "name": "Treble" + }, + "turn_on_volume": { + "name": "Turn-on volume" + } + } + }, "exceptions": { "entry_cannot_connect": { "message": "Error while connecting to {host}:{port}" diff --git a/tests/components/russound_rio/conftest.py b/tests/components/russound_rio/conftest.py index 2516bd81650..5e57c45193b 100644 --- a/tests/components/russound_rio/conftest.py +++ b/tests/components/russound_rio/conftest.py @@ -79,6 +79,10 @@ def mock_russound_client() -> Generator[AsyncMock]: zone.unmute = AsyncMock() zone.toggle_mute = AsyncMock() zone.set_seek_time = AsyncMock() + zone.set_balance = AsyncMock() + zone.set_bass = AsyncMock() + zone.set_treble = AsyncMock() + zone.set_turn_on_volume = AsyncMock() client.controllers = { 1: Controller( diff --git a/tests/components/russound_rio/snapshots/test_number.ambr b/tests/components/russound_rio/snapshots/test_number.ambr new file mode 100644 index 00000000000..f1b806a378a --- /dev/null +++ b/tests/components/russound_rio/snapshots/test_number.ambr @@ -0,0 +1,913 @@ +# serializer version: 1 +# name: test_all_entities[number.backyard_balance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.backyard_balance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Balance', + 'platform': 'russound_rio', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'balance', + 'unique_id': '00:11:22:33:44:55-C[1].Z[1]-balance', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[number.backyard_balance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Backyard Balance', + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.backyard_balance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[number.backyard_bass-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.backyard_bass', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Bass', + 'platform': 'russound_rio', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'bass', + 'unique_id': '00:11:22:33:44:55-C[1].Z[1]-bass', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[number.backyard_bass-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Backyard Bass', + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.backyard_bass', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[number.backyard_treble-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.backyard_treble', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Treble', + 'platform': 'russound_rio', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'treble', + 'unique_id': '00:11:22:33:44:55-C[1].Z[1]-treble', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[number.backyard_treble-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Backyard Treble', + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.backyard_treble', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[number.backyard_turn_on_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100, + 'min': 0, + 'mode': , + 'step': 2, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.backyard_turn_on_volume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Turn-on volume', + 'platform': 'russound_rio', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'turn_on_volume', + 'unique_id': '00:11:22:33:44:55-C[1].Z[1]-turn_on_volume', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[number.backyard_turn_on_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Backyard Turn-on volume', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 2, + }), + 'context': , + 'entity_id': 'number.backyard_turn_on_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.0', + }) +# --- +# name: test_all_entities[number.bedroom_balance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.bedroom_balance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Balance', + 'platform': 'russound_rio', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'balance', + 'unique_id': '00:11:22:33:44:55-C[1].Z[3]-balance', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[number.bedroom_balance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bedroom Balance', + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.bedroom_balance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[number.bedroom_bass-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.bedroom_bass', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Bass', + 'platform': 'russound_rio', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'bass', + 'unique_id': '00:11:22:33:44:55-C[1].Z[3]-bass', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[number.bedroom_bass-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bedroom Bass', + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.bedroom_bass', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[number.bedroom_treble-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.bedroom_treble', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Treble', + 'platform': 'russound_rio', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'treble', + 'unique_id': '00:11:22:33:44:55-C[1].Z[3]-treble', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[number.bedroom_treble-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bedroom Treble', + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.bedroom_treble', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[number.bedroom_turn_on_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100, + 'min': 0, + 'mode': , + 'step': 2, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.bedroom_turn_on_volume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Turn-on volume', + 'platform': 'russound_rio', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'turn_on_volume', + 'unique_id': '00:11:22:33:44:55-C[1].Z[3]-turn_on_volume', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[number.bedroom_turn_on_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Bedroom Turn-on volume', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 2, + }), + 'context': , + 'entity_id': 'number.bedroom_turn_on_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.0', + }) +# --- +# name: test_all_entities[number.kitchen_balance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.kitchen_balance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Balance', + 'platform': 'russound_rio', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'balance', + 'unique_id': '00:11:22:33:44:55-C[1].Z[2]-balance', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[number.kitchen_balance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Kitchen Balance', + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.kitchen_balance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[number.kitchen_bass-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.kitchen_bass', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Bass', + 'platform': 'russound_rio', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'bass', + 'unique_id': '00:11:22:33:44:55-C[1].Z[2]-bass', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[number.kitchen_bass-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Kitchen Bass', + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.kitchen_bass', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[number.kitchen_treble-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.kitchen_treble', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Treble', + 'platform': 'russound_rio', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'treble', + 'unique_id': '00:11:22:33:44:55-C[1].Z[2]-treble', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[number.kitchen_treble-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Kitchen Treble', + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.kitchen_treble', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[number.kitchen_turn_on_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100, + 'min': 0, + 'mode': , + 'step': 2, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.kitchen_turn_on_volume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Turn-on volume', + 'platform': 'russound_rio', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'turn_on_volume', + 'unique_id': '00:11:22:33:44:55-C[1].Z[2]-turn_on_volume', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[number.kitchen_turn_on_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Kitchen Turn-on volume', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 2, + }), + 'context': , + 'entity_id': 'number.kitchen_turn_on_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.0', + }) +# --- +# name: test_all_entities[number.living_room_balance-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.living_room_balance', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Balance', + 'platform': 'russound_rio', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'balance', + 'unique_id': '00:11:22:33:44:55-C[2].Z[9]-balance', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[number.living_room_balance-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Living Room Balance', + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.living_room_balance', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[number.living_room_bass-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.living_room_bass', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Bass', + 'platform': 'russound_rio', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'bass', + 'unique_id': '00:11:22:33:44:55-C[2].Z[9]-bass', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[number.living_room_bass-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Living Room Bass', + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.living_room_bass', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[number.living_room_treble-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.living_room_treble', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Treble', + 'platform': 'russound_rio', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'treble', + 'unique_id': '00:11:22:33:44:55-C[2].Z[9]-treble', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[number.living_room_treble-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Living Room Treble', + 'max': 10, + 'min': -10, + 'mode': , + 'step': 1, + }), + 'context': , + 'entity_id': 'number.living_room_treble', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '0.0', + }) +# --- +# name: test_all_entities[number.living_room_turn_on_volume-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 100, + 'min': 0, + 'mode': , + 'step': 2, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': , + 'entity_id': 'number.living_room_turn_on_volume', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Turn-on volume', + 'platform': 'russound_rio', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'turn_on_volume', + 'unique_id': '00:11:22:33:44:55-C[2].Z[9]-turn_on_volume', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[number.living_room_turn_on_volume-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Living Room Turn-on volume', + 'max': 100, + 'min': 0, + 'mode': , + 'step': 2, + }), + 'context': , + 'entity_id': 'number.living_room_turn_on_volume', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '40.0', + }) +# --- diff --git a/tests/components/russound_rio/test_number.py b/tests/components/russound_rio/test_number.py new file mode 100644 index 00000000000..ff2c46fb4e1 --- /dev/null +++ b/tests/components/russound_rio/test_number.py @@ -0,0 +1,70 @@ +"""Tests for the Russound RIO number platform.""" + +from unittest.mock import AsyncMock, patch + +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration +from .const import NAME_ZONE_1 + +from tests.common import MockConfigEntry, snapshot_platform + + +@pytest.mark.usefixtures("entity_registry_enabled_by_default") +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_russound_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.russound_rio.PLATFORMS", [Platform.NUMBER]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + ("entity_suffix", "value", "expected_method", "expected_arg"), + [ + ("bass", -5, "set_bass", -5), + ("balance", 3, "set_balance", 3), + ("treble", 7, "set_treble", 7), + ("turn_on_volume", 60, "set_turn_on_volume", 30), + ], +) +async def test_setting_number_value( + hass: HomeAssistant, + mock_russound_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_suffix: str, + value: int, + expected_method: str, + expected_arg: int, +) -> None: + """Test setting value on Russound number entity.""" + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: f"{NUMBER_DOMAIN}.{NAME_ZONE_1}_{entity_suffix}", + ATTR_VALUE: value, + }, + blocking=True, + ) + + zone = mock_russound_client.controllers[1].zones[1] + getattr(zone, expected_method).assert_called_once_with(expected_arg)