Add switch entity to Russound RIO (#147323)

* Add switch entity to Russound RIO

* Add switch snapshot
This commit is contained in:
Noah Husby 2025-06-23 04:42:36 -04:00 committed by GitHub
parent 69d2cd0ac0
commit 35f310748e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 361 additions and 1 deletions

View File

@ -14,7 +14,7 @@ from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC
from .const import DOMAIN, RUSSOUND_RIO_EXCEPTIONS
PLATFORMS = [Platform.MEDIA_PLAYER, Platform.NUMBER]
PLATFORMS = [Platform.MEDIA_PLAYER, Platform.NUMBER, Platform.SWITCH]
_LOGGER = logging.getLogger(__name__)

View File

@ -0,0 +1,12 @@
{
"entity": {
"switch": {
"loudness": {
"default": "mdi:volume-high",
"state": {
"off": "mdi:volume-low"
}
}
}
}
}

View File

@ -54,6 +54,11 @@
"turn_on_volume": {
"name": "Turn-on volume"
}
},
"switch": {
"loudness": {
"name": "Loudness"
}
}
},
"exceptions": {

View File

@ -0,0 +1,85 @@
"""Support for Russound RIO switch entities."""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
from aiorussound.rio import Controller, ZoneControlSurface
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
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 RussoundZoneSwitchEntityDescription(SwitchEntityDescription):
"""Describes Russound RIO switch entity description."""
value_fn: Callable[[ZoneControlSurface], bool]
set_value_fn: Callable[[ZoneControlSurface, bool], Awaitable[None]]
CONTROL_ENTITIES: tuple[RussoundZoneSwitchEntityDescription, ...] = (
RussoundZoneSwitchEntityDescription(
key="loudness",
translation_key="loudness",
entity_category=EntityCategory.CONFIG,
value_fn=lambda zone: zone.loudness,
set_value_fn=lambda zone, value: zone.set_loudness(value),
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: RussoundConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Russound RIO switch entities based on a config entry."""
client = entry.runtime_data
async_add_entities(
RussoundSwitchEntity(controller, zone_id, description)
for controller in client.controllers.values()
for zone_id in controller.zones
for description in CONTROL_ENTITIES
)
class RussoundSwitchEntity(RussoundBaseEntity, SwitchEntity):
"""Defines a Russound RIO switch entity."""
entity_description: RussoundZoneSwitchEntityDescription
def __init__(
self,
controller: Controller,
zone_id: int,
description: RussoundZoneSwitchEntityDescription,
) -> None:
"""Initialize Russound RIO switch."""
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 is_on(self) -> bool:
"""Return the state of the switch."""
return self.entity_description.value_fn(self._zone)
@command
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
await self.entity_description.set_value_fn(self._zone, True)
@command
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
await self.entity_description.set_value_fn(self._zone, False)

View File

@ -83,6 +83,7 @@ def mock_russound_client() -> Generator[AsyncMock]:
zone.set_bass = AsyncMock()
zone.set_treble = AsyncMock()
zone.set_turn_on_volume = AsyncMock()
zone.set_loudness = AsyncMock()
client.controllers = {
1: Controller(

View File

@ -0,0 +1,193 @@
# serializer version: 1
# name: test_all_entities[switch.backyard_loudness-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.backyard_loudness',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Loudness',
'platform': 'russound_rio',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'loudness',
'unique_id': '00:11:22:33:44:55-C[1].Z[1]-loudness',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[switch.backyard_loudness-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Backyard Loudness',
}),
'context': <ANY>,
'entity_id': 'switch.backyard_loudness',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[switch.bedroom_loudness-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.bedroom_loudness',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Loudness',
'platform': 'russound_rio',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'loudness',
'unique_id': '00:11:22:33:44:55-C[1].Z[3]-loudness',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[switch.bedroom_loudness-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Bedroom Loudness',
}),
'context': <ANY>,
'entity_id': 'switch.bedroom_loudness',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[switch.kitchen_loudness-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.kitchen_loudness',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Loudness',
'platform': 'russound_rio',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'loudness',
'unique_id': '00:11:22:33:44:55-C[1].Z[2]-loudness',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[switch.kitchen_loudness-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Kitchen Loudness',
}),
'context': <ANY>,
'entity_id': 'switch.kitchen_loudness',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---
# name: test_all_entities[switch.living_room_loudness-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'config_subentry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'switch',
'entity_category': <EntityCategory.CONFIG: 'config'>,
'entity_id': 'switch.living_room_loudness',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Loudness',
'platform': 'russound_rio',
'previous_unique_id': None,
'suggested_object_id': None,
'supported_features': 0,
'translation_key': 'loudness',
'unique_id': '00:11:22:33:44:55-C[2].Z[9]-loudness',
'unit_of_measurement': None,
})
# ---
# name: test_all_entities[switch.living_room_loudness-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'Living Room Loudness',
}),
'context': <ANY>,
'entity_id': 'switch.living_room_loudness',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'off',
})
# ---

View File

@ -0,0 +1,64 @@
"""Tests for the Russound RIO switch platform."""
from unittest.mock import AsyncMock, patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SERVICE_TURN_ON
from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from . import setup_integration
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.SWITCH]):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_setting_value(
hass: HomeAssistant,
mock_russound_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test setting value."""
await setup_integration(hass, mock_config_entry)
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_ON,
{
ATTR_ENTITY_ID: "switch.backyard_loudness",
},
blocking=True,
)
mock_russound_client.controllers[1].zones[1].set_loudness.assert_called_once_with(
True
)
mock_russound_client.controllers[1].zones[1].set_loudness.reset_mock()
await hass.services.async_call(
SWITCH_DOMAIN,
SERVICE_TURN_OFF,
{
ATTR_ENTITY_ID: "switch.backyard_loudness",
},
blocking=True,
)
mock_russound_client.controllers[1].zones[1].set_loudness.assert_called_once_with(
False
)