From 75946065f2660cb28f7efa693f2ced4759049b8a Mon Sep 17 00:00:00 2001 From: Guido Schmitz Date: Sun, 22 Jun 2025 23:55:51 +0200 Subject: [PATCH 1/5] Combine executor calls in devolo Home Control (#147216) --- .../devolo_home_control/__init__.py | 34 +++++++++++-------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/devolo_home_control/__init__.py b/homeassistant/components/devolo_home_control/__init__.py index 20a1edf734d04c..80320e2a8494c2 100644 --- a/homeassistant/components/devolo_home_control/__init__.py +++ b/homeassistant/components/devolo_home_control/__init__.py @@ -29,21 +29,9 @@ async def async_setup_entry( """Set up the devolo account from a config entry.""" mydevolo = configure_mydevolo(entry.data) - credentials_valid = await hass.async_add_executor_job(mydevolo.credentials_valid) - - if not credentials_valid: - raise ConfigEntryAuthFailed( - translation_domain=DOMAIN, - translation_key="invalid_auth", - ) - - if await hass.async_add_executor_job(mydevolo.maintenance): - raise ConfigEntryNotReady( - translation_domain=DOMAIN, - translation_key="maintenance", - ) - - gateway_ids = await hass.async_add_executor_job(mydevolo.get_gateway_ids) + gateway_ids = await hass.async_add_executor_job( + check_mydevolo_and_get_gateway_ids, mydevolo + ) if entry.unique_id and GATEWAY_SERIAL_PATTERN.match(entry.unique_id): uuid = await hass.async_add_executor_job(mydevolo.uuid) @@ -115,3 +103,19 @@ def configure_mydevolo(conf: Mapping[str, Any]) -> Mydevolo: mydevolo.user = conf[CONF_USERNAME] mydevolo.password = conf[CONF_PASSWORD] return mydevolo + + +def check_mydevolo_and_get_gateway_ids(mydevolo: Mydevolo) -> list[str]: + """Check if the credentials are valid and return user's gateway IDs as long as mydevolo is not in maintenance mode.""" + if not mydevolo.credentials_valid(): + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="invalid_auth", + ) + if mydevolo.maintenance(): + raise ConfigEntryNotReady( + translation_domain=DOMAIN, + translation_key="maintenance", + ) + + return mydevolo.get_gateway_ids() From fcba1183f86133be1f5bffce0278390a48d534fa Mon Sep 17 00:00:00 2001 From: msw Date: Sun, 22 Jun 2025 14:57:02 -0700 Subject: [PATCH 2/5] Add water filter replacement and usage sensors to SmartThings (#147279) * Add "Filter status" binary sensor for Samsung refrigerators * Add "Water filter usage" sensor for Samsung refrigerators --- .../components/smartthings/binary_sensor.py | 8 + .../components/smartthings/sensor.py | 10 ++ .../components/smartthings/strings.json | 3 + .../snapshots/test_binary_sensor.ambr | 147 +++++++++++++++++ .../smartthings/snapshots/test_sensor.ambr | 156 ++++++++++++++++++ 5 files changed, 324 insertions(+) diff --git a/homeassistant/components/smartthings/binary_sensor.py b/homeassistant/components/smartthings/binary_sensor.py index ea8db71c481aa4..aafb05576bf10a 100644 --- a/homeassistant/components/smartthings/binary_sensor.py +++ b/homeassistant/components/smartthings/binary_sensor.py @@ -80,6 +80,14 @@ class SmartThingsBinarySensorEntityDescription(BinarySensorEntityDescription): entity_category=EntityCategory.DIAGNOSTIC, ) }, + Capability.CUSTOM_WATER_FILTER: { + Attribute.WATER_FILTER_STATUS: SmartThingsBinarySensorEntityDescription( + key=Attribute.WATER_FILTER_STATUS, + translation_key="filter_status", + device_class=BinarySensorDeviceClass.PROBLEM, + is_on_key="replace", + ) + }, Capability.SAMSUNG_CE_STEAM_CLOSET_KEEP_FRESH_MODE: { Attribute.OPERATING_STATE: SmartThingsBinarySensorEntityDescription( key=Attribute.OPERATING_STATE, diff --git a/homeassistant/components/smartthings/sensor.py b/homeassistant/components/smartthings/sensor.py index ef066c02130df1..a38331d6aed77f 100644 --- a/homeassistant/components/smartthings/sensor.py +++ b/homeassistant/components/smartthings/sensor.py @@ -325,6 +325,16 @@ class SmartThingsSensorEntityDescription(SensorEntityDescription): ) ] }, + Capability.CUSTOM_WATER_FILTER: { + Attribute.WATER_FILTER_USAGE: [ + SmartThingsSensorEntityDescription( + key=Attribute.WATER_FILTER_USAGE, + translation_key="water_filter_usage", + native_unit_of_measurement=PERCENTAGE, + state_class=SensorStateClass.MEASUREMENT, + ) + ] + }, Capability.DISHWASHER_OPERATING_STATE: { Attribute.MACHINE_STATE: [ SmartThingsSensorEntityDescription( diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index 5a1d111b617e0a..53e08546583ae7 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -595,6 +595,9 @@ }, "water_consumption": { "name": "Water consumption" + }, + "water_filter_usage": { + "name": "Water filter usage" } }, "switch": { diff --git a/tests/components/smartthings/snapshots/test_binary_sensor.ambr b/tests/components/smartthings/snapshots/test_binary_sensor.ambr index 40784adcec6eed..7be4d3af55b173 100644 --- a/tests/components/smartthings/snapshots/test_binary_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_binary_sensor.ambr @@ -679,6 +679,55 @@ 'state': 'on', }) # --- +# name: test_all_entities[da_ref_normal_000001][binary_sensor.refrigerator_filter_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.refrigerator_filter_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Filter status', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'filter_status', + 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_main_custom.waterFilter_waterFilterStatus_waterFilterStatus', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ref_normal_000001][binary_sensor.refrigerator_filter_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Refrigerator Filter status', + }), + 'context': , + 'entity_id': 'binary_sensor.refrigerator_filter_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'on', + }) +# --- # name: test_all_entities[da_ref_normal_000001][binary_sensor.refrigerator_freezer_door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -826,6 +875,55 @@ 'state': 'off', }) # --- +# name: test_all_entities[da_ref_normal_01001][binary_sensor.refrigerator_filter_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.refrigerator_filter_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Filter status', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'filter_status', + 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_custom.waterFilter_waterFilterStatus_waterFilterStatus', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ref_normal_01001][binary_sensor.refrigerator_filter_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Refrigerator Filter status', + }), + 'context': , + 'entity_id': 'binary_sensor.refrigerator_filter_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[da_ref_normal_01001][binary_sensor.refrigerator_freezer_door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -924,6 +1022,55 @@ 'state': 'off', }) # --- +# name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_filter_status-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'binary_sensor', + 'entity_category': None, + 'entity_id': 'binary_sensor.frigo_filter_status', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Filter status', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'filter_status', + 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_main_custom.waterFilter_waterFilterStatus_waterFilterStatus', + 'unit_of_measurement': None, + }) +# --- +# name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_filter_status-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'problem', + 'friendly_name': 'Frigo Filter status', + }), + 'context': , + 'entity_id': 'binary_sensor.frigo_filter_status', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'off', + }) +# --- # name: test_all_entities[da_ref_normal_01011][binary_sensor.frigo_freezer_door-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ diff --git a/tests/components/smartthings/snapshots/test_sensor.ambr b/tests/components/smartthings/snapshots/test_sensor.ambr index 40180b88bca95d..f88524116ee209 100644 --- a/tests/components/smartthings/snapshots/test_sensor.ambr +++ b/tests/components/smartthings/snapshots/test_sensor.ambr @@ -5122,6 +5122,58 @@ 'state': '0.0135559777781698', }) # --- +# name: test_all_entities[da_ref_normal_000001][sensor.refrigerator_water_filter_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.refrigerator_water_filter_usage', + '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': 'Water filter usage', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'water_filter_usage', + 'unique_id': '7db87911-7dce-1cf2-7119-b953432a2f09_main_custom.waterFilter_waterFilterUsage_waterFilterUsage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[da_ref_normal_000001][sensor.refrigerator_water_filter_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Refrigerator Water filter usage', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.refrigerator_water_filter_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '100', + }) +# --- # name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5516,6 +5568,58 @@ 'state': '0.0270189050030708', }) # --- +# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_water_filter_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.refrigerator_water_filter_usage', + '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': 'Water filter usage', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'water_filter_usage', + 'unique_id': '7d3feb98-8a36-4351-c362-5e21ad3a78dd_main_custom.waterFilter_waterFilterUsage_waterFilterUsage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[da_ref_normal_01001][sensor.refrigerator_water_filter_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Refrigerator Water filter usage', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.refrigerator_water_filter_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '52', + }) +# --- # name: test_all_entities[da_ref_normal_01011][sensor.frigo_energy-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ @@ -5910,6 +6014,58 @@ 'state': '0.0143511805540986', }) # --- +# name: test_all_entities[da_ref_normal_01011][sensor.frigo_water_filter_usage-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'state_class': , + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.frigo_water_filter_usage', + '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': 'Water filter usage', + 'platform': 'smartthings', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': 'water_filter_usage', + 'unique_id': '5758b2ec-563e-f39b-ec39-208e54aabf60_main_custom.waterFilter_waterFilterUsage_waterFilterUsage', + 'unit_of_measurement': '%', + }) +# --- +# name: test_all_entities[da_ref_normal_01011][sensor.frigo_water_filter_usage-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Frigo Water filter usage', + 'state_class': , + 'unit_of_measurement': '%', + }), + 'context': , + 'entity_id': 'sensor.frigo_water_filter_usage', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '97', + }) +# --- # name: test_all_entities[da_rvc_normal_000001][sensor.robot_vacuum_battery-entry] EntityRegistryEntrySnapshot({ 'aliases': set({ From 25968925e7af48a76d630e7d8a7c35e7c69bb74e Mon Sep 17 00:00:00 2001 From: DeerMaximum <43999966+DeerMaximum@users.noreply.github.com> Date: Sun, 22 Jun 2025 23:57:33 +0200 Subject: [PATCH 3/5] Use has_entity_name in NINA (#146755) * Comply with has-entity-name rule. * Fix tests --- .../components/nina/binary_sensor.py | 1 + tests/components/nina/test_binary_sensor.py | 60 +++++++++---------- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/nina/binary_sensor.py b/homeassistant/components/nina/binary_sensor.py index be7e5995fbccdc..be37a802d47088 100644 --- a/homeassistant/components/nina/binary_sensor.py +++ b/homeassistant/components/nina/binary_sensor.py @@ -56,6 +56,7 @@ class NINAMessage(CoordinatorEntity[NINADataUpdateCoordinator], BinarySensorEnti """Representation of an NINA warning.""" _attr_device_class = BinarySensorDeviceClass.SAFETY + _attr_has_entity_name = True def __init__( self, diff --git a/tests/components/nina/test_binary_sensor.py b/tests/components/nina/test_binary_sensor.py index 6ed1aee7e9ddb0..d18b7562b53920 100644 --- a/tests/components/nina/test_binary_sensor.py +++ b/tests/components/nina/test_binary_sensor.py @@ -66,8 +66,8 @@ async def test_sensors(hass: HomeAssistant, entity_registry: er.EntityRegistry) assert conf_entry.state is ConfigEntryState.LOADED - state_w1 = hass.states.get("binary_sensor.warning_aach_stadt_1") - entry_w1 = entity_registry.async_get("binary_sensor.warning_aach_stadt_1") + state_w1 = hass.states.get("binary_sensor.nina_warning_aach_stadt_1") + entry_w1 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_1") assert state_w1.state == STATE_ON assert state_w1.attributes.get(ATTR_HEADLINE) == "Ausfall Notruf 112" @@ -91,8 +91,8 @@ async def test_sensors(hass: HomeAssistant, entity_registry: er.EntityRegistry) assert entry_w1.unique_id == "083350000000-1" assert state_w1.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY - state_w2 = hass.states.get("binary_sensor.warning_aach_stadt_2") - entry_w2 = entity_registry.async_get("binary_sensor.warning_aach_stadt_2") + state_w2 = hass.states.get("binary_sensor.nina_warning_aach_stadt_2") + entry_w2 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_2") assert state_w2.state == STATE_OFF assert state_w2.attributes.get(ATTR_HEADLINE) is None @@ -110,8 +110,8 @@ async def test_sensors(hass: HomeAssistant, entity_registry: er.EntityRegistry) assert entry_w2.unique_id == "083350000000-2" assert state_w2.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY - state_w3 = hass.states.get("binary_sensor.warning_aach_stadt_3") - entry_w3 = entity_registry.async_get("binary_sensor.warning_aach_stadt_3") + state_w3 = hass.states.get("binary_sensor.nina_warning_aach_stadt_3") + entry_w3 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_3") assert state_w3.state == STATE_OFF assert state_w3.attributes.get(ATTR_HEADLINE) is None @@ -129,8 +129,8 @@ async def test_sensors(hass: HomeAssistant, entity_registry: er.EntityRegistry) assert entry_w3.unique_id == "083350000000-3" assert state_w3.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY - state_w4 = hass.states.get("binary_sensor.warning_aach_stadt_4") - entry_w4 = entity_registry.async_get("binary_sensor.warning_aach_stadt_4") + state_w4 = hass.states.get("binary_sensor.nina_warning_aach_stadt_4") + entry_w4 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_4") assert state_w4.state == STATE_OFF assert state_w4.attributes.get(ATTR_HEADLINE) is None @@ -148,8 +148,8 @@ async def test_sensors(hass: HomeAssistant, entity_registry: er.EntityRegistry) assert entry_w4.unique_id == "083350000000-4" assert state_w4.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY - state_w5 = hass.states.get("binary_sensor.warning_aach_stadt_5") - entry_w5 = entity_registry.async_get("binary_sensor.warning_aach_stadt_5") + state_w5 = hass.states.get("binary_sensor.nina_warning_aach_stadt_5") + entry_w5 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_5") assert state_w5.state == STATE_OFF assert state_w5.attributes.get(ATTR_HEADLINE) is None @@ -187,8 +187,8 @@ async def test_sensors_without_corona_filter( assert conf_entry.state is ConfigEntryState.LOADED - state_w1 = hass.states.get("binary_sensor.warning_aach_stadt_1") - entry_w1 = entity_registry.async_get("binary_sensor.warning_aach_stadt_1") + state_w1 = hass.states.get("binary_sensor.nina_warning_aach_stadt_1") + entry_w1 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_1") assert state_w1.state == STATE_ON assert ( @@ -218,8 +218,8 @@ async def test_sensors_without_corona_filter( assert entry_w1.unique_id == "083350000000-1" assert state_w1.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY - state_w2 = hass.states.get("binary_sensor.warning_aach_stadt_2") - entry_w2 = entity_registry.async_get("binary_sensor.warning_aach_stadt_2") + state_w2 = hass.states.get("binary_sensor.nina_warning_aach_stadt_2") + entry_w2 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_2") assert state_w2.state == STATE_ON assert state_w2.attributes.get(ATTR_HEADLINE) == "Ausfall Notruf 112" @@ -243,8 +243,8 @@ async def test_sensors_without_corona_filter( assert entry_w2.unique_id == "083350000000-2" assert state_w2.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY - state_w3 = hass.states.get("binary_sensor.warning_aach_stadt_3") - entry_w3 = entity_registry.async_get("binary_sensor.warning_aach_stadt_3") + state_w3 = hass.states.get("binary_sensor.nina_warning_aach_stadt_3") + entry_w3 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_3") assert state_w3.state == STATE_OFF assert state_w3.attributes.get(ATTR_HEADLINE) is None @@ -262,8 +262,8 @@ async def test_sensors_without_corona_filter( assert entry_w3.unique_id == "083350000000-3" assert state_w3.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY - state_w4 = hass.states.get("binary_sensor.warning_aach_stadt_4") - entry_w4 = entity_registry.async_get("binary_sensor.warning_aach_stadt_4") + state_w4 = hass.states.get("binary_sensor.nina_warning_aach_stadt_4") + entry_w4 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_4") assert state_w4.state == STATE_OFF assert state_w4.attributes.get(ATTR_HEADLINE) is None @@ -281,8 +281,8 @@ async def test_sensors_without_corona_filter( assert entry_w4.unique_id == "083350000000-4" assert state_w4.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY - state_w5 = hass.states.get("binary_sensor.warning_aach_stadt_5") - entry_w5 = entity_registry.async_get("binary_sensor.warning_aach_stadt_5") + state_w5 = hass.states.get("binary_sensor.nina_warning_aach_stadt_5") + entry_w5 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_5") assert state_w5.state == STATE_OFF assert state_w5.attributes.get(ATTR_HEADLINE) is None @@ -320,40 +320,40 @@ async def test_sensors_with_area_filter( assert conf_entry.state is ConfigEntryState.LOADED - state_w1 = hass.states.get("binary_sensor.warning_aach_stadt_1") - entry_w1 = entity_registry.async_get("binary_sensor.warning_aach_stadt_1") + state_w1 = hass.states.get("binary_sensor.nina_warning_aach_stadt_1") + entry_w1 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_1") assert state_w1.state == STATE_ON assert entry_w1.unique_id == "083350000000-1" assert state_w1.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY - state_w2 = hass.states.get("binary_sensor.warning_aach_stadt_2") - entry_w2 = entity_registry.async_get("binary_sensor.warning_aach_stadt_2") + state_w2 = hass.states.get("binary_sensor.nina_warning_aach_stadt_2") + entry_w2 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_2") assert state_w2.state == STATE_OFF assert entry_w2.unique_id == "083350000000-2" assert state_w2.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY - state_w3 = hass.states.get("binary_sensor.warning_aach_stadt_3") - entry_w3 = entity_registry.async_get("binary_sensor.warning_aach_stadt_3") + state_w3 = hass.states.get("binary_sensor.nina_warning_aach_stadt_3") + entry_w3 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_3") assert state_w3.state == STATE_OFF assert entry_w3.unique_id == "083350000000-3" assert state_w3.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY - state_w4 = hass.states.get("binary_sensor.warning_aach_stadt_4") - entry_w4 = entity_registry.async_get("binary_sensor.warning_aach_stadt_4") + state_w4 = hass.states.get("binary_sensor.nina_warning_aach_stadt_4") + entry_w4 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_4") assert state_w4.state == STATE_OFF assert entry_w4.unique_id == "083350000000-4" assert state_w4.attributes.get("device_class") == BinarySensorDeviceClass.SAFETY - state_w5 = hass.states.get("binary_sensor.warning_aach_stadt_5") - entry_w5 = entity_registry.async_get("binary_sensor.warning_aach_stadt_5") + state_w5 = hass.states.get("binary_sensor.nina_warning_aach_stadt_5") + entry_w5 = entity_registry.async_get("binary_sensor.nina_warning_aach_stadt_5") assert state_w5.state == STATE_OFF From b47706f360341655b64f765249df27ec07ca1c68 Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Mon, 23 Jun 2025 01:01:15 +0300 Subject: [PATCH 4/5] Add sensor platform to Alexa Devices (#146469) * Add sensor platform to Amazon Devices * fix merge after rename * fix requirements * cleanup * Revert "cleanup" This reverts commit f34892da8a9cc1836870ceef8f8e48ca946b3ff6. * tests * move logic in sensor entity description * update tests * apply review comment * apply review comments --- .../components/alexa_devices/__init__.py | 1 + .../components/alexa_devices/sensor.py | 88 +++++++++++ tests/components/alexa_devices/conftest.py | 8 +- .../alexa_devices/snapshots/test_sensor.ambr | 54 +++++++ tests/components/alexa_devices/test_sensor.py | 143 ++++++++++++++++++ 5 files changed, 292 insertions(+), 2 deletions(-) create mode 100644 homeassistant/components/alexa_devices/sensor.py create mode 100644 tests/components/alexa_devices/snapshots/test_sensor.ambr create mode 100644 tests/components/alexa_devices/test_sensor.py diff --git a/homeassistant/components/alexa_devices/__init__.py b/homeassistant/components/alexa_devices/__init__.py index 7a4139a65da153..aff4c1bb391505 100644 --- a/homeassistant/components/alexa_devices/__init__.py +++ b/homeassistant/components/alexa_devices/__init__.py @@ -8,6 +8,7 @@ PLATFORMS = [ Platform.BINARY_SENSOR, Platform.NOTIFY, + Platform.SENSOR, Platform.SWITCH, ] diff --git a/homeassistant/components/alexa_devices/sensor.py b/homeassistant/components/alexa_devices/sensor.py new file mode 100644 index 00000000000000..89c2bdce9b7bef --- /dev/null +++ b/homeassistant/components/alexa_devices/sensor.py @@ -0,0 +1,88 @@ +"""Support for sensors.""" + +from __future__ import annotations + +from collections.abc import Callable +from dataclasses import dataclass +from typing import Final + +from aioamazondevices.api import AmazonDevice + +from homeassistant.components.sensor import ( + SensorDeviceClass, + SensorEntity, + SensorEntityDescription, +) +from homeassistant.const import LIGHT_LUX, UnitOfTemperature +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback +from homeassistant.helpers.typing import StateType + +from .coordinator import AmazonConfigEntry +from .entity import AmazonEntity + +# Coordinator is used to centralize the data updates +PARALLEL_UPDATES = 0 + + +@dataclass(frozen=True, kw_only=True) +class AmazonSensorEntityDescription(SensorEntityDescription): + """Amazon Devices sensor entity description.""" + + native_unit_of_measurement_fn: Callable[[AmazonDevice, str], str] | None = None + + +SENSORS: Final = ( + AmazonSensorEntityDescription( + key="temperature", + device_class=SensorDeviceClass.TEMPERATURE, + native_unit_of_measurement_fn=lambda device, _key: ( + UnitOfTemperature.CELSIUS + if device.sensors[_key].scale == "CELSIUS" + else UnitOfTemperature.FAHRENHEIT + ), + ), + AmazonSensorEntityDescription( + key="illuminance", + device_class=SensorDeviceClass.ILLUMINANCE, + native_unit_of_measurement=LIGHT_LUX, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: AmazonConfigEntry, + async_add_entities: AddConfigEntryEntitiesCallback, +) -> None: + """Set up Amazon Devices sensors based on a config entry.""" + + coordinator = entry.runtime_data + + async_add_entities( + AmazonSensorEntity(coordinator, serial_num, sensor_desc) + for sensor_desc in SENSORS + for serial_num in coordinator.data + if coordinator.data[serial_num].sensors.get(sensor_desc.key) is not None + ) + + +class AmazonSensorEntity(AmazonEntity, SensorEntity): + """Sensor device.""" + + entity_description: AmazonSensorEntityDescription + + @property + def native_unit_of_measurement(self) -> str | None: + """Return the unit of measurement of the sensor.""" + if self.entity_description.native_unit_of_measurement_fn: + return self.entity_description.native_unit_of_measurement_fn( + self.device, self.entity_description.key + ) + + return super().native_unit_of_measurement + + @property + def native_value(self) -> StateType: + """Return the state of the sensor.""" + return self.device.sensors[self.entity_description.key].value diff --git a/tests/components/alexa_devices/conftest.py b/tests/components/alexa_devices/conftest.py index f1f40eebd2747f..798515505289c2 100644 --- a/tests/components/alexa_devices/conftest.py +++ b/tests/components/alexa_devices/conftest.py @@ -3,7 +3,7 @@ from collections.abc import Generator from unittest.mock import AsyncMock, patch -from aioamazondevices.api import AmazonDevice +from aioamazondevices.api import AmazonDevice, AmazonDeviceSensor from aioamazondevices.const import DEVICE_TYPE_TO_MODEL import pytest @@ -58,7 +58,11 @@ def mock_amazon_devices_client() -> Generator[AsyncMock]: bluetooth_state=True, entity_id="11111111-2222-3333-4444-555555555555", appliance_id="G1234567890123456789012345678A", - sensors={}, + sensors={ + "temperature": AmazonDeviceSensor( + name="temperature", value="22.5", scale="CELSIUS" + ) + }, ) } client.get_model_details = lambda device: DEVICE_TYPE_TO_MODEL.get( diff --git a/tests/components/alexa_devices/snapshots/test_sensor.ambr b/tests/components/alexa_devices/snapshots/test_sensor.ambr new file mode 100644 index 00000000000000..ae245b5c463800 --- /dev/null +++ b/tests/components/alexa_devices/snapshots/test_sensor.ambr @@ -0,0 +1,54 @@ +# serializer version: 1 +# name: test_all_entities[sensor.echo_test_temperature-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': None, + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'sensor', + 'entity_category': None, + 'entity_id': 'sensor.echo_test_temperature', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + 'sensor': dict({ + 'suggested_display_precision': 1, + }), + }), + 'original_device_class': , + 'original_icon': None, + 'original_name': 'Temperature', + 'platform': 'alexa_devices', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': None, + 'unique_id': 'echo_test_serial_number-temperature', + 'unit_of_measurement': , + }) +# --- +# name: test_all_entities[sensor.echo_test_temperature-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'device_class': 'temperature', + 'friendly_name': 'Echo Test Temperature', + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'sensor.echo_test_temperature', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '22.5', + }) +# --- diff --git a/tests/components/alexa_devices/test_sensor.py b/tests/components/alexa_devices/test_sensor.py new file mode 100644 index 00000000000000..e8875fe08a4473 --- /dev/null +++ b/tests/components/alexa_devices/test_sensor.py @@ -0,0 +1,143 @@ +"""Tests for the Alexa Devices sensor platform.""" + +from typing import Any +from unittest.mock import AsyncMock, patch + +from aioamazondevices.api import AmazonDeviceSensor +from aioamazondevices.exceptions import ( + CannotAuthenticate, + CannotConnect, + CannotRetrieveData, +) +from freezegun.api import FrozenDateTimeFactory +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.alexa_devices.coordinator import SCAN_INTERVAL +from homeassistant.const import STATE_UNAVAILABLE, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration +from .const import TEST_SERIAL_NUMBER + +from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform + + +async def test_all_entities( + hass: HomeAssistant, + snapshot: SnapshotAssertion, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + entity_registry: er.EntityRegistry, +) -> None: + """Test all entities.""" + with patch("homeassistant.components.alexa_devices.PLATFORMS", [Platform.SENSOR]): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +@pytest.mark.parametrize( + "side_effect", + [ + CannotConnect, + CannotRetrieveData, + CannotAuthenticate, + ], +) +async def test_coordinator_data_update_fails( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + side_effect: Exception, +) -> None: + """Test coordinator data update exceptions.""" + + entity_id = "sensor.echo_test_temperature" + + await setup_integration(hass, mock_config_entry) + + assert (state := hass.states.get(entity_id)) + assert state.state == "22.5" + + mock_amazon_devices_client.get_devices_data.side_effect = side_effect + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_UNAVAILABLE + + +async def test_offline_device( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test offline device handling.""" + + entity_id = "sensor.echo_test_temperature" + + mock_amazon_devices_client.get_devices_data.return_value[ + TEST_SERIAL_NUMBER + ].online = False + + await setup_integration(hass, mock_config_entry) + + assert (state := hass.states.get(entity_id)) + assert state.state == STATE_UNAVAILABLE + + mock_amazon_devices_client.get_devices_data.return_value[ + TEST_SERIAL_NUMBER + ].online = True + + freezer.tick(SCAN_INTERVAL) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert (state := hass.states.get(entity_id)) + assert state.state != STATE_UNAVAILABLE + + +@pytest.mark.parametrize( + ("sensor", "api_value", "scale", "state_value", "unit"), + [ + ( + "temperature", + "86", + "FAHRENHEIT", + "30.0", # State machine converts to °C + "°C", # State machine converts to °C + ), + ("temperature", "22.5", "CELSIUS", "22.5", "°C"), + ("illuminance", "800", None, "800", "lx"), + ], +) +async def test_unit_of_measurement( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_amazon_devices_client: AsyncMock, + mock_config_entry: MockConfigEntry, + sensor: str, + api_value: Any, + scale: str | None, + state_value: Any, + unit: str | None, +) -> None: + """Test sensor unit of measurement handling.""" + + entity_id = f"sensor.echo_test_{sensor}" + + mock_amazon_devices_client.get_devices_data.return_value[ + TEST_SERIAL_NUMBER + ].sensors = {sensor: AmazonDeviceSensor(name=sensor, value=api_value, scale=scale)} + + await setup_integration(hass, mock_config_entry) + + assert (state := hass.states.get(entity_id)) + assert state.state == state_value + assert state.attributes["unit_of_measurement"] == unit 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 5/5] 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 f35a476bbb37cf..51ca9a9b1eafb1 100644 --- a/homeassistant/components/russound_rio/__init__.py +++ b/homeassistant/components/russound_rio/__init__.py @@ -14,7 +14,7 @@ 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 d7b4e412831e06..1fe6a7876d18e0 100644 --- a/homeassistant/components/russound_rio/entity.py +++ b/homeassistant/components/russound_rio/entity.py @@ -6,6 +6,7 @@ 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 @@ def __init__( 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 @@ def __init__( 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 7dbc3ae34be5ce..aaaad05a2bce49 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -9,7 +9,6 @@ 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 @@ def __init__( ) -> 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 00000000000000..ae13815fa0a262 --- /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 eba668563024f0..37f78bfa75d1eb 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 2516bd816505dd..5e57c45193b8a0 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 00000000000000..f1b806a378ae42 --- /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 00000000000000..ff2c46fb4e1f42 --- /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)