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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DQdoLT4SC1w5LUgDQA3vEq
This commit is contained in:
Claude
2026-08-19 09:34:06 +00:00
parent c377df6e95
commit 8a84eb48cb
8 changed files with 143 additions and 314 deletions
@@ -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"
@@ -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)
@@ -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(
(
+77 -173
View File
@@ -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
)
@@ -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:
@@ -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%]"
@@ -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(
(
+30 -6
View File
@@ -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)