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)