mirror of
https://github.com/home-assistant/core.git
synced 2026-08-19 21:01:05 +01:00
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:
@@ -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(
|
||||
(
|
||||
|
||||
@@ -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(
|
||||
(
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user