From 8a84eb48cbef83becce9468bc0d8274e594fb549 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 19 Aug 2026 09:34:06 +0000 Subject: [PATCH] Use the entity service helper for all xiaomi_miio services Address review feedback. The platform entity lookup resolves every xiaomi_miio entity of a domain, not just the ones the old dispatcher tracked in its data key, so the gateway lights, the eyecare ambient light and the coordinated and gateway switches were reachable without implementing the methods, raising AttributeError. The helper accepts a callable, so a handler that skips entities without the method keeps the behaviour of the old dispatcher while gaining the context and the entity permissions. All fourteen services now use it, and the data keys they dispatched through are gone. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01DQdoLT4SC1w5LUgDQA3vEq --- homeassistant/components/xiaomi_miio/const.py | 7 - homeassistant/components/xiaomi_miio/fan.py | 5 - homeassistant/components/xiaomi_miio/light.py | 11 - .../components/xiaomi_miio/services.py | 250 ++++++------------ .../components/xiaomi_miio/services.yaml | 88 +++--- .../components/xiaomi_miio/strings.json | 50 ---- .../components/xiaomi_miio/switch.py | 10 - tests/components/xiaomi_miio/test_services.py | 36 ++- 8 files changed, 143 insertions(+), 314 deletions(-) diff --git a/homeassistant/components/xiaomi_miio/const.py b/homeassistant/components/xiaomi_miio/const.py index 78c3861f166d..2f0df0cca905 100644 --- a/homeassistant/components/xiaomi_miio/const.py +++ b/homeassistant/components/xiaomi_miio/const.py @@ -278,16 +278,9 @@ SERVICE_SET_EXTRA_FEATURES = "fan_set_extra_features" SERVICE_SET_DRY = "set_dry" SERVICE_SET_MOTOR_SPEED = "fan_set_motor_speed" -# Fan/Humidifier data -FAN_DATA_KEY = "fan.xiaomi_miio" - # Light data -LIGHT_DATA_KEY = "light.xiaomi_miio" ATTR_SCENE = "scene" -# Switch data -SWITCH_DATA_KEY = "switch.xiaomi_miio" - # Light Services SERVICE_SET_SCENE = "light_set_scene" SERVICE_SET_DELAYED_TURN_OFF = "light_set_delayed_turn_off" diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index b47e3c6d430d..5c7356cb6115 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -40,7 +40,6 @@ from homeassistant.util.percentage import ( from .const import ( CONF_FLOW_TYPE, - FAN_DATA_KEY as DATA_KEY, FEATURE_FLAGS_AIRFRESH, FEATURE_FLAGS_AIRFRESH_A1, FEATURE_FLAGS_AIRFRESH_T2017, @@ -192,8 +191,6 @@ async def async_setup_entry( if config_entry.data[CONF_FLOW_TYPE] != CONF_DEVICE: return - hass.data.setdefault(DATA_KEY, {}) - model = config_entry.data[CONF_MODEL] unique_id = config_entry.unique_id device = config_entry.runtime_data.device @@ -234,8 +231,6 @@ async def async_setup_entry( else: return - hass.data[DATA_KEY][unique_id] = entity - entities.append(entity) async_add_entities(entities) diff --git a/homeassistant/components/xiaomi_miio/light.py b/homeassistant/components/xiaomi_miio/light.py index 35cf33778e50..f348e28e286e 100644 --- a/homeassistant/components/xiaomi_miio/light.py +++ b/homeassistant/components/xiaomi_miio/light.py @@ -41,7 +41,6 @@ from .const import ( CONF_FLOW_TYPE, CONF_GATEWAY, DOMAIN, - LIGHT_DATA_KEY as DATA_KEY, MODELS_LIGHT_BULB, MODELS_LIGHT_CEILING, MODELS_LIGHT_EYECARE, @@ -108,9 +107,6 @@ async def async_setup_entry( ) if config_entry.data[CONF_FLOW_TYPE] == CONF_DEVICE: - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {} - host = config_entry.data[CONF_HOST] token = config_entry.data[CONF_TOKEN] name = config_entry.title @@ -123,35 +119,28 @@ async def async_setup_entry( light = PhilipsEyecare(host, token) entity = XiaomiPhilipsEyecareLamp(name, light, config_entry, unique_id) entities.append(entity) - hass.data[DATA_KEY][host] = entity entities.append( XiaomiPhilipsEyecareLampAmbientLight( name, light, config_entry, unique_id ) ) - # The ambient light doesn't expose additional services. - # A hass.data[DATA_KEY] entry isn't needed. elif model in MODELS_LIGHT_CEILING: light = Ceil(host, token) entity = XiaomiPhilipsCeilingLamp(name, light, config_entry, unique_id) entities.append(entity) - hass.data[DATA_KEY][host] = entity elif model in MODELS_LIGHT_MOON: light = PhilipsMoonlight(host, token) entity = XiaomiPhilipsMoonlightLamp(name, light, config_entry, unique_id) entities.append(entity) - hass.data[DATA_KEY][host] = entity elif model in MODELS_LIGHT_BULB: light = PhilipsBulb(host, token) entity = XiaomiPhilipsBulb(name, light, config_entry, unique_id) entities.append(entity) - hass.data[DATA_KEY][host] = entity elif model in MODELS_LIGHT_MONO: light = PhilipsBulb(host, token) entity = XiaomiPhilipsGenericLight(name, light, config_entry, unique_id) entities.append(entity) - hass.data[DATA_KEY][host] = entity else: _LOGGER.error( ( diff --git a/homeassistant/components/xiaomi_miio/services.py b/homeassistant/components/xiaomi_miio/services.py index e82ffa536f57..44d411a2fa2b 100644 --- a/homeassistant/components/xiaomi_miio/services.py +++ b/homeassistant/components/xiaomi_miio/services.py @@ -1,22 +1,23 @@ """Xiaomi services.""" -import asyncio +from collections.abc import Callable, Coroutine import logging +from typing import Any import voluptuous as vol +from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.vacuum import DOMAIN as VACUUM_DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, ATTR_MODE +from homeassistant.const import ATTR_MODE from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.helpers import config_validation as cv, service +from homeassistant.helpers.entity import Entity from .const import ( ATTR_SCENE, DOMAIN, - FAN_DATA_KEY, - LIGHT_DATA_KEY, SERVICE_EYECARE_MODE_OFF, SERVICE_EYECARE_MODE_ON, SERVICE_NIGHT_LIGHT_MODE_OFF, @@ -31,9 +32,7 @@ from .const import ( SERVICE_SET_SCENE, SERVICE_SET_WIFI_LED_OFF, SERVICE_SET_WIFI_LED_ON, - SWITCH_DATA_KEY, ) -from .typing import ServiceMethodDetails _LOGGER = logging.getLogger(__name__) @@ -54,56 +53,36 @@ SERVICE_GOTO = "vacuum_goto" # Light Services ATTR_TIME_PERIOD = "time_period" -XIAOMI_MIIO_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) -LIGHT_SERVICE_TO_METHOD = { - SERVICE_REMINDER_ON: ServiceMethodDetails(method="async_reminder_on"), - SERVICE_REMINDER_OFF: ServiceMethodDetails(method="async_reminder_off"), - SERVICE_NIGHT_LIGHT_MODE_ON: ServiceMethodDetails( - method="async_night_light_mode_on" - ), - SERVICE_NIGHT_LIGHT_MODE_OFF: ServiceMethodDetails( - method="async_night_light_mode_off" - ), - SERVICE_EYECARE_MODE_ON: ServiceMethodDetails(method="async_eyecare_mode_on"), - SERVICE_EYECARE_MODE_OFF: ServiceMethodDetails(method="async_eyecare_mode_off"), -} # Switch Services ATTR_PRICE = "price" -SWITCH_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) -SWITCH_SERVICE_SCHEMA_POWER_MODE = SWITCH_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_MODE): vol.All(vol.In(["green", "normal"]))} -) -SWITCH_SERVICE_TO_METHOD = { - SERVICE_SET_POWER_MODE: ServiceMethodDetails( - method="async_set_power_mode", - schema=SWITCH_SERVICE_SCHEMA_POWER_MODE, - ), -} # Fan Services ATTR_FEATURES = "features" -FAN_SERVICE_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) -FAN_SERVICE_SCHEMA_EXTRA_FEATURES = FAN_SERVICE_SCHEMA.extend( - {vol.Required(ATTR_FEATURES): cv.positive_int} -) -FAN_SERVICE_TO_METHOD = { - SERVICE_RESET_FILTER: ServiceMethodDetails(method="async_reset_filter"), - SERVICE_SET_EXTRA_FEATURES: ServiceMethodDetails( - method="async_set_extra_features", - schema=FAN_SERVICE_SCHEMA_EXTRA_FEATURES, - ), -} + + +def _async_service_method( + method_name: str, *fields: str +) -> Callable[[Entity, ServiceCall], Coroutine[Any, Any, None]]: + """Return a handler calling the method on entities implementing it. + + The entities of a platform only partially implement these methods, so + entities without it are skipped instead of raising. + """ + + async def _async_call_method(entity: Entity, call: ServiceCall) -> None: + """Call the method on the entity.""" + if (method := getattr(entity, method_name, None)) is None: + return + await method(**{field: call.data[field] for field in fields}) + + return _async_call_method @callback def async_setup_services(hass: HomeAssistant) -> None: """Set up services.""" - _async_setup_fan_services(hass) - _async_setup_light_services(hass) - _async_setup_switch_services(hass) - # Light Services service.async_register_platform_entity_service( hass, @@ -113,7 +92,7 @@ def async_setup_services(hass: HomeAssistant) -> None: schema={ vol.Required(ATTR_SCENE): vol.All(vol.Coerce(int), vol.Clamp(min=1, max=6)) }, - func="async_set_scene", + func=_async_service_method("async_set_scene", ATTR_SCENE), ) service.async_register_platform_entity_service( @@ -122,26 +101,47 @@ def async_setup_services(hass: HomeAssistant) -> None: SERVICE_SET_DELAYED_TURN_OFF, entity_domain=LIGHT_DOMAIN, schema={vol.Required(ATTR_TIME_PERIOD): cv.positive_time_period}, - func="async_set_delayed_turn_off", + func=_async_service_method("async_set_delayed_turn_off", ATTR_TIME_PERIOD), ) + for light_service, light_method in ( + (SERVICE_REMINDER_ON, "async_reminder_on"), + (SERVICE_REMINDER_OFF, "async_reminder_off"), + (SERVICE_NIGHT_LIGHT_MODE_ON, "async_night_light_mode_on"), + (SERVICE_NIGHT_LIGHT_MODE_OFF, "async_night_light_mode_off"), + (SERVICE_EYECARE_MODE_ON, "async_eyecare_mode_on"), + (SERVICE_EYECARE_MODE_OFF, "async_eyecare_mode_off"), + ): + service.async_register_platform_entity_service( + hass, + DOMAIN, + light_service, + entity_domain=LIGHT_DOMAIN, + schema=None, + func=_async_service_method(light_method), + ) + # Switch Services - service.async_register_platform_entity_service( - hass, - DOMAIN, - SERVICE_SET_WIFI_LED_ON, - entity_domain=SWITCH_DOMAIN, - schema=None, - func="async_set_wifi_led_on", - ) + for switch_service, switch_method in ( + (SERVICE_SET_WIFI_LED_ON, "async_set_wifi_led_on"), + (SERVICE_SET_WIFI_LED_OFF, "async_set_wifi_led_off"), + ): + service.async_register_platform_entity_service( + hass, + DOMAIN, + switch_service, + entity_domain=SWITCH_DOMAIN, + schema=None, + func=_async_service_method(switch_method), + ) service.async_register_platform_entity_service( hass, DOMAIN, - SERVICE_SET_WIFI_LED_OFF, + SERVICE_SET_POWER_MODE, entity_domain=SWITCH_DOMAIN, - schema=None, - func="async_set_wifi_led_off", + schema={vol.Required(ATTR_MODE): vol.All(vol.In(["green", "normal"]))}, + func=_async_service_method("async_set_power_mode", ATTR_MODE), ) service.async_register_platform_entity_service( @@ -150,7 +150,26 @@ def async_setup_services(hass: HomeAssistant) -> None: SERVICE_SET_POWER_PRICE, entity_domain=SWITCH_DOMAIN, schema={vol.Required(ATTR_PRICE): cv.positive_float}, - func="async_set_power_price", + func=_async_service_method("async_set_power_price", ATTR_PRICE), + ) + + # Fan Services + service.async_register_platform_entity_service( + hass, + DOMAIN, + SERVICE_RESET_FILTER, + entity_domain=FAN_DOMAIN, + schema=None, + func=_async_service_method("async_reset_filter"), + ) + + service.async_register_platform_entity_service( + hass, + DOMAIN, + SERVICE_SET_EXTRA_FEATURES, + entity_domain=FAN_DOMAIN, + schema={vol.Required(ATTR_FEATURES): cv.positive_int}, + func=_async_service_method("async_set_extra_features", ATTR_FEATURES), ) # Vacuum Services @@ -251,118 +270,3 @@ def async_setup_services(hass: HomeAssistant) -> None: schema={vol.Required("segments"): vol.Any(vol.Coerce(int), [vol.Coerce(int)])}, func="async_clean_segment", ) - - -def _async_setup_light_services(hass: HomeAssistant) -> None: - """Set up Xiaomi Miio light services.""" - hass.data.setdefault(LIGHT_DATA_KEY, {}) - - async def async_service_handler(call: ServiceCall) -> None: - """Map services to methods on Xiaomi Philips Lights.""" - method = LIGHT_SERVICE_TO_METHOD[call.service] - params = { - key: value for key, value in call.data.items() if key != ATTR_ENTITY_ID - } - if entity_ids := call.data.get(ATTR_ENTITY_ID): - target_devices = [ - dev - for dev in hass.data[LIGHT_DATA_KEY].values() - if dev.entity_id in entity_ids - ] - else: - target_devices = hass.data[LIGHT_DATA_KEY].values() - - update_tasks = [] - for target_device in target_devices: - if not hasattr(target_device, method.method): - continue - target_device.async_set_context(call.context) - await getattr(target_device, method.method)(**params) - update_tasks.append( - asyncio.create_task(target_device.async_update_ha_state(True)) - ) - - if update_tasks: - await asyncio.wait(update_tasks) - - for xiaomi_miio_service, method in LIGHT_SERVICE_TO_METHOD.items(): - schema = method.schema or XIAOMI_MIIO_SERVICE_SCHEMA - hass.services.async_register( - DOMAIN, xiaomi_miio_service, async_service_handler, schema=schema - ) - - -def _async_setup_switch_services(hass: HomeAssistant) -> None: - """Set up Xiaomi Miio switch services.""" - hass.data.setdefault(SWITCH_DATA_KEY, {}) - - async def async_service_handler(call: ServiceCall) -> None: - """Map services to methods on XiaomiPlugGenericSwitch.""" - method = SWITCH_SERVICE_TO_METHOD[call.service] - params = { - key: value for key, value in call.data.items() if key != ATTR_ENTITY_ID - } - if entity_ids := call.data.get(ATTR_ENTITY_ID): - devices = [ - device - for device in hass.data[SWITCH_DATA_KEY].values() - if device.entity_id in entity_ids - ] - else: - devices = hass.data[SWITCH_DATA_KEY].values() - - update_tasks = [] - for device in devices: - if not hasattr(device, method.method): - continue - device.async_set_context(call.context) - await getattr(device, method.method)(**params) - update_tasks.append(asyncio.create_task(device.async_update_ha_state(True))) - - if update_tasks: - await asyncio.wait(update_tasks) - - for plug_service, method in SWITCH_SERVICE_TO_METHOD.items(): - schema = method.schema or SWITCH_SERVICE_SCHEMA - hass.services.async_register( - DOMAIN, plug_service, async_service_handler, schema=schema - ) - - -def _async_setup_fan_services(hass: HomeAssistant) -> None: - """Set up Xiaomi Miio fan services.""" - hass.data.setdefault(FAN_DATA_KEY, {}) - - async def async_service_handler(call: ServiceCall) -> None: - """Map services to methods on XiaomiAirPurifier.""" - method = FAN_SERVICE_TO_METHOD[call.service] - params = { - key: value for key, value in call.data.items() if key != ATTR_ENTITY_ID - } - if entity_ids := call.data.get(ATTR_ENTITY_ID): - filtered_entities = [ - entity - for entity in hass.data[FAN_DATA_KEY].values() - if entity.entity_id in entity_ids - ] - else: - filtered_entities = hass.data[FAN_DATA_KEY].values() - - update_tasks = [] - - for entity in filtered_entities: - entity_method = getattr(entity, method.method, None) - if not entity_method: - continue - entity.async_set_context(call.context) - await entity_method(**params) - update_tasks.append(asyncio.create_task(entity.async_update_ha_state(True))) - - if update_tasks: - await asyncio.wait(update_tasks) - - for air_purifier_service, method in FAN_SERVICE_TO_METHOD.items(): - schema = method.schema or FAN_SERVICE_SCHEMA - hass.services.async_register( - DOMAIN, air_purifier_service, async_service_handler, schema=schema - ) diff --git a/homeassistant/components/xiaomi_miio/services.yaml b/homeassistant/components/xiaomi_miio/services.yaml index dd53e3f98df0..60c4e0e67599 100644 --- a/homeassistant/components/xiaomi_miio/services.yaml +++ b/homeassistant/components/xiaomi_miio/services.yaml @@ -1,18 +1,15 @@ fan_reset_filter: - fields: - entity_id: - selector: - entity: - integration: xiaomi_miio - domain: fan + target: + entity: + integration: xiaomi_miio + domain: fan fan_set_extra_features: + target: + entity: + integration: xiaomi_miio + domain: fan fields: - entity_id: - selector: - entity: - integration: xiaomi_miio - domain: fan features: required: true selector: @@ -46,52 +43,40 @@ light_set_delayed_turn_off: object: light_reminder_on: - fields: - entity_id: - selector: - entity: - integration: xiaomi_miio - domain: light + target: + entity: + integration: xiaomi_miio + domain: light light_reminder_off: - fields: - entity_id: - selector: - entity: - integration: xiaomi_miio - domain: light + target: + entity: + integration: xiaomi_miio + domain: light light_night_light_mode_on: - fields: - entity_id: - selector: - entity: - integration: xiaomi_miio - domain: light + target: + entity: + integration: xiaomi_miio + domain: light light_night_light_mode_off: - fields: - entity_id: - selector: - entity: - integration: xiaomi_miio - domain: light + target: + entity: + integration: xiaomi_miio + domain: light light_eyecare_mode_on: - fields: - entity_id: - selector: - entity: - integration: xiaomi_miio - domain: light + target: + entity: + integration: xiaomi_miio + domain: light light_eyecare_mode_off: - fields: - entity_id: - selector: - entity: - integration: xiaomi_miio - domain: light + target: + entity: + integration: xiaomi_miio + domain: light remote_learn_command: target: @@ -151,12 +136,11 @@ switch_set_power_price: max: 999 switch_set_power_mode: + target: + entity: + integration: xiaomi_miio + domain: switch fields: - entity_id: - selector: - entity: - integration: xiaomi_miio - domain: switch mode: required: true selector: diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json index 0bd2bcec7af6..2fb6ed87638a 100644 --- a/homeassistant/components/xiaomi_miio/strings.json +++ b/homeassistant/components/xiaomi_miio/strings.json @@ -331,21 +331,11 @@ "services": { "fan_reset_filter": { "description": "Resets the filter lifetime and usage.", - "fields": { - "entity_id": { - "description": "Name of the Xiaomi Home entity.", - "name": "Entity ID" - } - }, "name": "Fan reset filter" }, "fan_set_extra_features": { "description": "Manipulates a storage register which advertises extra features. The Mi Home app evaluates the value. A feature called \"turbo mode\" is unlocked in the app on value 1.", "fields": { - "entity_id": { - "description": "[%key:component::xiaomi_miio::services::fan_reset_filter::fields::entity_id::description%]", - "name": "Entity ID" - }, "features": { "description": "Integer, known values are 0 (default) and 1 (turbo mode).", "name": "Features" @@ -355,62 +345,26 @@ }, "light_eyecare_mode_off": { "description": "Turns off the eyecare mode of a light (EYECARE SMART LAMP 2 ONLY).", - "fields": { - "entity_id": { - "description": "[%key:component::xiaomi_miio::services::light_reminder_on::fields::entity_id::description%]", - "name": "Entity ID" - } - }, "name": "Light eyecare mode off" }, "light_eyecare_mode_on": { "description": "Turns on the eyecare mode of a light (EYECARE SMART LAMP 2 ONLY).", - "fields": { - "entity_id": { - "description": "[%key:component::xiaomi_miio::services::light_reminder_on::fields::entity_id::description%]", - "name": "Entity ID" - } - }, "name": "Light eyecare mode on" }, "light_night_light_mode_off": { "description": "Turns off the night light mode of a light (EYECARE SMART LAMP 2 ONLY).", - "fields": { - "entity_id": { - "description": "[%key:component::xiaomi_miio::services::light_reminder_on::fields::entity_id::description%]", - "name": "Entity ID" - } - }, "name": "Light night light mode off" }, "light_night_light_mode_on": { "description": "Turns on the night light mode of a light (EYECARE SMART LAMP 2 ONLY).", - "fields": { - "entity_id": { - "description": "[%key:component::xiaomi_miio::services::light_reminder_on::fields::entity_id::description%]", - "name": "Entity ID" - } - }, "name": "Light night light mode on" }, "light_reminder_off": { "description": "Disables the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY).", - "fields": { - "entity_id": { - "description": "[%key:component::xiaomi_miio::services::light_reminder_on::fields::entity_id::description%]", - "name": "Entity ID" - } - }, "name": "Light reminder off" }, "light_reminder_on": { "description": "Enables the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY).", - "fields": { - "entity_id": { - "description": "Name of the entity to act on.", - "name": "Entity ID" - } - }, "name": "Light reminder on" }, "light_set_delayed_turn_off": { @@ -458,10 +412,6 @@ "switch_set_power_mode": { "description": "Sets the power mode.", "fields": { - "entity_id": { - "description": "[%key:component::xiaomi_miio::services::fan_reset_filter::fields::entity_id::description%]", - "name": "Entity ID" - }, "mode": { "description": "Power mode.", "name": "[%key:common::config_flow::data::mode%]" diff --git a/homeassistant/components/xiaomi_miio/switch.py b/homeassistant/components/xiaomi_miio/switch.py index be3a7d2e9371..57d5dd440a7d 100644 --- a/homeassistant/components/xiaomi_miio/switch.py +++ b/homeassistant/components/xiaomi_miio/switch.py @@ -109,7 +109,6 @@ from .const import ( MODELS_PURIFIER_MIIO, MODELS_PURIFIER_MIOT, SUCCESS, - SWITCH_DATA_KEY as DATA_KEY, ) from .coordinator import GatewayDeviceCoordinator from .entity import XiaomiCoordinatedMiioEntity, XiaomiGatewayDevice, XiaomiMiioEntity @@ -334,9 +333,6 @@ async def async_setup_coordinated_entry( device = config_entry.runtime_data.device coordinator = config_entry.runtime_data.device_coordinator - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {} - device_features = 0 if model in MODEL_TO_FEATURES_MAP: @@ -399,8 +395,6 @@ async def async_setup_other_entry( and model == "lumi.acpartner.v3" ): device: SwitchEntity - if DATA_KEY not in hass.data: - hass.data[DATA_KEY] = {} _LOGGER.debug("Initializing with host %s (token %s...)", host, token[:5]) @@ -418,12 +412,10 @@ async def async_setup_other_entry( name, chuangmi_plug, config_entry, unique_id_ch, channel_usb ) entities.append(device) - hass.data[DATA_KEY][host] = device elif model in ["qmi.powerstrip.v1", "zimi.powerstrip.v2"]: power_strip = PowerStrip(host, token, model=model) device = XiaomiPowerStripSwitch(name, power_strip, config_entry, unique_id) entities.append(device) - hass.data[DATA_KEY][host] = device elif model in [ "chuangmi.plug.m1", "chuangmi.plug.m3", @@ -436,14 +428,12 @@ async def async_setup_other_entry( name, chuangmi_plug, config_entry, unique_id ) entities.append(device) - hass.data[DATA_KEY][host] = device elif model == "lumi.acpartner.v3": ac_companion = AirConditioningCompanionV3(host, token) device = XiaomiAirConditioningCompanionSwitch( name, ac_companion, config_entry, unique_id ) entities.append(device) - hass.data[DATA_KEY][host] = device else: _LOGGER.error( ( diff --git a/tests/components/xiaomi_miio/test_services.py b/tests/components/xiaomi_miio/test_services.py index 898f66090182..9bf03af1b6f7 100644 --- a/tests/components/xiaomi_miio/test_services.py +++ b/tests/components/xiaomi_miio/test_services.py @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_MAC, CONF_MODEL, CONF_TOKEN, + ENTITY_MATCH_ALL, Platform, ) from homeassistant.core import Context, HomeAssistant @@ -29,6 +30,7 @@ CEILING_MODEL = "philips.light.ceiling" EYECARE_MODEL = "philips.light.sread1" CEILING_ENTITY_ID = "light.test_light" EYECARE_ENTITY_ID = "light.test_light_eyecare" +AMBIENT_ENTITY_ID = "light.test_light_eyecare_ambient_light" @pytest.fixture(name="mock_light") @@ -114,14 +116,10 @@ async def test_entity_service_forwards_context( @pytest.mark.usefixtures("mock_light") -async def test_legacy_service_forwards_context( +async def test_partially_implemented_service_forwards_context( hass: HomeAssistant, mock_light: MagicMock ) -> None: - """Test a service kept in-line attributes the caller. - - These services cannot use the entity service helper because the entities - sharing the data key only partially implement the methods. - """ + """Test a service only some entities implement attributes the caller.""" await setup_light(hass, EYECARE_MODEL, "Test Light Eyecare") context = Context() @@ -137,3 +135,29 @@ async def test_legacy_service_forwards_context( state = hass.states.get(EYECARE_ENTITY_ID) assert state.attributes["eyecare_mode"] is True assert state.context is context + + +@pytest.mark.usefixtures("mock_light") +async def test_service_skips_entities_without_the_method( + hass: HomeAssistant, mock_light: MagicMock +) -> None: + """Test entities not implementing the method are skipped, not an error.""" + await setup_light(hass, EYECARE_MODEL, "Test Light Eyecare") + + # The ambient light is on the same platform but has no async_set_scene + await hass.services.async_call( + DOMAIN, + SERVICE_SET_SCENE, + {"entity_id": AMBIENT_ENTITY_ID, "scene": 2}, + blocking=True, + ) + mock_light.set_scene.assert_not_called() + + # Targeting every entity reaches the eyecare lamp and skips the rest + await hass.services.async_call( + DOMAIN, + SERVICE_SET_SCENE, + {"entity_id": ENTITY_MATCH_ALL, "scene": 2}, + blocking=True, + ) + mock_light.set_scene.assert_called_once_with(2)