diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index ed74cde47e1..216261e3011 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -1,17 +1,18 @@ """Support for Zigbee Home Automation devices.""" import contextlib -import copy import logging -import re import voluptuous as vol -from zhaquirks import setup as setup_quirks +from zha.application.const import BAUD_RATES, RadioType +from zha.application.gateway import Gateway +from zha.application.helpers import ZHAData +from zha.zigbee.device import get_device_automation_triggers from zigpy.config import CONF_DATABASE, CONF_DEVICE, CONF_DEVICE_PATH from zigpy.exceptions import NetworkSettingsInconsistent, TransientConnectionError from homeassistant.config_entries import ConfigEntry -from homeassistant.const import CONF_TYPE, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_TYPE, EVENT_HOMEASSISTANT_STOP, Platform from homeassistant.core import Event, HomeAssistant from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr @@ -20,9 +21,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import ConfigType from . import repairs, websocket_api -from .core import ZHAGateway -from .core.const import ( - BAUD_RATES, +from .const import ( CONF_BAUDRATE, CONF_CUSTOM_QUIRKS_PATH, CONF_DEVICE_CONFIG, @@ -33,13 +32,14 @@ from .core.const import ( CONF_ZIGPY, DATA_ZHA, DOMAIN, - PLATFORMS, - SIGNAL_ADD_ENTITIES, - RadioType, ) -from .core.device import get_device_automation_triggers -from .core.discovery import GROUP_PROBE -from .core.helpers import ZHAData, get_zha_data +from .helpers import ( + SIGNAL_ADD_ENTITIES, + HAZHAData, + ZHAGatewayProxy, + create_zha_config, + get_zha_data, +) from .radio_manager import ZhaRadioManager from .repairs.network_settings_inconsistent import warn_on_inconsistent_network_settings from .repairs.wrong_silabs_firmware import ( @@ -74,6 +74,25 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) +PLATFORMS = ( + Platform.ALARM_CONTROL_PANEL, + Platform.BINARY_SENSOR, + Platform.BUTTON, + Platform.CLIMATE, + Platform.COVER, + Platform.DEVICE_TRACKER, + Platform.FAN, + Platform.LIGHT, + Platform.LOCK, + Platform.NUMBER, + Platform.SELECT, + Platform.SENSOR, + Platform.SIREN, + Platform.SWITCH, + Platform.UPDATE, +) + + # Zigbee definitions CENTICELSIUS = "C-100" @@ -83,49 +102,20 @@ _LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up ZHA from config.""" - zha_data = ZHAData() - zha_data.yaml_config = config.get(DOMAIN, {}) - hass.data[DATA_ZHA] = zha_data + ha_zha_data = HAZHAData(yaml_config=config.get(DOMAIN, {})) + hass.data[DATA_ZHA] = ha_zha_data return True -def _clean_serial_port_path(path: str) -> str: - """Clean the serial port path, applying corrections where necessary.""" - - if path.startswith("socket://"): - path = path.strip() - - # Removes extraneous brackets from IP addresses (they don't parse in CPython 3.11.4) - if re.match(r"^socket://\[\d+\.\d+\.\d+\.\d+\]:\d+$", path): - path = path.replace("[", "").replace("]", "") - - return path - - async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up ZHA. Will automatically load components to support devices found on the network. """ - - # Remove brackets around IP addresses, this no longer works in CPython 3.11.4 - # This will be removed in 2023.11.0 - path = config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] - cleaned_path = _clean_serial_port_path(path) - data = copy.deepcopy(dict(config_entry.data)) - - if path != cleaned_path: - _LOGGER.debug("Cleaned serial port path %r -> %r", path, cleaned_path) - data[CONF_DEVICE][CONF_DEVICE_PATH] = cleaned_path - hass.config_entries.async_update_entry(config_entry, data=data) - - zha_data = get_zha_data(hass) - - if zha_data.yaml_config.get(CONF_ENABLE_QUIRKS, True): - await hass.async_add_import_executor_job( - setup_quirks, zha_data.yaml_config.get(CONF_CUSTOM_QUIRKS_PATH) - ) + ha_zha_data: HAZHAData = get_zha_data(hass) + ha_zha_data.config_entry = config_entry + zha_lib_data: ZHAData = create_zha_config(hass, ha_zha_data) # Load and cache device trigger information early device_registry = dr.async_get(hass) @@ -141,19 +131,16 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b if dev_entry is None: continue - zha_data.device_trigger_cache[dev_entry.id] = ( + zha_lib_data.device_trigger_cache[dev_entry.id] = ( str(dev.ieee), get_device_automation_triggers(dev), ) + ha_zha_data.device_trigger_cache = zha_lib_data.device_trigger_cache - _LOGGER.debug("Trigger cache: %s", zha_data.device_trigger_cache) + _LOGGER.debug("Trigger cache: %s", zha_lib_data.device_trigger_cache) try: - zha_gateway = await ZHAGateway.async_from_config( - hass=hass, - config=zha_data.yaml_config, - config_entry=config_entry, - ) + zha_gateway = await Gateway.async_from_config(zha_lib_data) except NetworkSettingsInconsistent as exc: await warn_on_inconsistent_network_settings( hass, @@ -185,6 +172,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b repairs.async_delete_blocking_issues(hass) + ha_zha_data.gateway_proxy = ZHAGatewayProxy(hass, config_entry, zha_gateway) + manufacturer = zha_gateway.state.node_info.manufacturer model = zha_gateway.state.node_info.model @@ -205,13 +194,15 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b websocket_api.async_load_api(hass) async def async_shutdown(_: Event) -> None: - await zha_gateway.shutdown() + """Handle shutdown tasks.""" + assert ha_zha_data.gateway_proxy is not None + await ha_zha_data.gateway_proxy.shutdown() config_entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_shutdown) ) - await zha_gateway.async_initialize_devices_and_entities() + await ha_zha_data.gateway_proxy.async_initialize_devices_and_entities() await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) async_dispatcher_send(hass, SIGNAL_ADD_ENTITIES) return True @@ -219,11 +210,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Unload ZHA config entry.""" - zha_data = get_zha_data(hass) + ha_zha_data = get_zha_data(hass) + ha_zha_data.config_entry = None - if zha_data.gateway is not None: - await zha_data.gateway.shutdown() - zha_data.gateway = None + if ha_zha_data.gateway_proxy is not None: + await ha_zha_data.gateway_proxy.shutdown() + ha_zha_data.gateway_proxy = None # clean up any remaining entity metadata # (entities that have been discovered but not yet added to HA) @@ -231,15 +223,11 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> # be in when we get here in failure cases with contextlib.suppress(KeyError): for platform in PLATFORMS: - del zha_data.platforms[platform] + del ha_zha_data.platforms[platform] - GROUP_PROBE.cleanup() websocket_api.async_unload_api(hass) - # our components don't have unload methods so no need to look at return values - await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) - - return True + return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/zha/alarm_control_panel.py b/homeassistant/components/zha/alarm_control_panel.py index 7750e7f280d..c54d7c7ab2d 100644 --- a/homeassistant/components/zha/alarm_control_panel.py +++ b/homeassistant/components/zha/alarm_control_panel.py @@ -3,9 +3,6 @@ from __future__ import annotations import functools -from typing import TYPE_CHECKING - -from zigpy.zcl.clusters.security import IasAce from homeassistant.components.alarm_control_panel import ( AlarmControlPanelEntity, @@ -13,50 +10,18 @@ from homeassistant.components.alarm_control_panel import ( CodeFormat, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, - STATE_ALARM_ARMED_NIGHT, - STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED, - Platform, -) -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .core import discovery -from .core.cluster_handlers.security import ( - SIGNAL_ALARM_TRIGGERED, - SIGNAL_ARMED_STATE_CHANGED, - IasAceClusterHandler, -) -from .core.const import ( - CLUSTER_HANDLER_IAS_ACE, - CONF_ALARM_ARM_REQUIRES_CODE, - CONF_ALARM_FAILED_TRIES, - CONF_ALARM_MASTER_CODE, +from .entity import ZHAEntity +from .helpers import ( SIGNAL_ADD_ENTITIES, - ZHA_ALARM_OPTIONS, + async_add_entities as zha_async_add_entities, + convert_zha_error_to_ha_error, + get_zha_data, ) -from .core.helpers import async_get_zha_config_value, get_zha_data -from .core.registries import ZHA_ENTITIES -from .entity import ZhaEntity - -if TYPE_CHECKING: - from .core.device import ZHADevice - -STRICT_MATCH = functools.partial( - ZHA_ENTITIES.strict_match, Platform.ALARM_CONTROL_PANEL -) - -IAS_ACE_STATE_MAP = { - IasAce.PanelStatus.Panel_Disarmed: STATE_ALARM_DISARMED, - IasAce.PanelStatus.Armed_Stay: STATE_ALARM_ARMED_HOME, - IasAce.PanelStatus.Armed_Night: STATE_ALARM_ARMED_NIGHT, - IasAce.PanelStatus.Armed_Away: STATE_ALARM_ARMED_AWAY, - IasAce.PanelStatus.In_Alarm: STATE_ALARM_TRIGGERED, -} async def async_setup_entry( @@ -72,14 +37,16 @@ async def async_setup_entry( hass, SIGNAL_ADD_ENTITIES, functools.partial( - discovery.async_add_entities, async_add_entities, entities_to_create + zha_async_add_entities, + async_add_entities, + ZHAAlarmControlPanel, + entities_to_create, ), ) config_entry.async_on_unload(unsub) -@STRICT_MATCH(cluster_handler_names=CLUSTER_HANDLER_IAS_ACE) -class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity): +class ZHAAlarmControlPanel(ZHAEntity, AlarmControlPanelEntity): """Entity for ZHA alarm control devices.""" _attr_translation_key: str = "alarm_control_panel" @@ -91,68 +58,42 @@ class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity): | AlarmControlPanelEntityFeature.TRIGGER ) - def __init__( - self, unique_id, zha_device: ZHADevice, cluster_handlers, **kwargs - ) -> None: - """Initialize the ZHA alarm control device.""" - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - cfg_entry = zha_device.gateway.config_entry - self._cluster_handler: IasAceClusterHandler = cluster_handlers[0] - self._cluster_handler.panel_code = async_get_zha_config_value( - cfg_entry, ZHA_ALARM_OPTIONS, CONF_ALARM_MASTER_CODE, "1234" - ) - self._cluster_handler.code_required_arm_actions = async_get_zha_config_value( - cfg_entry, ZHA_ALARM_OPTIONS, CONF_ALARM_ARM_REQUIRES_CODE, False - ) - self._cluster_handler.max_invalid_tries = async_get_zha_config_value( - cfg_entry, ZHA_ALARM_OPTIONS, CONF_ALARM_FAILED_TRIES, 3 - ) - - async def async_added_to_hass(self) -> None: - """Run when about to be added to hass.""" - await super().async_added_to_hass() - self.async_accept_signal( - self._cluster_handler, SIGNAL_ARMED_STATE_CHANGED, self.async_set_armed_mode - ) - self.async_accept_signal( - self._cluster_handler, SIGNAL_ALARM_TRIGGERED, self.async_alarm_trigger - ) - - @callback - def async_set_armed_mode(self) -> None: - """Set the entity state.""" - self.async_write_ha_state() - @property def code_arm_required(self) -> bool: """Whether the code is required for arm actions.""" - return self._cluster_handler.code_required_arm_actions + return self.entity_data.entity.code_arm_required + @convert_zha_error_to_ha_error async def async_alarm_disarm(self, code: str | None = None) -> None: """Send disarm command.""" - self._cluster_handler.arm(IasAce.ArmMode.Disarm, code, 0) + await self.entity_data.entity.async_alarm_disarm(code) self.async_write_ha_state() + @convert_zha_error_to_ha_error async def async_alarm_arm_home(self, code: str | None = None) -> None: """Send arm home command.""" - self._cluster_handler.arm(IasAce.ArmMode.Arm_Day_Home_Only, code, 0) + await self.entity_data.entity.async_alarm_arm_home(code) self.async_write_ha_state() + @convert_zha_error_to_ha_error async def async_alarm_arm_away(self, code: str | None = None) -> None: """Send arm away command.""" - self._cluster_handler.arm(IasAce.ArmMode.Arm_All_Zones, code, 0) + await self.entity_data.entity.async_alarm_arm_away(code) self.async_write_ha_state() + @convert_zha_error_to_ha_error async def async_alarm_arm_night(self, code: str | None = None) -> None: """Send arm night command.""" - self._cluster_handler.arm(IasAce.ArmMode.Arm_Night_Sleep_Only, code, 0) + await self.entity_data.entity.async_alarm_arm_night(code) self.async_write_ha_state() + @convert_zha_error_to_ha_error async def async_alarm_trigger(self, code: str | None = None) -> None: """Send alarm trigger command.""" + await self.entity_data.entity.async_alarm_trigger(code) self.async_write_ha_state() @property def state(self) -> str | None: """Return the state of the entity.""" - return IAS_ACE_STATE_MAP.get(self._cluster_handler.armed_state) + return self.entity_data.entity.state["state"] diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index db0658eb632..60960a3e9fc 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -4,13 +4,14 @@ from __future__ import annotations from typing import TYPE_CHECKING, Literal +from zha.application.const import RadioType from zigpy.backups import NetworkBackup from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH from zigpy.types import Channels from zigpy.util import pick_optimal_channel -from .core.const import CONF_RADIO_TYPE, DOMAIN, RadioType -from .core.helpers import get_zha_gateway +from .const import CONF_RADIO_TYPE, DOMAIN +from .helpers import get_zha_data, get_zha_gateway from .radio_manager import ZhaRadioManager if TYPE_CHECKING: @@ -22,14 +23,12 @@ def _get_config_entry(hass: HomeAssistant) -> ConfigEntry: """Find the singleton ZHA config entry, if one exists.""" # If ZHA is already running, use its config entry - try: - zha_gateway = get_zha_gateway(hass) - except ValueError: - pass - else: - return zha_gateway.config_entry + zha_data = get_zha_data(hass) - # Otherwise, find one + if zha_data.config_entry is not None: + return zha_data.config_entry + + # Otherwise, find an inactive one entries = hass.config_entries.async_entries(DOMAIN) if len(entries) != 1: diff --git a/homeassistant/components/zha/backup.py b/homeassistant/components/zha/backup.py index e31ae09eeb6..a3d9090eaba 100644 --- a/homeassistant/components/zha/backup.py +++ b/homeassistant/components/zha/backup.py @@ -4,7 +4,7 @@ import logging from homeassistant.core import HomeAssistant -from .core.helpers import get_zha_gateway +from .helpers import get_zha_gateway _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index bdd2fd03ca0..f45ebf0c5a5 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -3,58 +3,24 @@ from __future__ import annotations import functools -import logging - -from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT -from zigpy.quirks.v2 import BinarySensorMetadata -import zigpy.types as t -from zigpy.zcl.clusters.general import OnOff -from zigpy.zcl.clusters.security import IasZone from homeassistant.components.binary_sensor import ( BinarySensorDeviceClass, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_ON, EntityCategory, Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .core import discovery -from .core.const import ( - CLUSTER_HANDLER_ACCELEROMETER, - CLUSTER_HANDLER_BINARY_INPUT, - CLUSTER_HANDLER_HUE_OCCUPANCY, - CLUSTER_HANDLER_OCCUPANCY, - CLUSTER_HANDLER_ON_OFF, - CLUSTER_HANDLER_THERMOSTAT, - CLUSTER_HANDLER_ZONE, - ENTITY_METADATA, +from .entity import ZHAEntity +from .helpers import ( SIGNAL_ADD_ENTITIES, - SIGNAL_ATTR_UPDATED, + EntityData, + async_add_entities as zha_async_add_entities, + get_zha_data, ) -from .core.helpers import get_zha_data, validate_device_class -from .core.registries import ZHA_ENTITIES -from .entity import ZhaEntity - -# Zigbee Cluster Library Zone Type to Home Assistant device class -IAS_ZONE_CLASS_MAPPING = { - IasZone.ZoneType.Motion_Sensor: BinarySensorDeviceClass.MOTION, - IasZone.ZoneType.Contact_Switch: BinarySensorDeviceClass.OPENING, - IasZone.ZoneType.Fire_Sensor: BinarySensorDeviceClass.SMOKE, - IasZone.ZoneType.Water_Sensor: BinarySensorDeviceClass.MOISTURE, - IasZone.ZoneType.Carbon_Monoxide_Sensor: BinarySensorDeviceClass.GAS, - IasZone.ZoneType.Vibration_Movement_Sensor: BinarySensorDeviceClass.VIBRATION, -} - -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.BINARY_SENSOR) -MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.BINARY_SENSOR) -CONFIG_DIAGNOSTIC_MATCH = functools.partial( - ZHA_ENTITIES.config_diagnostic_match, Platform.BINARY_SENSOR -) - -_LOGGER = logging.getLogger(__name__) async def async_setup_entry( @@ -70,312 +36,24 @@ async def async_setup_entry( hass, SIGNAL_ADD_ENTITIES, functools.partial( - discovery.async_add_entities, async_add_entities, entities_to_create + zha_async_add_entities, async_add_entities, BinarySensor, entities_to_create ), ) config_entry.async_on_unload(unsub) -class BinarySensor(ZhaEntity, BinarySensorEntity): +class BinarySensor(ZHAEntity, BinarySensorEntity): """ZHA BinarySensor.""" - _attribute_name: str - - def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs) -> None: + def __init__(self, entity_data: EntityData) -> None: """Initialize the ZHA binary sensor.""" - self._cluster_handler = cluster_handlers[0] - if ENTITY_METADATA in kwargs: - self._init_from_quirks_metadata(kwargs[ENTITY_METADATA]) - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - - def _init_from_quirks_metadata(self, entity_metadata: BinarySensorMetadata) -> None: - """Init this entity from the quirks metadata.""" - super()._init_from_quirks_metadata(entity_metadata) - self._attribute_name = entity_metadata.attribute_name - if entity_metadata.device_class is not None: - self._attr_device_class = validate_device_class( - BinarySensorDeviceClass, - entity_metadata.device_class, - Platform.BINARY_SENSOR.value, - _LOGGER, + super().__init__(entity_data) + if self.entity_data.entity.info_object.device_class is not None: + self._attr_device_class = BinarySensorDeviceClass( + self.entity_data.entity.info_object.device_class ) - async def async_added_to_hass(self) -> None: - """Run when about to be added to hass.""" - await super().async_added_to_hass() - self.async_accept_signal( - self._cluster_handler, SIGNAL_ATTR_UPDATED, self.async_set_state - ) - @property def is_on(self) -> bool: """Return True if the switch is on based on the state machine.""" - raw_state = self._cluster_handler.cluster.get(self._attribute_name) - if raw_state is None: - return False - return self.parse(raw_state) - - @callback - def async_set_state(self, attr_id, attr_name, value): - """Set the state.""" - self.async_write_ha_state() - - @staticmethod - def parse(value: bool | int) -> bool: - """Parse the raw attribute into a bool state.""" - return bool(value) - - -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ACCELEROMETER) -class Accelerometer(BinarySensor): - """ZHA BinarySensor.""" - - _attribute_name = "acceleration" - _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.MOVING - _attr_translation_key: str = "accelerometer" - - -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_OCCUPANCY) -class Occupancy(BinarySensor): - """ZHA BinarySensor.""" - - _attribute_name = "occupancy" - _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.OCCUPANCY - - -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_HUE_OCCUPANCY) -class HueOccupancy(Occupancy): - """ZHA Hue occupancy.""" - - _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.OCCUPANCY - - -@STRICT_MATCH(cluster_handler_names=CLUSTER_HANDLER_ON_OFF) -class Opening(BinarySensor): - """ZHA OnOff BinarySensor.""" - - _attribute_name = "on_off" - _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.OPENING - - # Client/out cluster attributes aren't stored in the zigpy database, but are properly stored in the runtime cache. - # We need to manually restore the last state from the sensor state to the runtime cache for now. - @callback - def async_restore_last_state(self, last_state): - """Restore previous state to zigpy cache.""" - self._cluster_handler.cluster.update_attribute( - OnOff.attributes_by_name[self._attribute_name].id, - t.Bool.true if last_state.state == STATE_ON else t.Bool.false, - ) - - -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_BINARY_INPUT) -class BinaryInput(BinarySensor): - """ZHA BinarySensor.""" - - _attribute_name = "present_value" - _attr_translation_key: str = "binary_input" - - -@STRICT_MATCH( - cluster_handler_names=CLUSTER_HANDLER_ON_OFF, - manufacturers="IKEA of Sweden", - models=lambda model: isinstance(model, str) - and model is not None - and model.find("motion") != -1, -) -@STRICT_MATCH( - cluster_handler_names=CLUSTER_HANDLER_ON_OFF, - manufacturers="Philips", - models={"SML001", "SML002"}, -) -class Motion(Opening): - """ZHA OnOff BinarySensor with motion device class.""" - - _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.MOTION - - -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ZONE) -class IASZone(BinarySensor): - """ZHA IAS BinarySensor.""" - - _attribute_name = "zone_status" - - @property - def translation_key(self) -> str | None: - """Return the name of the sensor.""" - zone_type = self._cluster_handler.cluster.get("zone_type") - if zone_type in IAS_ZONE_CLASS_MAPPING: - return None - return "ias_zone" - - @property - def device_class(self) -> BinarySensorDeviceClass | None: - """Return device class from component DEVICE_CLASSES.""" - zone_type = self._cluster_handler.cluster.get("zone_type") - return IAS_ZONE_CLASS_MAPPING.get(zone_type) - - @staticmethod - def parse(value: bool | int) -> bool: - """Parse the raw attribute into a bool state.""" - return BinarySensor.parse(value & 3) # use only bit 0 and 1 for alarm state - - -@STRICT_MATCH(cluster_handler_names=CLUSTER_HANDLER_ZONE, models={"WL4200", "WL4200S"}) -class SinopeLeakStatus(BinarySensor): - """Sinope water leak sensor.""" - - _attribute_name = "leak_status" - _attr_device_class = BinarySensorDeviceClass.MOISTURE - - -@MULTI_MATCH( - cluster_handler_names="tuya_manufacturer", - manufacturers={ - "_TZE200_htnnfasr", - }, -) -class FrostLock(BinarySensor): - """ZHA BinarySensor.""" - - _attribute_name = "frost_lock" - _unique_id_suffix = "frost_lock" - _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.LOCK - _attr_translation_key: str = "frost_lock" - - -@MULTI_MATCH(cluster_handler_names="ikea_airpurifier") -class ReplaceFilter(BinarySensor): - """ZHA BinarySensor.""" - - _attribute_name = "replace_filter" - _unique_id_suffix = "replace_filter" - _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.PROBLEM - _attr_entity_category: EntityCategory = EntityCategory.DIAGNOSTIC - _attr_translation_key: str = "replace_filter" - - -@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"}) -class AqaraPetFeederErrorDetected(BinarySensor): - """ZHA aqara pet feeder error detected binary sensor.""" - - _attribute_name = "error_detected" - _unique_id_suffix = "error_detected" - _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.PROBLEM - - -@MULTI_MATCH( - cluster_handler_names="opple_cluster", - models={"lumi.plug.mmeu01", "lumi.plug.maeu01"}, -) -class XiaomiPlugConsumerConnected(BinarySensor): - """ZHA Xiaomi plug consumer connected binary sensor.""" - - _attribute_name = "consumer_connected" - _unique_id_suffix = "consumer_connected" - _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.PLUG - _attr_translation_key: str = "consumer_connected" - - -@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"lumi.airrtc.agl001"}) -class AqaraThermostatWindowOpen(BinarySensor): - """ZHA Aqara thermostat window open binary sensor.""" - - _attribute_name = "window_open" - _unique_id_suffix = "window_open" - _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.WINDOW - - -@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"lumi.airrtc.agl001"}) -class AqaraThermostatValveAlarm(BinarySensor): - """ZHA Aqara thermostat valve alarm binary sensor.""" - - _attribute_name = "valve_alarm" - _unique_id_suffix = "valve_alarm" - _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.PROBLEM - _attr_translation_key: str = "valve_alarm" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.airrtc.agl001"} -) -class AqaraThermostatCalibrated(BinarySensor): - """ZHA Aqara thermostat calibrated binary sensor.""" - - _attribute_name = "calibrated" - _unique_id_suffix = "calibrated" - _attr_entity_category: EntityCategory = EntityCategory.DIAGNOSTIC - _attr_translation_key: str = "calibrated" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.airrtc.agl001"} -) -class AqaraThermostatExternalSensor(BinarySensor): - """ZHA Aqara thermostat external sensor binary sensor.""" - - _attribute_name = "sensor" - _unique_id_suffix = "sensor" - _attr_entity_category: EntityCategory = EntityCategory.DIAGNOSTIC - _attr_translation_key: str = "external_sensor" - - -@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"lumi.sensor_smoke.acn03"}) -class AqaraLinkageAlarmState(BinarySensor): - """ZHA Aqara linkage alarm state binary sensor.""" - - _attribute_name = "linkage_alarm_state" - _unique_id_suffix = "linkage_alarm_state" - _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.SMOKE - _attr_translation_key: str = "linkage_alarm_state" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.curtain.agl001"} -) -class AqaraE1CurtainMotorOpenedByHandBinarySensor(BinarySensor): - """Opened by hand binary sensor.""" - - _unique_id_suffix = "hand_open" - _attribute_name = "hand_open" - _attr_translation_key = "hand_open" - _attr_entity_category = EntityCategory.DIAGNOSTIC - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -class DanfossMountingModeActive(BinarySensor): - """Danfoss TRV proprietary attribute exposing whether in mounting mode.""" - - _unique_id_suffix = "mounting_mode_active" - _attribute_name = "mounting_mode_active" - _attr_translation_key: str = "mounting_mode_active" - _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.OPENING - _attr_entity_category = EntityCategory.DIAGNOSTIC - - -@MULTI_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -class DanfossHeatRequired(BinarySensor): - """Danfoss TRV proprietary attribute exposing whether heat is required.""" - - _unique_id_suffix = "heat_required" - _attribute_name = "heat_required" - _attr_translation_key: str = "heat_required" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -class DanfossPreheatStatus(BinarySensor): - """Danfoss TRV proprietary attribute exposing whether in pre-heating mode.""" - - _unique_id_suffix = "preheat_status" - _attribute_name = "preheat_status" - _attr_translation_key: str = "preheat_status" - _attr_entity_registry_enabled_default = False - _attr_entity_category = EntityCategory.DIAGNOSTIC + return self.entity_data.entity.is_on diff --git a/homeassistant/components/zha/button.py b/homeassistant/components/zha/button.py index 33102062443..ecd5cd51f61 100644 --- a/homeassistant/components/zha/button.py +++ b/homeassistant/components/zha/button.py @@ -4,33 +4,22 @@ from __future__ import annotations import functools import logging -from typing import TYPE_CHECKING, Any, Self - -from zigpy.quirks.v2 import WriteAttributeButtonMetadata, ZCLCommandButtonMetadata from homeassistant.components.button import ButtonDeviceClass, ButtonEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EntityCategory, Platform +from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .core import discovery -from .core.const import CLUSTER_HANDLER_IDENTIFY, ENTITY_METADATA, SIGNAL_ADD_ENTITIES -from .core.helpers import get_zha_data -from .core.registries import ZHA_ENTITIES -from .entity import ZhaEntity - -if TYPE_CHECKING: - from .core.cluster_handlers import ClusterHandler - from .core.device import ZHADevice - - -MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.BUTTON) -CONFIG_DIAGNOSTIC_MATCH = functools.partial( - ZHA_ENTITIES.config_diagnostic_match, Platform.BUTTON +from .entity import ZHAEntity +from .helpers import ( + SIGNAL_ADD_ENTITIES, + EntityData, + async_add_entities as zha_async_add_entities, + convert_zha_error_to_ha_error, + get_zha_data, ) -DEFAULT_DURATION = 5 # seconds _LOGGER = logging.getLogger(__name__) @@ -48,172 +37,24 @@ async def async_setup_entry( hass, SIGNAL_ADD_ENTITIES, functools.partial( - discovery.async_add_entities, - async_add_entities, - entities_to_create, + zha_async_add_entities, async_add_entities, ZHAButton, entities_to_create ), ) config_entry.async_on_unload(unsub) -class ZHAButton(ZhaEntity, ButtonEntity): +class ZHAButton(ZHAEntity, ButtonEntity): """Defines a ZHA button.""" - _command_name: str - _args: list[Any] - _kwargs: dict[str, Any] - - def __init__( - self, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> None: - """Init this button.""" - self._cluster_handler: ClusterHandler = cluster_handlers[0] - if ENTITY_METADATA in kwargs: - self._init_from_quirks_metadata(kwargs[ENTITY_METADATA]) - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - - def _init_from_quirks_metadata( - self, entity_metadata: ZCLCommandButtonMetadata - ) -> None: - """Init this entity from the quirks metadata.""" - super()._init_from_quirks_metadata(entity_metadata) - self._command_name = entity_metadata.command_name - self._args = entity_metadata.args - self._kwargs = entity_metadata.kwargs - - def get_args(self) -> list[Any]: - """Return the arguments to use in the command.""" - return list(self._args) if self._args else [] - - def get_kwargs(self) -> dict[str, Any]: - """Return the keyword arguments to use in the command.""" - return self._kwargs + def __init__(self, entity_data: EntityData) -> None: + """Initialize the ZHA binary sensor.""" + super().__init__(entity_data) + if self.entity_data.entity.info_object.device_class is not None: + self._attr_device_class = ButtonDeviceClass( + self.entity_data.entity.info_object.device_class + ) + @convert_zha_error_to_ha_error async def async_press(self) -> None: """Send out a update command.""" - command = getattr(self._cluster_handler, self._command_name) - arguments = self.get_args() or [] - kwargs = self.get_kwargs() or {} - await command(*arguments, **kwargs) - - -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_IDENTIFY) -class ZHAIdentifyButton(ZHAButton): - """Defines a ZHA identify button.""" - - @classmethod - def create_entity( - cls, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> Self | None: - """Entity Factory. - - Return entity if it is a supported configuration, otherwise return None - """ - if ZHA_ENTITIES.prevent_entity_creation( - Platform.BUTTON, zha_device.ieee, CLUSTER_HANDLER_IDENTIFY - ): - return None - return cls(unique_id, zha_device, cluster_handlers, **kwargs) - - _attr_device_class = ButtonDeviceClass.IDENTIFY - _attr_entity_category = EntityCategory.DIAGNOSTIC - _command_name = "identify" - _kwargs = {} - _args = [DEFAULT_DURATION] - - -class ZHAAttributeButton(ZhaEntity, ButtonEntity): - """Defines a ZHA button, which writes a value to an attribute.""" - - _attribute_name: str - _attribute_value: Any = None - - def __init__( - self, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> None: - """Init this button.""" - self._cluster_handler: ClusterHandler = cluster_handlers[0] - if ENTITY_METADATA in kwargs: - self._init_from_quirks_metadata(kwargs[ENTITY_METADATA]) - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - - def _init_from_quirks_metadata( - self, entity_metadata: WriteAttributeButtonMetadata - ) -> None: - """Init this entity from the quirks metadata.""" - super()._init_from_quirks_metadata(entity_metadata) - self._attribute_name = entity_metadata.attribute_name - self._attribute_value = entity_metadata.attribute_value - - async def async_press(self) -> None: - """Write attribute with defined value.""" - await self._cluster_handler.write_attributes_safe( - {self._attribute_name: self._attribute_value} - ) - self.async_write_ha_state() - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="tuya_manufacturer", - manufacturers={ - "_TZE200_htnnfasr", - }, -) -class FrostLockResetButton(ZHAAttributeButton): - """Defines a ZHA frost lock reset button.""" - - _unique_id_suffix = "reset_frost_lock" - _attribute_name = "frost_lock_reset" - _attribute_value = 0 - _attr_device_class = ButtonDeviceClass.RESTART - _attr_entity_category = EntityCategory.CONFIG - _attr_translation_key = "reset_frost_lock" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.motion.ac01"} -) -class NoPresenceStatusResetButton(ZHAAttributeButton): - """Defines a ZHA no presence status reset button.""" - - _unique_id_suffix = "reset_no_presence_status" - _attribute_name = "reset_no_presence_status" - _attribute_value = 1 - _attr_device_class = ButtonDeviceClass.RESTART - _attr_entity_category = EntityCategory.CONFIG - _attr_translation_key = "reset_no_presence_status" - - -@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"}) -class AqaraPetFeederFeedButton(ZHAAttributeButton): - """Defines a feed button for the aqara c1 pet feeder.""" - - _unique_id_suffix = "feeding" - _attribute_name = "feeding" - _attribute_value = 1 - _attr_translation_key = "feed" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.sensor_smoke.acn03"} -) -class AqaraSelfTestButton(ZHAAttributeButton): - """Defines a ZHA self-test button for Aqara smoke sensors.""" - - _unique_id_suffix = "self_test" - _attribute_name = "self_test" - _attribute_value = 1 - _attr_entity_category = EntityCategory.CONFIG - _attr_translation_key = "self_test" + await self.entity_data.entity.async_press() diff --git a/homeassistant/components/zha/climate.py b/homeassistant/components/zha/climate.py index 61c5f28ca8f..f4fb58c254a 100644 --- a/homeassistant/components/zha/climate.py +++ b/homeassistant/components/zha/climate.py @@ -6,109 +6,62 @@ at https://home-assistant.io/components/zha.climate/ from __future__ import annotations -from datetime import datetime, timedelta +from collections.abc import Mapping import functools -from random import randint from typing import Any -from zigpy.zcl.clusters.hvac import Fan as F, Thermostat as T +from zha.application.platforms.climate.const import ( + ClimateEntityFeature as ZHAClimateEntityFeature, + HVACAction as ZHAHVACAction, + HVACMode as ZHAHVACMode, +) from homeassistant.components.climate import ( ATTR_HVAC_MODE, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - FAN_AUTO, - FAN_ON, - PRESET_AWAY, - PRESET_BOOST, - PRESET_COMFORT, - PRESET_ECO, - PRESET_NONE, + ATTR_TEMPERATURE, ClimateEntity, ClimateEntityFeature, HVACAction, HVACMode, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_TEMPERATURE, - PRECISION_TENTHS, - Platform, - UnitOfTemperature, -) +from homeassistant.const import PRECISION_TENTHS, Platform, UnitOfTemperature from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_time_interval -import homeassistant.util.dt as dt_util -from .core import discovery -from .core.const import ( - CLUSTER_HANDLER_FAN, - CLUSTER_HANDLER_THERMOSTAT, - PRESET_COMPLEX, - PRESET_SCHEDULE, - PRESET_TEMP_MANUAL, +from .entity import ZHAEntity +from .helpers import ( SIGNAL_ADD_ENTITIES, - SIGNAL_ATTR_UPDATED, + EntityData, + async_add_entities as zha_async_add_entities, + convert_zha_error_to_ha_error, + exclude_none_values, + get_zha_data, ) -from .core.helpers import get_zha_data -from .core.registries import ZHA_ENTITIES -from .entity import ZhaEntity -ATTR_SYS_MODE = "system_mode" -ATTR_RUNNING_MODE = "running_mode" -ATTR_SETPT_CHANGE_SRC = "setpoint_change_source" -ATTR_SETPT_CHANGE_AMT = "setpoint_change_amount" -ATTR_OCCUPANCY = "occupancy" -ATTR_PI_COOLING_DEMAND = "pi_cooling_demand" -ATTR_PI_HEATING_DEMAND = "pi_heating_demand" -ATTR_OCCP_COOL_SETPT = "occupied_cooling_setpoint" -ATTR_OCCP_HEAT_SETPT = "occupied_heating_setpoint" -ATTR_UNOCCP_HEAT_SETPT = "unoccupied_heating_setpoint" -ATTR_UNOCCP_COOL_SETPT = "unoccupied_cooling_setpoint" - - -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.CLIMATE) -MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.CLIMATE) -RUNNING_MODE = {0x00: HVACMode.OFF, 0x03: HVACMode.COOL, 0x04: HVACMode.HEAT} - -SEQ_OF_OPERATION = { - 0x00: [HVACMode.OFF, HVACMode.COOL], # cooling only - 0x01: [HVACMode.OFF, HVACMode.COOL], # cooling with reheat - 0x02: [HVACMode.OFF, HVACMode.HEAT], # heating only - 0x03: [HVACMode.OFF, HVACMode.HEAT], # heating with reheat - # cooling and heating 4-pipes - 0x04: [HVACMode.OFF, HVACMode.HEAT_COOL, HVACMode.COOL, HVACMode.HEAT], - # cooling and heating 4-pipes - 0x05: [HVACMode.OFF, HVACMode.HEAT_COOL, HVACMode.COOL, HVACMode.HEAT], - 0x06: [HVACMode.COOL, HVACMode.HEAT, HVACMode.OFF], # centralite specific - 0x07: [HVACMode.HEAT_COOL, HVACMode.OFF], # centralite specific +ZHA_TO_HA_HVAC_MODE = { + ZHAHVACMode.OFF: HVACMode.OFF, + ZHAHVACMode.AUTO: HVACMode.AUTO, + ZHAHVACMode.HEAT: HVACMode.HEAT, + ZHAHVACMode.COOL: HVACMode.COOL, + ZHAHVACMode.HEAT_COOL: HVACMode.HEAT_COOL, + ZHAHVACMode.DRY: HVACMode.DRY, + ZHAHVACMode.FAN_ONLY: HVACMode.FAN_ONLY, } -HVAC_MODE_2_SYSTEM = { - HVACMode.OFF: T.SystemMode.Off, - HVACMode.HEAT_COOL: T.SystemMode.Auto, - HVACMode.COOL: T.SystemMode.Cool, - HVACMode.HEAT: T.SystemMode.Heat, - HVACMode.FAN_ONLY: T.SystemMode.Fan_only, - HVACMode.DRY: T.SystemMode.Dry, +ZHA_TO_HA_HVAC_ACTION = { + ZHAHVACAction.OFF: HVACAction.OFF, + ZHAHVACAction.HEATING: HVACAction.HEATING, + ZHAHVACAction.COOLING: HVACAction.COOLING, + ZHAHVACAction.DRYING: HVACAction.DRYING, + ZHAHVACAction.IDLE: HVACAction.IDLE, + ZHAHVACAction.FAN: HVACAction.FAN, + ZHAHVACAction.PREHEATING: HVACAction.PREHEATING, } -SYSTEM_MODE_2_HVAC = { - T.SystemMode.Off: HVACMode.OFF, - T.SystemMode.Auto: HVACMode.HEAT_COOL, - T.SystemMode.Cool: HVACMode.COOL, - T.SystemMode.Heat: HVACMode.HEAT, - T.SystemMode.Emergency_Heating: HVACMode.HEAT, - T.SystemMode.Pre_cooling: HVACMode.COOL, # this is 'precooling'. is it the same? - T.SystemMode.Fan_only: HVACMode.FAN_ONLY, - T.SystemMode.Dry: HVACMode.DRY, - T.SystemMode.Sleep: HVACMode.OFF, -} - -ZCL_TEMP = 100 - async def async_setup_entry( hass: HomeAssistant, @@ -118,708 +71,168 @@ async def async_setup_entry( """Set up the Zigbee Home Automation sensor from config entry.""" zha_data = get_zha_data(hass) entities_to_create = zha_data.platforms[Platform.CLIMATE] + unsub = async_dispatcher_connect( hass, SIGNAL_ADD_ENTITIES, functools.partial( - discovery.async_add_entities, async_add_entities, entities_to_create + zha_async_add_entities, async_add_entities, Thermostat, entities_to_create ), ) config_entry.async_on_unload(unsub) -@MULTI_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - aux_cluster_handlers=CLUSTER_HANDLER_FAN, - stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT, -) -class Thermostat(ZhaEntity, ClimateEntity): +class Thermostat(ZHAEntity, ClimateEntity): """Representation of a ZHA Thermostat device.""" - DEFAULT_MAX_TEMP = 35 - DEFAULT_MIN_TEMP = 7 - _attr_precision = PRECISION_TENTHS _attr_temperature_unit = UnitOfTemperature.CELSIUS _attr_translation_key: str = "thermostat" _enable_turn_on_off_backwards_compatibility = False - def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs): - """Initialize ZHA Thermostat instance.""" - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - self._thrm = self.cluster_handlers.get(CLUSTER_HANDLER_THERMOSTAT) - self._preset = PRESET_NONE - self._presets = [] - self._supported_flags = ( - ClimateEntityFeature.TARGET_TEMPERATURE - | ClimateEntityFeature.TURN_OFF - | ClimateEntityFeature.TURN_ON + def __init__(self, entity_data: EntityData, **kwargs: Any) -> None: + """Initialize the ZHA thermostat entity.""" + super().__init__(entity_data, **kwargs) + self._attr_hvac_modes = [ + ZHA_TO_HA_HVAC_MODE[mode] for mode in self.entity_data.entity.hvac_modes + ] + self._attr_hvac_mode = ZHA_TO_HA_HVAC_MODE.get( + self.entity_data.entity.hvac_mode ) - self._fan = self.cluster_handlers.get(CLUSTER_HANDLER_FAN) + self._attr_hvac_action = ZHA_TO_HA_HVAC_ACTION.get( + self.entity_data.entity.hvac_action + ) + + features: ClimateEntityFeature = ClimateEntityFeature(0) + zha_features: ZHAClimateEntityFeature = ( + self.entity_data.entity.supported_features + ) + + if ZHAClimateEntityFeature.TARGET_TEMPERATURE in zha_features: + features |= ClimateEntityFeature.TARGET_TEMPERATURE + if ZHAClimateEntityFeature.TARGET_TEMPERATURE_RANGE in zha_features: + features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE + if ZHAClimateEntityFeature.TARGET_HUMIDITY in zha_features: + features |= ClimateEntityFeature.TARGET_HUMIDITY + if ZHAClimateEntityFeature.PRESET_MODE in zha_features: + features |= ClimateEntityFeature.PRESET_MODE + if ZHAClimateEntityFeature.FAN_MODE in zha_features: + features |= ClimateEntityFeature.FAN_MODE + if ZHAClimateEntityFeature.SWING_MODE in zha_features: + features |= ClimateEntityFeature.SWING_MODE + if ZHAClimateEntityFeature.AUX_HEAT in zha_features: + features |= ClimateEntityFeature.AUX_HEAT + if ZHAClimateEntityFeature.TURN_OFF in zha_features: + features |= ClimateEntityFeature.TURN_OFF + if ZHAClimateEntityFeature.TURN_ON in zha_features: + features |= ClimateEntityFeature.TURN_ON + + self._attr_supported_features = features @property - def current_temperature(self): + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return entity specific state attributes.""" + state = self.entity_data.entity.state + + return exclude_none_values( + { + "occupancy": state.get("occupancy"), + "occupied_cooling_setpoint": state.get("occupied_cooling_setpoint"), + "occupied_heating_setpoint": state.get("occupied_heating_setpoint"), + "pi_cooling_demand": state.get("pi_cooling_demand"), + "pi_heating_demand": state.get("pi_heating_demand"), + "system_mode": state.get("system_mode"), + "unoccupied_cooling_setpoint": state.get("unoccupied_cooling_setpoint"), + "unoccupied_heating_setpoint": state.get("unoccupied_heating_setpoint"), + } + ) + + @property + def current_temperature(self) -> float | None: """Return the current temperature.""" - if self._thrm.local_temperature is None: - return None - return self._thrm.local_temperature / ZCL_TEMP - - @property - def extra_state_attributes(self): - """Return device specific state attributes.""" - data = {} - if self.hvac_mode: - mode = SYSTEM_MODE_2_HVAC.get(self._thrm.system_mode, "unknown") - data[ATTR_SYS_MODE] = f"[{self._thrm.system_mode}]/{mode}" - if self._thrm.occupancy is not None: - data[ATTR_OCCUPANCY] = self._thrm.occupancy - if self._thrm.occupied_cooling_setpoint is not None: - data[ATTR_OCCP_COOL_SETPT] = self._thrm.occupied_cooling_setpoint - if self._thrm.occupied_heating_setpoint is not None: - data[ATTR_OCCP_HEAT_SETPT] = self._thrm.occupied_heating_setpoint - if self._thrm.pi_heating_demand is not None: - data[ATTR_PI_HEATING_DEMAND] = self._thrm.pi_heating_demand - if self._thrm.pi_cooling_demand is not None: - data[ATTR_PI_COOLING_DEMAND] = self._thrm.pi_cooling_demand - - unoccupied_cooling_setpoint = self._thrm.unoccupied_cooling_setpoint - if unoccupied_cooling_setpoint is not None: - data[ATTR_UNOCCP_COOL_SETPT] = unoccupied_cooling_setpoint - - unoccupied_heating_setpoint = self._thrm.unoccupied_heating_setpoint - if unoccupied_heating_setpoint is not None: - data[ATTR_UNOCCP_HEAT_SETPT] = unoccupied_heating_setpoint - return data + return self.entity_data.entity.current_temperature @property def fan_mode(self) -> str | None: """Return current FAN mode.""" - if self._thrm.running_state is None: - return FAN_AUTO - - if self._thrm.running_state & ( - T.RunningState.Fan_State_On - | T.RunningState.Fan_2nd_Stage_On - | T.RunningState.Fan_3rd_Stage_On - ): - return FAN_ON - return FAN_AUTO + return self.entity_data.entity.fan_mode @property def fan_modes(self) -> list[str] | None: """Return supported FAN modes.""" - if not self._fan: - return None - return [FAN_AUTO, FAN_ON] - - @property - def hvac_action(self) -> HVACAction | None: - """Return the current HVAC action.""" - if ( - self._thrm.pi_heating_demand is None - and self._thrm.pi_cooling_demand is None - ): - return self._rm_rs_action - return self._pi_demand_action - - @property - def _rm_rs_action(self) -> HVACAction | None: - """Return the current HVAC action based on running mode and running state.""" - - if (running_state := self._thrm.running_state) is None: - return None - if running_state & ( - T.RunningState.Heat_State_On | T.RunningState.Heat_2nd_Stage_On - ): - return HVACAction.HEATING - if running_state & ( - T.RunningState.Cool_State_On | T.RunningState.Cool_2nd_Stage_On - ): - return HVACAction.COOLING - if running_state & ( - T.RunningState.Fan_State_On - | T.RunningState.Fan_2nd_Stage_On - | T.RunningState.Fan_3rd_Stage_On - ): - return HVACAction.FAN - if running_state & T.RunningState.Idle: - return HVACAction.IDLE - if self.hvac_mode != HVACMode.OFF: - return HVACAction.IDLE - return HVACAction.OFF - - @property - def _pi_demand_action(self) -> HVACAction | None: - """Return the current HVAC action based on pi_demands.""" - - heating_demand = self._thrm.pi_heating_demand - if heating_demand is not None and heating_demand > 0: - return HVACAction.HEATING - cooling_demand = self._thrm.pi_cooling_demand - if cooling_demand is not None and cooling_demand > 0: - return HVACAction.COOLING - - if self.hvac_mode != HVACMode.OFF: - return HVACAction.IDLE - return HVACAction.OFF - - @property - def hvac_mode(self) -> HVACMode | None: - """Return HVAC operation mode.""" - return SYSTEM_MODE_2_HVAC.get(self._thrm.system_mode) - - @property - def hvac_modes(self) -> list[HVACMode]: - """Return the list of available HVAC operation modes.""" - return SEQ_OF_OPERATION.get(self._thrm.ctrl_sequence_of_oper, [HVACMode.OFF]) + return self.entity_data.entity.fan_modes @property def preset_mode(self) -> str: """Return current preset mode.""" - return self._preset + return self.entity_data.entity.preset_mode @property def preset_modes(self) -> list[str] | None: """Return supported preset modes.""" - return self._presets + return self.entity_data.entity.preset_modes @property - def supported_features(self) -> ClimateEntityFeature: - """Return the list of supported features.""" - features = self._supported_flags - if HVACMode.HEAT_COOL in self.hvac_modes: - features |= ClimateEntityFeature.TARGET_TEMPERATURE_RANGE - if self._fan is not None: - self._supported_flags |= ClimateEntityFeature.FAN_MODE - return features - - @property - def target_temperature(self): + def target_temperature(self) -> float | None: """Return the temperature we try to reach.""" - temp = None - if self.hvac_mode == HVACMode.COOL: - if self.preset_mode == PRESET_AWAY: - temp = self._thrm.unoccupied_cooling_setpoint - else: - temp = self._thrm.occupied_cooling_setpoint - elif self.hvac_mode == HVACMode.HEAT: - if self.preset_mode == PRESET_AWAY: - temp = self._thrm.unoccupied_heating_setpoint - else: - temp = self._thrm.occupied_heating_setpoint - if temp is None: - return temp - return round(temp / ZCL_TEMP, 1) + return self.entity_data.entity.target_temperature @property - def target_temperature_high(self): + def target_temperature_high(self) -> float | None: """Return the upper bound temperature we try to reach.""" - if self.hvac_mode != HVACMode.HEAT_COOL: - return None - if self.preset_mode == PRESET_AWAY: - temp = self._thrm.unoccupied_cooling_setpoint - else: - temp = self._thrm.occupied_cooling_setpoint - - if temp is None: - return temp - - return round(temp / ZCL_TEMP, 1) + return self.entity_data.entity.target_temperature_high @property - def target_temperature_low(self): + def target_temperature_low(self) -> float | None: """Return the lower bound temperature we try to reach.""" - if self.hvac_mode != HVACMode.HEAT_COOL: - return None - if self.preset_mode == PRESET_AWAY: - temp = self._thrm.unoccupied_heating_setpoint - else: - temp = self._thrm.occupied_heating_setpoint - - if temp is None: - return temp - return round(temp / ZCL_TEMP, 1) + return self.entity_data.entity.target_temperature_low @property def max_temp(self) -> float: """Return the maximum temperature.""" - temps = [] - if HVACMode.HEAT in self.hvac_modes: - temps.append(self._thrm.max_heat_setpoint_limit) - if HVACMode.COOL in self.hvac_modes: - temps.append(self._thrm.max_cool_setpoint_limit) - - if not temps: - return self.DEFAULT_MAX_TEMP - return round(max(temps) / ZCL_TEMP, 1) + return self.entity_data.entity.max_temp @property def min_temp(self) -> float: """Return the minimum temperature.""" - temps = [] - if HVACMode.HEAT in self.hvac_modes: - temps.append(self._thrm.min_heat_setpoint_limit) - if HVACMode.COOL in self.hvac_modes: - temps.append(self._thrm.min_cool_setpoint_limit) - - if not temps: - return self.DEFAULT_MIN_TEMP - return round(min(temps) / ZCL_TEMP, 1) - - async def async_added_to_hass(self) -> None: - """Run when about to be added to hass.""" - await super().async_added_to_hass() - self.async_accept_signal( - self._thrm, SIGNAL_ATTR_UPDATED, self.async_attribute_updated - ) - - async def async_attribute_updated(self, attr_id, attr_name, value): - """Handle attribute update from device.""" - if ( - attr_name in (ATTR_OCCP_COOL_SETPT, ATTR_OCCP_HEAT_SETPT) - and self.preset_mode == PRESET_AWAY - ): - # occupancy attribute is an unreportable attribute, but if we get - # an attribute update for an "occupied" setpoint, there's a chance - # occupancy has changed - if await self._thrm.get_occupancy() is True: - self._preset = PRESET_NONE - - self.debug("Attribute '%s' = %s update", attr_name, value) - self.async_write_ha_state() - - async def async_set_fan_mode(self, fan_mode: str) -> None: - """Set fan mode.""" - if not self.fan_modes or fan_mode not in self.fan_modes: - self.warning("Unsupported '%s' fan mode", fan_mode) - return - - if fan_mode == FAN_ON: - mode = F.FanMode.On - else: - mode = F.FanMode.Auto - - await self._fan.async_set_speed(mode) - - async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: - """Set new target operation mode.""" - if hvac_mode not in self.hvac_modes: - self.warning( - "can't set '%s' mode. Supported modes are: %s", - hvac_mode, - self.hvac_modes, - ) - return - - if await self._thrm.async_set_operation_mode(HVAC_MODE_2_SYSTEM[hvac_mode]): - self.async_write_ha_state() - - async def async_set_preset_mode(self, preset_mode: str) -> None: - """Set new preset mode.""" - if not self.preset_modes or preset_mode not in self.preset_modes: - self.debug("Preset mode '%s' is not supported", preset_mode) - return - - if self.preset_mode not in ( - preset_mode, - PRESET_NONE, - ): - await self.async_preset_handler(self.preset_mode, enable=False) - - if preset_mode != PRESET_NONE: - await self.async_preset_handler(preset_mode, enable=True) - - self._preset = preset_mode - self.async_write_ha_state() - - async def async_set_temperature(self, **kwargs: Any) -> None: - """Set new target temperature.""" - low_temp = kwargs.get(ATTR_TARGET_TEMP_LOW) - high_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH) - temp = kwargs.get(ATTR_TEMPERATURE) - hvac_mode = kwargs.get(ATTR_HVAC_MODE) - - if hvac_mode is not None: - await self.async_set_hvac_mode(hvac_mode) - - is_away = self.preset_mode == PRESET_AWAY - - if self.hvac_mode == HVACMode.HEAT_COOL: - if low_temp is not None: - await self._thrm.async_set_heating_setpoint( - temperature=int(low_temp * ZCL_TEMP), - is_away=is_away, - ) - if high_temp is not None: - await self._thrm.async_set_cooling_setpoint( - temperature=int(high_temp * ZCL_TEMP), - is_away=is_away, - ) - elif temp is not None: - if self.hvac_mode == HVACMode.COOL: - await self._thrm.async_set_cooling_setpoint( - temperature=int(temp * ZCL_TEMP), - is_away=is_away, - ) - elif self.hvac_mode == HVACMode.HEAT: - await self._thrm.async_set_heating_setpoint( - temperature=int(temp * ZCL_TEMP), - is_away=is_away, - ) - else: - self.debug("Not setting temperature for '%s' mode", self.hvac_mode) - return - else: - self.debug("incorrect %s setting for '%s' mode", kwargs, self.hvac_mode) - return - - self.async_write_ha_state() - - async def async_preset_handler(self, preset: str, enable: bool = False) -> None: - """Set the preset mode via handler.""" - - handler = getattr(self, f"async_preset_handler_{preset}") - await handler(enable) - - -@MULTI_MATCH( - cluster_handler_names={CLUSTER_HANDLER_THERMOSTAT, "sinope_manufacturer_specific"}, - manufacturers="Sinope Technologies", - stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT, -) -class SinopeTechnologiesThermostat(Thermostat): - """Sinope Technologies Thermostat.""" - - manufacturer = 0x119C - update_time_interval = timedelta(minutes=randint(45, 75)) - - def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs): - """Initialize ZHA Thermostat instance.""" - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - self._presets = [PRESET_AWAY, PRESET_NONE] - self._supported_flags |= ClimateEntityFeature.PRESET_MODE - self._manufacturer_ch = self.cluster_handlers["sinope_manufacturer_specific"] - - @property - def _rm_rs_action(self) -> HVACAction: - """Return the current HVAC action based on running mode and running state.""" - - running_mode = self._thrm.running_mode - if running_mode == T.SystemMode.Heat: - return HVACAction.HEATING - if running_mode == T.SystemMode.Cool: - return HVACAction.COOLING - - running_state = self._thrm.running_state - if running_state and running_state & ( - T.RunningState.Fan_State_On - | T.RunningState.Fan_2nd_Stage_On - | T.RunningState.Fan_3rd_Stage_On - ): - return HVACAction.FAN - if self.hvac_mode != HVACMode.OFF and running_mode == T.SystemMode.Off: - return HVACAction.IDLE - return HVACAction.OFF + return self.entity_data.entity.min_temp @callback - def _async_update_time(self, timestamp=None) -> None: - """Update thermostat's time display.""" - - secs_2k = ( - dt_util.now().replace(tzinfo=None) - datetime(2000, 1, 1, 0, 0, 0, 0) - ).total_seconds() - - self.debug("Updating time: %s", secs_2k) - self._manufacturer_ch.cluster.create_catching_task( - self._manufacturer_ch.write_attributes_safe( - {"secs_since_2k": secs_2k}, manufacturer=self.manufacturer - ) + def _handle_entity_events(self, event: Any) -> None: + """Entity state changed.""" + self._attr_hvac_mode = self._attr_hvac_mode = ZHA_TO_HA_HVAC_MODE.get( + self.entity_data.entity.hvac_mode ) - - async def async_added_to_hass(self) -> None: - """Run when about to be added to Hass.""" - await super().async_added_to_hass() - self.async_on_remove( - async_track_time_interval( - self.hass, self._async_update_time, self.update_time_interval - ) + self._attr_hvac_action = ZHA_TO_HA_HVAC_ACTION.get( + self.entity_data.entity.hvac_action ) - self._async_update_time() + super()._handle_entity_events(event) - async def async_preset_handler_away(self, is_away: bool = False) -> None: - """Set occupancy.""" - mfg_code = self._zha_device.manufacturer_code - await self._thrm.write_attributes_safe( - {"set_occupancy": 0 if is_away else 1}, manufacturer=mfg_code + @convert_zha_error_to_ha_error + async def async_set_fan_mode(self, fan_mode: str) -> None: + """Set fan mode.""" + await self.entity_data.entity.async_set_fan_mode(fan_mode=fan_mode) + self.async_write_ha_state() + + @convert_zha_error_to_ha_error + async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None: + """Set new target operation mode.""" + await self.entity_data.entity.async_set_hvac_mode(hvac_mode=hvac_mode) + self.async_write_ha_state() + + @convert_zha_error_to_ha_error + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new preset mode.""" + await self.entity_data.entity.async_set_preset_mode(preset_mode=preset_mode) + self.async_write_ha_state() + + @convert_zha_error_to_ha_error + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + await self.entity_data.entity.async_set_temperature( + target_temp_low=kwargs.get(ATTR_TARGET_TEMP_LOW), + target_temp_high=kwargs.get(ATTR_TARGET_TEMP_HIGH), + temperature=kwargs.get(ATTR_TEMPERATURE), + hvac_mode=kwargs.get(ATTR_HVAC_MODE), ) - - -@MULTI_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - aux_cluster_handlers=CLUSTER_HANDLER_FAN, - manufacturers={"Zen Within", "LUX"}, - stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT, -) -class ZenWithinThermostat(Thermostat): - """Zen Within Thermostat implementation.""" - - -@MULTI_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - aux_cluster_handlers=CLUSTER_HANDLER_FAN, - manufacturers="Centralite", - models={"3157100", "3157100-E"}, - stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT, -) -class CentralitePearl(ZenWithinThermostat): - """Centralite Pearl Thermostat implementation.""" - - -@STRICT_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - manufacturers={ - "_TZE200_ckud7u2l", - "_TZE200_ywdxldoj", - "_TZE200_cwnjrr72", - "_TZE200_2atgpdho", - "_TZE200_pvvbommb", - "_TZE200_4eeyebrt", - "_TZE200_cpmgn2cf", - "_TZE200_9sfg7gm0", - "_TZE200_8whxpsiw", - "_TYST11_ckud7u2l", - "_TYST11_ywdxldoj", - "_TYST11_cwnjrr72", - "_TYST11_2atgpdho", - }, -) -class MoesThermostat(Thermostat): - """Moes Thermostat implementation.""" - - def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs): - """Initialize ZHA Thermostat instance.""" - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - self._presets = [ - PRESET_NONE, - PRESET_AWAY, - PRESET_SCHEDULE, - PRESET_COMFORT, - PRESET_ECO, - PRESET_BOOST, - PRESET_COMPLEX, - ] - self._supported_flags |= ClimateEntityFeature.PRESET_MODE - - @property - def hvac_modes(self) -> list[HVACMode]: - """Return only the heat mode, because the device can't be turned off.""" - return [HVACMode.HEAT] - - async def async_attribute_updated(self, attr_id, attr_name, value): - """Handle attribute update from device.""" - if attr_name == "operation_preset": - if value == 0: - self._preset = PRESET_AWAY - if value == 1: - self._preset = PRESET_SCHEDULE - if value == 2: - self._preset = PRESET_NONE - if value == 3: - self._preset = PRESET_COMFORT - if value == 4: - self._preset = PRESET_ECO - if value == 5: - self._preset = PRESET_BOOST - if value == 6: - self._preset = PRESET_COMPLEX - await super().async_attribute_updated(attr_id, attr_name, value) - - async def async_preset_handler(self, preset: str, enable: bool = False) -> None: - """Set the preset mode.""" - mfg_code = self._zha_device.manufacturer_code - if not enable: - return await self._thrm.write_attributes_safe( - {"operation_preset": 2}, manufacturer=mfg_code - ) - if preset == PRESET_AWAY: - return await self._thrm.write_attributes_safe( - {"operation_preset": 0}, manufacturer=mfg_code - ) - if preset == PRESET_SCHEDULE: - return await self._thrm.write_attributes_safe( - {"operation_preset": 1}, manufacturer=mfg_code - ) - if preset == PRESET_COMFORT: - return await self._thrm.write_attributes_safe( - {"operation_preset": 3}, manufacturer=mfg_code - ) - if preset == PRESET_ECO: - return await self._thrm.write_attributes_safe( - {"operation_preset": 4}, manufacturer=mfg_code - ) - if preset == PRESET_BOOST: - return await self._thrm.write_attributes_safe( - {"operation_preset": 5}, manufacturer=mfg_code - ) - if preset == PRESET_COMPLEX: - return await self._thrm.write_attributes_safe( - {"operation_preset": 6}, manufacturer=mfg_code - ) - - -@STRICT_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - manufacturers={ - "_TZE200_b6wax7g0", - }, -) -class BecaThermostat(Thermostat): - """Beca Thermostat implementation.""" - - def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs): - """Initialize ZHA Thermostat instance.""" - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - self._presets = [ - PRESET_NONE, - PRESET_AWAY, - PRESET_SCHEDULE, - PRESET_ECO, - PRESET_BOOST, - PRESET_TEMP_MANUAL, - ] - self._supported_flags |= ClimateEntityFeature.PRESET_MODE - - @property - def hvac_modes(self) -> list[HVACMode]: - """Return only the heat mode, because the device can't be turned off.""" - return [HVACMode.HEAT] - - async def async_attribute_updated(self, attr_id, attr_name, value): - """Handle attribute update from device.""" - if attr_name == "operation_preset": - if value == 0: - self._preset = PRESET_AWAY - if value == 1: - self._preset = PRESET_SCHEDULE - if value == 2: - self._preset = PRESET_NONE - if value == 4: - self._preset = PRESET_ECO - if value == 5: - self._preset = PRESET_BOOST - if value == 7: - self._preset = PRESET_TEMP_MANUAL - await super().async_attribute_updated(attr_id, attr_name, value) - - async def async_preset_handler(self, preset: str, enable: bool = False) -> None: - """Set the preset mode.""" - mfg_code = self._zha_device.manufacturer_code - if not enable: - return await self._thrm.write_attributes_safe( - {"operation_preset": 2}, manufacturer=mfg_code - ) - if preset == PRESET_AWAY: - return await self._thrm.write_attributes_safe( - {"operation_preset": 0}, manufacturer=mfg_code - ) - if preset == PRESET_SCHEDULE: - return await self._thrm.write_attributes_safe( - {"operation_preset": 1}, manufacturer=mfg_code - ) - if preset == PRESET_ECO: - return await self._thrm.write_attributes_safe( - {"operation_preset": 4}, manufacturer=mfg_code - ) - if preset == PRESET_BOOST: - return await self._thrm.write_attributes_safe( - {"operation_preset": 5}, manufacturer=mfg_code - ) - if preset == PRESET_TEMP_MANUAL: - return await self._thrm.write_attributes_safe( - {"operation_preset": 7}, manufacturer=mfg_code - ) - - -@MULTI_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - manufacturers="Stelpro", - models={"SORB"}, - stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT, -) -class StelproFanHeater(Thermostat): - """Stelpro Fan Heater implementation.""" - - @property - def hvac_modes(self) -> list[HVACMode]: - """Return only the heat mode, because the device can't be turned off.""" - return [HVACMode.HEAT] - - -@STRICT_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - manufacturers={ - "_TZE200_7yoranx2", - "_TZE200_e9ba97vf", # TV01-ZG - "_TZE200_hue3yfsn", # TV02-ZG - "_TZE200_husqqvux", # TSL-TRV-TV01ZG - "_TZE200_kds0pmmv", # MOES TRV TV02 - "_TZE200_kly8gjlz", # TV05-ZG - "_TZE200_lnbfnyxd", - "_TZE200_mudxchsu", - }, -) -class ZONNSMARTThermostat(Thermostat): - """ZONNSMART Thermostat implementation. - - Notice that this device uses two holiday presets (2: HolidayMode, - 3: HolidayModeTemp), but only one of them can be set. - """ - - PRESET_HOLIDAY = "holiday" - PRESET_FROST = "frost protect" - - def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs): - """Initialize ZHA Thermostat instance.""" - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - self._presets = [ - PRESET_NONE, - self.PRESET_HOLIDAY, - PRESET_SCHEDULE, - self.PRESET_FROST, - ] - self._supported_flags |= ClimateEntityFeature.PRESET_MODE - - async def async_attribute_updated(self, attr_id, attr_name, value): - """Handle attribute update from device.""" - if attr_name == "operation_preset": - if value == 0: - self._preset = PRESET_SCHEDULE - if value == 1: - self._preset = PRESET_NONE - if value == 2: - self._preset = self.PRESET_HOLIDAY - if value == 3: - self._preset = self.PRESET_HOLIDAY - if value == 4: - self._preset = self.PRESET_FROST - await super().async_attribute_updated(attr_id, attr_name, value) - - async def async_preset_handler(self, preset: str, enable: bool = False) -> None: - """Set the preset mode.""" - mfg_code = self._zha_device.manufacturer_code - if not enable: - return await self._thrm.write_attributes_safe( - {"operation_preset": 1}, manufacturer=mfg_code - ) - if preset == PRESET_SCHEDULE: - return await self._thrm.write_attributes_safe( - {"operation_preset": 0}, manufacturer=mfg_code - ) - if preset == self.PRESET_HOLIDAY: - return await self._thrm.write_attributes_safe( - {"operation_preset": 3}, manufacturer=mfg_code - ) - if preset == self.PRESET_FROST: - return await self._thrm.write_attributes_safe( - {"operation_preset": 4}, manufacturer=mfg_code - ) + self.async_write_ha_state() diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index 9be27f7b37c..3a7b54652d9 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -10,6 +10,7 @@ from typing import Any import serial.tools.list_ports from serial.tools.list_ports_common import ListPortInfo import voluptuous as vol +from zha.application.const import RadioType import zigpy.backups from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH @@ -35,13 +36,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.selector import FileSelector, FileSelectorConfig from homeassistant.util import dt as dt_util -from .core.const import ( - CONF_BAUDRATE, - CONF_FLOW_CONTROL, - CONF_RADIO_TYPE, - DOMAIN, - RadioType, -) +from .const import CONF_BAUDRATE, CONF_FLOW_CONTROL, CONF_RADIO_TYPE, DOMAIN from .radio_manager import ( DEVICE_SCHEMA, HARDWARE_DISCOVERY_SCHEMA, @@ -146,12 +141,12 @@ class BaseZhaFlow(ConfigEntryBaseFlow): self._title: str | None = None @property - def hass(self): + def hass(self) -> HomeAssistant: """Return hass.""" return self._hass @hass.setter - def hass(self, hass): + def hass(self, hass: HomeAssistant) -> None: """Set hass.""" self._hass = hass self._radio_mgr.hass = hass diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py new file mode 100644 index 00000000000..3986a99cf3f --- /dev/null +++ b/homeassistant/components/zha/const.py @@ -0,0 +1,76 @@ +"""Constants for the ZHA integration.""" + +EZSP_OVERWRITE_EUI64 = ( + "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it" +) + +ATTR_ACTIVE_COORDINATOR = "active_coordinator" +ATTR_ATTRIBUTES = "attributes" +ATTR_AVAILABLE = "available" +ATTR_DEVICE_TYPE = "device_type" +ATTR_CLUSTER_NAME = "cluster_name" +ATTR_ENDPOINT_NAMES = "endpoint_names" +ATTR_IEEE = "ieee" +ATTR_LAST_SEEN = "last_seen" +ATTR_LQI = "lqi" +ATTR_MANUFACTURER = "manufacturer" +ATTR_MANUFACTURER_CODE = "manufacturer_code" +ATTR_NEIGHBORS = "neighbors" +ATTR_NWK = "nwk" +ATTR_POWER_SOURCE = "power_source" +ATTR_QUIRK_APPLIED = "quirk_applied" +ATTR_QUIRK_CLASS = "quirk_class" +ATTR_QUIRK_ID = "quirk_id" +ATTR_ROUTES = "routes" +ATTR_RSSI = "rssi" +ATTR_SIGNATURE = "signature" +ATTR_SUCCESS = "success" + + +CONF_ALARM_MASTER_CODE = "alarm_master_code" +CONF_ALARM_FAILED_TRIES = "alarm_failed_tries" +CONF_ALARM_ARM_REQUIRES_CODE = "alarm_arm_requires_code" + +CONF_RADIO_TYPE = "radio_type" +CONF_USB_PATH = "usb_path" +CONF_USE_THREAD = "use_thread" +CONF_BAUDRATE = "baudrate" +CONF_FLOW_CONTROL = "flow_control" + +CONF_ENABLE_QUIRKS = "enable_quirks" +CONF_CUSTOM_QUIRKS_PATH = "custom_quirks_path" + +CONF_DEFAULT_LIGHT_TRANSITION = "default_light_transition" +CONF_ENABLE_ENHANCED_LIGHT_TRANSITION = "enhanced_light_transition" +CONF_ENABLE_LIGHT_TRANSITIONING_FLAG = "light_transitioning_flag" +CONF_ALWAYS_PREFER_XY_COLOR_MODE = "always_prefer_xy_color_mode" +CONF_GROUP_MEMBERS_ASSUME_STATE = "group_members_assume_state" + +CONF_ENABLE_IDENTIFY_ON_JOIN = "enable_identify_on_join" +CONF_CONSIDER_UNAVAILABLE_MAINS = "consider_unavailable_mains" +CONF_CONSIDER_UNAVAILABLE_BATTERY = "consider_unavailable_battery" + +CONF_ZIGPY = "zigpy_config" +CONF_DEVICE_CONFIG = "device_config" + +CUSTOM_CONFIGURATION = "custom_configuration" + +DATA_ZHA = "zha" +DATA_ZHA_DEVICE_TRIGGER_CACHE = "zha_device_trigger_cache" + +DEFAULT_DATABASE_NAME = "zigbee.db" + +DEVICE_PAIRING_STATUS = "pairing_status" + +DOMAIN = "zha" + +GROUP_ID = "group_id" + + +GROUP_IDS = "group_ids" +GROUP_NAME = "group_name" + +MFG_CLUSTER_ID_START = 0xFC00 + +ZHA_ALARM_OPTIONS = "zha_alarm_options" +ZHA_OPTIONS = "zha_options" diff --git a/homeassistant/components/zha/core/__init__.py b/homeassistant/components/zha/core/__init__.py deleted file mode 100644 index 755eac3c4ce..00000000000 --- a/homeassistant/components/zha/core/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Core module for Zigbee Home Automation.""" - -from .device import ZHADevice -from .gateway import ZHAGateway - -__all__ = ["ZHADevice", "ZHAGateway"] diff --git a/homeassistant/components/zha/core/cluster_handlers/__init__.py b/homeassistant/components/zha/core/cluster_handlers/__init__.py deleted file mode 100644 index 8833d5c116f..00000000000 --- a/homeassistant/components/zha/core/cluster_handlers/__init__.py +++ /dev/null @@ -1,654 +0,0 @@ -"""Cluster handlers module for Zigbee Home Automation.""" - -from __future__ import annotations - -from collections.abc import Awaitable, Callable, Coroutine, Iterator -import contextlib -from enum import Enum -import functools -import logging -from typing import TYPE_CHECKING, Any, TypedDict - -import zigpy.exceptions -import zigpy.util -import zigpy.zcl -from zigpy.zcl.foundation import ( - CommandSchema, - ConfigureReportingResponseRecord, - Status, - ZCLAttributeDef, -) - -from homeassistant.const import ATTR_COMMAND -from homeassistant.core import callback -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.dispatcher import async_dispatcher_send - -from ..const import ( - ATTR_ARGS, - ATTR_ATTRIBUTE_ID, - ATTR_ATTRIBUTE_NAME, - ATTR_CLUSTER_ID, - ATTR_PARAMS, - ATTR_TYPE, - ATTR_UNIQUE_ID, - ATTR_VALUE, - CLUSTER_HANDLER_ZDO, - REPORT_CONFIG_ATTR_PER_REQ, - SIGNAL_ATTR_UPDATED, - ZHA_CLUSTER_HANDLER_MSG, - ZHA_CLUSTER_HANDLER_MSG_BIND, - ZHA_CLUSTER_HANDLER_MSG_CFG_RPT, - ZHA_CLUSTER_HANDLER_MSG_DATA, - ZHA_CLUSTER_HANDLER_READS_PER_REQ, -) -from ..helpers import LogMixin, safe_read - -if TYPE_CHECKING: - from ..endpoint import Endpoint - -_LOGGER = logging.getLogger(__name__) -RETRYABLE_REQUEST_DECORATOR = zigpy.util.retryable_request(tries=3) -UNPROXIED_CLUSTER_METHODS = {"general_command"} - -type _FuncType[**_P] = Callable[_P, Awaitable[Any]] -type _ReturnFuncType[**_P] = Callable[_P, Coroutine[Any, Any, Any]] - - -@contextlib.contextmanager -def wrap_zigpy_exceptions() -> Iterator[None]: - """Wrap zigpy exceptions in `HomeAssistantError` exceptions.""" - try: - yield - except TimeoutError as exc: - raise HomeAssistantError( - "Failed to send request: device did not respond" - ) from exc - except zigpy.exceptions.ZigbeeException as exc: - message = "Failed to send request" - - if str(exc): - message = f"{message}: {exc}" - - raise HomeAssistantError(message) from exc - - -def retry_request[**_P](func: _FuncType[_P]) -> _ReturnFuncType[_P]: - """Send a request with retries and wrap expected zigpy exceptions.""" - - @functools.wraps(func) - async def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> Any: - with wrap_zigpy_exceptions(): - return await RETRYABLE_REQUEST_DECORATOR(func)(*args, **kwargs) - - return wrapper - - -class AttrReportConfig(TypedDict, total=True): - """Configuration to report for the attributes.""" - - # An attribute name - attr: str - # The config for the attribute reporting configuration consists of a tuple for - # (minimum_reported_time_interval_s, maximum_reported_time_interval_s, value_delta) - config: tuple[int, int, int | float] - - -def parse_and_log_command(cluster_handler, tsn, command_id, args): - """Parse and log a zigbee cluster command.""" - try: - name = cluster_handler.cluster.server_commands[command_id].name - except KeyError: - name = f"0x{command_id:02X}" - - cluster_handler.debug( - "received '%s' command with %s args on cluster_id '%s' tsn '%s'", - name, - args, - cluster_handler.cluster.cluster_id, - tsn, - ) - return name - - -class ClusterHandlerStatus(Enum): - """Status of a cluster handler.""" - - CREATED = 1 - CONFIGURED = 2 - INITIALIZED = 3 - - -class ClusterHandler(LogMixin): - """Base cluster handler for a Zigbee cluster.""" - - REPORT_CONFIG: tuple[AttrReportConfig, ...] = () - BIND: bool = True - - # Dict of attributes to read on cluster handler initialization. - # Dict keys -- attribute ID or names, with bool value indicating whether a cached - # attribute read is acceptable. - ZCL_INIT_ATTRS: dict[str, bool] = {} - - def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None: - """Initialize ClusterHandler.""" - self._generic_id = f"cluster_handler_0x{cluster.cluster_id:04x}" - self._endpoint: Endpoint = endpoint - self._cluster = cluster - self._id = f"{endpoint.id}:0x{cluster.cluster_id:04x}" - unique_id = endpoint.unique_id.replace("-", ":") - self._unique_id = f"{unique_id}:0x{cluster.cluster_id:04x}" - if not hasattr(self, "_value_attribute") and self.REPORT_CONFIG: - attr_def: ZCLAttributeDef = self.cluster.attributes_by_name[ - self.REPORT_CONFIG[0]["attr"] - ] - self.value_attribute = attr_def.id - self._status = ClusterHandlerStatus.CREATED - self._cluster.add_listener(self) - self.data_cache: dict[str, Enum] = {} - - @classmethod - def matches(cls, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> bool: - """Filter the cluster match for specific devices.""" - return True - - @property - def id(self) -> str: - """Return cluster handler id unique for this device only.""" - return self._id - - @property - def generic_id(self): - """Return the generic id for this cluster handler.""" - return self._generic_id - - @property - def unique_id(self): - """Return the unique id for this cluster handler.""" - return self._unique_id - - @property - def cluster(self): - """Return the zigpy cluster for this cluster handler.""" - return self._cluster - - @property - def name(self) -> str: - """Return friendly name.""" - return self.cluster.ep_attribute or self._generic_id - - @property - def status(self): - """Return the status of the cluster handler.""" - return self._status - - def __hash__(self) -> int: - """Make this a hashable.""" - return hash(self._unique_id) - - @callback - def async_send_signal(self, signal: str, *args: Any) -> None: - """Send a signal through hass dispatcher.""" - self._endpoint.async_send_signal(signal, *args) - - async def bind(self): - """Bind a zigbee cluster. - - This also swallows ZigbeeException exceptions that are thrown when - devices are unreachable. - """ - try: - res = await self.cluster.bind() - self.debug("bound '%s' cluster: %s", self.cluster.ep_attribute, res[0]) - async_dispatcher_send( - self._endpoint.device.hass, - ZHA_CLUSTER_HANDLER_MSG, - { - ATTR_TYPE: ZHA_CLUSTER_HANDLER_MSG_BIND, - ZHA_CLUSTER_HANDLER_MSG_DATA: { - "cluster_name": self.cluster.name, - "cluster_id": self.cluster.cluster_id, - "success": res[0] == 0, - }, - }, - ) - except (zigpy.exceptions.ZigbeeException, TimeoutError) as ex: - self.debug( - "Failed to bind '%s' cluster: %s", - self.cluster.ep_attribute, - str(ex), - exc_info=ex, - ) - async_dispatcher_send( - self._endpoint.device.hass, - ZHA_CLUSTER_HANDLER_MSG, - { - ATTR_TYPE: ZHA_CLUSTER_HANDLER_MSG_BIND, - ZHA_CLUSTER_HANDLER_MSG_DATA: { - "cluster_name": self.cluster.name, - "cluster_id": self.cluster.cluster_id, - "success": False, - }, - }, - ) - - async def configure_reporting(self) -> None: - """Configure attribute reporting for a cluster. - - This also swallows ZigbeeException exceptions that are thrown when - devices are unreachable. - """ - event_data = {} - kwargs = {} - if ( - self.cluster.cluster_id >= 0xFC00 - and self._endpoint.device.manufacturer_code - ): - kwargs["manufacturer"] = self._endpoint.device.manufacturer_code - - for attr_report in self.REPORT_CONFIG: - attr, config = attr_report["attr"], attr_report["config"] - - try: - attr_name = self.cluster.find_attribute(attr).name - except KeyError: - attr_name = attr - - event_data[attr_name] = { - "min": config[0], - "max": config[1], - "id": attr, - "name": attr_name, - "change": config[2], - "status": None, - } - - to_configure = [*self.REPORT_CONFIG] - chunk, rest = ( - to_configure[:REPORT_CONFIG_ATTR_PER_REQ], - to_configure[REPORT_CONFIG_ATTR_PER_REQ:], - ) - while chunk: - reports = {rec["attr"]: rec["config"] for rec in chunk} - try: - res = await self.cluster.configure_reporting_multiple(reports, **kwargs) - self._configure_reporting_status(reports, res[0], event_data) - except (zigpy.exceptions.ZigbeeException, TimeoutError) as ex: - self.debug( - "failed to set reporting on '%s' cluster for: %s", - self.cluster.ep_attribute, - str(ex), - ) - break - chunk, rest = ( - rest[:REPORT_CONFIG_ATTR_PER_REQ], - rest[REPORT_CONFIG_ATTR_PER_REQ:], - ) - - async_dispatcher_send( - self._endpoint.device.hass, - ZHA_CLUSTER_HANDLER_MSG, - { - ATTR_TYPE: ZHA_CLUSTER_HANDLER_MSG_CFG_RPT, - ZHA_CLUSTER_HANDLER_MSG_DATA: { - "cluster_name": self.cluster.name, - "cluster_id": self.cluster.cluster_id, - "attributes": event_data, - }, - }, - ) - - def _configure_reporting_status( - self, - attrs: dict[str, tuple[int, int, float | int]], - res: list | tuple, - event_data: dict[str, dict[str, Any]], - ) -> None: - """Parse configure reporting result.""" - if isinstance(res, (Exception, ConfigureReportingResponseRecord)): - # assume default response - self.debug( - "attr reporting for '%s' on '%s': %s", - attrs, - self.name, - res, - ) - for attr in attrs: - event_data[attr]["status"] = Status.FAILURE.name - return - if res[0].status == Status.SUCCESS and len(res) == 1: - self.debug( - "Successfully configured reporting for '%s' on '%s' cluster: %s", - attrs, - self.name, - res, - ) - # 2.5.8.1.3 Status Field - # The status field specifies the status of the Configure Reporting operation attempted on this attribute, as detailed in 2.5.7.3. - # Note that attribute status records are not included for successfully configured attributes, in order to save bandwidth. - # In the case of successful configuration of all attributes, only a single attribute status record SHALL be included in the command, - # with the status field set to SUCCESS and the direction and attribute identifier fields omitted. - for attr in attrs: - event_data[attr]["status"] = Status.SUCCESS.name - return - - for record in res: - event_data[self.cluster.find_attribute(record.attrid).name]["status"] = ( - record.status.name - ) - failed = [ - self.cluster.find_attribute(record.attrid).name - for record in res - if record.status != Status.SUCCESS - ] - self.debug( - "Failed to configure reporting for '%s' on '%s' cluster: %s", - failed, - self.name, - res, - ) - success = set(attrs) - set(failed) - self.debug( - "Successfully configured reporting for '%s' on '%s' cluster", - set(attrs) - set(failed), - self.name, - ) - for attr in success: - event_data[attr]["status"] = Status.SUCCESS.name - - async def async_configure(self) -> None: - """Set cluster binding and attribute reporting.""" - if not self._endpoint.device.skip_configuration: - if self.BIND: - self.debug("Performing cluster binding") - await self.bind() - if self.cluster.is_server: - self.debug("Configuring cluster attribute reporting") - await self.configure_reporting() - ch_specific_cfg = getattr( - self, "async_configure_cluster_handler_specific", None - ) - if ch_specific_cfg: - self.debug("Performing cluster handler specific configuration") - await ch_specific_cfg() - self.debug("finished cluster handler configuration") - else: - self.debug("skipping cluster handler configuration") - self._status = ClusterHandlerStatus.CONFIGURED - - async def async_initialize(self, from_cache: bool) -> None: - """Initialize cluster handler.""" - if not from_cache and self._endpoint.device.skip_configuration: - self.debug("Skipping cluster handler initialization") - self._status = ClusterHandlerStatus.INITIALIZED - return - - self.debug("initializing cluster handler: from_cache: %s", from_cache) - cached = [a for a, cached in self.ZCL_INIT_ATTRS.items() if cached] - uncached = [a for a, cached in self.ZCL_INIT_ATTRS.items() if not cached] - uncached.extend([cfg["attr"] for cfg in self.REPORT_CONFIG]) - - if cached: - self.debug("initializing cached cluster handler attributes: %s", cached) - await self._get_attributes( - True, cached, from_cache=True, only_cache=from_cache - ) - if uncached: - self.debug( - "initializing uncached cluster handler attributes: %s - from cache[%s]", - uncached, - from_cache, - ) - await self._get_attributes( - True, uncached, from_cache=from_cache, only_cache=from_cache - ) - - ch_specific_init = getattr( - self, "async_initialize_cluster_handler_specific", None - ) - if ch_specific_init: - self.debug( - "Performing cluster handler specific initialization: %s", uncached - ) - await ch_specific_init(from_cache=from_cache) - - self.debug("finished cluster handler initialization") - self._status = ClusterHandlerStatus.INITIALIZED - - @callback - def cluster_command(self, tsn, command_id, args): - """Handle commands received to this cluster.""" - - @callback - def attribute_updated(self, attrid: int, value: Any, _: Any) -> None: - """Handle attribute updates on this cluster.""" - attr_name = self._get_attribute_name(attrid) - self.debug( - "cluster_handler[%s] attribute_updated - cluster[%s] attr[%s] value[%s]", - self.name, - self.cluster.name, - attr_name, - value, - ) - self.async_send_signal( - f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", - attrid, - attr_name, - value, - ) - - @callback - def zdo_command(self, *args, **kwargs): - """Handle ZDO commands on this cluster.""" - - @callback - def zha_send_event(self, command: str, arg: list | dict | CommandSchema) -> None: - """Relay events to hass.""" - - args: list | dict - if isinstance(arg, CommandSchema): - args = [a for a in arg if a is not None] - params = arg.as_dict() - elif isinstance(arg, (list, dict)): - # Quirks can directly send lists and dicts to ZHA this way - args = arg - params = {} - else: - raise TypeError(f"Unexpected zha_send_event {command!r} argument: {arg!r}") - - self._endpoint.send_event( - { - ATTR_UNIQUE_ID: self.unique_id, - ATTR_CLUSTER_ID: self.cluster.cluster_id, - ATTR_COMMAND: command, - # Maintain backwards compatibility with the old zigpy response format - ATTR_ARGS: args, - ATTR_PARAMS: params, - } - ) - - async def async_update(self): - """Retrieve latest state from cluster.""" - - def _get_attribute_name(self, attrid: int) -> str | int: - if attrid not in self.cluster.attributes: - return attrid - - return self.cluster.attributes[attrid].name - - async def get_attribute_value(self, attribute, from_cache=True): - """Get the value for an attribute.""" - manufacturer = None - manufacturer_code = self._endpoint.device.manufacturer_code - if self.cluster.cluster_id >= 0xFC00 and manufacturer_code: - manufacturer = manufacturer_code - result = await safe_read( - self._cluster, - [attribute], - allow_cache=from_cache, - only_cache=from_cache, - manufacturer=manufacturer, - ) - return result.get(attribute) - - async def _get_attributes( - self, - raise_exceptions: bool, - attributes: list[str], - from_cache: bool = True, - only_cache: bool = True, - ) -> dict[int | str, Any]: - """Get the values for a list of attributes.""" - manufacturer = None - manufacturer_code = self._endpoint.device.manufacturer_code - if self.cluster.cluster_id >= 0xFC00 and manufacturer_code: - manufacturer = manufacturer_code - chunk = attributes[:ZHA_CLUSTER_HANDLER_READS_PER_REQ] - rest = attributes[ZHA_CLUSTER_HANDLER_READS_PER_REQ:] - result = {} - while chunk: - try: - self.debug("Reading attributes in chunks: %s", chunk) - read, _ = await self.cluster.read_attributes( - chunk, - allow_cache=from_cache, - only_cache=only_cache, - manufacturer=manufacturer, - ) - result.update(read) - except (TimeoutError, zigpy.exceptions.ZigbeeException) as ex: - self.debug( - "failed to get attributes '%s' on '%s' cluster: %s", - chunk, - self.cluster.ep_attribute, - str(ex), - ) - if raise_exceptions: - raise - chunk = rest[:ZHA_CLUSTER_HANDLER_READS_PER_REQ] - rest = rest[ZHA_CLUSTER_HANDLER_READS_PER_REQ:] - return result - - get_attributes = functools.partialmethod(_get_attributes, False) - - async def write_attributes_safe( - self, attributes: dict[str, Any], manufacturer: int | None = None - ) -> None: - """Wrap `write_attributes` to throw an exception on attribute write failure.""" - - res = await self.write_attributes(attributes, manufacturer=manufacturer) - - for record in res[0]: - if record.status != Status.SUCCESS: - try: - name = self.cluster.attributes[record.attrid].name - value = attributes.get(name, "unknown") - except KeyError: - name = f"0x{record.attrid:04x}" - value = "unknown" - - raise HomeAssistantError( - f"Failed to write attribute {name}={value}: {record.status}", - ) - - def log(self, level, msg, *args, **kwargs): - """Log a message.""" - msg = f"[%s:%s]: {msg}" - args = (self._endpoint.device.nwk, self._id, *args) - _LOGGER.log(level, msg, *args, **kwargs) - - def __getattr__(self, name): - """Get attribute or a decorated cluster command.""" - if ( - hasattr(self._cluster, name) - and callable(getattr(self._cluster, name)) - and name not in UNPROXIED_CLUSTER_METHODS - ): - command = getattr(self._cluster, name) - wrapped_command = retry_request(command) - wrapped_command.__name__ = name - - return wrapped_command - return self.__getattribute__(name) - - -class ZDOClusterHandler(LogMixin): - """Cluster handler for ZDO events.""" - - def __init__(self, device) -> None: - """Initialize ZDOClusterHandler.""" - self.name = CLUSTER_HANDLER_ZDO - self._cluster = device.device.endpoints[0] - self._zha_device = device - self._status = ClusterHandlerStatus.CREATED - self._unique_id = f"{device.ieee!s}:{device.name}_ZDO" - self._cluster.add_listener(self) - - @property - def unique_id(self): - """Return the unique id for this cluster handler.""" - return self._unique_id - - @property - def cluster(self): - """Return the aigpy cluster for this cluster handler.""" - return self._cluster - - @property - def status(self): - """Return the status of the cluster handler.""" - return self._status - - @callback - def device_announce(self, zigpy_device): - """Device announce handler.""" - - @callback - def permit_duration(self, duration): - """Permit handler.""" - - async def async_initialize(self, from_cache): - """Initialize cluster handler.""" - self._status = ClusterHandlerStatus.INITIALIZED - - async def async_configure(self): - """Configure cluster handler.""" - self._status = ClusterHandlerStatus.CONFIGURED - - def log(self, level, msg, *args, **kwargs): - """Log a message.""" - msg = f"[%s:ZDO](%s): {msg}" - args = (self._zha_device.nwk, self._zha_device.model, *args) - _LOGGER.log(level, msg, *args, **kwargs) - - -class ClientClusterHandler(ClusterHandler): - """ClusterHandler for Zigbee client (output) clusters.""" - - @callback - def attribute_updated(self, attrid: int, value: Any, timestamp: Any) -> None: - """Handle an attribute updated on this cluster.""" - super().attribute_updated(attrid, value, timestamp) - - try: - attr_name = self._cluster.attributes[attrid].name - except KeyError: - attr_name = "Unknown" - - self.zha_send_event( - SIGNAL_ATTR_UPDATED, - { - ATTR_ATTRIBUTE_ID: attrid, - ATTR_ATTRIBUTE_NAME: attr_name, - ATTR_VALUE: value, - }, - ) - - @callback - def cluster_command(self, tsn, command_id, args): - """Handle a cluster command received on this cluster.""" - if ( - self._cluster.server_commands is not None - and self._cluster.server_commands.get(command_id) is not None - ): - self.zha_send_event(self._cluster.server_commands[command_id].name, args) diff --git a/homeassistant/components/zha/core/cluster_handlers/closures.py b/homeassistant/components/zha/core/cluster_handlers/closures.py deleted file mode 100644 index e96d6492beb..00000000000 --- a/homeassistant/components/zha/core/cluster_handlers/closures.py +++ /dev/null @@ -1,271 +0,0 @@ -"""Closures cluster handlers module for Zigbee Home Automation.""" - -from __future__ import annotations - -from typing import Any - -import zigpy.types as t -from zigpy.zcl.clusters.closures import ConfigStatus, DoorLock, Shade, WindowCovering - -from homeassistant.core import callback - -from .. import registries -from ..const import REPORT_CONFIG_IMMEDIATE, SIGNAL_ATTR_UPDATED -from . import AttrReportConfig, ClientClusterHandler, ClusterHandler - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(DoorLock.cluster_id) -class DoorLockClusterHandler(ClusterHandler): - """Door lock cluster handler.""" - - _value_attribute = 0 - REPORT_CONFIG = ( - AttrReportConfig( - attr=DoorLock.AttributeDefs.lock_state.name, - config=REPORT_CONFIG_IMMEDIATE, - ), - ) - - async def async_update(self): - """Retrieve latest state.""" - result = await self.get_attribute_value( - DoorLock.AttributeDefs.lock_state.name, from_cache=True - ) - if result is not None: - self.async_send_signal( - f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", - DoorLock.AttributeDefs.lock_state.id, - DoorLock.AttributeDefs.lock_state.name, - result, - ) - - @callback - def cluster_command(self, tsn, command_id, args): - """Handle a cluster command received on this cluster.""" - - if ( - self._cluster.client_commands is None - or self._cluster.client_commands.get(command_id) is None - ): - return - - command_name = self._cluster.client_commands[command_id].name - - if command_name == DoorLock.ClientCommandDefs.operation_event_notification.name: - self.zha_send_event( - command_name, - { - "source": args[0].name, - "operation": args[1].name, - "code_slot": (args[2] + 1), # start code slots at 1 - }, - ) - - @callback - def attribute_updated(self, attrid: int, value: Any, _: Any) -> None: - """Handle attribute update from lock cluster.""" - attr_name = self._get_attribute_name(attrid) - self.debug( - "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value - ) - if attrid == self._value_attribute: - self.async_send_signal( - f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", attrid, attr_name, value - ) - - async def async_set_user_code(self, code_slot: int, user_code: str) -> None: - """Set the user code for the code slot.""" - - await self.set_pin_code( - code_slot - 1, # start code slots at 1, Zigbee internals use 0 - DoorLock.UserStatus.Enabled, - DoorLock.UserType.Unrestricted, - user_code, - ) - - async def async_enable_user_code(self, code_slot: int) -> None: - """Enable the code slot.""" - - await self.set_user_status(code_slot - 1, DoorLock.UserStatus.Enabled) - - async def async_disable_user_code(self, code_slot: int) -> None: - """Disable the code slot.""" - - await self.set_user_status(code_slot - 1, DoorLock.UserStatus.Disabled) - - async def async_get_user_code(self, code_slot: int) -> int: - """Get the user code from the code slot.""" - - return await self.get_pin_code(code_slot - 1) - - async def async_clear_user_code(self, code_slot: int) -> None: - """Clear the code slot.""" - - await self.clear_pin_code(code_slot - 1) - - async def async_clear_all_user_codes(self) -> None: - """Clear all code slots.""" - - await self.clear_all_pin_codes() - - async def async_set_user_type(self, code_slot: int, user_type: str) -> None: - """Set user type.""" - - await self.set_user_type(code_slot - 1, user_type) - - async def async_get_user_type(self, code_slot: int) -> str: - """Get user type.""" - - return await self.get_user_type(code_slot - 1) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Shade.cluster_id) -class ShadeClusterHandler(ClusterHandler): - """Shade cluster handler.""" - - -@registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(WindowCovering.cluster_id) -class WindowCoveringClientClusterHandler(ClientClusterHandler): - """Window client cluster handler.""" - - -@registries.BINDABLE_CLUSTERS.register(WindowCovering.cluster_id) -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(WindowCovering.cluster_id) -class WindowCoveringClusterHandler(ClusterHandler): - """Window cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=WindowCovering.AttributeDefs.current_position_lift_percentage.name, - config=REPORT_CONFIG_IMMEDIATE, - ), - AttrReportConfig( - attr=WindowCovering.AttributeDefs.current_position_tilt_percentage.name, - config=REPORT_CONFIG_IMMEDIATE, - ), - ) - - ZCL_INIT_ATTRS = { - WindowCovering.AttributeDefs.window_covering_type.name: True, - WindowCovering.AttributeDefs.window_covering_mode.name: True, - WindowCovering.AttributeDefs.config_status.name: True, - WindowCovering.AttributeDefs.installed_closed_limit_lift.name: True, - WindowCovering.AttributeDefs.installed_closed_limit_tilt.name: True, - WindowCovering.AttributeDefs.installed_open_limit_lift.name: True, - WindowCovering.AttributeDefs.installed_open_limit_tilt.name: True, - } - - async def async_update(self): - """Retrieve latest state.""" - results = await self.get_attributes( - [ - WindowCovering.AttributeDefs.current_position_lift_percentage.name, - WindowCovering.AttributeDefs.current_position_tilt_percentage.name, - ], - from_cache=False, - only_cache=False, - ) - self.debug( - "read current_position_lift_percentage and current_position_tilt_percentage - results: %s", - results, - ) - if ( - results - and results.get( - WindowCovering.AttributeDefs.current_position_lift_percentage.name - ) - is not None - ): - # the 100 - value is because we need to invert the value before giving it to the entity - self.async_send_signal( - f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", - WindowCovering.AttributeDefs.current_position_lift_percentage.id, - WindowCovering.AttributeDefs.current_position_lift_percentage.name, - 100 - - results.get( - WindowCovering.AttributeDefs.current_position_lift_percentage.name - ), - ) - if ( - results - and results.get( - WindowCovering.AttributeDefs.current_position_tilt_percentage.name - ) - is not None - ): - # the 100 - value is because we need to invert the value before giving it to the entity - self.async_send_signal( - f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", - WindowCovering.AttributeDefs.current_position_tilt_percentage.id, - WindowCovering.AttributeDefs.current_position_tilt_percentage.name, - 100 - - results.get( - WindowCovering.AttributeDefs.current_position_tilt_percentage.name - ), - ) - - @property - def inverted(self): - """Return true if the window covering is inverted.""" - config_status = self.cluster.get( - WindowCovering.AttributeDefs.config_status.name - ) - return ( - config_status is not None - and ConfigStatus.Open_up_commands_reversed in ConfigStatus(config_status) - ) - - @property - def current_position_lift_percentage(self) -> t.uint16_t | None: - """Return the current lift percentage of the window covering.""" - lift_percentage = self.cluster.get( - WindowCovering.AttributeDefs.current_position_lift_percentage.name - ) - if lift_percentage is not None: - # the 100 - value is because we need to invert the value before giving it to the entity - lift_percentage = 100 - lift_percentage - return lift_percentage - - @property - def current_position_tilt_percentage(self) -> t.uint16_t | None: - """Return the current tilt percentage of the window covering.""" - tilt_percentage = self.cluster.get( - WindowCovering.AttributeDefs.current_position_tilt_percentage.name - ) - if tilt_percentage is not None: - # the 100 - value is because we need to invert the value before giving it to the entity - tilt_percentage = 100 - tilt_percentage - return tilt_percentage - - @property - def installed_open_limit_lift(self) -> t.uint16_t | None: - """Return the installed open lift limit of the window covering.""" - return self.cluster.get( - WindowCovering.AttributeDefs.installed_open_limit_lift.name - ) - - @property - def installed_closed_limit_lift(self) -> t.uint16_t | None: - """Return the installed closed lift limit of the window covering.""" - return self.cluster.get( - WindowCovering.AttributeDefs.installed_closed_limit_lift.name - ) - - @property - def installed_open_limit_tilt(self) -> t.uint16_t | None: - """Return the installed open tilt limit of the window covering.""" - return self.cluster.get( - WindowCovering.AttributeDefs.installed_open_limit_tilt.name - ) - - @property - def installed_closed_limit_tilt(self) -> t.uint16_t | None: - """Return the installed closed tilt limit of the window covering.""" - return self.cluster.get( - WindowCovering.AttributeDefs.installed_closed_limit_tilt.name - ) - - @property - def window_covering_type(self) -> WindowCovering.WindowCoveringType | None: - """Return the window covering type.""" - return self.cluster.get(WindowCovering.AttributeDefs.window_covering_type.name) diff --git a/homeassistant/components/zha/core/cluster_handlers/general.py b/homeassistant/components/zha/core/cluster_handlers/general.py deleted file mode 100644 index 438fc6b1723..00000000000 --- a/homeassistant/components/zha/core/cluster_handlers/general.py +++ /dev/null @@ -1,690 +0,0 @@ -"""General cluster handlers module for Zigbee Home Automation.""" - -from __future__ import annotations - -from collections.abc import Coroutine -from typing import TYPE_CHECKING, Any - -from zhaquirks.quirk_ids import TUYA_PLUG_ONOFF -import zigpy.exceptions -import zigpy.types as t -import zigpy.zcl -from zigpy.zcl.clusters.general import ( - Alarms, - AnalogInput, - AnalogOutput, - AnalogValue, - ApplianceControl, - Basic, - BinaryInput, - BinaryOutput, - BinaryValue, - Commissioning, - DeviceTemperature, - GreenPowerProxy, - Groups, - Identify, - LevelControl, - MultistateInput, - MultistateOutput, - MultistateValue, - OnOff, - OnOffConfiguration, - Ota, - Partition, - PollControl, - PowerConfiguration, - PowerProfile, - RSSILocation, - Scenes, - Time, -) -from zigpy.zcl.foundation import Status - -from homeassistant.core import callback -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.event import async_call_later - -from .. import registries -from ..const import ( - REPORT_CONFIG_ASAP, - REPORT_CONFIG_BATTERY_SAVE, - REPORT_CONFIG_DEFAULT, - REPORT_CONFIG_IMMEDIATE, - REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_MIN_INT, - SIGNAL_ATTR_UPDATED, - SIGNAL_MOVE_LEVEL, - SIGNAL_SET_LEVEL, - SIGNAL_UPDATE_DEVICE, -) -from . import ( - AttrReportConfig, - ClientClusterHandler, - ClusterHandler, - parse_and_log_command, -) -from .helpers import is_hue_motion_sensor - -if TYPE_CHECKING: - from ..endpoint import Endpoint - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Alarms.cluster_id) -class AlarmsClusterHandler(ClusterHandler): - """Alarms cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(AnalogInput.cluster_id) -class AnalogInputClusterHandler(ClusterHandler): - """Analog Input cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=AnalogInput.AttributeDefs.present_value.name, - config=REPORT_CONFIG_DEFAULT, - ), - ) - - -@registries.BINDABLE_CLUSTERS.register(AnalogOutput.cluster_id) -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(AnalogOutput.cluster_id) -class AnalogOutputClusterHandler(ClusterHandler): - """Analog Output cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=AnalogOutput.AttributeDefs.present_value.name, - config=REPORT_CONFIG_DEFAULT, - ), - ) - ZCL_INIT_ATTRS = { - AnalogOutput.AttributeDefs.min_present_value.name: True, - AnalogOutput.AttributeDefs.max_present_value.name: True, - AnalogOutput.AttributeDefs.resolution.name: True, - AnalogOutput.AttributeDefs.relinquish_default.name: True, - AnalogOutput.AttributeDefs.description.name: True, - AnalogOutput.AttributeDefs.engineering_units.name: True, - AnalogOutput.AttributeDefs.application_type.name: True, - } - - @property - def present_value(self) -> float | None: - """Return cached value of present_value.""" - return self.cluster.get(AnalogOutput.AttributeDefs.present_value.name) - - @property - def min_present_value(self) -> float | None: - """Return cached value of min_present_value.""" - return self.cluster.get(AnalogOutput.AttributeDefs.min_present_value.name) - - @property - def max_present_value(self) -> float | None: - """Return cached value of max_present_value.""" - return self.cluster.get(AnalogOutput.AttributeDefs.max_present_value.name) - - @property - def resolution(self) -> float | None: - """Return cached value of resolution.""" - return self.cluster.get(AnalogOutput.AttributeDefs.resolution.name) - - @property - def relinquish_default(self) -> float | None: - """Return cached value of relinquish_default.""" - return self.cluster.get(AnalogOutput.AttributeDefs.relinquish_default.name) - - @property - def description(self) -> str | None: - """Return cached value of description.""" - return self.cluster.get(AnalogOutput.AttributeDefs.description.name) - - @property - def engineering_units(self) -> int | None: - """Return cached value of engineering_units.""" - return self.cluster.get(AnalogOutput.AttributeDefs.engineering_units.name) - - @property - def application_type(self) -> int | None: - """Return cached value of application_type.""" - return self.cluster.get(AnalogOutput.AttributeDefs.application_type.name) - - async def async_set_present_value(self, value: float) -> None: - """Update present_value.""" - await self.write_attributes_safe( - {AnalogOutput.AttributeDefs.present_value.name: value} - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(AnalogValue.cluster_id) -class AnalogValueClusterHandler(ClusterHandler): - """Analog Value cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=AnalogValue.AttributeDefs.present_value.name, - config=REPORT_CONFIG_DEFAULT, - ), - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(ApplianceControl.cluster_id) -class ApplianceControlClusterHandler(ClusterHandler): - """Appliance Control cluster handler.""" - - -@registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(Basic.cluster_id) -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Basic.cluster_id) -class BasicClusterHandler(ClusterHandler): - """Cluster handler to interact with the basic cluster.""" - - UNKNOWN = 0 - BATTERY = 3 - BIND: bool = False - - POWER_SOURCES = { - UNKNOWN: "Unknown", - 1: "Mains (single phase)", - 2: "Mains (3 phase)", - BATTERY: "Battery", - 4: "DC source", - 5: "Emergency mains constantly powered", - 6: "Emergency mains and transfer switch", - } - - def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None: - """Initialize Basic cluster handler.""" - super().__init__(cluster, endpoint) - if is_hue_motion_sensor(self) and self.cluster.endpoint.endpoint_id == 2: - self.ZCL_INIT_ATTRS = self.ZCL_INIT_ATTRS.copy() - self.ZCL_INIT_ATTRS["trigger_indicator"] = True - elif ( - self.cluster.endpoint.manufacturer == "TexasInstruments" - and self.cluster.endpoint.model == "ti.router" - ): - self.ZCL_INIT_ATTRS = self.ZCL_INIT_ATTRS.copy() - self.ZCL_INIT_ATTRS["transmit_power"] = True - elif self.cluster.endpoint.model == "lumi.curtain.agl001": - self.ZCL_INIT_ATTRS = self.ZCL_INIT_ATTRS.copy() - self.ZCL_INIT_ATTRS["power_source"] = True - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(BinaryInput.cluster_id) -class BinaryInputClusterHandler(ClusterHandler): - """Binary Input cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=BinaryInput.AttributeDefs.present_value.name, - config=REPORT_CONFIG_DEFAULT, - ), - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(BinaryOutput.cluster_id) -class BinaryOutputClusterHandler(ClusterHandler): - """Binary Output cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=BinaryOutput.AttributeDefs.present_value.name, - config=REPORT_CONFIG_DEFAULT, - ), - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(BinaryValue.cluster_id) -class BinaryValueClusterHandler(ClusterHandler): - """Binary Value cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=BinaryValue.AttributeDefs.present_value.name, - config=REPORT_CONFIG_DEFAULT, - ), - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Commissioning.cluster_id) -class CommissioningClusterHandler(ClusterHandler): - """Commissioning cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(DeviceTemperature.cluster_id) -class DeviceTemperatureClusterHandler(ClusterHandler): - """Device Temperature cluster handler.""" - - REPORT_CONFIG = ( - { - "attr": DeviceTemperature.AttributeDefs.current_temperature.name, - "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50), - }, - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(GreenPowerProxy.cluster_id) -class GreenPowerProxyClusterHandler(ClusterHandler): - """Green Power Proxy cluster handler.""" - - BIND: bool = False - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Groups.cluster_id) -class GroupsClusterHandler(ClusterHandler): - """Groups cluster handler.""" - - BIND: bool = False - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Identify.cluster_id) -class IdentifyClusterHandler(ClusterHandler): - """Identify cluster handler.""" - - BIND: bool = False - - @callback - def cluster_command(self, tsn, command_id, args): - """Handle commands received to this cluster.""" - cmd = parse_and_log_command(self, tsn, command_id, args) - - if cmd == Identify.ServerCommandDefs.trigger_effect.name: - self.async_send_signal(f"{self.unique_id}_{cmd}", args[0]) - - -@registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(LevelControl.cluster_id) -class LevelControlClientClusterHandler(ClientClusterHandler): - """LevelControl client cluster.""" - - -@registries.BINDABLE_CLUSTERS.register(LevelControl.cluster_id) -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(LevelControl.cluster_id) -class LevelControlClusterHandler(ClusterHandler): - """Cluster handler for the LevelControl Zigbee cluster.""" - - CURRENT_LEVEL = 0 - REPORT_CONFIG = ( - AttrReportConfig( - attr=LevelControl.AttributeDefs.current_level.name, - config=REPORT_CONFIG_ASAP, - ), - ) - ZCL_INIT_ATTRS = { - LevelControl.AttributeDefs.on_off_transition_time.name: True, - LevelControl.AttributeDefs.on_level.name: True, - LevelControl.AttributeDefs.on_transition_time.name: True, - LevelControl.AttributeDefs.off_transition_time.name: True, - LevelControl.AttributeDefs.default_move_rate.name: True, - LevelControl.AttributeDefs.start_up_current_level.name: True, - } - - @property - def current_level(self) -> int | None: - """Return cached value of the current_level attribute.""" - return self.cluster.get(LevelControl.AttributeDefs.current_level.name) - - @callback - def cluster_command(self, tsn, command_id, args): - """Handle commands received to this cluster.""" - cmd = parse_and_log_command(self, tsn, command_id, args) - - if cmd in ( - LevelControl.ServerCommandDefs.move_to_level.name, - LevelControl.ServerCommandDefs.move_to_level_with_on_off.name, - ): - self.dispatch_level_change(SIGNAL_SET_LEVEL, args[0]) - elif cmd in ( - LevelControl.ServerCommandDefs.move.name, - LevelControl.ServerCommandDefs.move_with_on_off.name, - ): - # We should dim slowly -- for now, just step once - rate = args[1] - if args[0] == 0xFF: - rate = 10 # Should read default move rate - self.dispatch_level_change(SIGNAL_MOVE_LEVEL, -rate if args[0] else rate) - elif cmd in ( - LevelControl.ServerCommandDefs.step.name, - LevelControl.ServerCommandDefs.step_with_on_off.name, - ): - # Step (technically may change on/off) - self.dispatch_level_change( - SIGNAL_MOVE_LEVEL, -args[1] if args[0] else args[1] - ) - - @callback - def attribute_updated(self, attrid: int, value: Any, _: Any) -> None: - """Handle attribute updates on this cluster.""" - self.debug("received attribute: %s update with value: %s", attrid, value) - if attrid == self.CURRENT_LEVEL: - self.dispatch_level_change(SIGNAL_SET_LEVEL, value) - - def dispatch_level_change(self, command, level): - """Dispatch level change.""" - self.async_send_signal(f"{self.unique_id}_{command}", level) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(MultistateInput.cluster_id) -class MultistateInputClusterHandler(ClusterHandler): - """Multistate Input cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=MultistateInput.AttributeDefs.present_value.name, - config=REPORT_CONFIG_DEFAULT, - ), - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(MultistateOutput.cluster_id) -class MultistateOutputClusterHandler(ClusterHandler): - """Multistate Output cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=MultistateOutput.AttributeDefs.present_value.name, - config=REPORT_CONFIG_DEFAULT, - ), - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(MultistateValue.cluster_id) -class MultistateValueClusterHandler(ClusterHandler): - """Multistate Value cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=MultistateValue.AttributeDefs.present_value.name, - config=REPORT_CONFIG_DEFAULT, - ), - ) - - -@registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(OnOff.cluster_id) -class OnOffClientClusterHandler(ClientClusterHandler): - """OnOff client cluster handler.""" - - -@registries.BINDABLE_CLUSTERS.register(OnOff.cluster_id) -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(OnOff.cluster_id) -class OnOffClusterHandler(ClusterHandler): - """Cluster handler for the OnOff Zigbee cluster.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=OnOff.AttributeDefs.on_off.name, config=REPORT_CONFIG_IMMEDIATE - ), - ) - ZCL_INIT_ATTRS = { - OnOff.AttributeDefs.start_up_on_off.name: True, - } - - def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None: - """Initialize OnOffClusterHandler.""" - super().__init__(cluster, endpoint) - self._off_listener = None - - if endpoint.device.quirk_id == TUYA_PLUG_ONOFF: - self.ZCL_INIT_ATTRS = self.ZCL_INIT_ATTRS.copy() - self.ZCL_INIT_ATTRS["backlight_mode"] = True - self.ZCL_INIT_ATTRS["power_on_state"] = True - self.ZCL_INIT_ATTRS["child_lock"] = True - - @classmethod - def matches(cls, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> bool: - """Filter the cluster match for specific devices.""" - return not ( - cluster.endpoint.device.manufacturer == "Konke" - and cluster.endpoint.device.model - in ("3AFE280100510001", "3AFE170100510001") - ) - - @property - def on_off(self) -> bool | None: - """Return cached value of on/off attribute.""" - return self.cluster.get(OnOff.AttributeDefs.on_off.name) - - async def turn_on(self) -> None: - """Turn the on off cluster on.""" - result = await self.on() - if result[1] is not Status.SUCCESS: - raise HomeAssistantError(f"Failed to turn on: {result[1]}") - self.cluster.update_attribute(OnOff.AttributeDefs.on_off.id, t.Bool.true) - - async def turn_off(self) -> None: - """Turn the on off cluster off.""" - result = await self.off() - if result[1] is not Status.SUCCESS: - raise HomeAssistantError(f"Failed to turn off: {result[1]}") - self.cluster.update_attribute(OnOff.AttributeDefs.on_off.id, t.Bool.false) - - @callback - def cluster_command(self, tsn, command_id, args): - """Handle commands received to this cluster.""" - cmd = parse_and_log_command(self, tsn, command_id, args) - - if cmd in ( - OnOff.ServerCommandDefs.off.name, - OnOff.ServerCommandDefs.off_with_effect.name, - ): - self.cluster.update_attribute(OnOff.AttributeDefs.on_off.id, t.Bool.false) - elif cmd in ( - OnOff.ServerCommandDefs.on.name, - OnOff.ServerCommandDefs.on_with_recall_global_scene.name, - ): - self.cluster.update_attribute(OnOff.AttributeDefs.on_off.id, t.Bool.true) - elif cmd == OnOff.ServerCommandDefs.on_with_timed_off.name: - should_accept = args[0] - on_time = args[1] - # 0 is always accept 1 is only accept when already on - if should_accept == 0 or (should_accept == 1 and bool(self.on_off)): - if self._off_listener is not None: - self._off_listener() - self._off_listener = None - self.cluster.update_attribute( - OnOff.AttributeDefs.on_off.id, t.Bool.true - ) - if on_time > 0: - self._off_listener = async_call_later( - self._endpoint.device.hass, - (on_time / 10), # value is in 10ths of a second - self.set_to_off, - ) - elif cmd == "toggle": - self.cluster.update_attribute( - OnOff.AttributeDefs.on_off.id, not bool(self.on_off) - ) - - @callback - def set_to_off(self, *_): - """Set the state to off.""" - self._off_listener = None - self.cluster.update_attribute(OnOff.AttributeDefs.on_off.id, t.Bool.false) - - @callback - def attribute_updated(self, attrid: int, value: Any, _: Any) -> None: - """Handle attribute updates on this cluster.""" - if attrid == OnOff.AttributeDefs.on_off.id: - self.async_send_signal( - f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", - attrid, - OnOff.AttributeDefs.on_off.name, - value, - ) - - async def async_update(self): - """Initialize cluster handler.""" - if self.cluster.is_client: - return - from_cache = not self._endpoint.device.is_mains_powered - self.debug("attempting to update onoff state - from cache: %s", from_cache) - await self.get_attribute_value( - OnOff.AttributeDefs.on_off.id, from_cache=from_cache - ) - await super().async_update() - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(OnOffConfiguration.cluster_id) -class OnOffConfigurationClusterHandler(ClusterHandler): - """OnOff Configuration cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Ota.cluster_id) -class OtaClusterHandler(ClusterHandler): - """OTA cluster handler.""" - - BIND: bool = False - - # Some devices have this cluster in the wrong collection (e.g. Third Reality) - ZCL_INIT_ATTRS = { - Ota.AttributeDefs.current_file_version.name: True, - } - - @property - def current_file_version(self) -> int | None: - """Return cached value of current_file_version attribute.""" - return self.cluster.get(Ota.AttributeDefs.current_file_version.name) - - -@registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(Ota.cluster_id) -class OtaClientClusterHandler(ClientClusterHandler): - """OTA client cluster handler.""" - - BIND: bool = False - - ZCL_INIT_ATTRS = { - Ota.AttributeDefs.current_file_version.name: True, - } - - @callback - def attribute_updated(self, attrid: int, value: Any, timestamp: Any) -> None: - """Handle an attribute updated on this cluster.""" - # We intentionally avoid the `ClientClusterHandler` attribute update handler: - # it emits a logbook event on every update, which pollutes the logbook - ClusterHandler.attribute_updated(self, attrid, value, timestamp) - - @property - def current_file_version(self) -> int | None: - """Return cached value of current_file_version attribute.""" - return self.cluster.get(Ota.AttributeDefs.current_file_version.name) - - @callback - def cluster_command( - self, tsn: int, command_id: int, args: list[Any] | None - ) -> None: - """Handle OTA commands.""" - if command_id not in self.cluster.server_commands: - return - - signal_id = self._endpoint.unique_id.split("-")[0] - cmd_name = self.cluster.server_commands[command_id].name - - if cmd_name == Ota.ServerCommandDefs.query_next_image.name: - assert args - - current_file_version = args[3] - self.cluster.update_attribute( - Ota.AttributeDefs.current_file_version.id, current_file_version - ) - self.async_send_signal( - SIGNAL_UPDATE_DEVICE.format(signal_id), current_file_version - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Partition.cluster_id) -class PartitionClusterHandler(ClusterHandler): - """Partition cluster handler.""" - - -@registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(PollControl.cluster_id) -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(PollControl.cluster_id) -class PollControlClusterHandler(ClusterHandler): - """Poll Control cluster handler.""" - - CHECKIN_INTERVAL = 55 * 60 * 4 # 55min - CHECKIN_FAST_POLL_TIMEOUT = 2 * 4 # 2s - LONG_POLL = 6 * 4 # 6s - _IGNORED_MANUFACTURER_ID = { - 4476, - } # IKEA - - async def async_configure_cluster_handler_specific(self) -> None: - """Configure cluster handler: set check-in interval.""" - await self.write_attributes_safe( - {PollControl.AttributeDefs.checkin_interval.name: self.CHECKIN_INTERVAL} - ) - - @callback - def cluster_command( - self, tsn: int, command_id: int, args: list[Any] | None - ) -> None: - """Handle commands received to this cluster.""" - if command_id in self.cluster.client_commands: - cmd_name = self.cluster.client_commands[command_id].name - else: - cmd_name = command_id - - self.debug("Received %s tsn command '%s': %s", tsn, cmd_name, args) - self.zha_send_event(cmd_name, args) - if cmd_name == PollControl.ClientCommandDefs.checkin.name: - self.cluster.create_catching_task(self.check_in_response(tsn)) - - async def check_in_response(self, tsn: int) -> None: - """Respond to checkin command.""" - await self.checkin_response(True, self.CHECKIN_FAST_POLL_TIMEOUT, tsn=tsn) - if self._endpoint.device.manufacturer_code not in self._IGNORED_MANUFACTURER_ID: - await self.set_long_poll_interval(self.LONG_POLL) - await self.fast_poll_stop() - - @callback - def skip_manufacturer_id(self, manufacturer_code: int) -> None: - """Block a specific manufacturer id from changing default polling.""" - self._IGNORED_MANUFACTURER_ID.add(manufacturer_code) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(PowerConfiguration.cluster_id) -class PowerConfigurationClusterHandler(ClusterHandler): - """Cluster handler for the zigbee power configuration cluster.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=PowerConfiguration.AttributeDefs.battery_voltage.name, - config=REPORT_CONFIG_BATTERY_SAVE, - ), - AttrReportConfig( - attr=PowerConfiguration.AttributeDefs.battery_percentage_remaining.name, - config=REPORT_CONFIG_BATTERY_SAVE, - ), - ) - - def async_initialize_cluster_handler_specific(self, from_cache: bool) -> Coroutine: - """Initialize cluster handler specific attrs.""" - attributes = [ - PowerConfiguration.AttributeDefs.battery_size.name, - PowerConfiguration.AttributeDefs.battery_quantity.name, - ] - return self.get_attributes( - attributes, from_cache=from_cache, only_cache=from_cache - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(PowerProfile.cluster_id) -class PowerProfileClusterHandler(ClusterHandler): - """Power Profile cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(RSSILocation.cluster_id) -class RSSILocationClusterHandler(ClusterHandler): - """RSSI Location cluster handler.""" - - -@registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(Scenes.cluster_id) -class ScenesClientClusterHandler(ClientClusterHandler): - """Scenes cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Scenes.cluster_id) -class ScenesClusterHandler(ClusterHandler): - """Scenes cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Time.cluster_id) -class TimeClusterHandler(ClusterHandler): - """Time cluster handler.""" diff --git a/homeassistant/components/zha/core/cluster_handlers/helpers.py b/homeassistant/components/zha/core/cluster_handlers/helpers.py deleted file mode 100644 index 46557bf23a8..00000000000 --- a/homeassistant/components/zha/core/cluster_handlers/helpers.py +++ /dev/null @@ -1,23 +0,0 @@ -"""Helpers for use with ZHA Zigbee cluster handlers.""" - -from . import ClusterHandler - - -def is_hue_motion_sensor(cluster_handler: ClusterHandler) -> bool: - """Return true if the manufacturer and model match known Hue motion sensor models.""" - return cluster_handler.cluster.endpoint.manufacturer in ( - "Philips", - "Signify Netherlands B.V.", - ) and cluster_handler.cluster.endpoint.model in ( - "SML001", - "SML002", - "SML003", - "SML004", - ) - - -def is_sonoff_presence_sensor(cluster_handler: ClusterHandler) -> bool: - """Return true if the manufacturer and model match known Sonoff sensor models.""" - return cluster_handler.cluster.endpoint.manufacturer in ( - "SONOFF", - ) and cluster_handler.cluster.endpoint.model in ("SNZB-06P",) diff --git a/homeassistant/components/zha/core/cluster_handlers/homeautomation.py b/homeassistant/components/zha/core/cluster_handlers/homeautomation.py deleted file mode 100644 index b287cb98f6a..00000000000 --- a/homeassistant/components/zha/core/cluster_handlers/homeautomation.py +++ /dev/null @@ -1,236 +0,0 @@ -"""Home automation cluster handlers module for Zigbee Home Automation.""" - -from __future__ import annotations - -import enum - -from zigpy.zcl.clusters.homeautomation import ( - ApplianceEventAlerts, - ApplianceIdentification, - ApplianceStatistics, - Diagnostic, - ElectricalMeasurement, - MeterIdentification, -) - -from .. import registries -from ..const import ( - CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT, - REPORT_CONFIG_DEFAULT, - REPORT_CONFIG_OP, - SIGNAL_ATTR_UPDATED, -) -from . import AttrReportConfig, ClusterHandler - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(ApplianceEventAlerts.cluster_id) -class ApplianceEventAlertsClusterHandler(ClusterHandler): - """Appliance Event Alerts cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(ApplianceIdentification.cluster_id) -class ApplianceIdentificationClusterHandler(ClusterHandler): - """Appliance Identification cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(ApplianceStatistics.cluster_id) -class ApplianceStatisticsClusterHandler(ClusterHandler): - """Appliance Statistics cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Diagnostic.cluster_id) -class DiagnosticClusterHandler(ClusterHandler): - """Diagnostic cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(ElectricalMeasurement.cluster_id) -class ElectricalMeasurementClusterHandler(ClusterHandler): - """Cluster handler that polls active power level.""" - - CLUSTER_HANDLER_NAME = CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT - - class MeasurementType(enum.IntFlag): - """Measurement types.""" - - ACTIVE_MEASUREMENT = 1 - REACTIVE_MEASUREMENT = 2 - APPARENT_MEASUREMENT = 4 - PHASE_A_MEASUREMENT = 8 - PHASE_B_MEASUREMENT = 16 - PHASE_C_MEASUREMENT = 32 - DC_MEASUREMENT = 64 - HARMONICS_MEASUREMENT = 128 - POWER_QUALITY_MEASUREMENT = 256 - - REPORT_CONFIG = ( - AttrReportConfig( - attr=ElectricalMeasurement.AttributeDefs.active_power.name, - config=REPORT_CONFIG_OP, - ), - AttrReportConfig( - attr=ElectricalMeasurement.AttributeDefs.active_power_max.name, - config=REPORT_CONFIG_DEFAULT, - ), - AttrReportConfig( - attr=ElectricalMeasurement.AttributeDefs.apparent_power.name, - config=REPORT_CONFIG_OP, - ), - AttrReportConfig( - attr=ElectricalMeasurement.AttributeDefs.rms_current.name, - config=REPORT_CONFIG_OP, - ), - AttrReportConfig( - attr=ElectricalMeasurement.AttributeDefs.rms_current_max.name, - config=REPORT_CONFIG_DEFAULT, - ), - AttrReportConfig( - attr=ElectricalMeasurement.AttributeDefs.rms_voltage.name, - config=REPORT_CONFIG_OP, - ), - AttrReportConfig( - attr=ElectricalMeasurement.AttributeDefs.rms_voltage_max.name, - config=REPORT_CONFIG_DEFAULT, - ), - AttrReportConfig( - attr=ElectricalMeasurement.AttributeDefs.ac_frequency.name, - config=REPORT_CONFIG_OP, - ), - AttrReportConfig( - attr=ElectricalMeasurement.AttributeDefs.ac_frequency_max.name, - config=REPORT_CONFIG_DEFAULT, - ), - ) - ZCL_INIT_ATTRS = { - ElectricalMeasurement.AttributeDefs.ac_current_divisor.name: True, - ElectricalMeasurement.AttributeDefs.ac_current_multiplier.name: True, - ElectricalMeasurement.AttributeDefs.ac_power_divisor.name: True, - ElectricalMeasurement.AttributeDefs.ac_power_multiplier.name: True, - ElectricalMeasurement.AttributeDefs.ac_voltage_divisor.name: True, - ElectricalMeasurement.AttributeDefs.ac_voltage_multiplier.name: True, - ElectricalMeasurement.AttributeDefs.ac_frequency_divisor.name: True, - ElectricalMeasurement.AttributeDefs.ac_frequency_multiplier.name: True, - ElectricalMeasurement.AttributeDefs.measurement_type.name: True, - ElectricalMeasurement.AttributeDefs.power_divisor.name: True, - ElectricalMeasurement.AttributeDefs.power_multiplier.name: True, - ElectricalMeasurement.AttributeDefs.power_factor.name: True, - } - - async def async_update(self): - """Retrieve latest state.""" - self.debug("async_update") - - # This is a polling cluster handler. Don't allow cache. - attrs = [ - a["attr"] - for a in self.REPORT_CONFIG - if a["attr"] not in self.cluster.unsupported_attributes - ] - result = await self.get_attributes(attrs, from_cache=False, only_cache=False) - if result: - for attr, value in result.items(): - self.async_send_signal( - f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", - self.cluster.find_attribute(attr).id, - attr, - value, - ) - - @property - def ac_current_divisor(self) -> int: - """Return ac current divisor.""" - return ( - self.cluster.get( - ElectricalMeasurement.AttributeDefs.ac_current_divisor.name - ) - or 1 - ) - - @property - def ac_current_multiplier(self) -> int: - """Return ac current multiplier.""" - return ( - self.cluster.get( - ElectricalMeasurement.AttributeDefs.ac_current_multiplier.name - ) - or 1 - ) - - @property - def ac_voltage_divisor(self) -> int: - """Return ac voltage divisor.""" - return ( - self.cluster.get( - ElectricalMeasurement.AttributeDefs.ac_voltage_divisor.name - ) - or 1 - ) - - @property - def ac_voltage_multiplier(self) -> int: - """Return ac voltage multiplier.""" - return ( - self.cluster.get( - ElectricalMeasurement.AttributeDefs.ac_voltage_multiplier.name - ) - or 1 - ) - - @property - def ac_frequency_divisor(self) -> int: - """Return ac frequency divisor.""" - return ( - self.cluster.get( - ElectricalMeasurement.AttributeDefs.ac_frequency_divisor.name - ) - or 1 - ) - - @property - def ac_frequency_multiplier(self) -> int: - """Return ac frequency multiplier.""" - return ( - self.cluster.get( - ElectricalMeasurement.AttributeDefs.ac_frequency_multiplier.name - ) - or 1 - ) - - @property - def ac_power_divisor(self) -> int: - """Return active power divisor.""" - return self.cluster.get( - ElectricalMeasurement.AttributeDefs.ac_power_divisor.name, - self.cluster.get(ElectricalMeasurement.AttributeDefs.power_divisor.name) - or 1, - ) - - @property - def ac_power_multiplier(self) -> int: - """Return active power divisor.""" - return self.cluster.get( - ElectricalMeasurement.AttributeDefs.ac_power_multiplier.name, - self.cluster.get(ElectricalMeasurement.AttributeDefs.power_multiplier.name) - or 1, - ) - - @property - def measurement_type(self) -> str | None: - """Return Measurement type.""" - if ( - meas_type := self.cluster.get( - ElectricalMeasurement.AttributeDefs.measurement_type.name - ) - ) is None: - return None - - meas_type = self.MeasurementType(meas_type) - return ", ".join( - m.name - for m in self.MeasurementType - if m in meas_type and m.name is not None - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(MeterIdentification.cluster_id) -class MeterIdentificationClusterHandler(ClusterHandler): - """Metering Identification cluster handler.""" diff --git a/homeassistant/components/zha/core/cluster_handlers/hvac.py b/homeassistant/components/zha/core/cluster_handlers/hvac.py deleted file mode 100644 index 1230549832b..00000000000 --- a/homeassistant/components/zha/core/cluster_handlers/hvac.py +++ /dev/null @@ -1,347 +0,0 @@ -"""HVAC cluster handlers module for Zigbee Home Automation. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/integrations/zha/ -""" - -from __future__ import annotations - -from typing import Any - -from zigpy.zcl.clusters.hvac import ( - Dehumidification, - Fan, - Pump, - Thermostat, - UserInterface, -) - -from homeassistant.core import callback - -from .. import registries -from ..const import ( - REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_MIN_INT, - REPORT_CONFIG_OP, - SIGNAL_ATTR_UPDATED, -) -from . import AttrReportConfig, ClusterHandler - -REPORT_CONFIG_CLIMATE = (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 25) -REPORT_CONFIG_CLIMATE_DEMAND = (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 5) -REPORT_CONFIG_CLIMATE_DISCRETE = (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 1) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Dehumidification.cluster_id) -class DehumidificationClusterHandler(ClusterHandler): - """Dehumidification cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Fan.cluster_id) -class FanClusterHandler(ClusterHandler): - """Fan cluster handler.""" - - _value_attribute = 0 - - REPORT_CONFIG = ( - AttrReportConfig(attr=Fan.AttributeDefs.fan_mode.name, config=REPORT_CONFIG_OP), - ) - ZCL_INIT_ATTRS = {Fan.AttributeDefs.fan_mode_sequence.name: True} - - @property - def fan_mode(self) -> int | None: - """Return current fan mode.""" - return self.cluster.get(Fan.AttributeDefs.fan_mode.name) - - @property - def fan_mode_sequence(self) -> int | None: - """Return possible fan mode speeds.""" - return self.cluster.get(Fan.AttributeDefs.fan_mode_sequence.name) - - async def async_set_speed(self, value) -> None: - """Set the speed of the fan.""" - await self.write_attributes_safe({Fan.AttributeDefs.fan_mode.name: value}) - - async def async_update(self) -> None: - """Retrieve latest state.""" - await self.get_attribute_value( - Fan.AttributeDefs.fan_mode.name, from_cache=False - ) - - @callback - def attribute_updated(self, attrid: int, value: Any, _: Any) -> None: - """Handle attribute update from fan cluster.""" - attr_name = self._get_attribute_name(attrid) - self.debug( - "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value - ) - if attr_name == "fan_mode": - self.async_send_signal( - f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", attrid, attr_name, value - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Pump.cluster_id) -class PumpClusterHandler(ClusterHandler): - """Pump cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Thermostat.cluster_id) -class ThermostatClusterHandler(ClusterHandler): - """Thermostat cluster handler.""" - - REPORT_CONFIG: tuple[AttrReportConfig, ...] = ( - AttrReportConfig( - attr=Thermostat.AttributeDefs.local_temperature.name, - config=REPORT_CONFIG_CLIMATE, - ), - AttrReportConfig( - attr=Thermostat.AttributeDefs.occupied_cooling_setpoint.name, - config=REPORT_CONFIG_CLIMATE, - ), - AttrReportConfig( - attr=Thermostat.AttributeDefs.occupied_heating_setpoint.name, - config=REPORT_CONFIG_CLIMATE, - ), - AttrReportConfig( - attr=Thermostat.AttributeDefs.unoccupied_cooling_setpoint.name, - config=REPORT_CONFIG_CLIMATE, - ), - AttrReportConfig( - attr=Thermostat.AttributeDefs.unoccupied_heating_setpoint.name, - config=REPORT_CONFIG_CLIMATE, - ), - AttrReportConfig( - attr=Thermostat.AttributeDefs.running_mode.name, - config=REPORT_CONFIG_CLIMATE, - ), - AttrReportConfig( - attr=Thermostat.AttributeDefs.running_state.name, - config=REPORT_CONFIG_CLIMATE_DEMAND, - ), - AttrReportConfig( - attr=Thermostat.AttributeDefs.system_mode.name, - config=REPORT_CONFIG_CLIMATE, - ), - AttrReportConfig( - attr=Thermostat.AttributeDefs.occupancy.name, - config=REPORT_CONFIG_CLIMATE_DISCRETE, - ), - AttrReportConfig( - attr=Thermostat.AttributeDefs.pi_cooling_demand.name, - config=REPORT_CONFIG_CLIMATE_DEMAND, - ), - AttrReportConfig( - attr=Thermostat.AttributeDefs.pi_heating_demand.name, - config=REPORT_CONFIG_CLIMATE_DEMAND, - ), - ) - ZCL_INIT_ATTRS: dict[str, bool] = { - Thermostat.AttributeDefs.abs_min_heat_setpoint_limit.name: True, - Thermostat.AttributeDefs.abs_max_heat_setpoint_limit.name: True, - Thermostat.AttributeDefs.abs_min_cool_setpoint_limit.name: True, - Thermostat.AttributeDefs.abs_max_cool_setpoint_limit.name: True, - Thermostat.AttributeDefs.ctrl_sequence_of_oper.name: False, - Thermostat.AttributeDefs.max_cool_setpoint_limit.name: True, - Thermostat.AttributeDefs.max_heat_setpoint_limit.name: True, - Thermostat.AttributeDefs.min_cool_setpoint_limit.name: True, - Thermostat.AttributeDefs.min_heat_setpoint_limit.name: True, - Thermostat.AttributeDefs.local_temperature_calibration.name: True, - Thermostat.AttributeDefs.setpoint_change_source.name: True, - } - - @property - def abs_max_cool_setpoint_limit(self) -> int: - """Absolute maximum cooling setpoint.""" - return self.cluster.get( - Thermostat.AttributeDefs.abs_max_cool_setpoint_limit.name, 3200 - ) - - @property - def abs_min_cool_setpoint_limit(self) -> int: - """Absolute minimum cooling setpoint.""" - return self.cluster.get( - Thermostat.AttributeDefs.abs_min_cool_setpoint_limit.name, 1600 - ) - - @property - def abs_max_heat_setpoint_limit(self) -> int: - """Absolute maximum heating setpoint.""" - return self.cluster.get( - Thermostat.AttributeDefs.abs_max_heat_setpoint_limit.name, 3000 - ) - - @property - def abs_min_heat_setpoint_limit(self) -> int: - """Absolute minimum heating setpoint.""" - return self.cluster.get( - Thermostat.AttributeDefs.abs_min_heat_setpoint_limit.name, 700 - ) - - @property - def ctrl_sequence_of_oper(self) -> int: - """Control Sequence of operations attribute.""" - return self.cluster.get( - Thermostat.AttributeDefs.ctrl_sequence_of_oper.name, 0xFF - ) - - @property - def max_cool_setpoint_limit(self) -> int: - """Maximum cooling setpoint.""" - sp_limit = self.cluster.get( - Thermostat.AttributeDefs.max_cool_setpoint_limit.name - ) - if sp_limit is None: - return self.abs_max_cool_setpoint_limit - return sp_limit - - @property - def min_cool_setpoint_limit(self) -> int: - """Minimum cooling setpoint.""" - sp_limit = self.cluster.get( - Thermostat.AttributeDefs.min_cool_setpoint_limit.name - ) - if sp_limit is None: - return self.abs_min_cool_setpoint_limit - return sp_limit - - @property - def max_heat_setpoint_limit(self) -> int: - """Maximum heating setpoint.""" - sp_limit = self.cluster.get( - Thermostat.AttributeDefs.max_heat_setpoint_limit.name - ) - if sp_limit is None: - return self.abs_max_heat_setpoint_limit - return sp_limit - - @property - def min_heat_setpoint_limit(self) -> int: - """Minimum heating setpoint.""" - sp_limit = self.cluster.get( - Thermostat.AttributeDefs.min_heat_setpoint_limit.name - ) - if sp_limit is None: - return self.abs_min_heat_setpoint_limit - return sp_limit - - @property - def local_temperature(self) -> int | None: - """Thermostat temperature.""" - return self.cluster.get(Thermostat.AttributeDefs.local_temperature.name) - - @property - def occupancy(self) -> int | None: - """Is occupancy detected.""" - return self.cluster.get(Thermostat.AttributeDefs.occupancy.name) - - @property - def occupied_cooling_setpoint(self) -> int | None: - """Temperature when room is occupied.""" - return self.cluster.get(Thermostat.AttributeDefs.occupied_cooling_setpoint.name) - - @property - def occupied_heating_setpoint(self) -> int | None: - """Temperature when room is occupied.""" - return self.cluster.get(Thermostat.AttributeDefs.occupied_heating_setpoint.name) - - @property - def pi_cooling_demand(self) -> int: - """Cooling demand.""" - return self.cluster.get(Thermostat.AttributeDefs.pi_cooling_demand.name) - - @property - def pi_heating_demand(self) -> int: - """Heating demand.""" - return self.cluster.get(Thermostat.AttributeDefs.pi_heating_demand.name) - - @property - def running_mode(self) -> int | None: - """Thermostat running mode.""" - return self.cluster.get(Thermostat.AttributeDefs.running_mode.name) - - @property - def running_state(self) -> int | None: - """Thermostat running state, state of heat, cool, fan relays.""" - return self.cluster.get(Thermostat.AttributeDefs.running_state.name) - - @property - def system_mode(self) -> int | None: - """System mode.""" - return self.cluster.get(Thermostat.AttributeDefs.system_mode.name) - - @property - def unoccupied_cooling_setpoint(self) -> int | None: - """Temperature when room is not occupied.""" - return self.cluster.get( - Thermostat.AttributeDefs.unoccupied_cooling_setpoint.name - ) - - @property - def unoccupied_heating_setpoint(self) -> int | None: - """Temperature when room is not occupied.""" - return self.cluster.get( - Thermostat.AttributeDefs.unoccupied_heating_setpoint.name - ) - - @callback - def attribute_updated(self, attrid: int, value: Any, _: Any) -> None: - """Handle attribute update cluster.""" - attr_name = self._get_attribute_name(attrid) - self.debug( - "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value - ) - self.async_send_signal( - f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", - attrid, - attr_name, - value, - ) - - async def async_set_operation_mode(self, mode) -> bool: - """Set Operation mode.""" - await self.write_attributes_safe( - {Thermostat.AttributeDefs.system_mode.name: mode} - ) - return True - - async def async_set_heating_setpoint( - self, temperature: int, is_away: bool = False - ) -> bool: - """Set heating setpoint.""" - attr = ( - Thermostat.AttributeDefs.unoccupied_heating_setpoint.name - if is_away - else Thermostat.AttributeDefs.occupied_heating_setpoint.name - ) - await self.write_attributes_safe({attr: temperature}) - return True - - async def async_set_cooling_setpoint( - self, temperature: int, is_away: bool = False - ) -> bool: - """Set cooling setpoint.""" - attr = ( - Thermostat.AttributeDefs.unoccupied_cooling_setpoint.name - if is_away - else Thermostat.AttributeDefs.occupied_cooling_setpoint.name - ) - await self.write_attributes_safe({attr: temperature}) - return True - - async def get_occupancy(self) -> bool | None: - """Get unreportable occupancy attribute.""" - res, fail = await self.read_attributes( - [Thermostat.AttributeDefs.occupancy.name] - ) - self.debug("read 'occupancy' attr, success: %s, fail: %s", res, fail) - if Thermostat.AttributeDefs.occupancy.name not in res: - return None - return bool(self.occupancy) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(UserInterface.cluster_id) -class UserInterfaceClusterHandler(ClusterHandler): - """User interface (thermostat) cluster handler.""" - - ZCL_INIT_ATTRS = {UserInterface.AttributeDefs.keypad_lockout.name: True} diff --git a/homeassistant/components/zha/core/cluster_handlers/lighting.py b/homeassistant/components/zha/core/cluster_handlers/lighting.py deleted file mode 100644 index bde0fdbb0e7..00000000000 --- a/homeassistant/components/zha/core/cluster_handlers/lighting.py +++ /dev/null @@ -1,196 +0,0 @@ -"""Lighting cluster handlers module for Zigbee Home Automation.""" - -from __future__ import annotations - -from functools import cached_property - -from zigpy.zcl.clusters.lighting import Ballast, Color - -from .. import registries -from ..const import REPORT_CONFIG_DEFAULT -from . import AttrReportConfig, ClientClusterHandler, ClusterHandler - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Ballast.cluster_id) -class BallastClusterHandler(ClusterHandler): - """Ballast cluster handler.""" - - -@registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(Color.cluster_id) -class ColorClientClusterHandler(ClientClusterHandler): - """Color client cluster handler.""" - - -@registries.BINDABLE_CLUSTERS.register(Color.cluster_id) -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Color.cluster_id) -class ColorClusterHandler(ClusterHandler): - """Color cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=Color.AttributeDefs.current_x.name, - config=REPORT_CONFIG_DEFAULT, - ), - AttrReportConfig( - attr=Color.AttributeDefs.current_y.name, - config=REPORT_CONFIG_DEFAULT, - ), - AttrReportConfig( - attr=Color.AttributeDefs.current_hue.name, - config=REPORT_CONFIG_DEFAULT, - ), - AttrReportConfig( - attr=Color.AttributeDefs.current_saturation.name, - config=REPORT_CONFIG_DEFAULT, - ), - AttrReportConfig( - attr=Color.AttributeDefs.color_temperature.name, - config=REPORT_CONFIG_DEFAULT, - ), - ) - MAX_MIREDS: int = 500 - MIN_MIREDS: int = 153 - ZCL_INIT_ATTRS = { - Color.AttributeDefs.color_mode.name: False, - Color.AttributeDefs.color_temp_physical_min.name: True, - Color.AttributeDefs.color_temp_physical_max.name: True, - Color.AttributeDefs.color_capabilities.name: True, - Color.AttributeDefs.color_loop_active.name: False, - Color.AttributeDefs.enhanced_current_hue.name: False, - Color.AttributeDefs.start_up_color_temperature.name: True, - Color.AttributeDefs.options.name: True, - } - - @cached_property - def color_capabilities(self) -> Color.ColorCapabilities: - """Return ZCL color capabilities of the light.""" - color_capabilities = self.cluster.get( - Color.AttributeDefs.color_capabilities.name - ) - if color_capabilities is None: - return Color.ColorCapabilities.XY_attributes - return Color.ColorCapabilities(color_capabilities) - - @property - def color_mode(self) -> int | None: - """Return cached value of the color_mode attribute.""" - return self.cluster.get(Color.AttributeDefs.color_mode.name) - - @property - def color_loop_active(self) -> int | None: - """Return cached value of the color_loop_active attribute.""" - return self.cluster.get(Color.AttributeDefs.color_loop_active.name) - - @property - def color_temperature(self) -> int | None: - """Return cached value of color temperature.""" - return self.cluster.get(Color.AttributeDefs.color_temperature.name) - - @property - def current_x(self) -> int | None: - """Return cached value of the current_x attribute.""" - return self.cluster.get(Color.AttributeDefs.current_x.name) - - @property - def current_y(self) -> int | None: - """Return cached value of the current_y attribute.""" - return self.cluster.get(Color.AttributeDefs.current_y.name) - - @property - def current_hue(self) -> int | None: - """Return cached value of the current_hue attribute.""" - return self.cluster.get(Color.AttributeDefs.current_hue.name) - - @property - def enhanced_current_hue(self) -> int | None: - """Return cached value of the enhanced_current_hue attribute.""" - return self.cluster.get(Color.AttributeDefs.enhanced_current_hue.name) - - @property - def current_saturation(self) -> int | None: - """Return cached value of the current_saturation attribute.""" - return self.cluster.get(Color.AttributeDefs.current_saturation.name) - - @property - def min_mireds(self) -> int: - """Return the coldest color_temp that this cluster handler supports.""" - min_mireds = self.cluster.get( - Color.AttributeDefs.color_temp_physical_min.name, self.MIN_MIREDS - ) - if min_mireds == 0: - self.warning( - ( - "[Min mireds is 0, setting to %s] Please open an issue on the" - " quirks repo to have this device corrected" - ), - self.MIN_MIREDS, - ) - min_mireds = self.MIN_MIREDS - return min_mireds - - @property - def max_mireds(self) -> int: - """Return the warmest color_temp that this cluster handler supports.""" - max_mireds = self.cluster.get( - Color.AttributeDefs.color_temp_physical_max.name, self.MAX_MIREDS - ) - if max_mireds == 0: - self.warning( - ( - "[Max mireds is 0, setting to %s] Please open an issue on the" - " quirks repo to have this device corrected" - ), - self.MAX_MIREDS, - ) - max_mireds = self.MAX_MIREDS - return max_mireds - - @property - def hs_supported(self) -> bool: - """Return True if the cluster handler supports hue and saturation.""" - return ( - self.color_capabilities is not None - and Color.ColorCapabilities.Hue_and_saturation in self.color_capabilities - ) - - @property - def enhanced_hue_supported(self) -> bool: - """Return True if the cluster handler supports enhanced hue and saturation.""" - return ( - self.color_capabilities is not None - and Color.ColorCapabilities.Enhanced_hue in self.color_capabilities - ) - - @property - def xy_supported(self) -> bool: - """Return True if the cluster handler supports xy.""" - return ( - self.color_capabilities is not None - and Color.ColorCapabilities.XY_attributes in self.color_capabilities - ) - - @property - def color_temp_supported(self) -> bool: - """Return True if the cluster handler supports color temperature.""" - return ( - self.color_capabilities is not None - and Color.ColorCapabilities.Color_temperature in self.color_capabilities - ) or self.color_temperature is not None - - @property - def color_loop_supported(self) -> bool: - """Return True if the cluster handler supports color loop.""" - return ( - self.color_capabilities is not None - and Color.ColorCapabilities.Color_loop in self.color_capabilities - ) - - @property - def options(self) -> Color.Options: - """Return ZCL options of the cluster handler.""" - return Color.Options(self.cluster.get(Color.AttributeDefs.options.name, 0)) - - @property - def execute_if_off_supported(self) -> bool: - """Return True if the cluster handler can execute commands when off.""" - return Color.Options.Execute_if_off in self.options diff --git a/homeassistant/components/zha/core/cluster_handlers/lightlink.py b/homeassistant/components/zha/core/cluster_handlers/lightlink.py deleted file mode 100644 index 85ec6905069..00000000000 --- a/homeassistant/components/zha/core/cluster_handlers/lightlink.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Lightlink cluster handlers module for Zigbee Home Automation.""" - -import zigpy.exceptions -from zigpy.zcl.clusters.lightlink import LightLink -from zigpy.zcl.foundation import GENERAL_COMMANDS, GeneralCommand - -from .. import registries -from . import ClusterHandler, ClusterHandlerStatus - - -@registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(LightLink.cluster_id) -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(LightLink.cluster_id) -class LightLinkClusterHandler(ClusterHandler): - """Lightlink cluster handler.""" - - BIND: bool = False - - async def async_configure(self) -> None: - """Add Coordinator to LightLink group.""" - - if self._endpoint.device.skip_configuration: - self._status = ClusterHandlerStatus.CONFIGURED - return - - application = self._endpoint.zigpy_endpoint.device.application - try: - coordinator = application.get_device(application.state.node_info.ieee) - except KeyError: - self.warning("Aborting - unable to locate required coordinator device.") - return - - try: - rsp = await self.cluster.get_group_identifiers(0) - except (zigpy.exceptions.ZigbeeException, TimeoutError) as exc: - self.warning("Couldn't get list of groups: %s", str(exc)) - return - - if isinstance(rsp, GENERAL_COMMANDS[GeneralCommand.Default_Response].schema): - groups = [] - else: - groups = rsp.group_info_records - - if groups: - for group in groups: - self.debug("Adding coordinator to 0x%04x group id", group.group_id) - await coordinator.add_to_group(group.group_id) - else: - await coordinator.add_to_group(0x0000, name="Default Lightlink Group") diff --git a/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py b/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py deleted file mode 100644 index 9d5d68d2c7e..00000000000 --- a/homeassistant/components/zha/core/cluster_handlers/manufacturerspecific.py +++ /dev/null @@ -1,515 +0,0 @@ -"""Manufacturer specific cluster handlers module for Zigbee Home Automation.""" - -from __future__ import annotations - -import logging -from typing import TYPE_CHECKING, Any - -from zhaquirks.inovelli.types import AllLEDEffectType, SingleLEDEffectType -from zhaquirks.quirk_ids import ( - DANFOSS_ALLY_THERMOSTAT, - TUYA_PLUG_MANUFACTURER, - XIAOMI_AQARA_VIBRATION_AQ1, -) -import zigpy.zcl -from zigpy.zcl import clusters -from zigpy.zcl.clusters.closures import DoorLock - -from homeassistant.core import callback - -from .. import registries -from ..const import ( - ATTR_ATTRIBUTE_ID, - ATTR_ATTRIBUTE_NAME, - ATTR_VALUE, - REPORT_CONFIG_ASAP, - REPORT_CONFIG_DEFAULT, - REPORT_CONFIG_IMMEDIATE, - REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_MIN_INT, - SIGNAL_ATTR_UPDATED, - UNKNOWN, -) -from . import AttrReportConfig, ClientClusterHandler, ClusterHandler -from .general import MultistateInputClusterHandler -from .homeautomation import DiagnosticClusterHandler -from .hvac import ThermostatClusterHandler, UserInterfaceClusterHandler - -if TYPE_CHECKING: - from ..endpoint import Endpoint - -_LOGGER = logging.getLogger(__name__) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register( - registries.SMARTTHINGS_HUMIDITY_CLUSTER -) -class SmartThingsHumidityClusterHandler(ClusterHandler): - """Smart Things Humidity cluster handler.""" - - REPORT_CONFIG = ( - { - "attr": "measured_value", - "config": (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50), - }, - ) - - -@registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(0xFD00) -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(0xFD00) -class OsramButtonClusterHandler(ClusterHandler): - """Osram button cluster handler.""" - - REPORT_CONFIG = () - - -@registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(registries.PHILLIPS_REMOTE_CLUSTER) -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(registries.PHILLIPS_REMOTE_CLUSTER) -class PhillipsRemoteClusterHandler(ClusterHandler): - """Phillips remote cluster handler.""" - - REPORT_CONFIG = () - - -@registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(registries.TUYA_MANUFACTURER_CLUSTER) -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register( - registries.TUYA_MANUFACTURER_CLUSTER -) -class TuyaClusterHandler(ClusterHandler): - """Cluster handler for the Tuya manufacturer Zigbee cluster.""" - - REPORT_CONFIG = () - - def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None: - """Initialize TuyaClusterHandler.""" - super().__init__(cluster, endpoint) - if endpoint.device.quirk_id == TUYA_PLUG_MANUFACTURER: - self.ZCL_INIT_ATTRS = { - "backlight_mode": True, - "power_on_state": True, - } - - -@registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(0xFCC0) -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(0xFCC0) -class OppleRemoteClusterHandler(ClusterHandler): - """Opple cluster handler.""" - - REPORT_CONFIG = () - - def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None: - """Initialize Opple cluster handler.""" - super().__init__(cluster, endpoint) - if self.cluster.endpoint.model == "lumi.motion.ac02": - self.ZCL_INIT_ATTRS = { - "detection_interval": True, - "motion_sensitivity": True, - "trigger_indicator": True, - } - elif self.cluster.endpoint.model == "lumi.motion.agl04": - self.ZCL_INIT_ATTRS = { - "detection_interval": True, - "motion_sensitivity": True, - } - elif self.cluster.endpoint.model == "lumi.motion.ac01": - self.ZCL_INIT_ATTRS = { - "presence": True, - "monitoring_mode": True, - "motion_sensitivity": True, - "approach_distance": True, - } - elif self.cluster.endpoint.model in ("lumi.plug.mmeu01", "lumi.plug.maeu01"): - self.ZCL_INIT_ATTRS = { - "power_outage_memory": True, - "consumer_connected": True, - } - elif self.cluster.endpoint.model == "aqara.feeder.acn001": - self.ZCL_INIT_ATTRS = { - "portions_dispensed": True, - "weight_dispensed": True, - "error_detected": True, - "disable_led_indicator": True, - "child_lock": True, - "feeding_mode": True, - "serving_size": True, - "portion_weight": True, - } - elif self.cluster.endpoint.model == "lumi.airrtc.agl001": - self.ZCL_INIT_ATTRS = { - "system_mode": True, - "preset": True, - "window_detection": True, - "valve_detection": True, - "valve_alarm": True, - "child_lock": True, - "away_preset_temperature": True, - "window_open": True, - "calibrated": True, - "schedule": True, - "sensor": True, - } - elif self.cluster.endpoint.model == "lumi.sensor_smoke.acn03": - self.ZCL_INIT_ATTRS = { - "buzzer_manual_mute": True, - "smoke_density": True, - "heartbeat_indicator": True, - "buzzer_manual_alarm": True, - "buzzer": True, - "linkage_alarm": True, - } - elif self.cluster.endpoint.model == "lumi.magnet.ac01": - self.ZCL_INIT_ATTRS = { - "detection_distance": True, - } - elif self.cluster.endpoint.model == "lumi.switch.acn047": - self.ZCL_INIT_ATTRS = { - "switch_mode": True, - "switch_type": True, - "startup_on_off": True, - "decoupled_mode": True, - } - elif self.cluster.endpoint.model == "lumi.curtain.agl001": - self.ZCL_INIT_ATTRS = { - "hooks_state": True, - "hooks_lock": True, - "positions_stored": True, - "light_level": True, - "hand_open": True, - } - - async def async_initialize_cluster_handler_specific(self, from_cache: bool) -> None: - """Initialize cluster handler specific.""" - if self.cluster.endpoint.model in ("lumi.motion.ac02", "lumi.motion.agl04"): - interval = self.cluster.get("detection_interval", self.cluster.get(0x0102)) - if interval is not None: - self.debug("Loaded detection interval at startup: %s", interval) - self.cluster.endpoint.ias_zone.reset_s = int(interval) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register( - registries.SMARTTHINGS_ACCELERATION_CLUSTER -) -class SmartThingsAccelerationClusterHandler(ClusterHandler): - """Smart Things Acceleration cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig(attr="acceleration", config=REPORT_CONFIG_ASAP), - AttrReportConfig(attr="x_axis", config=REPORT_CONFIG_ASAP), - AttrReportConfig(attr="y_axis", config=REPORT_CONFIG_ASAP), - AttrReportConfig(attr="z_axis", config=REPORT_CONFIG_ASAP), - ) - - @classmethod - def matches(cls, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> bool: - """Filter the cluster match for specific devices.""" - return cluster.endpoint.device.manufacturer in ( - "CentraLite", - "Samjin", - "SmartThings", - ) - - @callback - def attribute_updated(self, attrid: int, value: Any, _: Any) -> None: - """Handle attribute updates on this cluster.""" - try: - attr_name = self._cluster.attributes[attrid].name - except KeyError: - attr_name = UNKNOWN - - if attrid == self.value_attribute: - self.async_send_signal( - f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", - attrid, - attr_name, - value, - ) - return - - self.zha_send_event( - SIGNAL_ATTR_UPDATED, - { - ATTR_ATTRIBUTE_ID: attrid, - ATTR_ATTRIBUTE_NAME: attr_name, - ATTR_VALUE: value, - }, - ) - - -@registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(0xFC31) -class InovelliNotificationClientClusterHandler(ClientClusterHandler): - """Inovelli Notification cluster handler.""" - - @callback - def attribute_updated(self, attrid: int, value: Any, _: Any) -> None: - """Handle an attribute updated on this cluster.""" - - @callback - def cluster_command(self, tsn, command_id, args): - """Handle a cluster command received on this cluster.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(0xFC31) -class InovelliConfigEntityClusterHandler(ClusterHandler): - """Inovelli Configuration Entity cluster handler.""" - - REPORT_CONFIG = () - - def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None: - """Initialize Inovelli cluster handler.""" - super().__init__(cluster, endpoint) - if self.cluster.endpoint.model == "VZM31-SN": - self.ZCL_INIT_ATTRS = { - "dimming_speed_up_remote": True, - "dimming_speed_up_local": True, - "ramp_rate_off_to_on_local": True, - "ramp_rate_off_to_on_remote": True, - "dimming_speed_down_remote": True, - "dimming_speed_down_local": True, - "ramp_rate_on_to_off_local": True, - "ramp_rate_on_to_off_remote": True, - "minimum_level": True, - "maximum_level": True, - "invert_switch": True, - "auto_off_timer": True, - "default_level_local": True, - "default_level_remote": True, - "state_after_power_restored": True, - "load_level_indicator_timeout": True, - "active_power_reports": True, - "periodic_power_and_energy_reports": True, - "active_energy_reports": True, - "power_type": False, - "switch_type": False, - "increased_non_neutral_output": True, - "button_delay": False, - "smart_bulb_mode": False, - "double_tap_up_enabled": True, - "double_tap_down_enabled": True, - "double_tap_up_level": True, - "double_tap_down_level": True, - "led_color_when_on": True, - "led_color_when_off": True, - "led_intensity_when_on": True, - "led_intensity_when_off": True, - "led_scaling_mode": True, - "aux_switch_scenes": True, - "binding_off_to_on_sync_level": True, - "local_protection": False, - "output_mode": False, - "on_off_led_mode": True, - "firmware_progress_led": True, - "relay_click_in_on_off_mode": True, - "disable_clear_notifications_double_tap": True, - } - elif self.cluster.endpoint.model == "VZM35-SN": - self.ZCL_INIT_ATTRS = { - "dimming_speed_up_remote": True, - "dimming_speed_up_local": True, - "ramp_rate_off_to_on_local": True, - "ramp_rate_off_to_on_remote": True, - "dimming_speed_down_remote": True, - "dimming_speed_down_local": True, - "ramp_rate_on_to_off_local": True, - "ramp_rate_on_to_off_remote": True, - "minimum_level": True, - "maximum_level": True, - "invert_switch": True, - "auto_off_timer": True, - "default_level_local": True, - "default_level_remote": True, - "state_after_power_restored": True, - "load_level_indicator_timeout": True, - "power_type": False, - "switch_type": False, - "non_neutral_aux_med_gear_learn_value": True, - "non_neutral_aux_low_gear_learn_value": True, - "quick_start_time": False, - "button_delay": False, - "smart_fan_mode": False, - "double_tap_up_enabled": True, - "double_tap_down_enabled": True, - "double_tap_up_level": True, - "double_tap_down_level": True, - "led_color_when_on": True, - "led_color_when_off": True, - "led_intensity_when_on": True, - "led_intensity_when_off": True, - "aux_switch_scenes": True, - "local_protection": False, - "output_mode": False, - "on_off_led_mode": True, - "firmware_progress_led": True, - "smart_fan_led_display_levels": True, - } - - async def issue_all_led_effect( - self, - effect_type: AllLEDEffectType | int = AllLEDEffectType.Fast_Blink, - color: int = 200, - level: int = 100, - duration: int = 3, - **kwargs: Any, - ) -> None: - """Issue all LED effect command. - - This command is used to issue an LED effect to all LEDs on the device. - """ - - await self.led_effect(effect_type, color, level, duration, expect_reply=False) - - async def issue_individual_led_effect( - self, - led_number: int = 1, - effect_type: SingleLEDEffectType | int = SingleLEDEffectType.Fast_Blink, - color: int = 200, - level: int = 100, - duration: int = 3, - **kwargs: Any, - ) -> None: - """Issue individual LED effect command. - - This command is used to issue an LED effect to the specified LED on the device. - """ - - await self.individual_led_effect( - led_number, effect_type, color, level, duration, expect_reply=False - ) - - -@registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(registries.IKEA_AIR_PURIFIER_CLUSTER) -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register( - registries.IKEA_AIR_PURIFIER_CLUSTER -) -class IkeaAirPurifierClusterHandler(ClusterHandler): - """IKEA Air Purifier cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig(attr="filter_run_time", config=REPORT_CONFIG_DEFAULT), - AttrReportConfig(attr="replace_filter", config=REPORT_CONFIG_IMMEDIATE), - AttrReportConfig(attr="filter_life_time", config=REPORT_CONFIG_DEFAULT), - AttrReportConfig(attr="disable_led", config=REPORT_CONFIG_IMMEDIATE), - AttrReportConfig(attr="air_quality_25pm", config=REPORT_CONFIG_IMMEDIATE), - AttrReportConfig(attr="child_lock", config=REPORT_CONFIG_IMMEDIATE), - AttrReportConfig(attr="fan_mode", config=REPORT_CONFIG_IMMEDIATE), - AttrReportConfig(attr="fan_speed", config=REPORT_CONFIG_IMMEDIATE), - AttrReportConfig(attr="device_run_time", config=REPORT_CONFIG_DEFAULT), - ) - - @property - def fan_mode(self) -> int | None: - """Return current fan mode.""" - return self.cluster.get("fan_mode") - - @property - def fan_mode_sequence(self) -> int | None: - """Return possible fan mode speeds.""" - return self.cluster.get("fan_mode_sequence") - - async def async_set_speed(self, value) -> None: - """Set the speed of the fan.""" - await self.write_attributes_safe({"fan_mode": value}) - - async def async_update(self) -> None: - """Retrieve latest state.""" - await self.get_attribute_value("fan_mode", from_cache=False) - - @callback - def attribute_updated(self, attrid: int, value: Any, _: Any) -> None: - """Handle attribute update from fan cluster.""" - attr_name = self._get_attribute_name(attrid) - self.debug( - "Attribute report '%s'[%s] = %s", self.cluster.name, attr_name, value - ) - if attr_name == "fan_mode": - self.async_send_signal( - f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", attrid, attr_name, value - ) - - -@registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(0xFC80) -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(0xFC80) -class IkeaRemoteClusterHandler(ClusterHandler): - """Ikea Matter remote cluster handler.""" - - REPORT_CONFIG = () - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register( - DoorLock.cluster_id, XIAOMI_AQARA_VIBRATION_AQ1 -) -class XiaomiVibrationAQ1ClusterHandler(MultistateInputClusterHandler): - """Xiaomi DoorLock Cluster is in fact a MultiStateInput Cluster.""" - - -@registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(0xFC11) -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(0xFC11) -class SonoffPresenceSenorClusterHandler(ClusterHandler): - """SonoffPresenceSensor cluster handler.""" - - def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None: - """Initialize SonoffPresenceSensor cluster handler.""" - super().__init__(cluster, endpoint) - if self.cluster.endpoint.model == "SNZB-06P": - self.ZCL_INIT_ATTRS = {"last_illumination_state": True} - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register( - clusters.hvac.Thermostat.cluster_id, DANFOSS_ALLY_THERMOSTAT -) -class DanfossThermostatClusterHandler(ThermostatClusterHandler): - """Thermostat cluster handler for the Danfoss TRV and derivatives.""" - - REPORT_CONFIG = ( - *ThermostatClusterHandler.REPORT_CONFIG, - AttrReportConfig(attr="open_window_detection", config=REPORT_CONFIG_DEFAULT), - AttrReportConfig(attr="heat_required", config=REPORT_CONFIG_ASAP), - AttrReportConfig(attr="mounting_mode_active", config=REPORT_CONFIG_DEFAULT), - AttrReportConfig(attr="load_estimate", config=REPORT_CONFIG_DEFAULT), - AttrReportConfig(attr="adaptation_run_status", config=REPORT_CONFIG_DEFAULT), - AttrReportConfig(attr="preheat_status", config=REPORT_CONFIG_DEFAULT), - AttrReportConfig(attr="preheat_time", config=REPORT_CONFIG_DEFAULT), - ) - - ZCL_INIT_ATTRS = { - **ThermostatClusterHandler.ZCL_INIT_ATTRS, - "external_open_window_detected": True, - "window_open_feature": True, - "exercise_day_of_week": True, - "exercise_trigger_time": True, - "mounting_mode_control": False, # Can change - "orientation": True, - "external_measured_room_sensor": False, # Can change - "radiator_covered": True, - "heat_available": True, - "load_balancing_enable": True, - "load_room_mean": False, # Can change - "control_algorithm_scale_factor": True, - "regulation_setpoint_offset": True, - "adaptation_run_control": True, - "adaptation_run_settings": True, - } - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register( - clusters.hvac.UserInterface.cluster_id, DANFOSS_ALLY_THERMOSTAT -) -class DanfossUserInterfaceClusterHandler(UserInterfaceClusterHandler): - """Interface cluster handler for the Danfoss TRV and derivatives.""" - - ZCL_INIT_ATTRS = { - **UserInterfaceClusterHandler.ZCL_INIT_ATTRS, - "viewing_direction": True, - } - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register( - clusters.homeautomation.Diagnostic.cluster_id, DANFOSS_ALLY_THERMOSTAT -) -class DanfossDiagnosticClusterHandler(DiagnosticClusterHandler): - """Diagnostic cluster handler for the Danfoss TRV and derivatives.""" - - REPORT_CONFIG = ( - *DiagnosticClusterHandler.REPORT_CONFIG, - AttrReportConfig(attr="sw_error_code", config=REPORT_CONFIG_DEFAULT), - AttrReportConfig(attr="motor_step_counter", config=REPORT_CONFIG_DEFAULT), - ) diff --git a/homeassistant/components/zha/core/cluster_handlers/measurement.py b/homeassistant/components/zha/core/cluster_handlers/measurement.py deleted file mode 100644 index 768de8c4c73..00000000000 --- a/homeassistant/components/zha/core/cluster_handlers/measurement.py +++ /dev/null @@ -1,208 +0,0 @@ -"""Measurement cluster handlers module for Zigbee Home Automation.""" - -from __future__ import annotations - -from typing import TYPE_CHECKING - -import zigpy.zcl -from zigpy.zcl.clusters.measurement import ( - PM25, - CarbonDioxideConcentration, - CarbonMonoxideConcentration, - FlowMeasurement, - FormaldehydeConcentration, - IlluminanceLevelSensing, - IlluminanceMeasurement, - LeafWetness, - OccupancySensing, - PressureMeasurement, - RelativeHumidity, - SoilMoisture, - TemperatureMeasurement, -) - -from .. import registries -from ..const import ( - REPORT_CONFIG_DEFAULT, - REPORT_CONFIG_IMMEDIATE, - REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_MIN_INT, -) -from . import AttrReportConfig, ClusterHandler -from .helpers import is_hue_motion_sensor, is_sonoff_presence_sensor - -if TYPE_CHECKING: - from ..endpoint import Endpoint - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(FlowMeasurement.cluster_id) -class FlowMeasurementClusterHandler(ClusterHandler): - """Flow Measurement cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=FlowMeasurement.AttributeDefs.measured_value.name, - config=REPORT_CONFIG_DEFAULT, - ), - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(IlluminanceLevelSensing.cluster_id) -class IlluminanceLevelSensingClusterHandler(ClusterHandler): - """Illuminance Level Sensing cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=IlluminanceLevelSensing.AttributeDefs.level_status.name, - config=REPORT_CONFIG_DEFAULT, - ), - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(IlluminanceMeasurement.cluster_id) -class IlluminanceMeasurementClusterHandler(ClusterHandler): - """Illuminance Measurement cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=IlluminanceMeasurement.AttributeDefs.measured_value.name, - config=REPORT_CONFIG_DEFAULT, - ), - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(OccupancySensing.cluster_id) -class OccupancySensingClusterHandler(ClusterHandler): - """Occupancy Sensing cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=OccupancySensing.AttributeDefs.occupancy.name, - config=REPORT_CONFIG_IMMEDIATE, - ), - ) - - def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None: - """Initialize Occupancy cluster handler.""" - super().__init__(cluster, endpoint) - if is_hue_motion_sensor(self): - self.ZCL_INIT_ATTRS = self.ZCL_INIT_ATTRS.copy() - self.ZCL_INIT_ATTRS["sensitivity"] = True - if is_sonoff_presence_sensor(self): - self.ZCL_INIT_ATTRS = self.ZCL_INIT_ATTRS.copy() - self.ZCL_INIT_ATTRS["ultrasonic_o_to_u_delay"] = True - self.ZCL_INIT_ATTRS["ultrasonic_u_to_o_threshold"] = True - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(PressureMeasurement.cluster_id) -class PressureMeasurementClusterHandler(ClusterHandler): - """Pressure measurement cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=PressureMeasurement.AttributeDefs.measured_value.name, - config=REPORT_CONFIG_DEFAULT, - ), - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(RelativeHumidity.cluster_id) -class RelativeHumidityClusterHandler(ClusterHandler): - """Relative Humidity measurement cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=RelativeHumidity.AttributeDefs.measured_value.name, - config=(REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 100), - ), - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(SoilMoisture.cluster_id) -class SoilMoistureClusterHandler(ClusterHandler): - """Soil Moisture measurement cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=SoilMoisture.AttributeDefs.measured_value.name, - config=(REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 100), - ), - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(LeafWetness.cluster_id) -class LeafWetnessClusterHandler(ClusterHandler): - """Leaf Wetness measurement cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=LeafWetness.AttributeDefs.measured_value.name, - config=(REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 100), - ), - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(TemperatureMeasurement.cluster_id) -class TemperatureMeasurementClusterHandler(ClusterHandler): - """Temperature measurement cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=TemperatureMeasurement.AttributeDefs.measured_value.name, - config=(REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 50), - ), - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register( - CarbonMonoxideConcentration.cluster_id -) -class CarbonMonoxideConcentrationClusterHandler(ClusterHandler): - """Carbon Monoxide measurement cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=CarbonMonoxideConcentration.AttributeDefs.measured_value.name, - config=(REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 0.000001), - ), - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register( - CarbonDioxideConcentration.cluster_id -) -class CarbonDioxideConcentrationClusterHandler(ClusterHandler): - """Carbon Dioxide measurement cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=CarbonDioxideConcentration.AttributeDefs.measured_value.name, - config=(REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 0.000001), - ), - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(PM25.cluster_id) -class PM25ClusterHandler(ClusterHandler): - """Particulate Matter 2.5 microns or less measurement cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=PM25.AttributeDefs.measured_value.name, - config=(REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 0.1), - ), - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register( - FormaldehydeConcentration.cluster_id -) -class FormaldehydeConcentrationClusterHandler(ClusterHandler): - """Formaldehyde measurement cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=FormaldehydeConcentration.AttributeDefs.measured_value.name, - config=(REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 0.000001), - ), - ) diff --git a/homeassistant/components/zha/core/cluster_handlers/protocol.py b/homeassistant/components/zha/core/cluster_handlers/protocol.py deleted file mode 100644 index e1e3d7a5413..00000000000 --- a/homeassistant/components/zha/core/cluster_handlers/protocol.py +++ /dev/null @@ -1,129 +0,0 @@ -"""Protocol cluster handlers module for Zigbee Home Automation.""" - -from zigpy.zcl.clusters.protocol import ( - AnalogInputExtended, - AnalogInputRegular, - AnalogOutputExtended, - AnalogOutputRegular, - AnalogValueExtended, - AnalogValueRegular, - BacnetProtocolTunnel, - BinaryInputExtended, - BinaryInputRegular, - BinaryOutputExtended, - BinaryOutputRegular, - BinaryValueExtended, - BinaryValueRegular, - GenericTunnel, - MultistateInputExtended, - MultistateInputRegular, - MultistateOutputExtended, - MultistateOutputRegular, - MultistateValueExtended, - MultistateValueRegular, -) - -from .. import registries -from . import ClusterHandler - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(AnalogInputExtended.cluster_id) -class AnalogInputExtendedClusterHandler(ClusterHandler): - """Analog Input Extended cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(AnalogInputRegular.cluster_id) -class AnalogInputRegularClusterHandler(ClusterHandler): - """Analog Input Regular cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(AnalogOutputExtended.cluster_id) -class AnalogOutputExtendedClusterHandler(ClusterHandler): - """Analog Output Regular cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(AnalogOutputRegular.cluster_id) -class AnalogOutputRegularClusterHandler(ClusterHandler): - """Analog Output Regular cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(AnalogValueExtended.cluster_id) -class AnalogValueExtendedClusterHandler(ClusterHandler): - """Analog Value Extended edition cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(AnalogValueRegular.cluster_id) -class AnalogValueRegularClusterHandler(ClusterHandler): - """Analog Value Regular cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(BacnetProtocolTunnel.cluster_id) -class BacnetProtocolTunnelClusterHandler(ClusterHandler): - """Bacnet Protocol Tunnel cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(BinaryInputExtended.cluster_id) -class BinaryInputExtendedClusterHandler(ClusterHandler): - """Binary Input Extended cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(BinaryInputRegular.cluster_id) -class BinaryInputRegularClusterHandler(ClusterHandler): - """Binary Input Regular cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(BinaryOutputExtended.cluster_id) -class BinaryOutputExtendedClusterHandler(ClusterHandler): - """Binary Output Extended cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(BinaryOutputRegular.cluster_id) -class BinaryOutputRegularClusterHandler(ClusterHandler): - """Binary Output Regular cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(BinaryValueExtended.cluster_id) -class BinaryValueExtendedClusterHandler(ClusterHandler): - """Binary Value Extended cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(BinaryValueRegular.cluster_id) -class BinaryValueRegularClusterHandler(ClusterHandler): - """Binary Value Regular cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(GenericTunnel.cluster_id) -class GenericTunnelClusterHandler(ClusterHandler): - """Generic Tunnel cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(MultistateInputExtended.cluster_id) -class MultiStateInputExtendedClusterHandler(ClusterHandler): - """Multistate Input Extended cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(MultistateInputRegular.cluster_id) -class MultiStateInputRegularClusterHandler(ClusterHandler): - """Multistate Input Regular cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register( - MultistateOutputExtended.cluster_id -) -class MultiStateOutputExtendedClusterHandler(ClusterHandler): - """Multistate Output Extended cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(MultistateOutputRegular.cluster_id) -class MultiStateOutputRegularClusterHandler(ClusterHandler): - """Multistate Output Regular cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(MultistateValueExtended.cluster_id) -class MultiStateValueExtendedClusterHandler(ClusterHandler): - """Multistate Value Extended cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(MultistateValueRegular.cluster_id) -class MultiStateValueRegularClusterHandler(ClusterHandler): - """Multistate Value Regular cluster handler.""" diff --git a/homeassistant/components/zha/core/cluster_handlers/security.py b/homeassistant/components/zha/core/cluster_handlers/security.py deleted file mode 100644 index 8ebe09cef03..00000000000 --- a/homeassistant/components/zha/core/cluster_handlers/security.py +++ /dev/null @@ -1,400 +0,0 @@ -"""Security cluster handlers module for Zigbee Home Automation. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/integrations/zha/ -""" - -from __future__ import annotations - -from collections.abc import Callable -from typing import TYPE_CHECKING, Any - -import zigpy.zcl -from zigpy.zcl.clusters.security import IasAce as AceCluster, IasWd, IasZone - -from homeassistant.core import callback -from homeassistant.exceptions import HomeAssistantError - -from .. import registries -from ..const import ( - SIGNAL_ATTR_UPDATED, - WARNING_DEVICE_MODE_EMERGENCY, - WARNING_DEVICE_SOUND_HIGH, - WARNING_DEVICE_SQUAWK_MODE_ARMED, - WARNING_DEVICE_STROBE_HIGH, - WARNING_DEVICE_STROBE_YES, -) -from . import ClusterHandler, ClusterHandlerStatus - -if TYPE_CHECKING: - from ..endpoint import Endpoint - -SIGNAL_ARMED_STATE_CHANGED = "zha_armed_state_changed" -SIGNAL_ALARM_TRIGGERED = "zha_armed_triggered" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(AceCluster.cluster_id) -class IasAceClusterHandler(ClusterHandler): - """IAS Ancillary Control Equipment cluster handler.""" - - def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None: - """Initialize IAS Ancillary Control Equipment cluster handler.""" - super().__init__(cluster, endpoint) - self.command_map: dict[int, Callable[..., Any]] = { - AceCluster.ServerCommandDefs.arm.id: self.arm, - AceCluster.ServerCommandDefs.bypass.id: self._bypass, - AceCluster.ServerCommandDefs.emergency.id: self._emergency, - AceCluster.ServerCommandDefs.fire.id: self._fire, - AceCluster.ServerCommandDefs.panic.id: self._panic, - AceCluster.ServerCommandDefs.get_zone_id_map.id: self._get_zone_id_map, - AceCluster.ServerCommandDefs.get_zone_info.id: self._get_zone_info, - AceCluster.ServerCommandDefs.get_panel_status.id: self._send_panel_status_response, - AceCluster.ServerCommandDefs.get_bypassed_zone_list.id: self._get_bypassed_zone_list, - AceCluster.ServerCommandDefs.get_zone_status.id: self._get_zone_status, - } - self.arm_map: dict[AceCluster.ArmMode, Callable[..., Any]] = { - AceCluster.ArmMode.Disarm: self._disarm, - AceCluster.ArmMode.Arm_All_Zones: self._arm_away, - AceCluster.ArmMode.Arm_Day_Home_Only: self._arm_day, - AceCluster.ArmMode.Arm_Night_Sleep_Only: self._arm_night, - } - self.armed_state: AceCluster.PanelStatus = AceCluster.PanelStatus.Panel_Disarmed - self.invalid_tries: int = 0 - - # These will all be setup by the entity from ZHA configuration - self.panel_code: str = "1234" - self.code_required_arm_actions = False - self.max_invalid_tries: int = 3 - - # where do we store this to handle restarts - self.alarm_status: AceCluster.AlarmStatus = AceCluster.AlarmStatus.No_Alarm - - @callback - def cluster_command(self, tsn, command_id, args) -> None: - """Handle commands received to this cluster.""" - self.debug( - "received command %s", self._cluster.server_commands[command_id].name - ) - self.command_map[command_id](*args) - - def arm(self, arm_mode: int, code: str | None, zone_id: int) -> None: - """Handle the IAS ACE arm command.""" - mode = AceCluster.ArmMode(arm_mode) - - self.zha_send_event( - AceCluster.ServerCommandDefs.arm.name, - { - "arm_mode": mode.value, - "arm_mode_description": mode.name, - "code": code, - "zone_id": zone_id, - }, - ) - - zigbee_reply = self.arm_map[mode](code) - self._endpoint.device.hass.async_create_task(zigbee_reply) - - if self.invalid_tries >= self.max_invalid_tries: - self.alarm_status = AceCluster.AlarmStatus.Emergency - self.armed_state = AceCluster.PanelStatus.In_Alarm - self.async_send_signal(f"{self.unique_id}_{SIGNAL_ALARM_TRIGGERED}") - else: - self.async_send_signal(f"{self.unique_id}_{SIGNAL_ARMED_STATE_CHANGED}") - self._send_panel_status_changed() - - def _disarm(self, code: str): - """Test the code and disarm the panel if the code is correct.""" - if ( - code != self.panel_code - and self.armed_state != AceCluster.PanelStatus.Panel_Disarmed - ): - self.debug("Invalid code supplied to IAS ACE") - self.invalid_tries += 1 - zigbee_reply = self.arm_response( - AceCluster.ArmNotification.Invalid_Arm_Disarm_Code - ) - else: - self.invalid_tries = 0 - if ( - self.armed_state == AceCluster.PanelStatus.Panel_Disarmed - and self.alarm_status == AceCluster.AlarmStatus.No_Alarm - ): - self.debug("IAS ACE already disarmed") - zigbee_reply = self.arm_response( - AceCluster.ArmNotification.Already_Disarmed - ) - else: - self.debug("Disarming all IAS ACE zones") - zigbee_reply = self.arm_response( - AceCluster.ArmNotification.All_Zones_Disarmed - ) - - self.armed_state = AceCluster.PanelStatus.Panel_Disarmed - self.alarm_status = AceCluster.AlarmStatus.No_Alarm - return zigbee_reply - - def _arm_day(self, code: str) -> None: - """Arm the panel for day / home zones.""" - return self._handle_arm( - code, - AceCluster.PanelStatus.Armed_Stay, - AceCluster.ArmNotification.Only_Day_Home_Zones_Armed, - ) - - def _arm_night(self, code: str) -> None: - """Arm the panel for night / sleep zones.""" - return self._handle_arm( - code, - AceCluster.PanelStatus.Armed_Night, - AceCluster.ArmNotification.Only_Night_Sleep_Zones_Armed, - ) - - def _arm_away(self, code: str) -> None: - """Arm the panel for away mode.""" - return self._handle_arm( - code, - AceCluster.PanelStatus.Armed_Away, - AceCluster.ArmNotification.All_Zones_Armed, - ) - - def _handle_arm( - self, - code: str, - panel_status: AceCluster.PanelStatus, - armed_type: AceCluster.ArmNotification, - ) -> None: - """Arm the panel with the specified statuses.""" - if self.code_required_arm_actions and code != self.panel_code: - self.debug("Invalid code supplied to IAS ACE") - zigbee_reply = self.arm_response( - AceCluster.ArmNotification.Invalid_Arm_Disarm_Code - ) - else: - self.debug("Arming all IAS ACE zones") - self.armed_state = panel_status - zigbee_reply = self.arm_response(armed_type) - return zigbee_reply - - def _bypass(self, zone_list, code) -> None: - """Handle the IAS ACE bypass command.""" - self.zha_send_event( - AceCluster.ServerCommandDefs.bypass.name, - {"zone_list": zone_list, "code": code}, - ) - - def _emergency(self) -> None: - """Handle the IAS ACE emergency command.""" - self._set_alarm(AceCluster.AlarmStatus.Emergency) - - def _fire(self) -> None: - """Handle the IAS ACE fire command.""" - self._set_alarm(AceCluster.AlarmStatus.Fire) - - def _panic(self) -> None: - """Handle the IAS ACE panic command.""" - self._set_alarm(AceCluster.AlarmStatus.Emergency_Panic) - - def _set_alarm(self, status: AceCluster.AlarmStatus) -> None: - """Set the specified alarm status.""" - self.alarm_status = status - self.armed_state = AceCluster.PanelStatus.In_Alarm - self.async_send_signal(f"{self.unique_id}_{SIGNAL_ALARM_TRIGGERED}") - self._send_panel_status_changed() - - def _get_zone_id_map(self): - """Handle the IAS ACE zone id map command.""" - - def _get_zone_info(self, zone_id): - """Handle the IAS ACE zone info command.""" - - def _send_panel_status_response(self) -> None: - """Handle the IAS ACE panel status response command.""" - response = self.panel_status_response( - self.armed_state, - 0x00, - AceCluster.AudibleNotification.Default_Sound, - self.alarm_status, - ) - self._endpoint.device.hass.async_create_task(response) - - def _send_panel_status_changed(self) -> None: - """Handle the IAS ACE panel status changed command.""" - response = self.panel_status_changed( - self.armed_state, - 0x00, - AceCluster.AudibleNotification.Default_Sound, - self.alarm_status, - ) - self._endpoint.device.hass.async_create_task(response) - - def _get_bypassed_zone_list(self): - """Handle the IAS ACE bypassed zone list command.""" - - def _get_zone_status( - self, starting_zone_id, max_zone_ids, zone_status_mask_flag, zone_status_mask - ): - """Handle the IAS ACE zone status command.""" - - -@registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(IasWd.cluster_id) -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(IasWd.cluster_id) -class IasWdClusterHandler(ClusterHandler): - """IAS Warning Device cluster handler.""" - - @staticmethod - def set_bit(destination_value, destination_bit, source_value, source_bit): - """Set the specified bit in the value.""" - - if IasWdClusterHandler.get_bit(source_value, source_bit): - return destination_value | (1 << destination_bit) - return destination_value - - @staticmethod - def get_bit(value, bit): - """Get the specified bit from the value.""" - return (value & (1 << bit)) != 0 - - async def issue_squawk( - self, - mode=WARNING_DEVICE_SQUAWK_MODE_ARMED, - strobe=WARNING_DEVICE_STROBE_YES, - squawk_level=WARNING_DEVICE_SOUND_HIGH, - ): - """Issue a squawk command. - - This command uses the WD capabilities to emit a quick audible/visible - pulse called a "squawk". The squawk command has no effect if the WD - is currently active (warning in progress). - """ - value = 0 - value = IasWdClusterHandler.set_bit(value, 0, squawk_level, 0) - value = IasWdClusterHandler.set_bit(value, 1, squawk_level, 1) - - value = IasWdClusterHandler.set_bit(value, 3, strobe, 0) - - value = IasWdClusterHandler.set_bit(value, 4, mode, 0) - value = IasWdClusterHandler.set_bit(value, 5, mode, 1) - value = IasWdClusterHandler.set_bit(value, 6, mode, 2) - value = IasWdClusterHandler.set_bit(value, 7, mode, 3) - - await self.squawk(value) - - async def issue_start_warning( - self, - mode=WARNING_DEVICE_MODE_EMERGENCY, - strobe=WARNING_DEVICE_STROBE_YES, - siren_level=WARNING_DEVICE_SOUND_HIGH, - warning_duration=5, # seconds - strobe_duty_cycle=0x00, - strobe_intensity=WARNING_DEVICE_STROBE_HIGH, - ): - """Issue a start warning command. - - This command starts the WD operation. The WD alerts the surrounding area - by audible (siren) and visual (strobe) signals. - - strobe_duty_cycle indicates the length of the flash cycle. This provides a means - of varying the flash duration for different alarm types (e.g., fire, police, - burglar). Valid range is 0-100 in increments of 10. All other values SHALL - be rounded to the nearest valid value. Strobe SHALL calculate duty cycle over - a duration of one second. - - The ON state SHALL precede the OFF state. For example, if Strobe Duty Cycle - Field specifies “40,” then the strobe SHALL flash ON for 4/10ths of a second - and then turn OFF for 6/10ths of a second. - """ - value = 0 - value = IasWdClusterHandler.set_bit(value, 0, siren_level, 0) - value = IasWdClusterHandler.set_bit(value, 1, siren_level, 1) - - value = IasWdClusterHandler.set_bit(value, 2, strobe, 0) - - value = IasWdClusterHandler.set_bit(value, 4, mode, 0) - value = IasWdClusterHandler.set_bit(value, 5, mode, 1) - value = IasWdClusterHandler.set_bit(value, 6, mode, 2) - value = IasWdClusterHandler.set_bit(value, 7, mode, 3) - - await self.start_warning( - value, warning_duration, strobe_duty_cycle, strobe_intensity - ) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(IasZone.cluster_id) -class IASZoneClusterHandler(ClusterHandler): - """Cluster handler for the IASZone Zigbee cluster.""" - - ZCL_INIT_ATTRS = { - IasZone.AttributeDefs.zone_status.name: False, - IasZone.AttributeDefs.zone_state.name: True, - IasZone.AttributeDefs.zone_type.name: True, - } - - @callback - def cluster_command(self, tsn, command_id, args): - """Handle commands received to this cluster.""" - if command_id == IasZone.ClientCommandDefs.status_change_notification.id: - zone_status = args[0] - # update attribute cache with new zone status - self.cluster.update_attribute( - IasZone.AttributeDefs.zone_status.id, zone_status - ) - self.debug("Updated alarm state: %s", zone_status) - elif command_id == IasZone.ClientCommandDefs.enroll.id: - self.debug("Enroll requested") - self._cluster.create_catching_task( - self.enroll_response( - enroll_response_code=IasZone.EnrollResponse.Success, zone_id=0 - ) - ) - - async def async_configure(self): - """Configure IAS device.""" - await self.get_attribute_value( - IasZone.AttributeDefs.zone_type.name, from_cache=False - ) - if self._endpoint.device.skip_configuration: - self.debug("skipping IASZoneClusterHandler configuration") - return - - self.debug("started IASZoneClusterHandler configuration") - - await self.bind() - ieee = self.cluster.endpoint.device.application.state.node_info.ieee - - try: - await self.write_attributes_safe( - {IasZone.AttributeDefs.cie_addr.name: ieee} - ) - self.debug( - "wrote cie_addr: %s to '%s' cluster", - str(ieee), - self._cluster.ep_attribute, - ) - except HomeAssistantError as ex: - self.debug( - "Failed to write cie_addr: %s to '%s' cluster: %s", - str(ieee), - self._cluster.ep_attribute, - str(ex), - ) - - self.debug("Sending pro-active IAS enroll response") - self._cluster.create_catching_task( - self.enroll_response( - enroll_response_code=IasZone.EnrollResponse.Success, zone_id=0 - ) - ) - - self._status = ClusterHandlerStatus.CONFIGURED - self.debug("finished IASZoneClusterHandler configuration") - - @callback - def attribute_updated(self, attrid: int, value: Any, _: Any) -> None: - """Handle attribute updates on this cluster.""" - if attrid == IasZone.AttributeDefs.zone_status.id: - self.async_send_signal( - f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", - attrid, - IasZone.AttributeDefs.zone_status.name, - value, - ) diff --git a/homeassistant/components/zha/core/cluster_handlers/smartenergy.py b/homeassistant/components/zha/core/cluster_handlers/smartenergy.py deleted file mode 100644 index d167b8b1752..00000000000 --- a/homeassistant/components/zha/core/cluster_handlers/smartenergy.py +++ /dev/null @@ -1,388 +0,0 @@ -"""Smart energy cluster handlers module for Zigbee Home Automation.""" - -from __future__ import annotations - -import enum -from functools import partialmethod -from typing import TYPE_CHECKING - -import zigpy.zcl -from zigpy.zcl.clusters.smartenergy import ( - Calendar, - DeviceManagement, - Drlc, - EnergyManagement, - Events, - KeyEstablishment, - MduPairing, - Messaging, - Metering, - Prepayment, - Price, - Tunneling, -) - -from .. import registries -from ..const import ( - REPORT_CONFIG_ASAP, - REPORT_CONFIG_DEFAULT, - REPORT_CONFIG_OP, - SIGNAL_ATTR_UPDATED, -) -from . import AttrReportConfig, ClusterHandler - -if TYPE_CHECKING: - from ..endpoint import Endpoint - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Calendar.cluster_id) -class CalendarClusterHandler(ClusterHandler): - """Calendar cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(DeviceManagement.cluster_id) -class DeviceManagementClusterHandler(ClusterHandler): - """Device Management cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Drlc.cluster_id) -class DrlcClusterHandler(ClusterHandler): - """Demand Response and Load Control cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(EnergyManagement.cluster_id) -class EnergyManagementClusterHandler(ClusterHandler): - """Energy Management cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Events.cluster_id) -class EventsClusterHandler(ClusterHandler): - """Event cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(KeyEstablishment.cluster_id) -class KeyEstablishmentClusterHandler(ClusterHandler): - """Key Establishment cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(MduPairing.cluster_id) -class MduPairingClusterHandler(ClusterHandler): - """Pairing cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Messaging.cluster_id) -class MessagingClusterHandler(ClusterHandler): - """Messaging cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Metering.cluster_id) -class MeteringClusterHandler(ClusterHandler): - """Metering cluster handler.""" - - REPORT_CONFIG = ( - AttrReportConfig( - attr=Metering.AttributeDefs.instantaneous_demand.name, - config=REPORT_CONFIG_OP, - ), - AttrReportConfig( - attr=Metering.AttributeDefs.current_summ_delivered.name, - config=REPORT_CONFIG_DEFAULT, - ), - AttrReportConfig( - attr=Metering.AttributeDefs.current_tier1_summ_delivered.name, - config=REPORT_CONFIG_DEFAULT, - ), - AttrReportConfig( - attr=Metering.AttributeDefs.current_tier2_summ_delivered.name, - config=REPORT_CONFIG_DEFAULT, - ), - AttrReportConfig( - attr=Metering.AttributeDefs.current_tier3_summ_delivered.name, - config=REPORT_CONFIG_DEFAULT, - ), - AttrReportConfig( - attr=Metering.AttributeDefs.current_tier4_summ_delivered.name, - config=REPORT_CONFIG_DEFAULT, - ), - AttrReportConfig( - attr=Metering.AttributeDefs.current_tier5_summ_delivered.name, - config=REPORT_CONFIG_DEFAULT, - ), - AttrReportConfig( - attr=Metering.AttributeDefs.current_tier6_summ_delivered.name, - config=REPORT_CONFIG_DEFAULT, - ), - AttrReportConfig( - attr=Metering.AttributeDefs.current_summ_received.name, - config=REPORT_CONFIG_DEFAULT, - ), - AttrReportConfig( - attr=Metering.AttributeDefs.status.name, - config=REPORT_CONFIG_ASAP, - ), - ) - ZCL_INIT_ATTRS = { - Metering.AttributeDefs.demand_formatting.name: True, - Metering.AttributeDefs.divisor.name: True, - Metering.AttributeDefs.metering_device_type.name: True, - Metering.AttributeDefs.multiplier.name: True, - Metering.AttributeDefs.summation_formatting.name: True, - Metering.AttributeDefs.unit_of_measure.name: True, - } - - METERING_DEVICE_TYPES_ELECTRIC = { - 0, - 7, - 8, - 9, - 10, - 11, - 13, - 14, - 15, - 127, - 134, - 135, - 136, - 137, - 138, - 140, - 141, - 142, - } - METERING_DEVICE_TYPES_GAS = {1, 128} - METERING_DEVICE_TYPES_WATER = {2, 129} - METERING_DEVICE_TYPES_HEATING_COOLING = {3, 5, 6, 130, 132, 133} - - metering_device_type = { - 0: "Electric Metering", - 1: "Gas Metering", - 2: "Water Metering", - 3: "Thermal Metering", # deprecated - 4: "Pressure Metering", - 5: "Heat Metering", - 6: "Cooling Metering", - 7: "End Use Measurement Device (EUMD) for metering electric vehicle charging", - 8: "PV Generation Metering", - 9: "Wind Turbine Generation Metering", - 10: "Water Turbine Generation Metering", - 11: "Micro Generation Metering", - 12: "Solar Hot Water Generation Metering", - 13: "Electric Metering Element/Phase 1", - 14: "Electric Metering Element/Phase 2", - 15: "Electric Metering Element/Phase 3", - 127: "Mirrored Electric Metering", - 128: "Mirrored Gas Metering", - 129: "Mirrored Water Metering", - 130: "Mirrored Thermal Metering", # deprecated - 131: "Mirrored Pressure Metering", - 132: "Mirrored Heat Metering", - 133: "Mirrored Cooling Metering", - 134: "Mirrored End Use Measurement Device (EUMD) for metering electric vehicle charging", - 135: "Mirrored PV Generation Metering", - 136: "Mirrored Wind Turbine Generation Metering", - 137: "Mirrored Water Turbine Generation Metering", - 138: "Mirrored Micro Generation Metering", - 139: "Mirrored Solar Hot Water Generation Metering", - 140: "Mirrored Electric Metering Element/Phase 1", - 141: "Mirrored Electric Metering Element/Phase 2", - 142: "Mirrored Electric Metering Element/Phase 3", - } - - class DeviceStatusElectric(enum.IntFlag): - """Electric Metering Device Status.""" - - NO_ALARMS = 0 - CHECK_METER = 1 - LOW_BATTERY = 2 - TAMPER_DETECT = 4 - POWER_FAILURE = 8 - POWER_QUALITY = 16 - LEAK_DETECT = 32 # Really? - SERVICE_DISCONNECT = 64 - RESERVED = 128 - - class DeviceStatusGas(enum.IntFlag): - """Gas Metering Device Status.""" - - NO_ALARMS = 0 - CHECK_METER = 1 - LOW_BATTERY = 2 - TAMPER_DETECT = 4 - NOT_DEFINED = 8 - LOW_PRESSURE = 16 - LEAK_DETECT = 32 - SERVICE_DISCONNECT = 64 - REVERSE_FLOW = 128 - - class DeviceStatusWater(enum.IntFlag): - """Water Metering Device Status.""" - - NO_ALARMS = 0 - CHECK_METER = 1 - LOW_BATTERY = 2 - TAMPER_DETECT = 4 - PIPE_EMPTY = 8 - LOW_PRESSURE = 16 - LEAK_DETECT = 32 - SERVICE_DISCONNECT = 64 - REVERSE_FLOW = 128 - - class DeviceStatusHeatingCooling(enum.IntFlag): - """Heating and Cooling Metering Device Status.""" - - NO_ALARMS = 0 - CHECK_METER = 1 - LOW_BATTERY = 2 - TAMPER_DETECT = 4 - TEMPERATURE_SENSOR = 8 - BURST_DETECT = 16 - LEAK_DETECT = 32 - SERVICE_DISCONNECT = 64 - REVERSE_FLOW = 128 - - class DeviceStatusDefault(enum.IntFlag): - """Metering Device Status.""" - - NO_ALARMS = 0 - - class FormatSelector(enum.IntEnum): - """Format specified selector.""" - - DEMAND = 0 - SUMMATION = 1 - - def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None: - """Initialize Metering.""" - super().__init__(cluster, endpoint) - self._format_spec: str | None = None - self._summa_format: str | None = None - - @property - def divisor(self) -> int: - """Return divisor for the value.""" - return self.cluster.get(Metering.AttributeDefs.divisor.name) or 1 - - @property - def device_type(self) -> str | int | None: - """Return metering device type.""" - dev_type = self.cluster.get(Metering.AttributeDefs.metering_device_type.name) - if dev_type is None: - return None - return self.metering_device_type.get(dev_type, dev_type) - - @property - def multiplier(self) -> int: - """Return multiplier for the value.""" - return self.cluster.get(Metering.AttributeDefs.multiplier.name) or 1 - - @property - def status(self) -> int | None: - """Return metering device status.""" - if (status := self.cluster.get(Metering.AttributeDefs.status.name)) is None: - return None - - metering_device_type = self.cluster.get( - Metering.AttributeDefs.metering_device_type.name - ) - if metering_device_type in self.METERING_DEVICE_TYPES_ELECTRIC: - return self.DeviceStatusElectric(status) - if metering_device_type in self.METERING_DEVICE_TYPES_GAS: - return self.DeviceStatusGas(status) - if metering_device_type in self.METERING_DEVICE_TYPES_WATER: - return self.DeviceStatusWater(status) - if metering_device_type in self.METERING_DEVICE_TYPES_HEATING_COOLING: - return self.DeviceStatusHeatingCooling(status) - return self.DeviceStatusDefault(status) - - @property - def unit_of_measurement(self) -> int: - """Return unit of measurement.""" - return self.cluster.get(Metering.AttributeDefs.unit_of_measure.name) - - async def async_initialize_cluster_handler_specific(self, from_cache: bool) -> None: - """Fetch config from device and updates format specifier.""" - - fmting = self.cluster.get( - Metering.AttributeDefs.demand_formatting.name, 0xF9 - ) # 1 digit to the right, 15 digits to the left - self._format_spec = self.get_formatting(fmting) - - fmting = self.cluster.get( - Metering.AttributeDefs.summation_formatting.name, 0xF9 - ) # 1 digit to the right, 15 digits to the left - self._summa_format = self.get_formatting(fmting) - - async def async_update(self) -> None: - """Retrieve latest state.""" - self.debug("async_update") - - attrs = [ - a["attr"] - for a in self.REPORT_CONFIG - if a["attr"] not in self.cluster.unsupported_attributes - ] - result = await self.get_attributes(attrs, from_cache=False, only_cache=False) - if result: - for attr, value in result.items(): - self.async_send_signal( - f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", - self.cluster.find_attribute(attr).id, - attr, - value, - ) - - @staticmethod - def get_formatting(formatting: int) -> str: - """Return a formatting string, given the formatting value. - - Bits 0 to 2: Number of Digits to the right of the Decimal Point. - Bits 3 to 6: Number of Digits to the left of the Decimal Point. - Bit 7: If set, suppress leading zeros. - """ - r_digits = int(formatting & 0x07) # digits to the right of decimal point - l_digits = (formatting >> 3) & 0x0F # digits to the left of decimal point - if l_digits == 0: - l_digits = 15 - width = r_digits + l_digits + (1 if r_digits > 0 else 0) - - if formatting & 0x80: - # suppress leading 0 - return f"{{:{width}.{r_digits}f}}" - - return f"{{:0{width}.{r_digits}f}}" - - def _formatter_function( - self, selector: FormatSelector, value: int - ) -> int | float | str: - """Return formatted value for display.""" - value_float = value * self.multiplier / self.divisor - if self.unit_of_measurement == 0: - # Zigbee spec power unit is kW, but we show the value in W - value_watt = value_float * 1000 - if value_watt < 100: - return round(value_watt, 1) - return round(value_watt) - if selector == self.FormatSelector.SUMMATION: - assert self._summa_format - return self._summa_format.format(value_float).lstrip() - assert self._format_spec - return self._format_spec.format(value_float).lstrip() - - demand_formatter = partialmethod(_formatter_function, FormatSelector.DEMAND) - summa_formatter = partialmethod(_formatter_function, FormatSelector.SUMMATION) - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Prepayment.cluster_id) -class PrepaymentClusterHandler(ClusterHandler): - """Prepayment cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Price.cluster_id) -class PriceClusterHandler(ClusterHandler): - """Price cluster handler.""" - - -@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(Tunneling.cluster_id) -class TunnelingClusterHandler(ClusterHandler): - """Tunneling cluster handler.""" diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py deleted file mode 100644 index 2359fe0a1c3..00000000000 --- a/homeassistant/components/zha/core/const.py +++ /dev/null @@ -1,423 +0,0 @@ -"""All constants related to the ZHA component.""" - -from __future__ import annotations - -import enum -import logging - -import bellows.zigbee.application -import voluptuous as vol -import zigpy.application -import zigpy.types as t -import zigpy_deconz.zigbee.application -import zigpy_xbee.zigbee.application -import zigpy_zigate.zigbee.application -import zigpy_znp.zigbee.application - -from homeassistant.const import Platform -import homeassistant.helpers.config_validation as cv - -ATTR_ACTIVE_COORDINATOR = "active_coordinator" -ATTR_ARGS = "args" -ATTR_ATTRIBUTE = "attribute" -ATTR_ATTRIBUTE_ID = "attribute_id" -ATTR_ATTRIBUTE_NAME = "attribute_name" -ATTR_AVAILABLE = "available" -ATTR_CLUSTER_ID = "cluster_id" -ATTR_CLUSTER_TYPE = "cluster_type" -ATTR_COMMAND_TYPE = "command_type" -ATTR_DEVICE_IEEE = "device_ieee" -ATTR_DEVICE_TYPE = "device_type" -ATTR_ENDPOINTS = "endpoints" -ATTR_ENDPOINT_NAMES = "endpoint_names" -ATTR_ENDPOINT_ID = "endpoint_id" -ATTR_IEEE = "ieee" -ATTR_IN_CLUSTERS = "in_clusters" -ATTR_LAST_SEEN = "last_seen" -ATTR_LEVEL = "level" -ATTR_LQI = "lqi" -ATTR_MANUFACTURER = "manufacturer" -ATTR_MANUFACTURER_CODE = "manufacturer_code" -ATTR_MEMBERS = "members" -ATTR_MODEL = "model" -ATTR_NEIGHBORS = "neighbors" -ATTR_NODE_DESCRIPTOR = "node_descriptor" -ATTR_NWK = "nwk" -ATTR_OUT_CLUSTERS = "out_clusters" -ATTR_PARAMS = "params" -ATTR_POWER_SOURCE = "power_source" -ATTR_PROFILE_ID = "profile_id" -ATTR_QUIRK_APPLIED = "quirk_applied" -ATTR_QUIRK_CLASS = "quirk_class" -ATTR_QUIRK_ID = "quirk_id" -ATTR_ROUTES = "routes" -ATTR_RSSI = "rssi" -ATTR_SIGNATURE = "signature" -ATTR_TYPE = "type" -ATTR_UNIQUE_ID = "unique_id" -ATTR_VALUE = "value" -ATTR_WARNING_DEVICE_DURATION = "duration" -ATTR_WARNING_DEVICE_MODE = "mode" -ATTR_WARNING_DEVICE_STROBE = "strobe" -ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE = "duty_cycle" -ATTR_WARNING_DEVICE_STROBE_INTENSITY = "intensity" - -BAUD_RATES = [2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200, 128000, 256000] -BINDINGS = "bindings" - -CLUSTER_DETAILS = "cluster_details" - -CLUSTER_HANDLER_ACCELEROMETER = "accelerometer" -CLUSTER_HANDLER_BINARY_INPUT = "binary_input" -CLUSTER_HANDLER_ANALOG_INPUT = "analog_input" -CLUSTER_HANDLER_ANALOG_OUTPUT = "analog_output" -CLUSTER_HANDLER_ATTRIBUTE = "attribute" -CLUSTER_HANDLER_BASIC = "basic" -CLUSTER_HANDLER_COLOR = "light_color" -CLUSTER_HANDLER_COVER = "window_covering" -CLUSTER_HANDLER_DEVICE_TEMPERATURE = "device_temperature" -CLUSTER_HANDLER_DOORLOCK = "door_lock" -CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT = "electrical_measurement" -CLUSTER_HANDLER_EVENT_RELAY = "event_relay" -CLUSTER_HANDLER_FAN = "fan" -CLUSTER_HANDLER_HUMIDITY = "humidity" -CLUSTER_HANDLER_HUE_OCCUPANCY = "philips_occupancy" -CLUSTER_HANDLER_SOIL_MOISTURE = "soil_moisture" -CLUSTER_HANDLER_LEAF_WETNESS = "leaf_wetness" -CLUSTER_HANDLER_IAS_ACE = "ias_ace" -CLUSTER_HANDLER_IAS_WD = "ias_wd" -CLUSTER_HANDLER_IDENTIFY = "identify" -CLUSTER_HANDLER_ILLUMINANCE = "illuminance" -CLUSTER_HANDLER_LEVEL = ATTR_LEVEL -CLUSTER_HANDLER_MULTISTATE_INPUT = "multistate_input" -CLUSTER_HANDLER_OCCUPANCY = "occupancy" -CLUSTER_HANDLER_ON_OFF = "on_off" -CLUSTER_HANDLER_OTA = "ota" -CLUSTER_HANDLER_POWER_CONFIGURATION = "power" -CLUSTER_HANDLER_PRESSURE = "pressure" -CLUSTER_HANDLER_SHADE = "shade" -CLUSTER_HANDLER_SMARTENERGY_METERING = "smartenergy_metering" -CLUSTER_HANDLER_TEMPERATURE = "temperature" -CLUSTER_HANDLER_THERMOSTAT = "thermostat" -CLUSTER_HANDLER_ZDO = "zdo" -CLUSTER_HANDLER_ZONE = ZONE = "ias_zone" -CLUSTER_HANDLER_INOVELLI = "inovelli_vzm31sn_cluster" - -CLUSTER_COMMAND_SERVER = "server" -CLUSTER_COMMANDS_CLIENT = "client_commands" -CLUSTER_COMMANDS_SERVER = "server_commands" -CLUSTER_TYPE_IN = "in" -CLUSTER_TYPE_OUT = "out" - -PLATFORMS = ( - Platform.ALARM_CONTROL_PANEL, - Platform.BINARY_SENSOR, - Platform.BUTTON, - Platform.CLIMATE, - Platform.COVER, - Platform.DEVICE_TRACKER, - Platform.FAN, - Platform.LIGHT, - Platform.LOCK, - Platform.NUMBER, - Platform.SELECT, - Platform.SENSOR, - Platform.SIREN, - Platform.SWITCH, - Platform.UPDATE, -) - -CONF_ALARM_MASTER_CODE = "alarm_master_code" -CONF_ALARM_FAILED_TRIES = "alarm_failed_tries" -CONF_ALARM_ARM_REQUIRES_CODE = "alarm_arm_requires_code" - -CONF_BAUDRATE = "baudrate" -CONF_FLOW_CONTROL = "flow_control" -CONF_CUSTOM_QUIRKS_PATH = "custom_quirks_path" -CONF_DEFAULT_LIGHT_TRANSITION = "default_light_transition" -CONF_DEVICE_CONFIG = "device_config" -CONF_ENABLE_ENHANCED_LIGHT_TRANSITION = "enhanced_light_transition" -CONF_ENABLE_LIGHT_TRANSITIONING_FLAG = "light_transitioning_flag" -CONF_ALWAYS_PREFER_XY_COLOR_MODE = "always_prefer_xy_color_mode" -CONF_GROUP_MEMBERS_ASSUME_STATE = "group_members_assume_state" -CONF_ENABLE_IDENTIFY_ON_JOIN = "enable_identify_on_join" -CONF_ENABLE_QUIRKS = "enable_quirks" -CONF_RADIO_TYPE = "radio_type" -CONF_USB_PATH = "usb_path" -CONF_USE_THREAD = "use_thread" -CONF_ZIGPY = "zigpy_config" - -CONF_CONSIDER_UNAVAILABLE_MAINS = "consider_unavailable_mains" -CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS = 60 * 60 * 2 # 2 hours -CONF_CONSIDER_UNAVAILABLE_BATTERY = "consider_unavailable_battery" -CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY = 60 * 60 * 6 # 6 hours - -CONF_ZHA_OPTIONS_SCHEMA = vol.Schema( - { - vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION, default=0): vol.All( - vol.Coerce(float), vol.Range(min=0, max=2**16 / 10) - ), - vol.Required(CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, default=False): cv.boolean, - vol.Required(CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, default=True): cv.boolean, - vol.Required(CONF_ALWAYS_PREFER_XY_COLOR_MODE, default=True): cv.boolean, - vol.Required(CONF_GROUP_MEMBERS_ASSUME_STATE, default=True): cv.boolean, - vol.Required(CONF_ENABLE_IDENTIFY_ON_JOIN, default=True): cv.boolean, - vol.Optional( - CONF_CONSIDER_UNAVAILABLE_MAINS, - default=CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS, - ): cv.positive_int, - vol.Optional( - CONF_CONSIDER_UNAVAILABLE_BATTERY, - default=CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY, - ): cv.positive_int, - } -) - -CONF_ZHA_ALARM_SCHEMA = vol.Schema( - { - vol.Required(CONF_ALARM_MASTER_CODE, default="1234"): cv.string, - vol.Required(CONF_ALARM_FAILED_TRIES, default=3): cv.positive_int, - vol.Required(CONF_ALARM_ARM_REQUIRES_CODE, default=False): cv.boolean, - } -) - -CUSTOM_CONFIGURATION = "custom_configuration" - -DATA_DEVICE_CONFIG = "zha_device_config" -DATA_ZHA = "zha" -DATA_ZHA_CONFIG = "config" -DATA_ZHA_CORE_EVENTS = "zha_core_events" -DATA_ZHA_DEVICE_TRIGGER_CACHE = "zha_device_trigger_cache" -DATA_ZHA_GATEWAY = "zha_gateway" - -DEBUG_COMP_BELLOWS = "bellows" -DEBUG_COMP_ZHA = "homeassistant.components.zha" -DEBUG_COMP_ZIGPY = "zigpy" -DEBUG_COMP_ZIGPY_ZNP = "zigpy_znp" -DEBUG_COMP_ZIGPY_DECONZ = "zigpy_deconz" -DEBUG_COMP_ZIGPY_XBEE = "zigpy_xbee" -DEBUG_COMP_ZIGPY_ZIGATE = "zigpy_zigate" -DEBUG_LEVEL_CURRENT = "current" -DEBUG_LEVEL_ORIGINAL = "original" -DEBUG_LEVELS = { - DEBUG_COMP_BELLOWS: logging.DEBUG, - DEBUG_COMP_ZHA: logging.DEBUG, - DEBUG_COMP_ZIGPY: logging.DEBUG, - DEBUG_COMP_ZIGPY_ZNP: logging.DEBUG, - DEBUG_COMP_ZIGPY_DECONZ: logging.DEBUG, - DEBUG_COMP_ZIGPY_XBEE: logging.DEBUG, - DEBUG_COMP_ZIGPY_ZIGATE: logging.DEBUG, -} -DEBUG_RELAY_LOGGERS = [DEBUG_COMP_ZHA, DEBUG_COMP_ZIGPY] - -DEFAULT_RADIO_TYPE = "ezsp" -DEFAULT_BAUDRATE = 57600 -DEFAULT_DATABASE_NAME = "zigbee.db" - -DEVICE_PAIRING_STATUS = "pairing_status" - -DISCOVERY_KEY = "zha_discovery_info" - -DOMAIN = "zha" - -ENTITY_METADATA = "entity_metadata" - -GROUP_ID = "group_id" -GROUP_IDS = "group_ids" -GROUP_NAME = "group_name" - -MFG_CLUSTER_ID_START = 0xFC00 - -POWER_MAINS_POWERED = "Mains" -POWER_BATTERY_OR_UNKNOWN = "Battery or Unknown" - -PRESET_SCHEDULE = "Schedule" -PRESET_COMPLEX = "Complex" -PRESET_TEMP_MANUAL = "Temporary manual" - -ZCL_INIT_ATTRS = "ZCL_INIT_ATTRS" - -ZHA_ALARM_OPTIONS = "zha_alarm_options" -ZHA_OPTIONS = "zha_options" - -ZHA_CONFIG_SCHEMAS = { - ZHA_OPTIONS: CONF_ZHA_OPTIONS_SCHEMA, - ZHA_ALARM_OPTIONS: CONF_ZHA_ALARM_SCHEMA, -} - -type _ControllerClsType = type[zigpy.application.ControllerApplication] - - -class RadioType(enum.Enum): - """Possible options for radio type.""" - - ezsp = ( - "EZSP = Silicon Labs EmberZNet protocol: Elelabs, HUSBZB-1, Telegesis", - bellows.zigbee.application.ControllerApplication, - ) - znp = ( - "ZNP = Texas Instruments Z-Stack ZNP protocol: CC253x, CC26x2, CC13x2", - zigpy_znp.zigbee.application.ControllerApplication, - ) - deconz = ( - "deCONZ = dresden elektronik deCONZ protocol: ConBee I/II, RaspBee I/II", - zigpy_deconz.zigbee.application.ControllerApplication, - ) - zigate = ( - "ZiGate = ZiGate Zigbee radios: PiZiGate, ZiGate USB-TTL, ZiGate WiFi", - zigpy_zigate.zigbee.application.ControllerApplication, - ) - xbee = ( - "XBee = Digi XBee Zigbee radios: Digi XBee Series 2, 2C, 3", - zigpy_xbee.zigbee.application.ControllerApplication, - ) - - @classmethod - def list(cls) -> list[str]: - """Return a list of descriptions.""" - return [e.description for e in RadioType] - - @classmethod - def get_by_description(cls, description: str) -> RadioType: - """Get radio by description.""" - for radio in cls: - if radio.description == description: - return radio - raise ValueError - - def __init__(self, description: str, controller_cls: _ControllerClsType) -> None: - """Init instance.""" - self._desc = description - self._ctrl_cls = controller_cls - - @property - def controller(self) -> _ControllerClsType: - """Return controller class.""" - return self._ctrl_cls - - @property - def description(self) -> str: - """Return radio type description.""" - return self._desc - - -REPORT_CONFIG_ATTR_PER_REQ = 3 -REPORT_CONFIG_MAX_INT = 900 -REPORT_CONFIG_MAX_INT_BATTERY_SAVE = 10800 -REPORT_CONFIG_MIN_INT = 30 -REPORT_CONFIG_MIN_INT_ASAP = 1 -REPORT_CONFIG_MIN_INT_IMMEDIATE = 0 -REPORT_CONFIG_MIN_INT_OP = 5 -REPORT_CONFIG_MIN_INT_BATTERY_SAVE = 3600 -REPORT_CONFIG_RPT_CHANGE = 1 -REPORT_CONFIG_DEFAULT = ( - REPORT_CONFIG_MIN_INT, - REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_RPT_CHANGE, -) -REPORT_CONFIG_ASAP = ( - REPORT_CONFIG_MIN_INT_ASAP, - REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_RPT_CHANGE, -) -REPORT_CONFIG_BATTERY_SAVE = ( - REPORT_CONFIG_MIN_INT_BATTERY_SAVE, - REPORT_CONFIG_MAX_INT_BATTERY_SAVE, - REPORT_CONFIG_RPT_CHANGE, -) -REPORT_CONFIG_IMMEDIATE = ( - REPORT_CONFIG_MIN_INT_IMMEDIATE, - REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_RPT_CHANGE, -) -REPORT_CONFIG_OP = ( - REPORT_CONFIG_MIN_INT_OP, - REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_RPT_CHANGE, -) - -SENSOR_ACCELERATION = "acceleration" -SENSOR_BATTERY = "battery" -SENSOR_ELECTRICAL_MEASUREMENT = CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT -SENSOR_GENERIC = "generic" -SENSOR_HUMIDITY = CLUSTER_HANDLER_HUMIDITY -SENSOR_ILLUMINANCE = CLUSTER_HANDLER_ILLUMINANCE -SENSOR_METERING = "metering" -SENSOR_OCCUPANCY = CLUSTER_HANDLER_OCCUPANCY -SENSOR_OPENING = "opening" -SENSOR_PRESSURE = CLUSTER_HANDLER_PRESSURE -SENSOR_TEMPERATURE = CLUSTER_HANDLER_TEMPERATURE -SENSOR_TYPE = "sensor_type" - -SIGNAL_ADD_ENTITIES = "zha_add_new_entities" -SIGNAL_ATTR_UPDATED = "attribute_updated" -SIGNAL_AVAILABLE = "available" -SIGNAL_MOVE_LEVEL = "move_level" -SIGNAL_REMOVE = "remove" -SIGNAL_SET_LEVEL = "set_level" -SIGNAL_STATE_ATTR = "update_state_attribute" -SIGNAL_UPDATE_DEVICE = "{}_zha_update_device" -SIGNAL_GROUP_ENTITY_REMOVED = "group_entity_removed" -SIGNAL_GROUP_MEMBERSHIP_CHANGE = "group_membership_change" - -UNKNOWN = "unknown" -UNKNOWN_MANUFACTURER = "unk_manufacturer" -UNKNOWN_MODEL = "unk_model" - -WARNING_DEVICE_MODE_STOP = 0 -WARNING_DEVICE_MODE_BURGLAR = 1 -WARNING_DEVICE_MODE_FIRE = 2 -WARNING_DEVICE_MODE_EMERGENCY = 3 -WARNING_DEVICE_MODE_POLICE_PANIC = 4 -WARNING_DEVICE_MODE_FIRE_PANIC = 5 -WARNING_DEVICE_MODE_EMERGENCY_PANIC = 6 - -WARNING_DEVICE_STROBE_NO = 0 -WARNING_DEVICE_STROBE_YES = 1 - -WARNING_DEVICE_SOUND_LOW = 0 -WARNING_DEVICE_SOUND_MEDIUM = 1 -WARNING_DEVICE_SOUND_HIGH = 2 -WARNING_DEVICE_SOUND_VERY_HIGH = 3 - -WARNING_DEVICE_STROBE_LOW = 0x00 -WARNING_DEVICE_STROBE_MEDIUM = 0x01 -WARNING_DEVICE_STROBE_HIGH = 0x02 -WARNING_DEVICE_STROBE_VERY_HIGH = 0x03 - -WARNING_DEVICE_SQUAWK_MODE_ARMED = 0 -WARNING_DEVICE_SQUAWK_MODE_DISARMED = 1 - -ZHA_DISCOVERY_NEW = "zha_discovery_new_{}" -ZHA_CLUSTER_HANDLER_MSG = "zha_channel_message" -ZHA_CLUSTER_HANDLER_MSG_BIND = "zha_channel_bind" -ZHA_CLUSTER_HANDLER_MSG_CFG_RPT = "zha_channel_configure_reporting" -ZHA_CLUSTER_HANDLER_MSG_DATA = "zha_channel_msg_data" -ZHA_CLUSTER_HANDLER_CFG_DONE = "zha_channel_cfg_done" -ZHA_CLUSTER_HANDLER_READS_PER_REQ = 5 -ZHA_EVENT = "zha_event" -ZHA_GW_MSG = "zha_gateway_message" -ZHA_GW_MSG_DEVICE_FULL_INIT = "device_fully_initialized" -ZHA_GW_MSG_DEVICE_INFO = "device_info" -ZHA_GW_MSG_DEVICE_JOINED = "device_joined" -ZHA_GW_MSG_DEVICE_REMOVED = "device_removed" -ZHA_GW_MSG_GROUP_ADDED = "group_added" -ZHA_GW_MSG_GROUP_INFO = "group_info" -ZHA_GW_MSG_GROUP_MEMBER_ADDED = "group_member_added" -ZHA_GW_MSG_GROUP_MEMBER_REMOVED = "group_member_removed" -ZHA_GW_MSG_GROUP_REMOVED = "group_removed" -ZHA_GW_MSG_LOG_ENTRY = "log_entry" -ZHA_GW_MSG_LOG_OUTPUT = "log_output" -ZHA_GW_MSG_RAW_INIT = "raw_device_initialized" - - -class Strobe(t.enum8): - """Strobe enum.""" - - No_Strobe = 0x00 - Strobe = 0x01 - - -EZSP_OVERWRITE_EUI64 = ( - "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it" -) diff --git a/homeassistant/components/zha/core/decorators.py b/homeassistant/components/zha/core/decorators.py deleted file mode 100644 index d20fb7f2a38..00000000000 --- a/homeassistant/components/zha/core/decorators.py +++ /dev/null @@ -1,56 +0,0 @@ -"""Decorators for ZHA core registries.""" - -from __future__ import annotations - -from collections.abc import Callable -from typing import Any - - -class DictRegistry[_TypeT: type[Any]](dict[int | str, _TypeT]): - """Dict Registry of items.""" - - def register(self, name: int | str) -> Callable[[_TypeT], _TypeT]: - """Return decorator to register item with a specific name.""" - - def decorator(cluster_handler: _TypeT) -> _TypeT: - """Register decorated cluster handler or item.""" - self[name] = cluster_handler - return cluster_handler - - return decorator - - -class NestedDictRegistry[_TypeT: type[Any]]( - dict[int | str, dict[int | str | None, _TypeT]] -): - """Dict Registry of multiple items per key.""" - - def register( - self, name: int | str, sub_name: int | str | None = None - ) -> Callable[[_TypeT], _TypeT]: - """Return decorator to register item with a specific and a quirk name.""" - - def decorator(cluster_handler: _TypeT) -> _TypeT: - """Register decorated cluster handler or item.""" - if name not in self: - self[name] = {} - self[name][sub_name] = cluster_handler - return cluster_handler - - return decorator - - -class SetRegistry(set[int | str]): - """Set Registry of items.""" - - def register[_TypeT: type[Any]]( - self, name: int | str - ) -> Callable[[_TypeT], _TypeT]: - """Return decorator to register item with a specific name.""" - - def decorator(cluster_handler: _TypeT) -> _TypeT: - """Register decorated cluster handler or item.""" - self.add(name) - return cluster_handler - - return decorator diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py deleted file mode 100644 index 163674d614c..00000000000 --- a/homeassistant/components/zha/core/device.py +++ /dev/null @@ -1,1010 +0,0 @@ -"""Device for Zigbee Home Automation.""" - -from __future__ import annotations - -import asyncio -from collections.abc import Callable -from datetime import timedelta -from enum import Enum -from functools import cached_property -import logging -import random -import time -from typing import TYPE_CHECKING, Any, Self - -from zigpy import types -from zigpy.device import Device as ZigpyDevice -import zigpy.exceptions -from zigpy.profiles import PROFILES -import zigpy.quirks -from zigpy.quirks.v2 import CustomDeviceV2 -from zigpy.types.named import EUI64, NWK -from zigpy.zcl.clusters import Cluster -from zigpy.zcl.clusters.general import Groups, Identify -from zigpy.zcl.foundation import Status as ZclStatus, ZCLCommandDef -import zigpy.zdo.types as zdo_types - -from homeassistant.const import ATTR_COMMAND, ATTR_DEVICE_ID, ATTR_NAME -from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import device_registry as dr -from homeassistant.helpers.device_registry import DeviceEntry -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.event import async_track_time_interval - -from . import const, discovery -from .cluster_handlers import ClusterHandler, ZDOClusterHandler -from .const import ( - ATTR_ACTIVE_COORDINATOR, - ATTR_ARGS, - ATTR_ATTRIBUTE, - ATTR_AVAILABLE, - ATTR_CLUSTER_ID, - ATTR_CLUSTER_TYPE, - ATTR_COMMAND_TYPE, - ATTR_DEVICE_TYPE, - ATTR_ENDPOINT_ID, - ATTR_ENDPOINT_NAMES, - ATTR_ENDPOINTS, - ATTR_IEEE, - ATTR_LAST_SEEN, - ATTR_LQI, - ATTR_MANUFACTURER, - ATTR_MANUFACTURER_CODE, - ATTR_MODEL, - ATTR_NEIGHBORS, - ATTR_NODE_DESCRIPTOR, - ATTR_NWK, - ATTR_PARAMS, - ATTR_POWER_SOURCE, - ATTR_QUIRK_APPLIED, - ATTR_QUIRK_CLASS, - ATTR_QUIRK_ID, - ATTR_ROUTES, - ATTR_RSSI, - ATTR_SIGNATURE, - ATTR_VALUE, - CLUSTER_COMMAND_SERVER, - CLUSTER_COMMANDS_CLIENT, - CLUSTER_COMMANDS_SERVER, - CLUSTER_TYPE_IN, - CLUSTER_TYPE_OUT, - CONF_CONSIDER_UNAVAILABLE_BATTERY, - CONF_CONSIDER_UNAVAILABLE_MAINS, - CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY, - CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS, - CONF_ENABLE_IDENTIFY_ON_JOIN, - POWER_BATTERY_OR_UNKNOWN, - POWER_MAINS_POWERED, - SIGNAL_AVAILABLE, - SIGNAL_UPDATE_DEVICE, - UNKNOWN, - UNKNOWN_MANUFACTURER, - UNKNOWN_MODEL, - ZHA_OPTIONS, -) -from .endpoint import Endpoint -from .helpers import LogMixin, async_get_zha_config_value, convert_to_zcl_values - -if TYPE_CHECKING: - from ..websocket_api import ClusterBinding - from .gateway import ZHAGateway - -_LOGGER = logging.getLogger(__name__) -_UPDATE_ALIVE_INTERVAL = (60, 90) -_CHECKIN_GRACE_PERIODS = 2 - - -def get_device_automation_triggers( - device: zigpy.device.Device, -) -> dict[tuple[str, str], dict[str, str]]: - """Get the supported device automation triggers for a zigpy device.""" - return { - ("device_offline", "device_offline"): {"device_event_type": "device_offline"}, - **getattr(device, "device_automation_triggers", {}), - } - - -class DeviceStatus(Enum): - """Status of a device.""" - - CREATED = 1 - INITIALIZED = 2 - - -class ZHADevice(LogMixin): - """ZHA Zigbee device object.""" - - _ha_device_id: str - - def __init__( - self, - hass: HomeAssistant, - zigpy_device: zigpy.device.Device, - zha_gateway: ZHAGateway, - ) -> None: - """Initialize the gateway.""" - self.hass: HomeAssistant = hass - self._zigpy_device: ZigpyDevice = zigpy_device - self._zha_gateway: ZHAGateway = zha_gateway - self._available_signal: str = f"{self.name}_{self.ieee}_{SIGNAL_AVAILABLE}" - self._checkins_missed_count: int = 0 - self.unsubs: list[Callable[[], None]] = [] - self.quirk_applied: bool = isinstance( - self._zigpy_device, zigpy.quirks.CustomDevice - ) - self.quirk_class: str = ( - f"{self._zigpy_device.__class__.__module__}." - f"{self._zigpy_device.__class__.__name__}" - ) - self.quirk_id: str | None = getattr(self._zigpy_device, ATTR_QUIRK_ID, None) - - if self.is_mains_powered: - self.consider_unavailable_time: int = async_get_zha_config_value( - self._zha_gateway.config_entry, - ZHA_OPTIONS, - CONF_CONSIDER_UNAVAILABLE_MAINS, - CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS, - ) - else: - self.consider_unavailable_time = async_get_zha_config_value( - self._zha_gateway.config_entry, - ZHA_OPTIONS, - CONF_CONSIDER_UNAVAILABLE_BATTERY, - CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY, - ) - self._available: bool = self.is_coordinator or ( - self.last_seen is not None - and time.time() - self.last_seen < self.consider_unavailable_time - ) - self._zdo_handler: ZDOClusterHandler = ZDOClusterHandler(self) - self._power_config_ch: ClusterHandler | None = None - self._identify_ch: ClusterHandler | None = None - self._basic_ch: ClusterHandler | None = None - self.status: DeviceStatus = DeviceStatus.CREATED - - self._endpoints: dict[int, Endpoint] = {} - for ep_id, endpoint in zigpy_device.endpoints.items(): - if ep_id != 0: - self._endpoints[ep_id] = Endpoint.new(endpoint, self) - - if not self.is_coordinator: - keep_alive_interval = random.randint(*_UPDATE_ALIVE_INTERVAL) - self.debug( - "starting availability checks - interval: %s", keep_alive_interval - ) - self.unsubs.append( - async_track_time_interval( - self.hass, - self._check_available, - timedelta(seconds=keep_alive_interval), - ) - ) - - @property - def device_id(self) -> str: - """Return the HA device registry device id.""" - return self._ha_device_id - - def set_device_id(self, device_id: str) -> None: - """Set the HA device registry device id.""" - self._ha_device_id = device_id - - @property - def device(self) -> zigpy.device.Device: - """Return underlying Zigpy device.""" - return self._zigpy_device - - @property - def name(self) -> str: - """Return device name.""" - return f"{self.manufacturer} {self.model}" - - @property - def ieee(self) -> EUI64: - """Return ieee address for device.""" - return self._zigpy_device.ieee - - @property - def manufacturer(self) -> str: - """Return manufacturer for device.""" - if self._zigpy_device.manufacturer is None: - return UNKNOWN_MANUFACTURER - return self._zigpy_device.manufacturer - - @property - def model(self) -> str: - """Return model for device.""" - if self._zigpy_device.model is None: - return UNKNOWN_MODEL - return self._zigpy_device.model - - @property - def manufacturer_code(self) -> int | None: - """Return the manufacturer code for the device.""" - if self._zigpy_device.node_desc is None: - return None - - return self._zigpy_device.node_desc.manufacturer_code - - @property - def nwk(self) -> NWK: - """Return nwk for device.""" - return self._zigpy_device.nwk - - @property - def lqi(self): - """Return lqi for device.""" - return self._zigpy_device.lqi - - @property - def rssi(self): - """Return rssi for device.""" - return self._zigpy_device.rssi - - @property - def last_seen(self) -> float | None: - """Return last_seen for device.""" - return self._zigpy_device.last_seen - - @property - def is_mains_powered(self) -> bool | None: - """Return true if device is mains powered.""" - if self._zigpy_device.node_desc is None: - return None - - return self._zigpy_device.node_desc.is_mains_powered - - @property - def device_type(self) -> str: - """Return the logical device type for the device.""" - if self._zigpy_device.node_desc is None: - return UNKNOWN - - return self._zigpy_device.node_desc.logical_type.name - - @property - def power_source(self) -> str: - """Return the power source for the device.""" - return ( - POWER_MAINS_POWERED if self.is_mains_powered else POWER_BATTERY_OR_UNKNOWN - ) - - @property - def is_router(self) -> bool | None: - """Return true if this is a routing capable device.""" - if self._zigpy_device.node_desc is None: - return None - - return self._zigpy_device.node_desc.is_router - - @property - def is_coordinator(self) -> bool | None: - """Return true if this device represents a coordinator.""" - if self._zigpy_device.node_desc is None: - return None - - return self._zigpy_device.node_desc.is_coordinator - - @property - def is_active_coordinator(self) -> bool: - """Return true if this device is the active coordinator.""" - if not self.is_coordinator: - return False - - return self.ieee == self.gateway.state.node_info.ieee - - @property - def is_end_device(self) -> bool | None: - """Return true if this device is an end device.""" - if self._zigpy_device.node_desc is None: - return None - - return self._zigpy_device.node_desc.is_end_device - - @property - def is_groupable(self) -> bool: - """Return true if this device has a group cluster.""" - return self.is_coordinator or ( - self.available and bool(self.async_get_groupable_endpoints()) - ) - - @property - def skip_configuration(self) -> bool: - """Return true if the device should not issue configuration related commands.""" - return self._zigpy_device.skip_configuration or bool(self.is_coordinator) - - @property - def gateway(self): - """Return the gateway for this device.""" - return self._zha_gateway - - @cached_property - def device_automation_commands(self) -> dict[str, list[tuple[str, str]]]: - """Return the a lookup of commands to etype/sub_type.""" - commands: dict[str, list[tuple[str, str]]] = {} - for etype_subtype, trigger in self.device_automation_triggers.items(): - if command := trigger.get(ATTR_COMMAND): - commands.setdefault(command, []).append(etype_subtype) - return commands - - @cached_property - def device_automation_triggers(self) -> dict[tuple[str, str], dict[str, str]]: - """Return the device automation triggers for this device.""" - return get_device_automation_triggers(self._zigpy_device) - - @property - def available_signal(self) -> str: - """Signal to use to subscribe to device availability changes.""" - return self._available_signal - - @property - def available(self): - """Return True if device is available.""" - return self._available - - @available.setter - def available(self, new_availability: bool) -> None: - """Set device availability.""" - self._available = new_availability - - @property - def power_configuration_ch(self) -> ClusterHandler | None: - """Return power configuration cluster handler.""" - return self._power_config_ch - - @power_configuration_ch.setter - def power_configuration_ch(self, cluster_handler: ClusterHandler) -> None: - """Power configuration cluster handler setter.""" - if self._power_config_ch is None: - self._power_config_ch = cluster_handler - - @property - def basic_ch(self) -> ClusterHandler | None: - """Return basic cluster handler.""" - return self._basic_ch - - @basic_ch.setter - def basic_ch(self, cluster_handler: ClusterHandler) -> None: - """Set the basic cluster handler.""" - if self._basic_ch is None: - self._basic_ch = cluster_handler - - @property - def identify_ch(self) -> ClusterHandler | None: - """Return power configuration cluster handler.""" - return self._identify_ch - - @identify_ch.setter - def identify_ch(self, cluster_handler: ClusterHandler) -> None: - """Power configuration cluster handler setter.""" - if self._identify_ch is None: - self._identify_ch = cluster_handler - - @property - def zdo_cluster_handler(self) -> ZDOClusterHandler: - """Return ZDO cluster handler.""" - return self._zdo_handler - - @property - def endpoints(self) -> dict[int, Endpoint]: - """Return the endpoints for this device.""" - return self._endpoints - - @property - def zigbee_signature(self) -> dict[str, Any]: - """Get zigbee signature for this device.""" - return { - ATTR_NODE_DESCRIPTOR: str(self._zigpy_device.node_desc), - ATTR_ENDPOINTS: { - signature[0]: signature[1] - for signature in [ - endpoint.zigbee_signature for endpoint in self._endpoints.values() - ] - }, - ATTR_MANUFACTURER: self.manufacturer, - ATTR_MODEL: self.model, - } - - @property - def sw_version(self) -> str | None: - """Return the software version for this device.""" - device_registry = dr.async_get(self.hass) - reg_device: DeviceEntry | None = device_registry.async_get(self.device_id) - if reg_device is None: - return None - return reg_device.sw_version - - @classmethod - def new( - cls, - hass: HomeAssistant, - zigpy_dev: zigpy.device.Device, - gateway: ZHAGateway, - ) -> Self: - """Create new device.""" - zha_dev = cls(hass, zigpy_dev, gateway) - zha_dev.unsubs.append( - async_dispatcher_connect( - hass, - SIGNAL_UPDATE_DEVICE.format(str(zha_dev.ieee)), - zha_dev.async_update_sw_build_id, - ) - ) - discovery.PROBE.discover_device_entities(zha_dev) - return zha_dev - - @callback - def async_update_sw_build_id(self, sw_version: int) -> None: - """Update device sw version.""" - if self.device_id is None: - return - - device_registry = dr.async_get(self.hass) - device_registry.async_update_device( - self.device_id, sw_version=f"0x{sw_version:08x}" - ) - - async def _check_available(self, *_: Any) -> None: - # don't flip the availability state of the coordinator - if self.is_coordinator: - return - if self.last_seen is None: - self.debug("last_seen is None, marking the device unavailable") - self.update_available(False) - return - - difference = time.time() - self.last_seen - if difference < self.consider_unavailable_time: - self.debug( - "Device seen - marking the device available and resetting counter" - ) - self.update_available(True) - self._checkins_missed_count = 0 - return - - if self.hass.data[const.DATA_ZHA].allow_polling: - if ( - self._checkins_missed_count >= _CHECKIN_GRACE_PERIODS - or self.manufacturer == "LUMI" - or not self._endpoints - ): - self.debug( - ( - "last_seen is %s seconds ago and ping attempts have been exhausted," - " marking the device unavailable" - ), - difference, - ) - self.update_available(False) - return - - self._checkins_missed_count += 1 - self.debug( - "Attempting to checkin with device - missed checkins: %s", - self._checkins_missed_count, - ) - if not self.basic_ch: - self.debug("does not have a mandatory basic cluster") - self.update_available(False) - return - res = await self.basic_ch.get_attribute_value( - ATTR_MANUFACTURER, from_cache=False - ) - if res is not None: - self._checkins_missed_count = 0 - - def update_available(self, available: bool) -> None: - """Update device availability and signal entities.""" - self.debug( - ( - "Update device availability - device available: %s - new availability:" - " %s - changed: %s" - ), - self.available, - available, - self.available ^ available, - ) - availability_changed = self.available ^ available - self.available = available - if availability_changed and available: - # reinit cluster handlers then signal entities - self.debug( - "Device availability changed and device became available," - " reinitializing cluster handlers" - ) - self.hass.async_create_task(self._async_became_available()) - return - if availability_changed and not available: - self.debug("Device availability changed and device became unavailable") - self.zha_send_event( - { - "device_event_type": "device_offline", - }, - ) - async_dispatcher_send(self.hass, f"{self._available_signal}_entity") - - @callback - def zha_send_event(self, event_data: dict[str, str | int]) -> None: - """Relay events to hass.""" - self.hass.bus.async_fire( - const.ZHA_EVENT, - { - const.ATTR_DEVICE_IEEE: str(self.ieee), - const.ATTR_UNIQUE_ID: str(self.ieee), - ATTR_DEVICE_ID: self.device_id, - **event_data, - }, - ) - - async def _async_became_available(self) -> None: - """Update device availability and signal entities.""" - await self.async_initialize(False) - async_dispatcher_send(self.hass, f"{self._available_signal}_entity") - - @property - def device_info(self) -> dict[str, Any]: - """Return a device description for device.""" - ieee = str(self.ieee) - time_struct = time.localtime(self.last_seen) - update_time = time.strftime("%Y-%m-%dT%H:%M:%S", time_struct) - return { - ATTR_IEEE: ieee, - ATTR_NWK: self.nwk, - ATTR_MANUFACTURER: self.manufacturer, - ATTR_MODEL: self.model, - ATTR_NAME: self.name or ieee, - ATTR_QUIRK_APPLIED: self.quirk_applied, - ATTR_QUIRK_CLASS: self.quirk_class, - ATTR_QUIRK_ID: self.quirk_id, - ATTR_MANUFACTURER_CODE: self.manufacturer_code, - ATTR_POWER_SOURCE: self.power_source, - ATTR_LQI: self.lqi, - ATTR_RSSI: self.rssi, - ATTR_LAST_SEEN: update_time, - ATTR_AVAILABLE: self.available, - ATTR_DEVICE_TYPE: self.device_type, - ATTR_SIGNATURE: self.zigbee_signature, - } - - async def async_configure(self) -> None: - """Configure the device.""" - should_identify = async_get_zha_config_value( - self._zha_gateway.config_entry, - ZHA_OPTIONS, - CONF_ENABLE_IDENTIFY_ON_JOIN, - True, - ) - self.debug("started configuration") - await self._zdo_handler.async_configure() - self._zdo_handler.debug("'async_configure' stage succeeded") - await asyncio.gather( - *(endpoint.async_configure() for endpoint in self._endpoints.values()) - ) - if isinstance(self._zigpy_device, CustomDeviceV2): - self.debug("applying quirks v2 custom device configuration") - await self._zigpy_device.apply_custom_configuration() - async_dispatcher_send( - self.hass, - const.ZHA_CLUSTER_HANDLER_MSG, - { - const.ATTR_TYPE: const.ZHA_CLUSTER_HANDLER_CFG_DONE, - }, - ) - self.debug("completed configuration") - - if ( - should_identify - and self.identify_ch is not None - and not self.skip_configuration - ): - await self.identify_ch.trigger_effect( - effect_id=Identify.EffectIdentifier.Okay, - effect_variant=Identify.EffectVariant.Default, - ) - - async def async_initialize(self, from_cache: bool = False) -> None: - """Initialize cluster handlers.""" - self.debug("started initialization") - await self._zdo_handler.async_initialize(from_cache) - self._zdo_handler.debug("'async_initialize' stage succeeded") - - # We intentionally do not use `gather` here! This is so that if, for example, - # three `device.async_initialize()`s are spawned, only three concurrent requests - # will ever be in flight at once. Startup concurrency is managed at the device - # level. - for endpoint in self._endpoints.values(): - try: - await endpoint.async_initialize(from_cache) - except Exception: # noqa: BLE001 - self.debug("Failed to initialize endpoint", exc_info=True) - - self.debug("power source: %s", self.power_source) - self.status = DeviceStatus.INITIALIZED - self.debug("completed initialization") - - @callback - def async_cleanup_handles(self) -> None: - """Unsubscribe the dispatchers and timers.""" - for unsubscribe in self.unsubs: - unsubscribe() - - @property - def zha_device_info(self) -> dict[str, Any]: - """Get ZHA device information.""" - device_info: dict[str, Any] = {} - device_info.update(self.device_info) - device_info[ATTR_ACTIVE_COORDINATOR] = self.is_active_coordinator - device_info["entities"] = [ - { - "entity_id": entity_ref.reference_id, - ATTR_NAME: entity_ref.device_info[ATTR_NAME], - } - for entity_ref in self.gateway.device_registry[self.ieee] - ] - - topology = self.gateway.application_controller.topology - device_info[ATTR_NEIGHBORS] = [ - { - "device_type": neighbor.device_type.name, - "rx_on_when_idle": neighbor.rx_on_when_idle.name, - "relationship": neighbor.relationship.name, - "extended_pan_id": str(neighbor.extended_pan_id), - "ieee": str(neighbor.ieee), - "nwk": str(neighbor.nwk), - "permit_joining": neighbor.permit_joining.name, - "depth": str(neighbor.depth), - "lqi": str(neighbor.lqi), - } - for neighbor in topology.neighbors[self.ieee] - ] - - device_info[ATTR_ROUTES] = [ - { - "dest_nwk": str(route.DstNWK), - "route_status": str(route.RouteStatus.name), - "memory_constrained": bool(route.MemoryConstrained), - "many_to_one": bool(route.ManyToOne), - "route_record_required": bool(route.RouteRecordRequired), - "next_hop": str(route.NextHop), - } - for route in topology.routes[self.ieee] - ] - - # Return endpoint device type Names - names: list[dict[str, str]] = [] - for endpoint in (ep for epid, ep in self.device.endpoints.items() if epid): - profile = PROFILES.get(endpoint.profile_id) - if profile and endpoint.device_type is not None: - # DeviceType provides undefined enums - names.append({ATTR_NAME: profile.DeviceType(endpoint.device_type).name}) - else: - names.append( - { - ATTR_NAME: ( - f"unknown {endpoint.device_type} device_type " - f"of 0x{(endpoint.profile_id or 0xFFFF):04x} profile id" - ) - } - ) - device_info[ATTR_ENDPOINT_NAMES] = names - - device_registry = dr.async_get(self.hass) - reg_device = device_registry.async_get(self.device_id) - if reg_device is not None: - device_info["user_given_name"] = reg_device.name_by_user - device_info["device_reg_id"] = reg_device.id - device_info["area_id"] = reg_device.area_id - return device_info - - @callback - def async_get_clusters(self) -> dict[int, dict[str, dict[int, Cluster]]]: - """Get all clusters for this device.""" - return { - ep_id: { - CLUSTER_TYPE_IN: endpoint.in_clusters, - CLUSTER_TYPE_OUT: endpoint.out_clusters, - } - for (ep_id, endpoint) in self._zigpy_device.endpoints.items() - if ep_id != 0 - } - - @callback - def async_get_groupable_endpoints(self): - """Get device endpoints that have a group 'in' cluster.""" - return [ - ep_id - for (ep_id, clusters) in self.async_get_clusters().items() - if Groups.cluster_id in clusters[CLUSTER_TYPE_IN] - ] - - @callback - def async_get_std_clusters(self): - """Get ZHA and ZLL clusters for this device.""" - - return { - ep_id: { - CLUSTER_TYPE_IN: endpoint.in_clusters, - CLUSTER_TYPE_OUT: endpoint.out_clusters, - } - for (ep_id, endpoint) in self._zigpy_device.endpoints.items() - if ep_id != 0 and endpoint.profile_id in PROFILES - } - - @callback - def async_get_cluster( - self, endpoint_id: int, cluster_id: int, cluster_type: str = CLUSTER_TYPE_IN - ) -> Cluster: - """Get zigbee cluster from this entity.""" - clusters: dict[int, dict[str, dict[int, Cluster]]] = self.async_get_clusters() - return clusters[endpoint_id][cluster_type][cluster_id] - - @callback - def async_get_cluster_attributes( - self, endpoint_id, cluster_id, cluster_type=CLUSTER_TYPE_IN - ): - """Get zigbee attributes for specified cluster.""" - cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type) - if cluster is None: - return None - return cluster.attributes - - @callback - def async_get_cluster_commands( - self, endpoint_id, cluster_id, cluster_type=CLUSTER_TYPE_IN - ): - """Get zigbee commands for specified cluster.""" - cluster = self.async_get_cluster(endpoint_id, cluster_id, cluster_type) - if cluster is None: - return None - return { - CLUSTER_COMMANDS_CLIENT: cluster.client_commands, - CLUSTER_COMMANDS_SERVER: cluster.server_commands, - } - - async def write_zigbee_attribute( - self, - endpoint_id, - cluster_id, - attribute, - value, - cluster_type=CLUSTER_TYPE_IN, - manufacturer=None, - ): - """Write a value to a zigbee attribute for a cluster in this entity.""" - try: - cluster: Cluster = self.async_get_cluster( - endpoint_id, cluster_id, cluster_type - ) - except KeyError as exc: - raise ValueError( - f"Cluster {cluster_id} not found on endpoint {endpoint_id} while" - f" writing attribute {attribute} with value {value}" - ) from exc - - try: - response = await cluster.write_attributes( - {attribute: value}, manufacturer=manufacturer - ) - except zigpy.exceptions.ZigbeeException as exc: - raise HomeAssistantError( - f"Failed to set attribute: " - f"{ATTR_VALUE}: {value} " - f"{ATTR_ATTRIBUTE}: {attribute} " - f"{ATTR_CLUSTER_ID}: {cluster_id} " - f"{ATTR_ENDPOINT_ID}: {endpoint_id}" - ) from exc - - self.debug( - "set: %s for attr: %s to cluster: %s for ept: %s - res: %s", - value, - attribute, - cluster_id, - endpoint_id, - response, - ) - return response - - async def issue_cluster_command( - self, - endpoint_id: int, - cluster_id: int, - command: int, - command_type: str, - args: list | None, - params: dict[str, Any] | None, - cluster_type: str = CLUSTER_TYPE_IN, - manufacturer: int | None = None, - ) -> None: - """Issue a command against specified zigbee cluster on this device.""" - try: - cluster: Cluster = self.async_get_cluster( - endpoint_id, cluster_id, cluster_type - ) - except KeyError as exc: - raise ValueError( - f"Cluster {cluster_id} not found on endpoint {endpoint_id} while" - f" issuing command {command} with args {args}" - ) from exc - commands: dict[int, ZCLCommandDef] = ( - cluster.server_commands - if command_type == CLUSTER_COMMAND_SERVER - else cluster.client_commands - ) - if args is not None: - self.warning( - ( - "args [%s] are deprecated and should be passed with the params key." - " The parameter names are: %s" - ), - args, - [field.name for field in commands[command].schema.fields], - ) - response = await getattr(cluster, commands[command].name)(*args) - else: - assert params is not None - response = await getattr(cluster, commands[command].name)( - **convert_to_zcl_values(params, commands[command].schema) - ) - self.debug( - "Issued cluster command: %s %s %s %s %s %s %s %s", - f"{ATTR_CLUSTER_ID}: [{cluster_id}]", - f"{ATTR_CLUSTER_TYPE}: [{cluster_type}]", - f"{ATTR_ENDPOINT_ID}: [{endpoint_id}]", - f"{ATTR_COMMAND}: [{command}]", - f"{ATTR_COMMAND_TYPE}: [{command_type}]", - f"{ATTR_ARGS}: [{args}]", - f"{ATTR_PARAMS}: [{params}]", - f"{ATTR_MANUFACTURER}: [{manufacturer}]", - ) - if response is None: - return # client commands don't return a response - if isinstance(response, Exception): - raise HomeAssistantError("Failed to issue cluster command") from response - if response[1] is not ZclStatus.SUCCESS: - raise HomeAssistantError( - f"Failed to issue cluster command with status: {response[1]}" - ) - - async def async_add_to_group(self, group_id: int) -> None: - """Add this device to the provided zigbee group.""" - try: - # A group name is required. However, the spec also explicitly states that - # the group name can be ignored by the receiving device if a device cannot - # store it, so we cannot rely on it existing after being written. This is - # only done to make the ZCL command valid. - await self._zigpy_device.add_to_group(group_id, name=f"0x{group_id:04X}") - except (zigpy.exceptions.ZigbeeException, TimeoutError) as ex: - self.debug( - "Failed to add device '%s' to group: 0x%04x ex: %s", - self._zigpy_device.ieee, - group_id, - str(ex), - ) - - async def async_remove_from_group(self, group_id: int) -> None: - """Remove this device from the provided zigbee group.""" - try: - await self._zigpy_device.remove_from_group(group_id) - except (zigpy.exceptions.ZigbeeException, TimeoutError) as ex: - self.debug( - "Failed to remove device '%s' from group: 0x%04x ex: %s", - self._zigpy_device.ieee, - group_id, - str(ex), - ) - - async def async_add_endpoint_to_group( - self, endpoint_id: int, group_id: int - ) -> None: - """Add the device endpoint to the provided zigbee group.""" - try: - await self._zigpy_device.endpoints[endpoint_id].add_to_group( - group_id, name=f"0x{group_id:04X}" - ) - except (zigpy.exceptions.ZigbeeException, TimeoutError) as ex: - self.debug( - "Failed to add endpoint: %s for device: '%s' to group: 0x%04x ex: %s", - endpoint_id, - self._zigpy_device.ieee, - group_id, - str(ex), - ) - - async def async_remove_endpoint_from_group( - self, endpoint_id: int, group_id: int - ) -> None: - """Remove the device endpoint from the provided zigbee group.""" - try: - await self._zigpy_device.endpoints[endpoint_id].remove_from_group(group_id) - except (zigpy.exceptions.ZigbeeException, TimeoutError) as ex: - self.debug( - ( - "Failed to remove endpoint: %s for device '%s' from group: 0x%04x" - " ex: %s" - ), - endpoint_id, - self._zigpy_device.ieee, - group_id, - str(ex), - ) - - async def async_bind_to_group( - self, group_id: int, cluster_bindings: list[ClusterBinding] - ) -> None: - """Directly bind this device to a group for the given clusters.""" - await self._async_group_binding_operation( - group_id, zdo_types.ZDOCmd.Bind_req, cluster_bindings - ) - - async def async_unbind_from_group( - self, group_id: int, cluster_bindings: list[ClusterBinding] - ) -> None: - """Unbind this device from a group for the given clusters.""" - await self._async_group_binding_operation( - group_id, zdo_types.ZDOCmd.Unbind_req, cluster_bindings - ) - - async def _async_group_binding_operation( - self, - group_id: int, - operation: zdo_types.ZDOCmd, - cluster_bindings: list[ClusterBinding], - ) -> None: - """Create or remove a direct zigbee binding between a device and a group.""" - - zdo = self._zigpy_device.zdo - op_msg = "0x%04x: %s %s, ep: %s, cluster: %s to group: 0x%04x" - destination_address = zdo_types.MultiAddress() - destination_address.addrmode = types.uint8_t(1) - destination_address.nwk = types.uint16_t(group_id) - - tasks = [] - - for cluster_binding in cluster_bindings: - if cluster_binding.endpoint_id == 0: - continue - if ( - cluster_binding.id - in self._zigpy_device.endpoints[ - cluster_binding.endpoint_id - ].out_clusters - ): - op_params = ( - self.nwk, - operation.name, - str(self.ieee), - cluster_binding.endpoint_id, - cluster_binding.id, - group_id, - ) - zdo.debug(f"processing {op_msg}", *op_params) - tasks.append( - ( - zdo.request( - operation, - self.ieee, - cluster_binding.endpoint_id, - cluster_binding.id, - destination_address, - ), - op_msg, - op_params, - ) - ) - res = await asyncio.gather(*(t[0] for t in tasks), return_exceptions=True) - for outcome, log_msg in zip(res, tasks, strict=False): - if isinstance(outcome, Exception): - fmt = f"{log_msg[1]} failed: %s" - else: - fmt = f"{log_msg[1]} completed: %s" - zdo.debug(fmt, *(log_msg[2] + (outcome,))) - - def log(self, level: int, msg: str, *args: Any, **kwargs: Any) -> None: - """Log a message.""" - msg = f"[%s](%s): {msg}" - args = (self.nwk, self.model, *args) - _LOGGER.log(level, msg, *args, **kwargs) diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py deleted file mode 100644 index 3c342d14060..00000000000 --- a/homeassistant/components/zha/core/discovery.py +++ /dev/null @@ -1,661 +0,0 @@ -"""Device discovery functions for Zigbee Home Automation.""" - -from __future__ import annotations - -from collections import Counter -from collections.abc import Callable -import logging -from typing import TYPE_CHECKING, Any, cast - -from slugify import slugify -from zigpy.quirks.v2 import ( - BinarySensorMetadata, - CustomDeviceV2, - EntityType, - NumberMetadata, - SwitchMetadata, - WriteAttributeButtonMetadata, - ZCLCommandButtonMetadata, - ZCLEnumMetadata, - ZCLSensorMetadata, -) -from zigpy.state import State -from zigpy.zcl import ClusterType -from zigpy.zcl.clusters.general import Ota - -from homeassistant.const import CONF_TYPE, Platform -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.entity_registry import async_entries_for_device -from homeassistant.helpers.typing import ConfigType - -from .. import ( # noqa: F401 - alarm_control_panel, - binary_sensor, - button, - climate, - cover, - device_tracker, - fan, - light, - lock, - number, - select, - sensor, - siren, - switch, - update, -) -from . import const as zha_const, registries as zha_regs - -# importing cluster handlers updates registries -from .cluster_handlers import ( # noqa: F401 - ClusterHandler, - closures, - general, - homeautomation, - hvac, - lighting, - lightlink, - manufacturerspecific, - measurement, - protocol, - security, - smartenergy, -) -from .helpers import get_zha_data, get_zha_gateway - -if TYPE_CHECKING: - from ..entity import ZhaEntity - from .device import ZHADevice - from .endpoint import Endpoint - from .group import ZHAGroup - -_LOGGER = logging.getLogger(__name__) - - -QUIRKS_ENTITY_META_TO_ENTITY_CLASS = { - ( - Platform.BUTTON, - WriteAttributeButtonMetadata, - EntityType.CONFIG, - ): button.ZHAAttributeButton, - ( - Platform.BUTTON, - WriteAttributeButtonMetadata, - EntityType.STANDARD, - ): button.ZHAAttributeButton, - (Platform.BUTTON, ZCLCommandButtonMetadata, EntityType.CONFIG): button.ZHAButton, - ( - Platform.BUTTON, - ZCLCommandButtonMetadata, - EntityType.DIAGNOSTIC, - ): button.ZHAButton, - (Platform.BUTTON, ZCLCommandButtonMetadata, EntityType.STANDARD): button.ZHAButton, - ( - Platform.BINARY_SENSOR, - BinarySensorMetadata, - EntityType.CONFIG, - ): binary_sensor.BinarySensor, - ( - Platform.BINARY_SENSOR, - BinarySensorMetadata, - EntityType.DIAGNOSTIC, - ): binary_sensor.BinarySensor, - ( - Platform.BINARY_SENSOR, - BinarySensorMetadata, - EntityType.STANDARD, - ): binary_sensor.BinarySensor, - (Platform.SENSOR, ZCLEnumMetadata, EntityType.DIAGNOSTIC): sensor.EnumSensor, - (Platform.SENSOR, ZCLEnumMetadata, EntityType.STANDARD): sensor.EnumSensor, - (Platform.SENSOR, ZCLSensorMetadata, EntityType.DIAGNOSTIC): sensor.Sensor, - (Platform.SENSOR, ZCLSensorMetadata, EntityType.STANDARD): sensor.Sensor, - (Platform.SELECT, ZCLEnumMetadata, EntityType.CONFIG): select.ZCLEnumSelectEntity, - (Platform.SELECT, ZCLEnumMetadata, EntityType.STANDARD): select.ZCLEnumSelectEntity, - ( - Platform.SELECT, - ZCLEnumMetadata, - EntityType.DIAGNOSTIC, - ): select.ZCLEnumSelectEntity, - ( - Platform.NUMBER, - NumberMetadata, - EntityType.CONFIG, - ): number.ZHANumberConfigurationEntity, - (Platform.NUMBER, NumberMetadata, EntityType.DIAGNOSTIC): number.ZhaNumber, - (Platform.NUMBER, NumberMetadata, EntityType.STANDARD): number.ZhaNumber, - ( - Platform.SWITCH, - SwitchMetadata, - EntityType.CONFIG, - ): switch.ZHASwitchConfigurationEntity, - (Platform.SWITCH, SwitchMetadata, EntityType.STANDARD): switch.Switch, -} - - -@callback -async def async_add_entities( - _async_add_entities: AddEntitiesCallback, - entities: list[ - tuple[ - type[ZhaEntity], - tuple[str, ZHADevice, list[ClusterHandler]], - dict[str, Any], - ] - ], - **kwargs, -) -> None: - """Add entities helper.""" - if not entities: - return - - to_add = [ - ent_cls.create_entity(*args, **{**kwargs, **kw_args}) - for ent_cls, args, kw_args in entities - ] - entities_to_add = [entity for entity in to_add if entity is not None] - _async_add_entities(entities_to_add, update_before_add=False) - entities.clear() - - -class ProbeEndpoint: - """All discovered cluster handlers and entities of an endpoint.""" - - def __init__(self) -> None: - """Initialize instance.""" - self._device_configs: ConfigType = {} - - @callback - def discover_entities(self, endpoint: Endpoint) -> None: - """Process an endpoint on a zigpy device.""" - _LOGGER.debug( - "Discovering entities for endpoint: %s-%s", - str(endpoint.device.ieee), - endpoint.id, - ) - self.discover_by_device_type(endpoint) - self.discover_multi_entities(endpoint) - self.discover_by_cluster_id(endpoint) - self.discover_multi_entities(endpoint, config_diagnostic_entities=True) - zha_regs.ZHA_ENTITIES.clean_up() - - @callback - def discover_device_entities(self, device: ZHADevice) -> None: - """Discover entities for a ZHA device.""" - _LOGGER.debug( - "Discovering entities for device: %s-%s", - str(device.ieee), - device.name, - ) - - if device.is_coordinator: - self.discover_coordinator_device_entities(device) - return - - self.discover_quirks_v2_entities(device) - zha_regs.ZHA_ENTITIES.clean_up() - - @callback - def discover_quirks_v2_entities(self, device: ZHADevice) -> None: - """Discover entities for a ZHA device exposed by quirks v2.""" - _LOGGER.debug( - "Attempting to discover quirks v2 entities for device: %s-%s", - str(device.ieee), - device.name, - ) - - if not isinstance(device.device, CustomDeviceV2): - _LOGGER.debug( - "Device: %s-%s is not a quirks v2 device - skipping " - "discover_quirks_v2_entities", - str(device.ieee), - device.name, - ) - return - - zigpy_device: CustomDeviceV2 = device.device - - if not zigpy_device.exposes_metadata: - _LOGGER.debug( - "Device: %s-%s does not expose any quirks v2 entities", - str(device.ieee), - device.name, - ) - return - - for ( - cluster_details, - entity_metadata_list, - ) in zigpy_device.exposes_metadata.items(): - endpoint_id, cluster_id, cluster_type = cluster_details - - if endpoint_id not in device.endpoints: - _LOGGER.warning( - "Device: %s-%s does not have an endpoint with id: %s - unable to " - "create entity with cluster details: %s", - str(device.ieee), - device.name, - endpoint_id, - cluster_details, - ) - continue - - endpoint: Endpoint = device.endpoints[endpoint_id] - cluster = ( - endpoint.zigpy_endpoint.in_clusters.get(cluster_id) - if cluster_type is ClusterType.Server - else endpoint.zigpy_endpoint.out_clusters.get(cluster_id) - ) - - if cluster is None: - _LOGGER.warning( - "Device: %s-%s does not have a cluster with id: %s - " - "unable to create entity with cluster details: %s", - str(device.ieee), - device.name, - cluster_id, - cluster_details, - ) - continue - - cluster_handler_id = f"{endpoint.id}:0x{cluster.cluster_id:04x}" - cluster_handler = ( - endpoint.all_cluster_handlers.get(cluster_handler_id) - if cluster_type is ClusterType.Server - else endpoint.client_cluster_handlers.get(cluster_handler_id) - ) - assert cluster_handler - - for entity_metadata in entity_metadata_list: - platform = Platform(entity_metadata.entity_platform.value) - metadata_type = type(entity_metadata) - entity_class = QUIRKS_ENTITY_META_TO_ENTITY_CLASS.get( - (platform, metadata_type, entity_metadata.entity_type) - ) - - if entity_class is None: - _LOGGER.warning( - "Device: %s-%s has an entity with details: %s that does not" - " have an entity class mapping - unable to create entity", - str(device.ieee), - device.name, - { - zha_const.CLUSTER_DETAILS: cluster_details, - zha_const.ENTITY_METADATA: entity_metadata, - }, - ) - continue - - # automatically add the attribute to ZCL_INIT_ATTRS for the cluster - # handler if it is not already in the list - if ( - hasattr(entity_metadata, "attribute_name") - and entity_metadata.attribute_name - not in cluster_handler.ZCL_INIT_ATTRS - ): - init_attrs = cluster_handler.ZCL_INIT_ATTRS.copy() - init_attrs[entity_metadata.attribute_name] = ( - entity_metadata.attribute_initialized_from_cache - ) - cluster_handler.__dict__[zha_const.ZCL_INIT_ATTRS] = init_attrs - - endpoint.async_new_entity( - platform, - entity_class, - endpoint.unique_id, - [cluster_handler], - entity_metadata=entity_metadata, - ) - - _LOGGER.debug( - "'%s' platform -> '%s' using %s", - platform, - entity_class.__name__, - [cluster_handler.name], - ) - - @callback - def discover_coordinator_device_entities(self, device: ZHADevice) -> None: - """Discover entities for the coordinator device.""" - _LOGGER.debug( - "Discovering entities for coordinator device: %s-%s", - str(device.ieee), - device.name, - ) - state: State = device.gateway.application_controller.state - platforms: dict[Platform, list] = get_zha_data(device.hass).platforms - - @callback - def process_counters(counter_groups: str) -> None: - for counter_group, counters in getattr(state, counter_groups).items(): - for counter in counters: - platforms[Platform.SENSOR].append( - ( - sensor.DeviceCounterSensor, - ( - f"{slugify(str(device.ieee))}_{counter_groups}_{counter_group}_{counter}", - device, - counter_groups, - counter_group, - counter, - ), - {}, - ) - ) - _LOGGER.debug( - "'%s' platform -> '%s' using %s", - Platform.SENSOR, - sensor.DeviceCounterSensor.__name__, - f"counter groups[{counter_groups}] counter group[{counter_group}] counter[{counter}]", - ) - - process_counters("counters") - process_counters("broadcast_counters") - process_counters("device_counters") - process_counters("group_counters") - - @callback - def discover_by_device_type(self, endpoint: Endpoint) -> None: - """Process an endpoint on a zigpy device.""" - - unique_id = endpoint.unique_id - - platform: str | None = self._device_configs.get(unique_id, {}).get(CONF_TYPE) - if platform is None: - ep_profile_id = endpoint.zigpy_endpoint.profile_id - ep_device_type = endpoint.zigpy_endpoint.device_type - platform = zha_regs.DEVICE_CLASS[ep_profile_id].get(ep_device_type) - - if platform and platform in zha_const.PLATFORMS: - platform = cast(Platform, platform) - - cluster_handlers = endpoint.unclaimed_cluster_handlers() - platform_entity_class, claimed = zha_regs.ZHA_ENTITIES.get_entity( - platform, - endpoint.device.manufacturer, - endpoint.device.model, - cluster_handlers, - endpoint.device.quirk_id, - ) - if platform_entity_class is None: - return - endpoint.claim_cluster_handlers(claimed) - endpoint.async_new_entity( - platform, platform_entity_class, unique_id, claimed - ) - - @callback - def discover_by_cluster_id(self, endpoint: Endpoint) -> None: - """Process an endpoint on a zigpy device.""" - - items = zha_regs.SINGLE_INPUT_CLUSTER_DEVICE_CLASS.items() - single_input_clusters = { - cluster_class: match - for cluster_class, match in items - if not isinstance(cluster_class, int) - } - remaining_cluster_handlers = endpoint.unclaimed_cluster_handlers() - for cluster_handler in remaining_cluster_handlers: - if ( - cluster_handler.cluster.cluster_id - in zha_regs.CLUSTER_HANDLER_ONLY_CLUSTERS - ): - endpoint.claim_cluster_handlers([cluster_handler]) - continue - - platform = zha_regs.SINGLE_INPUT_CLUSTER_DEVICE_CLASS.get( - cluster_handler.cluster.cluster_id - ) - if platform is None: - for cluster_class, match in single_input_clusters.items(): - if isinstance(cluster_handler.cluster, cluster_class): - platform = match - break - - self.probe_single_cluster(platform, cluster_handler, endpoint) - - # until we can get rid of registries - self.handle_on_off_output_cluster_exception(endpoint) - - @staticmethod - def probe_single_cluster( - platform: Platform | None, - cluster_handler: ClusterHandler, - endpoint: Endpoint, - ) -> None: - """Probe specified cluster for specific component.""" - if platform is None or platform not in zha_const.PLATFORMS: - return - cluster_handler_list = [cluster_handler] - unique_id = f"{endpoint.unique_id}-{cluster_handler.cluster.cluster_id}" - - entity_class, claimed = zha_regs.ZHA_ENTITIES.get_entity( - platform, - endpoint.device.manufacturer, - endpoint.device.model, - cluster_handler_list, - endpoint.device.quirk_id, - ) - if entity_class is None: - return - endpoint.claim_cluster_handlers(claimed) - endpoint.async_new_entity(platform, entity_class, unique_id, claimed) - - def handle_on_off_output_cluster_exception(self, endpoint: Endpoint) -> None: - """Process output clusters of the endpoint.""" - - profile_id = endpoint.zigpy_endpoint.profile_id - device_type = endpoint.zigpy_endpoint.device_type - if device_type in zha_regs.REMOTE_DEVICE_TYPES.get(profile_id, []): - return - - for cluster_id, cluster in endpoint.zigpy_endpoint.out_clusters.items(): - platform = zha_regs.SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.get( - cluster.cluster_id - ) - if platform is None: - continue - - cluster_handler_classes = zha_regs.ZIGBEE_CLUSTER_HANDLER_REGISTRY.get( - cluster_id, {None: ClusterHandler} - ) - - quirk_id = ( - endpoint.device.quirk_id - if endpoint.device.quirk_id in cluster_handler_classes - else None - ) - - cluster_handler_class = cluster_handler_classes.get( - quirk_id, ClusterHandler - ) - - cluster_handler = cluster_handler_class(cluster, endpoint) - self.probe_single_cluster(platform, cluster_handler, endpoint) - - @staticmethod - @callback - def discover_multi_entities( - endpoint: Endpoint, - config_diagnostic_entities: bool = False, - ) -> None: - """Process an endpoint on and discover multiple entities.""" - - ep_profile_id = endpoint.zigpy_endpoint.profile_id - ep_device_type = endpoint.zigpy_endpoint.device_type - cmpt_by_dev_type = zha_regs.DEVICE_CLASS[ep_profile_id].get(ep_device_type) - - if config_diagnostic_entities: - cluster_handlers = list(endpoint.all_cluster_handlers.values()) - ota_handler_id = f"{endpoint.id}:0x{Ota.cluster_id:04x}" - if ota_handler_id in endpoint.client_cluster_handlers: - cluster_handlers.append( - endpoint.client_cluster_handlers[ota_handler_id] - ) - matches, claimed = zha_regs.ZHA_ENTITIES.get_config_diagnostic_entity( - endpoint.device.manufacturer, - endpoint.device.model, - cluster_handlers, - endpoint.device.quirk_id, - ) - else: - matches, claimed = zha_regs.ZHA_ENTITIES.get_multi_entity( - endpoint.device.manufacturer, - endpoint.device.model, - endpoint.unclaimed_cluster_handlers(), - endpoint.device.quirk_id, - ) - - endpoint.claim_cluster_handlers(claimed) - for platform, ent_n_handler_list in matches.items(): - for entity_and_handler in ent_n_handler_list: - _LOGGER.debug( - "'%s' platform -> '%s' using %s", - platform, - entity_and_handler.entity_class.__name__, - [ch.name for ch in entity_and_handler.claimed_cluster_handlers], - ) - for platform, ent_n_handler_list in matches.items(): - for entity_and_handler in ent_n_handler_list: - if platform == cmpt_by_dev_type: - # for well known device types, - # like thermostats we'll take only 1st class - endpoint.async_new_entity( - platform, - entity_and_handler.entity_class, - endpoint.unique_id, - entity_and_handler.claimed_cluster_handlers, - ) - break - first_ch = entity_and_handler.claimed_cluster_handlers[0] - endpoint.async_new_entity( - platform, - entity_and_handler.entity_class, - f"{endpoint.unique_id}-{first_ch.cluster.cluster_id}", - entity_and_handler.claimed_cluster_handlers, - ) - - def initialize(self, hass: HomeAssistant) -> None: - """Update device overrides config.""" - zha_config = get_zha_data(hass).yaml_config - if overrides := zha_config.get(zha_const.CONF_DEVICE_CONFIG): - self._device_configs.update(overrides) - - -class GroupProbe: - """Determine the appropriate component for a group.""" - - _hass: HomeAssistant - - def __init__(self) -> None: - """Initialize instance.""" - self._unsubs: list[Callable[[], None]] = [] - - def initialize(self, hass: HomeAssistant) -> None: - """Initialize the group probe.""" - self._hass = hass - self._unsubs.append( - async_dispatcher_connect( - hass, zha_const.SIGNAL_GROUP_ENTITY_REMOVED, self._reprobe_group - ) - ) - - def cleanup(self) -> None: - """Clean up on when ZHA shuts down.""" - for unsub in self._unsubs[:]: - unsub() - self._unsubs.remove(unsub) - - @callback - def _reprobe_group(self, group_id: int) -> None: - """Reprobe a group for entities after its members change.""" - zha_gateway = get_zha_gateway(self._hass) - if (zha_group := zha_gateway.groups.get(group_id)) is None: - return - self.discover_group_entities(zha_group) - - @callback - def discover_group_entities(self, group: ZHAGroup) -> None: - """Process a group and create any entities that are needed.""" - # only create a group entity if there are 2 or more members in a group - if len(group.members) < 2: - _LOGGER.debug( - "Group: %s:0x%04x has less than 2 members - skipping entity discovery", - group.name, - group.group_id, - ) - return - - entity_domains = GroupProbe.determine_entity_domains(self._hass, group) - - if not entity_domains: - return - - zha_data = get_zha_data(self._hass) - zha_gateway = get_zha_gateway(self._hass) - - for domain in entity_domains: - entity_class = zha_regs.ZHA_ENTITIES.get_group_entity(domain) - if entity_class is None: - continue - zha_data.platforms[domain].append( - ( - entity_class, - ( - group.get_domain_entity_ids(domain), - f"{domain}_zha_group_0x{group.group_id:04x}", - group.group_id, - zha_gateway.coordinator_zha_device, - ), - {}, - ) - ) - async_dispatcher_send(self._hass, zha_const.SIGNAL_ADD_ENTITIES) - - @staticmethod - def determine_entity_domains( - hass: HomeAssistant, group: ZHAGroup - ) -> list[Platform]: - """Determine the entity domains for this group.""" - entity_registry = er.async_get(hass) - - entity_domains: list[Platform] = [] - all_domain_occurrences: list[Platform] = [] - - for member in group.members: - if member.device.is_coordinator: - continue - entities = async_entries_for_device( - entity_registry, - member.device.device_id, - include_disabled_entities=True, - ) - all_domain_occurrences.extend( - [ - cast(Platform, entity.domain) - for entity in entities - if entity.domain in zha_regs.GROUP_ENTITY_DOMAINS - ] - ) - if not all_domain_occurrences: - return entity_domains - # get all domains we care about if there are more than 2 entities of this domain - counts = Counter(all_domain_occurrences) - entity_domains = [domain[0] for domain in counts.items() if domain[1] >= 2] - _LOGGER.debug( - "The entity domains are: %s for group: %s:0x%04x", - entity_domains, - group.name, - group.group_id, - ) - return entity_domains - - -PROBE = ProbeEndpoint() -GROUP_PROBE = GroupProbe() diff --git a/homeassistant/components/zha/core/endpoint.py b/homeassistant/components/zha/core/endpoint.py deleted file mode 100644 index 32483a3bc53..00000000000 --- a/homeassistant/components/zha/core/endpoint.py +++ /dev/null @@ -1,253 +0,0 @@ -"""Representation of a Zigbee endpoint for zha.""" - -from __future__ import annotations - -import asyncio -from collections.abc import Awaitable, Callable -import functools -import logging -from typing import TYPE_CHECKING, Any, Final - -from homeassistant.const import Platform -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.util.async_ import gather_with_limited_concurrency - -from . import const, discovery, registries -from .cluster_handlers import ClusterHandler -from .helpers import get_zha_data - -if TYPE_CHECKING: - from zigpy import Endpoint as ZigpyEndpoint - - from .cluster_handlers import ClientClusterHandler - from .device import ZHADevice - -ATTR_DEVICE_TYPE: Final[str] = "device_type" -ATTR_PROFILE_ID: Final[str] = "profile_id" -ATTR_IN_CLUSTERS: Final[str] = "input_clusters" -ATTR_OUT_CLUSTERS: Final[str] = "output_clusters" - -_LOGGER = logging.getLogger(__name__) - - -class Endpoint: - """Endpoint for a zha device.""" - - def __init__(self, zigpy_endpoint: ZigpyEndpoint, device: ZHADevice) -> None: - """Initialize instance.""" - assert zigpy_endpoint is not None - assert device is not None - self._zigpy_endpoint: ZigpyEndpoint = zigpy_endpoint - self._device: ZHADevice = device - self._all_cluster_handlers: dict[str, ClusterHandler] = {} - self._claimed_cluster_handlers: dict[str, ClusterHandler] = {} - self._client_cluster_handlers: dict[str, ClientClusterHandler] = {} - self._unique_id: str = f"{device.ieee!s}-{zigpy_endpoint.endpoint_id}" - - @property - def device(self) -> ZHADevice: - """Return the device this endpoint belongs to.""" - return self._device - - @property - def all_cluster_handlers(self) -> dict[str, ClusterHandler]: - """All server cluster handlers of an endpoint.""" - return self._all_cluster_handlers - - @property - def claimed_cluster_handlers(self) -> dict[str, ClusterHandler]: - """Cluster handlers in use.""" - return self._claimed_cluster_handlers - - @property - def client_cluster_handlers(self) -> dict[str, ClientClusterHandler]: - """Return a dict of client cluster handlers.""" - return self._client_cluster_handlers - - @property - def zigpy_endpoint(self) -> ZigpyEndpoint: - """Return endpoint of zigpy device.""" - return self._zigpy_endpoint - - @property - def id(self) -> int: - """Return endpoint id.""" - return self._zigpy_endpoint.endpoint_id - - @property - def unique_id(self) -> str: - """Return the unique id for this endpoint.""" - return self._unique_id - - @property - def zigbee_signature(self) -> tuple[int, dict[str, Any]]: - """Get the zigbee signature for the endpoint this pool represents.""" - return ( - self.id, - { - ATTR_PROFILE_ID: f"0x{self._zigpy_endpoint.profile_id:04x}" - if self._zigpy_endpoint.profile_id is not None - else "", - ATTR_DEVICE_TYPE: f"0x{self._zigpy_endpoint.device_type:04x}" - if self._zigpy_endpoint.device_type is not None - else "", - ATTR_IN_CLUSTERS: [ - f"0x{cluster_id:04x}" - for cluster_id in sorted(self._zigpy_endpoint.in_clusters) - ], - ATTR_OUT_CLUSTERS: [ - f"0x{cluster_id:04x}" - for cluster_id in sorted(self._zigpy_endpoint.out_clusters) - ], - }, - ) - - @classmethod - def new(cls, zigpy_endpoint: ZigpyEndpoint, device: ZHADevice) -> Endpoint: - """Create new endpoint and populate cluster handlers.""" - endpoint = cls(zigpy_endpoint, device) - endpoint.add_all_cluster_handlers() - endpoint.add_client_cluster_handlers() - if not device.is_coordinator: - discovery.PROBE.discover_entities(endpoint) - return endpoint - - def add_all_cluster_handlers(self) -> None: - """Create and add cluster handlers for all input clusters.""" - for cluster_id, cluster in self.zigpy_endpoint.in_clusters.items(): - cluster_handler_classes = registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.get( - cluster_id, {None: ClusterHandler} - ) - quirk_id = ( - self.device.quirk_id - if self.device.quirk_id in cluster_handler_classes - else None - ) - cluster_handler_class = cluster_handler_classes.get( - quirk_id, ClusterHandler - ) - - # Allow cluster handler to filter out bad matches - if not cluster_handler_class.matches(cluster, self): - cluster_handler_class = ClusterHandler - - _LOGGER.debug( - "Creating cluster handler for cluster id: %s class: %s", - cluster_id, - cluster_handler_class, - ) - - try: - cluster_handler = cluster_handler_class(cluster, self) - except KeyError as err: - _LOGGER.warning( - "Cluster handler %s for cluster %s on endpoint %s is invalid: %s", - cluster_handler_class, - cluster, - self, - err, - ) - continue - - if cluster_handler.name == const.CLUSTER_HANDLER_POWER_CONFIGURATION: - self._device.power_configuration_ch = cluster_handler - elif cluster_handler.name == const.CLUSTER_HANDLER_IDENTIFY: - self._device.identify_ch = cluster_handler - elif cluster_handler.name == const.CLUSTER_HANDLER_BASIC: - self._device.basic_ch = cluster_handler - self._all_cluster_handlers[cluster_handler.id] = cluster_handler - - def add_client_cluster_handlers(self) -> None: - """Create client cluster handlers for all output clusters if in the registry.""" - for ( - cluster_id, - cluster_handler_class, - ) in registries.CLIENT_CLUSTER_HANDLER_REGISTRY.items(): - cluster = self.zigpy_endpoint.out_clusters.get(cluster_id) - if cluster is not None: - cluster_handler = cluster_handler_class(cluster, self) - self.client_cluster_handlers[cluster_handler.id] = cluster_handler - - async def async_initialize(self, from_cache: bool = False) -> None: - """Initialize claimed cluster handlers.""" - await self._execute_handler_tasks( - "async_initialize", from_cache, max_concurrency=1 - ) - - async def async_configure(self) -> None: - """Configure claimed cluster handlers.""" - await self._execute_handler_tasks("async_configure") - - async def _execute_handler_tasks( - self, func_name: str, *args: Any, max_concurrency: int | None = None - ) -> None: - """Add a throttled cluster handler task and swallow exceptions.""" - cluster_handlers = [ - *self.claimed_cluster_handlers.values(), - *self.client_cluster_handlers.values(), - ] - tasks = [getattr(ch, func_name)(*args) for ch in cluster_handlers] - - gather: Callable[..., Awaitable] - - if max_concurrency is None: - gather = asyncio.gather - else: - gather = functools.partial(gather_with_limited_concurrency, max_concurrency) - - results = await gather(*tasks, return_exceptions=True) - for cluster_handler, outcome in zip(cluster_handlers, results, strict=False): - if isinstance(outcome, Exception): - cluster_handler.debug( - "'%s' stage failed: %s", func_name, str(outcome), exc_info=outcome - ) - else: - cluster_handler.debug("'%s' stage succeeded", func_name) - - def async_new_entity( - self, - platform: Platform, - entity_class: type, - unique_id: str, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> None: - """Create a new entity.""" - from .device import DeviceStatus # pylint: disable=import-outside-toplevel - - if self.device.status == DeviceStatus.INITIALIZED: - return - - zha_data = get_zha_data(self.device.hass) - zha_data.platforms[platform].append( - (entity_class, (unique_id, self.device, cluster_handlers), kwargs or {}) - ) - - @callback - def async_send_signal(self, signal: str, *args: Any) -> None: - """Send a signal through hass dispatcher.""" - async_dispatcher_send(self.device.hass, signal, *args) - - def send_event(self, signal: dict[str, Any]) -> None: - """Broadcast an event from this endpoint.""" - self.device.zha_send_event( - { - const.ATTR_UNIQUE_ID: self.unique_id, - const.ATTR_ENDPOINT_ID: self.id, - **signal, - } - ) - - def claim_cluster_handlers(self, cluster_handlers: list[ClusterHandler]) -> None: - """Claim cluster handlers.""" - self.claimed_cluster_handlers.update({ch.id: ch for ch in cluster_handlers}) - - def unclaimed_cluster_handlers(self) -> list[ClusterHandler]: - """Return a list of available (unclaimed) cluster handlers.""" - claimed = set(self.claimed_cluster_handlers) - available = set(self.all_cluster_handlers) - return [ - self.all_cluster_handlers[cluster_id] - for cluster_id in (available - claimed) - ] diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py deleted file mode 100644 index 8b8826e2648..00000000000 --- a/homeassistant/components/zha/core/gateway.py +++ /dev/null @@ -1,882 +0,0 @@ -"""Virtual gateway for Zigbee Home Automation.""" - -from __future__ import annotations - -import asyncio -import collections -from collections.abc import Callable -from contextlib import suppress -from datetime import timedelta -from enum import Enum -import itertools -import logging -import re -import time -from typing import TYPE_CHECKING, Any, NamedTuple, Self, cast - -from zigpy.application import ControllerApplication -from zigpy.config import ( - CONF_DATABASE, - CONF_DEVICE, - CONF_DEVICE_PATH, - CONF_NWK, - CONF_NWK_CHANNEL, - CONF_NWK_VALIDATE_SETTINGS, -) -import zigpy.device -import zigpy.endpoint -import zigpy.group -from zigpy.state import State -from zigpy.types.named import EUI64 - -from homeassistant import __path__ as HOMEASSISTANT_PATH -from homeassistant.components.system_log import LogEntry -from homeassistant.config_entries import ConfigEntry -from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import device_registry as dr, entity_registry as er -from homeassistant.helpers.device_registry import DeviceInfo -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.typing import ConfigType -from homeassistant.util.async_ import gather_with_limited_concurrency - -from . import discovery -from .const import ( - ATTR_IEEE, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NWK, - ATTR_SIGNATURE, - ATTR_TYPE, - CONF_RADIO_TYPE, - CONF_USE_THREAD, - CONF_ZIGPY, - DATA_ZHA, - DEBUG_COMP_BELLOWS, - DEBUG_COMP_ZHA, - DEBUG_COMP_ZIGPY, - DEBUG_COMP_ZIGPY_DECONZ, - DEBUG_COMP_ZIGPY_XBEE, - DEBUG_COMP_ZIGPY_ZIGATE, - DEBUG_COMP_ZIGPY_ZNP, - DEBUG_LEVEL_CURRENT, - DEBUG_LEVEL_ORIGINAL, - DEBUG_LEVELS, - DEBUG_RELAY_LOGGERS, - DEFAULT_DATABASE_NAME, - DEVICE_PAIRING_STATUS, - DOMAIN, - SIGNAL_ADD_ENTITIES, - SIGNAL_GROUP_MEMBERSHIP_CHANGE, - SIGNAL_REMOVE, - UNKNOWN_MANUFACTURER, - UNKNOWN_MODEL, - ZHA_GW_MSG, - ZHA_GW_MSG_DEVICE_FULL_INIT, - ZHA_GW_MSG_DEVICE_INFO, - ZHA_GW_MSG_DEVICE_JOINED, - ZHA_GW_MSG_DEVICE_REMOVED, - ZHA_GW_MSG_GROUP_ADDED, - ZHA_GW_MSG_GROUP_INFO, - ZHA_GW_MSG_GROUP_MEMBER_ADDED, - ZHA_GW_MSG_GROUP_MEMBER_REMOVED, - ZHA_GW_MSG_GROUP_REMOVED, - ZHA_GW_MSG_LOG_ENTRY, - ZHA_GW_MSG_LOG_OUTPUT, - ZHA_GW_MSG_RAW_INIT, - RadioType, -) -from .device import DeviceStatus, ZHADevice -from .group import GroupMember, ZHAGroup -from .helpers import get_zha_data -from .registries import GROUP_ENTITY_DOMAINS - -if TYPE_CHECKING: - from logging import Filter, LogRecord - - from ..entity import ZhaEntity - from .cluster_handlers import ClusterHandler - - type _LogFilterType = Filter | Callable[[LogRecord], bool] - -_LOGGER = logging.getLogger(__name__) - - -class EntityReference(NamedTuple): - """Describes an entity reference.""" - - reference_id: str - zha_device: ZHADevice - cluster_handlers: dict[str, ClusterHandler] - device_info: DeviceInfo - remove_future: asyncio.Future[Any] - - -class DevicePairingStatus(Enum): - """Status of a device.""" - - PAIRED = 1 - INTERVIEW_COMPLETE = 2 - CONFIGURED = 3 - INITIALIZED = 4 - - -class ZHAGateway: - """Gateway that handles events that happen on the ZHA Zigbee network.""" - - def __init__( - self, hass: HomeAssistant, config: ConfigType, config_entry: ConfigEntry - ) -> None: - """Initialize the gateway.""" - self.hass = hass - self._config = config - self._devices: dict[EUI64, ZHADevice] = {} - self._groups: dict[int, ZHAGroup] = {} - self.application_controller: ControllerApplication = None - self.coordinator_zha_device: ZHADevice = None # type: ignore[assignment] - self._device_registry: collections.defaultdict[EUI64, list[EntityReference]] = ( - collections.defaultdict(list) - ) - self._log_levels: dict[str, dict[str, int]] = { - DEBUG_LEVEL_ORIGINAL: async_capture_log_levels(), - DEBUG_LEVEL_CURRENT: async_capture_log_levels(), - } - self.debug_enabled = False - self._log_relay_handler = LogRelayHandler(hass, self) - self.config_entry = config_entry - self._unsubs: list[Callable[[], None]] = [] - - self.shutting_down = False - self._reload_task: asyncio.Task | None = None - - def get_application_controller_data(self) -> tuple[ControllerApplication, dict]: - """Get an uninitialized instance of a zigpy `ControllerApplication`.""" - radio_type = RadioType[self.config_entry.data[CONF_RADIO_TYPE]] - - app_config = self._config.get(CONF_ZIGPY, {}) - database = self._config.get( - CONF_DATABASE, - self.hass.config.path(DEFAULT_DATABASE_NAME), - ) - app_config[CONF_DATABASE] = database - app_config[CONF_DEVICE] = self.config_entry.data[CONF_DEVICE] - - if CONF_NWK_VALIDATE_SETTINGS not in app_config: - app_config[CONF_NWK_VALIDATE_SETTINGS] = True - - # The bellows UART thread sometimes propagates a cancellation into the main Core - # event loop, when a connection to a TCP coordinator fails in a specific way - if ( - CONF_USE_THREAD not in app_config - and radio_type is RadioType.ezsp - and app_config[CONF_DEVICE][CONF_DEVICE_PATH].startswith("socket://") - ): - app_config[CONF_USE_THREAD] = False - - # Local import to avoid circular dependencies - # pylint: disable-next=import-outside-toplevel - from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( - is_multiprotocol_url, - ) - - # Until we have a way to coordinate channels with the Thread half of multi-PAN, - # stick to the old zigpy default of channel 15 instead of dynamically scanning - if ( - is_multiprotocol_url(app_config[CONF_DEVICE][CONF_DEVICE_PATH]) - and app_config.get(CONF_NWK, {}).get(CONF_NWK_CHANNEL) is None - ): - app_config.setdefault(CONF_NWK, {})[CONF_NWK_CHANNEL] = 15 - - return radio_type.controller, radio_type.controller.SCHEMA(app_config) - - @classmethod - async def async_from_config( - cls, hass: HomeAssistant, config: ConfigType, config_entry: ConfigEntry - ) -> Self: - """Create an instance of a gateway from config objects.""" - instance = cls(hass, config, config_entry) - await instance.async_initialize() - return instance - - async def async_initialize(self) -> None: - """Initialize controller and connect radio.""" - discovery.PROBE.initialize(self.hass) - discovery.GROUP_PROBE.initialize(self.hass) - - self.shutting_down = False - - app_controller_cls, app_config = self.get_application_controller_data() - app = await app_controller_cls.new( - config=app_config, - auto_form=False, - start_radio=False, - ) - - try: - await app.startup(auto_form=True) - except Exception: - # Explicitly shut down the controller application on failure - await app.shutdown() - raise - - self.application_controller = app - - zha_data = get_zha_data(self.hass) - zha_data.gateway = self - - self.coordinator_zha_device = self._async_get_or_create_device( - self._find_coordinator_device() - ) - - self.async_load_devices() - self.async_load_groups() - - self.application_controller.add_listener(self) - self.application_controller.groups.add_listener(self) - - def connection_lost(self, exc: Exception) -> None: - """Handle connection lost event.""" - _LOGGER.debug("Connection to the radio was lost: %r", exc) - - if self.shutting_down: - return - - # Ensure we do not queue up multiple resets - if self._reload_task is not None: - _LOGGER.debug("Ignoring reset, one is already running") - return - - self._reload_task = self.hass.async_create_task( - self.hass.config_entries.async_reload(self.config_entry.entry_id) - ) - - def _find_coordinator_device(self) -> zigpy.device.Device: - zigpy_coordinator = self.application_controller.get_device(nwk=0x0000) - - if last_backup := self.application_controller.backups.most_recent_backup(): - with suppress(KeyError): - zigpy_coordinator = self.application_controller.get_device( - ieee=last_backup.node_info.ieee - ) - - return zigpy_coordinator - - @callback - def async_load_devices(self) -> None: - """Restore ZHA devices from zigpy application state.""" - - for zigpy_device in self.application_controller.devices.values(): - zha_device = self._async_get_or_create_device(zigpy_device) - delta_msg = "not known" - if zha_device.last_seen is not None: - delta = round(time.time() - zha_device.last_seen) - delta_msg = f"{timedelta(seconds=delta)!s} ago" - _LOGGER.debug( - ( - "[%s](%s) restored as '%s', last seen: %s," - " consider_unavailable_time: %s seconds" - ), - zha_device.nwk, - zha_device.name, - "available" if zha_device.available else "unavailable", - delta_msg, - zha_device.consider_unavailable_time, - ) - - @callback - def async_load_groups(self) -> None: - """Initialize ZHA groups.""" - - for group_id in self.application_controller.groups: - group = self.application_controller.groups[group_id] - zha_group = self._async_get_or_create_group(group) - # we can do this here because the entities are in the - # entity registry tied to the devices - discovery.GROUP_PROBE.discover_group_entities(zha_group) - - @property - def radio_concurrency(self) -> int: - """Maximum configured radio concurrency.""" - return self.application_controller._concurrent_requests_semaphore.max_value # noqa: SLF001 - - async def async_fetch_updated_state_mains(self) -> None: - """Fetch updated state for mains powered devices.""" - _LOGGER.debug("Fetching current state for mains powered devices") - - now = time.time() - - # Only delay startup to poll mains-powered devices that are online - online_devices = [ - dev - for dev in self.devices.values() - if dev.is_mains_powered - and dev.last_seen is not None - and (now - dev.last_seen) < dev.consider_unavailable_time - ] - - # Prioritize devices that have recently been contacted - online_devices.sort(key=lambda dev: cast(float, dev.last_seen), reverse=True) - - # Make sure that we always leave slots for non-startup requests - max_poll_concurrency = max(1, self.radio_concurrency - 4) - - await gather_with_limited_concurrency( - max_poll_concurrency, - *(dev.async_initialize(from_cache=False) for dev in online_devices), - ) - - _LOGGER.debug("completed fetching current state for mains powered devices") - - async def async_initialize_devices_and_entities(self) -> None: - """Initialize devices and load entities.""" - - _LOGGER.debug("Initializing all devices from Zigpy cache") - await asyncio.gather( - *(dev.async_initialize(from_cache=True) for dev in self.devices.values()) - ) - - async def fetch_updated_state() -> None: - """Fetch updated state for mains powered devices.""" - await self.async_fetch_updated_state_mains() - _LOGGER.debug("Allowing polled requests") - self.hass.data[DATA_ZHA].allow_polling = True - - # background the fetching of state for mains powered devices - self.config_entry.async_create_background_task( - self.hass, fetch_updated_state(), "zha.gateway-fetch_updated_state" - ) - - def device_joined(self, device: zigpy.device.Device) -> None: - """Handle device joined. - - At this point, no information about the device is known other than its - address - """ - async_dispatcher_send( - self.hass, - ZHA_GW_MSG, - { - ATTR_TYPE: ZHA_GW_MSG_DEVICE_JOINED, - ZHA_GW_MSG_DEVICE_INFO: { - ATTR_NWK: device.nwk, - ATTR_IEEE: str(device.ieee), - DEVICE_PAIRING_STATUS: DevicePairingStatus.PAIRED.name, - }, - }, - ) - - def raw_device_initialized(self, device: zigpy.device.Device) -> None: - """Handle a device initialization without quirks loaded.""" - manuf = device.manufacturer - async_dispatcher_send( - self.hass, - ZHA_GW_MSG, - { - ATTR_TYPE: ZHA_GW_MSG_RAW_INIT, - ZHA_GW_MSG_DEVICE_INFO: { - ATTR_NWK: device.nwk, - ATTR_IEEE: str(device.ieee), - DEVICE_PAIRING_STATUS: DevicePairingStatus.INTERVIEW_COMPLETE.name, - ATTR_MODEL: device.model if device.model else UNKNOWN_MODEL, - ATTR_MANUFACTURER: manuf if manuf else UNKNOWN_MANUFACTURER, - ATTR_SIGNATURE: device.get_signature(), - }, - }, - ) - - def device_initialized(self, device: zigpy.device.Device) -> None: - """Handle device joined and basic information discovered.""" - self.hass.async_create_task(self.async_device_initialized(device)) - - def device_left(self, device: zigpy.device.Device) -> None: - """Handle device leaving the network.""" - self.async_update_device(device, False) - - def group_member_removed( - self, zigpy_group: zigpy.group.Group, endpoint: zigpy.endpoint.Endpoint - ) -> None: - """Handle zigpy group member removed event.""" - # need to handle endpoint correctly on groups - zha_group = self._async_get_or_create_group(zigpy_group) - zha_group.info("group_member_removed - endpoint: %s", endpoint) - self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_MEMBER_REMOVED) - async_dispatcher_send( - self.hass, f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_0x{zigpy_group.group_id:04x}" - ) - - def group_member_added( - self, zigpy_group: zigpy.group.Group, endpoint: zigpy.endpoint.Endpoint - ) -> None: - """Handle zigpy group member added event.""" - # need to handle endpoint correctly on groups - zha_group = self._async_get_or_create_group(zigpy_group) - zha_group.info("group_member_added - endpoint: %s", endpoint) - self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_MEMBER_ADDED) - async_dispatcher_send( - self.hass, f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_0x{zigpy_group.group_id:04x}" - ) - if len(zha_group.members) == 2: - # we need to do this because there wasn't already - # a group entity to remove and re-add - discovery.GROUP_PROBE.discover_group_entities(zha_group) - - def group_added(self, zigpy_group: zigpy.group.Group) -> None: - """Handle zigpy group added event.""" - zha_group = self._async_get_or_create_group(zigpy_group) - zha_group.info("group_added") - # need to dispatch for entity creation here - self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_ADDED) - - def group_removed(self, zigpy_group: zigpy.group.Group) -> None: - """Handle zigpy group removed event.""" - self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_REMOVED) - zha_group = self._groups.pop(zigpy_group.group_id) - zha_group.info("group_removed") - self._cleanup_group_entity_registry_entries(zigpy_group) - - def _send_group_gateway_message( - self, zigpy_group: zigpy.group.Group, gateway_message_type: str - ) -> None: - """Send the gateway event for a zigpy group event.""" - zha_group = self._groups.get(zigpy_group.group_id) - if zha_group is not None: - async_dispatcher_send( - self.hass, - ZHA_GW_MSG, - { - ATTR_TYPE: gateway_message_type, - ZHA_GW_MSG_GROUP_INFO: zha_group.group_info, - }, - ) - - async def _async_remove_device( - self, device: ZHADevice, entity_refs: list[EntityReference] | None - ) -> None: - if entity_refs is not None: - remove_tasks: list[asyncio.Future[Any]] = [ - entity_ref.remove_future for entity_ref in entity_refs - ] - if remove_tasks: - await asyncio.wait(remove_tasks) - - device_registry = dr.async_get(self.hass) - reg_device = device_registry.async_get(device.device_id) - if reg_device is not None: - device_registry.async_remove_device(reg_device.id) - - def device_removed(self, device: zigpy.device.Device) -> None: - """Handle device being removed from the network.""" - zha_device = self._devices.pop(device.ieee, None) - entity_refs = self._device_registry.pop(device.ieee, None) - if zha_device is not None: - device_info = zha_device.zha_device_info - zha_device.async_cleanup_handles() - async_dispatcher_send(self.hass, f"{SIGNAL_REMOVE}_{zha_device.ieee!s}") - self.hass.async_create_task( - self._async_remove_device(zha_device, entity_refs), - "ZHAGateway._async_remove_device", - ) - if device_info is not None: - async_dispatcher_send( - self.hass, - ZHA_GW_MSG, - { - ATTR_TYPE: ZHA_GW_MSG_DEVICE_REMOVED, - ZHA_GW_MSG_DEVICE_INFO: device_info, - }, - ) - - def get_device(self, ieee: EUI64) -> ZHADevice | None: - """Return ZHADevice for given ieee.""" - return self._devices.get(ieee) - - def get_group(self, group_id: int) -> ZHAGroup | None: - """Return Group for given group id.""" - return self.groups.get(group_id) - - @callback - def async_get_group_by_name(self, group_name: str) -> ZHAGroup | None: - """Get ZHA group by name.""" - for group in self.groups.values(): - if group.name == group_name: - return group - return None - - def get_entity_reference(self, entity_id: str) -> EntityReference | None: - """Return entity reference for given entity_id if found.""" - for entity_reference in itertools.chain.from_iterable( - self.device_registry.values() - ): - if entity_id == entity_reference.reference_id: - return entity_reference - return None - - def remove_entity_reference(self, entity: ZhaEntity) -> None: - """Remove entity reference for given entity_id if found.""" - if entity.zha_device.ieee in self.device_registry: - entity_refs = self.device_registry.get(entity.zha_device.ieee) - self.device_registry[entity.zha_device.ieee] = [ - e - for e in entity_refs # type: ignore[union-attr] - if e.reference_id != entity.entity_id - ] - - def _cleanup_group_entity_registry_entries( - self, zigpy_group: zigpy.group.Group - ) -> None: - """Remove entity registry entries for group entities when the groups are removed from HA.""" - # first we collect the potential unique ids for entities that could be created from this group - possible_entity_unique_ids = [ - f"{domain}_zha_group_0x{zigpy_group.group_id:04x}" - for domain in GROUP_ENTITY_DOMAINS - ] - - # then we get all group entity entries tied to the coordinator - entity_registry = er.async_get(self.hass) - assert self.coordinator_zha_device - all_group_entity_entries = er.async_entries_for_device( - entity_registry, - self.coordinator_zha_device.device_id, - include_disabled_entities=True, - ) - - # then we get the entity entries for this specific group - # by getting the entries that match - entries_to_remove = [ - entry - for entry in all_group_entity_entries - if entry.unique_id in possible_entity_unique_ids - ] - - # then we remove the entries from the entity registry - for entry in entries_to_remove: - _LOGGER.debug( - "cleaning up entity registry entry for entity: %s", entry.entity_id - ) - entity_registry.async_remove(entry.entity_id) - - @property - def state(self) -> State: - """Return the active coordinator's network state.""" - return self.application_controller.state - - @property - def devices(self) -> dict[EUI64, ZHADevice]: - """Return devices.""" - return self._devices - - @property - def groups(self) -> dict[int, ZHAGroup]: - """Return groups.""" - return self._groups - - @property - def device_registry(self) -> collections.defaultdict[EUI64, list[EntityReference]]: - """Return entities by ieee.""" - return self._device_registry - - def register_entity_reference( - self, - ieee: EUI64, - reference_id: str, - zha_device: ZHADevice, - cluster_handlers: dict[str, ClusterHandler], - device_info: DeviceInfo, - remove_future: asyncio.Future[Any], - ): - """Record the creation of a hass entity associated with ieee.""" - self._device_registry[ieee].append( - EntityReference( - reference_id=reference_id, - zha_device=zha_device, - cluster_handlers=cluster_handlers, - device_info=device_info, - remove_future=remove_future, - ) - ) - - @callback - def async_enable_debug_mode(self, filterer: _LogFilterType | None = None) -> None: - """Enable debug mode for ZHA.""" - self._log_levels[DEBUG_LEVEL_ORIGINAL] = async_capture_log_levels() - async_set_logger_levels(DEBUG_LEVELS) - self._log_levels[DEBUG_LEVEL_CURRENT] = async_capture_log_levels() - - if filterer: - self._log_relay_handler.addFilter(filterer) - - for logger_name in DEBUG_RELAY_LOGGERS: - logging.getLogger(logger_name).addHandler(self._log_relay_handler) - - self.debug_enabled = True - - @callback - def async_disable_debug_mode(self, filterer: _LogFilterType | None = None) -> None: - """Disable debug mode for ZHA.""" - async_set_logger_levels(self._log_levels[DEBUG_LEVEL_ORIGINAL]) - self._log_levels[DEBUG_LEVEL_CURRENT] = async_capture_log_levels() - for logger_name in DEBUG_RELAY_LOGGERS: - logging.getLogger(logger_name).removeHandler(self._log_relay_handler) - if filterer: - self._log_relay_handler.removeFilter(filterer) - self.debug_enabled = False - - @callback - def _async_get_or_create_device( - self, zigpy_device: zigpy.device.Device - ) -> ZHADevice: - """Get or create a ZHA device.""" - if (zha_device := self._devices.get(zigpy_device.ieee)) is None: - zha_device = ZHADevice.new(self.hass, zigpy_device, self) - self._devices[zigpy_device.ieee] = zha_device - - device_registry = dr.async_get(self.hass) - device_registry_device = device_registry.async_get_or_create( - config_entry_id=self.config_entry.entry_id, - connections={(dr.CONNECTION_ZIGBEE, str(zha_device.ieee))}, - identifiers={(DOMAIN, str(zha_device.ieee))}, - name=zha_device.name, - manufacturer=zha_device.manufacturer, - model=zha_device.model, - ) - zha_device.set_device_id(device_registry_device.id) - return zha_device - - @callback - def _async_get_or_create_group(self, zigpy_group: zigpy.group.Group) -> ZHAGroup: - """Get or create a ZHA group.""" - zha_group = self._groups.get(zigpy_group.group_id) - if zha_group is None: - zha_group = ZHAGroup(self.hass, self, zigpy_group) - self._groups[zigpy_group.group_id] = zha_group - return zha_group - - @callback - def async_update_device( - self, sender: zigpy.device.Device, available: bool = True - ) -> None: - """Update device that has just become available.""" - if sender.ieee in self.devices: - device = self.devices[sender.ieee] - # avoid a race condition during new joins - if device.status is DeviceStatus.INITIALIZED: - device.update_available(available) - - async def async_device_initialized(self, device: zigpy.device.Device) -> None: - """Handle device joined and basic information discovered (async).""" - zha_device = self._async_get_or_create_device(device) - _LOGGER.debug( - "device - %s:%s entering async_device_initialized - is_new_join: %s", - device.nwk, - device.ieee, - zha_device.status is not DeviceStatus.INITIALIZED, - ) - - if zha_device.status is DeviceStatus.INITIALIZED: - # ZHA already has an initialized device so either the device was assigned a - # new nwk or device was physically reset and added again without being removed - _LOGGER.debug( - "device - %s:%s has been reset and re-added or its nwk address changed", - device.nwk, - device.ieee, - ) - await self._async_device_rejoined(zha_device) - else: - _LOGGER.debug( - "device - %s:%s has joined the ZHA zigbee network", - device.nwk, - device.ieee, - ) - await self._async_device_joined(zha_device) - - device_info = zha_device.zha_device_info - device_info[DEVICE_PAIRING_STATUS] = DevicePairingStatus.INITIALIZED.name - async_dispatcher_send( - self.hass, - ZHA_GW_MSG, - { - ATTR_TYPE: ZHA_GW_MSG_DEVICE_FULL_INIT, - ZHA_GW_MSG_DEVICE_INFO: device_info, - }, - ) - - async def _async_device_joined(self, zha_device: ZHADevice) -> None: - zha_device.available = True - device_info = zha_device.device_info - await zha_device.async_configure() - device_info[DEVICE_PAIRING_STATUS] = DevicePairingStatus.CONFIGURED.name - async_dispatcher_send( - self.hass, - ZHA_GW_MSG, - { - ATTR_TYPE: ZHA_GW_MSG_DEVICE_FULL_INIT, - ZHA_GW_MSG_DEVICE_INFO: device_info, - }, - ) - await zha_device.async_initialize(from_cache=False) - async_dispatcher_send(self.hass, SIGNAL_ADD_ENTITIES) - - async def _async_device_rejoined(self, zha_device: ZHADevice) -> None: - _LOGGER.debug( - "skipping discovery for previously discovered device - %s:%s", - zha_device.nwk, - zha_device.ieee, - ) - # we don't have to do this on a nwk swap - # but we don't have a way to tell currently - await zha_device.async_configure() - device_info = zha_device.device_info - device_info[DEVICE_PAIRING_STATUS] = DevicePairingStatus.CONFIGURED.name - async_dispatcher_send( - self.hass, - ZHA_GW_MSG, - { - ATTR_TYPE: ZHA_GW_MSG_DEVICE_FULL_INIT, - ZHA_GW_MSG_DEVICE_INFO: device_info, - }, - ) - # force async_initialize() to fire so don't explicitly call it - zha_device.available = False - zha_device.update_available(True) - - async def async_create_zigpy_group( - self, - name: str, - members: list[GroupMember] | None, - group_id: int | None = None, - ) -> ZHAGroup | None: - """Create a new Zigpy Zigbee group.""" - - # we start with two to fill any gaps from a user removing existing groups - - if group_id is None: - group_id = 2 - while group_id in self.groups: - group_id += 1 - - # guard against group already existing - if self.async_get_group_by_name(name) is None: - self.application_controller.groups.add_group(group_id, name) - if members is not None: - tasks = [] - for member in members: - _LOGGER.debug( - ( - "Adding member with IEEE: %s and endpoint ID: %s to group:" - " %s:0x%04x" - ), - member.ieee, - member.endpoint_id, - name, - group_id, - ) - tasks.append( - self.devices[member.ieee].async_add_endpoint_to_group( - member.endpoint_id, group_id - ) - ) - await asyncio.gather(*tasks) - return self.groups.get(group_id) - - async def async_remove_zigpy_group(self, group_id: int) -> None: - """Remove a Zigbee group from Zigpy.""" - if not (group := self.groups.get(group_id)): - _LOGGER.debug("Group: 0x%04x could not be found", group_id) - return - if group.members: - tasks = [member.async_remove_from_group() for member in group.members] - if tasks: - await asyncio.gather(*tasks) - self.application_controller.groups.pop(group_id) - - async def shutdown(self) -> None: - """Stop ZHA Controller Application.""" - if self.shutting_down: - _LOGGER.debug("Ignoring duplicate shutdown event") - return - - _LOGGER.debug("Shutting down ZHA ControllerApplication") - self.shutting_down = True - - for unsubscribe in self._unsubs: - unsubscribe() - for device in self.devices.values(): - device.async_cleanup_handles() - await self.application_controller.shutdown() - - def handle_message( - self, - sender: zigpy.device.Device, - profile: int, - cluster: int, - src_ep: int, - dst_ep: int, - message: bytes, - ) -> None: - """Handle message from a device Event handler.""" - if sender.ieee in self.devices and not self.devices[sender.ieee].available: - self.async_update_device(sender, available=True) - - -@callback -def async_capture_log_levels() -> dict[str, int]: - """Capture current logger levels for ZHA.""" - return { - DEBUG_COMP_BELLOWS: logging.getLogger(DEBUG_COMP_BELLOWS).getEffectiveLevel(), - DEBUG_COMP_ZHA: logging.getLogger(DEBUG_COMP_ZHA).getEffectiveLevel(), - DEBUG_COMP_ZIGPY: logging.getLogger(DEBUG_COMP_ZIGPY).getEffectiveLevel(), - DEBUG_COMP_ZIGPY_ZNP: logging.getLogger( - DEBUG_COMP_ZIGPY_ZNP - ).getEffectiveLevel(), - DEBUG_COMP_ZIGPY_DECONZ: logging.getLogger( - DEBUG_COMP_ZIGPY_DECONZ - ).getEffectiveLevel(), - DEBUG_COMP_ZIGPY_XBEE: logging.getLogger( - DEBUG_COMP_ZIGPY_XBEE - ).getEffectiveLevel(), - DEBUG_COMP_ZIGPY_ZIGATE: logging.getLogger( - DEBUG_COMP_ZIGPY_ZIGATE - ).getEffectiveLevel(), - } - - -@callback -def async_set_logger_levels(levels: dict[str, int]) -> None: - """Set logger levels for ZHA.""" - logging.getLogger(DEBUG_COMP_BELLOWS).setLevel(levels[DEBUG_COMP_BELLOWS]) - logging.getLogger(DEBUG_COMP_ZHA).setLevel(levels[DEBUG_COMP_ZHA]) - logging.getLogger(DEBUG_COMP_ZIGPY).setLevel(levels[DEBUG_COMP_ZIGPY]) - logging.getLogger(DEBUG_COMP_ZIGPY_ZNP).setLevel(levels[DEBUG_COMP_ZIGPY_ZNP]) - logging.getLogger(DEBUG_COMP_ZIGPY_DECONZ).setLevel(levels[DEBUG_COMP_ZIGPY_DECONZ]) - logging.getLogger(DEBUG_COMP_ZIGPY_XBEE).setLevel(levels[DEBUG_COMP_ZIGPY_XBEE]) - logging.getLogger(DEBUG_COMP_ZIGPY_ZIGATE).setLevel(levels[DEBUG_COMP_ZIGPY_ZIGATE]) - - -class LogRelayHandler(logging.Handler): - """Log handler for error messages.""" - - def __init__(self, hass: HomeAssistant, gateway: ZHAGateway) -> None: - """Initialize a new LogErrorHandler.""" - super().__init__() - self.hass = hass - self.gateway = gateway - hass_path: str = HOMEASSISTANT_PATH[0] - config_dir = self.hass.config.config_dir - self.paths_re = re.compile( - r"(?:{})/(.*)".format( - "|".join([re.escape(x) for x in (hass_path, config_dir)]) - ) - ) - - def emit(self, record: LogRecord) -> None: - """Relay log message via dispatcher.""" - entry = LogEntry( - record, - self.paths_re, - formatter=self.formatter, - figure_out_source=record.levelno >= logging.WARNING, - ) - async_dispatcher_send( - self.hass, - ZHA_GW_MSG, - {ATTR_TYPE: ZHA_GW_MSG_LOG_OUTPUT, ZHA_GW_MSG_LOG_ENTRY: entry.to_dict()}, - ) diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py deleted file mode 100644 index a6156ab63b7..00000000000 --- a/homeassistant/components/zha/core/group.py +++ /dev/null @@ -1,246 +0,0 @@ -"""Group for Zigbee Home Automation.""" - -from __future__ import annotations - -import asyncio -import logging -from typing import TYPE_CHECKING, Any, NamedTuple - -import zigpy.endpoint -import zigpy.exceptions -import zigpy.group -from zigpy.types.named import EUI64 - -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity_registry import async_entries_for_device - -from .helpers import LogMixin - -if TYPE_CHECKING: - from .device import ZHADevice - from .gateway import ZHAGateway - -_LOGGER = logging.getLogger(__name__) - - -class GroupMember(NamedTuple): - """Describes a group member.""" - - ieee: EUI64 - endpoint_id: int - - -class GroupEntityReference(NamedTuple): - """Reference to a group entity.""" - - name: str | None - original_name: str | None - entity_id: int - - -class ZHAGroupMember(LogMixin): - """Composite object that represents a device endpoint in a Zigbee group.""" - - def __init__( - self, zha_group: ZHAGroup, zha_device: ZHADevice, endpoint_id: int - ) -> None: - """Initialize the group member.""" - self._zha_group = zha_group - self._zha_device = zha_device - self._endpoint_id = endpoint_id - - @property - def group(self) -> ZHAGroup: - """Return the group this member belongs to.""" - return self._zha_group - - @property - def endpoint_id(self) -> int: - """Return the endpoint id for this group member.""" - return self._endpoint_id - - @property - def endpoint(self) -> zigpy.endpoint.Endpoint: - """Return the endpoint for this group member.""" - return self._zha_device.device.endpoints.get(self.endpoint_id) - - @property - def device(self) -> ZHADevice: - """Return the ZHA device for this group member.""" - return self._zha_device - - @property - def member_info(self) -> dict[str, Any]: - """Get ZHA group info.""" - member_info: dict[str, Any] = {} - member_info["endpoint_id"] = self.endpoint_id - member_info["device"] = self.device.zha_device_info - member_info["entities"] = self.associated_entities - return member_info - - @property - def associated_entities(self) -> list[dict[str, Any]]: - """Return the list of entities that were derived from this endpoint.""" - entity_registry = er.async_get(self._zha_device.hass) - zha_device_registry = self.device.gateway.device_registry - - entity_info = [] - - for entity_ref in zha_device_registry.get(self.device.ieee): - # We have device entities now that don't leverage cluster handlers - if not entity_ref.cluster_handlers: - continue - entity = entity_registry.async_get(entity_ref.reference_id) - handler = list(entity_ref.cluster_handlers.values())[0] - - if ( - entity is None - or handler.cluster.endpoint.endpoint_id != self.endpoint_id - ): - continue - - entity_info.append( - GroupEntityReference( - name=entity.name, - original_name=entity.original_name, - entity_id=entity_ref.reference_id, - )._asdict() - ) - - return entity_info - - async def async_remove_from_group(self) -> None: - """Remove the device endpoint from the provided zigbee group.""" - try: - await self._zha_device.device.endpoints[ - self._endpoint_id - ].remove_from_group(self._zha_group.group_id) - except (zigpy.exceptions.ZigbeeException, TimeoutError) as ex: - self.debug( - ( - "Failed to remove endpoint: %s for device '%s' from group: 0x%04x" - " ex: %s" - ), - self._endpoint_id, - self._zha_device.ieee, - self._zha_group.group_id, - str(ex), - ) - - def log(self, level: int, msg: str, *args: Any, **kwargs) -> None: - """Log a message.""" - msg = f"[%s](%s): {msg}" - args = (f"0x{self._zha_group.group_id:04x}", self.endpoint_id, *args) - _LOGGER.log(level, msg, *args, **kwargs) - - -class ZHAGroup(LogMixin): - """ZHA Zigbee group object.""" - - def __init__( - self, - hass: HomeAssistant, - zha_gateway: ZHAGateway, - zigpy_group: zigpy.group.Group, - ) -> None: - """Initialize the group.""" - self.hass = hass - self._zha_gateway = zha_gateway - self._zigpy_group = zigpy_group - - @property - def name(self) -> str: - """Return group name.""" - return self._zigpy_group.name - - @property - def group_id(self) -> int: - """Return group name.""" - return self._zigpy_group.group_id - - @property - def endpoint(self) -> zigpy.endpoint.Endpoint: - """Return the endpoint for this group.""" - return self._zigpy_group.endpoint - - @property - def members(self) -> list[ZHAGroupMember]: - """Return the ZHA devices that are members of this group.""" - return [ - ZHAGroupMember(self, self._zha_gateway.devices[member_ieee], endpoint_id) - for (member_ieee, endpoint_id) in self._zigpy_group.members - if member_ieee in self._zha_gateway.devices - ] - - async def async_add_members(self, members: list[GroupMember]) -> None: - """Add members to this group.""" - if len(members) > 1: - tasks = [ - self._zha_gateway.devices[member.ieee].async_add_endpoint_to_group( - member.endpoint_id, self.group_id - ) - for member in members - ] - await asyncio.gather(*tasks) - else: - await self._zha_gateway.devices[ - members[0].ieee - ].async_add_endpoint_to_group(members[0].endpoint_id, self.group_id) - - async def async_remove_members(self, members: list[GroupMember]) -> None: - """Remove members from this group.""" - if len(members) > 1: - tasks = [ - self._zha_gateway.devices[member.ieee].async_remove_endpoint_from_group( - member.endpoint_id, self.group_id - ) - for member in members - ] - await asyncio.gather(*tasks) - else: - await self._zha_gateway.devices[ - members[0].ieee - ].async_remove_endpoint_from_group(members[0].endpoint_id, self.group_id) - - @property - def member_entity_ids(self) -> list[str]: - """Return the ZHA entity ids for all entities for the members of this group.""" - return [ - entity_reference["entity_id"] - for member in self.members - for entity_reference in member.associated_entities - ] - - def get_domain_entity_ids(self, domain: str) -> list[str]: - """Return entity ids from the entity domain for this group.""" - entity_registry = er.async_get(self.hass) - domain_entity_ids: list[str] = [] - - for member in self.members: - if member.device.is_coordinator: - continue - entities = async_entries_for_device( - entity_registry, - member.device.device_id, - include_disabled_entities=True, - ) - domain_entity_ids.extend( - [entity.entity_id for entity in entities if entity.domain == domain] - ) - return domain_entity_ids - - @property - def group_info(self) -> dict[str, Any]: - """Get ZHA group info.""" - group_info: dict[str, Any] = {} - group_info["group_id"] = self.group_id - group_info["name"] = self.name - group_info["members"] = [member.member_info for member in self.members] - return group_info - - def log(self, level: int, msg: str, *args: Any, **kwargs) -> None: - """Log a message.""" - msg = f"[%s](%s): {msg}" - args = (self.name, self.group_id, *args) - _LOGGER.log(level, msg, *args, **kwargs) diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py deleted file mode 100644 index 2508dd34fd4..00000000000 --- a/homeassistant/components/zha/core/helpers.py +++ /dev/null @@ -1,523 +0,0 @@ -"""Helpers for Zigbee Home Automation. - -For more details about this component, please refer to the documentation at -https://home-assistant.io/integrations/zha/ -""" - -from __future__ import annotations - -import binascii -import collections -from collections.abc import Callable, Iterator -import dataclasses -from dataclasses import dataclass -import enum -import logging -import re -from typing import TYPE_CHECKING, Any, overload - -import voluptuous as vol -import zigpy.exceptions -import zigpy.types -import zigpy.util -import zigpy.zcl -from zigpy.zcl.foundation import CommandSchema -import zigpy.zdo.types as zdo_types - -from homeassistant.components.binary_sensor import BinarySensorDeviceClass -from homeassistant.components.number import NumberDeviceClass -from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - Platform, - UnitOfApparentPower, - UnitOfDataRate, - UnitOfElectricCurrent, - UnitOfElectricPotential, - UnitOfEnergy, - UnitOfFrequency, - UnitOfInformation, - UnitOfIrradiance, - UnitOfLength, - UnitOfMass, - UnitOfPower, - UnitOfPrecipitationDepth, - UnitOfPressure, - UnitOfSoundPressure, - UnitOfSpeed, - UnitOfTemperature, - UnitOfTime, - UnitOfVolume, - UnitOfVolumeFlowRate, - UnitOfVolumetricFlux, -) -from homeassistant.core import HomeAssistant, State, callback -from homeassistant.helpers import config_validation as cv, device_registry as dr -from homeassistant.helpers.typing import ConfigType - -from .const import CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, CUSTOM_CONFIGURATION, DATA_ZHA -from .registries import BINDABLE_CLUSTERS - -if TYPE_CHECKING: - from .device import ZHADevice - from .gateway import ZHAGateway - -_LOGGER = logging.getLogger(__name__) - - -@dataclass -class BindingPair: - """Information for binding.""" - - source_cluster: zigpy.zcl.Cluster - target_ieee: zigpy.types.EUI64 - target_ep_id: int - - @property - def destination_address(self) -> zdo_types.MultiAddress: - """Return a ZDO multi address instance.""" - return zdo_types.MultiAddress( - addrmode=3, ieee=self.target_ieee, endpoint=self.target_ep_id - ) - - -async def safe_read( - cluster, attributes, allow_cache=True, only_cache=False, manufacturer=None -): - """Swallow all exceptions from network read. - - If we throw during initialization, setup fails. Rather have an entity that - exists, but is in a maybe wrong state, than no entity. This method should - probably only be used during initialization. - """ - try: - result, _ = await cluster.read_attributes( - attributes, - allow_cache=allow_cache, - only_cache=only_cache, - manufacturer=manufacturer, - ) - except Exception: # noqa: BLE001 - return {} - return result - - -async def get_matched_clusters( - source_zha_device: ZHADevice, target_zha_device: ZHADevice -) -> list[BindingPair]: - """Get matched input/output cluster pairs for 2 devices.""" - source_clusters = source_zha_device.async_get_std_clusters() - target_clusters = target_zha_device.async_get_std_clusters() - clusters_to_bind = [] - - for endpoint_id in source_clusters: - for cluster_id in source_clusters[endpoint_id][CLUSTER_TYPE_OUT]: - if cluster_id not in BINDABLE_CLUSTERS: - continue - if target_zha_device.nwk == 0x0000: - cluster_pair = BindingPair( - source_cluster=source_clusters[endpoint_id][CLUSTER_TYPE_OUT][ - cluster_id - ], - target_ieee=target_zha_device.ieee, - target_ep_id=target_zha_device.device.application.get_endpoint_id( - cluster_id, is_server_cluster=True - ), - ) - clusters_to_bind.append(cluster_pair) - continue - for t_endpoint_id in target_clusters: - if cluster_id in target_clusters[t_endpoint_id][CLUSTER_TYPE_IN]: - cluster_pair = BindingPair( - source_cluster=source_clusters[endpoint_id][CLUSTER_TYPE_OUT][ - cluster_id - ], - target_ieee=target_zha_device.ieee, - target_ep_id=t_endpoint_id, - ) - clusters_to_bind.append(cluster_pair) - return clusters_to_bind - - -def cluster_command_schema_to_vol_schema(schema: CommandSchema) -> vol.Schema: - """Convert a cluster command schema to a voluptuous schema.""" - return vol.Schema( - { - vol.Optional(field.name) - if field.optional - else vol.Required(field.name): schema_type_to_vol(field.type) - for field in schema.fields - } - ) - - -def schema_type_to_vol(field_type: Any) -> Any: - """Convert a schema type to a voluptuous type.""" - if issubclass(field_type, enum.Flag) and field_type.__members__: - return cv.multi_select( - [key.replace("_", " ") for key in field_type.__members__] - ) - if issubclass(field_type, enum.Enum) and field_type.__members__: - return vol.In([key.replace("_", " ") for key in field_type.__members__]) - if ( - issubclass(field_type, zigpy.types.FixedIntType) - or issubclass(field_type, enum.Flag) - or issubclass(field_type, enum.Enum) - ): - return vol.All( - vol.Coerce(int), vol.Range(field_type.min_value, field_type.max_value) - ) - return str - - -def convert_to_zcl_values( - fields: dict[str, Any], schema: CommandSchema -) -> dict[str, Any]: - """Convert user input to ZCL values.""" - converted_fields: dict[str, Any] = {} - for field in schema.fields: - if field.name not in fields: - continue - value = fields[field.name] - if issubclass(field.type, enum.Flag) and isinstance(value, list): - new_value = 0 - - for flag in value: - if isinstance(flag, str): - new_value |= field.type[flag.replace(" ", "_")] - else: - new_value |= flag - - value = field.type(new_value) - elif issubclass(field.type, enum.Enum): - value = ( - field.type[value.replace(" ", "_")] - if isinstance(value, str) - else field.type(value) - ) - else: - value = field.type(value) - _LOGGER.debug( - "Converted ZCL schema field(%s) value from: %s to: %s", - field.name, - fields[field.name], - value, - ) - converted_fields[field.name] = value - return converted_fields - - -@callback -def async_is_bindable_target(source_zha_device, target_zha_device): - """Determine if target is bindable to source.""" - if target_zha_device.nwk == 0x0000: - return True - - source_clusters = source_zha_device.async_get_std_clusters() - target_clusters = target_zha_device.async_get_std_clusters() - - for endpoint_id in source_clusters: - for t_endpoint_id in target_clusters: - matches = set( - source_clusters[endpoint_id][CLUSTER_TYPE_OUT].keys() - ).intersection(target_clusters[t_endpoint_id][CLUSTER_TYPE_IN].keys()) - if any(bindable in BINDABLE_CLUSTERS for bindable in matches): - return True - return False - - -@callback -def async_get_zha_config_value[_T]( - config_entry: ConfigEntry, section: str, config_key: str, default: _T -) -> _T: - """Get the value for the specified configuration from the ZHA config entry.""" - return ( - config_entry.options.get(CUSTOM_CONFIGURATION, {}) - .get(section, {}) - .get(config_key, default) - ) - - -def async_cluster_exists(hass: HomeAssistant, cluster_id, skip_coordinator=True): - """Determine if a device containing the specified in cluster is paired.""" - zha_gateway = get_zha_gateway(hass) - zha_devices = zha_gateway.devices.values() - for zha_device in zha_devices: - if skip_coordinator and zha_device.is_coordinator: - continue - clusters_by_endpoint = zha_device.async_get_clusters() - for clusters in clusters_by_endpoint.values(): - if ( - cluster_id in clusters[CLUSTER_TYPE_IN] - or cluster_id in clusters[CLUSTER_TYPE_OUT] - ): - return True - return False - - -@callback -def async_get_zha_device(hass: HomeAssistant, device_id: str) -> ZHADevice: - """Get a ZHA device for the given device registry id.""" - device_registry = dr.async_get(hass) - registry_device = device_registry.async_get(device_id) - if not registry_device: - _LOGGER.error("Device id `%s` not found in registry", device_id) - raise KeyError(f"Device id `{device_id}` not found in registry.") - zha_gateway = get_zha_gateway(hass) - try: - ieee_address = list(registry_device.identifiers)[0][1] - ieee = zigpy.types.EUI64.convert(ieee_address) - except (IndexError, ValueError) as ex: - _LOGGER.error( - "Unable to determine device IEEE for device with device id `%s`", device_id - ) - raise KeyError( - f"Unable to determine device IEEE for device with device id `{device_id}`." - ) from ex - return zha_gateway.devices[ieee] - - -def find_state_attributes(states: list[State], key: str) -> Iterator[Any]: - """Find attributes with matching key from states.""" - for state in states: - if (value := state.attributes.get(key)) is not None: - yield value - - -def mean_int(*args): - """Return the mean of the supplied values.""" - return int(sum(args) / len(args)) - - -def mean_tuple(*args): - """Return the mean values along the columns of the supplied values.""" - return tuple(sum(x) / len(x) for x in zip(*args, strict=False)) - - -def reduce_attribute( - states: list[State], - key: str, - default: Any | None = None, - reduce: Callable[..., Any] = mean_int, -) -> Any: - """Find the first attribute matching key from states. - - If none are found, return default. - """ - attrs = list(find_state_attributes(states, key)) - - if not attrs: - return default - - if len(attrs) == 1: - return attrs[0] - - return reduce(*attrs) - - -class LogMixin: - """Log helper.""" - - def log(self, level, msg, *args, **kwargs): - """Log with level.""" - raise NotImplementedError - - def debug(self, msg, *args, **kwargs): - """Debug level log.""" - return self.log(logging.DEBUG, msg, *args, **kwargs) - - def info(self, msg, *args, **kwargs): - """Info level log.""" - return self.log(logging.INFO, msg, *args, **kwargs) - - def warning(self, msg, *args, **kwargs): - """Warning method log.""" - return self.log(logging.WARNING, msg, *args, **kwargs) - - def error(self, msg, *args, **kwargs): - """Error level log.""" - return self.log(logging.ERROR, msg, *args, **kwargs) - - -def convert_install_code(value: str) -> zigpy.types.KeyData: - """Convert string to install code bytes and validate length.""" - - try: - code = binascii.unhexlify(value.replace("-", "").lower()) - except binascii.Error as exc: - raise vol.Invalid(f"invalid hex string: {value}") from exc - - if len(code) != 18: # 16 byte code + 2 crc bytes - raise vol.Invalid("invalid length of the install code") - - link_key = zigpy.util.convert_install_code(code) - if link_key is None: - raise vol.Invalid("invalid install code") - - return link_key - - -QR_CODES = ( - # Consciot - r"^([\da-fA-F]{16})\|([\da-fA-F]{36})$", - # Enbrighten - r""" - ^Z: - ([0-9a-fA-F]{16}) # IEEE address - \$I: - ([0-9a-fA-F]{36}) # install code - $ - """, - # Aqara - r""" - \$A: - ([0-9a-fA-F]{16}) # IEEE address - \$I: - ([0-9a-fA-F]{36}) # install code - $ - """, - # Bosch - r""" - ^RB01SG - [0-9a-fA-F]{34} - ([0-9a-fA-F]{16}) # IEEE address - DLK - ([0-9a-fA-F]{36}|[0-9a-fA-F]{32}) # install code / link key - $ - """, -) - - -def qr_to_install_code(qr_code: str) -> tuple[zigpy.types.EUI64, zigpy.types.KeyData]: - """Try to parse the QR code. - - if successful, return a tuple of a EUI64 address and install code. - """ - - for code_pattern in QR_CODES: - match = re.search(code_pattern, qr_code, re.VERBOSE) - if match is None: - continue - - ieee_hex = binascii.unhexlify(match[1]) - ieee = zigpy.types.EUI64(ieee_hex[::-1]) - - # Bosch supplies (A) device specific link key (DSLK) or (A) install code + crc - if "RB01SG" in code_pattern and len(match[2]) == 32: - link_key_hex = binascii.unhexlify(match[2]) - link_key = zigpy.types.KeyData(link_key_hex) - return ieee, link_key - install_code = match[2] - # install_code sanity check - link_key = convert_install_code(install_code) - return ieee, link_key - - raise vol.Invalid(f"couldn't convert qr code: {qr_code}") - - -@dataclasses.dataclass(kw_only=True, slots=True) -class ZHAData: - """ZHA component data stored in `hass.data`.""" - - yaml_config: ConfigType = dataclasses.field(default_factory=dict) - platforms: collections.defaultdict[Platform, list] = dataclasses.field( - default_factory=lambda: collections.defaultdict(list) - ) - gateway: ZHAGateway | None = dataclasses.field(default=None) - device_trigger_cache: dict[str, tuple[str, dict]] = dataclasses.field( - default_factory=dict - ) - allow_polling: bool = dataclasses.field(default=False) - - -def get_zha_data(hass: HomeAssistant) -> ZHAData: - """Get the global ZHA data object.""" - if DATA_ZHA not in hass.data: - hass.data[DATA_ZHA] = ZHAData() - - return hass.data[DATA_ZHA] - - -def get_zha_gateway(hass: HomeAssistant) -> ZHAGateway: - """Get the ZHA gateway object.""" - if (zha_gateway := get_zha_data(hass).gateway) is None: - raise ValueError("No gateway object exists") - - return zha_gateway - - -UNITS_OF_MEASURE = { - UnitOfApparentPower.__name__: UnitOfApparentPower, - UnitOfPower.__name__: UnitOfPower, - UnitOfEnergy.__name__: UnitOfEnergy, - UnitOfElectricCurrent.__name__: UnitOfElectricCurrent, - UnitOfElectricPotential.__name__: UnitOfElectricPotential, - UnitOfTemperature.__name__: UnitOfTemperature, - UnitOfTime.__name__: UnitOfTime, - UnitOfLength.__name__: UnitOfLength, - UnitOfFrequency.__name__: UnitOfFrequency, - UnitOfPressure.__name__: UnitOfPressure, - UnitOfSoundPressure.__name__: UnitOfSoundPressure, - UnitOfVolume.__name__: UnitOfVolume, - UnitOfVolumeFlowRate.__name__: UnitOfVolumeFlowRate, - UnitOfMass.__name__: UnitOfMass, - UnitOfIrradiance.__name__: UnitOfIrradiance, - UnitOfVolumetricFlux.__name__: UnitOfVolumetricFlux, - UnitOfPrecipitationDepth.__name__: UnitOfPrecipitationDepth, - UnitOfSpeed.__name__: UnitOfSpeed, - UnitOfInformation.__name__: UnitOfInformation, - UnitOfDataRate.__name__: UnitOfDataRate, -} - - -def validate_unit(quirks_unit: enum.Enum) -> enum.Enum: - """Validate and return a unit of measure.""" - return UNITS_OF_MEASURE[type(quirks_unit).__name__](quirks_unit.value) - - -@overload -def validate_device_class( - device_class_enum: type[BinarySensorDeviceClass], - metadata_value, - platform: str, - logger: logging.Logger, -) -> BinarySensorDeviceClass | None: ... - - -@overload -def validate_device_class( - device_class_enum: type[SensorDeviceClass], - metadata_value, - platform: str, - logger: logging.Logger, -) -> SensorDeviceClass | None: ... - - -@overload -def validate_device_class( - device_class_enum: type[NumberDeviceClass], - metadata_value, - platform: str, - logger: logging.Logger, -) -> NumberDeviceClass | None: ... - - -def validate_device_class( - device_class_enum: type[ - BinarySensorDeviceClass | SensorDeviceClass | NumberDeviceClass - ], - metadata_value: enum.Enum, - platform: str, - logger: logging.Logger, -) -> BinarySensorDeviceClass | SensorDeviceClass | NumberDeviceClass | None: - """Validate and return a device class.""" - try: - return device_class_enum(metadata_value.value) - except ValueError as ex: - logger.warning( - "Quirks provided an invalid device class: %s for platform %s: %s", - metadata_value, - platform, - ex, - ) - return None diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py deleted file mode 100644 index 9d23b77efaa..00000000000 --- a/homeassistant/components/zha/core/registries.py +++ /dev/null @@ -1,516 +0,0 @@ -"""Mapping registries for Zigbee Home Automation.""" - -from __future__ import annotations - -import collections -from collections.abc import Callable -import dataclasses -from operator import attrgetter -from typing import TYPE_CHECKING - -import attr -from zigpy import zcl -import zigpy.profiles.zha -import zigpy.profiles.zll -from zigpy.types.named import EUI64 - -from homeassistant.const import Platform - -from .decorators import DictRegistry, NestedDictRegistry, SetRegistry - -if TYPE_CHECKING: - from ..entity import ZhaEntity, ZhaGroupEntity - from .cluster_handlers import ClientClusterHandler, ClusterHandler - - -GROUP_ENTITY_DOMAINS = [Platform.LIGHT, Platform.SWITCH, Platform.FAN] - -IKEA_AIR_PURIFIER_CLUSTER = 0xFC7D -PHILLIPS_REMOTE_CLUSTER = 0xFC00 -SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02 -SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000 -SMARTTHINGS_HUMIDITY_CLUSTER = 0xFC45 -TUYA_MANUFACTURER_CLUSTER = 0xEF00 -VOC_LEVEL_CLUSTER = 0x042E - -REMOTE_DEVICE_TYPES = { - zigpy.profiles.zha.PROFILE_ID: [ - zigpy.profiles.zha.DeviceType.COLOR_CONTROLLER, - zigpy.profiles.zha.DeviceType.COLOR_DIMMER_SWITCH, - zigpy.profiles.zha.DeviceType.COLOR_SCENE_CONTROLLER, - zigpy.profiles.zha.DeviceType.DIMMER_SWITCH, - zigpy.profiles.zha.DeviceType.LEVEL_CONTROL_SWITCH, - zigpy.profiles.zha.DeviceType.NON_COLOR_CONTROLLER, - zigpy.profiles.zha.DeviceType.NON_COLOR_SCENE_CONTROLLER, - zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH, - zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT_SWITCH, - zigpy.profiles.zha.DeviceType.REMOTE_CONTROL, - zigpy.profiles.zha.DeviceType.SCENE_SELECTOR, - ], - zigpy.profiles.zll.PROFILE_ID: [ - zigpy.profiles.zll.DeviceType.COLOR_CONTROLLER, - zigpy.profiles.zll.DeviceType.COLOR_SCENE_CONTROLLER, - zigpy.profiles.zll.DeviceType.CONTROL_BRIDGE, - zigpy.profiles.zll.DeviceType.CONTROLLER, - zigpy.profiles.zll.DeviceType.SCENE_CONTROLLER, - ], -} -REMOTE_DEVICE_TYPES = collections.defaultdict(list, REMOTE_DEVICE_TYPES) - -SINGLE_INPUT_CLUSTER_DEVICE_CLASS = { - # this works for now but if we hit conflicts we can break it out to - # a different dict that is keyed by manufacturer - zcl.clusters.general.AnalogOutput.cluster_id: Platform.NUMBER, - zcl.clusters.general.MultistateInput.cluster_id: Platform.SENSOR, - zcl.clusters.general.OnOff.cluster_id: Platform.SWITCH, - zcl.clusters.hvac.Fan.cluster_id: Platform.FAN, -} - -SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = { - zcl.clusters.general.OnOff.cluster_id: Platform.BINARY_SENSOR, - zcl.clusters.security.IasAce.cluster_id: Platform.ALARM_CONTROL_PANEL, -} - -BINDABLE_CLUSTERS = SetRegistry() - -DEVICE_CLASS = { - zigpy.profiles.zha.PROFILE_ID: { - SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE: Platform.DEVICE_TRACKER, - zigpy.profiles.zha.DeviceType.THERMOSTAT: Platform.CLIMATE, - zigpy.profiles.zha.DeviceType.COLOR_DIMMABLE_LIGHT: Platform.LIGHT, - zigpy.profiles.zha.DeviceType.COLOR_TEMPERATURE_LIGHT: Platform.LIGHT, - zigpy.profiles.zha.DeviceType.DIMMABLE_BALLAST: Platform.LIGHT, - zigpy.profiles.zha.DeviceType.DIMMABLE_LIGHT: Platform.LIGHT, - zigpy.profiles.zha.DeviceType.DIMMABLE_PLUG_IN_UNIT: Platform.LIGHT, - zigpy.profiles.zha.DeviceType.EXTENDED_COLOR_LIGHT: Platform.LIGHT, - zigpy.profiles.zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT: Platform.COVER, - zigpy.profiles.zha.DeviceType.ON_OFF_BALLAST: Platform.SWITCH, - zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT: Platform.LIGHT, - zigpy.profiles.zha.DeviceType.ON_OFF_PLUG_IN_UNIT: Platform.SWITCH, - zigpy.profiles.zha.DeviceType.SHADE: Platform.COVER, - zigpy.profiles.zha.DeviceType.SMART_PLUG: Platform.SWITCH, - zigpy.profiles.zha.DeviceType.IAS_ANCILLARY_CONTROL: Platform.ALARM_CONTROL_PANEL, - zigpy.profiles.zha.DeviceType.IAS_WARNING_DEVICE: Platform.SIREN, - }, - zigpy.profiles.zll.PROFILE_ID: { - zigpy.profiles.zll.DeviceType.COLOR_LIGHT: Platform.LIGHT, - zigpy.profiles.zll.DeviceType.COLOR_TEMPERATURE_LIGHT: Platform.LIGHT, - zigpy.profiles.zll.DeviceType.DIMMABLE_LIGHT: Platform.LIGHT, - zigpy.profiles.zll.DeviceType.DIMMABLE_PLUGIN_UNIT: Platform.LIGHT, - zigpy.profiles.zll.DeviceType.EXTENDED_COLOR_LIGHT: Platform.LIGHT, - zigpy.profiles.zll.DeviceType.ON_OFF_LIGHT: Platform.LIGHT, - zigpy.profiles.zll.DeviceType.ON_OFF_PLUGIN_UNIT: Platform.SWITCH, - }, -} -DEVICE_CLASS = collections.defaultdict(dict, DEVICE_CLASS) - -CLUSTER_HANDLER_ONLY_CLUSTERS = SetRegistry() -CLIENT_CLUSTER_HANDLER_REGISTRY: DictRegistry[type[ClientClusterHandler]] = ( - DictRegistry() -) -ZIGBEE_CLUSTER_HANDLER_REGISTRY: NestedDictRegistry[type[ClusterHandler]] = ( - NestedDictRegistry() -) - -WEIGHT_ATTR = attrgetter("weight") - - -def set_or_callable(value) -> frozenset[str] | Callable: - """Convert single str or None to a set. Pass through callables and sets.""" - if value is None: - return frozenset() - if callable(value): - return value - if isinstance(value, (frozenset, set, list)): - return frozenset(value) - return frozenset([str(value)]) - - -def _get_empty_frozenset() -> frozenset[str]: - return frozenset() - - -@attr.s(frozen=True) -class MatchRule: - """Match a ZHA Entity to a cluster handler name or generic id.""" - - cluster_handler_names: frozenset[str] = attr.ib( - factory=frozenset, converter=set_or_callable - ) - generic_ids: frozenset[str] = attr.ib(factory=frozenset, converter=set_or_callable) - manufacturers: frozenset[str] | Callable = attr.ib( - factory=_get_empty_frozenset, converter=set_or_callable - ) - models: frozenset[str] | Callable = attr.ib( - factory=_get_empty_frozenset, converter=set_or_callable - ) - aux_cluster_handlers: frozenset[str] | Callable = attr.ib( - factory=_get_empty_frozenset, converter=set_or_callable - ) - quirk_ids: frozenset[str] | Callable = attr.ib( - factory=_get_empty_frozenset, converter=set_or_callable - ) - - @property - def weight(self) -> int: - """Return the weight of the matching rule. - - More specific matches should be preferred over less specific. Quirk class - matching rules have priority over model matching rules - and have a priority over manufacturer matching rules and rules matching a - single model/manufacturer get a better priority over rules matching multiple - models/manufacturers. And any model or manufacturers matching rules get better - priority over rules matching only cluster handlers. - But in case of a cluster handler name/cluster handler id matching, we give rules matching - multiple cluster handlers a better priority over rules matching a single cluster handler. - """ - weight = 0 - if self.quirk_ids: - weight += 501 - (1 if callable(self.quirk_ids) else len(self.quirk_ids)) - - if self.models: - weight += 401 - (1 if callable(self.models) else len(self.models)) - - if self.manufacturers: - weight += 301 - ( - 1 if callable(self.manufacturers) else len(self.manufacturers) - ) - - weight += 10 * len(self.cluster_handler_names) - weight += 5 * len(self.generic_ids) - if isinstance(self.aux_cluster_handlers, frozenset): - weight += 1 * len(self.aux_cluster_handlers) - return weight - - def claim_cluster_handlers( - self, cluster_handlers: list[ClusterHandler] - ) -> list[ClusterHandler]: - """Return a list of cluster handlers this rule matches + aux cluster handlers.""" - claimed = [] - if isinstance(self.cluster_handler_names, frozenset): - claimed.extend( - [ch for ch in cluster_handlers if ch.name in self.cluster_handler_names] - ) - if isinstance(self.generic_ids, frozenset): - claimed.extend( - [ch for ch in cluster_handlers if ch.generic_id in self.generic_ids] - ) - if isinstance(self.aux_cluster_handlers, frozenset): - claimed.extend( - [ch for ch in cluster_handlers if ch.name in self.aux_cluster_handlers] - ) - return claimed - - def strict_matched( - self, - manufacturer: str, - model: str, - cluster_handlers: list, - quirk_id: str | None, - ) -> bool: - """Return True if this device matches the criteria.""" - return all(self._matched(manufacturer, model, cluster_handlers, quirk_id)) - - def loose_matched( - self, - manufacturer: str, - model: str, - cluster_handlers: list, - quirk_id: str | None, - ) -> bool: - """Return True if this device matches the criteria.""" - return any(self._matched(manufacturer, model, cluster_handlers, quirk_id)) - - def _matched( - self, - manufacturer: str, - model: str, - cluster_handlers: list, - quirk_id: str | None, - ) -> list: - """Return a list of field matches.""" - if not any(attr.asdict(self).values()): - return [False] - - matches = [] - if self.cluster_handler_names: - cluster_handler_names = {ch.name for ch in cluster_handlers} - matches.append(self.cluster_handler_names.issubset(cluster_handler_names)) - - if self.generic_ids: - all_generic_ids = {ch.generic_id for ch in cluster_handlers} - matches.append(self.generic_ids.issubset(all_generic_ids)) - - if self.manufacturers: - if callable(self.manufacturers): - matches.append(self.manufacturers(manufacturer)) - else: - matches.append(manufacturer in self.manufacturers) - - if self.models: - if callable(self.models): - matches.append(self.models(model)) - else: - matches.append(model in self.models) - - if self.quirk_ids: - if callable(self.quirk_ids): - matches.append(self.quirk_ids(quirk_id)) - else: - matches.append(quirk_id in self.quirk_ids) - - return matches - - -@dataclasses.dataclass -class EntityClassAndClusterHandlers: - """Container for entity class and corresponding cluster handlers.""" - - entity_class: type[ZhaEntity] - claimed_cluster_handlers: list[ClusterHandler] - - -class ZHAEntityRegistry: - """Cluster handler to ZHA Entity mapping.""" - - def __init__(self) -> None: - """Initialize Registry instance.""" - self._strict_registry: dict[Platform, dict[MatchRule, type[ZhaEntity]]] = ( - collections.defaultdict(dict) - ) - self._multi_entity_registry: dict[ - Platform, dict[int | str | None, dict[MatchRule, list[type[ZhaEntity]]]] - ] = collections.defaultdict( - lambda: collections.defaultdict(lambda: collections.defaultdict(list)) - ) - self._config_diagnostic_entity_registry: dict[ - Platform, dict[int | str | None, dict[MatchRule, list[type[ZhaEntity]]]] - ] = collections.defaultdict( - lambda: collections.defaultdict(lambda: collections.defaultdict(list)) - ) - self._group_registry: dict[str, type[ZhaGroupEntity]] = {} - self.single_device_matches: dict[Platform, dict[EUI64, list[str]]] = ( - collections.defaultdict(lambda: collections.defaultdict(list)) - ) - - def get_entity( - self, - component: Platform, - manufacturer: str, - model: str, - cluster_handlers: list[ClusterHandler], - quirk_id: str | None, - default: type[ZhaEntity] | None = None, - ) -> tuple[type[ZhaEntity] | None, list[ClusterHandler]]: - """Match a ZHA ClusterHandler to a ZHA Entity class.""" - matches = self._strict_registry[component] - for match in sorted(matches, key=WEIGHT_ATTR, reverse=True): - if match.strict_matched(manufacturer, model, cluster_handlers, quirk_id): - claimed = match.claim_cluster_handlers(cluster_handlers) - return self._strict_registry[component][match], claimed - - return default, [] - - def get_multi_entity( - self, - manufacturer: str, - model: str, - cluster_handlers: list[ClusterHandler], - quirk_id: str | None, - ) -> tuple[ - dict[Platform, list[EntityClassAndClusterHandlers]], list[ClusterHandler] - ]: - """Match ZHA cluster handlers to potentially multiple ZHA Entity classes.""" - result: dict[Platform, list[EntityClassAndClusterHandlers]] = ( - collections.defaultdict(list) - ) - all_claimed: set[ClusterHandler] = set() - for component, stop_match_groups in self._multi_entity_registry.items(): - for stop_match_grp, matches in stop_match_groups.items(): - sorted_matches = sorted(matches, key=WEIGHT_ATTR, reverse=True) - for match in sorted_matches: - if match.strict_matched( - manufacturer, model, cluster_handlers, quirk_id - ): - claimed = match.claim_cluster_handlers(cluster_handlers) - for ent_class in stop_match_groups[stop_match_grp][match]: - ent_n_cluster_handlers = EntityClassAndClusterHandlers( - ent_class, claimed - ) - result[component].append(ent_n_cluster_handlers) - all_claimed |= set(claimed) - if stop_match_grp: - break - - return result, list(all_claimed) - - def get_config_diagnostic_entity( - self, - manufacturer: str, - model: str, - cluster_handlers: list[ClusterHandler], - quirk_id: str | None, - ) -> tuple[ - dict[Platform, list[EntityClassAndClusterHandlers]], list[ClusterHandler] - ]: - """Match ZHA cluster handlers to potentially multiple ZHA Entity classes.""" - result: dict[Platform, list[EntityClassAndClusterHandlers]] = ( - collections.defaultdict(list) - ) - all_claimed: set[ClusterHandler] = set() - for ( - component, - stop_match_groups, - ) in self._config_diagnostic_entity_registry.items(): - for stop_match_grp, matches in stop_match_groups.items(): - sorted_matches = sorted(matches, key=WEIGHT_ATTR, reverse=True) - for match in sorted_matches: - if match.strict_matched( - manufacturer, model, cluster_handlers, quirk_id - ): - claimed = match.claim_cluster_handlers(cluster_handlers) - for ent_class in stop_match_groups[stop_match_grp][match]: - ent_n_cluster_handlers = EntityClassAndClusterHandlers( - ent_class, claimed - ) - result[component].append(ent_n_cluster_handlers) - all_claimed |= set(claimed) - if stop_match_grp: - break - - return result, list(all_claimed) - - def get_group_entity(self, component: str) -> type[ZhaGroupEntity] | None: - """Match a ZHA group to a ZHA Entity class.""" - return self._group_registry.get(component) - - def strict_match[_ZhaEntityT: type[ZhaEntity]]( - self, - component: Platform, - cluster_handler_names: set[str] | str | None = None, - generic_ids: set[str] | str | None = None, - manufacturers: Callable | set[str] | str | None = None, - models: Callable | set[str] | str | None = None, - aux_cluster_handlers: Callable | set[str] | str | None = None, - quirk_ids: set[str] | str | None = None, - ) -> Callable[[_ZhaEntityT], _ZhaEntityT]: - """Decorate a strict match rule.""" - - rule = MatchRule( - cluster_handler_names, - generic_ids, - manufacturers, - models, - aux_cluster_handlers, - quirk_ids, - ) - - def decorator(zha_ent: _ZhaEntityT) -> _ZhaEntityT: - """Register a strict match rule. - - All non-empty fields of a match rule must match. - """ - self._strict_registry[component][rule] = zha_ent - return zha_ent - - return decorator - - def multipass_match[_ZhaEntityT: type[ZhaEntity]]( - self, - component: Platform, - cluster_handler_names: set[str] | str | None = None, - generic_ids: set[str] | str | None = None, - manufacturers: Callable | set[str] | str | None = None, - models: Callable | set[str] | str | None = None, - aux_cluster_handlers: Callable | set[str] | str | None = None, - stop_on_match_group: int | str | None = None, - quirk_ids: set[str] | str | None = None, - ) -> Callable[[_ZhaEntityT], _ZhaEntityT]: - """Decorate a loose match rule.""" - - rule = MatchRule( - cluster_handler_names, - generic_ids, - manufacturers, - models, - aux_cluster_handlers, - quirk_ids, - ) - - def decorator(zha_entity: _ZhaEntityT) -> _ZhaEntityT: - """Register a loose match rule. - - All non empty fields of a match rule must match. - """ - # group the rules by cluster handlers - self._multi_entity_registry[component][stop_on_match_group][rule].append( - zha_entity - ) - return zha_entity - - return decorator - - def config_diagnostic_match[_ZhaEntityT: type[ZhaEntity]]( - self, - component: Platform, - cluster_handler_names: set[str] | str | None = None, - generic_ids: set[str] | str | None = None, - manufacturers: Callable | set[str] | str | None = None, - models: Callable | set[str] | str | None = None, - aux_cluster_handlers: Callable | set[str] | str | None = None, - stop_on_match_group: int | str | None = None, - quirk_ids: set[str] | str | None = None, - ) -> Callable[[_ZhaEntityT], _ZhaEntityT]: - """Decorate a loose match rule.""" - - rule = MatchRule( - cluster_handler_names, - generic_ids, - manufacturers, - models, - aux_cluster_handlers, - quirk_ids, - ) - - def decorator(zha_entity: _ZhaEntityT) -> _ZhaEntityT: - """Register a loose match rule. - - All non-empty fields of a match rule must match. - """ - # group the rules by cluster handlers - self._config_diagnostic_entity_registry[component][stop_on_match_group][ - rule - ].append(zha_entity) - return zha_entity - - return decorator - - def group_match[_ZhaGroupEntityT: type[ZhaGroupEntity]]( - self, component: Platform - ) -> Callable[[_ZhaGroupEntityT], _ZhaGroupEntityT]: - """Decorate a group match rule.""" - - def decorator(zha_ent: _ZhaGroupEntityT) -> _ZhaGroupEntityT: - """Register a group match rule.""" - self._group_registry[component] = zha_ent - return zha_ent - - return decorator - - def prevent_entity_creation(self, platform: Platform, ieee: EUI64, key: str): - """Return True if the entity should not be created.""" - platform_restrictions = self.single_device_matches[platform] - device_restrictions = platform_restrictions[ieee] - if key in device_restrictions: - return True - device_restrictions.append(key) - return False - - def clean_up(self) -> None: - """Clean up post discovery.""" - self.single_device_matches = collections.defaultdict( - lambda: collections.defaultdict(list) - ) - - -ZHA_ENTITIES = ZHAEntityRegistry() diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index 718b6fed3a2..0d6be2dbb35 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -2,16 +2,17 @@ from __future__ import annotations -import asyncio +from collections.abc import Mapping import functools import logging -from typing import TYPE_CHECKING, Any, cast +from typing import Any -from zigpy.zcl.clusters.closures import WindowCovering as WindowCoveringCluster -from zigpy.zcl.foundation import Status +from zha.application.platforms.cover import Shade as ZhaShade +from zha.application.platforms.cover.const import ( + CoverEntityFeature as ZHACoverEntityFeature, +) from homeassistant.components.cover import ( - ATTR_CURRENT_POSITION, ATTR_POSITION, ATTR_TILT_POSITION, CoverDeviceClass, @@ -19,41 +20,22 @@ from homeassistant.components.cover import ( CoverEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - STATE_CLOSED, - STATE_CLOSING, - STATE_OPEN, - STATE_OPENING, - Platform, -) -from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import HomeAssistantError +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .core import discovery -from .core.cluster_handlers.closures import WindowCoveringClusterHandler -from .core.const import ( - CLUSTER_HANDLER_COVER, - CLUSTER_HANDLER_LEVEL, - CLUSTER_HANDLER_ON_OFF, - CLUSTER_HANDLER_SHADE, +from .entity import ZHAEntity +from .helpers import ( SIGNAL_ADD_ENTITIES, - SIGNAL_ATTR_UPDATED, - SIGNAL_SET_LEVEL, + EntityData, + async_add_entities as zha_async_add_entities, + convert_zha_error_to_ha_error, + get_zha_data, ) -from .core.helpers import get_zha_data -from .core.registries import ZHA_ENTITIES -from .entity import ZhaEntity - -if TYPE_CHECKING: - from .core.cluster_handlers import ClusterHandler - from .core.device import ZHADevice _LOGGER = logging.getLogger(__name__) -MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.COVER) - async def async_setup_entry( hass: HomeAssistant, @@ -68,421 +50,143 @@ async def async_setup_entry( hass, SIGNAL_ADD_ENTITIES, functools.partial( - discovery.async_add_entities, async_add_entities, entities_to_create + zha_async_add_entities, async_add_entities, ZhaCover, entities_to_create ), ) config_entry.async_on_unload(unsub) -WCAttrs = WindowCoveringCluster.AttributeDefs -WCT = WindowCoveringCluster.WindowCoveringType -WCCS = WindowCoveringCluster.ConfigStatus - -ZCL_TO_COVER_DEVICE_CLASS = { - WCT.Awning: CoverDeviceClass.AWNING, - WCT.Drapery: CoverDeviceClass.CURTAIN, - WCT.Projector_screen: CoverDeviceClass.SHADE, - WCT.Rollershade: CoverDeviceClass.SHADE, - WCT.Rollershade_two_motors: CoverDeviceClass.SHADE, - WCT.Rollershade_exterior: CoverDeviceClass.SHADE, - WCT.Rollershade_exterior_two_motors: CoverDeviceClass.SHADE, - WCT.Shutter: CoverDeviceClass.SHUTTER, - WCT.Tilt_blind_tilt_only: CoverDeviceClass.BLIND, - WCT.Tilt_blind_tilt_and_lift: CoverDeviceClass.BLIND, -} - - -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_COVER) -class ZhaCover(ZhaEntity, CoverEntity): +class ZhaCover(ZHAEntity, CoverEntity): """Representation of a ZHA cover.""" - _attr_translation_key: str = "cover" + def __init__(self, entity_data: EntityData) -> None: + """Initialize the ZHA cover.""" + super().__init__(entity_data) - def __init__( - self, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> None: - """Init this cover.""" - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - cluster_handler = self.cluster_handlers.get(CLUSTER_HANDLER_COVER) - assert cluster_handler - self._cover_cluster_handler: WindowCoveringClusterHandler = cast( - WindowCoveringClusterHandler, cluster_handler - ) - if self._cover_cluster_handler.window_covering_type: - self._attr_device_class: CoverDeviceClass | None = ( - ZCL_TO_COVER_DEVICE_CLASS.get( - self._cover_cluster_handler.window_covering_type - ) + if self.entity_data.entity.info_object.device_class is not None: + self._attr_device_class = CoverDeviceClass( + self.entity_data.entity.info_object.device_class ) - self._attr_supported_features: CoverEntityFeature = ( - self._determine_supported_features() - ) - self._target_lift_position: int | None = None - self._target_tilt_position: int | None = None - self._determine_initial_state() - def _determine_supported_features(self) -> CoverEntityFeature: - """Determine the supported cover features.""" - supported_features: CoverEntityFeature = ( - CoverEntityFeature.OPEN - | CoverEntityFeature.CLOSE - | CoverEntityFeature.STOP - | CoverEntityFeature.SET_POSITION - ) - if ( - self._cover_cluster_handler.window_covering_type - and self._cover_cluster_handler.window_covering_type - in ( - WCT.Shutter, - WCT.Tilt_blind_tilt_only, - WCT.Tilt_blind_tilt_and_lift, - ) - ): - supported_features |= CoverEntityFeature.SET_TILT_POSITION - supported_features |= CoverEntityFeature.OPEN_TILT - supported_features |= CoverEntityFeature.CLOSE_TILT - supported_features |= CoverEntityFeature.STOP_TILT - return supported_features + features = CoverEntityFeature(0) + zha_features: ZHACoverEntityFeature = self.entity_data.entity.supported_features - def _determine_initial_state(self) -> None: - """Determine the initial state of the cover.""" - if ( - self._cover_cluster_handler.window_covering_type - and self._cover_cluster_handler.window_covering_type - in ( - WCT.Shutter, - WCT.Tilt_blind_tilt_only, - WCT.Tilt_blind_tilt_and_lift, - ) - ): - self._determine_state( - self.current_cover_tilt_position, is_lift_update=False - ) - if ( - self._cover_cluster_handler.window_covering_type - == WCT.Tilt_blind_tilt_and_lift - ): - state = self._state - self._determine_state(self.current_cover_position) - if state == STATE_OPEN and self._state == STATE_CLOSED: - # let the tilt state override the lift state - self._state = STATE_OPEN - else: - self._determine_state(self.current_cover_position) + if ZHACoverEntityFeature.OPEN in zha_features: + features |= CoverEntityFeature.OPEN + if ZHACoverEntityFeature.CLOSE in zha_features: + features |= CoverEntityFeature.CLOSE + if ZHACoverEntityFeature.SET_POSITION in zha_features: + features |= CoverEntityFeature.SET_POSITION + if ZHACoverEntityFeature.STOP in zha_features: + features |= CoverEntityFeature.STOP + if ZHACoverEntityFeature.OPEN_TILT in zha_features: + features |= CoverEntityFeature.OPEN_TILT + if ZHACoverEntityFeature.CLOSE_TILT in zha_features: + features |= CoverEntityFeature.CLOSE_TILT + if ZHACoverEntityFeature.STOP_TILT in zha_features: + features |= CoverEntityFeature.STOP_TILT + if ZHACoverEntityFeature.SET_TILT_POSITION in zha_features: + features |= CoverEntityFeature.SET_TILT_POSITION - def _determine_state(self, position_or_tilt, is_lift_update=True) -> None: - """Determine the state of the cover. + self._attr_supported_features = features - In HA None is unknown, 0 is closed, 100 is fully open. - In ZCL 0 is fully open, 100 is fully closed. - Keep in mind the values have already been flipped to match HA - in the WindowCovering cluster handler - """ - if is_lift_update: - target = self._target_lift_position - current = self.current_cover_position - else: - target = self._target_tilt_position - current = self.current_cover_tilt_position - - if position_or_tilt == 100: - self._state = STATE_CLOSED - return - if target is not None and target != current: - # we are mid transition and shouldn't update the state - return - self._state = STATE_OPEN - - async def async_added_to_hass(self) -> None: - """Run when the cover entity is about to be added to hass.""" - await super().async_added_to_hass() - self.async_accept_signal( - self._cover_cluster_handler, SIGNAL_ATTR_UPDATED, self.zcl_attribute_updated - ) + @property + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return entity specific state attributes.""" + state = self.entity_data.entity.state + return { + "target_lift_position": state.get("target_lift_position"), + "target_tilt_position": state.get("target_tilt_position"), + } @property def is_closed(self) -> bool | None: - """Return True if the cover is closed. - - In HA None is unknown, 0 is closed, 100 is fully open. - In ZCL 0 is fully open, 100 is fully closed. - Keep in mind the values have already been flipped to match HA - in the WindowCovering cluster handler - """ - if self.current_cover_position is None: - return None - return self.current_cover_position == 0 + """Return True if the cover is closed.""" + return self.entity_data.entity.is_closed @property def is_opening(self) -> bool: """Return if the cover is opening or not.""" - return self._state == STATE_OPENING + return self.entity_data.entity.is_opening @property def is_closing(self) -> bool: """Return if the cover is closing or not.""" - return self._state == STATE_CLOSING + return self.entity_data.entity.is_closing @property def current_cover_position(self) -> int | None: - """Return the current position of ZHA cover. - - In HA None is unknown, 0 is closed, 100 is fully open. - In ZCL 0 is fully open, 100 is fully closed. - Keep in mind the values have already been flipped to match HA - in the WindowCovering cluster handler - """ - return self._cover_cluster_handler.current_position_lift_percentage + """Return the current position of ZHA cover.""" + return self.entity_data.entity.current_cover_position @property def current_cover_tilt_position(self) -> int | None: """Return the current tilt position of the cover.""" - return self._cover_cluster_handler.current_position_tilt_percentage - - @callback - def zcl_attribute_updated(self, attr_id, attr_name, value): - """Handle position update from cluster handler.""" - if attr_id in ( - WCAttrs.current_position_lift_percentage.id, - WCAttrs.current_position_tilt_percentage.id, - ): - value = ( - self.current_cover_position - if attr_id == WCAttrs.current_position_lift_percentage.id - else self.current_cover_tilt_position - ) - self._determine_state( - value, - is_lift_update=attr_id == WCAttrs.current_position_lift_percentage.id, - ) - self.async_write_ha_state() - - @callback - def async_update_state(self, state): - """Handle state update from HA operations below.""" - _LOGGER.debug("async_update_state=%s", state) - self._state = state - self.async_write_ha_state() + return self.entity_data.entity.current_cover_tilt_position + @convert_zha_error_to_ha_error async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" - res = await self._cover_cluster_handler.up_open() - if res[1] is not Status.SUCCESS: - raise HomeAssistantError(f"Failed to open cover: {res[1]}") - self.async_update_state(STATE_OPENING) + await self.entity_data.entity.async_open_cover() + self.async_write_ha_state() + @convert_zha_error_to_ha_error async def async_open_cover_tilt(self, **kwargs: Any) -> None: """Open the cover tilt.""" - # 0 is open in ZCL - res = await self._cover_cluster_handler.go_to_tilt_percentage(0) - if res[1] is not Status.SUCCESS: - raise HomeAssistantError(f"Failed to open cover tilt: {res[1]}") - self.async_update_state(STATE_OPENING) + await self.entity_data.entity.async_open_cover_tilt() + self.async_write_ha_state() + @convert_zha_error_to_ha_error async def async_close_cover(self, **kwargs: Any) -> None: """Close the cover.""" - res = await self._cover_cluster_handler.down_close() - if res[1] is not Status.SUCCESS: - raise HomeAssistantError(f"Failed to close cover: {res[1]}") - self.async_update_state(STATE_CLOSING) + await self.entity_data.entity.async_close_cover() + self.async_write_ha_state() + @convert_zha_error_to_ha_error async def async_close_cover_tilt(self, **kwargs: Any) -> None: """Close the cover tilt.""" - # 100 is closed in ZCL - res = await self._cover_cluster_handler.go_to_tilt_percentage(100) - if res[1] is not Status.SUCCESS: - raise HomeAssistantError(f"Failed to close cover tilt: {res[1]}") - self.async_update_state(STATE_CLOSING) + await self.entity_data.entity.async_close_cover_tilt() + self.async_write_ha_state() + @convert_zha_error_to_ha_error async def async_set_cover_position(self, **kwargs: Any) -> None: """Move the cover to a specific position.""" - self._target_lift_position = kwargs[ATTR_POSITION] - assert self._target_lift_position is not None - assert self.current_cover_position is not None - # the 100 - value is because we need to invert the value before giving it to ZCL - res = await self._cover_cluster_handler.go_to_lift_percentage( - 100 - self._target_lift_position - ) - if res[1] is not Status.SUCCESS: - raise HomeAssistantError(f"Failed to set cover position: {res[1]}") - self.async_update_state( - STATE_CLOSING - if self._target_lift_position < self.current_cover_position - else STATE_OPENING + await self.entity_data.entity.async_set_cover_position( + position=kwargs.get(ATTR_POSITION) ) + self.async_write_ha_state() + @convert_zha_error_to_ha_error async def async_set_cover_tilt_position(self, **kwargs: Any) -> None: """Move the cover tilt to a specific position.""" - self._target_tilt_position = kwargs[ATTR_TILT_POSITION] - assert self._target_tilt_position is not None - assert self.current_cover_tilt_position is not None - # the 100 - value is because we need to invert the value before giving it to ZCL - res = await self._cover_cluster_handler.go_to_tilt_percentage( - 100 - self._target_tilt_position + await self.entity_data.entity.async_set_cover_tilt_position( + tilt_position=kwargs.get(ATTR_TILT_POSITION) ) - if res[1] is not Status.SUCCESS: - raise HomeAssistantError(f"Failed to set cover tilt position: {res[1]}") - self.async_update_state( - STATE_CLOSING - if self._target_tilt_position < self.current_cover_tilt_position - else STATE_OPENING - ) - - async def async_stop_cover(self, **kwargs: Any) -> None: - """Stop the cover.""" - res = await self._cover_cluster_handler.stop() - if res[1] is not Status.SUCCESS: - raise HomeAssistantError(f"Failed to stop cover: {res[1]}") - self._target_lift_position = self.current_cover_position - self._determine_state(self.current_cover_position) self.async_write_ha_state() + @convert_zha_error_to_ha_error + async def async_stop_cover(self, **kwargs: Any) -> None: + """Stop the cover.""" + await self.entity_data.entity.async_stop_cover() + self.async_write_ha_state() + + @convert_zha_error_to_ha_error async def async_stop_cover_tilt(self, **kwargs: Any) -> None: """Stop the cover tilt.""" - res = await self._cover_cluster_handler.stop() - if res[1] is not Status.SUCCESS: - raise HomeAssistantError(f"Failed to stop cover: {res[1]}") - self._target_tilt_position = self.current_cover_tilt_position - self._determine_state(self.current_cover_tilt_position, is_lift_update=False) - self.async_write_ha_state() - - -@MULTI_MATCH( - cluster_handler_names={ - CLUSTER_HANDLER_LEVEL, - CLUSTER_HANDLER_ON_OFF, - CLUSTER_HANDLER_SHADE, - } -) -class Shade(ZhaEntity, CoverEntity): - """ZHA Shade.""" - - _attr_device_class = CoverDeviceClass.SHADE - _attr_translation_key: str = "shade" - - def __init__( - self, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs, - ) -> None: - """Initialize the ZHA light.""" - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - self._on_off_cluster_handler = self.cluster_handlers[CLUSTER_HANDLER_ON_OFF] - self._level_cluster_handler = self.cluster_handlers[CLUSTER_HANDLER_LEVEL] - self._position: int | None = None - self._is_open: bool | None = None - - @property - def current_cover_position(self) -> int | None: - """Return current position of cover. - - None is unknown, 0 is closed, 100 is fully open. - """ - return self._position - - @property - def is_closed(self) -> bool | None: - """Return True if shade is closed.""" - if self._is_open is None: - return None - return not self._is_open - - async def async_added_to_hass(self) -> None: - """Run when about to be added to hass.""" - await super().async_added_to_hass() - self.async_accept_signal( - self._on_off_cluster_handler, - SIGNAL_ATTR_UPDATED, - self.async_set_open_closed, - ) - self.async_accept_signal( - self._level_cluster_handler, SIGNAL_SET_LEVEL, self.async_set_level - ) - - @callback - def async_restore_last_state(self, last_state): - """Restore previous state.""" - self._is_open = last_state.state == STATE_OPEN - if ATTR_CURRENT_POSITION in last_state.attributes: - self._position = last_state.attributes[ATTR_CURRENT_POSITION] - - @callback - def async_set_open_closed(self, attr_id: int, attr_name: str, value: bool) -> None: - """Set open/closed state.""" - self._is_open = bool(value) + await self.entity_data.entity.async_stop_cover_tilt() self.async_write_ha_state() @callback - def async_set_level(self, value: int) -> None: - """Set the reported position.""" - value = max(0, min(255, value)) - self._position = int(value * 100 / 255) - self.async_write_ha_state() + def restore_external_state_attributes(self, state: State) -> None: + """Restore entity state.""" - async def async_open_cover(self, **kwargs: Any) -> None: - """Open the window cover.""" - res = await self._on_off_cluster_handler.on() - if res[1] != Status.SUCCESS: - raise HomeAssistantError(f"Failed to open cover: {res[1]}") + # Shades are a subtype of cover that do not need external state restored + if isinstance(self.entity_data.entity, ZhaShade): + return - self._is_open = True - self.async_write_ha_state() - - async def async_close_cover(self, **kwargs: Any) -> None: - """Close the window cover.""" - res = await self._on_off_cluster_handler.off() - if res[1] != Status.SUCCESS: - raise HomeAssistantError(f"Failed to close cover: {res[1]}") - - self._is_open = False - self.async_write_ha_state() - - async def async_set_cover_position(self, **kwargs: Any) -> None: - """Move the roller shutter to a specific position.""" - new_pos = kwargs[ATTR_POSITION] - res = await self._level_cluster_handler.move_to_level_with_on_off( - new_pos * 255 / 100, 1 + # Same as `light`, some entity state is not derived from ZCL attributes + self.entity_data.entity.restore_external_state_attributes( + state=state.state, + target_lift_position=state.attributes.get("target_lift_position"), + target_tilt_position=state.attributes.get("target_tilt_position"), ) - - if res[1] != Status.SUCCESS: - raise HomeAssistantError(f"Failed to set cover position: {res[1]}") - - self._position = new_pos - self.async_write_ha_state() - - async def async_stop_cover(self, **kwargs: Any) -> None: - """Stop the cover.""" - res = await self._level_cluster_handler.stop() - if res[1] != Status.SUCCESS: - raise HomeAssistantError(f"Failed to stop cover: {res[1]}") - - -@MULTI_MATCH( - cluster_handler_names={CLUSTER_HANDLER_LEVEL, CLUSTER_HANDLER_ON_OFF}, - manufacturers="Keen Home Inc", -) -class KeenVent(Shade): - """Keen vent cover.""" - - _attr_device_class = CoverDeviceClass.DAMPER - _attr_translation_key: str = "keen_vent" - - async def async_open_cover(self, **kwargs: Any) -> None: - """Open the cover.""" - position = self._position or 100 - await asyncio.gather( - self._level_cluster_handler.move_to_level_with_on_off( - position * 255 / 100, 1 - ), - self._on_off_cluster_handler.on(), - ) - - self._is_open = True - self._position = position - self.async_write_ha_state() diff --git a/homeassistant/components/zha/device_action.py b/homeassistant/components/zha/device_action.py index a0f16d61f41..b4b40880734 100644 --- a/homeassistant/components/zha/device_action.py +++ b/homeassistant/components/zha/device_action.py @@ -5,20 +5,25 @@ from __future__ import annotations from typing import Any import voluptuous as vol +from zha.exceptions import ZHAException +from zha.zigbee.cluster_handlers.const import ( + CLUSTER_HANDLER_IAS_WD, + CLUSTER_HANDLER_INOVELLI, +) +from zha.zigbee.cluster_handlers.manufacturerspecific import ( + AllLEDEffectType, + SingleLEDEffectType, +) from homeassistant.components.device_automation import InvalidDeviceAutomationConfig from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE from homeassistant.core import Context, HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv from homeassistant.helpers.typing import ConfigType, TemplateVarsType -from . import DOMAIN -from .core.cluster_handlers.manufacturerspecific import ( - AllLEDEffectType, - SingleLEDEffectType, -) -from .core.const import CLUSTER_HANDLER_IAS_WD, CLUSTER_HANDLER_INOVELLI -from .core.helpers import async_get_zha_device +from .const import DOMAIN +from .helpers import async_get_zha_device_proxy from .websocket_api import SERVICE_WARNING_DEVICE_SQUAWK, SERVICE_WARNING_DEVICE_WARN # mypy: disallow-any-generics @@ -144,7 +149,7 @@ async def async_get_actions( ) -> list[dict[str, str]]: """List device actions.""" try: - zha_device = async_get_zha_device(hass, device_id) + zha_device = async_get_zha_device_proxy(hass, device_id).device except (KeyError, AttributeError): return [] cluster_handlers = [ @@ -181,7 +186,7 @@ async def _execute_service_based_action( action_type = config[CONF_TYPE] service_name = SERVICE_NAMES[action_type] try: - zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID]) + zha_device = async_get_zha_device_proxy(hass, config[CONF_DEVICE_ID]).device except (KeyError, AttributeError): return @@ -201,7 +206,7 @@ async def _execute_cluster_handler_command_based_action( action_type = config[CONF_TYPE] cluster_handler_name = CLUSTER_HANDLER_MAPPINGS[action_type] try: - zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID]) + zha_device = async_get_zha_device_proxy(hass, config[CONF_DEVICE_ID]).device except (KeyError, AttributeError): return @@ -224,7 +229,10 @@ async def _execute_cluster_handler_command_based_action( f" {action_type}" ) - await getattr(action_cluster_handler, action_type)(**config) + try: + await getattr(action_cluster_handler, action_type)(**config) + except ZHAException as err: + raise HomeAssistantError(err) from err ZHA_ACTION_TYPES = { diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index 9c96fd0e346..247219777f4 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -3,28 +3,21 @@ from __future__ import annotations import functools -import time from homeassistant.components.device_tracker import ScannerEntity, SourceType from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .core import discovery -from .core.const import ( - CLUSTER_HANDLER_POWER_CONFIGURATION, +from .entity import ZHAEntity +from .helpers import ( SIGNAL_ADD_ENTITIES, - SIGNAL_ATTR_UPDATED, + async_add_entities as zha_async_add_entities, + get_zha_data, ) -from .core.helpers import get_zha_data -from .core.registries import ZHA_ENTITIES -from .entity import ZhaEntity -from .sensor import Battery - -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.DEVICE_TRACKER) async def async_setup_entry( @@ -40,92 +33,48 @@ async def async_setup_entry( hass, SIGNAL_ADD_ENTITIES, functools.partial( - discovery.async_add_entities, async_add_entities, entities_to_create + zha_async_add_entities, + async_add_entities, + ZHADeviceScannerEntity, + entities_to_create, ), ) config_entry.async_on_unload(unsub) -@STRICT_MATCH(cluster_handler_names=CLUSTER_HANDLER_POWER_CONFIGURATION) -class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity): +class ZHADeviceScannerEntity(ScannerEntity, ZHAEntity): """Represent a tracked device.""" _attr_should_poll = True # BaseZhaEntity defaults to False _attr_name: str = "Device scanner" - def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs): - """Initialize the ZHA device tracker.""" - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - self._battery_cluster_handler = self.cluster_handlers.get( - CLUSTER_HANDLER_POWER_CONFIGURATION - ) - self._connected = False - self._keepalive_interval = 60 - self._battery_level = None - - async def async_added_to_hass(self) -> None: - """Run when about to be added to hass.""" - await super().async_added_to_hass() - if self._battery_cluster_handler: - self.async_accept_signal( - self._battery_cluster_handler, - SIGNAL_ATTR_UPDATED, - self.async_battery_percentage_remaining_updated, - ) - - async def async_update(self) -> None: - """Handle polling.""" - if self.zha_device.last_seen is None: - self._connected = False - else: - difference = time.time() - self.zha_device.last_seen - if difference > self._keepalive_interval: - self._connected = False - else: - self._connected = True - @property - def is_connected(self): + def is_connected(self) -> bool: """Return true if the device is connected to the network.""" - return self._connected + return self.entity_data.entity.is_connected @property def source_type(self) -> SourceType: """Return the source type, eg gps or router, of the device.""" return SourceType.ROUTER - @callback - def async_battery_percentage_remaining_updated(self, attr_id, attr_name, value): - """Handle tracking.""" - if attr_name != "battery_percentage_remaining": - return - self.debug("battery_percentage_remaining updated: %s", value) - self._connected = True - self._battery_level = Battery.formatter(value) - self.async_write_ha_state() - @property - def battery_level(self): + def battery_level(self) -> int | None: """Return the battery level of the device. Percentage from 0-100. """ - return self._battery_level + return self.entity_data.entity.battery_level - @property # type: ignore[misc] - def device_info( - self, - ) -> DeviceInfo: + @property # type: ignore[explicit-override, misc] + def device_info(self) -> DeviceInfo: """Return device info.""" # We opt ZHA device tracker back into overriding this method because # it doesn't track IP-based devices. - # Call Super because ScannerEntity overrode it. - # mypy doesn't know about fget: https://github.com/python/mypy/issues/6185 - return ZhaEntity.device_info.fget(self) # type: ignore[attr-defined] + return ZHAEntity.device_info.__get__(self) @property def unique_id(self) -> str: """Return unique ID.""" # Call Super because ScannerEntity overrode it. - # mypy doesn't know about fget: https://github.com/python/mypy/issues/6185 - return ZhaEntity.unique_id.fget(self) # type: ignore[attr-defined] + return ZHAEntity.unique_id.__get__(self) diff --git a/homeassistant/components/zha/device_trigger.py b/homeassistant/components/zha/device_trigger.py index a2ae734b8fc..a134d2aa59b 100644 --- a/homeassistant/components/zha/device_trigger.py +++ b/homeassistant/components/zha/device_trigger.py @@ -1,6 +1,7 @@ """Provides device automations for ZHA devices that emit events.""" import voluptuous as vol +from zha.application.const import ZHA_EVENT from homeassistant.components.device_automation import DEVICE_TRIGGER_BASE_SCHEMA from homeassistant.components.device_automation.exceptions import ( @@ -13,9 +14,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.trigger import TriggerActionType, TriggerInfo from homeassistant.helpers.typing import ConfigType -from . import DOMAIN as ZHA_DOMAIN -from .core.const import ZHA_EVENT -from .core.helpers import async_get_zha_device, get_zha_data +from .const import DOMAIN as ZHA_DOMAIN +from .helpers import async_get_zha_device_proxy, get_zha_data CONF_SUBTYPE = "subtype" DEVICE = "device" @@ -31,7 +31,7 @@ def _get_device_trigger_data(hass: HomeAssistant, device_id: str) -> tuple[str, # First, try checking to see if the device itself is accessible try: - zha_device = async_get_zha_device(hass, device_id) + zha_device = async_get_zha_device_proxy(hass, device_id).device except ValueError: pass else: diff --git a/homeassistant/components/zha/diagnostics.py b/homeassistant/components/zha/diagnostics.py index fff816777c0..bc4738d032a 100644 --- a/homeassistant/components/zha/diagnostics.py +++ b/homeassistant/components/zha/diagnostics.py @@ -6,6 +6,18 @@ import dataclasses from importlib.metadata import version from typing import Any +from zha.application.const import ( + ATTR_ATTRIBUTE_NAME, + ATTR_DEVICE_TYPE, + ATTR_IEEE, + ATTR_IN_CLUSTERS, + ATTR_OUT_CLUSTERS, + ATTR_PROFILE_ID, + ATTR_VALUE, + UNKNOWN, +) +from zha.application.gateway import Gateway +from zha.zigbee.device import Device from zigpy.config import CONF_NWK_EXTENDED_PAN_ID from zigpy.profiles import PROFILES from zigpy.types import Channels @@ -17,20 +29,13 @@ from homeassistant.const import CONF_ID, CONF_NAME, CONF_UNIQUE_ID from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr -from .core.const import ( - ATTR_ATTRIBUTE_NAME, - ATTR_DEVICE_TYPE, - ATTR_IEEE, - ATTR_IN_CLUSTERS, - ATTR_OUT_CLUSTERS, - ATTR_PROFILE_ID, - ATTR_VALUE, - CONF_ALARM_MASTER_CODE, - UNKNOWN, +from .const import CONF_ALARM_MASTER_CODE +from .helpers import ( + ZHADeviceProxy, + async_get_zha_device_proxy, + get_zha_data, + get_zha_gateway, ) -from .core.device import ZHADevice -from .core.gateway import ZHAGateway -from .core.helpers import async_get_zha_device, get_zha_data, get_zha_gateway KEYS_TO_REDACT = { ATTR_IEEE, @@ -65,7 +70,7 @@ async def async_get_config_entry_diagnostics( ) -> dict[str, Any]: """Return diagnostics for a config entry.""" zha_data = get_zha_data(hass) - gateway: ZHAGateway = get_zha_gateway(hass) + gateway: Gateway = get_zha_gateway(hass) app = gateway.application_controller energy_scan = await app.energy_scan( @@ -88,6 +93,7 @@ async def async_get_config_entry_diagnostics( "zigpy_znp": version("zigpy_znp"), "zigpy_zigate": version("zigpy-zigate"), "zhaquirks": version("zha-quirks"), + "zha": version("zha"), }, "devices": [ { @@ -106,13 +112,15 @@ async def async_get_device_diagnostics( hass: HomeAssistant, config_entry: ConfigEntry, device: dr.DeviceEntry ) -> dict[str, Any]: """Return diagnostics for a device.""" - zha_device: ZHADevice = async_get_zha_device(hass, device.id) - device_info: dict[str, Any] = zha_device.zha_device_info - device_info[CLUSTER_DETAILS] = get_endpoint_cluster_attr_data(zha_device) + zha_device_proxy: ZHADeviceProxy = async_get_zha_device_proxy(hass, device.id) + device_info: dict[str, Any] = zha_device_proxy.zha_device_info + device_info[CLUSTER_DETAILS] = get_endpoint_cluster_attr_data( + zha_device_proxy.device + ) return async_redact_data(device_info, KEYS_TO_REDACT) -def get_endpoint_cluster_attr_data(zha_device: ZHADevice) -> dict: +def get_endpoint_cluster_attr_data(zha_device: Device) -> dict: """Return endpoint cluster attribute data.""" cluster_details = {} for ep_id, endpoint in zha_device.device.endpoints.items(): diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index f10e377dc46..6db0ffad964 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -6,84 +6,70 @@ import asyncio from collections.abc import Callable import functools import logging -from typing import TYPE_CHECKING, Any, Self +from typing import Any -from zigpy.quirks.v2 import EntityMetadata, EntityType +from zha.mixins import LogMixin -from homeassistant.const import ATTR_NAME, EntityCategory -from homeassistant.core import CALLBACK_TYPE, Event, EventStateChangedData, callback -from homeassistant.helpers import entity -from homeassistant.helpers.debounce import Debouncer +from homeassistant.const import ATTR_MANUFACTURER, ATTR_MODEL, ATTR_NAME, EntityCategory +from homeassistant.core import State, callback from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE, DeviceInfo -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) -from homeassistant.helpers.event import async_track_state_change_event +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity from homeassistant.helpers.restore_state import RestoreEntity -from .core.const import ( - ATTR_MANUFACTURER, - ATTR_MODEL, - DOMAIN, - SIGNAL_GROUP_ENTITY_REMOVED, - SIGNAL_GROUP_MEMBERSHIP_CHANGE, - SIGNAL_REMOVE, -) -from .core.helpers import LogMixin, get_zha_gateway - -if TYPE_CHECKING: - from .core.cluster_handlers import ClusterHandler - from .core.device import ZHADevice +from .const import DOMAIN +from .helpers import SIGNAL_REMOVE_ENTITIES, EntityData, convert_zha_error_to_ha_error _LOGGER = logging.getLogger(__name__) -ENTITY_SUFFIX = "entity_suffix" -DEFAULT_UPDATE_GROUP_FROM_CHILD_DELAY = 0.5 - -class BaseZhaEntity(LogMixin, entity.Entity): - """A base class for ZHA entities.""" - - _unique_id_suffix: str | None = None - """suffix to add to the unique_id of the entity. Used for multi - entities using the same cluster handler/cluster id for the entity.""" +class ZHAEntity(LogMixin, RestoreEntity, Entity): + """ZHA eitity.""" _attr_has_entity_name = True _attr_should_poll = False + remove_future: asyncio.Future[Any] - def __init__(self, unique_id: str, zha_device: ZHADevice, **kwargs: Any) -> None: + def __init__(self, entity_data: EntityData, *args, **kwargs) -> None: """Init ZHA entity.""" - self._unique_id: str = unique_id - if self._unique_id_suffix: - self._unique_id += f"-{self._unique_id_suffix}" - self._state: Any = None - self._extra_state_attributes: dict[str, Any] = {} - self._zha_device = zha_device + super().__init__(*args, **kwargs) + self.entity_data: EntityData = entity_data self._unsubs: list[Callable[[], None]] = [] - @property - def unique_id(self) -> str: - """Return a unique ID.""" - return self._unique_id + if self.entity_data.entity.icon is not None: + # Only custom quirks will realistically set an icon + self._attr_icon = self.entity_data.entity.icon + + meta = self.entity_data.entity.info_object + self._attr_unique_id = meta.unique_id + + if meta.translation_key is not None: + self._attr_translation_key = meta.translation_key + elif meta.fallback_name is not None: + # Only custom quirks will create entities with just a fallback name! + # + # This is to allow local development and to register niche devices, since + # their translation_key will probably never be added to `zha/strings.json`. + self._attr_name = meta.fallback_name + + if meta.entity_category is not None: + self._attr_entity_category = EntityCategory(meta.entity_category) + + self._attr_entity_registry_enabled_default = ( + meta.entity_registry_enabled_default + ) @property - def zha_device(self) -> ZHADevice: - """Return the ZHA device this entity is attached to.""" - return self._zha_device - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return device specific state attributes.""" - return self._extra_state_attributes + def available(self) -> bool: + """Return entity availability.""" + return self.entity_data.device_proxy.device.available @property def device_info(self) -> DeviceInfo: """Return a device description for device registry.""" - zha_device_info = self._zha_device.device_info + zha_device_info = self.entity_data.device_proxy.device_info ieee = zha_device_info["ieee"] - - zha_gateway = get_zha_gateway(self.hass) + zha_gateway = self.entity_data.device_proxy.gateway_proxy.gateway return DeviceInfo( connections={(CONNECTION_ZIGBEE, ieee)}, @@ -95,265 +81,67 @@ class BaseZhaEntity(LogMixin, entity.Entity): ) @callback - def async_state_changed(self) -> None: + def _handle_entity_events(self, event: Any) -> None: """Entity state changed.""" + self.debug("Handling event from entity: %s", event) self.async_write_ha_state() - @callback - def async_update_state_attribute(self, key: str, value: Any) -> None: - """Update a single device state attribute.""" - self._extra_state_attributes.update({key: value}) - self.async_write_ha_state() + async def async_added_to_hass(self) -> None: + """Run when about to be added to hass.""" + self.remove_future = self.hass.loop.create_future() + self._unsubs.append( + self.entity_data.entity.on_all_events(self._handle_entity_events) + ) + remove_signal = ( + f"{SIGNAL_REMOVE_ENTITIES}_group_{self.entity_data.group_proxy.group.group_id}" + if self.entity_data.is_group_entity + and self.entity_data.group_proxy is not None + else f"{SIGNAL_REMOVE_ENTITIES}_{self.entity_data.device_proxy.device.ieee}" + ) + self._unsubs.append( + async_dispatcher_connect( + self.hass, + remove_signal, + functools.partial(self.async_remove, force_remove=True), + ) + ) + self.entity_data.device_proxy.gateway_proxy.register_entity_reference( + self.entity_id, + self.entity_data, + self.device_info, + self.remove_future, + ) + + if (state := await self.async_get_last_state()) is None: + return + + self.restore_external_state_attributes(state) @callback - def async_set_state(self, attr_id: int, attr_name: str, value: Any) -> None: - """Set the entity state.""" + def restore_external_state_attributes(self, state: State) -> None: + """Restore ephemeral external state from Home Assistant back into ZHA.""" + + # Some operations rely on extra state that is not maintained in the ZCL + # attribute cache. Until ZHA is able to maintain its own persistent state (or + # provides a more generic hook to utilize HA to do this), we directly restore + # them. async def async_will_remove_from_hass(self) -> None: """Disconnect entity object when removed.""" for unsub in self._unsubs[:]: unsub() self._unsubs.remove(unsub) + await super().async_will_remove_from_hass() + self.remove_future.set_result(True) - @callback - def async_accept_signal( - self, - cluster_handler: ClusterHandler | None, - signal: str, - func: Callable[..., Any], - signal_override=False, - ): - """Accept a signal from a cluster handler.""" - unsub = None - if signal_override: - unsub = async_dispatcher_connect(self.hass, signal, func) - else: - assert cluster_handler - unsub = async_dispatcher_connect( - self.hass, f"{cluster_handler.unique_id}_{signal}", func - ) - self._unsubs.append(unsub) + @convert_zha_error_to_ha_error + async def async_update(self) -> None: + """Update the entity.""" + await self.entity_data.entity.async_update() + self.async_write_ha_state() def log(self, level: int, msg: str, *args, **kwargs): """Log a message.""" msg = f"%s: {msg}" args = (self.entity_id, *args) _LOGGER.log(level, msg, *args, **kwargs) - - -class ZhaEntity(BaseZhaEntity, RestoreEntity): - """A base class for non group ZHA entities.""" - - remove_future: asyncio.Future[Any] - - def __init__( - self, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> None: - """Init ZHA entity.""" - super().__init__(unique_id, zha_device, **kwargs) - - self.cluster_handlers: dict[str, ClusterHandler] = {} - for cluster_handler in cluster_handlers: - self.cluster_handlers[cluster_handler.name] = cluster_handler - - @classmethod - def create_entity( - cls, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> Self | None: - """Entity Factory. - - Return entity if it is a supported configuration, otherwise return None - """ - return cls(unique_id, zha_device, cluster_handlers, **kwargs) - - def _init_from_quirks_metadata(self, entity_metadata: EntityMetadata) -> None: - """Init this entity from the quirks metadata.""" - if entity_metadata.initially_disabled: - self._attr_entity_registry_enabled_default = False - - has_device_class = hasattr(entity_metadata, "device_class") - has_attribute_name = hasattr(entity_metadata, "attribute_name") - has_command_name = hasattr(entity_metadata, "command_name") - if not has_device_class or ( - has_device_class and entity_metadata.device_class is None - ): - if entity_metadata.translation_key: - self._attr_translation_key = entity_metadata.translation_key - elif has_attribute_name: - self._attr_translation_key = entity_metadata.attribute_name - elif has_command_name: - self._attr_translation_key = entity_metadata.command_name - if has_attribute_name: - self._unique_id_suffix = entity_metadata.attribute_name - elif has_command_name: - self._unique_id_suffix = entity_metadata.command_name - if entity_metadata.entity_type is EntityType.CONFIG: - self._attr_entity_category = EntityCategory.CONFIG - elif entity_metadata.entity_type is EntityType.DIAGNOSTIC: - self._attr_entity_category = EntityCategory.DIAGNOSTIC - else: - self._attr_entity_category = None - - @property - def available(self) -> bool: - """Return entity availability.""" - return self._zha_device.available - - async def async_added_to_hass(self) -> None: - """Run when about to be added to hass.""" - self.remove_future = self.hass.loop.create_future() - self.async_accept_signal( - None, - f"{SIGNAL_REMOVE}_{self.zha_device.ieee}", - functools.partial(self.async_remove, force_remove=True), - signal_override=True, - ) - - if last_state := await self.async_get_last_state(): - self.async_restore_last_state(last_state) - - self.async_accept_signal( - None, - f"{self.zha_device.available_signal}_entity", - self.async_state_changed, - signal_override=True, - ) - self._zha_device.gateway.register_entity_reference( - self._zha_device.ieee, - self.entity_id, - self._zha_device, - self.cluster_handlers, - self.device_info, - self.remove_future, - ) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect entity object when removed.""" - await super().async_will_remove_from_hass() - self.zha_device.gateway.remove_entity_reference(self) - self.remove_future.set_result(True) - - @callback - def async_restore_last_state(self, last_state) -> None: - """Restore previous state.""" - - async def async_update(self) -> None: - """Retrieve latest state.""" - tasks = [ - cluster_handler.async_update() - for cluster_handler in self.cluster_handlers.values() - if hasattr(cluster_handler, "async_update") - ] - if tasks: - await asyncio.gather(*tasks) - - -class ZhaGroupEntity(BaseZhaEntity): - """A base class for ZHA group entities.""" - - # The group name is set in the initializer - _attr_name: str - - def __init__( - self, - entity_ids: list[str], - unique_id: str, - group_id: int, - zha_device: ZHADevice, - **kwargs: Any, - ) -> None: - """Initialize a ZHA group.""" - super().__init__(unique_id, zha_device, **kwargs) - self._available = False - self._group = zha_device.gateway.groups.get(group_id) - self._group_id: int = group_id - self._entity_ids: list[str] = entity_ids - self._async_unsub_state_changed: CALLBACK_TYPE | None = None - self._handled_group_membership = False - self._change_listener_debouncer: Debouncer | None = None - self._update_group_from_child_delay = DEFAULT_UPDATE_GROUP_FROM_CHILD_DELAY - - self._attr_name = self._group.name - - @property - def available(self) -> bool: - """Return entity availability.""" - return self._available - - @classmethod - def create_entity( - cls, - entity_ids: list[str], - unique_id: str, - group_id: int, - zha_device: ZHADevice, - **kwargs: Any, - ) -> Self | None: - """Group Entity Factory. - - Return entity if it is a supported configuration, otherwise return None - """ - return cls(entity_ids, unique_id, group_id, zha_device, **kwargs) - - async def _handle_group_membership_changed(self): - """Handle group membership changed.""" - # Make sure we don't call remove twice as members are removed - if self._handled_group_membership: - return - - self._handled_group_membership = True - await self.async_remove(force_remove=True) - if len(self._group.members) >= 2: - async_dispatcher_send( - self.hass, SIGNAL_GROUP_ENTITY_REMOVED, self._group_id - ) - - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - await super().async_added_to_hass() - await self.async_update() - - self.async_accept_signal( - None, - f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_0x{self._group_id:04x}", - self._handle_group_membership_changed, - signal_override=True, - ) - - if self._change_listener_debouncer is None: - self._change_listener_debouncer = Debouncer( - self.hass, - _LOGGER, - cooldown=self._update_group_from_child_delay, - immediate=False, - function=functools.partial(self.async_update_ha_state, True), - ) - self.async_on_remove(self._change_listener_debouncer.async_cancel) - self._async_unsub_state_changed = async_track_state_change_event( - self.hass, self._entity_ids, self.async_state_changed_listener - ) - - @callback - def async_state_changed_listener(self, event: Event[EventStateChangedData]) -> None: - """Handle child updates.""" - # Delay to ensure that we get updates from all members before updating the group - assert self._change_listener_debouncer - self._change_listener_debouncer.async_schedule_call() - - async def async_will_remove_from_hass(self) -> None: - """Handle removal from Home Assistant.""" - await super().async_will_remove_from_hass() - if self._async_unsub_state_changed is not None: - self._async_unsub_state_changed() - self._async_unsub_state_changed = None - - async def async_update(self) -> None: - """Update the state of the group entity.""" diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 3677befb76e..f9bd9535a07 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -2,54 +2,23 @@ from __future__ import annotations -from abc import abstractmethod import functools -import math from typing import Any -from zigpy.zcl.clusters import hvac - -from homeassistant.components.fan import ( - ATTR_PERCENTAGE, - ATTR_PRESET_MODE, - FanEntity, - FanEntityFeature, -) +from homeassistant.components.fan import FanEntity, FanEntityFeature from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_UNAVAILABLE, Platform -from homeassistant.core import HomeAssistant, State, callback +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.util.percentage import ( - percentage_to_ranged_value, - ranged_value_to_percentage, + +from .entity import ZHAEntity +from .helpers import ( + SIGNAL_ADD_ENTITIES, + async_add_entities as zha_async_add_entities, + convert_zha_error_to_ha_error, + get_zha_data, ) -from homeassistant.util.scaling import int_states_in_range - -from .core import discovery -from .core.cluster_handlers import wrap_zigpy_exceptions -from .core.const import CLUSTER_HANDLER_FAN, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED -from .core.helpers import get_zha_data -from .core.registries import ZHA_ENTITIES -from .entity import ZhaEntity, ZhaGroupEntity - -# Additional speeds in zigbee's ZCL -# Spec is unclear as to what this value means. On King Of Fans HBUniversal -# receiver, this means Very High. -PRESET_MODE_ON = "on" -# The fan speed is self-regulated -PRESET_MODE_AUTO = "auto" -# When the heated/cooled space is occupied, the fan is always on -PRESET_MODE_SMART = "smart" - -SPEED_RANGE = (1, 3) # off is not included -PRESET_MODES_TO_NAME = {4: PRESET_MODE_ON, 5: PRESET_MODE_AUTO, 6: PRESET_MODE_SMART} - -DEFAULT_ON_PERCENTAGE = 50 - -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.FAN) -GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.FAN) -MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.FAN) async def async_setup_entry( @@ -65,50 +34,44 @@ async def async_setup_entry( hass, SIGNAL_ADD_ENTITIES, functools.partial( - discovery.async_add_entities, - async_add_entities, - entities_to_create, + zha_async_add_entities, async_add_entities, ZhaFan, entities_to_create ), ) config_entry.async_on_unload(unsub) -class BaseFan(FanEntity): - """Base representation of a ZHA fan.""" +class ZhaFan(FanEntity, ZHAEntity): + """Representation of a ZHA fan.""" _attr_supported_features = FanEntityFeature.SET_SPEED _attr_translation_key: str = "fan" + @property + def preset_mode(self) -> str | None: + """Return the current preset mode.""" + return self.entity_data.entity.preset_mode + @property def preset_modes(self) -> list[str]: """Return the available preset modes.""" - return list(self.preset_modes_to_name.values()) - - @property - def preset_modes_to_name(self) -> dict[int, str]: - """Return a dict from preset mode to name.""" - return PRESET_MODES_TO_NAME - - @property - def preset_name_to_mode(self) -> dict[str, int]: - """Return a dict from preset name to mode.""" - return {v: k for k, v in self.preset_modes_to_name.items()} + return self.entity_data.entity.preset_modes @property def default_on_percentage(self) -> int: """Return the default on percentage.""" - return DEFAULT_ON_PERCENTAGE + return self.entity_data.entity.default_on_percentage @property def speed_range(self) -> tuple[int, int]: """Return the range of speeds the fan supports. Off is not included.""" - return SPEED_RANGE + return self.entity_data.entity.speed_range @property def speed_count(self) -> int: """Return the number of speeds the fan supports.""" - return int_states_in_range(self.speed_range) + return self.entity_data.entity.speed_count + @convert_zha_error_to_ha_error async def async_turn_on( self, percentage: int | None = None, @@ -116,201 +79,30 @@ class BaseFan(FanEntity): **kwargs: Any, ) -> None: """Turn the entity on.""" - if percentage is None: - percentage = self.default_on_percentage - await self.async_set_percentage(percentage) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the entity off.""" - await self.async_set_percentage(0) - - async def async_set_percentage(self, percentage: int) -> None: - """Set the speed percentage of the fan.""" - fan_mode = math.ceil(percentage_to_ranged_value(self.speed_range, percentage)) - await self._async_set_fan_mode(fan_mode) - - async def async_set_preset_mode(self, preset_mode: str) -> None: - """Set the preset mode for the fan.""" - await self._async_set_fan_mode(self.preset_name_to_mode[preset_mode]) - - @abstractmethod - async def _async_set_fan_mode(self, fan_mode: int) -> None: - """Set the fan mode for the fan.""" - - @callback - def async_set_state(self, attr_id, attr_name, value): - """Handle state update from cluster handler.""" - - -@STRICT_MATCH(cluster_handler_names=CLUSTER_HANDLER_FAN) -class ZhaFan(BaseFan, ZhaEntity): - """Representation of a ZHA fan.""" - - def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs): - """Init this sensor.""" - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - self._fan_cluster_handler = self.cluster_handlers.get(CLUSTER_HANDLER_FAN) - - async def async_added_to_hass(self) -> None: - """Run when about to be added to hass.""" - await super().async_added_to_hass() - self.async_accept_signal( - self._fan_cluster_handler, SIGNAL_ATTR_UPDATED, self.async_set_state + await self.entity_data.entity.async_turn_on( + percentage=percentage, preset_mode=preset_mode ) - - @property - def percentage(self) -> int | None: - """Return the current speed percentage.""" - if ( - self._fan_cluster_handler.fan_mode is None - or self._fan_cluster_handler.fan_mode > self.speed_range[1] - ): - return None - if self._fan_cluster_handler.fan_mode == 0: - return 0 - return ranged_value_to_percentage( - self.speed_range, self._fan_cluster_handler.fan_mode - ) - - @property - def preset_mode(self) -> str | None: - """Return the current preset mode.""" - return self.preset_modes_to_name.get(self._fan_cluster_handler.fan_mode) - - @callback - def async_set_state(self, attr_id, attr_name, value): - """Handle state update from cluster handler.""" self.async_write_ha_state() - async def _async_set_fan_mode(self, fan_mode: int) -> None: - """Set the fan mode for the fan.""" - await self._fan_cluster_handler.async_set_speed(fan_mode) - self.async_set_state(0, "fan_mode", fan_mode) + @convert_zha_error_to_ha_error + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the entity off.""" + await self.entity_data.entity.async_turn_off() + self.async_write_ha_state() + @convert_zha_error_to_ha_error + async def async_set_percentage(self, percentage: int) -> None: + """Set the speed percentage of the fan.""" + await self.entity_data.entity.async_set_percentage(percentage=percentage) + self.async_write_ha_state() -@GROUP_MATCH() -class FanGroup(BaseFan, ZhaGroupEntity): - """Representation of a fan group.""" - - _attr_translation_key: str = "fan_group" - - def __init__( - self, entity_ids: list[str], unique_id: str, group_id: int, zha_device, **kwargs - ) -> None: - """Initialize a fan group.""" - super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs) - self._available: bool = False - group = self.zha_device.gateway.get_group(self._group_id) - self._fan_cluster_handler = group.endpoint[hvac.Fan.cluster_id] - self._percentage = None - self._preset_mode = None + @convert_zha_error_to_ha_error + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set the preset mode for the fan.""" + await self.entity_data.entity.async_set_preset_mode(preset_mode=preset_mode) + self.async_write_ha_state() @property def percentage(self) -> int | None: """Return the current speed percentage.""" - return self._percentage - - @property - def preset_mode(self) -> str | None: - """Return the current preset mode.""" - return self._preset_mode - - async def _async_set_fan_mode(self, fan_mode: int) -> None: - """Set the fan mode for the group.""" - - with wrap_zigpy_exceptions(): - await self._fan_cluster_handler.write_attributes({"fan_mode": fan_mode}) - - self.async_set_state(0, "fan_mode", fan_mode) - - async def async_update(self) -> None: - """Attempt to retrieve on off state from the fan.""" - all_states = [self.hass.states.get(x) for x in self._entity_ids] - states: list[State] = list(filter(None, all_states)) - percentage_states: list[State] = [ - state for state in states if state.attributes.get(ATTR_PERCENTAGE) - ] - preset_mode_states: list[State] = [ - state for state in states if state.attributes.get(ATTR_PRESET_MODE) - ] - self._available = any(state.state != STATE_UNAVAILABLE for state in states) - - if percentage_states: - self._percentage = percentage_states[0].attributes[ATTR_PERCENTAGE] - self._preset_mode = None - elif preset_mode_states: - self._preset_mode = preset_mode_states[0].attributes[ATTR_PRESET_MODE] - self._percentage = None - else: - self._percentage = None - self._preset_mode = None - - async def async_added_to_hass(self) -> None: - """Run when about to be added to hass.""" - await self.async_update() - await super().async_added_to_hass() - - -IKEA_SPEED_RANGE = (1, 10) # off is not included -IKEA_PRESET_MODES_TO_NAME = { - 1: PRESET_MODE_AUTO, - 2: "Speed 1", - 3: "Speed 1.5", - 4: "Speed 2", - 5: "Speed 2.5", - 6: "Speed 3", - 7: "Speed 3.5", - 8: "Speed 4", - 9: "Speed 4.5", - 10: "Speed 5", -} - - -@MULTI_MATCH( - cluster_handler_names="ikea_airpurifier", - models={"STARKVIND Air purifier", "STARKVIND Air purifier table"}, -) -class IkeaFan(ZhaFan): - """Representation of an Ikea fan.""" - - def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs) -> None: - """Init this sensor.""" - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - self._fan_cluster_handler = self.cluster_handlers.get("ikea_airpurifier") - - @property - def preset_modes_to_name(self) -> dict[int, str]: - """Return a dict from preset mode to name.""" - return IKEA_PRESET_MODES_TO_NAME - - @property - def speed_range(self) -> tuple[int, int]: - """Return the range of speeds the fan supports. Off is not included.""" - return IKEA_SPEED_RANGE - - @property - def default_on_percentage(self) -> int: - """Return the default on percentage.""" - return int( - (100 / self.speed_count) * self.preset_name_to_mode[PRESET_MODE_AUTO] - ) - - -@MULTI_MATCH( - cluster_handler_names=CLUSTER_HANDLER_FAN, - models={"HBUniversalCFRemote", "HDC52EastwindFan"}, -) -class KofFan(ZhaFan): - """Representation of a fan made by King Of Fans.""" - - _attr_supported_features = FanEntityFeature.SET_SPEED | FanEntityFeature.PRESET_MODE - - @property - def speed_range(self) -> tuple[int, int]: - """Return the range of speeds the fan supports. Off is not included.""" - return (1, 4) - - @property - def preset_modes_to_name(self) -> dict[int, str]: - """Return a dict from preset mode to name.""" - return {6: PRESET_MODE_SMART} + return self.entity_data.entity.percentage diff --git a/homeassistant/components/zha/helpers.py b/homeassistant/components/zha/helpers.py new file mode 100644 index 00000000000..5e84eca36ce --- /dev/null +++ b/homeassistant/components/zha/helpers.py @@ -0,0 +1,1282 @@ +"""Helper functions for the ZHA integration.""" + +from __future__ import annotations + +import asyncio +import collections +from collections.abc import Awaitable, Callable, Coroutine, Mapping +import copy +import dataclasses +import enum +import functools +import itertools +import logging +import re +import time +from types import MappingProxyType +from typing import TYPE_CHECKING, Any, Concatenate, NamedTuple, ParamSpec, TypeVar, cast + +import voluptuous as vol +from zha.application.const import ( + ATTR_CLUSTER_ID, + ATTR_DEVICE_IEEE, + ATTR_TYPE, + ATTR_UNIQUE_ID, + CLUSTER_TYPE_IN, + CLUSTER_TYPE_OUT, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY, + CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS, + UNKNOWN_MANUFACTURER, + UNKNOWN_MODEL, + ZHA_CLUSTER_HANDLER_CFG_DONE, + ZHA_CLUSTER_HANDLER_MSG, + ZHA_CLUSTER_HANDLER_MSG_BIND, + ZHA_CLUSTER_HANDLER_MSG_CFG_RPT, + ZHA_CLUSTER_HANDLER_MSG_DATA, + ZHA_EVENT, + ZHA_GW_MSG, + ZHA_GW_MSG_DEVICE_FULL_INIT, + ZHA_GW_MSG_DEVICE_INFO, + ZHA_GW_MSG_DEVICE_JOINED, + ZHA_GW_MSG_DEVICE_REMOVED, + ZHA_GW_MSG_GROUP_ADDED, + ZHA_GW_MSG_GROUP_INFO, + ZHA_GW_MSG_GROUP_MEMBER_ADDED, + ZHA_GW_MSG_GROUP_MEMBER_REMOVED, + ZHA_GW_MSG_GROUP_REMOVED, + ZHA_GW_MSG_RAW_INIT, + RadioType, +) +from zha.application.gateway import ( + ConnectionLostEvent, + DeviceFullInitEvent, + DeviceJoinedEvent, + DeviceLeftEvent, + DeviceRemovedEvent, + Gateway, + GroupEvent, + RawDeviceInitializedEvent, +) +from zha.application.helpers import ( + AlarmControlPanelOptions, + CoordinatorConfiguration, + DeviceOptions, + DeviceOverridesConfiguration, + LightOptions, + QuirksConfiguration, + ZHAConfiguration, + ZHAData, +) +from zha.application.platforms import GroupEntity, PlatformEntity +from zha.event import EventBase +from zha.exceptions import ZHAException +from zha.mixins import LogMixin +from zha.zigbee.cluster_handlers import ClusterBindEvent, ClusterConfigureReportingEvent +from zha.zigbee.device import ClusterHandlerConfigurationComplete, Device, ZHAEvent +from zha.zigbee.group import Group, GroupMember +from zigpy.config import ( + CONF_DATABASE, + CONF_DEVICE, + CONF_DEVICE_PATH, + CONF_NWK, + CONF_NWK_CHANNEL, +) +import zigpy.exceptions +from zigpy.profiles import PROFILES +import zigpy.types +from zigpy.types import EUI64 +import zigpy.util +import zigpy.zcl +from zigpy.zcl.foundation import CommandSchema + +from homeassistant import __path__ as HOMEASSISTANT_PATH +from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon import ( + is_multiprotocol_url, +) +from homeassistant.components.system_log import LogEntry +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_AREA_ID, + ATTR_DEVICE_ID, + ATTR_ENTITY_ID, + ATTR_MODEL, + ATTR_NAME, + Platform, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import ( + config_validation as cv, + device_registry as dr, + entity_registry as er, +) +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType + +from .const import ( + ATTR_ACTIVE_COORDINATOR, + ATTR_ATTRIBUTES, + ATTR_AVAILABLE, + ATTR_CLUSTER_NAME, + ATTR_DEVICE_TYPE, + ATTR_ENDPOINT_NAMES, + ATTR_IEEE, + ATTR_LAST_SEEN, + ATTR_LQI, + ATTR_MANUFACTURER, + ATTR_MANUFACTURER_CODE, + ATTR_NEIGHBORS, + ATTR_NWK, + ATTR_POWER_SOURCE, + ATTR_QUIRK_APPLIED, + ATTR_QUIRK_CLASS, + ATTR_QUIRK_ID, + ATTR_ROUTES, + ATTR_RSSI, + ATTR_SIGNATURE, + ATTR_SUCCESS, + CONF_ALARM_ARM_REQUIRES_CODE, + CONF_ALARM_FAILED_TRIES, + CONF_ALARM_MASTER_CODE, + CONF_ALWAYS_PREFER_XY_COLOR_MODE, + CONF_BAUDRATE, + CONF_CONSIDER_UNAVAILABLE_BATTERY, + CONF_CONSIDER_UNAVAILABLE_MAINS, + CONF_CUSTOM_QUIRKS_PATH, + CONF_DEFAULT_LIGHT_TRANSITION, + CONF_DEVICE_CONFIG, + CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, + CONF_ENABLE_IDENTIFY_ON_JOIN, + CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, + CONF_ENABLE_QUIRKS, + CONF_FLOW_CONTROL, + CONF_GROUP_MEMBERS_ASSUME_STATE, + CONF_RADIO_TYPE, + CONF_ZIGPY, + CUSTOM_CONFIGURATION, + DATA_ZHA, + DEFAULT_DATABASE_NAME, + DEVICE_PAIRING_STATUS, + DOMAIN, + ZHA_ALARM_OPTIONS, + ZHA_OPTIONS, +) + +if TYPE_CHECKING: + from logging import Filter, LogRecord + + from .entity import ZHAEntity + from .update import ZHAFirmwareUpdateCoordinator + + _LogFilterType = Filter | Callable[[LogRecord], bool] + +_P = ParamSpec("_P") +_EntityT = TypeVar("_EntityT", bound="ZHAEntity") + +_LOGGER = logging.getLogger(__name__) + +DEBUG_COMP_BELLOWS = "bellows" +DEBUG_COMP_ZHA = "homeassistant.components.zha" +DEBUG_LIB_ZHA = "zha" +DEBUG_COMP_ZIGPY = "zigpy" +DEBUG_COMP_ZIGPY_ZNP = "zigpy_znp" +DEBUG_COMP_ZIGPY_DECONZ = "zigpy_deconz" +DEBUG_COMP_ZIGPY_XBEE = "zigpy_xbee" +DEBUG_COMP_ZIGPY_ZIGATE = "zigpy_zigate" +DEBUG_LEVEL_CURRENT = "current" +DEBUG_LEVEL_ORIGINAL = "original" +DEBUG_LEVELS = { + DEBUG_COMP_BELLOWS: logging.DEBUG, + DEBUG_COMP_ZHA: logging.DEBUG, + DEBUG_COMP_ZIGPY: logging.DEBUG, + DEBUG_COMP_ZIGPY_ZNP: logging.DEBUG, + DEBUG_COMP_ZIGPY_DECONZ: logging.DEBUG, + DEBUG_COMP_ZIGPY_XBEE: logging.DEBUG, + DEBUG_COMP_ZIGPY_ZIGATE: logging.DEBUG, + DEBUG_LIB_ZHA: logging.DEBUG, +} +DEBUG_RELAY_LOGGERS = [DEBUG_COMP_ZHA, DEBUG_COMP_ZIGPY, DEBUG_LIB_ZHA] +ZHA_GW_MSG_LOG_ENTRY = "log_entry" +ZHA_GW_MSG_LOG_OUTPUT = "log_output" +SIGNAL_REMOVE_ENTITIES = "zha_remove_entities" +GROUP_ENTITY_DOMAINS = [Platform.LIGHT, Platform.SWITCH, Platform.FAN] +SIGNAL_ADD_ENTITIES = "zha_add_entities" +ENTITIES = "entities" + +RX_ON_WHEN_IDLE = "rx_on_when_idle" +RELATIONSHIP = "relationship" +EXTENDED_PAN_ID = "extended_pan_id" +PERMIT_JOINING = "permit_joining" +DEPTH = "depth" + +DEST_NWK = "dest_nwk" +ROUTE_STATUS = "route_status" +MEMORY_CONSTRAINED = "memory_constrained" +MANY_TO_ONE = "many_to_one" +ROUTE_RECORD_REQUIRED = "route_record_required" +NEXT_HOP = "next_hop" + +USER_GIVEN_NAME = "user_given_name" +DEVICE_REG_ID = "device_reg_id" + + +class GroupEntityReference(NamedTuple): + """Reference to a group entity.""" + + name: str | None + original_name: str | None + entity_id: str + + +class ZHAGroupProxy(LogMixin): + """Proxy class to interact with the ZHA group instances.""" + + def __init__(self, group: Group, gateway_proxy: ZHAGatewayProxy) -> None: + """Initialize the gateway proxy.""" + self.group: Group = group + self.gateway_proxy: ZHAGatewayProxy = gateway_proxy + + @property + def group_info(self) -> dict[str, Any]: + """Return a group description for group.""" + return { + "name": self.group.name, + "group_id": self.group.group_id, + "members": [ + { + "endpoint_id": member.endpoint_id, + "device": self.gateway_proxy.device_proxies[ + member.device.ieee + ].zha_device_info, + "entities": [e._asdict() for e in self.associated_entities(member)], + } + for member in self.group.members + ], + } + + def associated_entities(self, member: GroupMember) -> list[GroupEntityReference]: + """Return the list of entities that were derived from this endpoint.""" + entity_registry = er.async_get(self.gateway_proxy.hass) + entity_refs: collections.defaultdict[EUI64, list[EntityReference]] = ( + self.gateway_proxy.ha_entity_refs + ) + + entity_info = [] + + for entity_ref in entity_refs.get(member.device.ieee): # type: ignore[union-attr] + if not entity_ref.entity_data.is_group_entity: + continue + entity = entity_registry.async_get(entity_ref.ha_entity_id) + + if ( + entity is None + or entity_ref.entity_data.group_proxy is None + or entity_ref.entity_data.group_proxy.group.group_id + != member.group.group_id + ): + continue + + entity_info.append( + GroupEntityReference( + name=entity.name, + original_name=entity.original_name, + entity_id=entity_ref.ha_entity_id, + ) + ) + + return entity_info + + def log(self, level: int, msg: str, *args: Any, **kwargs) -> None: + """Log a message.""" + msg = f"[%s](%s): {msg}" + args = (f"0x{self.group.group_id:04x}", self.group.endpoint.id, *args) + _LOGGER.log(level, msg, *args, **kwargs) + + +class ZHADeviceProxy(EventBase): + """Proxy class to interact with the ZHA device instances.""" + + _ha_device_id: str + + def __init__(self, device: Device, gateway_proxy: ZHAGatewayProxy) -> None: + """Initialize the gateway proxy.""" + super().__init__() + self.device = device + self.gateway_proxy = gateway_proxy + self._unsubs: list[Callable[[], None]] = [] + self._unsubs.append(self.device.on_all_events(self._handle_event_protocol)) + + @property + def device_id(self) -> str: + """Return the HA device registry device id.""" + return self._ha_device_id + + @device_id.setter + def device_id(self, device_id: str) -> None: + """Set the HA device registry device id.""" + self._ha_device_id = device_id + + @property + def device_info(self) -> dict[str, Any]: + """Return a device description for device.""" + ieee = str(self.device.ieee) + time_struct = time.localtime(self.device.last_seen) + update_time = time.strftime("%Y-%m-%dT%H:%M:%S", time_struct) + return { + ATTR_IEEE: ieee, + ATTR_NWK: self.device.nwk, + ATTR_MANUFACTURER: self.device.manufacturer, + ATTR_MODEL: self.device.model, + ATTR_NAME: self.device.name or ieee, + ATTR_QUIRK_APPLIED: self.device.quirk_applied, + ATTR_QUIRK_CLASS: self.device.quirk_class, + ATTR_QUIRK_ID: self.device.quirk_id, + ATTR_MANUFACTURER_CODE: self.device.manufacturer_code, + ATTR_POWER_SOURCE: self.device.power_source, + ATTR_LQI: self.device.lqi, + ATTR_RSSI: self.device.rssi, + ATTR_LAST_SEEN: update_time, + ATTR_AVAILABLE: self.device.available, + ATTR_DEVICE_TYPE: self.device.device_type, + ATTR_SIGNATURE: self.device.zigbee_signature, + } + + @property + def zha_device_info(self) -> dict[str, Any]: + """Get ZHA device information.""" + device_info: dict[str, Any] = {} + device_info.update(self.device_info) + device_info[ATTR_ACTIVE_COORDINATOR] = self.device.is_active_coordinator + device_info[ENTITIES] = [ + { + ATTR_ENTITY_ID: entity_ref.ha_entity_id, + ATTR_NAME: entity_ref.ha_device_info[ATTR_NAME], + } + for entity_ref in self.gateway_proxy.ha_entity_refs[self.device.ieee] + ] + + topology = self.gateway_proxy.gateway.application_controller.topology + device_info[ATTR_NEIGHBORS] = [ + { + ATTR_DEVICE_TYPE: neighbor.device_type.name, + RX_ON_WHEN_IDLE: neighbor.rx_on_when_idle.name, + RELATIONSHIP: neighbor.relationship.name, + EXTENDED_PAN_ID: str(neighbor.extended_pan_id), + ATTR_IEEE: str(neighbor.ieee), + ATTR_NWK: str(neighbor.nwk), + PERMIT_JOINING: neighbor.permit_joining.name, + DEPTH: str(neighbor.depth), + ATTR_LQI: str(neighbor.lqi), + } + for neighbor in topology.neighbors[self.device.ieee] + ] + + device_info[ATTR_ROUTES] = [ + { + DEST_NWK: str(route.DstNWK), + ROUTE_STATUS: str(route.RouteStatus.name), + MEMORY_CONSTRAINED: bool(route.MemoryConstrained), + MANY_TO_ONE: bool(route.ManyToOne), + ROUTE_RECORD_REQUIRED: bool(route.RouteRecordRequired), + NEXT_HOP: str(route.NextHop), + } + for route in topology.routes[self.device.ieee] + ] + + # Return endpoint device type Names + names: list[dict[str, str]] = [] + for endpoint in ( + ep for epid, ep in self.device.device.endpoints.items() if epid + ): + profile = PROFILES.get(endpoint.profile_id) + if profile and endpoint.device_type is not None: + # DeviceType provides undefined enums + names.append({ATTR_NAME: profile.DeviceType(endpoint.device_type).name}) + else: + names.append( + { + ATTR_NAME: ( + f"unknown {endpoint.device_type} device_type " + f"of 0x{(endpoint.profile_id or 0xFFFF):04x} profile id" + ) + } + ) + device_info[ATTR_ENDPOINT_NAMES] = names + + device_registry = dr.async_get(self.gateway_proxy.hass) + reg_device = device_registry.async_get(self.device_id) + if reg_device is not None: + device_info[USER_GIVEN_NAME] = reg_device.name_by_user + device_info[DEVICE_REG_ID] = reg_device.id + device_info[ATTR_AREA_ID] = reg_device.area_id + return device_info + + @callback + def handle_zha_event(self, zha_event: ZHAEvent) -> None: + """Handle a ZHA event.""" + self.gateway_proxy.hass.bus.async_fire( + ZHA_EVENT, + { + ATTR_DEVICE_IEEE: str(zha_event.device_ieee), + ATTR_UNIQUE_ID: zha_event.unique_id, + ATTR_DEVICE_ID: self.device_id, + **zha_event.data, + }, + ) + + @callback + def handle_zha_channel_configure_reporting( + self, event: ClusterConfigureReportingEvent + ) -> None: + """Handle a ZHA cluster configure reporting event.""" + async_dispatcher_send( + self.gateway_proxy.hass, + ZHA_CLUSTER_HANDLER_MSG, + { + ATTR_TYPE: ZHA_CLUSTER_HANDLER_MSG_CFG_RPT, + ZHA_CLUSTER_HANDLER_MSG_DATA: { + ATTR_CLUSTER_NAME: event.cluster_name, + ATTR_CLUSTER_ID: event.cluster_id, + ATTR_ATTRIBUTES: event.attributes, + }, + }, + ) + + @callback + def handle_zha_channel_cfg_done( + self, event: ClusterHandlerConfigurationComplete + ) -> None: + """Handle a ZHA cluster configure reporting event.""" + async_dispatcher_send( + self.gateway_proxy.hass, + ZHA_CLUSTER_HANDLER_MSG, + { + ATTR_TYPE: ZHA_CLUSTER_HANDLER_CFG_DONE, + }, + ) + + @callback + def handle_zha_channel_bind(self, event: ClusterBindEvent) -> None: + """Handle a ZHA cluster bind event.""" + async_dispatcher_send( + self.gateway_proxy.hass, + ZHA_CLUSTER_HANDLER_MSG, + { + ATTR_TYPE: ZHA_CLUSTER_HANDLER_MSG_BIND, + ZHA_CLUSTER_HANDLER_MSG_DATA: { + ATTR_CLUSTER_NAME: event.cluster_name, + ATTR_CLUSTER_ID: event.cluster_id, + ATTR_SUCCESS: event.success, + }, + }, + ) + + +class EntityReference(NamedTuple): + """Describes an entity reference.""" + + ha_entity_id: str + entity_data: EntityData + ha_device_info: dr.DeviceInfo + remove_future: asyncio.Future[Any] + + +class ZHAGatewayProxy(EventBase): + """Proxy class to interact with the ZHA gateway.""" + + def __init__( + self, hass: HomeAssistant, config_entry: ConfigEntry, gateway: Gateway + ) -> None: + """Initialize the gateway proxy.""" + super().__init__() + self.hass = hass + self.config_entry = config_entry + self.gateway = gateway + self.device_proxies: dict[str, ZHADeviceProxy] = {} + self.group_proxies: dict[int, ZHAGroupProxy] = {} + self._ha_entity_refs: collections.defaultdict[EUI64, list[EntityReference]] = ( + collections.defaultdict(list) + ) + self._log_levels: dict[str, dict[str, int]] = { + DEBUG_LEVEL_ORIGINAL: async_capture_log_levels(), + DEBUG_LEVEL_CURRENT: async_capture_log_levels(), + } + self.debug_enabled: bool = False + self._log_relay_handler: LogRelayHandler = LogRelayHandler(hass, self) + self._unsubs: list[Callable[[], None]] = [] + self._unsubs.append(self.gateway.on_all_events(self._handle_event_protocol)) + self._reload_task: asyncio.Task | None = None + + @property + def ha_entity_refs(self) -> collections.defaultdict[EUI64, list[EntityReference]]: + """Return entities by ieee.""" + return self._ha_entity_refs + + def register_entity_reference( + self, + ha_entity_id: str, + entity_data: EntityData, + ha_device_info: dr.DeviceInfo, + remove_future: asyncio.Future[Any], + ) -> None: + """Record the creation of a hass entity associated with ieee.""" + self._ha_entity_refs[entity_data.device_proxy.device.ieee].append( + EntityReference( + ha_entity_id=ha_entity_id, + entity_data=entity_data, + ha_device_info=ha_device_info, + remove_future=remove_future, + ) + ) + + async def async_initialize_devices_and_entities(self) -> None: + """Initialize devices and entities.""" + for device in self.gateway.devices.values(): + device_proxy = self._async_get_or_create_device_proxy(device) + self._create_entity_metadata(device_proxy) + for group in self.gateway.groups.values(): + group_proxy = self._async_get_or_create_group_proxy(group) + self._create_entity_metadata(group_proxy) + + await self.gateway.async_initialize_devices_and_entities() + + @callback + def handle_connection_lost(self, event: ConnectionLostEvent) -> None: + """Handle a connection lost event.""" + + _LOGGER.debug("Connection to the radio was lost: %r", event) + + # Ensure we do not queue up multiple resets + if self._reload_task is not None: + _LOGGER.debug("Ignoring reset, one is already running") + return + + self._reload_task = self.hass.async_create_task( + self.hass.config_entries.async_reload(self.config_entry.entry_id), + ) + + @callback + def handle_device_joined(self, event: DeviceJoinedEvent) -> None: + """Handle a device joined event.""" + async_dispatcher_send( + self.hass, + ZHA_GW_MSG, + { + ATTR_TYPE: ZHA_GW_MSG_DEVICE_JOINED, + ZHA_GW_MSG_DEVICE_INFO: { + ATTR_NWK: event.device_info.nwk, + ATTR_IEEE: str(event.device_info.ieee), + DEVICE_PAIRING_STATUS: event.device_info.pairing_status.name, + }, + }, + ) + + @callback + def handle_device_removed(self, event: DeviceRemovedEvent) -> None: + """Handle a device removed event.""" + zha_device_proxy = self.device_proxies.pop(event.device_info.ieee, None) + entity_refs = self._ha_entity_refs.pop(event.device_info.ieee, None) + if zha_device_proxy is not None: + device_info = zha_device_proxy.zha_device_info + # zha_device_proxy.async_cleanup_handles() + async_dispatcher_send( + self.hass, + f"{SIGNAL_REMOVE_ENTITIES}_{zha_device_proxy.device.ieee!s}", + ) + self.hass.async_create_task( + self._async_remove_device(zha_device_proxy, entity_refs), + "ZHAGateway._async_remove_device", + ) + if device_info is not None: + async_dispatcher_send( + self.hass, + ZHA_GW_MSG, + { + ATTR_TYPE: ZHA_GW_MSG_DEVICE_REMOVED, + ZHA_GW_MSG_DEVICE_INFO: device_info, + }, + ) + + @callback + def handle_device_left(self, event: DeviceLeftEvent) -> None: + """Handle a device left event.""" + + @callback + def handle_raw_device_initialized(self, event: RawDeviceInitializedEvent) -> None: + """Handle a raw device initialized event.""" + manuf = event.device_info.manufacturer + async_dispatcher_send( + self.hass, + ZHA_GW_MSG, + { + ATTR_TYPE: ZHA_GW_MSG_RAW_INIT, + ZHA_GW_MSG_DEVICE_INFO: { + ATTR_NWK: str(event.device_info.nwk), + ATTR_IEEE: str(event.device_info.ieee), + DEVICE_PAIRING_STATUS: event.device_info.pairing_status.name, + ATTR_MODEL: event.device_info.model + if event.device_info.model + else UNKNOWN_MODEL, + ATTR_MANUFACTURER: manuf if manuf else UNKNOWN_MANUFACTURER, + ATTR_SIGNATURE: event.device_info.signature, + }, + }, + ) + + @callback + def handle_device_fully_initialized(self, event: DeviceFullInitEvent) -> None: + """Handle a device fully initialized event.""" + zha_device = self.gateway.get_device(event.device_info.ieee) + zha_device_proxy = self._async_get_or_create_device_proxy(zha_device) + + device_info = zha_device_proxy.zha_device_info + device_info[DEVICE_PAIRING_STATUS] = event.device_info.pairing_status.name + if event.new_join: + self._create_entity_metadata(zha_device_proxy) + async_dispatcher_send(self.hass, SIGNAL_ADD_ENTITIES) + async_dispatcher_send( + self.hass, + ZHA_GW_MSG, + { + ATTR_TYPE: ZHA_GW_MSG_DEVICE_FULL_INIT, + ZHA_GW_MSG_DEVICE_INFO: device_info, + }, + ) + + @callback + def handle_group_member_removed(self, event: GroupEvent) -> None: + """Handle a group member removed event.""" + zha_group_proxy = self._async_get_or_create_group_proxy(event.group_info) + zha_group_proxy.info("group_member_removed - group_info: %s", event.group_info) + self._update_group_entities(event) + self._send_group_gateway_message( + zha_group_proxy, ZHA_GW_MSG_GROUP_MEMBER_REMOVED + ) + + @callback + def handle_group_member_added(self, event: GroupEvent) -> None: + """Handle a group member added event.""" + zha_group_proxy = self._async_get_or_create_group_proxy(event.group_info) + zha_group_proxy.info("group_member_added - group_info: %s", event.group_info) + self._send_group_gateway_message(zha_group_proxy, ZHA_GW_MSG_GROUP_MEMBER_ADDED) + self._update_group_entities(event) + + @callback + def handle_group_added(self, event: GroupEvent) -> None: + """Handle a group added event.""" + zha_group_proxy = self._async_get_or_create_group_proxy(event.group_info) + zha_group_proxy.info("group_added") + self._update_group_entities(event) + self._send_group_gateway_message(zha_group_proxy, ZHA_GW_MSG_GROUP_ADDED) + + @callback + def handle_group_removed(self, event: GroupEvent) -> None: + """Handle a group removed event.""" + self._send_group_gateway_message(event.group_info, ZHA_GW_MSG_GROUP_REMOVED) + zha_group_proxy = self.group_proxies.pop(event.group_info.group_id) + zha_group_proxy.info("group_removed") + self._cleanup_group_entity_registry_entries(zha_group_proxy) + + @callback + def async_enable_debug_mode(self, filterer: _LogFilterType | None = None) -> None: + """Enable debug mode for ZHA.""" + self._log_levels[DEBUG_LEVEL_ORIGINAL] = async_capture_log_levels() + async_set_logger_levels(DEBUG_LEVELS) + self._log_levels[DEBUG_LEVEL_CURRENT] = async_capture_log_levels() + + if filterer: + self._log_relay_handler.addFilter(filterer) + + for logger_name in DEBUG_RELAY_LOGGERS: + logging.getLogger(logger_name).addHandler(self._log_relay_handler) + + self.debug_enabled = True + + @callback + def async_disable_debug_mode(self, filterer: _LogFilterType | None = None) -> None: + """Disable debug mode for ZHA.""" + async_set_logger_levels(self._log_levels[DEBUG_LEVEL_ORIGINAL]) + self._log_levels[DEBUG_LEVEL_CURRENT] = async_capture_log_levels() + for logger_name in DEBUG_RELAY_LOGGERS: + logging.getLogger(logger_name).removeHandler(self._log_relay_handler) + if filterer: + self._log_relay_handler.removeFilter(filterer) + self.debug_enabled = False + + async def shutdown(self) -> None: + """Shutdown the gateway proxy.""" + for unsub in self._unsubs: + unsub() + await self.gateway.shutdown() + + def get_device_proxy(self, ieee: EUI64) -> ZHADeviceProxy | None: + """Return ZHADevice for given ieee.""" + return self.device_proxies.get(ieee) + + def get_group_proxy(self, group_id: int | str) -> ZHAGroupProxy | None: + """Return Group for given group id.""" + if isinstance(group_id, str): + for group_proxy in self.group_proxies.values(): + if group_proxy.group.name == group_id: + return group_proxy + return None + return self.group_proxies.get(group_id) + + def get_entity_reference(self, entity_id: str) -> EntityReference | None: + """Return entity reference for given entity_id if found.""" + for entity_reference in itertools.chain.from_iterable( + self.ha_entity_refs.values() + ): + if entity_id == entity_reference.ha_entity_id: + return entity_reference + return None + + def remove_entity_reference(self, entity: ZHAEntity) -> None: + """Remove entity reference for given entity_id if found.""" + if entity.zha_device.ieee in self.ha_entity_refs: + entity_refs = self.ha_entity_refs.get(entity.zha_device.ieee) + self.ha_entity_refs[entity.zha_device.ieee] = [ + e + for e in entity_refs # type: ignore[union-attr] + if e.ha_entity_id != entity.entity_id + ] + + def _async_get_or_create_device_proxy(self, zha_device: Device) -> ZHADeviceProxy: + """Get or create a ZHA device.""" + if (zha_device_proxy := self.device_proxies.get(zha_device.ieee)) is None: + zha_device_proxy = ZHADeviceProxy(zha_device, self) + self.device_proxies[zha_device_proxy.device.ieee] = zha_device_proxy + + device_registry = dr.async_get(self.hass) + device_registry_device = device_registry.async_get_or_create( + config_entry_id=self.config_entry.entry_id, + connections={(dr.CONNECTION_ZIGBEE, str(zha_device.ieee))}, + identifiers={(DOMAIN, str(zha_device.ieee))}, + name=zha_device.name, + manufacturer=zha_device.manufacturer, + model=zha_device.model, + ) + zha_device_proxy.device_id = device_registry_device.id + return zha_device_proxy + + def _async_get_or_create_group_proxy(self, zha_group: Group) -> ZHAGroupProxy: + """Get or create a ZHA group.""" + zha_group_proxy = self.group_proxies.get(zha_group.group_id) + if zha_group_proxy is None: + zha_group_proxy = ZHAGroupProxy(zha_group, self) + self.group_proxies[zha_group.group_id] = zha_group_proxy + return zha_group_proxy + + def _create_entity_metadata( + self, proxy_object: ZHADeviceProxy | ZHAGroupProxy + ) -> None: + """Create HA entity metadata.""" + ha_zha_data = get_zha_data(self.hass) + coordinator_proxy = self.device_proxies[ + self.gateway.coordinator_zha_device.ieee + ] + + if isinstance(proxy_object, ZHADeviceProxy): + for entity in proxy_object.device.platform_entities.values(): + ha_zha_data.platforms[Platform(entity.PLATFORM)].append( + EntityData( + entity=entity, device_proxy=proxy_object, group_proxy=None + ) + ) + else: + for entity in proxy_object.group.group_entities.values(): + ha_zha_data.platforms[Platform(entity.PLATFORM)].append( + EntityData( + entity=entity, + device_proxy=coordinator_proxy, + group_proxy=proxy_object, + ) + ) + + def _cleanup_group_entity_registry_entries( + self, zigpy_group: zigpy.group.Group + ) -> None: + """Remove entity registry entries for group entities when the groups are removed from HA.""" + # first we collect the potential unique ids for entities that could be created from this group + possible_entity_unique_ids = [ + f"{domain}_zha_group_0x{zigpy_group.group_id:04x}" + for domain in GROUP_ENTITY_DOMAINS + ] + + # then we get all group entity entries tied to the coordinator + entity_registry = er.async_get(self.hass) + assert self.coordinator_zha_device + all_group_entity_entries = er.async_entries_for_device( + entity_registry, + self.coordinator_zha_device.device_id, + include_disabled_entities=True, + ) + + # then we get the entity entries for this specific group + # by getting the entries that match + entries_to_remove = [ + entry + for entry in all_group_entity_entries + if entry.unique_id in possible_entity_unique_ids + ] + + # then we remove the entries from the entity registry + for entry in entries_to_remove: + _LOGGER.debug( + "cleaning up entity registry entry for entity: %s", entry.entity_id + ) + entity_registry.async_remove(entry.entity_id) + + def _update_group_entities(self, group_event: GroupEvent) -> None: + """Update group entities when a group event is received.""" + async_dispatcher_send( + self.hass, + f"{SIGNAL_REMOVE_ENTITIES}_group_{group_event.group_info.group_id}", + ) + self._create_entity_metadata( + self.group_proxies[group_event.group_info.group_id] + ) + async_dispatcher_send(self.hass, SIGNAL_ADD_ENTITIES) + + def _send_group_gateway_message( + self, zigpy_group: zigpy.group.Group, gateway_message_type: str + ) -> None: + """Send the gateway event for a zigpy group event.""" + zha_group = self.group_proxies.get(zigpy_group.group_id) + if zha_group is not None: + async_dispatcher_send( + self.hass, + ZHA_GW_MSG, + { + ATTR_TYPE: gateway_message_type, + ZHA_GW_MSG_GROUP_INFO: zha_group.group_info, + }, + ) + + async def _async_remove_device( + self, device: ZHADeviceProxy, entity_refs: list[EntityReference] | None + ) -> None: + if entity_refs is not None: + remove_tasks: list[asyncio.Future[Any]] = [ + entity_ref.remove_future for entity_ref in entity_refs + ] + if remove_tasks: + await asyncio.wait(remove_tasks) + + device_registry = dr.async_get(self.hass) + reg_device = device_registry.async_get(device.device_id) + if reg_device is not None: + device_registry.async_remove_device(reg_device.id) + + +@callback +def async_capture_log_levels() -> dict[str, int]: + """Capture current logger levels for ZHA.""" + return { + DEBUG_COMP_BELLOWS: logging.getLogger(DEBUG_COMP_BELLOWS).getEffectiveLevel(), + DEBUG_COMP_ZHA: logging.getLogger(DEBUG_COMP_ZHA).getEffectiveLevel(), + DEBUG_COMP_ZIGPY: logging.getLogger(DEBUG_COMP_ZIGPY).getEffectiveLevel(), + DEBUG_COMP_ZIGPY_ZNP: logging.getLogger( + DEBUG_COMP_ZIGPY_ZNP + ).getEffectiveLevel(), + DEBUG_COMP_ZIGPY_DECONZ: logging.getLogger( + DEBUG_COMP_ZIGPY_DECONZ + ).getEffectiveLevel(), + DEBUG_COMP_ZIGPY_XBEE: logging.getLogger( + DEBUG_COMP_ZIGPY_XBEE + ).getEffectiveLevel(), + DEBUG_COMP_ZIGPY_ZIGATE: logging.getLogger( + DEBUG_COMP_ZIGPY_ZIGATE + ).getEffectiveLevel(), + DEBUG_LIB_ZHA: logging.getLogger(DEBUG_LIB_ZHA).getEffectiveLevel(), + } + + +@callback +def async_set_logger_levels(levels: dict[str, int]) -> None: + """Set logger levels for ZHA.""" + logging.getLogger(DEBUG_COMP_BELLOWS).setLevel(levels[DEBUG_COMP_BELLOWS]) + logging.getLogger(DEBUG_COMP_ZHA).setLevel(levels[DEBUG_COMP_ZHA]) + logging.getLogger(DEBUG_COMP_ZIGPY).setLevel(levels[DEBUG_COMP_ZIGPY]) + logging.getLogger(DEBUG_COMP_ZIGPY_ZNP).setLevel(levels[DEBUG_COMP_ZIGPY_ZNP]) + logging.getLogger(DEBUG_COMP_ZIGPY_DECONZ).setLevel(levels[DEBUG_COMP_ZIGPY_DECONZ]) + logging.getLogger(DEBUG_COMP_ZIGPY_XBEE).setLevel(levels[DEBUG_COMP_ZIGPY_XBEE]) + logging.getLogger(DEBUG_COMP_ZIGPY_ZIGATE).setLevel(levels[DEBUG_COMP_ZIGPY_ZIGATE]) + logging.getLogger(DEBUG_LIB_ZHA).setLevel(levels[DEBUG_LIB_ZHA]) + + +class LogRelayHandler(logging.Handler): + """Log handler for error messages.""" + + def __init__(self, hass: HomeAssistant, gateway: ZHAGatewayProxy) -> None: + """Initialize a new LogErrorHandler.""" + super().__init__() + self.hass = hass + self.gateway = gateway + hass_path: str = HOMEASSISTANT_PATH[0] + config_dir = self.hass.config.config_dir + self.paths_re = re.compile( + r"(?:{})/(.*)".format( + "|".join([re.escape(x) for x in (hass_path, config_dir)]) + ) + ) + + def emit(self, record: LogRecord) -> None: + """Relay log message via dispatcher.""" + entry = LogEntry( + record, self.paths_re, figure_out_source=record.levelno >= logging.WARNING + ) + async_dispatcher_send( + self.hass, + ZHA_GW_MSG, + {ATTR_TYPE: ZHA_GW_MSG_LOG_OUTPUT, ZHA_GW_MSG_LOG_ENTRY: entry.to_dict()}, + ) + + +@dataclasses.dataclass(kw_only=True, slots=True) +class HAZHAData: + """ZHA data stored in `hass.data`.""" + + yaml_config: ConfigType = dataclasses.field(default_factory=dict) + config_entry: ConfigEntry | None = dataclasses.field(default=None) + device_trigger_cache: dict[str, tuple[str, dict]] = dataclasses.field( + default_factory=dict + ) + gateway_proxy: ZHAGatewayProxy | None = dataclasses.field(default=None) + platforms: collections.defaultdict[Platform, list] = dataclasses.field( + default_factory=lambda: collections.defaultdict(list) + ) + update_coordinator: ZHAFirmwareUpdateCoordinator | None = dataclasses.field( + default=None + ) + + +@dataclasses.dataclass(kw_only=True, slots=True) +class EntityData: + """ZHA entity data.""" + + entity: PlatformEntity | GroupEntity + device_proxy: ZHADeviceProxy + group_proxy: ZHAGroupProxy | None = dataclasses.field(default=None) + + @property + def is_group_entity(self) -> bool: + """Return if this is a group entity.""" + return self.group_proxy is not None and isinstance(self.entity, GroupEntity) + + +def get_zha_data(hass: HomeAssistant) -> HAZHAData: + """Get the global ZHA data object.""" + if DATA_ZHA not in hass.data: + hass.data[DATA_ZHA] = HAZHAData() + + return hass.data[DATA_ZHA] + + +def get_zha_gateway(hass: HomeAssistant) -> Gateway: + """Get the ZHA gateway object.""" + if (gateway_proxy := get_zha_data(hass).gateway_proxy) is None: + raise ValueError("No gateway object exists") + + return gateway_proxy.gateway + + +def get_zha_gateway_proxy(hass: HomeAssistant) -> ZHAGatewayProxy: + """Get the ZHA gateway object.""" + if (gateway_proxy := get_zha_data(hass).gateway_proxy) is None: + raise ValueError("No gateway object exists") + + return gateway_proxy + + +def get_config_entry(hass: HomeAssistant) -> ConfigEntry: + """Get the ZHA gateway object.""" + if (gateway_proxy := get_zha_data(hass).gateway_proxy) is None: + raise ValueError("No gateway object exists to retrieve the config entry from.") + + return gateway_proxy.config_entry + + +@callback +def async_get_zha_device_proxy(hass: HomeAssistant, device_id: str) -> ZHADeviceProxy: + """Get a ZHA device for the given device registry id.""" + device_registry = dr.async_get(hass) + registry_device = device_registry.async_get(device_id) + if not registry_device: + _LOGGER.error("Device id `%s` not found in registry", device_id) + raise KeyError(f"Device id `{device_id}` not found in registry.") + zha_gateway_proxy = get_zha_gateway_proxy(hass) + try: + ieee_address = list(registry_device.identifiers)[0][1] + ieee = EUI64.convert(ieee_address) + except (IndexError, ValueError) as ex: + _LOGGER.error( + "Unable to determine device IEEE for device with device id `%s`", device_id + ) + raise KeyError( + f"Unable to determine device IEEE for device with device id `{device_id}`." + ) from ex + return zha_gateway_proxy.device_proxies[ieee] + + +def cluster_command_schema_to_vol_schema(schema: CommandSchema) -> vol.Schema: + """Convert a cluster command schema to a voluptuous schema.""" + return vol.Schema( + { + vol.Optional(field.name) + if field.optional + else vol.Required(field.name): schema_type_to_vol(field.type) + for field in schema.fields + } + ) + + +def schema_type_to_vol(field_type: Any) -> Any: + """Convert a schema type to a voluptuous type.""" + if issubclass(field_type, enum.Flag) and field_type.__members__: + return cv.multi_select( + [key.replace("_", " ") for key in field_type.__members__] + ) + if issubclass(field_type, enum.Enum) and field_type.__members__: + return vol.In([key.replace("_", " ") for key in field_type.__members__]) + if ( + issubclass(field_type, zigpy.types.FixedIntType) + or issubclass(field_type, enum.Flag) + or issubclass(field_type, enum.Enum) + ): + return vol.All( + vol.Coerce(int), vol.Range(field_type.min_value, field_type.max_value) + ) + return str + + +def convert_to_zcl_values( + fields: dict[str, Any], schema: CommandSchema +) -> dict[str, Any]: + """Convert user input to ZCL values.""" + converted_fields: dict[str, Any] = {} + for field in schema.fields: + if field.name not in fields: + continue + value = fields[field.name] + if issubclass(field.type, enum.Flag) and isinstance(value, list): + new_value = 0 + + for flag in value: + if isinstance(flag, str): + new_value |= field.type[flag.replace(" ", "_")] + else: + new_value |= flag + + value = field.type(new_value) + elif issubclass(field.type, enum.Enum): + value = ( + field.type[value.replace(" ", "_")] + if isinstance(value, str) + else field.type(value) + ) + else: + value = field.type(value) + _LOGGER.debug( + "Converted ZCL schema field(%s) value from: %s to: %s", + field.name, + fields[field.name], + value, + ) + converted_fields[field.name] = value + return converted_fields + + +def async_cluster_exists(hass: HomeAssistant, cluster_id, skip_coordinator=True): + """Determine if a device containing the specified in cluster is paired.""" + zha_gateway = get_zha_gateway(hass) + zha_devices = zha_gateway.devices.values() + for zha_device in zha_devices: + if skip_coordinator and zha_device.is_coordinator: + continue + clusters_by_endpoint = zha_device.async_get_clusters() + for clusters in clusters_by_endpoint.values(): + if ( + cluster_id in clusters[CLUSTER_TYPE_IN] + or cluster_id in clusters[CLUSTER_TYPE_OUT] + ): + return True + return False + + +@callback +async def async_add_entities( + _async_add_entities: AddEntitiesCallback, + entity_class: type[ZHAEntity], + entities: list[EntityData], + **kwargs, +) -> None: + """Add entities helper.""" + if not entities: + return + + entities_to_add = [entity_class(entity_data) for entity_data in entities] + _async_add_entities(entities_to_add, update_before_add=False) + entities.clear() + + +def _clean_serial_port_path(path: str) -> str: + """Clean the serial port path, applying corrections where necessary.""" + + if path.startswith("socket://"): + path = path.strip() + + # Removes extraneous brackets from IP addresses (they don't parse in CPython 3.11.4) + if re.match(r"^socket://\[\d+\.\d+\.\d+\.\d+\]:\d+$", path): + path = path.replace("[", "").replace("]", "") + + return path + + +CONF_ZHA_OPTIONS_SCHEMA = vol.Schema( + { + vol.Optional(CONF_DEFAULT_LIGHT_TRANSITION, default=0): vol.All( + vol.Coerce(float), vol.Range(min=0, max=2**16 / 10) + ), + vol.Required(CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, default=False): cv.boolean, + vol.Required(CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, default=True): cv.boolean, + vol.Required(CONF_ALWAYS_PREFER_XY_COLOR_MODE, default=True): cv.boolean, + vol.Required(CONF_GROUP_MEMBERS_ASSUME_STATE, default=True): cv.boolean, + vol.Required(CONF_ENABLE_IDENTIFY_ON_JOIN, default=True): cv.boolean, + vol.Optional( + CONF_CONSIDER_UNAVAILABLE_MAINS, + default=CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS, + ): cv.positive_int, + vol.Optional( + CONF_CONSIDER_UNAVAILABLE_BATTERY, + default=CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY, + ): cv.positive_int, + } +) + +CONF_ZHA_ALARM_SCHEMA = vol.Schema( + { + vol.Required(CONF_ALARM_MASTER_CODE, default="1234"): cv.string, + vol.Required(CONF_ALARM_FAILED_TRIES, default=3): cv.positive_int, + vol.Required(CONF_ALARM_ARM_REQUIRES_CODE, default=False): cv.boolean, + } +) + + +def create_zha_config(hass: HomeAssistant, ha_zha_data: HAZHAData) -> ZHAData: + """Create ZHA lib configuration from HA config objects.""" + + # ensure that we have the necessary HA configuration data + assert ha_zha_data.config_entry is not None + assert ha_zha_data.yaml_config is not None + + # Remove brackets around IP addresses, this no longer works in CPython 3.11.4 + # This will be removed in 2023.11.0 + path = ha_zha_data.config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] + cleaned_path = _clean_serial_port_path(path) + + if path != cleaned_path: + _LOGGER.debug("Cleaned serial port path %r -> %r", path, cleaned_path) + ha_zha_data.config_entry.data[CONF_DEVICE][CONF_DEVICE_PATH] = cleaned_path + hass.config_entries.async_update_entry( + ha_zha_data.config_entry, data=ha_zha_data.config_entry.data + ) + + # deep copy the yaml config to avoid modifying the original and to safely + # pass it to the ZHA library + app_config = copy.deepcopy(ha_zha_data.yaml_config.get(CONF_ZIGPY, {})) + database = app_config.get( + CONF_DATABASE, + hass.config.path(DEFAULT_DATABASE_NAME), + ) + app_config[CONF_DATABASE] = database + app_config[CONF_DEVICE] = ha_zha_data.config_entry.data[CONF_DEVICE] + + radio_type = RadioType[ha_zha_data.config_entry.data[CONF_RADIO_TYPE]] + + # Until we have a way to coordinate channels with the Thread half of multi-PAN, + # stick to the old zigpy default of channel 15 instead of dynamically scanning + if ( + is_multiprotocol_url(app_config[CONF_DEVICE][CONF_DEVICE_PATH]) + and app_config.get(CONF_NWK, {}).get(CONF_NWK_CHANNEL) is None + ): + app_config.setdefault(CONF_NWK, {})[CONF_NWK_CHANNEL] = 15 + + options: MappingProxyType[str, Any] = ha_zha_data.config_entry.options.get( + CUSTOM_CONFIGURATION, {} + ) + zha_options = CONF_ZHA_OPTIONS_SCHEMA(options.get(ZHA_OPTIONS, {})) + ha_acp_options = CONF_ZHA_ALARM_SCHEMA(options.get(ZHA_ALARM_OPTIONS, {})) + light_options: LightOptions = LightOptions( + default_light_transition=zha_options.get(CONF_DEFAULT_LIGHT_TRANSITION), + enable_enhanced_light_transition=zha_options.get( + CONF_ENABLE_ENHANCED_LIGHT_TRANSITION + ), + enable_light_transitioning_flag=zha_options.get( + CONF_ENABLE_LIGHT_TRANSITIONING_FLAG + ), + always_prefer_xy_color_mode=zha_options.get(CONF_ALWAYS_PREFER_XY_COLOR_MODE), + group_members_assume_state=zha_options.get(CONF_GROUP_MEMBERS_ASSUME_STATE), + ) + device_options: DeviceOptions = DeviceOptions( + enable_identify_on_join=zha_options.get(CONF_ENABLE_IDENTIFY_ON_JOIN), + consider_unavailable_mains=zha_options.get(CONF_CONSIDER_UNAVAILABLE_MAINS), + consider_unavailable_battery=zha_options.get(CONF_CONSIDER_UNAVAILABLE_BATTERY), + ) + acp_options: AlarmControlPanelOptions = AlarmControlPanelOptions( + master_code=ha_acp_options.get(CONF_ALARM_MASTER_CODE), + failed_tries=ha_acp_options.get(CONF_ALARM_FAILED_TRIES), + arm_requires_code=ha_acp_options.get(CONF_ALARM_ARM_REQUIRES_CODE), + ) + coord_config: CoordinatorConfiguration = CoordinatorConfiguration( + path=app_config[CONF_DEVICE][CONF_DEVICE_PATH], + baudrate=app_config[CONF_DEVICE][CONF_BAUDRATE], + flow_control=app_config[CONF_DEVICE][CONF_FLOW_CONTROL], + radio_type=radio_type.name, + ) + quirks_config: QuirksConfiguration = QuirksConfiguration( + enabled=ha_zha_data.yaml_config.get(CONF_ENABLE_QUIRKS, True), + custom_quirks_path=ha_zha_data.yaml_config.get(CONF_CUSTOM_QUIRKS_PATH), + ) + overrides_config: dict[str, DeviceOverridesConfiguration] = {} + overrides: dict[str, dict[str, Any]] = cast( + dict[str, dict[str, Any]], ha_zha_data.yaml_config.get(CONF_DEVICE_CONFIG) + ) + if overrides is not None: + for unique_id, override in overrides.items(): + overrides_config[unique_id] = DeviceOverridesConfiguration( + type=override["type"], + ) + + return ZHAData( + zigpy_config=app_config, + config=ZHAConfiguration( + light_options=light_options, + device_options=device_options, + alarm_control_panel_options=acp_options, + coordinator_configuration=coord_config, + quirks_configuration=quirks_config, + device_overrides=overrides_config, + ), + ) + + +def convert_zha_error_to_ha_error( + func: Callable[Concatenate[_EntityT, _P], Awaitable[None]], +) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]: + """Decorate ZHA commands and re-raises ZHAException as HomeAssistantError.""" + + @functools.wraps(func) + async def handler(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None: + try: + return await func(self, *args, **kwargs) + except ZHAException as err: + raise HomeAssistantError(err) from err + + return handler + + +def exclude_none_values(obj: Mapping[str, Any]) -> dict[str, Any]: + """Return a new dictionary excluding keys with None values.""" + return {k: v for k, v in obj.items() if v is not None} diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 6fd08de889f..4a36030a0dd 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -2,93 +2,63 @@ from __future__ import annotations -from collections import Counter -from collections.abc import Callable -from datetime import timedelta +from collections.abc import Mapping import functools -import itertools import logging -import random -from typing import TYPE_CHECKING, Any +from typing import Any -from zigpy.zcl.clusters.general import Identify, LevelControl, OnOff -from zigpy.zcl.clusters.lighting import Color -from zigpy.zcl.foundation import Status +from zha.application.platforms.light.const import ( + ColorMode as ZhaColorMode, + LightEntityFeature as ZhaLightEntityFeature, +) -from homeassistant.components import light from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_FLASH, + ATTR_HS_COLOR, + ATTR_TRANSITION, + ATTR_XY_COLOR, ColorMode, + LightEntity, LightEntityFeature, - brightness_supported, - filter_supported_color_modes, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, - STATE_ON, - STATE_UNAVAILABLE, - Platform, -) -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, State, callback -from homeassistant.helpers.debounce import Debouncer -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) +from homeassistant.const import STATE_ON, Platform +from homeassistant.core import HomeAssistant, State, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_call_later, async_track_time_interval -from .core import discovery, helpers -from .core.const import ( - CLUSTER_HANDLER_COLOR, - CLUSTER_HANDLER_LEVEL, - CLUSTER_HANDLER_ON_OFF, - CONF_ALWAYS_PREFER_XY_COLOR_MODE, - CONF_DEFAULT_LIGHT_TRANSITION, - CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, - CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, - CONF_GROUP_MEMBERS_ASSUME_STATE, - DATA_ZHA, +from .entity import ZHAEntity +from .helpers import ( SIGNAL_ADD_ENTITIES, - SIGNAL_ATTR_UPDATED, - SIGNAL_SET_LEVEL, - ZHA_OPTIONS, + EntityData, + async_add_entities as zha_async_add_entities, + convert_zha_error_to_ha_error, + get_zha_data, ) -from .core.helpers import LogMixin, async_get_zha_config_value, get_zha_data -from .core.registries import ZHA_ENTITIES -from .entity import ZhaEntity, ZhaGroupEntity -if TYPE_CHECKING: - from .core.device import ZHADevice - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_ON_OFF_TRANSITION = 1 # most bulbs default to a 1-second turn on/off transition -DEFAULT_EXTRA_TRANSITION_DELAY_SHORT = 0.25 -DEFAULT_EXTRA_TRANSITION_DELAY_LONG = 2.0 -DEFAULT_LONG_TRANSITION_TIME = 10 -DEFAULT_MIN_BRIGHTNESS = 2 -ASSUME_UPDATE_GROUP_FROM_CHILD_DELAY = 0.05 - -FLASH_EFFECTS = { - light.FLASH_SHORT: Identify.EffectIdentifier.Blink, - light.FLASH_LONG: Identify.EffectIdentifier.Breathe, +ZHA_TO_HA_COLOR_MODE = { + ZhaColorMode.UNKNOWN: ColorMode.UNKNOWN, + ZhaColorMode.ONOFF: ColorMode.ONOFF, + ZhaColorMode.BRIGHTNESS: ColorMode.BRIGHTNESS, + ZhaColorMode.COLOR_TEMP: ColorMode.COLOR_TEMP, + ZhaColorMode.HS: ColorMode.HS, + ZhaColorMode.XY: ColorMode.XY, + ZhaColorMode.RGB: ColorMode.RGB, + ZhaColorMode.RGBW: ColorMode.RGBW, + ZhaColorMode.RGBWW: ColorMode.RGBWW, + ZhaColorMode.WHITE: ColorMode.WHITE, } -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.LIGHT) -GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.LIGHT) -SIGNAL_LIGHT_GROUP_STATE_CHANGED = "zha_light_group_state_changed" -SIGNAL_LIGHT_GROUP_TRANSITION_START = "zha_light_group_transition_start" -SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED = "zha_light_group_transition_finished" -SIGNAL_LIGHT_GROUP_ASSUME_GROUP_STATE = "zha_light_group_assume_group_state" -DEFAULT_MIN_TRANSITION_MANUFACTURERS = {"sengled"} +HA_TO_ZHA_COLOR_MODE = {v: k for k, v in ZHA_TO_HA_COLOR_MODE.items()} -COLOR_MODES_GROUP_LIGHT = {ColorMode.COLOR_TEMP, ColorMode.XY} -SUPPORT_GROUP_LIGHT = ( - light.LightEntityFeature.EFFECT - | light.LightEntityFeature.FLASH - | light.LightEntityFeature.TRANSITION -) +OFF_BRIGHTNESS = "off_brightness" +OFF_WITH_TRANSITION = "off_with_transition" + +_LOGGER = logging.getLogger(__name__) async def async_setup_entry( @@ -104,1280 +74,144 @@ async def async_setup_entry( hass, SIGNAL_ADD_ENTITIES, functools.partial( - discovery.async_add_entities, async_add_entities, entities_to_create + zha_async_add_entities, async_add_entities, Light, entities_to_create ), ) config_entry.async_on_unload(unsub) -class BaseLight(LogMixin, light.LightEntity): - """Operations common to all light entities.""" +class Light(LightEntity, ZHAEntity): + """Representation of a ZHA or ZLL light.""" - _FORCE_ON = False - _DEFAULT_MIN_TRANSITION_TIME: float = 0 + def __init__(self, entity_data: EntityData) -> None: + """Initialize the ZHA light.""" + super().__init__(entity_data) + color_modes: set[ColorMode] = set() + has_brightness = False + for color_mode in self.entity_data.entity.supported_color_modes: + if color_mode == ZhaColorMode.BRIGHTNESS: + has_brightness = True + if color_mode not in (ZhaColorMode.BRIGHTNESS, ZhaColorMode.ONOFF): + color_modes.add(ZHA_TO_HA_COLOR_MODE[color_mode]) + if color_modes: + self._attr_supported_color_modes = color_modes + elif has_brightness: + color_modes.add(ColorMode.BRIGHTNESS) + self._attr_supported_color_modes = color_modes + else: + color_modes.add(ColorMode.ONOFF) + self._attr_supported_color_modes = color_modes - def __init__(self, *args, **kwargs): - """Initialize the light.""" - self._zha_device: ZHADevice = None - super().__init__(*args, **kwargs) - self._attr_min_mireds: int | None = 153 - self._attr_max_mireds: int | None = 500 - self._attr_color_mode = ColorMode.UNKNOWN # Set by subclasses - self._attr_supported_features: int = 0 - self._attr_state: bool | None - self._off_with_transition: bool = False - self._off_brightness: int | None = None - self._zha_config_transition = self._DEFAULT_MIN_TRANSITION_TIME - self._zha_config_enhanced_light_transition: bool = False - self._zha_config_enable_light_transitioning_flag: bool = True - self._zha_config_always_prefer_xy_color_mode: bool = True - self._on_off_cluster_handler = None - self._level_cluster_handler = None - self._color_cluster_handler = None - self._identify_cluster_handler = None - self._transitioning_individual: bool = False - self._transitioning_group: bool = False - self._transition_listener: Callable[[], None] | None = None + features = LightEntityFeature(0) + zha_features: ZhaLightEntityFeature = self.entity_data.entity.supported_features - async def async_will_remove_from_hass(self) -> None: - """Disconnect entity object when removed.""" - self._async_unsub_transition_listener() - await super().async_will_remove_from_hass() + if ZhaLightEntityFeature.EFFECT in zha_features: + features |= LightEntityFeature.EFFECT + if ZhaLightEntityFeature.FLASH in zha_features: + features |= LightEntityFeature.FLASH + if ZhaLightEntityFeature.TRANSITION in zha_features: + features |= LightEntityFeature.TRANSITION + + self._attr_supported_features = features @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return state attributes.""" + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return entity specific state attributes.""" + state = self.entity_data.entity.state return { - "off_with_transition": self._off_with_transition, - "off_brightness": self._off_brightness, + "off_with_transition": state.get("off_with_transition"), + "off_brightness": state.get("off_brightness"), } @property def is_on(self) -> bool: """Return true if entity is on.""" - if self._attr_state is None: - return False - return self._attr_state - - @callback - def set_level(self, value: int) -> None: - """Set the brightness of this light between 0..254. - - brightness level 255 is a special value instructing the device to come - on at `on_level` Zigbee attribute value, regardless of the last set - level - """ - if self.is_transitioning: - self.debug( - "received level %s while transitioning - skipping update", - value, - ) - return - value = max(0, min(254, value)) - self._attr_brightness = value - self.async_write_ha_state() - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the entity on.""" - transition = kwargs.get(light.ATTR_TRANSITION) - duration = ( - transition if transition is not None else self._zha_config_transition - ) or ( - # if 0 is passed in some devices still need the minimum default - self._DEFAULT_MIN_TRANSITION_TIME - ) - brightness = kwargs.get(light.ATTR_BRIGHTNESS) - effect = kwargs.get(light.ATTR_EFFECT) - flash = kwargs.get(light.ATTR_FLASH) - temperature = kwargs.get(light.ATTR_COLOR_TEMP) - xy_color = kwargs.get(light.ATTR_XY_COLOR) - hs_color = kwargs.get(light.ATTR_HS_COLOR) - - execute_if_off_supported = ( - self._GROUP_SUPPORTS_EXECUTE_IF_OFF - if isinstance(self, LightGroup) - else self._color_cluster_handler - and self._color_cluster_handler.execute_if_off_supported - ) - - set_transition_flag = ( - brightness_supported(self._attr_supported_color_modes) - or temperature is not None - or xy_color is not None - or hs_color is not None - ) and self._zha_config_enable_light_transitioning_flag - transition_time = ( - ( - duration + DEFAULT_EXTRA_TRANSITION_DELAY_SHORT - if ( - (brightness is not None or transition is not None) - and brightness_supported(self._attr_supported_color_modes) - or (self._off_with_transition and self._off_brightness is not None) - or temperature is not None - or xy_color is not None - or hs_color is not None - ) - else DEFAULT_ON_OFF_TRANSITION + DEFAULT_EXTRA_TRANSITION_DELAY_SHORT - ) - if set_transition_flag - else 0 - ) - - # If we need to pause attribute report parsing, we'll do so here. - # After successful calls, we later start a timer to unset the flag after - # transition_time. - # - On an error on the first move to level call, we unset the flag immediately - # if no previous timer is running. - # - On an error on subsequent calls, we start the transition timer, - # as a brightness call might have come through. - if set_transition_flag: - self.async_transition_set_flag() - - # If the light is currently off but a turn_on call with a color/temperature is - # sent, the light needs to be turned on first at a low brightness level where - # the light is immediately transitioned to the correct color. Afterwards, the - # transition is only from the low brightness to the new brightness. - # Otherwise, the transition is from the color the light had before being turned - # on to the new color. This can look especially bad with transitions longer than - # a second. We do not want to do this for devices that need to be forced to use - # the on command because we would end up with 4 commands sent: - # move to level, on, color, move to level... We also will not set this - # if the bulb is already in the desired color mode with the desired color - # or color temperature. - new_color_provided_while_off = ( - self._zha_config_enhanced_light_transition - and not self._FORCE_ON - and not self._attr_state - and ( - ( - temperature is not None - and ( - self._attr_color_temp != temperature - or self._attr_color_mode != ColorMode.COLOR_TEMP - ) - ) - or ( - xy_color is not None - and ( - self._attr_xy_color != xy_color - or self._attr_color_mode != ColorMode.XY - ) - ) - or ( - hs_color is not None - and ( - self._attr_hs_color != hs_color - or self._attr_color_mode != ColorMode.HS - ) - ) - ) - and brightness_supported(self._attr_supported_color_modes) - and not execute_if_off_supported - ) - - if ( - brightness is None - and (self._off_with_transition or new_color_provided_while_off) - and self._off_brightness is not None - ): - brightness = self._off_brightness - - if brightness is not None: - level = min(254, brightness) - else: - level = self._attr_brightness or 254 - - t_log = {} - - if new_color_provided_while_off: - # If the light is currently off, we first need to turn it on at a low - # brightness level with no transition. - # After that, we set it to the desired color/temperature with no transition. - result = await self._level_cluster_handler.move_to_level_with_on_off( - level=DEFAULT_MIN_BRIGHTNESS, - transition_time=int(10 * self._DEFAULT_MIN_TRANSITION_TIME), - ) - t_log["move_to_level_with_on_off"] = result - if result[1] is not Status.SUCCESS: - # First 'move to level' call failed, so if the transitioning delay - # isn't running from a previous call, - # the flag can be unset immediately - if set_transition_flag and not self._transition_listener: - self.async_transition_complete() - self.debug("turned on: %s", t_log) - return - # Currently only setting it to "on", as the correct level state will - # be set at the second move_to_level call - self._attr_state = True - - if execute_if_off_supported: - self.debug("handling color commands before turning on/level") - if not await self.async_handle_color_commands( - temperature, - duration, # duration is ignored by lights when off - hs_color, - xy_color, - new_color_provided_while_off, - t_log, - ): - # Color calls before on/level calls failed, - # so if the transitioning delay isn't running from a previous call, - # the flag can be unset immediately - if set_transition_flag and not self._transition_listener: - self.async_transition_complete() - self.debug("turned on: %s", t_log) - return - - if ( - (brightness is not None or transition is not None) - and not new_color_provided_while_off - and brightness_supported(self._attr_supported_color_modes) - ): - result = await self._level_cluster_handler.move_to_level_with_on_off( - level=level, - transition_time=int(10 * duration), - ) - t_log["move_to_level_with_on_off"] = result - if result[1] is not Status.SUCCESS: - # First 'move to level' call failed, so if the transitioning delay - # isn't running from a previous call, the flag can be unset immediately - if set_transition_flag and not self._transition_listener: - self.async_transition_complete() - self.debug("turned on: %s", t_log) - return - self._attr_state = bool(level) - if level: - self._attr_brightness = level - - if ( - (brightness is None and transition is None) - and not new_color_provided_while_off - or (self._FORCE_ON and brightness != 0) - ): - # since FORCE_ON lights don't turn on with move_to_level_with_on_off, - # we should call the on command on the on_off cluster - # if brightness is not 0. - result = await self._on_off_cluster_handler.on() - t_log["on_off"] = result - if result[1] is not Status.SUCCESS: - # 'On' call failed, but as brightness may still transition - # (for FORCE_ON lights), we start the timer to unset the flag after - # the transition_time if necessary. - self.async_transition_start_timer(transition_time) - self.debug("turned on: %s", t_log) - return - self._attr_state = True - - if not execute_if_off_supported: - self.debug("handling color commands after turning on/level") - if not await self.async_handle_color_commands( - temperature, - duration, - hs_color, - xy_color, - new_color_provided_while_off, - t_log, - ): - # Color calls failed, but as brightness may still transition, - # we start the timer to unset the flag - self.async_transition_start_timer(transition_time) - self.debug("turned on: %s", t_log) - return - - if new_color_provided_while_off: - # The light has the correct color, so we can now transition - # it to the correct brightness level. - result = await self._level_cluster_handler.move_to_level( - level=level, transition_time=int(10 * duration) - ) - t_log["move_to_level_if_color"] = result - if result[1] is not Status.SUCCESS: - self.debug("turned on: %s", t_log) - return - self._attr_state = bool(level) - if level: - self._attr_brightness = level - - # Our light is guaranteed to have just started the transitioning process - # if necessary, so we start the delay for the transition (to stop parsing - # attribute reports after the completed transition). - self.async_transition_start_timer(transition_time) - - if effect == light.EFFECT_COLORLOOP: - result = await self._color_cluster_handler.color_loop_set( - update_flags=( - Color.ColorLoopUpdateFlags.Action - | Color.ColorLoopUpdateFlags.Direction - | Color.ColorLoopUpdateFlags.Time - ), - action=Color.ColorLoopAction.Activate_from_current_hue, - direction=Color.ColorLoopDirection.Increment, - time=transition if transition else 7, - start_hue=0, - ) - t_log["color_loop_set"] = result - self._attr_effect = light.EFFECT_COLORLOOP - elif ( - self._attr_effect == light.EFFECT_COLORLOOP - and effect != light.EFFECT_COLORLOOP - ): - result = await self._color_cluster_handler.color_loop_set( - update_flags=Color.ColorLoopUpdateFlags.Action, - action=Color.ColorLoopAction.Deactivate, - direction=Color.ColorLoopDirection.Decrement, - time=0, - start_hue=0, - ) - t_log["color_loop_set"] = result - self._attr_effect = None - - if flash is not None: - result = await self._identify_cluster_handler.trigger_effect( - effect_id=FLASH_EFFECTS[flash], - effect_variant=Identify.EffectVariant.Default, - ) - t_log["trigger_effect"] = result - - self._off_with_transition = False - self._off_brightness = None - self.debug("turned on: %s", t_log) - self.async_write_ha_state() - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the entity off.""" - transition = kwargs.get(light.ATTR_TRANSITION) - supports_level = brightness_supported(self._attr_supported_color_modes) - - transition_time = ( - transition or self._DEFAULT_MIN_TRANSITION_TIME - if transition is not None - else DEFAULT_ON_OFF_TRANSITION - ) + DEFAULT_EXTRA_TRANSITION_DELAY_SHORT - - # Start pausing attribute report parsing - if self._zha_config_enable_light_transitioning_flag: - self.async_transition_set_flag() - - # is not none looks odd here, but it will override built in bulb - # transition times if we pass 0 in here - if transition is not None and supports_level: - result = await self._level_cluster_handler.move_to_level_with_on_off( - level=0, - transition_time=int( - 10 * (transition or self._DEFAULT_MIN_TRANSITION_TIME) - ), - ) - else: - result = await self._on_off_cluster_handler.off() - - # Pause parsing attribute reports until transition is complete - if self._zha_config_enable_light_transitioning_flag: - self.async_transition_start_timer(transition_time) - self.debug("turned off: %s", result) - if result[1] is not Status.SUCCESS: - return - self._attr_state = False - - if supports_level and not self._off_with_transition: - # store current brightness so that the next turn_on uses it: - # when using "enhanced turn on" - self._off_brightness = self._attr_brightness - if transition is not None: - # save for when calling turn_on without a brightness: - # current_level is set to 1 after transitioning to level 0, - # needed for correct state with light groups - self._attr_brightness = 1 - self._off_with_transition = transition is not None - - self.async_write_ha_state() - - async def async_handle_color_commands( - self, - temperature, - duration, - hs_color, - xy_color, - new_color_provided_while_off, - t_log, - ): - """Process ZCL color commands.""" - - transition_time = ( - self._DEFAULT_MIN_TRANSITION_TIME - if new_color_provided_while_off - else duration - ) - - if temperature is not None: - result = await self._color_cluster_handler.move_to_color_temp( - color_temp_mireds=temperature, - transition_time=int(10 * transition_time), - ) - t_log["move_to_color_temp"] = result - if result[1] is not Status.SUCCESS: - return False - self._attr_color_mode = ColorMode.COLOR_TEMP - self._attr_color_temp = temperature - self._attr_xy_color = None - self._attr_hs_color = None - - if hs_color is not None: - if ( - not isinstance(self, LightGroup) - and self._color_cluster_handler.enhanced_hue_supported - ): - result = await self._color_cluster_handler.enhanced_move_to_hue_and_saturation( - enhanced_hue=int(hs_color[0] * 65535 / 360), - saturation=int(hs_color[1] * 2.54), - transition_time=int(10 * transition_time), - ) - t_log["enhanced_move_to_hue_and_saturation"] = result - else: - result = await self._color_cluster_handler.move_to_hue_and_saturation( - hue=int(hs_color[0] * 254 / 360), - saturation=int(hs_color[1] * 2.54), - transition_time=int(10 * transition_time), - ) - t_log["move_to_hue_and_saturation"] = result - if result[1] is not Status.SUCCESS: - return False - self._attr_color_mode = ColorMode.HS - self._attr_hs_color = hs_color - self._attr_xy_color = None - self._attr_color_temp = None - xy_color = None # don't set xy_color if it is also present - - if xy_color is not None: - result = await self._color_cluster_handler.move_to_color( - color_x=int(xy_color[0] * 65535), - color_y=int(xy_color[1] * 65535), - transition_time=int(10 * transition_time), - ) - t_log["move_to_color"] = result - if result[1] is not Status.SUCCESS: - return False - self._attr_color_mode = ColorMode.XY - self._attr_xy_color = xy_color - self._attr_color_temp = None - self._attr_hs_color = None - - return True + return self.entity_data.entity.is_on @property - def is_transitioning(self) -> bool: - """Return if the light is transitioning.""" - return self._transitioning_individual or self._transitioning_group + def brightness(self) -> int: + """Return the brightness of this light.""" + return self.entity_data.entity.brightness - @callback - def async_transition_set_flag(self) -> None: - """Set _transitioning to True.""" - self.debug("setting transitioning flag to True") - self._transitioning_individual = True - self._transitioning_group = False - if isinstance(self, LightGroup): - async_dispatcher_send( - self.hass, - SIGNAL_LIGHT_GROUP_TRANSITION_START, - {"entity_ids": self._entity_ids}, - ) - self._async_unsub_transition_listener() - - @callback - def async_transition_start_timer(self, transition_time) -> None: - """Start a timer to unset _transitioning_individual after transition_time. - - If necessary. - """ - if not transition_time: - return - # For longer transitions, we want to extend the timer a bit more - if transition_time >= DEFAULT_LONG_TRANSITION_TIME: - transition_time += DEFAULT_EXTRA_TRANSITION_DELAY_LONG - self.debug("starting transitioning timer for %s", transition_time) - self._transition_listener = async_call_later( - self._zha_device.hass, - transition_time, - self.async_transition_complete, - ) - - @callback - def _async_unsub_transition_listener(self) -> None: - """Unsubscribe transition listener.""" - if self._transition_listener: - self._transition_listener() - self._transition_listener = None - - @callback - def async_transition_complete(self, _=None) -> None: - """Set _transitioning_individual to False and write HA state.""" - self.debug("transition complete - future attribute reports will write HA state") - self._transitioning_individual = False - self._async_unsub_transition_listener() - self.async_write_ha_state() - if isinstance(self, LightGroup): - async_dispatcher_send( - self.hass, - SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED, - {"entity_ids": self._entity_ids}, - ) - if self._debounced_member_refresh is not None: - self.debug("transition complete - refreshing group member states") - assert self.platform.config_entry - self.platform.config_entry.async_create_background_task( - self.hass, - self._debounced_member_refresh.async_call(), - "zha.light-refresh-debounced-member", - ) - - -@STRICT_MATCH( - cluster_handler_names=CLUSTER_HANDLER_ON_OFF, - aux_cluster_handlers={CLUSTER_HANDLER_COLOR, CLUSTER_HANDLER_LEVEL}, -) -class Light(BaseLight, ZhaEntity): - """Representation of a ZHA or ZLL light.""" - - _attr_supported_color_modes: set[ColorMode] - _attr_translation_key: str = "light" - _REFRESH_INTERVAL = (45, 75) - - def __init__( - self, unique_id, zha_device: ZHADevice, cluster_handlers, **kwargs - ) -> None: - """Initialize the ZHA light.""" - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - self._on_off_cluster_handler = self.cluster_handlers[CLUSTER_HANDLER_ON_OFF] - self._attr_state = bool(self._on_off_cluster_handler.on_off) - self._level_cluster_handler = self.cluster_handlers.get(CLUSTER_HANDLER_LEVEL) - self._color_cluster_handler = self.cluster_handlers.get(CLUSTER_HANDLER_COLOR) - self._identify_cluster_handler = zha_device.identify_ch - if self._color_cluster_handler: - self._attr_min_mireds: int = self._color_cluster_handler.min_mireds - self._attr_max_mireds: int = self._color_cluster_handler.max_mireds - self._cancel_refresh_handle: CALLBACK_TYPE | None = None - effect_list = [] - - self._zha_config_always_prefer_xy_color_mode = async_get_zha_config_value( - zha_device.gateway.config_entry, - ZHA_OPTIONS, - CONF_ALWAYS_PREFER_XY_COLOR_MODE, - True, - ) - - self._attr_supported_color_modes = {ColorMode.ONOFF} - if self._level_cluster_handler: - self._attr_supported_color_modes.add(ColorMode.BRIGHTNESS) - self._attr_supported_features |= light.LightEntityFeature.TRANSITION - self._attr_brightness = self._level_cluster_handler.current_level - - if self._color_cluster_handler: - if self._color_cluster_handler.color_temp_supported: - self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP) - self._attr_color_temp = self._color_cluster_handler.color_temperature - - if self._color_cluster_handler.xy_supported and ( - self._zha_config_always_prefer_xy_color_mode - or not self._color_cluster_handler.hs_supported - ): - self._attr_supported_color_modes.add(ColorMode.XY) - curr_x = self._color_cluster_handler.current_x - curr_y = self._color_cluster_handler.current_y - if curr_x is not None and curr_y is not None: - self._attr_xy_color = (curr_x / 65535, curr_y / 65535) - else: - self._attr_xy_color = (0, 0) - - if ( - self._color_cluster_handler.hs_supported - and not self._zha_config_always_prefer_xy_color_mode - ): - self._attr_supported_color_modes.add(ColorMode.HS) - if ( - self._color_cluster_handler.enhanced_hue_supported - and self._color_cluster_handler.enhanced_current_hue is not None - ): - curr_hue = ( - self._color_cluster_handler.enhanced_current_hue * 65535 / 360 - ) - elif self._color_cluster_handler.current_hue is not None: - curr_hue = self._color_cluster_handler.current_hue * 254 / 360 - else: - curr_hue = 0 - - if ( - curr_saturation := self._color_cluster_handler.current_saturation - ) is None: - curr_saturation = 0 - - self._attr_hs_color = ( - int(curr_hue), - int(curr_saturation * 2.54), - ) - - if self._color_cluster_handler.color_loop_supported: - self._attr_supported_features |= light.LightEntityFeature.EFFECT - effect_list.append(light.EFFECT_COLORLOOP) - if self._color_cluster_handler.color_loop_active == 1: - self._attr_effect = light.EFFECT_COLORLOOP - self._attr_supported_color_modes = filter_supported_color_modes( - self._attr_supported_color_modes - ) - if len(self._attr_supported_color_modes) == 1: - self._attr_color_mode = next(iter(self._attr_supported_color_modes)) - else: # Light supports color_temp + hs, determine which mode the light is in - assert self._color_cluster_handler - if ( - self._color_cluster_handler.color_mode - == Color.ColorMode.Color_temperature - ): - self._attr_color_mode = ColorMode.COLOR_TEMP - else: - self._attr_color_mode = ColorMode.XY - - if self._identify_cluster_handler: - self._attr_supported_features |= light.LightEntityFeature.FLASH - - if effect_list: - self._attr_effect_list = effect_list - - self._zha_config_transition = async_get_zha_config_value( - zha_device.gateway.config_entry, - ZHA_OPTIONS, - CONF_DEFAULT_LIGHT_TRANSITION, - 0, - ) - self._zha_config_enhanced_light_transition = async_get_zha_config_value( - zha_device.gateway.config_entry, - ZHA_OPTIONS, - CONF_ENABLE_ENHANCED_LIGHT_TRANSITION, - False, - ) - self._zha_config_enable_light_transitioning_flag = async_get_zha_config_value( - zha_device.gateway.config_entry, - ZHA_OPTIONS, - CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, - True, - ) - - @callback - def async_set_state(self, attr_id, attr_name, value): - """Set the state.""" - if self.is_transitioning: - self.debug( - "received onoff %s while transitioning - skipping update", - value, - ) - return - self._attr_state = bool(value) - if value: - self._off_with_transition = False - self._off_brightness = None - self.async_write_ha_state() - - async def async_added_to_hass(self) -> None: - """Run when about to be added to hass.""" - await super().async_added_to_hass() - self.async_accept_signal( - self._on_off_cluster_handler, SIGNAL_ATTR_UPDATED, self.async_set_state - ) - if self._level_cluster_handler: - self.async_accept_signal( - self._level_cluster_handler, SIGNAL_SET_LEVEL, self.set_level - ) - refresh_interval = random.randint(*(x * 60 for x in self._REFRESH_INTERVAL)) - self._cancel_refresh_handle = async_track_time_interval( - self.hass, self._refresh, timedelta(seconds=refresh_interval) - ) - self.debug("started polling with refresh interval of %s", refresh_interval) - self.async_accept_signal( - None, - SIGNAL_LIGHT_GROUP_STATE_CHANGED, - self._maybe_force_refresh, - signal_override=True, - ) - - @callback - def transition_on(signal): - """Handle a transition start event from a group.""" - if self.entity_id in signal["entity_ids"]: - self.debug( - "group transition started - setting member transitioning flag" - ) - self._transitioning_group = True - - self.async_accept_signal( - None, - SIGNAL_LIGHT_GROUP_TRANSITION_START, - transition_on, - signal_override=True, - ) - - @callback - def transition_off(signal): - """Handle a transition finished event from a group.""" - if self.entity_id in signal["entity_ids"]: - self.debug( - "group transition completed - unsetting member transitioning flag" - ) - self._transitioning_group = False - - self.async_accept_signal( - None, - SIGNAL_LIGHT_GROUP_TRANSITION_FINISHED, - transition_off, - signal_override=True, - ) - - self.async_accept_signal( - None, - SIGNAL_LIGHT_GROUP_ASSUME_GROUP_STATE, - self._assume_group_state, - signal_override=True, - ) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect entity object when removed.""" - assert self._cancel_refresh_handle - self._cancel_refresh_handle() - self._cancel_refresh_handle = None - self.debug("stopped polling during device removal") - await super().async_will_remove_from_hass() - - @callback - def async_restore_last_state(self, last_state): - """Restore previous state.""" - self._attr_state = last_state.state == STATE_ON - if "brightness" in last_state.attributes: - self._attr_brightness = last_state.attributes["brightness"] - if "off_with_transition" in last_state.attributes: - self._off_with_transition = last_state.attributes["off_with_transition"] - if "off_brightness" in last_state.attributes: - self._off_brightness = last_state.attributes["off_brightness"] - if (color_mode := last_state.attributes.get("color_mode")) is not None: - self._attr_color_mode = ColorMode(color_mode) - if "color_temp" in last_state.attributes: - self._attr_color_temp = last_state.attributes["color_temp"] - if "xy_color" in last_state.attributes: - self._attr_xy_color = last_state.attributes["xy_color"] - if "hs_color" in last_state.attributes: - self._attr_hs_color = last_state.attributes["hs_color"] - if "effect" in last_state.attributes: - self._attr_effect = last_state.attributes["effect"] - - async def async_get_state(self) -> None: - """Attempt to retrieve the state from the light.""" - if not self._attr_available: - return - self.debug("polling current state") - - if self._on_off_cluster_handler: - state = await self._on_off_cluster_handler.get_attribute_value( - "on_off", from_cache=False - ) - # check if transition started whilst waiting for polled state - if self.is_transitioning: - return - - if state is not None: - self._attr_state = state - if state: # reset "off with transition" flag if the light is on - self._off_with_transition = False - self._off_brightness = None - - if self._level_cluster_handler: - level = await self._level_cluster_handler.get_attribute_value( - "current_level", from_cache=False - ) - # check if transition started whilst waiting for polled state - if self.is_transitioning: - return - if level is not None: - self._attr_brightness = level - - if self._color_cluster_handler: - attributes = [ - "color_mode", - "current_x", - "current_y", - ] - if ( - not self._zha_config_always_prefer_xy_color_mode - and self._color_cluster_handler.enhanced_hue_supported - ): - attributes.append("enhanced_current_hue") - attributes.append("current_saturation") - if ( - self._color_cluster_handler.hs_supported - and not self._color_cluster_handler.enhanced_hue_supported - and not self._zha_config_always_prefer_xy_color_mode - ): - attributes.append("current_hue") - attributes.append("current_saturation") - if self._color_cluster_handler.color_temp_supported: - attributes.append("color_temperature") - if self._color_cluster_handler.color_loop_supported: - attributes.append("color_loop_active") - - results = await self._color_cluster_handler.get_attributes( - attributes, from_cache=False, only_cache=False - ) - - # although rare, a transition might have been started while we were waiting - # for the polled attributes, so abort if we are transitioning, - # as that state will not be accurate - if self.is_transitioning: - return - - if (color_mode := results.get("color_mode")) is not None: - if color_mode == Color.ColorMode.Color_temperature: - self._attr_color_mode = ColorMode.COLOR_TEMP - color_temp = results.get("color_temperature") - if color_temp is not None and color_mode: - self._attr_color_temp = color_temp - self._attr_xy_color = None - self._attr_hs_color = None - elif ( - color_mode == Color.ColorMode.Hue_and_saturation - and not self._zha_config_always_prefer_xy_color_mode - ): - self._attr_color_mode = ColorMode.HS - if self._color_cluster_handler.enhanced_hue_supported: - current_hue = results.get("enhanced_current_hue") - else: - current_hue = results.get("current_hue") - current_saturation = results.get("current_saturation") - if current_hue is not None and current_saturation is not None: - self._attr_hs_color = ( - int(current_hue * 360 / 65535) - if self._color_cluster_handler.enhanced_hue_supported - else int(current_hue * 360 / 254), - int(current_saturation / 2.54), - ) - self._attr_xy_color = None - self._attr_color_temp = None - else: - self._attr_color_mode = ColorMode.XY - color_x = results.get("current_x") - color_y = results.get("current_y") - if color_x is not None and color_y is not None: - self._attr_xy_color = (color_x / 65535, color_y / 65535) - self._attr_color_temp = None - self._attr_hs_color = None - - color_loop_active = results.get("color_loop_active") - if color_loop_active is not None: - if color_loop_active == 1: - self._attr_effect = light.EFFECT_COLORLOOP - else: - self._attr_effect = None - - async def async_update(self) -> None: - """Update to the latest state.""" - if self.is_transitioning: - self.debug("skipping async_update while transitioning") - return - await self.async_get_state() - - async def _refresh(self, time): - """Call async_get_state at an interval.""" - if self.is_transitioning: - self.debug("skipping _refresh while transitioning") - return - if self._zha_device.available and self.hass.data[DATA_ZHA].allow_polling: - self.debug("polling for updated state") - await self.async_get_state() - self.async_write_ha_state() - else: - self.debug( - "skipping polling for updated state, available: %s, allow polled requests: %s", - self._zha_device.available, - self.hass.data[DATA_ZHA].allow_polling, - ) - - async def _maybe_force_refresh(self, signal): - """Force update the state if the signal contains the entity id for this entity.""" - if self.entity_id in signal["entity_ids"]: - if self.is_transitioning: - self.debug("skipping _maybe_force_refresh while transitioning") - return - if self._zha_device.available and self.hass.data[DATA_ZHA].allow_polling: - self.debug("forcing polling for updated state") - await self.async_get_state() - self.async_write_ha_state() - else: - self.debug( - "skipping _maybe_force_refresh, available: %s, allow polled requests: %s", - self._zha_device.available, - self.hass.data[DATA_ZHA].allow_polling, - ) - - @callback - def _assume_group_state(self, signal, update_params) -> None: - """Handle an assume group state event from a group.""" - if self.entity_id in signal["entity_ids"] and self._attr_available: - self.debug("member assuming group state with: %s", update_params) - - state = update_params["state"] - brightness = update_params.get(light.ATTR_BRIGHTNESS) - color_mode = update_params.get(light.ATTR_COLOR_MODE) - color_temp = update_params.get(light.ATTR_COLOR_TEMP) - xy_color = update_params.get(light.ATTR_XY_COLOR) - hs_color = update_params.get(light.ATTR_HS_COLOR) - effect = update_params.get(light.ATTR_EFFECT) - - supported_modes = self._attr_supported_color_modes - - # unset "off brightness" and "off with transition" - # if group turned on this light - if state and not self._attr_state: - self._off_with_transition = False - self._off_brightness = None - - # set "off brightness" and "off with transition" - # if group turned off this light, and the light was not already off - # (to not override _off_with_transition) - elif ( - not state and self._attr_state and brightness_supported(supported_modes) - ): - # use individual brightness, instead of possibly averaged - # brightness from group - self._off_brightness = self._attr_brightness - self._off_with_transition = update_params["off_with_transition"] - - # Note: If individual lights have off_with_transition set, but not the - # group, and the group is then turned on without a level, individual lights - # might fall back to brightness level 1. - # Since all lights might need different brightness levels to be turned on, - # we can't use one group call. And making individual calls when turning on - # a ZHA group would cause a lot of traffic. In this case, - # turn_on should either just be called with a level or individual turn_on - # calls can be used. - - # state is always set (light.turn_on/light.turn_off) - self._attr_state = state - - # before assuming a group state attribute, check if the attribute - # was actually set in that call - if brightness is not None and brightness_supported(supported_modes): - self._attr_brightness = brightness - if color_mode is not None and color_mode in supported_modes: - self._attr_color_mode = color_mode - if color_temp is not None and ColorMode.COLOR_TEMP in supported_modes: - self._attr_color_temp = color_temp - if xy_color is not None and ColorMode.XY in supported_modes: - self._attr_xy_color = xy_color - if hs_color is not None and ColorMode.HS in supported_modes: - self._attr_hs_color = hs_color - # the effect is always deactivated in async_turn_on if not provided - if effect is None: - self._attr_effect = None - elif self._attr_effect_list and effect in self._attr_effect_list: - self._attr_effect = effect - - self.async_write_ha_state() - - -@STRICT_MATCH( - cluster_handler_names=CLUSTER_HANDLER_ON_OFF, - aux_cluster_handlers={CLUSTER_HANDLER_COLOR, CLUSTER_HANDLER_LEVEL}, - manufacturers={"Philips", "Signify Netherlands B.V."}, -) -class HueLight(Light): - """Representation of a HUE light which does not report attributes.""" - - _REFRESH_INTERVAL = (3, 5) - - -@STRICT_MATCH( - cluster_handler_names=CLUSTER_HANDLER_ON_OFF, - aux_cluster_handlers={CLUSTER_HANDLER_COLOR, CLUSTER_HANDLER_LEVEL}, - manufacturers={"Jasco", "Jasco Products", "Quotra-Vision", "eWeLight", "eWeLink"}, -) -class ForceOnLight(Light): - """Representation of a light which does not respect on/off for move_to_level_with_on_off commands.""" - - _FORCE_ON = True - - -@STRICT_MATCH( - cluster_handler_names=CLUSTER_HANDLER_ON_OFF, - aux_cluster_handlers={CLUSTER_HANDLER_COLOR, CLUSTER_HANDLER_LEVEL}, - manufacturers=DEFAULT_MIN_TRANSITION_MANUFACTURERS, -) -class MinTransitionLight(Light): - """Representation of a light which does not react to any "move to" calls with 0 as a transition.""" - - # Transitions are counted in 1/10th of a second increments, so this is the smallest - _DEFAULT_MIN_TRANSITION_TIME = 0.1 - - -@GROUP_MATCH() -class LightGroup(BaseLight, ZhaGroupEntity): - """Representation of a light group.""" - - _attr_translation_key: str = "light_group" - - def __init__( - self, - entity_ids: list[str], - unique_id: str, - group_id: int, - zha_device: ZHADevice, - **kwargs: Any, - ) -> None: - """Initialize a light group.""" - super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs) - group = self.zha_device.gateway.get_group(self._group_id) - - self._GROUP_SUPPORTS_EXECUTE_IF_OFF = True - - for member in group.members: - # Ensure we do not send group commands that violate the minimum transition - # time of any members. - if member.device.manufacturer in DEFAULT_MIN_TRANSITION_MANUFACTURERS: - self._DEFAULT_MIN_TRANSITION_TIME = ( - MinTransitionLight._DEFAULT_MIN_TRANSITION_TIME # noqa: SLF001 - ) - - # Check all group members to see if they support execute_if_off. - # If at least one member has a color cluster and doesn't support it, - # it's not used. - for endpoint in member.device._endpoints.values(): # noqa: SLF001 - for cluster_handler in endpoint.all_cluster_handlers.values(): - if ( - cluster_handler.name == CLUSTER_HANDLER_COLOR - and not cluster_handler.execute_if_off_supported - ): - self._GROUP_SUPPORTS_EXECUTE_IF_OFF = False - break - - self._on_off_cluster_handler = group.endpoint[OnOff.cluster_id] - self._level_cluster_handler = group.endpoint[LevelControl.cluster_id] - self._color_cluster_handler = group.endpoint[Color.cluster_id] - self._identify_cluster_handler = group.endpoint[Identify.cluster_id] - self._debounced_member_refresh: Debouncer | None = None - self._zha_config_transition = async_get_zha_config_value( - zha_device.gateway.config_entry, - ZHA_OPTIONS, - CONF_DEFAULT_LIGHT_TRANSITION, - 0, - ) - self._zha_config_enable_light_transitioning_flag = async_get_zha_config_value( - zha_device.gateway.config_entry, - ZHA_OPTIONS, - CONF_ENABLE_LIGHT_TRANSITIONING_FLAG, - True, - ) - self._zha_config_always_prefer_xy_color_mode = async_get_zha_config_value( - zha_device.gateway.config_entry, - ZHA_OPTIONS, - CONF_ALWAYS_PREFER_XY_COLOR_MODE, - True, - ) - self._zha_config_group_members_assume_state = async_get_zha_config_value( - zha_device.gateway.config_entry, - ZHA_OPTIONS, - CONF_GROUP_MEMBERS_ASSUME_STATE, - True, - ) - if self._zha_config_group_members_assume_state: - self._update_group_from_child_delay = ASSUME_UPDATE_GROUP_FROM_CHILD_DELAY - self._zha_config_enhanced_light_transition = False - - self._attr_color_mode = ColorMode.UNKNOWN - self._attr_supported_color_modes = {ColorMode.ONOFF} - - # remove this when all ZHA platforms and base entities are updated @property - def available(self) -> bool: - """Return entity availability.""" - return self._attr_available + def min_mireds(self) -> int: + """Return the coldest color_temp that this light supports.""" + return self.entity_data.entity.min_mireds - async def async_added_to_hass(self) -> None: - """Run when about to be added to hass.""" - await super().async_added_to_hass() - if self._debounced_member_refresh is None: - force_refresh_debouncer = Debouncer( - self.hass, - _LOGGER, - cooldown=3, - immediate=True, - function=self._force_member_updates, - ) - self._debounced_member_refresh = force_refresh_debouncer - self.async_on_remove(force_refresh_debouncer.async_cancel) + @property + def max_mireds(self) -> int: + """Return the warmest color_temp that this light supports.""" + return self.entity_data.entity.max_mireds + @property + def hs_color(self) -> tuple[float, float] | None: + """Return the hs color value [int, int].""" + return self.entity_data.entity.hs_color + + @property + def xy_color(self) -> tuple[float, float] | None: + """Return the xy color value [float, float].""" + return self.entity_data.entity.xy_color + + @property + def color_temp(self) -> int | None: + """Return the CT color value in mireds.""" + return self.entity_data.entity.color_temp + + @property + def color_mode(self) -> ColorMode | None: + """Return the color mode.""" + if self.entity_data.entity.color_mode is None: + return None + return ZHA_TO_HA_COLOR_MODE[self.entity_data.entity.color_mode] + + @property + def effect_list(self) -> list[str] | None: + """Return the list of supported effects.""" + return self.entity_data.entity.effect_list + + @property + def effect(self) -> str | None: + """Return the current effect.""" + return self.entity_data.entity.effect + + @convert_zha_error_to_ha_error async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - # "off with transition" and "off brightness" will get overridden when - # turning on the group, but they are needed for setting the assumed - # member state correctly, so save them here - off_brightness = self._off_brightness if self._off_with_transition else None - await super().async_turn_on(**kwargs) - if self._zha_config_group_members_assume_state: - self._send_member_assume_state_event(True, kwargs, off_brightness) - if self.is_transitioning: # when transitioning, state is refreshed at the end - return - if self._debounced_member_refresh: - await self._debounced_member_refresh.async_call() + await self.entity_data.entity.async_turn_on( + transition=kwargs.get(ATTR_TRANSITION), + brightness=kwargs.get(ATTR_BRIGHTNESS), + effect=kwargs.get(ATTR_EFFECT), + flash=kwargs.get(ATTR_FLASH), + color_temp=kwargs.get(ATTR_COLOR_TEMP), + xy_color=kwargs.get(ATTR_XY_COLOR), + hs_color=kwargs.get(ATTR_HS_COLOR), + ) + self.async_write_ha_state() + @convert_zha_error_to_ha_error async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - await super().async_turn_off(**kwargs) - if self._zha_config_group_members_assume_state: - self._send_member_assume_state_event(False, kwargs) - if self.is_transitioning: - return - if self._debounced_member_refresh: - await self._debounced_member_refresh.async_call() - - async def async_update(self) -> None: - """Query all members and determine the light group state.""" - self.debug("updating group state") - all_states = [self.hass.states.get(x) for x in self._entity_ids] - states: list[State] = list(filter(None, all_states)) - on_states = [state for state in states if state.state == STATE_ON] - - self._attr_state = len(on_states) > 0 - - # reset "off with transition" flag if any member is on - if self._attr_state: - self._off_with_transition = False - self._off_brightness = None - - self._attr_available = any(state.state != STATE_UNAVAILABLE for state in states) - - self._attr_brightness = helpers.reduce_attribute( - on_states, light.ATTR_BRIGHTNESS + await self.entity_data.entity.async_turn_off( + transition=kwargs.get(ATTR_TRANSITION) ) + self.async_write_ha_state() - self._attr_xy_color = helpers.reduce_attribute( - on_states, light.ATTR_XY_COLOR, reduce=helpers.mean_tuple - ) - - if not self._zha_config_always_prefer_xy_color_mode: - self._attr_hs_color = helpers.reduce_attribute( - on_states, light.ATTR_HS_COLOR, reduce=helpers.mean_tuple - ) - - self._attr_color_temp = helpers.reduce_attribute( - on_states, light.ATTR_COLOR_TEMP - ) - self._attr_min_mireds = helpers.reduce_attribute( - states, light.ATTR_MIN_MIREDS, default=153, reduce=min - ) - self._attr_max_mireds = helpers.reduce_attribute( - states, light.ATTR_MAX_MIREDS, default=500, reduce=max - ) - - self._attr_effect_list = None - all_effect_lists = list( - helpers.find_state_attributes(states, light.ATTR_EFFECT_LIST) - ) - if all_effect_lists: - # Merge all effects from all effect_lists with a union merge. - self._attr_effect_list = list(set().union(*all_effect_lists)) - - self._attr_effect = None - all_effects = list(helpers.find_state_attributes(on_states, light.ATTR_EFFECT)) - if all_effects: - # Report the most common effect. - effects_count = Counter(itertools.chain(all_effects)) - self._attr_effect = effects_count.most_common(1)[0][0] - - supported_color_modes = {ColorMode.ONOFF} - all_supported_color_modes: list[set[ColorMode]] = list( - helpers.find_state_attributes(states, light.ATTR_SUPPORTED_COLOR_MODES) - ) - if all_supported_color_modes: - # Merge all color modes. - supported_color_modes = filter_supported_color_modes( - set().union(*all_supported_color_modes) - ) - - self._attr_supported_color_modes = supported_color_modes - - self._attr_color_mode = ColorMode.UNKNOWN - all_color_modes = list( - helpers.find_state_attributes(on_states, light.ATTR_COLOR_MODE) - ) - if all_color_modes: - # Report the most common color mode, select brightness and onoff last - color_mode_count = Counter(itertools.chain(all_color_modes)) - if ColorMode.ONOFF in color_mode_count: - if ColorMode.ONOFF in supported_color_modes: - color_mode_count[ColorMode.ONOFF] = -1 - else: - color_mode_count.pop(ColorMode.ONOFF) - if ColorMode.BRIGHTNESS in color_mode_count: - if ColorMode.BRIGHTNESS in supported_color_modes: - color_mode_count[ColorMode.BRIGHTNESS] = 0 - else: - color_mode_count.pop(ColorMode.BRIGHTNESS) - if color_mode_count: - self._attr_color_mode = color_mode_count.most_common(1)[0][0] - else: - self._attr_color_mode = next(iter(supported_color_modes)) - - if self._attr_color_mode == ColorMode.HS and ( - color_mode_count[ColorMode.HS] != len(self._group.members) - or self._zha_config_always_prefer_xy_color_mode - ): # switch to XY if all members do not support HS - self._attr_color_mode = ColorMode.XY - - self._attr_supported_features = LightEntityFeature(0) - for support in helpers.find_state_attributes(states, ATTR_SUPPORTED_FEATURES): - # Merge supported features by emulating support for every feature - # we find. - self._attr_supported_features |= support - # Bitwise-and the supported features with the GroupedLight's features - # so that we don't break in the future when a new feature is added. - self._attr_supported_features &= SUPPORT_GROUP_LIGHT - - async def _force_member_updates(self) -> None: - """Force the update of member entities to ensure the states are correct for bulbs that don't report their state.""" - async_dispatcher_send( - self.hass, - SIGNAL_LIGHT_GROUP_STATE_CHANGED, - {"entity_ids": self._entity_ids}, - ) - - def _send_member_assume_state_event( - self, state, service_kwargs, off_brightness=None - ) -> None: - """Send an assume event to all members of the group.""" - update_params = { - "state": state, - "off_with_transition": self._off_with_transition, - } - - # check if the parameters were actually updated - # in the service call before updating members - if light.ATTR_BRIGHTNESS in service_kwargs: # or off brightness - update_params[light.ATTR_BRIGHTNESS] = self._attr_brightness - elif off_brightness is not None: - # if we turn on the group light with "off brightness", - # pass that to the members - update_params[light.ATTR_BRIGHTNESS] = off_brightness - - if light.ATTR_COLOR_TEMP in service_kwargs: - update_params[light.ATTR_COLOR_MODE] = self._attr_color_mode - update_params[light.ATTR_COLOR_TEMP] = self._attr_color_temp - - if light.ATTR_XY_COLOR in service_kwargs: - update_params[light.ATTR_COLOR_MODE] = self._attr_color_mode - update_params[light.ATTR_XY_COLOR] = self._attr_xy_color - - if light.ATTR_HS_COLOR in service_kwargs: - update_params[light.ATTR_COLOR_MODE] = self._attr_color_mode - update_params[light.ATTR_HS_COLOR] = self._attr_hs_color - - if light.ATTR_EFFECT in service_kwargs: - update_params[light.ATTR_EFFECT] = self._attr_effect - - async_dispatcher_send( - self.hass, - SIGNAL_LIGHT_GROUP_ASSUME_GROUP_STATE, - {"entity_ids": self._entity_ids}, - update_params, + @callback + def restore_external_state_attributes(self, state: State) -> None: + """Restore entity state.""" + self.entity_data.entity.restore_external_state_attributes( + state=(state.state == STATE_ON), + off_with_transition=state.attributes.get(OFF_WITH_TRANSITION), + off_brightness=state.attributes.get(OFF_BRIGHTNESS), + brightness=state.attributes.get(ATTR_BRIGHTNESS), + color_temp=state.attributes.get(ATTR_COLOR_TEMP), + xy_color=state.attributes.get(ATTR_XY_COLOR), + hs_color=state.attributes.get(ATTR_HS_COLOR), + color_mode=( + HA_TO_ZHA_COLOR_MODE[ColorMode(state.attributes[ATTR_COLOR_MODE])] + if state.attributes.get(ATTR_COLOR_MODE) is not None + else None + ), + effect=state.attributes.get(ATTR_EFFECT), ) diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index fa719075c05..ebac03eb7b8 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -4,35 +4,25 @@ import functools from typing import Any import voluptuous as vol -from zigpy.zcl.foundation import Status -from homeassistant.components.lock import STATE_LOCKED, STATE_UNLOCKED, LockEntity +from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import ( AddEntitiesCallback, async_get_current_platform, ) -from homeassistant.helpers.typing import StateType -from .core import discovery -from .core.const import ( - CLUSTER_HANDLER_DOORLOCK, +from .entity import ZHAEntity +from .helpers import ( SIGNAL_ADD_ENTITIES, - SIGNAL_ATTR_UPDATED, + async_add_entities as zha_async_add_entities, + convert_zha_error_to_ha_error, + get_zha_data, ) -from .core.helpers import get_zha_data -from .core.registries import ZHA_ENTITIES -from .entity import ZhaEntity - -# The first state is Zigbee 'Not fully locked' -STATE_LIST = [STATE_UNLOCKED, STATE_LOCKED, STATE_UNLOCKED] -MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.LOCK) - -VALUE_TO_STATE = dict(enumerate(STATE_LIST)) SERVICE_SET_LOCK_USER_CODE = "set_lock_user_code" SERVICE_ENABLE_LOCK_USER_CODE = "enable_lock_user_code" @@ -53,7 +43,7 @@ async def async_setup_entry( hass, SIGNAL_ADD_ENTITIES, functools.partial( - discovery.async_add_entities, async_add_entities, entities_to_create + zha_async_add_entities, async_add_entities, ZhaDoorLock, entities_to_create ), ) config_entry.async_on_unload(unsub) @@ -94,105 +84,57 @@ async def async_setup_entry( ) -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_DOORLOCK) -class ZhaDoorLock(ZhaEntity, LockEntity): +class ZhaDoorLock(ZHAEntity, LockEntity): """Representation of a ZHA lock.""" _attr_translation_key: str = "door_lock" - def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs): - """Init this sensor.""" - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - self._doorlock_cluster_handler = self.cluster_handlers.get( - CLUSTER_HANDLER_DOORLOCK - ) - - async def async_added_to_hass(self) -> None: - """Run when about to be added to hass.""" - await super().async_added_to_hass() - self.async_accept_signal( - self._doorlock_cluster_handler, SIGNAL_ATTR_UPDATED, self.async_set_state - ) - - @callback - def async_restore_last_state(self, last_state): - """Restore previous state.""" - self._state = VALUE_TO_STATE.get(last_state.state, last_state.state) - @property def is_locked(self) -> bool: """Return true if entity is locked.""" - if self._state is None: - return False - return self._state == STATE_LOCKED - - @property - def extra_state_attributes(self) -> dict[str, StateType]: - """Return state attributes.""" - return self.state_attributes + return self.entity_data.entity.is_locked + @convert_zha_error_to_ha_error async def async_lock(self, **kwargs: Any) -> None: """Lock the lock.""" - result = await self._doorlock_cluster_handler.lock_door() - if result[0] is not Status.SUCCESS: - self.error("Error with lock_door: %s", result) - return + await self.entity_data.entity.async_lock() self.async_write_ha_state() + @convert_zha_error_to_ha_error async def async_unlock(self, **kwargs: Any) -> None: """Unlock the lock.""" - result = await self._doorlock_cluster_handler.unlock_door() - if result[0] is not Status.SUCCESS: - self.error("Error with unlock_door: %s", result) - return + await self.entity_data.entity.async_unlock() self.async_write_ha_state() - async def async_update(self) -> None: - """Attempt to retrieve state from the lock.""" - await super().async_update() - await self.async_get_state() - - @callback - def async_set_state(self, attr_id, attr_name, value): - """Handle state update from cluster handler.""" - self._state = VALUE_TO_STATE.get(value, self._state) - self.async_write_ha_state() - - async def async_get_state(self, from_cache=True): - """Attempt to retrieve state from the lock.""" - if self._doorlock_cluster_handler: - state = await self._doorlock_cluster_handler.get_attribute_value( - "lock_state", from_cache=from_cache - ) - if state is not None: - self._state = VALUE_TO_STATE.get(state, self._state) - - async def refresh(self, time): - """Call async_get_state at an interval.""" - await self.async_get_state(from_cache=False) - + @convert_zha_error_to_ha_error async def async_set_lock_user_code(self, code_slot: int, user_code: str) -> None: """Set the user_code to index X on the lock.""" - if self._doorlock_cluster_handler: - await self._doorlock_cluster_handler.async_set_user_code( - code_slot, user_code - ) - self.debug("User code at slot %s set", code_slot) + await self.entity_data.entity.async_set_lock_user_code( + code_slot=code_slot, user_code=user_code + ) + self.async_write_ha_state() + @convert_zha_error_to_ha_error async def async_enable_lock_user_code(self, code_slot: int) -> None: """Enable user_code at index X on the lock.""" - if self._doorlock_cluster_handler: - await self._doorlock_cluster_handler.async_enable_user_code(code_slot) - self.debug("User code at slot %s enabled", code_slot) + await self.entity_data.entity.async_enable_lock_user_code(code_slot=code_slot) + self.async_write_ha_state() + @convert_zha_error_to_ha_error async def async_disable_lock_user_code(self, code_slot: int) -> None: """Disable user_code at index X on the lock.""" - if self._doorlock_cluster_handler: - await self._doorlock_cluster_handler.async_disable_user_code(code_slot) - self.debug("User code at slot %s disabled", code_slot) + await self.entity_data.entity.async_disable_lock_user_code(code_slot=code_slot) + self.async_write_ha_state() + @convert_zha_error_to_ha_error async def async_clear_lock_user_code(self, code_slot: int) -> None: """Clear the user_code at index X on the lock.""" - if self._doorlock_cluster_handler: - await self._doorlock_cluster_handler.async_clear_user_code(code_slot) - self.debug("User code at slot %s cleared", code_slot) + await self.entity_data.entity.async_clear_lock_user_code(code_slot=code_slot) + self.async_write_ha_state() + + @callback + def restore_external_state_attributes(self, state: State) -> None: + """Restore entity state.""" + self.entity_data.entity.restore_external_state_attributes( + state=state.state, + ) diff --git a/homeassistant/components/zha/logbook.py b/homeassistant/components/zha/logbook.py index e63ef565824..3de81e1255d 100644 --- a/homeassistant/components/zha/logbook.py +++ b/homeassistant/components/zha/logbook.py @@ -5,16 +5,18 @@ from __future__ import annotations from collections.abc import Callable from typing import TYPE_CHECKING +from zha.application.const import ZHA_EVENT + from homeassistant.components.logbook import LOGBOOK_ENTRY_MESSAGE, LOGBOOK_ENTRY_NAME from homeassistant.const import ATTR_COMMAND, ATTR_DEVICE_ID from homeassistant.core import Event, HomeAssistant, callback import homeassistant.helpers.device_registry as dr -from .core.const import DOMAIN as ZHA_DOMAIN, ZHA_EVENT -from .core.helpers import async_get_zha_device +from .const import DOMAIN as ZHA_DOMAIN +from .helpers import async_get_zha_device_proxy if TYPE_CHECKING: - from .core.device import ZHADevice + from zha.zigbee.device import Device @callback @@ -30,7 +32,7 @@ def async_describe_events( """Describe ZHA logbook event.""" device: dr.DeviceEntry | None = None device_name: str = "Unknown device" - zha_device: ZHADevice | None = None + zha_device: Device | None = None event_data = event.data event_type: str | None = None event_subtype: str | None = None @@ -39,7 +41,9 @@ def async_describe_events( device = device_registry.devices[event.data[ATTR_DEVICE_ID]] if device: device_name = device.name_by_user or device.name or "Unknown device" - zha_device = async_get_zha_device(hass, event.data[ATTR_DEVICE_ID]) + zha_device = async_get_zha_device_proxy( + hass, event.data[ATTR_DEVICE_ID] + ).device except (KeyError, AttributeError): pass diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 7087ff0b2f0..1b538690079 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -18,20 +18,10 @@ "zigpy_xbee", "zigpy_zigate", "zigpy_znp", + "zha", "universal_silabs_flasher" ], - "requirements": [ - "bellows==0.39.1", - "pyserial==3.5", - "zha-quirks==0.0.117", - "zigpy-deconz==0.23.2", - "zigpy==0.64.1", - "zigpy-xbee==0.20.1", - "zigpy-zigate==0.12.1", - "zigpy-znp==0.12.2", - "universal-silabs-flasher==0.0.20", - "pyserial-asyncio-fast==0.11" - ], + "requirements": ["universal-silabs-flasher==0.0.20", "zha==0.0.18"], "usb": [ { "vid": "10C4", diff --git a/homeassistant/components/zha/number.py b/homeassistant/components/zha/number.py index 9320b4494a4..263f5262994 100644 --- a/homeassistant/components/zha/number.py +++ b/homeassistant/components/zha/number.py @@ -4,267 +4,25 @@ from __future__ import annotations import functools import logging -from typing import TYPE_CHECKING, Any, Self -from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT -from zigpy.quirks.v2 import NumberMetadata -from zigpy.zcl.clusters.hvac import Thermostat - -from homeassistant.components.number import NumberDeviceClass, NumberEntity, NumberMode +from homeassistant.components.number import RestoreNumber from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - EntityCategory, - Platform, - UnitOfMass, - UnitOfTemperature, - UnitOfTime, -) -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import UndefinedType -from .core import discovery -from .core.const import ( - CLUSTER_HANDLER_ANALOG_OUTPUT, - CLUSTER_HANDLER_BASIC, - CLUSTER_HANDLER_COLOR, - CLUSTER_HANDLER_INOVELLI, - CLUSTER_HANDLER_LEVEL, - CLUSTER_HANDLER_OCCUPANCY, - CLUSTER_HANDLER_THERMOSTAT, - ENTITY_METADATA, +from .entity import ZHAEntity +from .helpers import ( SIGNAL_ADD_ENTITIES, - SIGNAL_ATTR_UPDATED, + async_add_entities as zha_async_add_entities, + convert_zha_error_to_ha_error, + get_zha_data, ) -from .core.helpers import get_zha_data, validate_device_class, validate_unit -from .core.registries import ZHA_ENTITIES -from .entity import ZhaEntity - -if TYPE_CHECKING: - from .core.cluster_handlers import ClusterHandler - from .core.device import ZHADevice _LOGGER = logging.getLogger(__name__) -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.NUMBER) -CONFIG_DIAGNOSTIC_MATCH = functools.partial( - ZHA_ENTITIES.config_diagnostic_match, Platform.NUMBER -) - - -UNITS = { - 0: "Square-meters", - 1: "Square-feet", - 2: "Milliamperes", - 3: "Amperes", - 4: "Ohms", - 5: "Volts", - 6: "Kilo-volts", - 7: "Mega-volts", - 8: "Volt-amperes", - 9: "Kilo-volt-amperes", - 10: "Mega-volt-amperes", - 11: "Volt-amperes-reactive", - 12: "Kilo-volt-amperes-reactive", - 13: "Mega-volt-amperes-reactive", - 14: "Degrees-phase", - 15: "Power-factor", - 16: "Joules", - 17: "Kilojoules", - 18: "Watt-hours", - 19: "Kilowatt-hours", - 20: "BTUs", - 21: "Therms", - 22: "Ton-hours", - 23: "Joules-per-kilogram-dry-air", - 24: "BTUs-per-pound-dry-air", - 25: "Cycles-per-hour", - 26: "Cycles-per-minute", - 27: "Hertz", - 28: "Grams-of-water-per-kilogram-dry-air", - 29: "Percent-relative-humidity", - 30: "Millimeters", - 31: "Meters", - 32: "Inches", - 33: "Feet", - 34: "Watts-per-square-foot", - 35: "Watts-per-square-meter", - 36: "Lumens", - 37: "Luxes", - 38: "Foot-candles", - 39: "Kilograms", - 40: "Pounds-mass", - 41: "Tons", - 42: "Kilograms-per-second", - 43: "Kilograms-per-minute", - 44: "Kilograms-per-hour", - 45: "Pounds-mass-per-minute", - 46: "Pounds-mass-per-hour", - 47: "Watts", - 48: "Kilowatts", - 49: "Megawatts", - 50: "BTUs-per-hour", - 51: "Horsepower", - 52: "Tons-refrigeration", - 53: "Pascals", - 54: "Kilopascals", - 55: "Bars", - 56: "Pounds-force-per-square-inch", - 57: "Centimeters-of-water", - 58: "Inches-of-water", - 59: "Millimeters-of-mercury", - 60: "Centimeters-of-mercury", - 61: "Inches-of-mercury", - 62: "°C", - 63: "°K", - 64: "°F", - 65: "Degree-days-Celsius", - 66: "Degree-days-Fahrenheit", - 67: "Years", - 68: "Months", - 69: "Weeks", - 70: "Days", - 71: "Hours", - 72: "Minutes", - 73: "Seconds", - 74: "Meters-per-second", - 75: "Kilometers-per-hour", - 76: "Feet-per-second", - 77: "Feet-per-minute", - 78: "Miles-per-hour", - 79: "Cubic-feet", - 80: "Cubic-meters", - 81: "Imperial-gallons", - 82: "Liters", - 83: "Us-gallons", - 84: "Cubic-feet-per-minute", - 85: "Cubic-meters-per-second", - 86: "Imperial-gallons-per-minute", - 87: "Liters-per-second", - 88: "Liters-per-minute", - 89: "Us-gallons-per-minute", - 90: "Degrees-angular", - 91: "Degrees-Celsius-per-hour", - 92: "Degrees-Celsius-per-minute", - 93: "Degrees-Fahrenheit-per-hour", - 94: "Degrees-Fahrenheit-per-minute", - 95: None, - 96: "Parts-per-million", - 97: "Parts-per-billion", - 98: "%", - 99: "Percent-per-second", - 100: "Per-minute", - 101: "Per-second", - 102: "Psi-per-Degree-Fahrenheit", - 103: "Radians", - 104: "Revolutions-per-minute", - 105: "Currency1", - 106: "Currency2", - 107: "Currency3", - 108: "Currency4", - 109: "Currency5", - 110: "Currency6", - 111: "Currency7", - 112: "Currency8", - 113: "Currency9", - 114: "Currency10", - 115: "Square-inches", - 116: "Square-centimeters", - 117: "BTUs-per-pound", - 118: "Centimeters", - 119: "Pounds-mass-per-second", - 120: "Delta-Degrees-Fahrenheit", - 121: "Delta-Degrees-Kelvin", - 122: "Kilohms", - 123: "Megohms", - 124: "Millivolts", - 125: "Kilojoules-per-kilogram", - 126: "Megajoules", - 127: "Joules-per-degree-Kelvin", - 128: "Joules-per-kilogram-degree-Kelvin", - 129: "Kilohertz", - 130: "Megahertz", - 131: "Per-hour", - 132: "Milliwatts", - 133: "Hectopascals", - 134: "Millibars", - 135: "Cubic-meters-per-hour", - 136: "Liters-per-hour", - 137: "Kilowatt-hours-per-square-meter", - 138: "Kilowatt-hours-per-square-foot", - 139: "Megajoules-per-square-meter", - 140: "Megajoules-per-square-foot", - 141: "Watts-per-square-meter-Degree-Kelvin", - 142: "Cubic-feet-per-second", - 143: "Percent-obscuration-per-foot", - 144: "Percent-obscuration-per-meter", - 145: "Milliohms", - 146: "Megawatt-hours", - 147: "Kilo-BTUs", - 148: "Mega-BTUs", - 149: "Kilojoules-per-kilogram-dry-air", - 150: "Megajoules-per-kilogram-dry-air", - 151: "Kilojoules-per-degree-Kelvin", - 152: "Megajoules-per-degree-Kelvin", - 153: "Newton", - 154: "Grams-per-second", - 155: "Grams-per-minute", - 156: "Tons-per-hour", - 157: "Kilo-BTUs-per-hour", - 158: "Hundredths-seconds", - 159: "Milliseconds", - 160: "Newton-meters", - 161: "Millimeters-per-second", - 162: "Millimeters-per-minute", - 163: "Meters-per-minute", - 164: "Meters-per-hour", - 165: "Cubic-meters-per-minute", - 166: "Meters-per-second-per-second", - 167: "Amperes-per-meter", - 168: "Amperes-per-square-meter", - 169: "Ampere-square-meters", - 170: "Farads", - 171: "Henrys", - 172: "Ohm-meters", - 173: "Siemens", - 174: "Siemens-per-meter", - 175: "Teslas", - 176: "Volts-per-degree-Kelvin", - 177: "Volts-per-meter", - 178: "Webers", - 179: "Candelas", - 180: "Candelas-per-square-meter", - 181: "Kelvins-per-hour", - 182: "Kelvins-per-minute", - 183: "Joule-seconds", - 185: "Square-meters-per-Newton", - 186: "Kilogram-per-cubic-meter", - 187: "Newton-seconds", - 188: "Newtons-per-meter", - 189: "Watts-per-meter-per-degree-Kelvin", -} - -ICONS = { - 0: "mdi:temperature-celsius", - 1: "mdi:water-percent", - 2: "mdi:gauge", - 3: "mdi:speedometer", - 4: "mdi:percent", - 5: "mdi:air-filter", - 6: "mdi:fan", - 7: "mdi:flash", - 8: "mdi:current-ac", - 9: "mdi:flash", - 10: "mdi:flash", - 11: "mdi:flash", - 12: "mdi:counter", - 13: "mdi:thermometer-lines", - 14: "mdi:timer", - 15: "mdi:palette", - 16: "mdi:brightness-percent", -} - async def async_setup_entry( hass: HomeAssistant, @@ -279,875 +37,53 @@ async def async_setup_entry( hass, SIGNAL_ADD_ENTITIES, functools.partial( - discovery.async_add_entities, - async_add_entities, - entities_to_create, + zha_async_add_entities, async_add_entities, ZhaNumber, entities_to_create ), ) config_entry.async_on_unload(unsub) -@STRICT_MATCH(cluster_handler_names=CLUSTER_HANDLER_ANALOG_OUTPUT) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class ZhaNumber(ZhaEntity, NumberEntity): +class ZhaNumber(ZHAEntity, RestoreNumber): """Representation of a ZHA Number entity.""" - _attr_translation_key: str = "number" - - def __init__( - self, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> None: - """Init this entity.""" - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - self._analog_output_cluster_handler = self.cluster_handlers[ - CLUSTER_HANDLER_ANALOG_OUTPUT - ] - - async def async_added_to_hass(self) -> None: - """Run when about to be added to hass.""" - await super().async_added_to_hass() - self.async_accept_signal( - self._analog_output_cluster_handler, - SIGNAL_ATTR_UPDATED, - self.async_set_state, - ) - - @property - def native_value(self) -> float | None: - """Return the current value.""" - return self._analog_output_cluster_handler.present_value - - @property - def native_min_value(self) -> float: - """Return the minimum value.""" - min_present_value = self._analog_output_cluster_handler.min_present_value - if min_present_value is not None: - return min_present_value - return 0 - - @property - def native_max_value(self) -> float: - """Return the maximum value.""" - max_present_value = self._analog_output_cluster_handler.max_present_value - if max_present_value is not None: - return max_present_value - return 1023 - - @property - def native_step(self) -> float | None: - """Return the value step.""" - resolution = self._analog_output_cluster_handler.resolution - if resolution is not None: - return resolution - return super().native_step - @property def name(self) -> str | UndefinedType | None: """Return the name of the number entity.""" - description = self._analog_output_cluster_handler.description - if description is not None and len(description) > 0: - return f"{super().name} {description}" - return super().name + if (description := self.entity_data.entity.description) is None: + return super().name + + # The name of this entity is reported by the device itself. + # For backwards compatibility, we keep the same format as before. This + # should probably be changed in the future to omit the prefix. + return f"{super().name} {description}" @property - def icon(self) -> str | None: - """Return the icon to be used for this entity.""" - application_type = self._analog_output_cluster_handler.application_type - if application_type is not None: - return ICONS.get(application_type >> 16, super().icon) - return super().icon - - @property - def native_unit_of_measurement(self) -> str | None: - """Return the unit the value is expressed in.""" - engineering_units = self._analog_output_cluster_handler.engineering_units - return UNITS.get(engineering_units) - - @callback - def async_set_state(self, attr_id, attr_name, value): - """Handle value update from cluster handler.""" - self.async_write_ha_state() - - async def async_set_native_value(self, value: float) -> None: - """Update the current value from HA.""" - await self._analog_output_cluster_handler.async_set_present_value(float(value)) - self.async_write_ha_state() - - async def async_update(self) -> None: - """Attempt to retrieve the state of the entity.""" - await super().async_update() - _LOGGER.debug("polling current state") - if self._analog_output_cluster_handler: - value = await self._analog_output_cluster_handler.get_attribute_value( - "present_value", from_cache=False - ) - _LOGGER.debug("read value=%s", value) - - -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity): - """Representation of a ZHA number configuration entity.""" - - _attr_entity_category = EntityCategory.CONFIG - _attr_native_step: float = 1.0 - _attr_multiplier: float = 1 - _attribute_name: str - - @classmethod - def create_entity( - cls, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> Self | None: - """Entity Factory. - - Return entity if it is a supported configuration, otherwise return None - """ - cluster_handler = cluster_handlers[0] - if ENTITY_METADATA not in kwargs and ( - cls._attribute_name in cluster_handler.cluster.unsupported_attributes - or cls._attribute_name not in cluster_handler.cluster.attributes_by_name - or cluster_handler.cluster.get(cls._attribute_name) is None - ): - _LOGGER.debug( - "%s is not supported - skipping %s entity creation", - cls._attribute_name, - cls.__name__, - ) - return None - - return cls(unique_id, zha_device, cluster_handlers, **kwargs) - - def __init__( - self, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> None: - """Init this number configuration entity.""" - self._cluster_handler: ClusterHandler = cluster_handlers[0] - if ENTITY_METADATA in kwargs: - self._init_from_quirks_metadata(kwargs[ENTITY_METADATA]) - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - - def _init_from_quirks_metadata(self, entity_metadata: NumberMetadata) -> None: - """Init this entity from the quirks metadata.""" - super()._init_from_quirks_metadata(entity_metadata) - self._attribute_name = entity_metadata.attribute_name - - if entity_metadata.min is not None: - self._attr_native_min_value = entity_metadata.min - if entity_metadata.max is not None: - self._attr_native_max_value = entity_metadata.max - if entity_metadata.step is not None: - self._attr_native_step = entity_metadata.step - if entity_metadata.multiplier is not None: - self._attr_multiplier = entity_metadata.multiplier - if entity_metadata.device_class is not None: - self._attr_device_class = validate_device_class( - NumberDeviceClass, - entity_metadata.device_class, - Platform.NUMBER.value, - _LOGGER, - ) - if entity_metadata.device_class is None and entity_metadata.unit is not None: - self._attr_native_unit_of_measurement = validate_unit( - entity_metadata.unit - ).value - - @property - def native_value(self) -> float: + def native_value(self) -> float | None: """Return the current value.""" - return ( - self._cluster_handler.cluster.get(self._attribute_name) - * self._attr_multiplier - ) - - async def async_set_native_value(self, value: float) -> None: - """Update the current value from HA.""" - await self._cluster_handler.write_attributes_safe( - {self._attribute_name: int(value / self._attr_multiplier)} - ) - self.async_write_ha_state() - - async def async_update(self) -> None: - """Attempt to retrieve the state of the entity.""" - await super().async_update() - _LOGGER.debug("polling current state") - if self._cluster_handler: - value = await self._cluster_handler.get_attribute_value( - self._attribute_name, from_cache=False - ) - _LOGGER.debug("read value=%s", value) - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", - models={"lumi.motion.ac02", "lumi.motion.agl04"}, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class AqaraMotionDetectionInterval(ZHANumberConfigurationEntity): - """Representation of a ZHA motion detection interval configuration entity.""" - - _unique_id_suffix = "detection_interval" - _attr_native_min_value: float = 2 - _attr_native_max_value: float = 65535 - _attribute_name = "detection_interval" - _attr_translation_key: str = "detection_interval" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_LEVEL) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class OnOffTransitionTimeConfigurationEntity(ZHANumberConfigurationEntity): - """Representation of a ZHA on off transition time configuration entity.""" - - _unique_id_suffix = "on_off_transition_time" - _attr_native_min_value: float = 0x0000 - _attr_native_max_value: float = 0xFFFF - _attribute_name = "on_off_transition_time" - _attr_translation_key: str = "on_off_transition_time" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_LEVEL) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class OnLevelConfigurationEntity(ZHANumberConfigurationEntity): - """Representation of a ZHA on level configuration entity.""" - - _unique_id_suffix = "on_level" - _attr_native_min_value: float = 0x00 - _attr_native_max_value: float = 0xFF - _attribute_name = "on_level" - _attr_translation_key: str = "on_level" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_LEVEL) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class OnTransitionTimeConfigurationEntity(ZHANumberConfigurationEntity): - """Representation of a ZHA on transition time configuration entity.""" - - _unique_id_suffix = "on_transition_time" - _attr_native_min_value: float = 0x0000 - _attr_native_max_value: float = 0xFFFE - _attribute_name = "on_transition_time" - _attr_translation_key: str = "on_transition_time" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_LEVEL) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class OffTransitionTimeConfigurationEntity(ZHANumberConfigurationEntity): - """Representation of a ZHA off transition time configuration entity.""" - - _unique_id_suffix = "off_transition_time" - _attr_native_min_value: float = 0x0000 - _attr_native_max_value: float = 0xFFFE - _attribute_name = "off_transition_time" - _attr_translation_key: str = "off_transition_time" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_LEVEL) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class DefaultMoveRateConfigurationEntity(ZHANumberConfigurationEntity): - """Representation of a ZHA default move rate configuration entity.""" - - _unique_id_suffix = "default_move_rate" - _attr_native_min_value: float = 0x00 - _attr_native_max_value: float = 0xFE - _attribute_name = "default_move_rate" - _attr_translation_key: str = "default_move_rate" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_LEVEL) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class StartUpCurrentLevelConfigurationEntity(ZHANumberConfigurationEntity): - """Representation of a ZHA startup current level configuration entity.""" - - _unique_id_suffix = "start_up_current_level" - _attr_native_min_value: float = 0x00 - _attr_native_max_value: float = 0xFF - _attribute_name = "start_up_current_level" - _attr_translation_key: str = "start_up_current_level" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_COLOR) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class StartUpColorTemperatureConfigurationEntity(ZHANumberConfigurationEntity): - """Representation of a ZHA startup color temperature configuration entity.""" - - _unique_id_suffix = "start_up_color_temperature" - _attr_native_min_value: float = 153 - _attr_native_max_value: float = 500 - _attribute_name = "start_up_color_temperature" - _attr_translation_key: str = "start_up_color_temperature" - - def __init__( - self, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> None: - """Init this ZHA startup color temperature entity.""" - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - if self._cluster_handler: - self._attr_native_min_value: float = self._cluster_handler.min_mireds - self._attr_native_max_value: float = self._cluster_handler.max_mireds - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="tuya_manufacturer", - manufacturers={ - "_TZE200_htnnfasr", - }, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class TimerDurationMinutes(ZHANumberConfigurationEntity): - """Representation of a ZHA timer duration configuration entity.""" - - _unique_id_suffix = "timer_duration" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 0x00 - _attr_native_max_value: float = 0x257 - _attr_native_unit_of_measurement: str | None = UNITS[72] - _attribute_name = "timer_duration" - _attr_translation_key: str = "timer_duration" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names="ikea_airpurifier") -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class FilterLifeTime(ZHANumberConfigurationEntity): - """Representation of a ZHA filter lifetime configuration entity.""" - - _unique_id_suffix = "filter_life_time" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 0x00 - _attr_native_max_value: float = 0xFFFFFFFF - _attr_native_unit_of_measurement: str | None = UNITS[72] - _attribute_name = "filter_life_time" - _attr_translation_key: str = "filter_life_time" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_BASIC, - manufacturers={"TexasInstruments"}, - models={"ti.router"}, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class TiRouterTransmitPower(ZHANumberConfigurationEntity): - """Representation of a ZHA TI transmit power configuration entity.""" - - _unique_id_suffix = "transmit_power" - _attr_native_min_value: float = -20 - _attr_native_max_value: float = 20 - _attribute_name = "transmit_power" - _attr_translation_key: str = "transmit_power" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class InovelliRemoteDimmingUpSpeed(ZHANumberConfigurationEntity): - """Inovelli remote dimming up speed configuration entity.""" - - _unique_id_suffix = "dimming_speed_up_remote" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 0 - _attr_native_max_value: float = 126 - _attribute_name = "dimming_speed_up_remote" - _attr_translation_key: str = "dimming_speed_up_remote" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class InovelliButtonDelay(ZHANumberConfigurationEntity): - """Inovelli button delay configuration entity.""" - - _unique_id_suffix = "button_delay" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 0 - _attr_native_max_value: float = 9 - _attribute_name = "button_delay" - _attr_translation_key: str = "button_delay" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class InovelliLocalDimmingUpSpeed(ZHANumberConfigurationEntity): - """Inovelli local dimming up speed configuration entity.""" - - _unique_id_suffix = "dimming_speed_up_local" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 0 - _attr_native_max_value: float = 127 - _attribute_name = "dimming_speed_up_local" - _attr_translation_key: str = "dimming_speed_up_local" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class InovelliLocalRampRateOffToOn(ZHANumberConfigurationEntity): - """Inovelli off to on local ramp rate configuration entity.""" - - _unique_id_suffix = "ramp_rate_off_to_on_local" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 0 - _attr_native_max_value: float = 127 - _attribute_name = "ramp_rate_off_to_on_local" - _attr_translation_key: str = "ramp_rate_off_to_on_local" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class InovelliRemoteDimmingSpeedOffToOn(ZHANumberConfigurationEntity): - """Inovelli off to on remote ramp rate configuration entity.""" - - _unique_id_suffix = "ramp_rate_off_to_on_remote" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 0 - _attr_native_max_value: float = 127 - _attribute_name = "ramp_rate_off_to_on_remote" - _attr_translation_key: str = "ramp_rate_off_to_on_remote" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class InovelliRemoteDimmingDownSpeed(ZHANumberConfigurationEntity): - """Inovelli remote dimming down speed configuration entity.""" - - _unique_id_suffix = "dimming_speed_down_remote" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 0 - _attr_native_max_value: float = 127 - _attribute_name = "dimming_speed_down_remote" - _attr_translation_key: str = "dimming_speed_down_remote" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class InovelliLocalDimmingDownSpeed(ZHANumberConfigurationEntity): - """Inovelli local dimming down speed configuration entity.""" - - _unique_id_suffix = "dimming_speed_down_local" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 0 - _attr_native_max_value: float = 127 - _attribute_name = "dimming_speed_down_local" - _attr_translation_key: str = "dimming_speed_down_local" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class InovelliLocalRampRateOnToOff(ZHANumberConfigurationEntity): - """Inovelli local on to off ramp rate configuration entity.""" - - _unique_id_suffix = "ramp_rate_on_to_off_local" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 0 - _attr_native_max_value: float = 127 - _attribute_name = "ramp_rate_on_to_off_local" - _attr_translation_key: str = "ramp_rate_on_to_off_local" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class InovelliRemoteDimmingSpeedOnToOff(ZHANumberConfigurationEntity): - """Inovelli remote on to off ramp rate configuration entity.""" - - _unique_id_suffix = "ramp_rate_on_to_off_remote" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 0 - _attr_native_max_value: float = 127 - _attribute_name = "ramp_rate_on_to_off_remote" - _attr_translation_key: str = "ramp_rate_on_to_off_remote" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class InovelliMinimumLoadDimmingLevel(ZHANumberConfigurationEntity): - """Inovelli minimum load dimming level configuration entity.""" - - _unique_id_suffix = "minimum_level" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 1 - _attr_native_max_value: float = 254 - _attribute_name = "minimum_level" - _attr_translation_key: str = "minimum_level" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class InovelliMaximumLoadDimmingLevel(ZHANumberConfigurationEntity): - """Inovelli maximum load dimming level configuration entity.""" - - _unique_id_suffix = "maximum_level" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 2 - _attr_native_max_value: float = 255 - _attribute_name = "maximum_level" - _attr_translation_key: str = "maximum_level" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class InovelliAutoShutoffTimer(ZHANumberConfigurationEntity): - """Inovelli automatic switch shutoff timer configuration entity.""" - - _unique_id_suffix = "auto_off_timer" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 0 - _attr_native_max_value: float = 32767 - _attribute_name = "auto_off_timer" - _attr_translation_key: str = "auto_off_timer" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_INOVELLI, models={"VZM35-SN"} -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class InovelliQuickStartTime(ZHANumberConfigurationEntity): - """Inovelli fan quick start time configuration entity.""" - - _unique_id_suffix = "quick_start_time" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 0 - _attr_native_max_value: float = 10 - _attribute_name = "quick_start_time" - _attr_translation_key: str = "quick_start_time" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class InovelliLoadLevelIndicatorTimeout(ZHANumberConfigurationEntity): - """Inovelli load level indicator timeout configuration entity.""" - - _unique_id_suffix = "load_level_indicator_timeout" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 0 - _attr_native_max_value: float = 11 - _attribute_name = "load_level_indicator_timeout" - _attr_translation_key: str = "load_level_indicator_timeout" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class InovelliDefaultAllLEDOnColor(ZHANumberConfigurationEntity): - """Inovelli default all led color when on configuration entity.""" - - _unique_id_suffix = "led_color_when_on" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 0 - _attr_native_max_value: float = 255 - _attribute_name = "led_color_when_on" - _attr_translation_key: str = "led_color_when_on" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class InovelliDefaultAllLEDOffColor(ZHANumberConfigurationEntity): - """Inovelli default all led color when off configuration entity.""" - - _unique_id_suffix = "led_color_when_off" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 0 - _attr_native_max_value: float = 255 - _attribute_name = "led_color_when_off" - _attr_translation_key: str = "led_color_when_off" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class InovelliDefaultAllLEDOnIntensity(ZHANumberConfigurationEntity): - """Inovelli default all led intensity when on configuration entity.""" - - _unique_id_suffix = "led_intensity_when_on" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 0 - _attr_native_max_value: float = 100 - _attribute_name = "led_intensity_when_on" - _attr_translation_key: str = "led_intensity_when_on" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class InovelliDefaultAllLEDOffIntensity(ZHANumberConfigurationEntity): - """Inovelli default all led intensity when off configuration entity.""" - - _unique_id_suffix = "led_intensity_when_off" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 0 - _attr_native_max_value: float = 100 - _attribute_name = "led_intensity_when_off" - _attr_translation_key: str = "led_intensity_when_off" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class InovelliDoubleTapUpLevel(ZHANumberConfigurationEntity): - """Inovelli double tap up level configuration entity.""" - - _unique_id_suffix = "double_tap_up_level" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 2 - _attr_native_max_value: float = 254 - _attribute_name = "double_tap_up_level" - _attr_translation_key: str = "double_tap_up_level" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class InovelliDoubleTapDownLevel(ZHANumberConfigurationEntity): - """Inovelli double tap down level configuration entity.""" - - _unique_id_suffix = "double_tap_down_level" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 0 - _attr_native_max_value: float = 254 - _attribute_name = "double_tap_down_level" - _attr_translation_key: str = "double_tap_down_level" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"} -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class AqaraPetFeederServingSize(ZHANumberConfigurationEntity): - """Aqara pet feeder serving size configuration entity.""" - - _unique_id_suffix = "serving_size" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 1 - _attr_native_max_value: float = 10 - _attribute_name = "serving_size" - _attr_translation_key: str = "serving_size" - - _attr_mode: NumberMode = NumberMode.BOX - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"} -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class AqaraPetFeederPortionWeight(ZHANumberConfigurationEntity): - """Aqara pet feeder portion weight configuration entity.""" - - _unique_id_suffix = "portion_weight" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 1 - _attr_native_max_value: float = 100 - _attribute_name = "portion_weight" - _attr_translation_key: str = "portion_weight" - - _attr_mode: NumberMode = NumberMode.BOX - _attr_native_unit_of_measurement: str = UnitOfMass.GRAMS - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.airrtc.agl001"} -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class AqaraThermostatAwayTemp(ZHANumberConfigurationEntity): - """Aqara away preset temperature configuration entity.""" - - _unique_id_suffix = "away_preset_temperature" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: float = 5 - _attr_native_max_value: float = 30 - _attr_multiplier: float = 0.01 - _attribute_name = "away_preset_temperature" - _attr_translation_key: str = "away_preset_temperature" - - _attr_mode: NumberMode = NumberMode.SLIDER - _attr_native_unit_of_measurement: str = UnitOfTemperature.CELSIUS - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class ThermostatLocalTempCalibration(ZHANumberConfigurationEntity): - """Local temperature calibration.""" - - _unique_id_suffix = "local_temperature_calibration" - _attr_native_min_value: float = -2.5 - _attr_native_max_value: float = 2.5 - _attr_native_step: float = 0.1 - _attr_multiplier: float = 0.1 - _attribute_name = "local_temperature_calibration" - _attr_translation_key: str = "local_temperature_calibration" - - _attr_mode: NumberMode = NumberMode.SLIDER - _attr_native_unit_of_measurement: str = UnitOfTemperature.CELSIUS - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - models={"TRVZB"}, - stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class SonoffThermostatLocalTempCalibration(ThermostatLocalTempCalibration): - """Local temperature calibration for the Sonoff TRVZB.""" - - _attr_native_min_value: float = -7 - _attr_native_max_value: float = 7 - _attr_native_step: float = 0.2 - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_OCCUPANCY, models={"SNZB-06P"} -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class SonoffPresenceSenorTimeout(ZHANumberConfigurationEntity): - """Configuration of Sonoff sensor presence detection timeout.""" - - _unique_id_suffix = "presence_detection_timeout" - _attr_entity_category = EntityCategory.CONFIG - _attr_native_min_value: int = 15 - _attr_native_max_value: int = 60 - _attribute_name = "ultrasonic_o_to_u_delay" - _attr_translation_key: str = "presence_detection_timeout" - - _attr_mode: NumberMode = NumberMode.BOX - - -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class ZCLTemperatureEntity(ZHANumberConfigurationEntity): - """Common entity class for ZCL temperature input.""" - - _attr_native_unit_of_measurement: str = UnitOfTemperature.CELSIUS - _attr_mode: NumberMode = NumberMode.BOX - _attr_native_step: float = 0.01 - _attr_multiplier: float = 0.01 - - -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class ZCLHeatSetpointLimitEntity(ZCLTemperatureEntity): - """Min or max heat setpoint setting on thermostats.""" - - _attr_icon: str = "mdi:thermostat" - _attr_native_step: float = 0.5 - - _min_source = Thermostat.AttributeDefs.abs_min_heat_setpoint_limit.name - _max_source = Thermostat.AttributeDefs.abs_max_heat_setpoint_limit.name + return self.entity_data.entity.native_value @property def native_min_value(self) -> float: """Return the minimum value.""" - # The spec says 0x954D, which is a signed integer, therefore the value is in decimals - min_present_value = self._cluster_handler.cluster.get(self._min_source, -27315) - return min_present_value * self._attr_multiplier + return self.entity_data.entity.native_min_value @property def native_max_value(self) -> float: """Return the maximum value.""" - max_present_value = self._cluster_handler.cluster.get(self._max_source, 0x7FFF) - return max_present_value * self._attr_multiplier + return self.entity_data.entity.native_max_value + @property + def native_step(self) -> float | None: + """Return the value step.""" + return self.entity_data.entity.native_step -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class MaxHeatSetpointLimit(ZCLHeatSetpointLimitEntity): - """Max heat setpoint setting on thermostats. + @property + def native_unit_of_measurement(self) -> str | None: + """Return the unit the value is expressed in.""" + return self.entity_data.entity.native_unit_of_measurement - Optional thermostat attribute. - """ - - _unique_id_suffix = "max_heat_setpoint_limit" - _attribute_name: str = "max_heat_setpoint_limit" - _attr_translation_key: str = "max_heat_setpoint_limit" - _attr_entity_category = EntityCategory.CONFIG - - _min_source = Thermostat.AttributeDefs.min_heat_setpoint_limit.name - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class MinHeatSetpointLimit(ZCLHeatSetpointLimitEntity): - """Min heat setpoint setting on thermostats. - - Optional thermostat attribute. - """ - - _unique_id_suffix = "min_heat_setpoint_limit" - _attribute_name: str = "min_heat_setpoint_limit" - _attr_translation_key: str = "min_heat_setpoint_limit" - _attr_entity_category = EntityCategory.CONFIG - - _max_source = Thermostat.AttributeDefs.max_heat_setpoint_limit.name - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class DanfossExerciseTriggerTime(ZHANumberConfigurationEntity): - """Danfoss proprietary attribute to set the time to exercise the valve.""" - - _unique_id_suffix = "exercise_trigger_time" - _attribute_name: str = "exercise_trigger_time" - _attr_translation_key: str = "exercise_trigger_time" - _attr_native_min_value: int = 0 - _attr_native_max_value: int = 1439 - _attr_mode: NumberMode = NumberMode.BOX - _attr_native_unit_of_measurement: str = UnitOfTime.MINUTES - _attr_icon: str = "mdi:clock" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class DanfossExternalMeasuredRoomSensor(ZCLTemperatureEntity): - """Danfoss proprietary attribute to communicate the value of the external temperature sensor.""" - - _unique_id_suffix = "external_measured_room_sensor" - _attribute_name: str = "external_measured_room_sensor" - _attr_translation_key: str = "external_temperature_sensor" - _attr_native_min_value: float = -80 - _attr_native_max_value: float = 35 - _attr_icon: str = "mdi:thermometer" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class DanfossLoadRoomMean(ZHANumberConfigurationEntity): - """Danfoss proprietary attribute to set a value for the load.""" - - _unique_id_suffix = "load_room_mean" - _attribute_name: str = "load_room_mean" - _attr_translation_key: str = "load_room_mean" - _attr_native_min_value: int = -8000 - _attr_native_max_value: int = 2000 - _attr_mode: NumberMode = NumberMode.BOX - _attr_icon: str = "mdi:scale-balance" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class DanfossRegulationSetpointOffset(ZHANumberConfigurationEntity): - """Danfoss proprietary attribute to set the regulation setpoint offset.""" - - _unique_id_suffix = "regulation_setpoint_offset" - _attribute_name: str = "regulation_setpoint_offset" - _attr_translation_key: str = "regulation_setpoint_offset" - _attr_mode: NumberMode = NumberMode.BOX - _attr_native_unit_of_measurement: str = UnitOfTemperature.CELSIUS - _attr_icon: str = "mdi:thermostat" - _attr_native_min_value: float = -2.5 - _attr_native_max_value: float = 2.5 - _attr_native_step: float = 0.1 - _attr_multiplier = 1 / 10 + @convert_zha_error_to_ha_error + async def async_set_native_value(self, value: float) -> None: + """Update the current value from HA.""" + await self.entity_data.entity.async_set_native_value(value=value) + self.async_write_ha_state() diff --git a/homeassistant/components/zha/radio_manager.py b/homeassistant/components/zha/radio_manager.py index 44b7304c58e..9278b5da75f 100644 --- a/homeassistant/components/zha/radio_manager.py +++ b/homeassistant/components/zha/radio_manager.py @@ -13,6 +13,7 @@ from typing import Any, Self from bellows.config import CONF_USE_THREAD import voluptuous as vol +from zha.application.const import RadioType from zigpy.application import ControllerApplication import zigpy.backups from zigpy.config import ( @@ -29,14 +30,13 @@ from homeassistant.components import usb from homeassistant.core import HomeAssistant from . import repairs -from .core.const import ( +from .const import ( CONF_RADIO_TYPE, CONF_ZIGPY, DEFAULT_DATABASE_NAME, EZSP_OVERWRITE_EUI64, - RadioType, ) -from .core.helpers import get_zha_data +from .helpers import get_zha_data # Only the common radio types will be autoprobed, ordered by new device popularity. # XBee takes too long to probe since it scans through all possible bauds and likely has diff --git a/homeassistant/components/zha/repairs/__init__.py b/homeassistant/components/zha/repairs/__init__.py index 3d8f2553baa..3fcbdb66bbc 100644 --- a/homeassistant/components/zha/repairs/__init__.py +++ b/homeassistant/components/zha/repairs/__init__.py @@ -8,7 +8,7 @@ from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow from homeassistant.core import HomeAssistant from homeassistant.helpers import issue_registry as ir -from ..core.const import DOMAIN +from ..const import DOMAIN from .network_settings_inconsistent import ( ISSUE_INCONSISTENT_NETWORK_SETTINGS, NetworkSettingsInconsistentFlow, diff --git a/homeassistant/components/zha/repairs/network_settings_inconsistent.py b/homeassistant/components/zha/repairs/network_settings_inconsistent.py index 2598ff8f98a..ef38ebc3d47 100644 --- a/homeassistant/components/zha/repairs/network_settings_inconsistent.py +++ b/homeassistant/components/zha/repairs/network_settings_inconsistent.py @@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import issue_registry as ir -from ..core.const import DOMAIN +from ..const import DOMAIN from ..radio_manager import ZhaRadioManager _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/repairs/wrong_silabs_firmware.py b/homeassistant/components/zha/repairs/wrong_silabs_firmware.py index 3cd22c99ec7..4d6d1ae52d8 100644 --- a/homeassistant/components/zha/repairs/wrong_silabs_firmware.py +++ b/homeassistant/components/zha/repairs/wrong_silabs_firmware.py @@ -19,7 +19,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import issue_registry as ir -from ..core.const import DOMAIN +from ..const import DOMAIN _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/select.py b/homeassistant/components/zha/select.py index 026a85fbfdc..dfe9de24b40 100644 --- a/homeassistant/components/zha/select.py +++ b/homeassistant/components/zha/select.py @@ -2,56 +2,26 @@ from __future__ import annotations -from enum import Enum import functools import logging -from typing import TYPE_CHECKING, Any, Self - -from zhaquirks.danfoss import thermostat as danfoss_thermostat -from zhaquirks.quirk_ids import ( - DANFOSS_ALLY_THERMOSTAT, - TUYA_PLUG_MANUFACTURER, - TUYA_PLUG_ONOFF, -) -from zhaquirks.xiaomi.aqara.magnet_ac01 import OppleCluster as MagnetAC01OppleCluster -from zhaquirks.xiaomi.aqara.switch_acn047 import OppleCluster as T2RelayOppleCluster -from zigpy import types -from zigpy.quirks.v2 import ZCLEnumMetadata -from zigpy.zcl.clusters.general import OnOff -from zigpy.zcl.clusters.security import IasWd +from typing import Any from homeassistant.components.select import SelectEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_UNKNOWN, EntityCategory, Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import STATE_UNKNOWN, Platform +from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .core import discovery -from .core.const import ( - CLUSTER_HANDLER_HUE_OCCUPANCY, - CLUSTER_HANDLER_IAS_WD, - CLUSTER_HANDLER_INOVELLI, - CLUSTER_HANDLER_OCCUPANCY, - CLUSTER_HANDLER_ON_OFF, - CLUSTER_HANDLER_THERMOSTAT, - ENTITY_METADATA, +from .entity import ZHAEntity +from .helpers import ( SIGNAL_ADD_ENTITIES, - SIGNAL_ATTR_UPDATED, - Strobe, + EntityData, + async_add_entities as zha_async_add_entities, + convert_zha_error_to_ha_error, + get_zha_data, ) -from .core.helpers import get_zha_data -from .core.registries import ZHA_ENTITIES -from .entity import ZhaEntity -if TYPE_CHECKING: - from .core.cluster_handlers import ClusterHandler - from .core.device import ZHADevice - - -CONFIG_DIAGNOSTIC_MATCH = functools.partial( - ZHA_ENTITIES.config_diagnostic_match, Platform.SELECT -) _LOGGER = logging.getLogger(__name__) @@ -68,731 +38,38 @@ async def async_setup_entry( hass, SIGNAL_ADD_ENTITIES, functools.partial( - discovery.async_add_entities, + zha_async_add_entities, async_add_entities, + ZHAEnumSelectEntity, entities_to_create, ), ) config_entry.async_on_unload(unsub) -class ZHAEnumSelectEntity(ZhaEntity, SelectEntity): +class ZHAEnumSelectEntity(ZHAEntity, SelectEntity): """Representation of a ZHA select entity.""" - _attr_entity_category = EntityCategory.CONFIG - _attribute_name: str - _enum: type[Enum] - - def __init__( - self, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> None: - """Init this select entity.""" - self._cluster_handler: ClusterHandler = cluster_handlers[0] - self._attribute_name = self._enum.__name__ - self._attr_options = [entry.name.replace("_", " ") for entry in self._enum] - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) + def __init__(self, entity_data: EntityData, **kwargs: Any) -> None: + """Initialize the ZHA select entity.""" + super().__init__(entity_data, **kwargs) + self._attr_options = self.entity_data.entity.info_object.options @property def current_option(self) -> str | None: """Return the selected entity option to represent the entity state.""" - option = self._cluster_handler.data_cache.get(self._attribute_name) - if option is None: - return None - return option.name.replace("_", " ") + return self.entity_data.entity.current_option + @convert_zha_error_to_ha_error async def async_select_option(self, option: str) -> None: """Change the selected option.""" - self._cluster_handler.data_cache[self._attribute_name] = self._enum[ - option.replace(" ", "_") - ] + await self.entity_data.entity.async_select_option(option=option) self.async_write_ha_state() @callback - def async_restore_last_state(self, last_state) -> None: - """Restore previous state.""" - if last_state.state and last_state.state != STATE_UNKNOWN: - self._cluster_handler.data_cache[self._attribute_name] = self._enum[ - last_state.state.replace(" ", "_") - ] - - -class ZHANonZCLSelectEntity(ZHAEnumSelectEntity): - """Representation of a ZHA select entity with no ZCL interaction.""" - - @property - def available(self) -> bool: - """Return entity availability.""" - return True - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_IAS_WD) -class ZHADefaultToneSelectEntity(ZHANonZCLSelectEntity): - """Representation of a ZHA default siren tone select entity.""" - - _unique_id_suffix = IasWd.Warning.WarningMode.__name__ - _enum = IasWd.Warning.WarningMode - _attr_translation_key: str = "default_siren_tone" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_IAS_WD) -class ZHADefaultSirenLevelSelectEntity(ZHANonZCLSelectEntity): - """Representation of a ZHA default siren level select entity.""" - - _unique_id_suffix = IasWd.Warning.SirenLevel.__name__ - _enum = IasWd.Warning.SirenLevel - _attr_translation_key: str = "default_siren_level" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_IAS_WD) -class ZHADefaultStrobeLevelSelectEntity(ZHANonZCLSelectEntity): - """Representation of a ZHA default siren strobe level select entity.""" - - _unique_id_suffix = IasWd.StrobeLevel.__name__ - _enum = IasWd.StrobeLevel - _attr_translation_key: str = "default_strobe_level" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_IAS_WD) -class ZHADefaultStrobeSelectEntity(ZHANonZCLSelectEntity): - """Representation of a ZHA default siren strobe select entity.""" - - _unique_id_suffix = Strobe.__name__ - _enum = Strobe - _attr_translation_key: str = "default_strobe" - - -class ZCLEnumSelectEntity(ZhaEntity, SelectEntity): - """Representation of a ZHA ZCL enum select entity.""" - - _attribute_name: str - _attr_entity_category = EntityCategory.CONFIG - _enum: type[Enum] - - @classmethod - def create_entity( - cls, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> Self | None: - """Entity Factory. - - Return entity if it is a supported configuration, otherwise return None - """ - cluster_handler = cluster_handlers[0] - if ENTITY_METADATA not in kwargs and ( - cls._attribute_name in cluster_handler.cluster.unsupported_attributes - or cls._attribute_name not in cluster_handler.cluster.attributes_by_name - or cluster_handler.cluster.get(cls._attribute_name) is None - ): - _LOGGER.debug( - "%s is not supported - skipping %s entity creation", - cls._attribute_name, - cls.__name__, + def restore_external_state_attributes(self, state: State) -> None: + """Restore entity state.""" + if state.state and state.state != STATE_UNKNOWN: + self.entity_data.entity.restore_external_state_attributes( + state=state.state, ) - return None - - return cls(unique_id, zha_device, cluster_handlers, **kwargs) - - def __init__( - self, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> None: - """Init this select entity.""" - self._cluster_handler: ClusterHandler = cluster_handlers[0] - if ENTITY_METADATA in kwargs: - self._init_from_quirks_metadata(kwargs[ENTITY_METADATA]) - self._attr_options = [entry.name.replace("_", " ") for entry in self._enum] - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - - def _init_from_quirks_metadata(self, entity_metadata: ZCLEnumMetadata) -> None: - """Init this entity from the quirks metadata.""" - super()._init_from_quirks_metadata(entity_metadata) - self._attribute_name = entity_metadata.attribute_name - self._enum = entity_metadata.enum - - @property - def current_option(self) -> str | None: - """Return the selected entity option to represent the entity state.""" - option = self._cluster_handler.cluster.get(self._attribute_name) - if option is None: - return None - option = self._enum(option) - return option.name.replace("_", " ") - - async def async_select_option(self, option: str) -> None: - """Change the selected option.""" - await self._cluster_handler.write_attributes_safe( - {self._attribute_name: self._enum[option.replace(" ", "_")]} - ) - self.async_write_ha_state() - - async def async_added_to_hass(self) -> None: - """Run when about to be added to hass.""" - await super().async_added_to_hass() - self.async_accept_signal( - self._cluster_handler, SIGNAL_ATTR_UPDATED, self.async_set_state - ) - - @callback - def async_set_state(self, attr_id: int, attr_name: str, value: Any): - """Handle state update from cluster handler.""" - self.async_write_ha_state() - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_ON_OFF) -class ZHAStartupOnOffSelectEntity(ZCLEnumSelectEntity): - """Representation of a ZHA startup onoff select entity.""" - - _unique_id_suffix = OnOff.StartUpOnOff.__name__ - _attribute_name = "start_up_on_off" - _enum = OnOff.StartUpOnOff - _attr_translation_key: str = "start_up_on_off" - - -class TuyaPowerOnState(types.enum8): - """Tuya power on state enum.""" - - Off = 0x00 - On = 0x01 - LastState = 0x02 - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_ON_OFF, quirk_ids=TUYA_PLUG_ONOFF -) -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="tuya_manufacturer", quirk_ids=TUYA_PLUG_MANUFACTURER -) -class TuyaPowerOnStateSelectEntity(ZCLEnumSelectEntity): - """Representation of a ZHA power on state select entity.""" - - _unique_id_suffix = "power_on_state" - _attribute_name = "power_on_state" - _enum = TuyaPowerOnState - _attr_translation_key: str = "power_on_state" - - -class TuyaBacklightMode(types.enum8): - """Tuya switch backlight mode enum.""" - - Off = 0x00 - LightWhenOn = 0x01 - LightWhenOff = 0x02 - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_ON_OFF, quirk_ids=TUYA_PLUG_ONOFF -) -class TuyaBacklightModeSelectEntity(ZCLEnumSelectEntity): - """Representation of a ZHA backlight mode select entity.""" - - _unique_id_suffix = "backlight_mode" - _attribute_name = "backlight_mode" - _enum = TuyaBacklightMode - _attr_translation_key: str = "backlight_mode" - - -class MoesBacklightMode(types.enum8): - """MOES switch backlight mode enum.""" - - Off = 0x00 - LightWhenOn = 0x01 - LightWhenOff = 0x02 - Freeze = 0x03 - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="tuya_manufacturer", quirk_ids=TUYA_PLUG_MANUFACTURER -) -class MoesBacklightModeSelectEntity(ZCLEnumSelectEntity): - """Moes devices have a different backlight mode select options.""" - - _unique_id_suffix = "backlight_mode" - _attribute_name = "backlight_mode" - _enum = MoesBacklightMode - _attr_translation_key: str = "backlight_mode" - - -class AqaraMotionSensitivities(types.enum8): - """Aqara motion sensitivities.""" - - Low = 0x01 - Medium = 0x02 - High = 0x03 - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", - models={"lumi.motion.ac01", "lumi.motion.ac02", "lumi.motion.agl04"}, -) -class AqaraMotionSensitivity(ZCLEnumSelectEntity): - """Representation of a ZHA motion sensitivity configuration entity.""" - - _unique_id_suffix = "motion_sensitivity" - _attribute_name = "motion_sensitivity" - _enum = AqaraMotionSensitivities - _attr_translation_key: str = "motion_sensitivity" - - -class HueV1MotionSensitivities(types.enum8): - """Hue v1 motion sensitivities.""" - - Low = 0x00 - Medium = 0x01 - High = 0x02 - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_HUE_OCCUPANCY, - manufacturers={"Philips", "Signify Netherlands B.V."}, - models={"SML001"}, -) -class HueV1MotionSensitivity(ZCLEnumSelectEntity): - """Representation of a ZHA motion sensitivity configuration entity.""" - - _unique_id_suffix = "motion_sensitivity" - _attribute_name = "sensitivity" - _enum = HueV1MotionSensitivities - _attr_translation_key: str = "motion_sensitivity" - - -class HueV2MotionSensitivities(types.enum8): - """Hue v2 motion sensitivities.""" - - Lowest = 0x00 - Low = 0x01 - Medium = 0x02 - High = 0x03 - Highest = 0x04 - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_HUE_OCCUPANCY, - manufacturers={"Philips", "Signify Netherlands B.V."}, - models={"SML002", "SML003", "SML004"}, -) -class HueV2MotionSensitivity(ZCLEnumSelectEntity): - """Representation of a ZHA motion sensitivity configuration entity.""" - - _unique_id_suffix = "motion_sensitivity" - _attribute_name = "sensitivity" - _enum = HueV2MotionSensitivities - _attr_translation_key: str = "motion_sensitivity" - - -class AqaraMonitoringModess(types.enum8): - """Aqara monitoring modes.""" - - Undirected = 0x00 - Left_Right = 0x01 - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.motion.ac01"} -) -class AqaraMonitoringMode(ZCLEnumSelectEntity): - """Representation of a ZHA monitoring mode configuration entity.""" - - _unique_id_suffix = "monitoring_mode" - _attribute_name = "monitoring_mode" - _enum = AqaraMonitoringModess - _attr_translation_key: str = "monitoring_mode" - - -class AqaraApproachDistances(types.enum8): - """Aqara approach distances.""" - - Far = 0x00 - Medium = 0x01 - Near = 0x02 - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.motion.ac01"} -) -class AqaraApproachDistance(ZCLEnumSelectEntity): - """Representation of a ZHA approach distance configuration entity.""" - - _unique_id_suffix = "approach_distance" - _attribute_name = "approach_distance" - _enum = AqaraApproachDistances - _attr_translation_key: str = "approach_distance" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.magnet.ac01"} -) -class AqaraMagnetAC01DetectionDistance(ZCLEnumSelectEntity): - """Representation of a ZHA detection distance configuration entity.""" - - _unique_id_suffix = "detection_distance" - _attribute_name = "detection_distance" - _enum = MagnetAC01OppleCluster.DetectionDistance - _attr_translation_key: str = "detection_distance" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.switch.acn047"} -) -class AqaraT2RelaySwitchMode(ZCLEnumSelectEntity): - """Representation of a ZHA switch mode configuration entity.""" - - _unique_id_suffix = "switch_mode" - _attribute_name = "switch_mode" - _enum = T2RelayOppleCluster.SwitchMode - _attr_translation_key: str = "switch_mode" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.switch.acn047"} -) -class AqaraT2RelaySwitchType(ZCLEnumSelectEntity): - """Representation of a ZHA switch type configuration entity.""" - - _unique_id_suffix = "switch_type" - _attribute_name = "switch_type" - _enum = T2RelayOppleCluster.SwitchType - _attr_translation_key: str = "switch_type" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.switch.acn047"} -) -class AqaraT2RelayStartupOnOff(ZCLEnumSelectEntity): - """Representation of a ZHA startup on off configuration entity.""" - - _unique_id_suffix = "startup_on_off" - _attribute_name = "startup_on_off" - _enum = T2RelayOppleCluster.StartupOnOff - _attr_translation_key: str = "start_up_on_off" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.switch.acn047"} -) -class AqaraT2RelayDecoupledMode(ZCLEnumSelectEntity): - """Representation of a ZHA switch decoupled mode configuration entity.""" - - _unique_id_suffix = "decoupled_mode" - _attribute_name = "decoupled_mode" - _enum = T2RelayOppleCluster.DecoupledMode - _attr_translation_key: str = "decoupled_mode" - - -class InovelliOutputMode(types.enum1): - """Inovelli output mode.""" - - Dimmer = 0x00 - OnOff = 0x01 - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_INOVELLI, -) -class InovelliOutputModeEntity(ZCLEnumSelectEntity): - """Inovelli output mode control.""" - - _unique_id_suffix = "output_mode" - _attribute_name = "output_mode" - _enum = InovelliOutputMode - _attr_translation_key: str = "output_mode" - - -class InovelliSwitchType(types.enum8): - """Inovelli switch mode.""" - - Single_Pole = 0x00 - Three_Way_Dumb = 0x01 - Three_Way_AUX = 0x02 - Single_Pole_Full_Sine = 0x03 - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_INOVELLI, models={"VZM31-SN"} -) -class InovelliSwitchTypeEntity(ZCLEnumSelectEntity): - """Inovelli switch type control.""" - - _unique_id_suffix = "switch_type" - _attribute_name = "switch_type" - _enum = InovelliSwitchType - _attr_translation_key: str = "switch_type" - - -class InovelliFanSwitchType(types.enum1): - """Inovelli fan switch mode.""" - - Load_Only = 0x00 - Three_Way_AUX = 0x01 - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_INOVELLI, models={"VZM35-SN"} -) -class InovelliFanSwitchTypeEntity(ZCLEnumSelectEntity): - """Inovelli fan switch type control.""" - - _unique_id_suffix = "switch_type" - _attribute_name = "switch_type" - _enum = InovelliFanSwitchType - _attr_translation_key: str = "switch_type" - - -class InovelliLedScalingMode(types.enum1): - """Inovelli led mode.""" - - VZM31SN = 0x00 - LZW31SN = 0x01 - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_INOVELLI, -) -class InovelliLedScalingModeEntity(ZCLEnumSelectEntity): - """Inovelli led mode control.""" - - _unique_id_suffix = "led_scaling_mode" - _attribute_name = "led_scaling_mode" - _enum = InovelliLedScalingMode - _attr_translation_key: str = "led_scaling_mode" - - -class InovelliFanLedScalingMode(types.enum8): - """Inovelli fan led mode.""" - - VZM31SN = 0x00 - Grade_1 = 0x01 - Grade_2 = 0x02 - Grade_3 = 0x03 - Grade_4 = 0x04 - Grade_5 = 0x05 - Grade_6 = 0x06 - Grade_7 = 0x07 - Grade_8 = 0x08 - Grade_9 = 0x09 - Adaptive = 0x0A - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_INOVELLI, models={"VZM35-SN"} -) -class InovelliFanLedScalingModeEntity(ZCLEnumSelectEntity): - """Inovelli fan switch led mode control.""" - - _unique_id_suffix = "smart_fan_led_display_levels" - _attribute_name = "smart_fan_led_display_levels" - _enum = InovelliFanLedScalingMode - _attr_translation_key: str = "smart_fan_led_display_levels" - - -class InovelliNonNeutralOutput(types.enum1): - """Inovelli non neutral output selection.""" - - Low = 0x00 - High = 0x01 - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_INOVELLI, -) -class InovelliNonNeutralOutputEntity(ZCLEnumSelectEntity): - """Inovelli non neutral output control.""" - - _unique_id_suffix = "increased_non_neutral_output" - _attribute_name = "increased_non_neutral_output" - _enum = InovelliNonNeutralOutput - _attr_translation_key: str = "increased_non_neutral_output" - - -class AqaraFeedingMode(types.enum8): - """Feeding mode.""" - - Manual = 0x00 - Schedule = 0x01 - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"} -) -class AqaraPetFeederMode(ZCLEnumSelectEntity): - """Representation of an Aqara pet feeder mode configuration entity.""" - - _unique_id_suffix = "feeding_mode" - _attribute_name = "feeding_mode" - _enum = AqaraFeedingMode - _attr_translation_key: str = "feeding_mode" - - -class AqaraThermostatPresetMode(types.enum8): - """Thermostat preset mode.""" - - Manual = 0x00 - Auto = 0x01 - Away = 0x02 - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.airrtc.agl001"} -) -class AqaraThermostatPreset(ZCLEnumSelectEntity): - """Representation of an Aqara thermostat preset configuration entity.""" - - _unique_id_suffix = "preset" - _attribute_name = "preset" - _enum = AqaraThermostatPresetMode - _attr_translation_key: str = "preset" - - -class SonoffPresenceDetectionSensitivityEnum(types.enum8): - """Enum for detection sensitivity select entity.""" - - Low = 0x01 - Medium = 0x02 - High = 0x03 - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_OCCUPANCY, models={"SNZB-06P"} -) -class SonoffPresenceDetectionSensitivity(ZCLEnumSelectEntity): - """Entity to set the detection sensitivity of the Sonoff SNZB-06P.""" - - _unique_id_suffix = "detection_sensitivity" - _attribute_name = "ultrasonic_u_to_o_threshold" - _enum = SonoffPresenceDetectionSensitivityEnum - _attr_translation_key: str = "detection_sensitivity" - - -class KeypadLockoutEnum(types.enum8): - """Keypad lockout options.""" - - Unlock = 0x00 - Lock1 = 0x01 - Lock2 = 0x02 - Lock3 = 0x03 - Lock4 = 0x04 - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names="thermostat_ui") -class KeypadLockout(ZCLEnumSelectEntity): - """Mandatory attribute for thermostat_ui cluster. - - Often only the first two are implemented, and Lock2 to Lock4 should map to Lock1 in the firmware. - This however covers all bases. - """ - - _unique_id_suffix = "keypad_lockout" - _attribute_name: str = "keypad_lockout" - _enum = KeypadLockoutEnum - _attr_translation_key: str = "keypad_lockout" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -class DanfossExerciseDayOfTheWeek(ZCLEnumSelectEntity): - """Danfoss proprietary attribute for setting the day of the week for exercising.""" - - _unique_id_suffix = "exercise_day_of_week" - _attribute_name = "exercise_day_of_week" - _attr_translation_key: str = "exercise_day_of_week" - _enum = danfoss_thermostat.DanfossExerciseDayOfTheWeekEnum - _attr_icon: str = "mdi:wrench-clock" - - -class DanfossOrientationEnum(types.enum8): - """Vertical or Horizontal.""" - - Horizontal = 0x00 - Vertical = 0x01 - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -class DanfossOrientation(ZCLEnumSelectEntity): - """Danfoss proprietary attribute for setting the orientation of the valve. - - Needed for biasing the internal temperature sensor. - This is implemented as an enum here, but is a boolean on the device. - """ - - _unique_id_suffix = "orientation" - _attribute_name = "orientation" - _attr_translation_key: str = "valve_orientation" - _enum = DanfossOrientationEnum - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -class DanfossAdaptationRunControl(ZCLEnumSelectEntity): - """Danfoss proprietary attribute for controlling the current adaptation run.""" - - _unique_id_suffix = "adaptation_run_control" - _attribute_name = "adaptation_run_control" - _attr_translation_key: str = "adaptation_run_command" - _enum = danfoss_thermostat.DanfossAdaptationRunControlEnum - - -class DanfossControlAlgorithmScaleFactorEnum(types.enum8): - """The time scale factor for changing the opening of the valve. - - Not all values are given, therefore there are some extrapolated values with a margin of error of about 5 minutes. - This is implemented as an enum here, but is a number on the device. - """ - - quick_5min = 0x01 - - quick_10min = 0x02 # extrapolated - quick_15min = 0x03 # extrapolated - quick_25min = 0x04 # extrapolated - - moderate_30min = 0x05 - - moderate_40min = 0x06 # extrapolated - moderate_50min = 0x07 # extrapolated - moderate_60min = 0x08 # extrapolated - moderate_70min = 0x09 # extrapolated - - slow_80min = 0x0A - - quick_open_disabled = 0x11 # not sure what it does; also requires lower 4 bits to be in [1, 10] I assume - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -class DanfossControlAlgorithmScaleFactor(ZCLEnumSelectEntity): - """Danfoss proprietary attribute for setting the scale factor of the setpoint filter time constant.""" - - _unique_id_suffix = "control_algorithm_scale_factor" - _attribute_name = "control_algorithm_scale_factor" - _attr_translation_key: str = "setpoint_response_time" - _enum = DanfossControlAlgorithmScaleFactorEnum - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="thermostat_ui", - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -class DanfossViewingDirection(ZCLEnumSelectEntity): - """Danfoss proprietary attribute for setting the viewing direction of the screen.""" - - _unique_id_suffix = "viewing_direction" - _attribute_name = "viewing_direction" - _attr_translation_key: str = "viewing_direction" - _enum = danfoss_thermostat.DanfossViewingDirectionEnum diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 99d950dc06a..dde000b24b5 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -2,115 +2,71 @@ from __future__ import annotations -import asyncio -from dataclasses import dataclass -from datetime import timedelta -import enum +from collections.abc import Mapping import functools import logging -import numbers -import random -from typing import TYPE_CHECKING, Any, Self +from typing import Any -from zhaquirks.danfoss import thermostat as danfoss_thermostat -from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT -from zigpy import types -from zigpy.quirks.v2 import ZCLEnumMetadata, ZCLSensorMetadata -from zigpy.state import Counter, State -from zigpy.zcl.clusters.closures import WindowCovering -from zigpy.zcl.clusters.general import Basic - -from homeassistant.components.climate import HVACAction from homeassistant.components.sensor import ( SensorDeviceClass, SensorEntity, - SensorEntityDescription, SensorStateClass, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - CONCENTRATION_PARTS_PER_BILLION, - CONCENTRATION_PARTS_PER_MILLION, - LIGHT_LUX, - PERCENTAGE, - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - EntityCategory, - Platform, - UnitOfApparentPower, - UnitOfElectricCurrent, - UnitOfElectricPotential, - UnitOfEnergy, - UnitOfFrequency, - UnitOfMass, - UnitOfPower, - UnitOfPressure, - UnitOfTemperature, - UnitOfTime, - UnitOfVolume, - UnitOfVolumeFlowRate, -) -from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import StateType -from .core import discovery -from .core.const import ( - CLUSTER_HANDLER_ANALOG_INPUT, - CLUSTER_HANDLER_BASIC, - CLUSTER_HANDLER_COVER, - CLUSTER_HANDLER_DEVICE_TEMPERATURE, - CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT, - CLUSTER_HANDLER_HUMIDITY, - CLUSTER_HANDLER_ILLUMINANCE, - CLUSTER_HANDLER_LEAF_WETNESS, - CLUSTER_HANDLER_POWER_CONFIGURATION, - CLUSTER_HANDLER_PRESSURE, - CLUSTER_HANDLER_SMARTENERGY_METERING, - CLUSTER_HANDLER_SOIL_MOISTURE, - CLUSTER_HANDLER_TEMPERATURE, - CLUSTER_HANDLER_THERMOSTAT, - DATA_ZHA, - ENTITY_METADATA, +from .entity import ZHAEntity +from .helpers import ( SIGNAL_ADD_ENTITIES, - SIGNAL_ATTR_UPDATED, + EntityData, + async_add_entities as zha_async_add_entities, + exclude_none_values, + get_zha_data, ) -from .core.helpers import get_zha_data, validate_device_class, validate_unit -from .core.registries import SMARTTHINGS_HUMIDITY_CLUSTER, ZHA_ENTITIES -from .entity import BaseZhaEntity, ZhaEntity - -if TYPE_CHECKING: - from .core.cluster_handlers import ClusterHandler - from .core.device import ZHADevice - -BATTERY_SIZES = { - 0: "No battery", - 1: "Built in", - 2: "Other", - 3: "AA", - 4: "AAA", - 5: "C", - 6: "D", - 7: "CR2", - 8: "CR123A", - 9: "CR2450", - 10: "CR2032", - 11: "CR1632", - 255: "Unknown", -} _LOGGER = logging.getLogger(__name__) -CLUSTER_HANDLER_ST_HUMIDITY_CLUSTER = ( - f"cluster_handler_0x{SMARTTHINGS_HUMIDITY_CLUSTER:04x}" -) -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.SENSOR) -MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.SENSOR) -CONFIG_DIAGNOSTIC_MATCH = functools.partial( - ZHA_ENTITIES.config_diagnostic_match, Platform.SENSOR -) +# For backwards compatibility and transparency, all expected extra state attributes are +# explicitly listed below. These should have been sensors themselves but for whatever +# reason were not created as such. They will be migrated to independent sensor entities +# in a future release. +_EXTRA_STATE_ATTRIBUTES: set[str] = { + # Battery + "battery_size", + "battery_quantity", + "battery_voltage", + # Power + "measurement_type", + "apparent_power_max", + "rms_current_max", + "rms_voltage_max", + "ac_frequency_max", + "power_factor_max", + "active_power_max", + # Smart Energy metering + "device_type", + "status", + "zcl_unit_of_measurement", + # Danfoss bitmaps + "In_progress", + "Valve_characteristic_found", + "Valve_characteristic_lost", + "Top_pcb_sensor_error", + "Side_pcb_sensor_error", + "Non_volatile_memory_error", + "Unknown_hw_error", + "Motor_error", + "Invalid_internal_communication", + "Invalid_clock_information", + "Radio_communication_error", + "Encoder_jammed", + "Low_battery", + "Critical_low_battery", +} async def async_setup_entry( @@ -126,1504 +82,76 @@ async def async_setup_entry( hass, SIGNAL_ADD_ENTITIES, functools.partial( - discovery.async_add_entities, - async_add_entities, - entities_to_create, + zha_async_add_entities, async_add_entities, Sensor, entities_to_create ), ) config_entry.async_on_unload(unsub) # pylint: disable-next=hass-invalid-inheritance # needs fixing -class Sensor(ZhaEntity, SensorEntity): - """Base ZHA sensor.""" +class Sensor(ZHAEntity, SensorEntity): + """ZHA sensor.""" - _attribute_name: int | str | None = None - _decimals: int = 1 - _divisor: int = 1 - _multiplier: int | float = 1 + def __init__(self, entity_data: EntityData, **kwargs: Any) -> None: + """Initialize the ZHA select entity.""" + super().__init__(entity_data, **kwargs) + entity = self.entity_data.entity - @classmethod - def create_entity( - cls, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> Self | None: - """Entity Factory. + if entity.device_class is not None: + self._attr_device_class = SensorDeviceClass(entity.device_class) - Return entity if it is a supported configuration, otherwise return None - """ - cluster_handler = cluster_handlers[0] - if ENTITY_METADATA not in kwargs and ( - cls._attribute_name in cluster_handler.cluster.unsupported_attributes - or cls._attribute_name not in cluster_handler.cluster.attributes_by_name + if entity.state_class is not None: + self._attr_state_class = SensorStateClass(entity.state_class) + + if hasattr(entity.info_object, "unit") and entity.info_object.unit is not None: + self._attr_native_unit_of_measurement = entity.info_object.unit + + if ( + hasattr(entity, "entity_description") + and entity.entity_description is not None ): - _LOGGER.debug( - "%s is not supported - skipping %s entity creation", - cls._attribute_name, - cls.__name__, - ) - return None + entity_description = entity.entity_description - return cls(unique_id, zha_device, cluster_handlers, **kwargs) - - def __init__( - self, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> None: - """Init this sensor.""" - self._cluster_handler: ClusterHandler = cluster_handlers[0] - if ENTITY_METADATA in kwargs: - self._init_from_quirks_metadata(kwargs[ENTITY_METADATA]) - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - - def _init_from_quirks_metadata(self, entity_metadata: ZCLSensorMetadata) -> None: - """Init this entity from the quirks metadata.""" - super()._init_from_quirks_metadata(entity_metadata) - self._attribute_name = entity_metadata.attribute_name - if entity_metadata.divisor is not None: - self._divisor = entity_metadata.divisor - if entity_metadata.multiplier is not None: - self._multiplier = entity_metadata.multiplier - if entity_metadata.device_class is not None: - self._attr_device_class = validate_device_class( - SensorDeviceClass, - entity_metadata.device_class, - Platform.SENSOR.value, - _LOGGER, - ) - if entity_metadata.device_class is None and entity_metadata.unit is not None: - self._attr_native_unit_of_measurement = validate_unit( - entity_metadata.unit - ).value - - async def async_added_to_hass(self) -> None: - """Run when about to be added to hass.""" - await super().async_added_to_hass() - self.async_accept_signal( - self._cluster_handler, SIGNAL_ATTR_UPDATED, self.async_set_state - ) - - @property - def native_value(self) -> StateType: - """Return the state of the entity.""" - assert self._attribute_name is not None - raw_state = self._cluster_handler.cluster.get(self._attribute_name) - if raw_state is None: - return None - return self.formatter(raw_state) - - @callback - def async_set_state(self, attr_id: int, attr_name: str, value: Any) -> None: - """Handle state update from cluster handler.""" - self.async_write_ha_state() - - def formatter(self, value: int | enum.IntEnum) -> int | float | str | None: - """Numeric pass-through formatter.""" - if self._decimals > 0: - return round( - float(value * self._multiplier) / self._divisor, self._decimals - ) - return round(float(value * self._multiplier) / self._divisor) - - -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class PollableSensor(Sensor): - """Base ZHA sensor that polls for state.""" - - _use_custom_polling: bool = True - - def __init__( - self, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> None: - """Init this sensor.""" - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - self._cancel_refresh_handle: CALLBACK_TYPE | None = None - - async def async_added_to_hass(self) -> None: - """Run when about to be added to hass.""" - await super().async_added_to_hass() - if self._use_custom_polling: - refresh_interval = random.randint(30, 60) - self._cancel_refresh_handle = async_track_time_interval( - self.hass, self._refresh, timedelta(seconds=refresh_interval) - ) - self.debug("started polling with refresh interval of %s", refresh_interval) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect entity object when removed.""" - if self._cancel_refresh_handle is not None: - self._cancel_refresh_handle() - self._cancel_refresh_handle = None - self.debug("stopped polling during device removal") - await super().async_will_remove_from_hass() - - async def _refresh(self, time): - """Call async_update at a constrained random interval.""" - if self._zha_device.available and self.hass.data[DATA_ZHA].allow_polling: - self.debug("polling for updated state") - await self.async_update() - self.async_write_ha_state() - else: - self.debug( - "skipping polling for updated state, available: %s, allow polled requests: %s", - self._zha_device.available, - self.hass.data[DATA_ZHA].allow_polling, - ) - - -class DeviceCounterSensor(BaseZhaEntity, SensorEntity): - """Device counter sensor.""" - - _attr_should_poll = True - _attr_state_class: SensorStateClass = SensorStateClass.TOTAL - _attr_entity_category = EntityCategory.DIAGNOSTIC - _attr_entity_registry_enabled_default = False - - @classmethod - def create_entity( - cls, - unique_id: str, - zha_device: ZHADevice, - counter_groups: str, - counter_group: str, - counter: str, - **kwargs: Any, - ) -> Self | None: - """Entity Factory. - - Return entity if it is a supported configuration, otherwise return None - """ - return cls( - unique_id, zha_device, counter_groups, counter_group, counter, **kwargs - ) - - def __init__( - self, - unique_id: str, - zha_device: ZHADevice, - counter_groups: str, - counter_group: str, - counter: str, - **kwargs: Any, - ) -> None: - """Init this sensor.""" - super().__init__(unique_id, zha_device, **kwargs) - state: State = self._zha_device.gateway.application_controller.state - self._zigpy_counter: Counter = ( - getattr(state, counter_groups).get(counter_group, {}).get(counter, None) - ) - self._attr_name: str = self._zigpy_counter.name - self.remove_future: asyncio.Future - - @property - def available(self) -> bool: - """Return entity availability.""" - return self._zha_device.available - - async def async_added_to_hass(self) -> None: - """Run when about to be added to hass.""" - self.remove_future = self.hass.loop.create_future() - self._zha_device.gateway.register_entity_reference( - self._zha_device.ieee, - self.entity_id, - self._zha_device, - {}, - self.device_info, - self.remove_future, - ) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect entity object when removed.""" - await super().async_will_remove_from_hass() - self.zha_device.gateway.remove_entity_reference(self) - self.remove_future.set_result(True) - - @property - def native_value(self) -> StateType: - """Return the state of the entity.""" - return self._zigpy_counter.value - - async def async_update(self) -> None: - """Retrieve latest state.""" - self.async_write_ha_state() - - -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class EnumSensor(Sensor): - """Sensor with value from enum.""" - - _attr_device_class: SensorDeviceClass = SensorDeviceClass.ENUM - _enum: type[enum.Enum] - - def __init__( - self, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> None: - """Init this sensor.""" - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - self._attr_options = [e.name for e in self._enum] - - def _init_from_quirks_metadata(self, entity_metadata: ZCLEnumMetadata) -> None: - """Init this entity from the quirks metadata.""" - ZhaEntity._init_from_quirks_metadata(self, entity_metadata) # noqa: SLF001 - self._attribute_name = entity_metadata.attribute_name - self._enum = entity_metadata.enum - - def formatter(self, value: int) -> str | None: - """Use name of enum.""" - assert self._enum is not None - return self._enum(value).name - - -@MULTI_MATCH( - cluster_handler_names=CLUSTER_HANDLER_ANALOG_INPUT, - manufacturers="Digi", - stop_on_match_group=CLUSTER_HANDLER_ANALOG_INPUT, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class AnalogInput(Sensor): - """Sensor that displays analog input values.""" - - _attribute_name = "present_value" - _attr_translation_key: str = "analog_input" - - -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_POWER_CONFIGURATION) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class Battery(Sensor): - """Battery sensor of power configuration cluster.""" - - _attribute_name = "battery_percentage_remaining" - _attr_device_class: SensorDeviceClass = SensorDeviceClass.BATTERY - _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _attr_entity_category = EntityCategory.DIAGNOSTIC - _attr_native_unit_of_measurement = PERCENTAGE - - @classmethod - def create_entity( - cls, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> Self | None: - """Entity Factory. - - Unlike any other entity, PowerConfiguration cluster may not support - battery_percent_remaining attribute, but zha-device-handlers takes care of it - so create the entity regardless - """ - if zha_device.is_mains_powered: - return None - return cls(unique_id, zha_device, cluster_handlers, **kwargs) - - @staticmethod - def formatter(value: int) -> int | None: - """Return the state of the entity.""" - # per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯ - if not isinstance(value, numbers.Number) or value == -1 or value == 255: - return None - return round(value / 2) - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return device state attrs for battery sensors.""" - state_attrs = {} - battery_size = self._cluster_handler.cluster.get("battery_size") - if battery_size is not None: - state_attrs["battery_size"] = BATTERY_SIZES.get(battery_size, "Unknown") - battery_quantity = self._cluster_handler.cluster.get("battery_quantity") - if battery_quantity is not None: - state_attrs["battery_quantity"] = battery_quantity - battery_voltage = self._cluster_handler.cluster.get("battery_voltage") - if battery_voltage is not None: - state_attrs["battery_voltage"] = round(battery_voltage / 10, 2) - return state_attrs - - -@MULTI_MATCH( - cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT, - stop_on_match_group=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT, - models={"VZM31-SN", "SP 234", "outletv4"}, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class ElectricalMeasurement(PollableSensor): - """Active power measurement.""" - - _use_custom_polling: bool = False - _attribute_name = "active_power" - _attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER - _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _attr_native_unit_of_measurement: str = UnitOfPower.WATT - _div_mul_prefix: str | None = "ac_power" - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return device state attrs for sensor.""" - attrs = {} - if self._cluster_handler.measurement_type is not None: - attrs["measurement_type"] = self._cluster_handler.measurement_type - - max_attr_name = f"{self._attribute_name}_max" - - try: - max_v = self._cluster_handler.cluster.get(max_attr_name) - except KeyError: - pass - else: - if max_v is not None: - attrs[max_attr_name] = str(self.formatter(max_v)) - - return attrs - - def formatter(self, value: int) -> int | float: - """Return 'normalized' value.""" - if self._div_mul_prefix: - multiplier = getattr( - self._cluster_handler, f"{self._div_mul_prefix}_multiplier" - ) - divisor = getattr(self._cluster_handler, f"{self._div_mul_prefix}_divisor") - else: - multiplier = self._multiplier - divisor = self._divisor - value = float(value * multiplier) / divisor - if value < 100 and divisor > 1: - return round(value, self._decimals) - return round(value) - - -@MULTI_MATCH( - cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT, - stop_on_match_group=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class PolledElectricalMeasurement(ElectricalMeasurement): - """Polled active power measurement.""" - - _use_custom_polling: bool = True - - -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class ElectricalMeasurementApparentPower(PolledElectricalMeasurement): - """Apparent power measurement.""" - - _attribute_name = "apparent_power" - _unique_id_suffix = "apparent_power" - _use_custom_polling = False # Poll indirectly by ElectricalMeasurementSensor - _attr_device_class: SensorDeviceClass = SensorDeviceClass.APPARENT_POWER - _attr_native_unit_of_measurement = UnitOfApparentPower.VOLT_AMPERE - _div_mul_prefix = "ac_power" - - -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class ElectricalMeasurementRMSCurrent(PolledElectricalMeasurement): - """RMS current measurement.""" - - _attribute_name = "rms_current" - _unique_id_suffix = "rms_current" - _use_custom_polling = False # Poll indirectly by ElectricalMeasurementSensor - _attr_device_class: SensorDeviceClass = SensorDeviceClass.CURRENT - _attr_native_unit_of_measurement = UnitOfElectricCurrent.AMPERE - _div_mul_prefix = "ac_current" - - -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class ElectricalMeasurementRMSVoltage(PolledElectricalMeasurement): - """RMS Voltage measurement.""" - - _attribute_name = "rms_voltage" - _unique_id_suffix = "rms_voltage" - _use_custom_polling = False # Poll indirectly by ElectricalMeasurementSensor - _attr_device_class: SensorDeviceClass = SensorDeviceClass.VOLTAGE - _attr_native_unit_of_measurement = UnitOfElectricPotential.VOLT - _div_mul_prefix = "ac_voltage" - - -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class ElectricalMeasurementFrequency(PolledElectricalMeasurement): - """Frequency measurement.""" - - _attribute_name = "ac_frequency" - _unique_id_suffix = "ac_frequency" - _use_custom_polling = False # Poll indirectly by ElectricalMeasurementSensor - _attr_device_class: SensorDeviceClass = SensorDeviceClass.FREQUENCY - _attr_translation_key: str = "ac_frequency" - _attr_native_unit_of_measurement = UnitOfFrequency.HERTZ - _div_mul_prefix = "ac_frequency" - - -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class ElectricalMeasurementPowerFactor(PolledElectricalMeasurement): - """Power Factor measurement.""" - - _attribute_name = "power_factor" - _unique_id_suffix = "power_factor" - _use_custom_polling = False # Poll indirectly by ElectricalMeasurementSensor - _attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER_FACTOR - _attr_native_unit_of_measurement = PERCENTAGE - _div_mul_prefix = None - - -@MULTI_MATCH( - generic_ids=CLUSTER_HANDLER_ST_HUMIDITY_CLUSTER, - stop_on_match_group=CLUSTER_HANDLER_HUMIDITY, -) -@MULTI_MATCH( - cluster_handler_names=CLUSTER_HANDLER_HUMIDITY, - stop_on_match_group=CLUSTER_HANDLER_HUMIDITY, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class Humidity(Sensor): - """Humidity sensor.""" - - _attribute_name = "measured_value" - _attr_device_class: SensorDeviceClass = SensorDeviceClass.HUMIDITY - _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _divisor = 100 - _attr_native_unit_of_measurement = PERCENTAGE - - -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_SOIL_MOISTURE) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class SoilMoisture(Sensor): - """Soil Moisture sensor.""" - - _attribute_name = "measured_value" - _attr_device_class: SensorDeviceClass = SensorDeviceClass.HUMIDITY - _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _attr_translation_key: str = "soil_moisture" - _divisor = 100 - _attr_native_unit_of_measurement = PERCENTAGE - - -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_LEAF_WETNESS) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class LeafWetness(Sensor): - """Leaf Wetness sensor.""" - - _attribute_name = "measured_value" - _attr_device_class: SensorDeviceClass = SensorDeviceClass.HUMIDITY - _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _attr_translation_key: str = "leaf_wetness" - _divisor = 100 - _attr_native_unit_of_measurement = PERCENTAGE - - -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ILLUMINANCE) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class Illuminance(Sensor): - """Illuminance Sensor.""" - - _attribute_name = "measured_value" - _attr_device_class: SensorDeviceClass = SensorDeviceClass.ILLUMINANCE - _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _attr_native_unit_of_measurement = LIGHT_LUX - - def formatter(self, value: int) -> int | None: - """Convert illumination data.""" - if value == 0: - return 0 - if value == 0xFFFF: - return None - return round(pow(10, ((value - 1) / 10000))) - - -@dataclass(frozen=True, kw_only=True) -class SmartEnergyMeteringEntityDescription(SensorEntityDescription): - """Dataclass that describes a Zigbee smart energy metering entity.""" - - key: str = "instantaneous_demand" - state_class: SensorStateClass | None = SensorStateClass.MEASUREMENT - scale: int = 1 - - -@MULTI_MATCH( - cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING, - stop_on_match_group=CLUSTER_HANDLER_SMARTENERGY_METERING, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class SmartEnergyMetering(PollableSensor): - """Metering sensor.""" - - entity_description: SmartEnergyMeteringEntityDescription - _use_custom_polling: bool = False - _attribute_name = "instantaneous_demand" - _attr_translation_key: str = "instantaneous_demand" - - _ENTITY_DESCRIPTION_MAP = { - 0x00: SmartEnergyMeteringEntityDescription( - native_unit_of_measurement=UnitOfPower.WATT, - device_class=SensorDeviceClass.POWER, - ), - 0x01: SmartEnergyMeteringEntityDescription( - native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, - device_class=None, # volume flow rate is not supported yet - ), - 0x02: SmartEnergyMeteringEntityDescription( - native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE, - device_class=None, # volume flow rate is not supported yet - ), - 0x03: SmartEnergyMeteringEntityDescription( - native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR, - device_class=None, # volume flow rate is not supported yet - scale=100, - ), - 0x04: SmartEnergyMeteringEntityDescription( - native_unit_of_measurement=f"{UnitOfVolume.GALLONS}/{UnitOfTime.HOURS}", # US gallons per hour - device_class=None, # volume flow rate is not supported yet - ), - 0x05: SmartEnergyMeteringEntityDescription( - native_unit_of_measurement=f"IMP {UnitOfVolume.GALLONS}/{UnitOfTime.HOURS}", # IMP gallons per hour - device_class=None, # needs to be None as imperial gallons are not supported - ), - 0x06: SmartEnergyMeteringEntityDescription( - native_unit_of_measurement=UnitOfPower.BTU_PER_HOUR, - device_class=None, - state_class=None, - ), - 0x07: SmartEnergyMeteringEntityDescription( - native_unit_of_measurement=f"l/{UnitOfTime.HOURS}", - device_class=None, # volume flow rate is not supported yet - ), - 0x08: SmartEnergyMeteringEntityDescription( - native_unit_of_measurement=UnitOfPressure.KPA, - device_class=SensorDeviceClass.PRESSURE, - ), # gauge - 0x09: SmartEnergyMeteringEntityDescription( - native_unit_of_measurement=UnitOfPressure.KPA, - device_class=SensorDeviceClass.PRESSURE, - ), # absolute - 0x0A: SmartEnergyMeteringEntityDescription( - native_unit_of_measurement=f"{UnitOfVolume.CUBIC_FEET}/{UnitOfTime.HOURS}", # cubic feet per hour - device_class=None, # volume flow rate is not supported yet - scale=1000, - ), - 0x0B: SmartEnergyMeteringEntityDescription( - native_unit_of_measurement="unitless", device_class=None, state_class=None - ), - 0x0C: SmartEnergyMeteringEntityDescription( - native_unit_of_measurement=f"{UnitOfEnergy.MEGA_JOULE}/{UnitOfTime.SECONDS}", - device_class=None, # needs to be None as MJ/s is not supported - ), - } - - def __init__( - self, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> None: - """Init.""" - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - - entity_description = self._ENTITY_DESCRIPTION_MAP.get( - self._cluster_handler.unit_of_measurement - ) - if entity_description is not None: - self.entity_description = entity_description - - def formatter(self, value: int) -> int | float: - """Pass through cluster handler formatter.""" - return self._cluster_handler.demand_formatter(value) - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Return device state attrs for battery sensors.""" - attrs = {} - if self._cluster_handler.device_type is not None: - attrs["device_type"] = self._cluster_handler.device_type - if (status := self._cluster_handler.status) is not None: - if isinstance(status, enum.IntFlag): - attrs["status"] = str( - status.name if status.name is not None else status.value + if entity_description.state_class is not None: + self._attr_state_class = SensorStateClass( + entity_description.state_class.value + ) + + if entity_description.scale is not None: + self._attr_scale = entity_description.scale + + if entity_description.native_unit_of_measurement is not None: + self._attr_native_unit_of_measurement = ( + entity_description.native_unit_of_measurement + ) + + if entity_description.device_class is not None: + self._attr_device_class = SensorDeviceClass( + entity_description.device_class.value ) - else: - attrs["status"] = str(status)[len(status.__class__.__name__) + 1 :] - return attrs @property def native_value(self) -> StateType: """Return the state of the entity.""" - state = super().native_value - if hasattr(self, "entity_description") and state is not None: - return float(state) * self.entity_description.scale - - return state - - -@dataclass(frozen=True, kw_only=True) -class SmartEnergySummationEntityDescription(SmartEnergyMeteringEntityDescription): - """Dataclass that describes a Zigbee smart energy summation entity.""" - - key: str = "summation_delivered" - state_class: SensorStateClass | None = SensorStateClass.TOTAL_INCREASING - - -@MULTI_MATCH( - cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING, - stop_on_match_group=CLUSTER_HANDLER_SMARTENERGY_METERING, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class SmartEnergySummation(SmartEnergyMetering): - """Smart Energy Metering summation sensor.""" - - entity_description: SmartEnergySummationEntityDescription - _attribute_name = "current_summ_delivered" - _unique_id_suffix = "summation_delivered" - _attr_translation_key: str = "summation_delivered" - - _ENTITY_DESCRIPTION_MAP = { - 0x00: SmartEnergySummationEntityDescription( - native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR, - device_class=SensorDeviceClass.ENERGY, - ), - 0x01: SmartEnergySummationEntityDescription( - native_unit_of_measurement=UnitOfVolume.CUBIC_METERS, - device_class=SensorDeviceClass.VOLUME, - ), - 0x02: SmartEnergySummationEntityDescription( - native_unit_of_measurement=UnitOfVolume.CUBIC_FEET, - device_class=SensorDeviceClass.VOLUME, - ), - 0x03: SmartEnergySummationEntityDescription( - native_unit_of_measurement=UnitOfVolume.CUBIC_FEET, - device_class=SensorDeviceClass.VOLUME, - scale=100, - ), - 0x04: SmartEnergySummationEntityDescription( - native_unit_of_measurement=UnitOfVolume.GALLONS, # US gallons - device_class=SensorDeviceClass.VOLUME, - ), - 0x05: SmartEnergySummationEntityDescription( - native_unit_of_measurement=f"IMP {UnitOfVolume.GALLONS}", - device_class=None, # needs to be None as imperial gallons are not supported - ), - 0x06: SmartEnergySummationEntityDescription( - native_unit_of_measurement="BTU", device_class=None, state_class=None - ), - 0x07: SmartEnergySummationEntityDescription( - native_unit_of_measurement=UnitOfVolume.LITERS, - device_class=SensorDeviceClass.VOLUME, - ), - 0x08: SmartEnergySummationEntityDescription( - native_unit_of_measurement=UnitOfPressure.KPA, - device_class=SensorDeviceClass.PRESSURE, - state_class=SensorStateClass.MEASUREMENT, - ), # gauge - 0x09: SmartEnergySummationEntityDescription( - native_unit_of_measurement=UnitOfPressure.KPA, - device_class=SensorDeviceClass.PRESSURE, - state_class=SensorStateClass.MEASUREMENT, - ), # absolute - 0x0A: SmartEnergySummationEntityDescription( - native_unit_of_measurement=UnitOfVolume.CUBIC_FEET, - device_class=SensorDeviceClass.VOLUME, - scale=1000, - ), - 0x0B: SmartEnergySummationEntityDescription( - native_unit_of_measurement="unitless", device_class=None, state_class=None - ), - 0x0C: SmartEnergySummationEntityDescription( - native_unit_of_measurement=UnitOfEnergy.MEGA_JOULE, - device_class=SensorDeviceClass.ENERGY, - ), - } - - def formatter(self, value: int) -> int | float: - """Numeric pass-through formatter.""" - if self._cluster_handler.unit_of_measurement != 0: - return self._cluster_handler.summa_formatter(value) - - cooked = ( - float(self._cluster_handler.multiplier * value) - / self._cluster_handler.divisor - ) - return round(cooked, 3) - - -@MULTI_MATCH( - cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING, - models={"TS011F", "ZLinky_TIC", "TICMeter"}, - stop_on_match_group=CLUSTER_HANDLER_SMARTENERGY_METERING, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class PolledSmartEnergySummation(SmartEnergySummation): - """Polled Smart Energy Metering summation sensor.""" - - _use_custom_polling: bool = True - - -@MULTI_MATCH( - cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING, - models={"ZLinky_TIC", "TICMeter"}, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class Tier1SmartEnergySummation(PolledSmartEnergySummation): - """Tier 1 Smart Energy Metering summation sensor.""" - - _use_custom_polling = False # Poll indirectly by PolledSmartEnergySummation - _attribute_name = "current_tier1_summ_delivered" - _unique_id_suffix = "tier1_summation_delivered" - _attr_translation_key: str = "tier1_summation_delivered" - - -@MULTI_MATCH( - cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING, - models={"ZLinky_TIC", "TICMeter"}, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class Tier2SmartEnergySummation(PolledSmartEnergySummation): - """Tier 2 Smart Energy Metering summation sensor.""" - - _use_custom_polling = False # Poll indirectly by PolledSmartEnergySummation - _attribute_name = "current_tier2_summ_delivered" - _unique_id_suffix = "tier2_summation_delivered" - _attr_translation_key: str = "tier2_summation_delivered" - - -@MULTI_MATCH( - cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING, - models={"ZLinky_TIC", "TICMeter"}, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class Tier3SmartEnergySummation(PolledSmartEnergySummation): - """Tier 3 Smart Energy Metering summation sensor.""" - - _use_custom_polling = False # Poll indirectly by PolledSmartEnergySummation - _attribute_name = "current_tier3_summ_delivered" - _unique_id_suffix = "tier3_summation_delivered" - _attr_translation_key: str = "tier3_summation_delivered" - - -@MULTI_MATCH( - cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING, - models={"ZLinky_TIC", "TICMeter"}, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class Tier4SmartEnergySummation(PolledSmartEnergySummation): - """Tier 4 Smart Energy Metering summation sensor.""" - - _use_custom_polling = False # Poll indirectly by PolledSmartEnergySummation - _attribute_name = "current_tier4_summ_delivered" - _unique_id_suffix = "tier4_summation_delivered" - _attr_translation_key: str = "tier4_summation_delivered" - - -@MULTI_MATCH( - cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING, - models={"ZLinky_TIC", "TICMeter"}, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class Tier5SmartEnergySummation(PolledSmartEnergySummation): - """Tier 5 Smart Energy Metering summation sensor.""" - - _use_custom_polling = False # Poll indirectly by PolledSmartEnergySummation - _attribute_name = "current_tier5_summ_delivered" - _unique_id_suffix = "tier5_summation_delivered" - _attr_translation_key: str = "tier5_summation_delivered" - - -@MULTI_MATCH( - cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING, - models={"ZLinky_TIC", "TICMeter"}, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class Tier6SmartEnergySummation(PolledSmartEnergySummation): - """Tier 6 Smart Energy Metering summation sensor.""" - - _use_custom_polling = False # Poll indirectly by PolledSmartEnergySummation - _attribute_name = "current_tier6_summ_delivered" - _unique_id_suffix = "tier6_summation_delivered" - _attr_translation_key: str = "tier6_summation_delivered" - - -@MULTI_MATCH( - cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class SmartEnergySummationReceived(PolledSmartEnergySummation): - """Smart Energy Metering summation received sensor.""" - - _use_custom_polling = False # Poll indirectly by PolledSmartEnergySummation - _attribute_name = "current_summ_received" - _unique_id_suffix = "summation_received" - _attr_translation_key: str = "summation_received" - - @classmethod - def create_entity( - cls, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> Self | None: - """Entity Factory. - - This attribute only started to be initialized in HA 2024.2.0, - so the entity would be created on the first HA start after the - upgrade for existing devices, as the initialization to see if - an attribute is unsupported happens later in the background. - To avoid creating unnecessary entities for existing devices, - wait until the attribute was properly initialized once for now. - """ - if cluster_handlers[0].cluster.get(cls._attribute_name) is None: - return None - return super().create_entity(unique_id, zha_device, cluster_handlers, **kwargs) - - -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_PRESSURE) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class Pressure(Sensor): - """Pressure sensor.""" - - _attribute_name = "measured_value" - _attr_device_class: SensorDeviceClass = SensorDeviceClass.PRESSURE - _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _decimals = 0 - _attr_native_unit_of_measurement = UnitOfPressure.HPA - - -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_TEMPERATURE) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class Temperature(Sensor): - """Temperature Sensor.""" - - _attribute_name = "measured_value" - _attr_device_class: SensorDeviceClass = SensorDeviceClass.TEMPERATURE - _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _divisor = 100 - _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS - - -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_DEVICE_TEMPERATURE) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class DeviceTemperature(Sensor): - """Device Temperature Sensor.""" - - _attribute_name = "current_temperature" - _attr_device_class: SensorDeviceClass = SensorDeviceClass.TEMPERATURE - _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _attr_translation_key: str = "device_temperature" - _divisor = 100 - _attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS - _attr_entity_category = EntityCategory.DIAGNOSTIC - - -@MULTI_MATCH(cluster_handler_names="carbon_dioxide_concentration") -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class CarbonDioxideConcentration(Sensor): - """Carbon Dioxide Concentration sensor.""" - - _attribute_name = "measured_value" - _attr_device_class: SensorDeviceClass = SensorDeviceClass.CO2 - _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _decimals = 0 - _multiplier = 1e6 - _attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION - - -@MULTI_MATCH(cluster_handler_names="carbon_monoxide_concentration") -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class CarbonMonoxideConcentration(Sensor): - """Carbon Monoxide Concentration sensor.""" - - _attribute_name = "measured_value" - _attr_device_class: SensorDeviceClass = SensorDeviceClass.CO - _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _decimals = 0 - _multiplier = 1e6 - _attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION - - -@MULTI_MATCH(generic_ids="cluster_handler_0x042e", stop_on_match_group="voc_level") -@MULTI_MATCH(cluster_handler_names="voc_level", stop_on_match_group="voc_level") -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class VOCLevel(Sensor): - """VOC Level sensor.""" - - _attribute_name = "measured_value" - _attr_device_class: SensorDeviceClass = SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS - _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _decimals = 0 - _multiplier = 1e6 - _attr_native_unit_of_measurement = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - - -@MULTI_MATCH( - cluster_handler_names="voc_level", - models="lumi.airmonitor.acn01", - stop_on_match_group="voc_level", -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class PPBVOCLevel(Sensor): - """VOC Level sensor.""" - - _attribute_name = "measured_value" - _attr_device_class: SensorDeviceClass = ( - SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS - ) - _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _decimals = 0 - _multiplier = 1 - _attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_BILLION - - -@MULTI_MATCH(cluster_handler_names="pm25") -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class PM25(Sensor): - """Particulate Matter 2.5 microns or less sensor.""" - - _attribute_name = "measured_value" - _attr_device_class: SensorDeviceClass = SensorDeviceClass.PM25 - _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _decimals = 0 - _multiplier = 1 - _attr_native_unit_of_measurement = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER - - -@MULTI_MATCH(cluster_handler_names="formaldehyde_concentration") -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class FormaldehydeConcentration(Sensor): - """Formaldehyde Concentration sensor.""" - - _attribute_name = "measured_value" - _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _attr_translation_key: str = "formaldehyde" - _decimals = 0 - _multiplier = 1e6 - _attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION - - -@MULTI_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class ThermostatHVACAction(Sensor): - """Thermostat HVAC action sensor.""" - - _unique_id_suffix = "hvac_action" - _attr_translation_key: str = "hvac_action" - - @classmethod - def create_entity( - cls, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> Self | None: - """Entity Factory. - - Return entity if it is a supported configuration, otherwise return None - """ - - return cls(unique_id, zha_device, cluster_handlers, **kwargs) + return self.entity_data.entity.native_value @property - def native_value(self) -> str | None: - """Return the current HVAC action.""" - if ( - self._cluster_handler.pi_heating_demand is None - and self._cluster_handler.pi_cooling_demand is None - ): - return self._rm_rs_action - return self._pi_demand_action - - @property - def _rm_rs_action(self) -> HVACAction | None: - """Return the current HVAC action based on running mode and running state.""" - - if (running_state := self._cluster_handler.running_state) is None: + def extra_state_attributes(self) -> Mapping[str, Any] | None: + """Return entity specific state attributes.""" + entity = self.entity_data.entity + if entity.extra_state_attribute_names is None: return None - rs_heat = ( - self._cluster_handler.RunningState.Heat_State_On - | self._cluster_handler.RunningState.Heat_2nd_Stage_On + if not entity.extra_state_attribute_names <= _EXTRA_STATE_ATTRIBUTES: + _LOGGER.warning( + "Unexpected extra state attributes found for sensor %s: %s", + entity, + entity.extra_state_attribute_names - _EXTRA_STATE_ATTRIBUTES, + ) + + return exclude_none_values( + { + name: entity.state.get(name) + for name in entity.extra_state_attribute_names + } ) - if running_state & rs_heat: - return HVACAction.HEATING - - rs_cool = ( - self._cluster_handler.RunningState.Cool_State_On - | self._cluster_handler.RunningState.Cool_2nd_Stage_On - ) - if running_state & rs_cool: - return HVACAction.COOLING - - running_state = self._cluster_handler.running_state - if running_state and running_state & ( - self._cluster_handler.RunningState.Fan_State_On - | self._cluster_handler.RunningState.Fan_2nd_Stage_On - | self._cluster_handler.RunningState.Fan_3rd_Stage_On - ): - return HVACAction.FAN - - running_state = self._cluster_handler.running_state - if running_state and running_state & self._cluster_handler.RunningState.Idle: - return HVACAction.IDLE - - if self._cluster_handler.system_mode != self._cluster_handler.SystemMode.Off: - return HVACAction.IDLE - return HVACAction.OFF - - @property - def _pi_demand_action(self) -> HVACAction: - """Return the current HVAC action based on pi_demands.""" - - heating_demand = self._cluster_handler.pi_heating_demand - if heating_demand is not None and heating_demand > 0: - return HVACAction.HEATING - cooling_demand = self._cluster_handler.pi_cooling_demand - if cooling_demand is not None and cooling_demand > 0: - return HVACAction.COOLING - - if self._cluster_handler.system_mode != self._cluster_handler.SystemMode.Off: - return HVACAction.IDLE - return HVACAction.OFF - - -@MULTI_MATCH( - cluster_handler_names={CLUSTER_HANDLER_THERMOSTAT}, - manufacturers="Sinope Technologies", - stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class SinopeHVACAction(ThermostatHVACAction): - """Sinope Thermostat HVAC action sensor.""" - - @property - def _rm_rs_action(self) -> HVACAction: - """Return the current HVAC action based on running mode and running state.""" - - running_mode = self._cluster_handler.running_mode - if running_mode == self._cluster_handler.RunningMode.Heat: - return HVACAction.HEATING - if running_mode == self._cluster_handler.RunningMode.Cool: - return HVACAction.COOLING - - running_state = self._cluster_handler.running_state - if running_state and running_state & ( - self._cluster_handler.RunningState.Fan_State_On - | self._cluster_handler.RunningState.Fan_2nd_Stage_On - | self._cluster_handler.RunningState.Fan_3rd_Stage_On - ): - return HVACAction.FAN - if ( - self._cluster_handler.system_mode != self._cluster_handler.SystemMode.Off - and running_mode == self._cluster_handler.SystemMode.Off - ): - return HVACAction.IDLE - return HVACAction.OFF - - -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_BASIC) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class RSSISensor(Sensor): - """RSSI sensor for a device.""" - - _attribute_name = "rssi" - _unique_id_suffix = "rssi" - _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _attr_device_class: SensorDeviceClass | None = SensorDeviceClass.SIGNAL_STRENGTH - _attr_native_unit_of_measurement: str | None = SIGNAL_STRENGTH_DECIBELS_MILLIWATT - _attr_entity_category = EntityCategory.DIAGNOSTIC - _attr_entity_registry_enabled_default = False - _attr_should_poll = True # BaseZhaEntity defaults to False - _attr_translation_key: str = "rssi" - - @classmethod - def create_entity( - cls, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> Self | None: - """Entity Factory. - - Return entity if it is a supported configuration, otherwise return None - """ - key = f"{CLUSTER_HANDLER_BASIC}_{cls._unique_id_suffix}" - if ZHA_ENTITIES.prevent_entity_creation(Platform.SENSOR, zha_device.ieee, key): - return None - return cls(unique_id, zha_device, cluster_handlers, **kwargs) - - @property - def native_value(self) -> StateType: - """Return the state of the entity.""" - return getattr(self._zha_device.device, self._attribute_name) - - -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_BASIC) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class LQISensor(RSSISensor): - """LQI sensor for a device.""" - - _attribute_name = "lqi" - _unique_id_suffix = "lqi" - _attr_device_class = None - _attr_native_unit_of_measurement = None - _attr_translation_key = "lqi" - - -@MULTI_MATCH( - cluster_handler_names="tuya_manufacturer", - manufacturers={ - "_TZE200_htnnfasr", - }, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class TimeLeft(Sensor): - """Sensor that displays time left value.""" - - _attribute_name = "timer_time_left" - _unique_id_suffix = "time_left" - _attr_device_class: SensorDeviceClass = SensorDeviceClass.DURATION - _attr_translation_key: str = "timer_time_left" - _attr_native_unit_of_measurement = UnitOfTime.MINUTES - - -@MULTI_MATCH(cluster_handler_names="ikea_airpurifier") -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class IkeaDeviceRunTime(Sensor): - """Sensor that displays device run time (in minutes).""" - - _attribute_name = "device_run_time" - _unique_id_suffix = "device_run_time" - _attr_device_class: SensorDeviceClass = SensorDeviceClass.DURATION - _attr_translation_key: str = "device_run_time" - _attr_native_unit_of_measurement = UnitOfTime.MINUTES - _attr_entity_category: EntityCategory = EntityCategory.DIAGNOSTIC - - -@MULTI_MATCH(cluster_handler_names="ikea_airpurifier") -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class IkeaFilterRunTime(Sensor): - """Sensor that displays run time of the current filter (in minutes).""" - - _attribute_name = "filter_run_time" - _unique_id_suffix = "filter_run_time" - _attr_device_class: SensorDeviceClass = SensorDeviceClass.DURATION - _attr_translation_key: str = "filter_run_time" - _attr_native_unit_of_measurement = UnitOfTime.MINUTES - _attr_entity_category: EntityCategory = EntityCategory.DIAGNOSTIC - - -class AqaraFeedingSource(types.enum8): - """Aqara pet feeder feeding source.""" - - Feeder = 0x01 - HomeAssistant = 0x02 - - -@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"}) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class AqaraPetFeederLastFeedingSource(EnumSensor): - """Sensor that displays the last feeding source of pet feeder.""" - - _attribute_name = "last_feeding_source" - _unique_id_suffix = "last_feeding_source" - _attr_translation_key: str = "last_feeding_source" - _enum = AqaraFeedingSource - - -@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"}) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class AqaraPetFeederLastFeedingSize(Sensor): - """Sensor that displays the last feeding size of the pet feeder.""" - - _attribute_name = "last_feeding_size" - _unique_id_suffix = "last_feeding_size" - _attr_translation_key: str = "last_feeding_size" - - -@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"}) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class AqaraPetFeederPortionsDispensed(Sensor): - """Sensor that displays the number of portions dispensed by the pet feeder.""" - - _attribute_name = "portions_dispensed" - _unique_id_suffix = "portions_dispensed" - _attr_translation_key: str = "portions_dispensed_today" - _attr_state_class: SensorStateClass = SensorStateClass.TOTAL_INCREASING - - -@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"}) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class AqaraPetFeederWeightDispensed(Sensor): - """Sensor that displays the weight dispensed by the pet feeder.""" - - _attribute_name = "weight_dispensed" - _unique_id_suffix = "weight_dispensed" - _attr_translation_key: str = "weight_dispensed_today" - _attr_native_unit_of_measurement = UnitOfMass.GRAMS - _attr_state_class: SensorStateClass = SensorStateClass.TOTAL_INCREASING - - -@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"lumi.sensor_smoke.acn03"}) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class AqaraSmokeDensityDbm(Sensor): - """Sensor that displays the smoke density of an Aqara smoke sensor in dB/m.""" - - _attribute_name = "smoke_density_dbm" - _unique_id_suffix = "smoke_density_dbm" - _attr_translation_key: str = "smoke_density" - _attr_native_unit_of_measurement = "dB/m" - _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _attr_suggested_display_precision: int = 3 - - -class SonoffIlluminationStates(types.enum8): - """Enum for displaying last Illumination state.""" - - Dark = 0x00 - Light = 0x01 - - -@MULTI_MATCH(cluster_handler_names="sonoff_manufacturer", models={"SNZB-06P"}) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class SonoffPresenceSenorIlluminationStatus(EnumSensor): - """Sensor that displays the illumination status the last time peresence was detected.""" - - _attribute_name = "last_illumination_state" - _unique_id_suffix = "last_illumination" - _attr_translation_key: str = "last_illumination_state" - _enum = SonoffIlluminationStates - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class PiHeatingDemand(Sensor): - """Sensor that displays the percentage of heating power demanded. - - Optional thermostat attribute. - """ - - _unique_id_suffix = "pi_heating_demand" - _attribute_name = "pi_heating_demand" - _attr_translation_key: str = "pi_heating_demand" - _attr_native_unit_of_measurement = PERCENTAGE - _decimals = 0 - _attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT - _attr_entity_category = EntityCategory.DIAGNOSTIC - - -class SetpointChangeSourceEnum(types.enum8): - """The source of the setpoint change.""" - - Manual = 0x00 - Schedule = 0x01 - External = 0x02 - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class SetpointChangeSource(EnumSensor): - """Sensor that displays the source of the setpoint change. - - Optional thermostat attribute. - """ - - _unique_id_suffix = "setpoint_change_source" - _attribute_name = "setpoint_change_source" - _attr_translation_key: str = "setpoint_change_source" - _attr_entity_category = EntityCategory.DIAGNOSTIC - _enum = SetpointChangeSourceEnum - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_COVER) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class WindowCoveringTypeSensor(EnumSensor): - """Sensor that displays the type of a cover device.""" - - _attribute_name: str = WindowCovering.AttributeDefs.window_covering_type.name - _enum = WindowCovering.WindowCoveringType - _unique_id_suffix: str = WindowCovering.AttributeDefs.window_covering_type.name - _attr_translation_key: str = WindowCovering.AttributeDefs.window_covering_type.name - _attr_entity_category = EntityCategory.DIAGNOSTIC - _attr_icon = "mdi:curtains" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_BASIC, models={"lumi.curtain.agl001"} -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class AqaraCurtainMotorPowerSourceSensor(EnumSensor): - """Sensor that displays the power source of the Aqara E1 curtain motor device.""" - - _attribute_name: str = Basic.AttributeDefs.power_source.name - _enum = Basic.PowerSource - _unique_id_suffix: str = Basic.AttributeDefs.power_source.name - _attr_translation_key: str = Basic.AttributeDefs.power_source.name - _attr_entity_category = EntityCategory.DIAGNOSTIC - _attr_icon = "mdi:battery-positive" - - -class AqaraE1HookState(types.enum8): - """Aqara hook state.""" - - Unlocked = 0x00 - Locked = 0x01 - Locking = 0x02 - Unlocking = 0x03 - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.curtain.agl001"} -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class AqaraCurtainHookStateSensor(EnumSensor): - """Representation of a ZHA curtain mode configuration entity.""" - - _attribute_name = "hooks_state" - _enum = AqaraE1HookState - _unique_id_suffix = "hooks_state" - _attr_translation_key: str = "hooks_state" - _attr_icon: str = "mdi:hook" - _attr_entity_category = EntityCategory.DIAGNOSTIC - - -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class BitMapSensor(Sensor): - """A sensor with only state attributes. - - The sensor value will be an aggregate of the state attributes. - """ - - _bitmap: types.bitmap8 | types.bitmap16 - - def formatter(self, _value: int) -> str: - """Summary of all attributes.""" - binary_state_attributes = [ - key for (key, elem) in self.extra_state_attributes.items() if elem - ] - - return "something" if binary_state_attributes else "nothing" - - @property - def extra_state_attributes(self) -> dict[str, Any]: - """Bitmap.""" - value = self._cluster_handler.cluster.get(self._attribute_name) - - state_attr = {} - - for bit in list(self._bitmap): - if value is None: - state_attr[bit.name] = False - else: - state_attr[bit.name] = bit in self._bitmap(value) - - return state_attr - - -@MULTI_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class DanfossOpenWindowDetection(EnumSensor): - """Danfoss proprietary attribute. - - Sensor that displays whether the TRV detects an open window using the temperature sensor. - """ - - _unique_id_suffix = "open_window_detection" - _attribute_name = "open_window_detection" - _attr_translation_key: str = "open_window_detected" - _attr_icon: str = "mdi:window-open" - _enum = danfoss_thermostat.DanfossOpenWindowDetectionEnum - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class DanfossLoadEstimate(Sensor): - """Danfoss proprietary attribute for communicating its estimate of the radiator load.""" - - _unique_id_suffix = "load_estimate" - _attribute_name = "load_estimate" - _attr_translation_key: str = "load_estimate" - _attr_icon: str = "mdi:scale-balance" - _attr_entity_category = EntityCategory.DIAGNOSTIC - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class DanfossAdaptationRunStatus(BitMapSensor): - """Danfoss proprietary attribute for showing the status of the adaptation run.""" - - _unique_id_suffix = "adaptation_run_status" - _attribute_name = "adaptation_run_status" - _attr_translation_key: str = "adaptation_run_status" - _attr_entity_category = EntityCategory.DIAGNOSTIC - _bitmap = danfoss_thermostat.DanfossAdaptationRunStatusBitmap - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class DanfossPreheatTime(Sensor): - """Danfoss proprietary attribute for communicating the time when it starts pre-heating.""" - - _unique_id_suffix = "preheat_time" - _attribute_name = "preheat_time" - _attr_translation_key: str = "preheat_time" - _attr_icon: str = "mdi:radiator" - _attr_entity_registry_enabled_default = False - _attr_entity_category = EntityCategory.DIAGNOSTIC - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="diagnostic", - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class DanfossSoftwareErrorCode(BitMapSensor): - """Danfoss proprietary attribute for communicating the error code.""" - - _unique_id_suffix = "sw_error_code" - _attribute_name = "sw_error_code" - _attr_translation_key: str = "software_error" - _attr_entity_category = EntityCategory.DIAGNOSTIC - _bitmap = danfoss_thermostat.DanfossSoftwareErrorCodeBitmap - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="diagnostic", - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -# pylint: disable-next=hass-invalid-inheritance # needs fixing -class DanfossMotorStepCounter(Sensor): - """Danfoss proprietary attribute for communicating the motor step counter.""" - - _unique_id_suffix = "motor_step_counter" - _attribute_name = "motor_step_counter" - _attr_translation_key: str = "motor_stepcount" - _attr_entity_category = EntityCategory.DIAGNOSTIC diff --git a/homeassistant/components/zha/siren.py b/homeassistant/components/zha/siren.py index 3aab332f746..9d876d9ca4d 100644 --- a/homeassistant/components/zha/siren.py +++ b/homeassistant/components/zha/siren.py @@ -2,11 +2,18 @@ from __future__ import annotations -from collections.abc import Callable import functools -from typing import TYPE_CHECKING, Any, cast +from typing import Any -from zigpy.zcl.clusters.security import IasWd as WD +from zha.application.const import ( + WARNING_DEVICE_MODE_BURGLAR, + WARNING_DEVICE_MODE_EMERGENCY, + WARNING_DEVICE_MODE_EMERGENCY_PANIC, + WARNING_DEVICE_MODE_FIRE, + WARNING_DEVICE_MODE_FIRE_PANIC, + WARNING_DEVICE_MODE_POLICE_PANIC, +) +from zha.application.platforms.siren import SirenEntityFeature as ZHASirenEntityFeature from homeassistant.components.siren import ( ATTR_DURATION, @@ -17,38 +24,18 @@ from homeassistant.components.siren import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_call_later -from .core import discovery -from .core.cluster_handlers.security import IasWdClusterHandler -from .core.const import ( - CLUSTER_HANDLER_IAS_WD, +from .entity import ZHAEntity +from .helpers import ( SIGNAL_ADD_ENTITIES, - WARNING_DEVICE_MODE_BURGLAR, - WARNING_DEVICE_MODE_EMERGENCY, - WARNING_DEVICE_MODE_EMERGENCY_PANIC, - WARNING_DEVICE_MODE_FIRE, - WARNING_DEVICE_MODE_FIRE_PANIC, - WARNING_DEVICE_MODE_POLICE_PANIC, - WARNING_DEVICE_MODE_STOP, - WARNING_DEVICE_SOUND_HIGH, - WARNING_DEVICE_STROBE_HIGH, - WARNING_DEVICE_STROBE_NO, - Strobe, + EntityData, + async_add_entities as zha_async_add_entities, + convert_zha_error_to_ha_error, + get_zha_data, ) -from .core.helpers import get_zha_data -from .core.registries import ZHA_ENTITIES -from .entity import ZhaEntity - -if TYPE_CHECKING: - from .core.cluster_handlers import ClusterHandler - from .core.device import ZHADevice - -MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.SIREN) -DEFAULT_DURATION = 5 # seconds async def async_setup_entry( @@ -64,115 +51,61 @@ async def async_setup_entry( hass, SIGNAL_ADD_ENTITIES, functools.partial( - discovery.async_add_entities, - async_add_entities, - entities_to_create, + zha_async_add_entities, async_add_entities, ZHASiren, entities_to_create ), ) config_entry.async_on_unload(unsub) -@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_IAS_WD) -class ZHASiren(ZhaEntity, SirenEntity): +class ZHASiren(ZHAEntity, SirenEntity): """Representation of a ZHA siren.""" - _attr_name: str = "Siren" + _attr_available_tones: list[int | str] | dict[int, str] | None = { + WARNING_DEVICE_MODE_BURGLAR: "Burglar", + WARNING_DEVICE_MODE_FIRE: "Fire", + WARNING_DEVICE_MODE_EMERGENCY: "Emergency", + WARNING_DEVICE_MODE_POLICE_PANIC: "Police Panic", + WARNING_DEVICE_MODE_FIRE_PANIC: "Fire Panic", + WARNING_DEVICE_MODE_EMERGENCY_PANIC: "Emergency Panic", + } - def __init__( - self, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs, - ) -> None: - """Init this siren.""" - self._attr_supported_features = ( - SirenEntityFeature.TURN_ON - | SirenEntityFeature.TURN_OFF - | SirenEntityFeature.DURATION - | SirenEntityFeature.VOLUME_SET - | SirenEntityFeature.TONES - ) - self._attr_available_tones: list[int | str] | dict[int, str] | None = { - WARNING_DEVICE_MODE_BURGLAR: "Burglar", - WARNING_DEVICE_MODE_FIRE: "Fire", - WARNING_DEVICE_MODE_EMERGENCY: "Emergency", - WARNING_DEVICE_MODE_POLICE_PANIC: "Police Panic", - WARNING_DEVICE_MODE_FIRE_PANIC: "Fire Panic", - WARNING_DEVICE_MODE_EMERGENCY_PANIC: "Emergency Panic", - } - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - self._cluster_handler: IasWdClusterHandler = cast( - IasWdClusterHandler, cluster_handlers[0] - ) - self._attr_is_on: bool = False - self._off_listener: Callable[[], None] | None = None + def __init__(self, entity_data: EntityData, **kwargs: Any) -> None: + """Initialize the ZHA siren.""" + super().__init__(entity_data, **kwargs) + features: SirenEntityFeature = SirenEntityFeature(0) + zha_features: ZHASirenEntityFeature = self.entity_data.entity.supported_features + + if ZHASirenEntityFeature.TURN_ON in zha_features: + features |= SirenEntityFeature.TURN_ON + if ZHASirenEntityFeature.TURN_OFF in zha_features: + features |= SirenEntityFeature.TURN_OFF + if ZHASirenEntityFeature.TONES in zha_features: + features |= SirenEntityFeature.TONES + if ZHASirenEntityFeature.VOLUME_SET in zha_features: + features |= SirenEntityFeature.VOLUME_SET + if ZHASirenEntityFeature.DURATION in zha_features: + features |= SirenEntityFeature.DURATION + + self._attr_supported_features = features + + @property + def is_on(self) -> bool: + """Return True if entity is on.""" + return self.entity_data.entity.is_on + + @convert_zha_error_to_ha_error async def async_turn_on(self, **kwargs: Any) -> None: """Turn on siren.""" - if self._off_listener: - self._off_listener() - self._off_listener = None - tone_cache = self._cluster_handler.data_cache.get( - WD.Warning.WarningMode.__name__ - ) - siren_tone = ( - tone_cache.value - if tone_cache is not None - else WARNING_DEVICE_MODE_EMERGENCY - ) - siren_duration = DEFAULT_DURATION - level_cache = self._cluster_handler.data_cache.get( - WD.Warning.SirenLevel.__name__ - ) - siren_level = ( - level_cache.value if level_cache is not None else WARNING_DEVICE_SOUND_HIGH - ) - strobe_cache = self._cluster_handler.data_cache.get(Strobe.__name__) - should_strobe = ( - strobe_cache.value if strobe_cache is not None else Strobe.No_Strobe - ) - strobe_level_cache = self._cluster_handler.data_cache.get( - WD.StrobeLevel.__name__ - ) - strobe_level = ( - strobe_level_cache.value - if strobe_level_cache is not None - else WARNING_DEVICE_STROBE_HIGH - ) - if (duration := kwargs.get(ATTR_DURATION)) is not None: - siren_duration = duration - if (tone := kwargs.get(ATTR_TONE)) is not None: - siren_tone = tone - if (level := kwargs.get(ATTR_VOLUME_LEVEL)) is not None: - siren_level = int(level) - await self._cluster_handler.issue_start_warning( - mode=siren_tone, - warning_duration=siren_duration, - siren_level=siren_level, - strobe=should_strobe, - strobe_duty_cycle=50 if should_strobe else 0, - strobe_intensity=strobe_level, - ) - self._attr_is_on = True - self._off_listener = async_call_later( - self._zha_device.hass, siren_duration, self.async_set_off + await self.entity_data.entity.async_turn_on( + duration=kwargs.get(ATTR_DURATION), + tone=kwargs.get(ATTR_TONE), + volume_level=kwargs.get(ATTR_VOLUME_LEVEL), ) self.async_write_ha_state() + @convert_zha_error_to_ha_error async def async_turn_off(self, **kwargs: Any) -> None: """Turn off siren.""" - await self._cluster_handler.issue_start_warning( - mode=WARNING_DEVICE_MODE_STOP, strobe=WARNING_DEVICE_STROBE_NO - ) - self._attr_is_on = False - self.async_write_ha_state() - - @callback - def async_set_off(self, _) -> None: - """Set is_on to False and write HA state.""" - self._attr_is_on = False - if self._off_listener: - self._off_listener() - self._off_listener = None + await self.entity_data.entity.async_turn_off() self.async_write_ha_state() diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index f07d3d4c8e3..cb0268f98e0 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -4,44 +4,21 @@ from __future__ import annotations import functools import logging -from typing import TYPE_CHECKING, Any, Self - -from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT, TUYA_PLUG_ONOFF -from zigpy.quirks.v2 import SwitchMetadata -from zigpy.zcl.clusters.closures import ConfigStatus, WindowCovering, WindowCoveringMode -from zigpy.zcl.clusters.general import OnOff -from zigpy.zcl.foundation import Status +from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry -from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, EntityCategory, Platform -from homeassistant.core import HomeAssistant, State, callback +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .core import discovery -from .core.const import ( - CLUSTER_HANDLER_BASIC, - CLUSTER_HANDLER_COVER, - CLUSTER_HANDLER_INOVELLI, - CLUSTER_HANDLER_ON_OFF, - CLUSTER_HANDLER_THERMOSTAT, - ENTITY_METADATA, +from .entity import ZHAEntity +from .helpers import ( SIGNAL_ADD_ENTITIES, - SIGNAL_ATTR_UPDATED, -) -from .core.helpers import get_zha_data -from .core.registries import ZHA_ENTITIES -from .entity import ZhaEntity, ZhaGroupEntity - -if TYPE_CHECKING: - from .core.cluster_handlers import ClusterHandler - from .core.device import ZHADevice - -STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.SWITCH) -GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, Platform.SWITCH) -CONFIG_DIAGNOSTIC_MATCH = functools.partial( - ZHA_ENTITIES.config_diagnostic_match, Platform.SWITCH + async_add_entities as zha_async_add_entities, + convert_zha_error_to_ha_error, + get_zha_data, ) _LOGGER = logging.getLogger(__name__) @@ -60,752 +37,28 @@ async def async_setup_entry( hass, SIGNAL_ADD_ENTITIES, functools.partial( - discovery.async_add_entities, async_add_entities, entities_to_create + zha_async_add_entities, async_add_entities, Switch, entities_to_create ), ) config_entry.async_on_unload(unsub) -@STRICT_MATCH(cluster_handler_names=CLUSTER_HANDLER_ON_OFF) -class Switch(ZhaEntity, SwitchEntity): +class Switch(ZHAEntity, SwitchEntity): """ZHA switch.""" - _attr_translation_key = "switch" - - def __init__( - self, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> None: - """Initialize the ZHA switch.""" - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - self._on_off_cluster_handler = self.cluster_handlers[CLUSTER_HANDLER_ON_OFF] - @property def is_on(self) -> bool: """Return if the switch is on based on the statemachine.""" - if self._on_off_cluster_handler.on_off is None: - return False - return self._on_off_cluster_handler.on_off + return self.entity_data.entity.is_on + @convert_zha_error_to_ha_error async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" - await self._on_off_cluster_handler.turn_on() + await self.entity_data.entity.async_turn_on() self.async_write_ha_state() + @convert_zha_error_to_ha_error async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" - await self._on_off_cluster_handler.turn_off() + await self.entity_data.entity.async_turn_off() self.async_write_ha_state() - - @callback - def async_set_state(self, attr_id: int, attr_name: str, value: Any): - """Handle state update from cluster handler.""" - self.async_write_ha_state() - - async def async_added_to_hass(self) -> None: - """Run when about to be added to hass.""" - await super().async_added_to_hass() - self.async_accept_signal( - self._on_off_cluster_handler, SIGNAL_ATTR_UPDATED, self.async_set_state - ) - - async def async_update(self) -> None: - """Attempt to retrieve on off state from the switch.""" - self.debug("Polling current state") - await self._on_off_cluster_handler.get_attribute_value( - "on_off", from_cache=False - ) - - -@GROUP_MATCH() -class SwitchGroup(ZhaGroupEntity, SwitchEntity): - """Representation of a switch group.""" - - def __init__( - self, - entity_ids: list[str], - unique_id: str, - group_id: int, - zha_device: ZHADevice, - **kwargs: Any, - ) -> None: - """Initialize a switch group.""" - super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs) - self._available: bool - self._state: bool - group = self.zha_device.gateway.get_group(self._group_id) - self._on_off_cluster_handler = group.endpoint[OnOff.cluster_id] - - @property - def is_on(self) -> bool: - """Return if the switch is on based on the statemachine.""" - return bool(self._state) - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the entity on.""" - result = await self._on_off_cluster_handler.on() - if result[1] is not Status.SUCCESS: - return - self._state = True - self.async_write_ha_state() - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the entity off.""" - result = await self._on_off_cluster_handler.off() - if result[1] is not Status.SUCCESS: - return - self._state = False - self.async_write_ha_state() - - async def async_update(self) -> None: - """Query all members and determine the switch group state.""" - all_states = [self.hass.states.get(x) for x in self._entity_ids] - states: list[State] = list(filter(None, all_states)) - on_states = [state for state in states if state.state == STATE_ON] - - self._state = len(on_states) > 0 - self._available = any(state.state != STATE_UNAVAILABLE for state in states) - - -class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): - """Representation of a ZHA switch configuration entity.""" - - _attr_entity_category = EntityCategory.CONFIG - _attribute_name: str - _inverter_attribute_name: str | None = None - _force_inverted: bool = False - _off_value: int = 0 - _on_value: int = 1 - - @classmethod - def create_entity( - cls, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> Self | None: - """Entity Factory. - - Return entity if it is a supported configuration, otherwise return None - """ - cluster_handler = cluster_handlers[0] - if ENTITY_METADATA not in kwargs and ( - cls._attribute_name in cluster_handler.cluster.unsupported_attributes - or cls._attribute_name not in cluster_handler.cluster.attributes_by_name - or cluster_handler.cluster.get(cls._attribute_name) is None - ): - _LOGGER.debug( - "%s is not supported - skipping %s entity creation", - cls._attribute_name, - cls.__name__, - ) - return None - - return cls(unique_id, zha_device, cluster_handlers, **kwargs) - - def __init__( - self, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> None: - """Init this number configuration entity.""" - self._cluster_handler: ClusterHandler = cluster_handlers[0] - if ENTITY_METADATA in kwargs: - self._init_from_quirks_metadata(kwargs[ENTITY_METADATA]) - super().__init__(unique_id, zha_device, cluster_handlers, **kwargs) - - def _init_from_quirks_metadata(self, entity_metadata: SwitchMetadata) -> None: - """Init this entity from the quirks metadata.""" - super()._init_from_quirks_metadata(entity_metadata) - self._attribute_name = entity_metadata.attribute_name - if entity_metadata.invert_attribute_name: - self._inverter_attribute_name = entity_metadata.invert_attribute_name - if entity_metadata.force_inverted: - self._force_inverted = entity_metadata.force_inverted - self._off_value = entity_metadata.off_value - self._on_value = entity_metadata.on_value - - async def async_added_to_hass(self) -> None: - """Run when about to be added to hass.""" - await super().async_added_to_hass() - self.async_accept_signal( - self._cluster_handler, SIGNAL_ATTR_UPDATED, self.async_set_state - ) - - @callback - def async_set_state(self, attr_id: int, attr_name: str, value: Any): - """Handle state update from cluster handler.""" - self.async_write_ha_state() - - @property - def inverted(self) -> bool: - """Return True if the switch is inverted.""" - if self._inverter_attribute_name: - return bool( - self._cluster_handler.cluster.get(self._inverter_attribute_name) - ) - return self._force_inverted - - @property - def is_on(self) -> bool: - """Return if the switch is on based on the statemachine.""" - if self._on_value != 1: - val = self._cluster_handler.cluster.get(self._attribute_name) - val = val == self._on_value - else: - val = bool(self._cluster_handler.cluster.get(self._attribute_name)) - return (not val) if self.inverted else val - - async def async_turn_on_off(self, state: bool) -> None: - """Turn the entity on or off.""" - if self.inverted: - state = not state - if state: - await self._cluster_handler.write_attributes_safe( - {self._attribute_name: self._on_value} - ) - else: - await self._cluster_handler.write_attributes_safe( - {self._attribute_name: self._off_value} - ) - self.async_write_ha_state() - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the entity on.""" - await self.async_turn_on_off(True) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the entity off.""" - await self.async_turn_on_off(False) - - async def async_update(self) -> None: - """Attempt to retrieve the state of the entity.""" - self.debug("Polling current state") - value = await self._cluster_handler.get_attribute_value( - self._attribute_name, from_cache=False - ) - await self._cluster_handler.get_attribute_value( - self._inverter_attribute_name, from_cache=False - ) - self.debug("read value=%s, inverted=%s", value, self.inverted) - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="tuya_manufacturer", - manufacturers={ - "_TZE200_b6wax7g0", - }, -) -class OnOffWindowDetectionFunctionConfigurationEntity(ZHASwitchConfigurationEntity): - """Representation of a ZHA window detection configuration entity.""" - - _unique_id_suffix = "on_off_window_opened_detection" - _attribute_name = "window_detection_function" - _inverter_attribute_name = "window_detection_function_inverter" - _attr_translation_key = "window_detection_function" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.motion.ac02"} -) -class P1MotionTriggerIndicatorSwitch(ZHASwitchConfigurationEntity): - """Representation of a ZHA motion triggering configuration entity.""" - - _unique_id_suffix = "trigger_indicator" - _attribute_name = "trigger_indicator" - _attr_translation_key = "trigger_indicator" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", - models={"lumi.plug.mmeu01", "lumi.plug.maeu01"}, -) -class XiaomiPlugPowerOutageMemorySwitch(ZHASwitchConfigurationEntity): - """Representation of a ZHA power outage memory configuration entity.""" - - _unique_id_suffix = "power_outage_memory" - _attribute_name = "power_outage_memory" - _attr_translation_key = "power_outage_memory" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_BASIC, - manufacturers={"Philips", "Signify Netherlands B.V."}, - models={"SML001", "SML002", "SML003", "SML004"}, -) -class HueMotionTriggerIndicatorSwitch(ZHASwitchConfigurationEntity): - """Representation of a ZHA motion triggering configuration entity.""" - - _unique_id_suffix = "trigger_indicator" - _attribute_name = "trigger_indicator" - _attr_translation_key = "trigger_indicator" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="ikea_airpurifier", - models={"STARKVIND Air purifier", "STARKVIND Air purifier table"}, -) -class ChildLock(ZHASwitchConfigurationEntity): - """ZHA BinarySensor.""" - - _unique_id_suffix = "child_lock" - _attribute_name = "child_lock" - _attr_translation_key = "child_lock" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="ikea_airpurifier", - models={"STARKVIND Air purifier", "STARKVIND Air purifier table"}, -) -class DisableLed(ZHASwitchConfigurationEntity): - """ZHA BinarySensor.""" - - _unique_id_suffix = "disable_led" - _attribute_name = "disable_led" - _attr_translation_key = "disable_led" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_INOVELLI, -) -class InovelliInvertSwitch(ZHASwitchConfigurationEntity): - """Inovelli invert switch control.""" - - _unique_id_suffix = "invert_switch" - _attribute_name = "invert_switch" - _attr_translation_key = "invert_switch" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_INOVELLI, -) -class InovelliSmartBulbMode(ZHASwitchConfigurationEntity): - """Inovelli smart bulb mode control.""" - - _unique_id_suffix = "smart_bulb_mode" - _attribute_name = "smart_bulb_mode" - _attr_translation_key = "smart_bulb_mode" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_INOVELLI, models={"VZM35-SN"} -) -class InovelliSmartFanMode(ZHASwitchConfigurationEntity): - """Inovelli smart fan mode control.""" - - _unique_id_suffix = "smart_fan_mode" - _attribute_name = "smart_fan_mode" - _attr_translation_key = "smart_fan_mode" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_INOVELLI, -) -class InovelliDoubleTapUpEnabled(ZHASwitchConfigurationEntity): - """Inovelli double tap up enabled.""" - - _unique_id_suffix = "double_tap_up_enabled" - _attribute_name = "double_tap_up_enabled" - _attr_translation_key = "double_tap_up_enabled" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_INOVELLI, -) -class InovelliDoubleTapDownEnabled(ZHASwitchConfigurationEntity): - """Inovelli double tap down enabled.""" - - _unique_id_suffix = "double_tap_down_enabled" - _attribute_name = "double_tap_down_enabled" - _attr_translation_key = "double_tap_down_enabled" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_INOVELLI, -) -class InovelliAuxSwitchScenes(ZHASwitchConfigurationEntity): - """Inovelli unique aux switch scenes.""" - - _unique_id_suffix = "aux_switch_scenes" - _attribute_name = "aux_switch_scenes" - _attr_translation_key = "aux_switch_scenes" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_INOVELLI, -) -class InovelliBindingOffToOnSyncLevel(ZHASwitchConfigurationEntity): - """Inovelli send move to level with on/off to bound devices.""" - - _unique_id_suffix = "binding_off_to_on_sync_level" - _attribute_name = "binding_off_to_on_sync_level" - _attr_translation_key = "binding_off_to_on_sync_level" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_INOVELLI, -) -class InovelliLocalProtection(ZHASwitchConfigurationEntity): - """Inovelli local protection control.""" - - _unique_id_suffix = "local_protection" - _attribute_name = "local_protection" - _attr_translation_key = "local_protection" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_INOVELLI, -) -class InovelliOnOffLEDMode(ZHASwitchConfigurationEntity): - """Inovelli only 1 LED mode control.""" - - _unique_id_suffix = "on_off_led_mode" - _attribute_name = "on_off_led_mode" - _attr_translation_key = "one_led_mode" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_INOVELLI, -) -class InovelliFirmwareProgressLED(ZHASwitchConfigurationEntity): - """Inovelli firmware progress LED control.""" - - _unique_id_suffix = "firmware_progress_led" - _attribute_name = "firmware_progress_led" - _attr_translation_key = "firmware_progress_led" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_INOVELLI, -) -class InovelliRelayClickInOnOffMode(ZHASwitchConfigurationEntity): - """Inovelli relay click in on off mode control.""" - - _unique_id_suffix = "relay_click_in_on_off_mode" - _attribute_name = "relay_click_in_on_off_mode" - _attr_translation_key = "relay_click_in_on_off_mode" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_INOVELLI, -) -class InovelliDisableDoubleTapClearNotificationsMode(ZHASwitchConfigurationEntity): - """Inovelli disable clear notifications double tap control.""" - - _unique_id_suffix = "disable_clear_notifications_double_tap" - _attribute_name = "disable_clear_notifications_double_tap" - _attr_translation_key = "disable_clear_notifications_double_tap" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"} -) -class AqaraPetFeederLEDIndicator(ZHASwitchConfigurationEntity): - """Representation of a LED indicator configuration entity.""" - - _unique_id_suffix = "disable_led_indicator" - _attribute_name = "disable_led_indicator" - _attr_translation_key = "led_indicator" - _force_inverted = True - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"} -) -class AqaraPetFeederChildLock(ZHASwitchConfigurationEntity): - """Representation of a child lock configuration entity.""" - - _unique_id_suffix = "child_lock" - _attribute_name = "child_lock" - _attr_translation_key = "child_lock" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_ON_OFF, quirk_ids=TUYA_PLUG_ONOFF -) -class TuyaChildLockSwitch(ZHASwitchConfigurationEntity): - """Representation of a child lock configuration entity.""" - - _unique_id_suffix = "child_lock" - _attribute_name = "child_lock" - _attr_translation_key = "child_lock" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.airrtc.agl001"} -) -class AqaraThermostatWindowDetection(ZHASwitchConfigurationEntity): - """Representation of an Aqara thermostat window detection configuration entity.""" - - _unique_id_suffix = "window_detection" - _attribute_name = "window_detection" - _attr_translation_key = "window_detection" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.airrtc.agl001"} -) -class AqaraThermostatValveDetection(ZHASwitchConfigurationEntity): - """Representation of an Aqara thermostat valve detection configuration entity.""" - - _unique_id_suffix = "valve_detection" - _attribute_name = "valve_detection" - _attr_translation_key = "valve_detection" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.airrtc.agl001"} -) -class AqaraThermostatChildLock(ZHASwitchConfigurationEntity): - """Representation of an Aqara thermostat child lock configuration entity.""" - - _unique_id_suffix = "child_lock" - _attribute_name = "child_lock" - _attr_translation_key = "child_lock" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.sensor_smoke.acn03"} -) -class AqaraHeartbeatIndicator(ZHASwitchConfigurationEntity): - """Representation of a heartbeat indicator configuration entity for Aqara smoke sensors.""" - - _unique_id_suffix = "heartbeat_indicator" - _attribute_name = "heartbeat_indicator" - _attr_translation_key = "heartbeat_indicator" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.sensor_smoke.acn03"} -) -class AqaraLinkageAlarm(ZHASwitchConfigurationEntity): - """Representation of a linkage alarm configuration entity for Aqara smoke sensors.""" - - _unique_id_suffix = "linkage_alarm" - _attribute_name = "linkage_alarm" - _attr_translation_key = "linkage_alarm" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.sensor_smoke.acn03"} -) -class AqaraBuzzerManualMute(ZHASwitchConfigurationEntity): - """Representation of a buzzer manual mute configuration entity for Aqara smoke sensors.""" - - _unique_id_suffix = "buzzer_manual_mute" - _attribute_name = "buzzer_manual_mute" - _attr_translation_key = "buzzer_manual_mute" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.sensor_smoke.acn03"} -) -class AqaraBuzzerManualAlarm(ZHASwitchConfigurationEntity): - """Representation of a buzzer manual mute configuration entity for Aqara smoke sensors.""" - - _unique_id_suffix = "buzzer_manual_alarm" - _attribute_name = "buzzer_manual_alarm" - _attr_translation_key = "buzzer_manual_alarm" - - -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_COVER) -class WindowCoveringInversionSwitch(ZHASwitchConfigurationEntity): - """Representation of a switch that controls inversion for window covering devices. - - This is necessary because this cluster uses 2 attributes to control inversion. - """ - - _unique_id_suffix = "inverted" - _attribute_name = WindowCovering.AttributeDefs.config_status.name - _attr_translation_key = "inverted" - - @classmethod - def create_entity( - cls, - unique_id: str, - zha_device: ZHADevice, - cluster_handlers: list[ClusterHandler], - **kwargs: Any, - ) -> Self | None: - """Entity Factory. - - Return entity if it is a supported configuration, otherwise return None - """ - cluster_handler = cluster_handlers[0] - window_covering_mode_attr = ( - WindowCovering.AttributeDefs.window_covering_mode.name - ) - # this entity needs 2 attributes to function - if ( - cls._attribute_name in cluster_handler.cluster.unsupported_attributes - or cls._attribute_name not in cluster_handler.cluster.attributes_by_name - or cluster_handler.cluster.get(cls._attribute_name) is None - or window_covering_mode_attr - in cluster_handler.cluster.unsupported_attributes - or window_covering_mode_attr - not in cluster_handler.cluster.attributes_by_name - or cluster_handler.cluster.get(window_covering_mode_attr) is None - ): - _LOGGER.debug( - "%s is not supported - skipping %s entity creation", - cls._attribute_name, - cls.__name__, - ) - return None - - return cls(unique_id, zha_device, cluster_handlers, **kwargs) - - @property - def is_on(self) -> bool: - """Return if the switch is on based on the statemachine.""" - config_status = ConfigStatus( - self._cluster_handler.cluster.get(self._attribute_name) - ) - return ConfigStatus.Open_up_commands_reversed in config_status - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the entity on.""" - await self._async_on_off(True) - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn the entity off.""" - await self._async_on_off(False) - - async def async_update(self) -> None: - """Attempt to retrieve the state of the entity.""" - self.debug("Polling current state") - await self._cluster_handler.get_attributes( - [ - self._attribute_name, - WindowCovering.AttributeDefs.window_covering_mode.name, - ], - from_cache=False, - only_cache=False, - ) - self.async_write_ha_state() - - async def _async_on_off(self, invert: bool) -> None: - """Turn the entity on or off.""" - name: str = WindowCovering.AttributeDefs.window_covering_mode.name - current_mode: WindowCoveringMode = WindowCoveringMode( - self._cluster_handler.cluster.get(name) - ) - send_command: bool = False - if invert and WindowCoveringMode.Motor_direction_reversed not in current_mode: - current_mode |= WindowCoveringMode.Motor_direction_reversed - send_command = True - elif not invert and WindowCoveringMode.Motor_direction_reversed in current_mode: - current_mode &= ~WindowCoveringMode.Motor_direction_reversed - send_command = True - if send_command: - await self._cluster_handler.write_attributes_safe({name: current_mode}) - await self.async_update() - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names="opple_cluster", models={"lumi.curtain.agl001"} -) -class AqaraE1CurtainMotorHooksLockedSwitch(ZHASwitchConfigurationEntity): - """Representation of a switch that controls whether the curtain motor hooks are locked.""" - - _unique_id_suffix = "hooks_lock" - _attribute_name = "hooks_lock" - _attr_translation_key = "hooks_locked" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -class DanfossExternalOpenWindowDetected(ZHASwitchConfigurationEntity): - """Danfoss proprietary attribute for communicating an open window.""" - - _unique_id_suffix = "external_open_window_detected" - _attribute_name: str = "external_open_window_detected" - _attr_translation_key: str = "external_window_sensor" - _attr_icon: str = "mdi:window-open" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -class DanfossWindowOpenFeature(ZHASwitchConfigurationEntity): - """Danfoss proprietary attribute enabling open window detection.""" - - _unique_id_suffix = "window_open_feature" - _attribute_name: str = "window_open_feature" - _attr_translation_key: str = "use_internal_window_detection" - _attr_icon: str = "mdi:window-open" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -class DanfossMountingModeControl(ZHASwitchConfigurationEntity): - """Danfoss proprietary attribute for switching to mounting mode.""" - - _unique_id_suffix = "mounting_mode_control" - _attribute_name: str = "mounting_mode_control" - _attr_translation_key: str = "mounting_mode" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -class DanfossRadiatorCovered(ZHASwitchConfigurationEntity): - """Danfoss proprietary attribute for communicating full usage of the external temperature sensor.""" - - _unique_id_suffix = "radiator_covered" - _attribute_name: str = "radiator_covered" - _attr_translation_key: str = "prioritize_external_temperature_sensor" - _attr_icon: str = "mdi:thermometer" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -class DanfossHeatAvailable(ZHASwitchConfigurationEntity): - """Danfoss proprietary attribute for communicating available heat.""" - - _unique_id_suffix = "heat_available" - _attribute_name: str = "heat_available" - _attr_translation_key: str = "heat_available" - _attr_icon: str = "mdi:water-boiler" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -class DanfossLoadBalancingEnable(ZHASwitchConfigurationEntity): - """Danfoss proprietary attribute for enabling load balancing.""" - - _unique_id_suffix = "load_balancing_enable" - _attribute_name: str = "load_balancing_enable" - _attr_translation_key: str = "use_load_balancing" - _attr_icon: str = "mdi:scale-balance" - - -@CONFIG_DIAGNOSTIC_MATCH( - cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT, - quirk_ids={DANFOSS_ALLY_THERMOSTAT}, -) -class DanfossAdaptationRunSettings(ZHASwitchConfigurationEntity): - """Danfoss proprietary attribute for enabling daily adaptation run. - - Actually a bitmap, but only the first bit is used. - """ - - _unique_id_suffix = "adaptation_run_settings" - _attribute_name: str = "adaptation_run_settings" - _attr_translation_key: str = "adaptation_run_enabled" diff --git a/homeassistant/components/zha/update.py b/homeassistant/components/zha/update.py index 0cb80d13119..e12d048b190 100644 --- a/homeassistant/components/zha/update.py +++ b/homeassistant/components/zha/update.py @@ -5,11 +5,10 @@ from __future__ import annotations import functools import logging import math -from typing import TYPE_CHECKING, Any +from typing import Any -from zigpy.ota import OtaImageWithMetadata -from zigpy.zcl.clusters.general import Ota -from zigpy.zcl.foundation import Status +from zha.exceptions import ZHAException +from zigpy.application import ControllerApplication from homeassistant.components.update import ( UpdateDeviceClass, @@ -17,8 +16,8 @@ from homeassistant.components.update import ( UpdateEntityFeature, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EntityCategory, Platform -from homeassistant.core import HomeAssistant, callback +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity_platform import AddEntitiesCallback @@ -27,24 +26,17 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .core import discovery -from .core.const import CLUSTER_HANDLER_OTA, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED -from .core.helpers import get_zha_data, get_zha_gateway -from .core.registries import ZHA_ENTITIES -from .entity import ZhaEntity - -if TYPE_CHECKING: - from zigpy.application import ControllerApplication - - from .core.cluster_handlers import ClusterHandler - from .core.device import ZHADevice +from .entity import ZHAEntity +from .helpers import ( + SIGNAL_ADD_ENTITIES, + EntityData, + async_add_entities as zha_async_add_entities, + get_zha_data, + get_zha_gateway, +) _LOGGER = logging.getLogger(__name__) -CONFIG_DIAGNOSTIC_MATCH = functools.partial( - ZHA_ENTITIES.config_diagnostic_match, Platform.UPDATE -) - async def async_setup_entry( hass: HomeAssistant, @@ -53,20 +45,20 @@ async def async_setup_entry( ) -> None: """Set up the Zigbee Home Automation update from config entry.""" zha_data = get_zha_data(hass) + if zha_data.update_coordinator is None: + zha_data.update_coordinator = ZHAFirmwareUpdateCoordinator( + hass, get_zha_gateway(hass).application_controller + ) entities_to_create = zha_data.platforms[Platform.UPDATE] - coordinator = ZHAFirmwareUpdateCoordinator( - hass, get_zha_gateway(hass).application_controller - ) - unsub = async_dispatcher_connect( hass, SIGNAL_ADD_ENTITIES, functools.partial( - discovery.async_add_entities, + zha_async_add_entities, async_add_entities, + ZHAFirmwareUpdateEntity, entities_to_create, - coordinator=coordinator, ), ) config_entry.async_on_unload(unsub) @@ -93,14 +85,11 @@ class ZHAFirmwareUpdateCoordinator(DataUpdateCoordinator[None]): # pylint: disa await self.controller_application.ota.broadcast_notify(jitter=100) -@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_OTA) class ZHAFirmwareUpdateEntity( - ZhaEntity, CoordinatorEntity[ZHAFirmwareUpdateCoordinator], UpdateEntity + ZHAEntity, CoordinatorEntity[ZHAFirmwareUpdateCoordinator], UpdateEntity ): """Representation of a ZHA firmware update entity.""" - _unique_id_suffix = "firmware_update" - _attr_entity_category = EntityCategory.CONFIG _attr_device_class = UpdateDeviceClass.FIRMWARE _attr_supported_features = ( UpdateEntityFeature.INSTALL @@ -108,113 +97,70 @@ class ZHAFirmwareUpdateEntity( | UpdateEntityFeature.SPECIFIC_VERSION ) - def __init__( - self, - unique_id: str, - zha_device: ZHADevice, - channels: list[ClusterHandler], - coordinator: ZHAFirmwareUpdateCoordinator, - **kwargs: Any, - ) -> None: - """Initialize the ZHA update entity.""" - super().__init__(unique_id, zha_device, channels, **kwargs) - CoordinatorEntity.__init__(self, coordinator) + def __init__(self, entity_data: EntityData, **kwargs: Any) -> None: + """Initialize the ZHA siren.""" + zha_data = get_zha_data(entity_data.device_proxy.gateway_proxy.hass) + assert zha_data.update_coordinator is not None - self._ota_cluster_handler: ClusterHandler = self.cluster_handlers[ - CLUSTER_HANDLER_OTA - ] - self._attr_installed_version: str | None = self._get_cluster_version() - self._attr_latest_version = self._attr_installed_version - self._latest_firmware: OtaImageWithMetadata | None = None + super().__init__(entity_data, coordinator=zha_data.update_coordinator, **kwargs) + CoordinatorEntity.__init__(self, zha_data.update_coordinator) - def _get_cluster_version(self) -> str | None: - """Synchronize current file version with the cluster.""" + @property + def installed_version(self) -> str | None: + """Version installed and in use.""" + return self.entity_data.entity.installed_version - if self._ota_cluster_handler.current_file_version is not None: - return f"0x{self._ota_cluster_handler.current_file_version:08x}" + @property + def in_progress(self) -> bool | int | None: + """Update installation progress. - return None + Needs UpdateEntityFeature.PROGRESS flag to be set for it to be used. - @callback - def attribute_updated(self, attrid: int, name: str, value: Any) -> None: - """Handle attribute updates on the OTA cluster.""" - if attrid == Ota.AttributeDefs.current_file_version.id: - self._attr_installed_version = f"0x{value:08x}" - self.async_write_ha_state() + Can either return a boolean (True if in progress, False if not) + or an integer to indicate the progress in from 0 to 100%. + """ + if not self.entity_data.entity.in_progress: + return self.entity_data.entity.in_progress - @callback - def device_ota_update_available( - self, image: OtaImageWithMetadata, current_file_version: int - ) -> None: - """Handle ota update available signal from Zigpy.""" - self._latest_firmware = image - self._attr_latest_version = f"0x{image.version:08x}" - self._attr_installed_version = f"0x{current_file_version:08x}" + # Stay in an indeterminate state until we actually send something + if self.entity_data.entity.progress == 0: + return True - if image.metadata.changelog: - self._attr_release_summary = image.metadata.changelog + # Rescale 0-100% to 2-100% to avoid 0 and 1 colliding with None, False, and True + return int(math.ceil(2 + 98 * self.entity_data.entity.progress / 100)) - self.async_write_ha_state() + @property + def latest_version(self) -> str | None: + """Latest version available for install.""" + return self.entity_data.entity.latest_version - @callback - def _update_progress(self, current: int, total: int, progress: float) -> None: - """Update install progress on event.""" - # If we are not supposed to be updating, do nothing - if self._attr_in_progress is False: - return + @property + def release_summary(self) -> str | None: + """Summary of the release notes or changelog. - # Remap progress to 2-100 to avoid 0 and 1 - self._attr_in_progress = int(math.ceil(2 + 98 * progress / 100)) - self.async_write_ha_state() + This is not suitable for long changelogs, but merely suitable + for a short excerpt update description of max 255 characters. + """ + return self.entity_data.entity.release_summary + @property + def release_url(self) -> str | None: + """URL to the full release notes of the latest version available.""" + return self.entity_data.entity.release_url + + # We explicitly convert ZHA exceptions to HA exceptions here so there is no need to + # use the `@convert_zha_error_to_ha_error` decorator. async def async_install( self, version: str | None, backup: bool, **kwargs: Any ) -> None: """Install an update.""" - assert self._latest_firmware is not None - - # Set the progress to an indeterminate state - self._attr_in_progress = True - self.async_write_ha_state() - try: - result = await self.zha_device.device.update_firmware( - image=self._latest_firmware, - progress_callback=self._update_progress, - ) - except Exception as ex: - raise HomeAssistantError(f"Update was not successful: {ex}") from ex - - # If we tried to install firmware that is no longer compatible with the device, - # bail out - if result == Status.NO_IMAGE_AVAILABLE: - self._attr_latest_version = self._attr_installed_version + await self.entity_data.entity.async_install(version=version, backup=backup) + except ZHAException as exc: + raise HomeAssistantError(exc) from exc + finally: self.async_write_ha_state() - # If the update finished but was not successful, we should also throw an error - if result != Status.SUCCESS: - raise HomeAssistantError(f"Update was not successful: {result}") - - # Clear the state - self._latest_firmware = None - self._attr_in_progress = False - self.async_write_ha_state() - - async def async_added_to_hass(self) -> None: - """Call when entity is added.""" - await super().async_added_to_hass() - - # OTA events are sent by the device - self.zha_device.device.add_listener(self) - self.async_accept_signal( - self._ota_cluster_handler, SIGNAL_ATTR_UPDATED, self.attribute_updated - ) - - async def async_will_remove_from_hass(self) -> None: - """Call when entity will be removed.""" - await super().async_will_remove_from_hass() - self._attr_in_progress = False - async def async_update(self) -> None: """Update the entity.""" await CoordinatorEntity.async_update(self) diff --git a/homeassistant/components/zha/websocket_api.py b/homeassistant/components/zha/websocket_api.py index cb95e930b1a..053a941de8d 100644 --- a/homeassistant/components/zha/websocket_api.py +++ b/homeassistant/components/zha/websocket_api.py @@ -7,28 +7,7 @@ import logging from typing import TYPE_CHECKING, Any, Literal, NamedTuple, cast import voluptuous as vol -import zigpy.backups -from zigpy.config import CONF_DEVICE -from zigpy.config.validators import cv_boolean -from zigpy.types.named import EUI64, KeyData -from zigpy.zcl.clusters.security import IasAce -import zigpy.zdo.types as zdo_types - -from homeassistant.components import websocket_api -from homeassistant.const import ATTR_COMMAND, ATTR_ID, ATTR_NAME -from homeassistant.core import HomeAssistant, ServiceCall, callback -from homeassistant.helpers import entity_registry as er -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.service import async_register_admin_service -from homeassistant.helpers.typing import VolDictType, VolSchemaType - -from .api import ( - async_change_channel, - async_get_active_network_settings, - async_get_radio_type, -) -from .core.const import ( +from zha.application.const import ( ATTR_ARGS, ATTR_ATTRIBUTE, ATTR_CLUSTER_ID, @@ -47,13 +26,51 @@ from .core.const import ( ATTR_WARNING_DEVICE_STROBE, ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE, ATTR_WARNING_DEVICE_STROBE_INTENSITY, - BINDINGS, CLUSTER_COMMAND_SERVER, CLUSTER_COMMANDS_CLIENT, CLUSTER_COMMANDS_SERVER, - CLUSTER_HANDLER_IAS_WD, CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, + WARNING_DEVICE_MODE_EMERGENCY, + WARNING_DEVICE_SOUND_HIGH, + WARNING_DEVICE_SQUAWK_MODE_ARMED, + WARNING_DEVICE_STROBE_HIGH, + WARNING_DEVICE_STROBE_YES, + ZHA_CLUSTER_HANDLER_MSG, +) +from zha.application.gateway import Gateway +from zha.application.helpers import ( + async_is_bindable_target, + convert_install_code, + get_matched_clusters, + qr_to_install_code, +) +from zha.zigbee.cluster_handlers.const import CLUSTER_HANDLER_IAS_WD +from zha.zigbee.device import Device +from zha.zigbee.group import GroupMember +import zigpy.backups +from zigpy.config import CONF_DEVICE +from zigpy.config.validators import cv_boolean +from zigpy.types.named import EUI64, KeyData +from zigpy.zcl.clusters.security import IasAce +import zigpy.zdo.types as zdo_types + +from homeassistant.components import websocket_api +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_COMMAND, ATTR_ID, ATTR_NAME +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.helpers import entity_registry as er +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.service import async_register_admin_service +from homeassistant.helpers.typing import VolDictType, VolSchemaType + +from .api import ( + async_change_channel, + async_get_active_network_settings, + async_get_radio_type, +) +from .const import ( CUSTOM_CONFIGURATION, DOMAIN, EZSP_OVERWRITE_EUI64, @@ -61,33 +78,24 @@ from .core.const import ( GROUP_IDS, GROUP_NAME, MFG_CLUSTER_ID_START, - WARNING_DEVICE_MODE_EMERGENCY, - WARNING_DEVICE_SOUND_HIGH, - WARNING_DEVICE_SQUAWK_MODE_ARMED, - WARNING_DEVICE_STROBE_HIGH, - WARNING_DEVICE_STROBE_YES, ZHA_ALARM_OPTIONS, - ZHA_CLUSTER_HANDLER_MSG, - ZHA_CONFIG_SCHEMAS, + ZHA_OPTIONS, ) -from .core.gateway import EntityReference -from .core.group import GroupMember -from .core.helpers import ( +from .helpers import ( + CONF_ZHA_ALARM_SCHEMA, + CONF_ZHA_OPTIONS_SCHEMA, + EntityReference, + ZHAGatewayProxy, async_cluster_exists, - async_is_bindable_target, cluster_command_schema_to_vol_schema, - convert_install_code, - get_matched_clusters, + get_config_entry, get_zha_gateway, - qr_to_install_code, + get_zha_gateway_proxy, ) if TYPE_CHECKING: from homeassistant.components.websocket_api.connection import ActiveConnection - from .core.device import ZHADevice - from .core.gateway import ZHAGateway - _LOGGER = logging.getLogger(__name__) TYPE = "type" @@ -105,6 +113,8 @@ ATTR_SOURCE_IEEE = "source_ieee" ATTR_TARGET_IEEE = "target_ieee" ATTR_QR_CODE = "qr_code" +BINDINGS = "bindings" + SERVICE_PERMIT = "permit" SERVICE_REMOVE = "remove" SERVICE_SET_ZIGBEE_CLUSTER_ATTRIBUTE = "set_zigbee_cluster_attribute" @@ -234,6 +244,12 @@ SERVICE_SCHEMAS: dict[str, VolSchemaType] = { } +ZHA_CONFIG_SCHEMAS = { + ZHA_OPTIONS: CONF_ZHA_OPTIONS_SCHEMA, + ZHA_ALARM_OPTIONS: CONF_ZHA_ALARM_SCHEMA, +} + + class ClusterBinding(NamedTuple): """Describes a cluster binding.""" @@ -306,7 +322,7 @@ async def websocket_permit_devices( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Permit ZHA zigbee devices.""" - zha_gateway = get_zha_gateway(hass) + zha_gateway_proxy = get_zha_gateway_proxy(hass) duration: int = msg[ATTR_DURATION] ieee: EUI64 | None = msg.get(ATTR_IEEE) @@ -321,28 +337,30 @@ async def websocket_permit_devices( @callback def async_cleanup() -> None: """Remove signal listener and turn off debug mode.""" - zha_gateway.async_disable_debug_mode() + zha_gateway_proxy.async_disable_debug_mode() remove_dispatcher_function() connection.subscriptions[msg["id"]] = async_cleanup - zha_gateway.async_enable_debug_mode() + zha_gateway_proxy.async_enable_debug_mode() src_ieee: EUI64 link_key: KeyData if ATTR_SOURCE_IEEE in msg: src_ieee = msg[ATTR_SOURCE_IEEE] link_key = msg[ATTR_INSTALL_CODE] _LOGGER.debug("Allowing join for %s device with link key", src_ieee) - await zha_gateway.application_controller.permit_with_link_key( + await zha_gateway_proxy.gateway.application_controller.permit_with_link_key( time_s=duration, node=src_ieee, link_key=link_key ) elif ATTR_QR_CODE in msg: src_ieee, link_key = msg[ATTR_QR_CODE] _LOGGER.debug("Allowing join for %s device with link key", src_ieee) - await zha_gateway.application_controller.permit_with_link_key( + await zha_gateway_proxy.gateway.application_controller.permit_with_link_key( time_s=duration, node=src_ieee, link_key=link_key ) else: - await zha_gateway.application_controller.permit(time_s=duration, node=ieee) + await zha_gateway_proxy.gateway.application_controller.permit( + time_s=duration, node=ieee + ) connection.send_result(msg[ID]) @@ -353,26 +371,26 @@ async def websocket_get_devices( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Get ZHA devices.""" - zha_gateway = get_zha_gateway(hass) - devices = [device.zha_device_info for device in zha_gateway.devices.values()] + zha_gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass) + devices = [ + device.zha_device_info for device in zha_gateway_proxy.device_proxies.values() + ] connection.send_result(msg[ID], devices) @callback -def _get_entity_name( - zha_gateway: ZHAGateway, entity_ref: EntityReference -) -> str | None: +def _get_entity_name(zha_gateway: Gateway, entity_ref: EntityReference) -> str | None: entity_registry = er.async_get(zha_gateway.hass) - entry = entity_registry.async_get(entity_ref.reference_id) + entry = entity_registry.async_get(entity_ref.ha_entity_id) return entry.name if entry else None @callback def _get_entity_original_name( - zha_gateway: ZHAGateway, entity_ref: EntityReference + zha_gateway: Gateway, entity_ref: EntityReference ) -> str | None: entity_registry = er.async_get(zha_gateway.hass) - entry = entity_registry.async_get(entity_ref.reference_id) + entry = entity_registry.async_get(entity_ref.ha_entity_id) return entry.original_name if entry else None @@ -383,32 +401,36 @@ async def websocket_get_groupable_devices( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Get ZHA devices that can be grouped.""" - zha_gateway = get_zha_gateway(hass) + zha_gateway_proxy = get_zha_gateway_proxy(hass) - devices = [device for device in zha_gateway.devices.values() if device.is_groupable] + devices = [ + device + for device in zha_gateway_proxy.device_proxies.values() + if device.device.is_groupable + ] groupable_devices: list[dict[str, Any]] = [] for device in devices: - entity_refs = zha_gateway.device_registry[device.ieee] + entity_refs = zha_gateway_proxy.ha_entity_refs[device.device.ieee] groupable_devices.extend( { "endpoint_id": ep_id, "entities": [ { - "name": _get_entity_name(zha_gateway, entity_ref), + "name": _get_entity_name(zha_gateway_proxy, entity_ref), "original_name": _get_entity_original_name( - zha_gateway, entity_ref + zha_gateway_proxy, entity_ref ), } for entity_ref in entity_refs - if list(entity_ref.cluster_handlers.values())[ + if list(entity_ref.entity_data.entity.cluster_handlers.values())[ 0 ].cluster.endpoint.endpoint_id == ep_id ], "device": device.zha_device_info, } - for ep_id in device.async_get_groupable_endpoints() + for ep_id in device.device.async_get_groupable_endpoints() ) connection.send_result(msg[ID], groupable_devices) @@ -421,8 +443,8 @@ async def websocket_get_groups( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Get ZHA groups.""" - zha_gateway = get_zha_gateway(hass) - groups = [group.group_info for group in zha_gateway.groups.values()] + zha_gateway_proxy = get_zha_gateway_proxy(hass) + groups = [group.group_info for group in zha_gateway_proxy.group_proxies.values()] connection.send_result(msg[ID], groups) @@ -438,10 +460,10 @@ async def websocket_get_device( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Get ZHA devices.""" - zha_gateway = get_zha_gateway(hass) + zha_gateway_proxy = get_zha_gateway_proxy(hass) ieee: EUI64 = msg[ATTR_IEEE] - if not (zha_device := zha_gateway.devices.get(ieee)): + if not (zha_device := zha_gateway_proxy.device_proxies.get(ieee)): connection.send_message( websocket_api.error_message( msg[ID], websocket_api.ERR_NOT_FOUND, "ZHA Device not found" @@ -465,10 +487,10 @@ async def websocket_get_group( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Get ZHA group.""" - zha_gateway = get_zha_gateway(hass) + zha_gateway_proxy = get_zha_gateway_proxy(hass) group_id: int = msg[GROUP_ID] - if not (zha_group := zha_gateway.groups.get(group_id)): + if not (zha_group := zha_gateway_proxy.group_proxies.get(group_id)): connection.send_message( websocket_api.error_message( msg[ID], websocket_api.ERR_NOT_FOUND, "ZHA Group not found" @@ -494,13 +516,17 @@ async def websocket_add_group( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Add a new ZHA group.""" - zha_gateway = get_zha_gateway(hass) + zha_gateway = get_zha_gateway_proxy(hass) group_name: str = msg[GROUP_NAME] group_id: int | None = msg.get(GROUP_ID) members: list[GroupMember] | None = msg.get(ATTR_MEMBERS) - group = await zha_gateway.async_create_zigpy_group(group_name, members, group_id) + group = await zha_gateway.gateway.async_create_zigpy_group( + group_name, members, group_id + ) assert group - connection.send_result(msg[ID], group.group_info) + connection.send_result( + msg[ID], zha_gateway.group_proxies[group.group_id].group_info + ) @websocket_api.require_admin @@ -515,17 +541,18 @@ async def websocket_remove_groups( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Remove the specified ZHA groups.""" - zha_gateway = get_zha_gateway(hass) + zha_gateway = get_zha_gateway_proxy(hass) group_ids: list[int] = msg[GROUP_IDS] if len(group_ids) > 1: tasks = [ - zha_gateway.async_remove_zigpy_group(group_id) for group_id in group_ids + zha_gateway.gateway.async_remove_zigpy_group(group_id) + for group_id in group_ids ] await asyncio.gather(*tasks) else: - await zha_gateway.async_remove_zigpy_group(group_ids[0]) - ret_groups = [group.group_info for group in zha_gateway.groups.values()] + await zha_gateway.gateway.async_remove_zigpy_group(group_ids[0]) + ret_groups = [group.group_info for group in zha_gateway.group_proxies.values()] connection.send_result(msg[ID], ret_groups) @@ -603,7 +630,7 @@ async def websocket_reconfigure_node( """Reconfigure a ZHA nodes entities by its ieee address.""" zha_gateway = get_zha_gateway(hass) ieee: EUI64 = msg[ATTR_IEEE] - device: ZHADevice | None = zha_gateway.get_device(ieee) + device: Device | None = zha_gateway.get_device(ieee) async def forward_messages(data): """Forward events to websocket.""" @@ -865,14 +892,15 @@ async def websocket_get_bindable_devices( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Directly bind devices.""" - zha_gateway = get_zha_gateway(hass) + zha_gateway_proxy = get_zha_gateway_proxy(hass) source_ieee: EUI64 = msg[ATTR_IEEE] - source_device = zha_gateway.get_device(source_ieee) + source_device = zha_gateway_proxy.device_proxies.get(source_ieee) + assert source_device is not None devices = [ device.zha_device_info - for device in zha_gateway.devices.values() - if async_is_bindable_target(source_device, device) + for device in zha_gateway_proxy.device_proxies.values() + if async_is_bindable_target(source_device.device, device.device) ] _LOGGER.debug( @@ -993,7 +1021,7 @@ async def websocket_unbind_group( async def async_binding_operation( - zha_gateway: ZHAGateway, + zha_gateway: Gateway, source_ieee: EUI64, target_ieee: EUI64, operation: zdo_types.ZDOCmd, @@ -1047,7 +1075,7 @@ async def websocket_get_configuration( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Get ZHA configuration.""" - zha_gateway = get_zha_gateway(hass) + config_entry: ConfigEntry = get_config_entry(hass) import voluptuous_serialize # pylint: disable=import-outside-toplevel def custom_serializer(schema: Any) -> Any: @@ -1070,9 +1098,9 @@ async def websocket_get_configuration( data["schemas"][section] = voluptuous_serialize.convert( schema, custom_serializer=custom_serializer ) - data["data"][section] = zha_gateway.config_entry.options.get( - CUSTOM_CONFIGURATION, {} - ).get(section, {}) + data["data"][section] = config_entry.options.get(CUSTOM_CONFIGURATION, {}).get( + section, {} + ) # send default values for unconfigured options for entry in data["schemas"][section]: @@ -1094,8 +1122,8 @@ async def websocket_update_zha_configuration( hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] ) -> None: """Update the ZHA configuration.""" - zha_gateway = get_zha_gateway(hass) - options = zha_gateway.config_entry.options + config_entry: ConfigEntry = get_config_entry(hass) + options = config_entry.options data_to_save = {**options, CUSTOM_CONFIGURATION: msg["data"]} for section, schema in ZHA_CONFIG_SCHEMAS.items(): @@ -1126,10 +1154,8 @@ async def websocket_update_zha_configuration( data_to_save, ) - hass.config_entries.async_update_entry( - zha_gateway.config_entry, options=data_to_save - ) - status = await hass.config_entries.async_reload(zha_gateway.config_entry.entry_id) + hass.config_entries.async_update_entry(config_entry, options=data_to_save) + status = await hass.config_entries.async_reload(config_entry.entry_id) connection.send_result(msg[ID], status) @@ -1142,10 +1168,11 @@ async def websocket_get_network_settings( """Get ZHA network settings.""" backup = async_get_active_network_settings(hass) zha_gateway = get_zha_gateway(hass) + config_entry: ConfigEntry = get_config_entry(hass) connection.send_result( msg[ID], { - "radio_type": async_get_radio_type(hass, zha_gateway.config_entry).name, + "radio_type": async_get_radio_type(hass, config_entry).name, "device": zha_gateway.application_controller.config[CONF_DEVICE], "settings": backup.as_dict(), }, @@ -1280,7 +1307,7 @@ def async_load_api(hass: HomeAssistant) -> None: """Remove a node from the network.""" zha_gateway = get_zha_gateway(hass) ieee: EUI64 = service.data[ATTR_IEEE] - zha_device: ZHADevice | None = zha_gateway.get_device(ieee) + zha_device: Device | None = zha_gateway.get_device(ieee) if zha_device is not None and zha_device.is_active_coordinator: _LOGGER.info("Removing the coordinator (%s) is not allowed", ieee) return diff --git a/requirements_all.txt b/requirements_all.txt index 3aa5226f48b..dba453c6b30 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -555,9 +555,6 @@ beautifulsoup4==4.12.3 # homeassistant.components.beewi_smartclim # beewi-smartclim==0.0.10 -# homeassistant.components.zha -bellows==0.39.1 - # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.15.3 @@ -2151,13 +2148,11 @@ pyschlage==2024.6.0 pysensibo==1.0.36 # homeassistant.components.serial -# homeassistant.components.zha pyserial-asyncio-fast==0.11 # homeassistant.components.acer_projector # homeassistant.components.crownstone # homeassistant.components.usb -# homeassistant.components.zha # homeassistant.components.zwave_js pyserial==3.5 @@ -2973,7 +2968,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.117 +zha==0.0.18 # homeassistant.components.zhong_hong zhong-hong-hvac==1.0.12 @@ -2981,21 +2976,6 @@ zhong-hong-hvac==1.0.12 # homeassistant.components.ziggo_mediabox_xl ziggo-mediabox-xl==1.1.0 -# homeassistant.components.zha -zigpy-deconz==0.23.2 - -# homeassistant.components.zha -zigpy-xbee==0.20.1 - -# homeassistant.components.zha -zigpy-zigate==0.12.1 - -# homeassistant.components.zha -zigpy-znp==0.12.2 - -# homeassistant.components.zha -zigpy==0.64.1 - # homeassistant.components.zoneminder zm-py==0.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f3cd59ae03a..d39cc69e841 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -480,9 +480,6 @@ base36==0.1.1 # homeassistant.components.scrape beautifulsoup4==4.12.3 -# homeassistant.components.zha -bellows==0.39.1 - # homeassistant.components.bmw_connected_drive bimmer-connected[china]==0.15.3 @@ -1692,14 +1689,9 @@ pyschlage==2024.6.0 # homeassistant.components.sensibo pysensibo==1.0.36 -# homeassistant.components.serial -# homeassistant.components.zha -pyserial-asyncio-fast==0.11 - # homeassistant.components.acer_projector # homeassistant.components.crownstone # homeassistant.components.usb -# homeassistant.components.zha # homeassistant.components.zwave_js pyserial==3.5 @@ -2326,22 +2318,7 @@ zeroconf==0.132.2 zeversolar==0.3.1 # homeassistant.components.zha -zha-quirks==0.0.117 - -# homeassistant.components.zha -zigpy-deconz==0.23.2 - -# homeassistant.components.zha -zigpy-xbee==0.20.1 - -# homeassistant.components.zha -zigpy-zigate==0.12.1 - -# homeassistant.components.zha -zigpy-znp==0.12.2 - -# homeassistant.components.zha -zigpy==0.64.1 +zha==0.0.18 # homeassistant.components.zwave_js zwave-js-server-python==0.57.0 diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index a8bec33a23a..2958c92c81f 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -1,19 +1,11 @@ """Common test objects.""" -import asyncio from datetime import timedelta -import math -from typing import Any -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, Mock import zigpy.zcl import zigpy.zcl.foundation as zcl_f -import homeassistant.components.zha.core.const as zha_const -from homeassistant.components.zha.core.helpers import ( - async_get_zha_config_value, - get_zha_gateway, -) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er import homeassistant.util.dt as dt_util @@ -98,7 +90,7 @@ def make_attribute(attrid, value, status=0): return attr -def send_attribute_report(hass, cluster, attrid, value): +def send_attribute_report(hass: HomeAssistant, cluster, attrid, value): """Send a single attribute report.""" return send_attributes_report(hass, cluster, {attrid: value}) @@ -131,7 +123,7 @@ async def send_attributes_report( await hass.async_block_till_done() -def find_entity_id(domain, zha_device, hass, qualifier=None): +def find_entity_id(domain, zha_device, hass: HomeAssistant, qualifier=None): """Find the entity id under the testing. This is used to get the entity id in order to get the state from the state @@ -148,7 +140,7 @@ def find_entity_id(domain, zha_device, hass, qualifier=None): return entities[0] -def find_entity_ids(domain, zha_device, hass): +def find_entity_ids(domain, zha_device, hass: HomeAssistant): """Find the entity ids under the testing. This is used to get the entity id in order to get the state from the state @@ -163,7 +155,7 @@ def find_entity_ids(domain, zha_device, hass): ] -def async_find_group_entity_id(hass, domain, group): +def async_find_group_entity_id(hass: HomeAssistant, domain, group): """Find the group entity id under test.""" entity_id = f"{domain}.coordinator_manufacturer_coordinator_model_{group.name.lower().replace(' ', '_')}" @@ -172,13 +164,6 @@ def async_find_group_entity_id(hass, domain, group): return entity_id -async def async_enable_traffic(hass, zha_devices, enabled=True): - """Allow traffic to flow through the gateway and the ZHA device.""" - for zha_device in zha_devices: - zha_device.update_available(enabled) - await hass.async_block_till_done() - - def make_zcl_header( command_id: int, global_command: bool = True, tsn: int = 1 ) -> zcl_f.ZCLHeader: @@ -199,57 +184,8 @@ def reset_clusters(clusters): cluster.write_attributes.reset_mock() -async def async_test_rejoin(hass, zigpy_device, clusters, report_counts, ep_id=1): - """Test device rejoins.""" - reset_clusters(clusters) - - zha_gateway = get_zha_gateway(hass) - await zha_gateway.async_device_initialized(zigpy_device) - await hass.async_block_till_done() - for cluster, reports in zip(clusters, report_counts, strict=False): - assert cluster.bind.call_count == 1 - assert cluster.bind.await_count == 1 - if reports: - assert cluster.configure_reporting.call_count == 0 - assert cluster.configure_reporting.await_count == 0 - assert cluster.configure_reporting_multiple.call_count == math.ceil( - reports / zha_const.REPORT_CONFIG_ATTR_PER_REQ - ) - assert cluster.configure_reporting_multiple.await_count == math.ceil( - reports / zha_const.REPORT_CONFIG_ATTR_PER_REQ - ) - else: - # no reports at all - assert cluster.configure_reporting.call_count == reports - assert cluster.configure_reporting.await_count == reports - assert cluster.configure_reporting_multiple.call_count == reports - assert cluster.configure_reporting_multiple.await_count == reports - - -async def async_wait_for_updates(hass): - """Wait until all scheduled updates are executed.""" - await hass.async_block_till_done() - await asyncio.sleep(0) - await asyncio.sleep(0) - await hass.async_block_till_done() - - -async def async_shift_time(hass): +async def async_shift_time(hass: HomeAssistant): """Shift time to cause call later tasks to run.""" next_update = dt_util.utcnow() + timedelta(seconds=11) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() - - -def patch_zha_config(component: str, overrides: dict[tuple[str, str], Any]): - """Patch the ZHA custom configuration defaults.""" - - def new_get_config(config_entry, section, config_key, default): - if (section, config_key) in overrides: - return overrides[section, config_key] - return async_get_zha_config_value(config_entry, section, config_key, default) - - return patch( - f"homeassistant.components.zha.{component}.async_get_zha_config_value", - side_effect=new_get_config, - ) diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 5658e995a11..9b1ec7b33bf 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -1,6 +1,6 @@ """Test configuration for the ZHA component.""" -from collections.abc import Callable, Generator +from collections.abc import Generator import itertools import time from typing import Any @@ -24,14 +24,9 @@ from zigpy.zcl.clusters.general import Basic, Groups from zigpy.zcl.foundation import Status import zigpy.zdo.types as zdo_t -import homeassistant.components.zha.core.const as zha_const -import homeassistant.components.zha.core.device as zha_core_device -from homeassistant.components.zha.core.gateway import ZHAGateway -from homeassistant.components.zha.core.helpers import get_zha_gateway +import homeassistant.components.zha.const as zha_const from homeassistant.core import HomeAssistant -from homeassistant.helpers import restore_state from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util from .common import patch_cluster as common_patch_cluster @@ -43,17 +38,6 @@ FIXTURE_GRP_NAME = "fixture group" COUNTER_NAMES = ["counter_1", "counter_2", "counter_3"] -@pytest.fixture(scope="module", autouse=True) -def disable_request_retry_delay(): - """Disable ZHA request retrying delay to speed up failures.""" - - with patch( - "homeassistant.components.zha.core.cluster_handlers.RETRYABLE_REQUEST_DECORATOR", - zigpy.util.retryable_request(tries=3, delay=0), - ): - yield - - @pytest.fixture(scope="module", autouse=True) def globally_load_quirks(): """Load quirks automatically so that ZHA tests run deterministically in isolation. @@ -127,6 +111,9 @@ class _FakeApp(ControllerApplication): ) -> None: pass + def _persist_coordinator_model_strings_in_db(self) -> None: + pass + def _wrap_mock_instance(obj: Any) -> MagicMock: """Auto-mock every attribute and method in an object.""" @@ -201,10 +188,14 @@ async def zigpy_app_controller(): async def config_entry_fixture() -> MockConfigEntry: """Fixture representing a config entry.""" return MockConfigEntry( - version=3, + version=4, domain=zha_const.DOMAIN, data={ - zigpy.config.CONF_DEVICE: {zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB0"}, + zigpy.config.CONF_DEVICE: { + zigpy.config.CONF_DEVICE_PATH: "/dev/ttyUSB0", + zigpy.config.CONF_DEVICE_BAUDRATE: 115200, + zigpy.config.CONF_DEVICE_FLOW_CONTROL: "hardware", + }, zha_const.CONF_RADIO_TYPE: "ezsp", }, options={ @@ -279,170 +270,6 @@ def cluster_handler(): return cluster_handler -@pytest.fixture -def zigpy_device_mock(zigpy_app_controller): - """Make a fake device using the specified cluster classes.""" - - def _mock_dev( - endpoints, - ieee="00:0d:6f:00:0a:90:69:e7", - manufacturer="FakeManufacturer", - model="FakeModel", - node_descriptor=b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00", - nwk=0xB79C, - patch_cluster=True, - quirk=None, - attributes=None, - ): - """Make a fake device using the specified cluster classes.""" - device = zigpy.device.Device( - zigpy_app_controller, zigpy.types.EUI64.convert(ieee), nwk - ) - device.manufacturer = manufacturer - device.model = model - device.node_desc = zdo_t.NodeDescriptor.deserialize(node_descriptor)[0] - device.last_seen = time.time() - - for epid, ep in endpoints.items(): - endpoint = device.add_endpoint(epid) - endpoint.device_type = ep[SIG_EP_TYPE] - endpoint.profile_id = ep.get(SIG_EP_PROFILE, 0x0104) - endpoint.request = AsyncMock() - - for cluster_id in ep.get(SIG_EP_INPUT, []): - endpoint.add_input_cluster(cluster_id) - - for cluster_id in ep.get(SIG_EP_OUTPUT, []): - endpoint.add_output_cluster(cluster_id) - - device.status = zigpy.device.Status.ENDPOINTS_INIT - - if quirk: - device = quirk(zigpy_app_controller, device.ieee, device.nwk, device) - else: - # Allow zigpy to apply quirks if we don't pass one explicitly - device = zigpy.quirks.get_device(device) - - if patch_cluster: - for endpoint in (ep for epid, ep in device.endpoints.items() if epid): - endpoint.request = AsyncMock(return_value=[0]) - for cluster in itertools.chain( - endpoint.in_clusters.values(), endpoint.out_clusters.values() - ): - common_patch_cluster(cluster) - - if attributes is not None: - for ep_id, clusters in attributes.items(): - for cluster_name, attrs in clusters.items(): - cluster = getattr(device.endpoints[ep_id], cluster_name) - - for name, value in attrs.items(): - attr_id = cluster.find_attribute(name).id - cluster._attr_cache[attr_id] = value - - return device - - return _mock_dev - - -@patch("homeassistant.components.zha.setup_quirks", MagicMock(return_value=True)) -@pytest.fixture -def zha_device_joined(hass, setup_zha): - """Return a newly joined ZHA device.""" - setup_zha_fixture = setup_zha - - async def _zha_device(zigpy_dev, *, setup_zha: bool = True): - zigpy_dev.last_seen = time.time() - - if setup_zha: - await setup_zha_fixture() - - zha_gateway = get_zha_gateway(hass) - zha_gateway.application_controller.devices[zigpy_dev.ieee] = zigpy_dev - await zha_gateway.async_device_initialized(zigpy_dev) - await hass.async_block_till_done() - return zha_gateway.get_device(zigpy_dev.ieee) - - return _zha_device - - -@patch("homeassistant.components.zha.setup_quirks", MagicMock(return_value=True)) -@pytest.fixture -def zha_device_restored(hass, zigpy_app_controller, setup_zha): - """Return a restored ZHA device.""" - setup_zha_fixture = setup_zha - - async def _zha_device(zigpy_dev, *, last_seen=None, setup_zha: bool = True): - zigpy_app_controller.devices[zigpy_dev.ieee] = zigpy_dev - - if last_seen is not None: - zigpy_dev.last_seen = last_seen - - if setup_zha: - await setup_zha_fixture() - - zha_gateway = get_zha_gateway(hass) - return zha_gateway.get_device(zigpy_dev.ieee) - - return _zha_device - - -@pytest.fixture(params=["zha_device_joined", "zha_device_restored"]) -def zha_device_joined_restored(request: pytest.FixtureRequest): - """Join or restore ZHA device.""" - named_method = request.getfixturevalue(request.param) - named_method.name = request.param - return named_method - - -@pytest.fixture -def zha_device_mock( - hass: HomeAssistant, config_entry, zigpy_device_mock -) -> Callable[..., zha_core_device.ZHADevice]: - """Return a ZHA Device factory.""" - - def _zha_device( - endpoints=None, - ieee="00:11:22:33:44:55:66:77", - manufacturer="mock manufacturer", - model="mock model", - node_desc=b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00", - patch_cluster=True, - ) -> zha_core_device.ZHADevice: - if endpoints is None: - endpoints = { - 1: { - "in_clusters": [0, 1, 8, 768], - "out_clusters": [0x19], - "device_type": 0x0105, - }, - 2: { - "in_clusters": [0], - "out_clusters": [6, 8, 0x19, 768], - "device_type": 0x0810, - }, - } - zigpy_device = zigpy_device_mock( - endpoints, ieee, manufacturer, model, node_desc, patch_cluster=patch_cluster - ) - return zha_core_device.ZHADevice( - hass, - zigpy_device, - ZHAGateway(hass, {}, config_entry), - ) - - return _zha_device - - -@pytest.fixture -def hass_disable_services(hass): - """Mock services.""" - with patch.object( - hass, "services", MagicMock(has_service=MagicMock(return_value=True)) - ): - yield hass - - @pytest.fixture(autouse=True) def speed_up_radio_mgr(): """Speed up the radio manager connection time by removing delays.""" @@ -522,31 +349,66 @@ def network_backup() -> zigpy.backups.NetworkBackup: @pytest.fixture -def core_rs(hass_storage: dict[str, Any]) -> Callable[[str, Any, dict[str, Any]], None]: - """Core.restore_state fixture.""" +def zigpy_device_mock(zigpy_app_controller): + """Make a fake device using the specified cluster classes.""" - def _storage(entity_id: str, state: str, attributes: dict[str, Any]) -> None: - now = dt_util.utcnow().isoformat() + def _mock_dev( + endpoints, + ieee="00:0d:6f:00:0a:90:69:e7", + manufacturer="FakeManufacturer", + model="FakeModel", + node_descriptor=b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00", + nwk=0xB79C, + patch_cluster=True, + quirk=None, + attributes=None, + ): + """Make a fake device using the specified cluster classes.""" + device = zigpy.device.Device( + zigpy_app_controller, zigpy.types.EUI64.convert(ieee), nwk + ) + device.manufacturer = manufacturer + device.model = model + device.node_desc = zdo_t.NodeDescriptor.deserialize(node_descriptor)[0] + device.last_seen = time.time() - hass_storage[restore_state.STORAGE_KEY] = { - "version": restore_state.STORAGE_VERSION, - "key": restore_state.STORAGE_KEY, - "data": [ - { - "state": { - "entity_id": entity_id, - "state": str(state), - "attributes": attributes, - "last_changed": now, - "last_updated": now, - "context": { - "id": "3c2243ff5f30447eb12e7348cfd5b8ff", - "user_id": None, - }, - }, - "last_seen": now, - } - ], - } + for epid, ep in endpoints.items(): + endpoint = device.add_endpoint(epid) + endpoint.device_type = ep[SIG_EP_TYPE] + endpoint.profile_id = ep.get(SIG_EP_PROFILE, 0x0104) + endpoint.request = AsyncMock() - return _storage + for cluster_id in ep.get(SIG_EP_INPUT, []): + endpoint.add_input_cluster(cluster_id) + + for cluster_id in ep.get(SIG_EP_OUTPUT, []): + endpoint.add_output_cluster(cluster_id) + + device.status = zigpy.device.Status.ENDPOINTS_INIT + + if quirk: + device = quirk(zigpy_app_controller, device.ieee, device.nwk, device) + else: + # Allow zigpy to apply quirks if we don't pass one explicitly + device = zigpy.quirks.get_device(device) + + if patch_cluster: + for endpoint in (ep for epid, ep in device.endpoints.items() if epid): + endpoint.request = AsyncMock(return_value=[0]) + for cluster in itertools.chain( + endpoint.in_clusters.values(), endpoint.out_clusters.values() + ): + common_patch_cluster(cluster) + + if attributes is not None: + for ep_id, clusters in attributes.items(): + for cluster_name, attrs in clusters.items(): + cluster = getattr(device.endpoints[ep_id], cluster_name) + + for name, value in attrs.items(): + attr_id = cluster.find_attribute(name).id + cluster._attr_cache[attr_id] = value + + return device + + return _mock_dev diff --git a/tests/components/zha/test_alarm_control_panel.py b/tests/components/zha/test_alarm_control_panel.py index 8d3bd76ef61..3473a9b00ad 100644 --- a/tests/components/zha/test_alarm_control_panel.py +++ b/tests/components/zha/test_alarm_control_panel.py @@ -4,10 +4,17 @@ from unittest.mock import AsyncMock, call, patch, sentinel import pytest from zigpy.profiles import zha +from zigpy.zcl import Cluster from zigpy.zcl.clusters import security import zigpy.zcl.foundation as zcl_f from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN +from homeassistant.components.zha.helpers import ( + ZHADeviceProxy, + ZHAGatewayProxy, + get_zha_gateway, + get_zha_gateway_proxy, +) from homeassistant.const import ( ATTR_ENTITY_ID, STATE_ALARM_ARMED_AWAY, @@ -15,12 +22,11 @@ from homeassistant.const import ( STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, STATE_ALARM_TRIGGERED, - STATE_UNAVAILABLE, Platform, ) from homeassistant.core import HomeAssistant -from .common import async_enable_traffic, find_entity_id +from .common import find_entity_id from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE @@ -39,44 +45,40 @@ def alarm_control_panel_platform_only(): yield -@pytest.fixture -def zigpy_device(zigpy_device_mock): - """Device tracker zigpy device.""" - endpoints = { - 1: { - SIG_EP_INPUT: [security.IasAce.cluster_id], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.IAS_ANCILLARY_CONTROL, - SIG_EP_PROFILE: zha.PROFILE_ID, - } - } - return zigpy_device_mock( - endpoints, node_descriptor=b"\x02@\x8c\x02\x10RR\x00\x00\x00R\x00\x00" - ) - - @patch( "zigpy.zcl.clusters.security.IasAce.client_command", new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), ) async def test_alarm_control_panel( - hass: HomeAssistant, zha_device_joined_restored, zigpy_device + hass: HomeAssistant, setup_zha, zigpy_device_mock ) -> None: """Test ZHA alarm control panel platform.""" - zha_device = await zha_device_joined_restored(zigpy_device) - cluster = zigpy_device.endpoints.get(1).ias_ace - entity_id = find_entity_id(Platform.ALARM_CONTROL_PANEL, zha_device, hass) + await setup_zha() + gateway = get_zha_gateway(hass) + gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass) + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [security.IasAce.cluster_id], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zha.DeviceType.IAS_ANCILLARY_CONTROL, + SIG_EP_PROFILE: zha.PROFILE_ID, + } + }, + node_descriptor=b"\x02@\x8c\x02\x10RR\x00\x00\x00R\x00\x00", + ) + + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) + + zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee) + entity_id = find_entity_id(Platform.ALARM_CONTROL_PANEL, zha_device_proxy, hass) + cluster = zigpy_device.endpoints[1].ias_ace assert entity_id is not None - assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED - await async_enable_traffic(hass, [zha_device], enabled=False) - # test that the panel was created and that it is unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - - # test that the state has changed from unavailable to STATE_ALARM_DISARMED assert hass.states.get(entity_id).state == STATE_ALARM_DISARMED # arm_away from HA @@ -255,8 +257,30 @@ async def test_alarm_control_panel( # reset the panel await reset_alarm_panel(hass, cluster, entity_id) + await hass.services.async_call( + ALARM_DOMAIN, + "alarm_trigger", + {ATTR_ENTITY_ID: entity_id}, + blocking=True, + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ALARM_TRIGGERED + assert cluster.client_command.call_count == 1 + assert cluster.client_command.await_count == 1 + assert cluster.client_command.call_args == call( + 4, + security.IasAce.PanelStatus.In_Alarm, + 0, + security.IasAce.AudibleNotification.Default_Sound, + security.IasAce.AlarmStatus.Emergency_Panic, + ) -async def reset_alarm_panel(hass, cluster, entity_id): + # reset the panel + await reset_alarm_panel(hass, cluster, entity_id) + cluster.client_command.reset_mock() + + +async def reset_alarm_panel(hass: HomeAssistant, cluster: Cluster, entity_id: str): """Reset the state of the alarm panel.""" cluster.client_command.reset_mock() await hass.services.async_call( diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index ed3394aafba..7aff6d81f5d 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -6,12 +6,12 @@ from typing import TYPE_CHECKING from unittest.mock import AsyncMock, MagicMock, call, patch import pytest +from zha.application.const import RadioType import zigpy.backups import zigpy.state from homeassistant.components.zha import api -from homeassistant.components.zha.core.const import RadioType -from homeassistant.components.zha.core.helpers import get_zha_gateway +from homeassistant.components.zha.helpers import get_zha_gateway_proxy from homeassistant.core import HomeAssistant if TYPE_CHECKING: @@ -41,7 +41,7 @@ async def test_async_get_network_settings_inactive( """Test reading settings with an inactive ZHA installation.""" await setup_zha() - gateway = get_zha_gateway(hass) + gateway = get_zha_gateway_proxy(hass) await hass.config_entries.async_unload(gateway.config_entry.entry_id) backup = zigpy.backups.NetworkBackup() @@ -53,7 +53,7 @@ async def test_async_get_network_settings_inactive( controller.new = AsyncMock(return_value=zigpy_app_controller) with patch.dict( - "homeassistant.components.zha.core.const.RadioType._member_map_", + "homeassistant.components.zha.api.RadioType._member_map_", ezsp=MagicMock(controller=controller, description="EZSP"), ): settings = await api.async_get_network_settings(hass) @@ -68,7 +68,7 @@ async def test_async_get_network_settings_missing( """Test reading settings with an inactive ZHA installation, no valid channel.""" await setup_zha() - gateway = get_zha_gateway(hass) + gateway = get_zha_gateway_proxy(hass) await hass.config_entries.async_unload(gateway.config_entry.entry_id) # Network settings were never loaded for whatever reason diff --git a/tests/components/zha/test_base.py b/tests/components/zha/test_base.py deleted file mode 100644 index 203df2ffda5..00000000000 --- a/tests/components/zha/test_base.py +++ /dev/null @@ -1,19 +0,0 @@ -"""Test ZHA base cluster handlers module.""" - -from homeassistant.components.zha.core.cluster_handlers import parse_and_log_command - -from .test_cluster_handlers import ( # noqa: F401 - endpoint, - poll_control_ch, - zigpy_coordinator_device, -) - - -def test_parse_and_log_command(poll_control_ch) -> None: # noqa: F811 - """Test that `parse_and_log_command` correctly parses a known command.""" - assert parse_and_log_command(poll_control_ch, 0x00, 0x01, []) == "fast_poll_stop" - - -def test_parse_and_log_command_unknown(poll_control_ch) -> None: # noqa: F811 - """Test that `parse_and_log_command` correctly parses an unknown command.""" - assert parse_and_log_command(poll_control_ch, 0x00, 0xAB, []) == "0xAB" diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index 8276223926d..419823b3b52 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -1,54 +1,25 @@ """Test ZHA binary sensor.""" -from collections.abc import Callable -from typing import Any from unittest.mock import patch import pytest -import zigpy.profiles.zha -from zigpy.zcl.clusters import general, measurement, security +from zigpy.profiles import zha +from zigpy.zcl.clusters import general -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform +from homeassistant.components.zha.helpers import ( + ZHADeviceProxy, + ZHAGatewayProxy, + get_zha_gateway, + get_zha_gateway_proxy, +) +from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant -from .common import ( - async_enable_traffic, - async_test_rejoin, - find_entity_id, - send_attributes_report, -) +from .common import find_entity_id, send_attributes_report from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE -from tests.common import async_mock_load_restore_state_from_storage - -DEVICE_IAS = { - 1: { - SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.IAS_ZONE, - SIG_EP_INPUT: [security.IasZone.cluster_id], - SIG_EP_OUTPUT: [], - } -} - - -DEVICE_OCCUPANCY = { - 1: { - SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.OCCUPANCY_SENSOR, - SIG_EP_INPUT: [measurement.OccupancySensing.cluster_id], - SIG_EP_OUTPUT: [], - } -} - - -DEVICE_ONOFF = { - 1: { - SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SENSOR, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [general.OnOff.cluster_id], - } -} +ON = 1 +OFF = 0 @pytest.fixture(autouse=True) @@ -58,121 +29,51 @@ def binary_sensor_platform_only(): "homeassistant.components.zha.PLATFORMS", ( Platform.BINARY_SENSOR, - Platform.DEVICE_TRACKER, - Platform.NUMBER, - Platform.SELECT, + Platform.SENSOR, ), ): yield -async def async_test_binary_sensor_on_off(hass, cluster, entity_id): - """Test getting on and off messages for binary sensors.""" - # binary sensor on - await send_attributes_report(hass, cluster, {1: 0, 0: 1, 2: 2}) - assert hass.states.get(entity_id).state == STATE_ON - - # binary sensor off - await send_attributes_report(hass, cluster, {1: 1, 0: 0, 2: 2}) - assert hass.states.get(entity_id).state == STATE_OFF - - -async def async_test_iaszone_on_off(hass, cluster, entity_id): - """Test getting on and off messages for iaszone binary sensors.""" - # binary sensor on - cluster.listener_event("cluster_command", 1, 0, [1]) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_ON - - # binary sensor off - cluster.listener_event("cluster_command", 1, 0, [0]) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_OFF - - # check that binary sensor remains off when non-alarm bits change - cluster.listener_event("cluster_command", 1, 0, [0b1111111100]) - await hass.async_block_till_done() - assert hass.states.get(entity_id).state == STATE_OFF - - -@pytest.mark.parametrize( - ("device", "on_off_test", "cluster_name", "reporting", "name"), - [ - ( - DEVICE_IAS, - async_test_iaszone_on_off, - "ias_zone", - (0,), - "FakeManufacturer FakeModel IAS zone", - ), - ( - DEVICE_OCCUPANCY, - async_test_binary_sensor_on_off, - "occupancy", - (1,), - "FakeManufacturer FakeModel Occupancy", - ), - ], -) async def test_binary_sensor( hass: HomeAssistant, + setup_zha, zigpy_device_mock, - zha_device_joined_restored, - device, - on_off_test, - cluster_name, - reporting, - name, ) -> None: """Test ZHA binary_sensor platform.""" - zigpy_device = zigpy_device_mock(device) - zha_device = await zha_device_joined_restored(zigpy_device) - entity_id = find_entity_id(Platform.BINARY_SENSOR, zha_device, hass) + await setup_zha() + gateway = get_zha_gateway(hass) + gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass) + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_PROFILE: zha.PROFILE_ID, + SIG_EP_TYPE: zha.DeviceType.ON_OFF_SENSOR, + SIG_EP_INPUT: [general.Basic.cluster_id], + SIG_EP_OUTPUT: [general.OnOff.cluster_id], + } + }, + ieee="01:2d:6f:00:0a:90:69:e8", + ) + cluster = zigpy_device.endpoints[1].out_clusters[general.OnOff.cluster_id] + + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) + + zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee) + entity_id = find_entity_id(Platform.BINARY_SENSOR, zha_device_proxy, hass) assert entity_id is not None - assert hass.states.get(entity_id).name == name - assert hass.states.get(entity_id).state == STATE_OFF - await async_enable_traffic(hass, [zha_device], enabled=False) - # test that the sensors exist and are in the unavailable state - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - await async_enable_traffic(hass, [zha_device]) - - # test that the sensors exist and are in the off state assert hass.states.get(entity_id).state == STATE_OFF - # test getting messages that trigger and reset the sensors - cluster = getattr(zigpy_device.endpoints[1], cluster_name) - await on_off_test(hass, cluster, entity_id) + await send_attributes_report( + hass, cluster, {general.OnOff.AttributeDefs.on_off.id: ON} + ) + assert hass.states.get(entity_id).state == STATE_ON - # test rejoin - await async_test_rejoin(hass, zigpy_device, [cluster], reporting) + await send_attributes_report( + hass, cluster, {general.OnOff.AttributeDefs.on_off.id: OFF} + ) assert hass.states.get(entity_id).state == STATE_OFF - - -@pytest.mark.parametrize( - "restored_state", - [ - STATE_ON, - STATE_OFF, - ], -) -async def test_onoff_binary_sensor_restore_state( - hass: HomeAssistant, - zigpy_device_mock, - core_rs: Callable[[str, Any, dict[str, Any]], None], - zha_device_restored, - restored_state: str, -) -> None: - """Test ZHA OnOff binary_sensor restores last state from HA.""" - - entity_id = "binary_sensor.fakemanufacturer_fakemodel_opening" - core_rs(entity_id, state=restored_state, attributes={}) - await async_mock_load_restore_state_from_storage(hass) - - zigpy_device = zigpy_device_mock(DEVICE_ONOFF) - zha_device = await zha_device_restored(zigpy_device) - entity_id = find_entity_id(Platform.BINARY_SENSOR, zha_device, hass) - - assert entity_id is not None - assert hass.states.get(entity_id).state == restored_state diff --git a/tests/components/zha/test_button.py b/tests/components/zha/test_button.py index fdcc0d7271c..574805db5f6 100644 --- a/tests/components/zha/test_button.py +++ b/tests/components/zha/test_button.py @@ -1,29 +1,21 @@ """Test ZHA button.""" -from typing import Final -from unittest.mock import call, patch +from unittest.mock import patch from freezegun import freeze_time import pytest -from zhaquirks.const import ( - DEVICE_TYPE, - ENDPOINTS, - INPUT_CLUSTERS, - OUTPUT_CLUSTERS, - PROFILE_ID, -) -from zhaquirks.tuya.ts0601_valve import ParksideTuyaValveManufCluster -from zigpy.const import SIG_EP_PROFILE -from zigpy.exceptions import ZigbeeException +from zigpy.const import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE from zigpy.profiles import zha -from zigpy.quirks import CustomCluster, CustomDevice -from zigpy.quirks.v2 import add_to_registry_v2 -import zigpy.types as t -from zigpy.zcl.clusters import general, security -from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster +from zigpy.zcl.clusters import general import zigpy.zcl.foundation as zcl_f from homeassistant.components.button import DOMAIN, SERVICE_PRESS, ButtonDeviceClass +from homeassistant.components.zha.helpers import ( + ZHADeviceProxy, + ZHAGatewayProxy, + get_zha_gateway, + get_zha_gateway_proxy, +) from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, @@ -32,11 +24,9 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from .common import find_entity_id, update_attribute_cache -from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE +from .common import find_entity_id @pytest.fixture(autouse=True) @@ -44,106 +34,53 @@ def button_platform_only(): """Only set up the button and required base platforms to speed up tests.""" with patch( "homeassistant.components.zha.PLATFORMS", - ( - Platform.BINARY_SENSOR, - Platform.BUTTON, - Platform.DEVICE_TRACKER, - Platform.NUMBER, - Platform.SELECT, - Platform.SENSOR, - Platform.SWITCH, - ), + (Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR), ): yield @pytest.fixture -async def contact_sensor( - hass: HomeAssistant, zigpy_device_mock, zha_device_joined_restored -): - """Contact sensor fixture.""" +async def setup_zha_integration(hass: HomeAssistant, setup_zha): + """Set up ZHA component.""" - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [ - general.Basic.cluster_id, - general.Identify.cluster_id, - security.IasZone.cluster_id, - ], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.IAS_ZONE, - SIG_EP_PROFILE: zha.PROFILE_ID, - } - }, - ) - - zha_device = await zha_device_joined_restored(zigpy_device) - return zha_device, zigpy_device.endpoints[1].identify - - -class FrostLockQuirk(CustomDevice): - """Quirk with frost lock attribute.""" - - class TuyaManufCluster(CustomCluster, ManufacturerSpecificCluster): - """Tuya manufacturer specific cluster.""" - - cluster_id = 0xEF00 - ep_attribute = "tuya_manufacturer" - - attributes = {0xEF01: ("frost_lock_reset", t.Bool)} - - replacement = { - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, - INPUT_CLUSTERS: [general.Basic.cluster_id, TuyaManufCluster], - OUTPUT_CLUSTERS: [], - }, - } - } - - -@pytest.fixture -async def tuya_water_valve( - hass: HomeAssistant, zigpy_device_mock, zha_device_joined_restored -): - """Tuya Water Valve fixture.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, - INPUT_CLUSTERS: [ - general.Basic.cluster_id, - general.Identify.cluster_id, - general.Groups.cluster_id, - general.Scenes.cluster_id, - general.OnOff.cluster_id, - ParksideTuyaValveManufCluster.cluster_id, - ], - OUTPUT_CLUSTERS: [general.Time.cluster_id, general.Ota.cluster_id], - }, - }, - manufacturer="_TZE200_htnnfasr", - model="TS0601", - ) - - zha_device = await zha_device_joined_restored(zigpy_device) - return zha_device, zigpy_device.endpoints[1].tuya_manufacturer + # if we call this in the test itself the test hangs forever + await setup_zha() @freeze_time("2021-11-04 17:37:00", tz_offset=-1) async def test_button( - hass: HomeAssistant, entity_registry: er.EntityRegistry, contact_sensor + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + setup_zha_integration, # pylint: disable=unused-argument + zigpy_device_mock, ) -> None: """Test ZHA button platform.""" - zha_device, cluster = contact_sensor - assert cluster is not None - entity_id = find_entity_id(DOMAIN, zha_device, hass) + gateway = get_zha_gateway(hass) + gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass) + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_PROFILE: zha.PROFILE_ID, + SIG_EP_TYPE: zha.DeviceType.ON_OFF_SENSOR, + SIG_EP_INPUT: [ + general.Basic.cluster_id, + general.Identify.cluster_id, + ], + SIG_EP_OUTPUT: [general.OnOff.cluster_id], + } + }, + ieee="01:2d:6f:00:0a:90:69:e8", + ) + cluster = zigpy_device.endpoints[1].identify + + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) + + zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee) + entity_id = find_entity_id(Platform.BUTTON, zha_device_proxy, hass) assert entity_id is not None state = hass.states.get(entity_id) @@ -175,198 +112,3 @@ async def test_button( assert state assert state.state == "2021-11-04T16:37:00+00:00" assert state.attributes[ATTR_DEVICE_CLASS] == ButtonDeviceClass.IDENTIFY - - -async def test_frost_unlock( - hass: HomeAssistant, entity_registry: er.EntityRegistry, tuya_water_valve -) -> None: - """Test custom frost unlock ZHA button.""" - - zha_device, cluster = tuya_water_valve - assert cluster is not None - entity_id = find_entity_id(DOMAIN, zha_device, hass, qualifier="frost_lock_reset") - assert entity_id is not None - - state = hass.states.get(entity_id) - assert state - assert state.state == STATE_UNKNOWN - assert state.attributes[ATTR_DEVICE_CLASS] == ButtonDeviceClass.RESTART - - entry = entity_registry.async_get(entity_id) - assert entry - assert entry.entity_category == EntityCategory.CONFIG - - with patch( - "zigpy.zcl.Cluster.request", - return_value=[0x00, zcl_f.Status.SUCCESS], - ): - await hass.services.async_call( - DOMAIN, - SERVICE_PRESS, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - await hass.async_block_till_done() - assert cluster.write_attributes.mock_calls == [ - call({"frost_lock_reset": 0}, manufacturer=None) - ] - - state = hass.states.get(entity_id) - assert state - assert state.attributes[ATTR_DEVICE_CLASS] == ButtonDeviceClass.RESTART - - cluster.write_attributes.reset_mock() - cluster.write_attributes.side_effect = ZigbeeException - - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - DOMAIN, - SERVICE_PRESS, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - - # There are three retries - assert cluster.write_attributes.mock_calls == [ - call({"frost_lock_reset": 0}, manufacturer=None), - call({"frost_lock_reset": 0}, manufacturer=None), - call({"frost_lock_reset": 0}, manufacturer=None), - ] - - -class FakeManufacturerCluster(CustomCluster, ManufacturerSpecificCluster): - """Fake manufacturer cluster.""" - - cluster_id: Final = 0xFFF3 - ep_attribute: Final = "mfg_identify" - - class AttributeDefs(zcl_f.BaseAttributeDefs): - """Attribute definitions.""" - - feed: Final = zcl_f.ZCLAttributeDef( - id=0x0000, type=t.uint8_t, access="rw", is_manufacturer_specific=True - ) - - class ServerCommandDefs(zcl_f.BaseCommandDefs): - """Server command definitions.""" - - self_test: Final = zcl_f.ZCLCommandDef( - id=0x00, schema={"identify_time": t.uint16_t}, direction=False - ) - - -( - add_to_registry_v2("Fake_Model", "Fake_Manufacturer") - .replaces(FakeManufacturerCluster) - .command_button( - FakeManufacturerCluster.ServerCommandDefs.self_test.name, - FakeManufacturerCluster.cluster_id, - command_args=(5,), - ) - .write_attr_button( - FakeManufacturerCluster.AttributeDefs.feed.name, - 2, - FakeManufacturerCluster.cluster_id, - ) -) - - -@pytest.fixture -async def custom_button_device( - hass: HomeAssistant, zigpy_device_mock, zha_device_joined_restored -): - """Button device fixture for quirks button tests.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [ - general.Basic.cluster_id, - FakeManufacturerCluster.cluster_id, - ], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.REMOTE_CONTROL, - SIG_EP_PROFILE: zha.PROFILE_ID, - } - }, - manufacturer="Fake_Model", - model="Fake_Manufacturer", - ) - - zigpy_device.endpoints[1].mfg_identify.PLUGGED_ATTR_READS = { - FakeManufacturerCluster.AttributeDefs.feed.name: 0, - } - update_attribute_cache(zigpy_device.endpoints[1].mfg_identify) - zha_device = await zha_device_joined_restored(zigpy_device) - return zha_device, zigpy_device.endpoints[1].mfg_identify - - -@freeze_time("2021-11-04 17:37:00", tz_offset=-1) -async def test_quirks_command_button(hass: HomeAssistant, custom_button_device) -> None: - """Test ZHA button platform.""" - - zha_device, cluster = custom_button_device - assert cluster is not None - entity_id = find_entity_id(DOMAIN, zha_device, hass, qualifier="self_test") - assert entity_id is not None - - state = hass.states.get(entity_id) - assert state - assert state.state == STATE_UNKNOWN - - with patch( - "zigpy.zcl.Cluster.request", - return_value=[0x00, zcl_f.Status.SUCCESS], - ): - await hass.services.async_call( - DOMAIN, - SERVICE_PRESS, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - await hass.async_block_till_done() - assert len(cluster.request.mock_calls) == 1 - assert cluster.request.call_args[0][0] is False - assert cluster.request.call_args[0][1] == 0 - assert cluster.request.call_args[0][3] == 5 # duration in seconds - - state = hass.states.get(entity_id) - assert state - assert state.state == "2021-11-04T16:37:00+00:00" - - -@freeze_time("2021-11-04 17:37:00", tz_offset=-1) -async def test_quirks_write_attr_button( - hass: HomeAssistant, custom_button_device -) -> None: - """Test ZHA button platform.""" - - zha_device, cluster = custom_button_device - assert cluster is not None - entity_id = find_entity_id(DOMAIN, zha_device, hass, qualifier="feed") - assert entity_id is not None - - state = hass.states.get(entity_id) - assert state - assert state.state == STATE_UNKNOWN - assert cluster.get(cluster.AttributeDefs.feed.name) == 0 - - with patch( - "zigpy.zcl.Cluster.request", - return_value=[0x00, zcl_f.Status.SUCCESS], - ): - await hass.services.async_call( - DOMAIN, - SERVICE_PRESS, - {ATTR_ENTITY_ID: entity_id}, - blocking=True, - ) - await hass.async_block_till_done() - assert cluster.write_attributes.mock_calls == [ - call({cluster.AttributeDefs.feed.name: 2}, manufacturer=None) - ] - - state = hass.states.get(entity_id) - assert state - assert state.state == "2021-11-04T16:37:00+00:00" - assert cluster.get(cluster.AttributeDefs.feed.name) == 2 diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index 32ef08fcd96..7b94db51d04 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -1,17 +1,17 @@ """Test ZHA climate.""" from typing import Literal -from unittest.mock import call, patch +from unittest.mock import patch import pytest +from zha.application.platforms.climate.const import HVAC_MODE_2_SYSTEM, SEQ_OF_OPERATION import zhaquirks.sinope.thermostat -from zhaquirks.sinope.thermostat import SinopeTechnologiesThermostatCluster import zhaquirks.tuya.ts0601_trv import zigpy.profiles +from zigpy.profiles import zha import zigpy.types import zigpy.zcl.clusters from zigpy.zcl.clusters.hvac import Thermostat -import zigpy.zcl.foundation as zcl_f from homeassistant.components.climate import ( ATTR_CURRENT_TEMPERATURE, @@ -28,10 +28,6 @@ from homeassistant.components.climate import ( FAN_LOW, FAN_ON, PRESET_AWAY, - PRESET_BOOST, - PRESET_COMFORT, - PRESET_ECO, - PRESET_NONE, SERVICE_SET_FAN_MODE, SERVICE_SET_HVAC_MODE, SERVICE_SET_PRESET_MODE, @@ -39,13 +35,11 @@ from homeassistant.components.climate import ( HVACAction, HVACMode, ) -from homeassistant.components.zha.climate import HVAC_MODE_2_SYSTEM, SEQ_OF_OPERATION -from homeassistant.components.zha.core.const import ( - PRESET_COMPLEX, - PRESET_SCHEDULE, - PRESET_TEMP_MANUAL, +from homeassistant.components.zha.helpers import ( + ZHAGatewayProxy, + get_zha_gateway, + get_zha_gateway_proxy, ) -from homeassistant.components.zha.core.device import ZHADevice from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_TEMPERATURE, @@ -53,15 +47,15 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError, ServiceValidationError +from homeassistant.exceptions import ServiceValidationError -from .common import async_enable_traffic, find_entity_id, send_attributes_report +from .common import find_entity_id, send_attributes_report from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE CLIMATE = { 1: { - SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.THERMOSTAT, + SIG_EP_PROFILE: zha.PROFILE_ID, + SIG_EP_TYPE: zha.DeviceType.THERMOSTAT, SIG_EP_INPUT: [ zigpy.zcl.clusters.general.Basic.cluster_id, zigpy.zcl.clusters.general.Identify.cluster_id, @@ -74,8 +68,8 @@ CLIMATE = { CLIMATE_FAN = { 1: { - SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.THERMOSTAT, + SIG_EP_PROFILE: zha.PROFILE_ID, + SIG_EP_TYPE: zha.DeviceType.THERMOSTAT, SIG_EP_INPUT: [ zigpy.zcl.clusters.general.Basic.cluster_id, zigpy.zcl.clusters.general.Identify.cluster_id, @@ -108,72 +102,7 @@ CLIMATE_SINOPE = { }, } -CLIMATE_ZEN = { - 1: { - SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.THERMOSTAT, - SIG_EP_INPUT: [ - zigpy.zcl.clusters.general.Basic.cluster_id, - zigpy.zcl.clusters.general.Identify.cluster_id, - zigpy.zcl.clusters.hvac.Fan.cluster_id, - zigpy.zcl.clusters.hvac.Thermostat.cluster_id, - zigpy.zcl.clusters.hvac.UserInterface.cluster_id, - ], - SIG_EP_OUTPUT: [zigpy.zcl.clusters.general.Ota.cluster_id], - } -} - -CLIMATE_MOES = { - 1: { - SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.THERMOSTAT, - SIG_EP_INPUT: [ - zigpy.zcl.clusters.general.Basic.cluster_id, - zigpy.zcl.clusters.general.Identify.cluster_id, - zigpy.zcl.clusters.hvac.Thermostat.cluster_id, - zigpy.zcl.clusters.hvac.UserInterface.cluster_id, - 61148, - ], - SIG_EP_OUTPUT: [zigpy.zcl.clusters.general.Ota.cluster_id], - } -} - -CLIMATE_BECA = { - 1: { - SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.SMART_PLUG, - SIG_EP_INPUT: [ - zigpy.zcl.clusters.general.Basic.cluster_id, - zigpy.zcl.clusters.general.Groups.cluster_id, - zigpy.zcl.clusters.general.Scenes.cluster_id, - 61148, - ], - SIG_EP_OUTPUT: [ - zigpy.zcl.clusters.general.Time.cluster_id, - zigpy.zcl.clusters.general.Ota.cluster_id, - ], - } -} - -CLIMATE_ZONNSMART = { - 1: { - SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.THERMOSTAT, - SIG_EP_INPUT: [ - zigpy.zcl.clusters.general.Basic.cluster_id, - zigpy.zcl.clusters.hvac.Thermostat.cluster_id, - zigpy.zcl.clusters.hvac.UserInterface.cluster_id, - 61148, - ], - SIG_EP_OUTPUT: [zigpy.zcl.clusters.general.Ota.cluster_id], - } -} - MANUF_SINOPE = "Sinope Technologies" -MANUF_ZEN = "Zen Within" -MANUF_MOES = "_TZE200_ckud7u2l" -MANUF_BECA = "_TZE200_b6wax7g0" -MANUF_ZONNSMART = "_TZE200_hue3yfsn" ZCL_ATTR_PLUG = { "abs_min_heat_setpoint_limit": 800, @@ -218,22 +147,22 @@ def climate_platform_only(): @pytest.fixture -def device_climate_mock(hass, zigpy_device_mock, zha_device_joined): +def device_climate_mock(hass: HomeAssistant, setup_zha, zigpy_device_mock): """Test regular thermostat device.""" async def _dev(clusters, plug=None, manuf=None, quirk=None): - if plug is None: - plugged_attrs = ZCL_ATTR_PLUG - else: - plugged_attrs = {**ZCL_ATTR_PLUG, **plug} - + plugged_attrs = ZCL_ATTR_PLUG if plug is None else {**ZCL_ATTR_PLUG, **plug} zigpy_device = zigpy_device_mock(clusters, manufacturer=manuf, quirk=quirk) zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100 zigpy_device.endpoints[1].thermostat.PLUGGED_ATTR_READS = plugged_attrs - zha_device = await zha_device_joined(zigpy_device) - await async_enable_traffic(hass, [zha_device]) - await hass.async_block_till_done() - return zha_device + + await setup_zha() + gateway = get_zha_gateway(hass) + gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass) + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) + return gateway_proxy.get_device_proxy(zigpy_device.ieee) return _dev @@ -268,44 +197,6 @@ async def device_climate_sinope(device_climate_mock): ) -@pytest.fixture -async def device_climate_zen(device_climate_mock): - """Zen Within thermostat.""" - - return await device_climate_mock(CLIMATE_ZEN, manuf=MANUF_ZEN) - - -@pytest.fixture -async def device_climate_moes(device_climate_mock): - """MOES thermostat.""" - - return await device_climate_mock( - CLIMATE_MOES, manuf=MANUF_MOES, quirk=zhaquirks.tuya.ts0601_trv.MoesHY368_Type1 - ) - - -@pytest.fixture -async def device_climate_beca(device_climate_mock) -> ZHADevice: - """Beca thermostat.""" - - return await device_climate_mock( - CLIMATE_BECA, - manuf=MANUF_BECA, - quirk=zhaquirks.tuya.ts0601_trv.MoesHY368_Type1new, - ) - - -@pytest.fixture -async def device_climate_zonnsmart(device_climate_mock): - """ZONNSMART thermostat.""" - - return await device_climate_mock( - CLIMATE_ZONNSMART, - manuf=MANUF_ZONNSMART, - quirk=zhaquirks.tuya.ts0601_trv.ZonnsmartTV01_ZG, - ) - - def test_sequence_mappings() -> None: """Test correct mapping between control sequence -> HVAC Mode -> Sysmode.""" @@ -318,7 +209,7 @@ def test_sequence_mappings() -> None: async def test_climate_local_temperature(hass: HomeAssistant, device_climate) -> None: """Test local temperature.""" - thrm_cluster = device_climate.device.endpoints[1].thermostat + thrm_cluster = device_climate.device.device.endpoints[1].thermostat entity_id = find_entity_id(Platform.CLIMATE, device_climate, hass) state = hass.states.get(entity_id) @@ -334,7 +225,7 @@ async def test_climate_hvac_action_running_state( ) -> None: """Test hvac action via running state.""" - thrm_cluster = device_climate_sinope.device.endpoints[1].thermostat + thrm_cluster = device_climate_sinope.device.device.endpoints[1].thermostat entity_id = find_entity_id(Platform.CLIMATE, device_climate_sinope, hass) sensor_entity_id = find_entity_id( Platform.SENSOR, device_climate_sinope, hass, "hvac" @@ -394,101 +285,12 @@ async def test_climate_hvac_action_running_state( assert hvac_sensor_state.state == HVACAction.FAN -async def test_climate_hvac_action_running_state_zen( - hass: HomeAssistant, device_climate_zen -) -> None: - """Test Zen hvac action via running state.""" - - thrm_cluster = device_climate_zen.device.endpoints[1].thermostat - entity_id = find_entity_id(Platform.CLIMATE, device_climate_zen, hass) - sensor_entity_id = find_entity_id( - Platform.SENSOR, device_climate_zen, hass, "hvac_action" - ) - - state = hass.states.get(entity_id) - assert ATTR_HVAC_ACTION not in state.attributes - hvac_sensor_state = hass.states.get(sensor_entity_id) - assert hvac_sensor_state.state == "unknown" - - await send_attributes_report( - hass, thrm_cluster, {0x0029: Thermostat.RunningState.Cool_2nd_Stage_On} - ) - state = hass.states.get(entity_id) - assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.COOLING - hvac_sensor_state = hass.states.get(sensor_entity_id) - assert hvac_sensor_state.state == HVACAction.COOLING - - await send_attributes_report( - hass, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_State_On} - ) - state = hass.states.get(entity_id) - assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.FAN - hvac_sensor_state = hass.states.get(sensor_entity_id) - assert hvac_sensor_state.state == HVACAction.FAN - - await send_attributes_report( - hass, thrm_cluster, {0x0029: Thermostat.RunningState.Heat_2nd_Stage_On} - ) - state = hass.states.get(entity_id) - assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.HEATING - hvac_sensor_state = hass.states.get(sensor_entity_id) - assert hvac_sensor_state.state == HVACAction.HEATING - - await send_attributes_report( - hass, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_2nd_Stage_On} - ) - state = hass.states.get(entity_id) - assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.FAN - hvac_sensor_state = hass.states.get(sensor_entity_id) - assert hvac_sensor_state.state == HVACAction.FAN - - await send_attributes_report( - hass, thrm_cluster, {0x0029: Thermostat.RunningState.Cool_State_On} - ) - state = hass.states.get(entity_id) - assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.COOLING - hvac_sensor_state = hass.states.get(sensor_entity_id) - assert hvac_sensor_state.state == HVACAction.COOLING - - await send_attributes_report( - hass, thrm_cluster, {0x0029: Thermostat.RunningState.Fan_3rd_Stage_On} - ) - state = hass.states.get(entity_id) - assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.FAN - hvac_sensor_state = hass.states.get(sensor_entity_id) - assert hvac_sensor_state.state == HVACAction.FAN - - await send_attributes_report( - hass, thrm_cluster, {0x0029: Thermostat.RunningState.Heat_State_On} - ) - state = hass.states.get(entity_id) - assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.HEATING - hvac_sensor_state = hass.states.get(sensor_entity_id) - assert hvac_sensor_state.state == HVACAction.HEATING - - await send_attributes_report( - hass, thrm_cluster, {0x0029: Thermostat.RunningState.Idle} - ) - state = hass.states.get(entity_id) - assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.OFF - hvac_sensor_state = hass.states.get(sensor_entity_id) - assert hvac_sensor_state.state == HVACAction.OFF - - await send_attributes_report( - hass, thrm_cluster, {0x001C: Thermostat.SystemMode.Heat} - ) - state = hass.states.get(entity_id) - assert state.attributes[ATTR_HVAC_ACTION] == HVACAction.IDLE - hvac_sensor_state = hass.states.get(sensor_entity_id) - assert hvac_sensor_state.state == HVACAction.IDLE - - async def test_climate_hvac_action_pi_demand( hass: HomeAssistant, device_climate ) -> None: """Test hvac action based on pi_heating/cooling_demand attrs.""" - thrm_cluster = device_climate.device.endpoints[1].thermostat + thrm_cluster = device_climate.device.device.endpoints[1].thermostat entity_id = find_entity_id(Platform.CLIMATE, device_climate, hass) state = hass.states.get(entity_id) @@ -537,7 +339,7 @@ async def test_hvac_mode( ) -> None: """Test HVAC mode.""" - thrm_cluster = device_climate.device.endpoints[1].thermostat + thrm_cluster = device_climate.device.device.endpoints[1].thermostat entity_id = find_entity_id(Platform.CLIMATE, device_climate, hass) state = hass.states.get(entity_id) @@ -714,7 +516,7 @@ async def test_set_hvac_mode( ) -> None: """Test setting hvac mode.""" - thrm_cluster = device_climate.device.endpoints[1].thermostat + thrm_cluster = device_climate.device.device.endpoints[1].thermostat entity_id = find_entity_id(Platform.CLIMATE, device_climate, hass) state = hass.states.get(entity_id) @@ -753,134 +555,11 @@ async def test_set_hvac_mode( } -async def test_preset_setting(hass: HomeAssistant, device_climate_sinope) -> None: - """Test preset setting.""" - - entity_id = find_entity_id(Platform.CLIMATE, device_climate_sinope, hass) - thrm_cluster = device_climate_sinope.device.endpoints[1].thermostat - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE - - # unsuccessful occupancy change - thrm_cluster.write_attributes.return_value = [ - zcl_f.WriteAttributesResponse( - [ - zcl_f.WriteAttributesStatusRecord( - status=zcl_f.Status.FAILURE, - attrid=SinopeTechnologiesThermostatCluster.AttributeDefs.set_occupancy.id, - ) - ] - ) - ] - - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, - blocking=True, - ) - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE - assert thrm_cluster.write_attributes.call_count == 1 - assert thrm_cluster.write_attributes.call_args[0][0] == {"set_occupancy": 0} - - # successful occupancy change - thrm_cluster.write_attributes.reset_mock() - thrm_cluster.write_attributes.return_value = [ - zcl_f.WriteAttributesResponse( - [zcl_f.WriteAttributesStatusRecord(status=zcl_f.Status.SUCCESS)] - ) - ] - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, - blocking=True, - ) - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == PRESET_AWAY - assert thrm_cluster.write_attributes.call_count == 1 - assert thrm_cluster.write_attributes.call_args[0][0] == {"set_occupancy": 0} - - # unsuccessful occupancy change - thrm_cluster.write_attributes.reset_mock() - thrm_cluster.write_attributes.return_value = [ - zcl_f.WriteAttributesResponse( - [ - zcl_f.WriteAttributesStatusRecord( - status=zcl_f.Status.FAILURE, - attrid=SinopeTechnologiesThermostatCluster.AttributeDefs.set_occupancy.id, - ) - ] - ) - ] - - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, - blocking=True, - ) - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == PRESET_AWAY - assert thrm_cluster.write_attributes.call_count == 1 - assert thrm_cluster.write_attributes.call_args[0][0] == {"set_occupancy": 1} - - # successful occupancy change - thrm_cluster.write_attributes.reset_mock() - thrm_cluster.write_attributes.return_value = [ - zcl_f.WriteAttributesResponse( - [zcl_f.WriteAttributesStatusRecord(status=zcl_f.Status.SUCCESS)] - ) - ] - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, - blocking=True, - ) - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE - assert thrm_cluster.write_attributes.call_count == 1 - assert thrm_cluster.write_attributes.call_args[0][0] == {"set_occupancy": 1} - - -async def test_preset_setting_invalid( - hass: HomeAssistant, device_climate_sinope -) -> None: - """Test invalid preset setting.""" - - entity_id = find_entity_id(Platform.CLIMATE, device_climate_sinope, hass) - thrm_cluster = device_climate_sinope.device.endpoints[1].thermostat - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE - - with pytest.raises(ServiceValidationError): - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "invalid_preset"}, - blocking=True, - ) - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE - assert thrm_cluster.write_attributes.call_count == 0 - - async def test_set_temperature_hvac_mode(hass: HomeAssistant, device_climate) -> None: """Test setting HVAC mode in temperature service call.""" entity_id = find_entity_id(Platform.CLIMATE, device_climate, hass) - thrm_cluster = device_climate.device.endpoints[1].thermostat + thrm_cluster = device_climate.device.device.endpoints[1].thermostat state = hass.states.get(entity_id) assert state.state == HVACMode.OFF @@ -922,7 +601,7 @@ async def test_set_temperature_heat_cool( quirk=zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat, ) entity_id = find_entity_id(Platform.CLIMATE, device_climate, hass) - thrm_cluster = device_climate.device.endpoints[1].thermostat + thrm_cluster = device_climate.device.device.endpoints[1].thermostat state = hass.states.get(entity_id) assert state.state == HVACMode.HEAT_COOL @@ -1008,7 +687,7 @@ async def test_set_temperature_heat(hass: HomeAssistant, device_climate_mock) -> quirk=zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat, ) entity_id = find_entity_id(Platform.CLIMATE, device_climate, hass) - thrm_cluster = device_climate.device.endpoints[1].thermostat + thrm_cluster = device_climate.device.device.endpoints[1].thermostat state = hass.states.get(entity_id) assert state.state == HVACMode.HEAT @@ -1087,7 +766,7 @@ async def test_set_temperature_cool(hass: HomeAssistant, device_climate_mock) -> quirk=zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat, ) entity_id = find_entity_id(Platform.CLIMATE, device_climate, hass) - thrm_cluster = device_climate.device.endpoints[1].thermostat + thrm_cluster = device_climate.device.device.endpoints[1].thermostat state = hass.states.get(entity_id) assert state.state == HVACMode.COOL @@ -1172,7 +851,7 @@ async def test_set_temperature_wrong_mode( manuf=MANUF_SINOPE, ) entity_id = find_entity_id(Platform.CLIMATE, device_climate, hass) - thrm_cluster = device_climate.device.endpoints[1].thermostat + thrm_cluster = device_climate.device.device.endpoints[1].thermostat state = hass.states.get(entity_id) assert state.state == HVACMode.DRY @@ -1191,38 +870,11 @@ async def test_set_temperature_wrong_mode( assert thrm_cluster.write_attributes.await_count == 0 -async def test_occupancy_reset(hass: HomeAssistant, device_climate_sinope) -> None: - """Test away preset reset.""" - - entity_id = find_entity_id(Platform.CLIMATE, device_climate_sinope, hass) - thrm_cluster = device_climate_sinope.device.endpoints[1].thermostat - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE - - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, - blocking=True, - ) - thrm_cluster.write_attributes.reset_mock() - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == PRESET_AWAY - - await send_attributes_report( - hass, thrm_cluster, {"occupied_heating_setpoint": zigpy.types.uint16_t(1950)} - ) - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE - - async def test_fan_mode(hass: HomeAssistant, device_climate_fan) -> None: """Test fan mode.""" entity_id = find_entity_id(Platform.CLIMATE, device_climate_fan, hass) - thrm_cluster = device_climate_fan.device.endpoints[1].thermostat + thrm_cluster = device_climate_fan.device.device.endpoints[1].thermostat state = hass.states.get(entity_id) assert set(state.attributes[ATTR_FAN_MODES]) == {FAN_AUTO, FAN_ON} @@ -1253,7 +905,7 @@ async def test_set_fan_mode_not_supported( """Test fan setting unsupported mode.""" entity_id = find_entity_id(Platform.CLIMATE, device_climate_fan, hass) - fan_cluster = device_climate_fan.device.endpoints[1].fan + fan_cluster = device_climate_fan.device.device.endpoints[1].fan with pytest.raises(ServiceValidationError): await hass.services.async_call( @@ -1269,7 +921,7 @@ async def test_set_fan_mode(hass: HomeAssistant, device_climate_fan) -> None: """Test fan mode setting.""" entity_id = find_entity_id(Platform.CLIMATE, device_climate_fan, hass) - fan_cluster = device_climate_fan.device.endpoints[1].fan + fan_cluster = device_climate_fan.device.device.endpoints[1].fan state = hass.states.get(entity_id) assert state.attributes[ATTR_FAN_MODE] == FAN_AUTO @@ -1292,309 +944,3 @@ async def test_set_fan_mode(hass: HomeAssistant, device_climate_fan) -> None: ) assert fan_cluster.write_attributes.await_count == 1 assert fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 5} - - -async def test_set_moes_preset(hass: HomeAssistant, device_climate_moes) -> None: - """Test setting preset for moes trv.""" - - entity_id = find_entity_id(Platform.CLIMATE, device_climate_moes, hass) - thrm_cluster = device_climate_moes.device.endpoints[1].thermostat - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE - - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_AWAY}, - blocking=True, - ) - - assert thrm_cluster.write_attributes.await_count == 1 - assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { - "operation_preset": 0 - } - - thrm_cluster.write_attributes.reset_mock() - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_SCHEDULE}, - blocking=True, - ) - - assert thrm_cluster.write_attributes.await_count == 2 - assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { - "operation_preset": 2 - } - assert thrm_cluster.write_attributes.call_args_list[1][0][0] == { - "operation_preset": 1 - } - - thrm_cluster.write_attributes.reset_mock() - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_COMFORT}, - blocking=True, - ) - - assert thrm_cluster.write_attributes.await_count == 2 - assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { - "operation_preset": 2 - } - assert thrm_cluster.write_attributes.call_args_list[1][0][0] == { - "operation_preset": 3 - } - - thrm_cluster.write_attributes.reset_mock() - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_ECO}, - blocking=True, - ) - - assert thrm_cluster.write_attributes.await_count == 2 - assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { - "operation_preset": 2 - } - assert thrm_cluster.write_attributes.call_args_list[1][0][0] == { - "operation_preset": 4 - } - - thrm_cluster.write_attributes.reset_mock() - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_BOOST}, - blocking=True, - ) - - assert thrm_cluster.write_attributes.await_count == 2 - assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { - "operation_preset": 2 - } - assert thrm_cluster.write_attributes.call_args_list[1][0][0] == { - "operation_preset": 5 - } - - thrm_cluster.write_attributes.reset_mock() - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_COMPLEX}, - blocking=True, - ) - - assert thrm_cluster.write_attributes.await_count == 2 - assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { - "operation_preset": 2 - } - assert thrm_cluster.write_attributes.call_args_list[1][0][0] == { - "operation_preset": 6 - } - - thrm_cluster.write_attributes.reset_mock() - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, - blocking=True, - ) - - assert thrm_cluster.write_attributes.await_count == 1 - assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { - "operation_preset": 2 - } - - -async def test_set_moes_operation_mode( - hass: HomeAssistant, device_climate_moes -) -> None: - """Test setting preset for moes trv.""" - - entity_id = find_entity_id(Platform.CLIMATE, device_climate_moes, hass) - thrm_cluster = device_climate_moes.device.endpoints[1].thermostat - - await send_attributes_report(hass, thrm_cluster, {"operation_preset": 0}) - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == PRESET_AWAY - - await send_attributes_report(hass, thrm_cluster, {"operation_preset": 1}) - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == PRESET_SCHEDULE - - await send_attributes_report(hass, thrm_cluster, {"operation_preset": 2}) - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE - - await send_attributes_report(hass, thrm_cluster, {"operation_preset": 3}) - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == PRESET_COMFORT - - await send_attributes_report(hass, thrm_cluster, {"operation_preset": 4}) - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == PRESET_ECO - - await send_attributes_report(hass, thrm_cluster, {"operation_preset": 5}) - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == PRESET_BOOST - - await send_attributes_report(hass, thrm_cluster, {"operation_preset": 6}) - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == PRESET_COMPLEX - - -@pytest.mark.parametrize( - ("preset_attr", "preset_mode"), - [ - (0, PRESET_AWAY), - (1, PRESET_SCHEDULE), - # pylint: disable-next=fixme - # (2, PRESET_NONE), # TODO: why does this not work? - (4, PRESET_ECO), - (5, PRESET_BOOST), - (7, PRESET_TEMP_MANUAL), - ], -) -async def test_beca_operation_mode_update( - hass: HomeAssistant, - device_climate_beca: ZHADevice, - preset_attr: int, - preset_mode: str, -) -> None: - """Test beca trv operation mode attribute update.""" - - entity_id = find_entity_id(Platform.CLIMATE, device_climate_beca, hass) - thrm_cluster = device_climate_beca.device.endpoints[1].thermostat - - # Test sending an attribute report - await send_attributes_report(hass, thrm_cluster, {"operation_preset": preset_attr}) - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == preset_mode - - # Test setting the preset - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: preset_mode}, - blocking=True, - ) - - assert thrm_cluster.write_attributes.mock_calls == [ - call( - {"operation_preset": preset_attr}, - manufacturer=device_climate_beca.manufacturer_code, - ) - ] - - -async def test_set_zonnsmart_preset( - hass: HomeAssistant, device_climate_zonnsmart -) -> None: - """Test setting preset from homeassistant for zonnsmart trv.""" - - entity_id = find_entity_id(Platform.CLIMATE, device_climate_zonnsmart, hass) - thrm_cluster = device_climate_zonnsmart.device.endpoints[1].thermostat - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE - - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_SCHEDULE}, - blocking=True, - ) - - assert thrm_cluster.write_attributes.await_count == 1 - assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { - "operation_preset": 0 - } - - thrm_cluster.write_attributes.reset_mock() - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "holiday"}, - blocking=True, - ) - - assert thrm_cluster.write_attributes.await_count == 2 - assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { - "operation_preset": 1 - } - assert thrm_cluster.write_attributes.call_args_list[1][0][0] == { - "operation_preset": 3 - } - - thrm_cluster.write_attributes.reset_mock() - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: "frost protect"}, - blocking=True, - ) - - assert thrm_cluster.write_attributes.await_count == 2 - assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { - "operation_preset": 1 - } - assert thrm_cluster.write_attributes.call_args_list[1][0][0] == { - "operation_preset": 4 - } - - thrm_cluster.write_attributes.reset_mock() - await hass.services.async_call( - CLIMATE_DOMAIN, - SERVICE_SET_PRESET_MODE, - {ATTR_ENTITY_ID: entity_id, ATTR_PRESET_MODE: PRESET_NONE}, - blocking=True, - ) - - assert thrm_cluster.write_attributes.await_count == 1 - assert thrm_cluster.write_attributes.call_args_list[0][0][0] == { - "operation_preset": 1 - } - - -async def test_set_zonnsmart_operation_mode( - hass: HomeAssistant, device_climate_zonnsmart -) -> None: - """Test setting preset from trv for zonnsmart trv.""" - - entity_id = find_entity_id(Platform.CLIMATE, device_climate_zonnsmart, hass) - thrm_cluster = device_climate_zonnsmart.device.endpoints[1].thermostat - - await send_attributes_report(hass, thrm_cluster, {"operation_preset": 0}) - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == PRESET_SCHEDULE - - await send_attributes_report(hass, thrm_cluster, {"operation_preset": 1}) - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE - - await send_attributes_report(hass, thrm_cluster, {"operation_preset": 2}) - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == "holiday" - - await send_attributes_report(hass, thrm_cluster, {"operation_preset": 3}) - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == "holiday" - - await send_attributes_report(hass, thrm_cluster, {"operation_preset": 4}) - - state = hass.states.get(entity_id) - assert state.attributes[ATTR_PRESET_MODE] == "frost protect" diff --git a/tests/components/zha/test_cluster_handlers.py b/tests/components/zha/test_cluster_handlers.py deleted file mode 100644 index 655a36a2492..00000000000 --- a/tests/components/zha/test_cluster_handlers.py +++ /dev/null @@ -1,1009 +0,0 @@ -"""Test ZHA Core cluster handlers.""" - -from collections.abc import Callable -import logging -import math -import threading -from types import NoneType -from unittest import mock -from unittest.mock import AsyncMock, patch - -import pytest -import zigpy.device -import zigpy.endpoint -from zigpy.endpoint import Endpoint as ZigpyEndpoint -import zigpy.profiles.zha -import zigpy.quirks as zigpy_quirks -import zigpy.types as t -from zigpy.zcl import foundation -import zigpy.zcl.clusters -from zigpy.zcl.clusters import CLUSTERS_BY_ID -import zigpy.zdo.types as zdo_t - -from homeassistant.components.zha.core import cluster_handlers, registries -from homeassistant.components.zha.core.cluster_handlers.lighting import ( - ColorClusterHandler, -) -import homeassistant.components.zha.core.const as zha_const -from homeassistant.components.zha.core.device import ZHADevice -from homeassistant.components.zha.core.endpoint import Endpoint -from homeassistant.components.zha.core.helpers import get_zha_gateway -from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError - -from .common import make_zcl_header -from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE - -from tests.common import async_capture_events - - -@pytest.fixture(autouse=True) -def disable_platform_only(): - """Disable platforms to speed up tests.""" - with patch("homeassistant.components.zha.PLATFORMS", []): - yield - - -@pytest.fixture -def ieee(): - """IEEE fixture.""" - return t.EUI64.deserialize(b"ieeeaddr")[0] - - -@pytest.fixture -def nwk(): - """NWK fixture.""" - return t.NWK(0xBEEF) - - -@pytest.fixture -async def zha_gateway(hass, setup_zha): - """Return ZhaGateway fixture.""" - await setup_zha() - return get_zha_gateway(hass) - - -@pytest.fixture -def zigpy_coordinator_device(zigpy_device_mock): - """Coordinator device fixture.""" - - coordinator = zigpy_device_mock( - {1: {SIG_EP_INPUT: [0x1000], SIG_EP_OUTPUT: [], SIG_EP_TYPE: 0x1234}}, - "00:11:22:33:44:55:66:77", - "test manufacturer", - "test model", - ) - with patch.object(coordinator, "add_to_group", AsyncMock(return_value=[0])): - yield coordinator - - -@pytest.fixture -def endpoint(zigpy_coordinator_device): - """Endpoint fixture.""" - endpoint_mock = mock.MagicMock(spec_set=Endpoint) - endpoint_mock.zigpy_endpoint.device.application.get_device.return_value = ( - zigpy_coordinator_device - ) - type(endpoint_mock.device).skip_configuration = mock.PropertyMock( - return_value=False - ) - endpoint_mock.device.hass.loop_thread_id = threading.get_ident() - endpoint_mock.id = 1 - return endpoint_mock - - -@pytest.fixture -def poll_control_ch(endpoint, zigpy_device_mock): - """Poll control cluster handler fixture.""" - cluster_id = zigpy.zcl.clusters.general.PollControl.cluster_id - zigpy_dev = zigpy_device_mock( - {1: {SIG_EP_INPUT: [cluster_id], SIG_EP_OUTPUT: [], SIG_EP_TYPE: 0x1234}}, - "00:11:22:33:44:55:66:77", - "test manufacturer", - "test model", - ) - - cluster = zigpy_dev.endpoints[1].in_clusters[cluster_id] - cluster_handler_class = registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.get( - cluster_id - ).get(None) - return cluster_handler_class(cluster, endpoint) - - -@pytest.fixture -async def poll_control_device(zha_device_restored, zigpy_device_mock): - """Poll control device fixture.""" - cluster_id = zigpy.zcl.clusters.general.PollControl.cluster_id - zigpy_dev = zigpy_device_mock( - {1: {SIG_EP_INPUT: [cluster_id], SIG_EP_OUTPUT: [], SIG_EP_TYPE: 0x1234}}, - "00:11:22:33:44:55:66:77", - "test manufacturer", - "test model", - ) - - return await zha_device_restored(zigpy_dev) - - -@pytest.mark.parametrize( - ("cluster_id", "bind_count", "attrs"), - [ - (zigpy.zcl.clusters.general.Basic.cluster_id, 0, {}), - ( - zigpy.zcl.clusters.general.PowerConfiguration.cluster_id, - 1, - {"battery_voltage", "battery_percentage_remaining"}, - ), - ( - zigpy.zcl.clusters.general.DeviceTemperature.cluster_id, - 1, - {"current_temperature"}, - ), - (zigpy.zcl.clusters.general.Identify.cluster_id, 0, {}), - (zigpy.zcl.clusters.general.Groups.cluster_id, 0, {}), - (zigpy.zcl.clusters.general.Scenes.cluster_id, 1, {}), - (zigpy.zcl.clusters.general.OnOff.cluster_id, 1, {"on_off"}), - (zigpy.zcl.clusters.general.OnOffConfiguration.cluster_id, 1, {}), - (zigpy.zcl.clusters.general.LevelControl.cluster_id, 1, {"current_level"}), - (zigpy.zcl.clusters.general.Alarms.cluster_id, 1, {}), - (zigpy.zcl.clusters.general.AnalogInput.cluster_id, 1, {"present_value"}), - (zigpy.zcl.clusters.general.AnalogOutput.cluster_id, 1, {"present_value"}), - (zigpy.zcl.clusters.general.AnalogValue.cluster_id, 1, {"present_value"}), - (zigpy.zcl.clusters.general.BinaryOutput.cluster_id, 1, {"present_value"}), - (zigpy.zcl.clusters.general.BinaryValue.cluster_id, 1, {"present_value"}), - (zigpy.zcl.clusters.general.MultistateInput.cluster_id, 1, {"present_value"}), - (zigpy.zcl.clusters.general.MultistateOutput.cluster_id, 1, {"present_value"}), - (zigpy.zcl.clusters.general.MultistateValue.cluster_id, 1, {"present_value"}), - (zigpy.zcl.clusters.general.Commissioning.cluster_id, 1, {}), - (zigpy.zcl.clusters.general.Partition.cluster_id, 1, {}), - (zigpy.zcl.clusters.general.Ota.cluster_id, 0, {}), - (zigpy.zcl.clusters.general.PowerProfile.cluster_id, 1, {}), - (zigpy.zcl.clusters.general.ApplianceControl.cluster_id, 1, {}), - (zigpy.zcl.clusters.general.PollControl.cluster_id, 1, {}), - (zigpy.zcl.clusters.general.GreenPowerProxy.cluster_id, 0, {}), - (zigpy.zcl.clusters.closures.DoorLock.cluster_id, 1, {"lock_state"}), - ( - zigpy.zcl.clusters.hvac.Thermostat.cluster_id, - 1, - { - "local_temperature", - "occupied_cooling_setpoint", - "occupied_heating_setpoint", - "unoccupied_cooling_setpoint", - "unoccupied_heating_setpoint", - "running_mode", - "running_state", - "system_mode", - "occupancy", - "pi_cooling_demand", - "pi_heating_demand", - }, - ), - (zigpy.zcl.clusters.hvac.Fan.cluster_id, 1, {"fan_mode"}), - ( - zigpy.zcl.clusters.lighting.Color.cluster_id, - 1, - { - "current_x", - "current_y", - "color_temperature", - "current_hue", - "enhanced_current_hue", - "current_saturation", - }, - ), - ( - zigpy.zcl.clusters.measurement.IlluminanceMeasurement.cluster_id, - 1, - {"measured_value"}, - ), - ( - zigpy.zcl.clusters.measurement.IlluminanceLevelSensing.cluster_id, - 1, - {"level_status"}, - ), - ( - zigpy.zcl.clusters.measurement.TemperatureMeasurement.cluster_id, - 1, - {"measured_value"}, - ), - ( - zigpy.zcl.clusters.measurement.PressureMeasurement.cluster_id, - 1, - {"measured_value"}, - ), - ( - zigpy.zcl.clusters.measurement.FlowMeasurement.cluster_id, - 1, - {"measured_value"}, - ), - ( - zigpy.zcl.clusters.measurement.RelativeHumidity.cluster_id, - 1, - {"measured_value"}, - ), - (zigpy.zcl.clusters.measurement.OccupancySensing.cluster_id, 1, {"occupancy"}), - ( - zigpy.zcl.clusters.smartenergy.Metering.cluster_id, - 1, - { - "instantaneous_demand", - "current_summ_delivered", - "current_tier1_summ_delivered", - "current_tier2_summ_delivered", - "current_tier3_summ_delivered", - "current_tier4_summ_delivered", - "current_tier5_summ_delivered", - "current_tier6_summ_delivered", - "current_summ_received", - "status", - }, - ), - ( - zigpy.zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id, - 1, - { - "active_power", - "active_power_max", - "apparent_power", - "rms_current", - "rms_current_max", - "rms_voltage", - "rms_voltage_max", - }, - ), - ], -) -async def test_in_cluster_handler_config( - cluster_id, bind_count, attrs, endpoint, zigpy_device_mock, zha_gateway -) -> None: - """Test ZHA core cluster handler configuration for input clusters.""" - zigpy_dev = zigpy_device_mock( - {1: {SIG_EP_INPUT: [cluster_id], SIG_EP_OUTPUT: [], SIG_EP_TYPE: 0x1234}}, - "00:11:22:33:44:55:66:77", - "test manufacturer", - "test model", - ) - - cluster = zigpy_dev.endpoints[1].in_clusters[cluster_id] - cluster_handler_class = registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.get( - cluster_id, {None, cluster_handlers.ClusterHandler} - ).get(None) - cluster_handler = cluster_handler_class(cluster, endpoint) - - await cluster_handler.async_configure() - - assert cluster.bind.call_count == bind_count - assert cluster.configure_reporting.call_count == 0 - assert cluster.configure_reporting_multiple.call_count == math.ceil(len(attrs) / 3) - reported_attrs = { - a - for a in attrs - for attr in cluster.configure_reporting_multiple.call_args_list - for attrs in attr[0][0] - } - assert set(attrs) == reported_attrs - - -@pytest.mark.parametrize( - ("cluster_id", "bind_count"), - [ - (0x0000, 0), - (0x0001, 1), - (0x0002, 1), - (0x0003, 0), - (0x0004, 0), - (0x0005, 1), - (0x0006, 1), - (0x0007, 1), - (0x0008, 1), - (0x0009, 1), - (0x0015, 1), - (0x0016, 1), - (0x0019, 0), - (0x001A, 1), - (0x001B, 1), - (0x0020, 1), - (0x0021, 0), - (0x0101, 1), - (0x0202, 1), - (0x0300, 1), - (0x0400, 1), - (0x0402, 1), - (0x0403, 1), - (0x0405, 1), - (0x0406, 1), - (0x0702, 1), - (0x0B04, 1), - ], -) -async def test_out_cluster_handler_config( - cluster_id, bind_count, endpoint, zigpy_device_mock, zha_gateway -) -> None: - """Test ZHA core cluster handler configuration for output clusters.""" - zigpy_dev = zigpy_device_mock( - {1: {SIG_EP_OUTPUT: [cluster_id], SIG_EP_INPUT: [], SIG_EP_TYPE: 0x1234}}, - "00:11:22:33:44:55:66:77", - "test manufacturer", - "test model", - ) - - cluster = zigpy_dev.endpoints[1].out_clusters[cluster_id] - cluster.bind_only = True - cluster_handler_class = registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.get( - cluster_id, {None: cluster_handlers.ClusterHandler} - ).get(None) - cluster_handler = cluster_handler_class(cluster, endpoint) - - await cluster_handler.async_configure() - - assert cluster.bind.call_count == bind_count - assert cluster.configure_reporting.call_count == 0 - - -def test_cluster_handler_registry() -> None: - """Test ZIGBEE cluster handler Registry.""" - - # get all quirk ID from zigpy quirks registry - all_quirk_ids = {} - for cluster_id in CLUSTERS_BY_ID: - all_quirk_ids[cluster_id] = {None} - # pylint: disable-next=too-many-nested-blocks - for manufacturer in zigpy_quirks._DEVICE_REGISTRY.registry.values(): - for model_quirk_list in manufacturer.values(): - for quirk in model_quirk_list: - quirk_id = getattr(quirk, zha_const.ATTR_QUIRK_ID, None) - device_description = getattr(quirk, "replacement", None) or getattr( - quirk, "signature", None - ) - - for endpoint in device_description["endpoints"].values(): - cluster_ids = set() - if "input_clusters" in endpoint: - cluster_ids.update(endpoint["input_clusters"]) - if "output_clusters" in endpoint: - cluster_ids.update(endpoint["output_clusters"]) - for cluster_id in cluster_ids: - if not isinstance(cluster_id, int): - cluster_id = cluster_id.cluster_id - if cluster_id not in all_quirk_ids: - all_quirk_ids[cluster_id] = {None} - all_quirk_ids[cluster_id].add(quirk_id) - - # pylint: disable-next=undefined-loop-variable - del quirk, model_quirk_list, manufacturer - - for ( - cluster_id, - cluster_handler_classes, - ) in registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.items(): - assert isinstance(cluster_id, int) - assert 0 <= cluster_id <= 0xFFFF - assert cluster_id in all_quirk_ids - assert isinstance(cluster_handler_classes, dict) - for quirk_id, cluster_handler in cluster_handler_classes.items(): - assert isinstance(quirk_id, (NoneType, str)) - assert issubclass(cluster_handler, cluster_handlers.ClusterHandler) - assert quirk_id in all_quirk_ids[cluster_id] - - -def test_epch_unclaimed_cluster_handlers(cluster_handler) -> None: - """Test unclaimed cluster handlers.""" - - ch_1 = cluster_handler(zha_const.CLUSTER_HANDLER_ON_OFF, 6) - ch_2 = cluster_handler(zha_const.CLUSTER_HANDLER_LEVEL, 8) - ch_3 = cluster_handler(zha_const.CLUSTER_HANDLER_COLOR, 768) - - ep_cluster_handlers = Endpoint( - mock.MagicMock(spec_set=ZigpyEndpoint), mock.MagicMock(spec_set=ZHADevice) - ) - all_cluster_handlers = {ch_1.id: ch_1, ch_2.id: ch_2, ch_3.id: ch_3} - with mock.patch.dict( - ep_cluster_handlers.all_cluster_handlers, all_cluster_handlers, clear=True - ): - available = ep_cluster_handlers.unclaimed_cluster_handlers() - assert ch_1 in available - assert ch_2 in available - assert ch_3 in available - - ep_cluster_handlers.claimed_cluster_handlers[ch_2.id] = ch_2 - available = ep_cluster_handlers.unclaimed_cluster_handlers() - assert ch_1 in available - assert ch_2 not in available - assert ch_3 in available - - ep_cluster_handlers.claimed_cluster_handlers[ch_1.id] = ch_1 - available = ep_cluster_handlers.unclaimed_cluster_handlers() - assert ch_1 not in available - assert ch_2 not in available - assert ch_3 in available - - ep_cluster_handlers.claimed_cluster_handlers[ch_3.id] = ch_3 - available = ep_cluster_handlers.unclaimed_cluster_handlers() - assert ch_1 not in available - assert ch_2 not in available - assert ch_3 not in available - - -def test_epch_claim_cluster_handlers(cluster_handler) -> None: - """Test cluster handler claiming.""" - - ch_1 = cluster_handler(zha_const.CLUSTER_HANDLER_ON_OFF, 6) - ch_2 = cluster_handler(zha_const.CLUSTER_HANDLER_LEVEL, 8) - ch_3 = cluster_handler(zha_const.CLUSTER_HANDLER_COLOR, 768) - - ep_cluster_handlers = Endpoint( - mock.MagicMock(spec_set=ZigpyEndpoint), mock.MagicMock(spec_set=ZHADevice) - ) - all_cluster_handlers = {ch_1.id: ch_1, ch_2.id: ch_2, ch_3.id: ch_3} - with mock.patch.dict( - ep_cluster_handlers.all_cluster_handlers, all_cluster_handlers, clear=True - ): - assert ch_1.id not in ep_cluster_handlers.claimed_cluster_handlers - assert ch_2.id not in ep_cluster_handlers.claimed_cluster_handlers - assert ch_3.id not in ep_cluster_handlers.claimed_cluster_handlers - - ep_cluster_handlers.claim_cluster_handlers([ch_2]) - assert ch_1.id not in ep_cluster_handlers.claimed_cluster_handlers - assert ch_2.id in ep_cluster_handlers.claimed_cluster_handlers - assert ep_cluster_handlers.claimed_cluster_handlers[ch_2.id] is ch_2 - assert ch_3.id not in ep_cluster_handlers.claimed_cluster_handlers - - ep_cluster_handlers.claim_cluster_handlers([ch_3, ch_1]) - assert ch_1.id in ep_cluster_handlers.claimed_cluster_handlers - assert ep_cluster_handlers.claimed_cluster_handlers[ch_1.id] is ch_1 - assert ch_2.id in ep_cluster_handlers.claimed_cluster_handlers - assert ep_cluster_handlers.claimed_cluster_handlers[ch_2.id] is ch_2 - assert ch_3.id in ep_cluster_handlers.claimed_cluster_handlers - assert ep_cluster_handlers.claimed_cluster_handlers[ch_3.id] is ch_3 - assert "1:0x0300" in ep_cluster_handlers.claimed_cluster_handlers - - -@mock.patch( - "homeassistant.components.zha.core.endpoint.Endpoint.add_client_cluster_handlers" -) -@mock.patch( - "homeassistant.components.zha.core.discovery.PROBE.discover_entities", - mock.MagicMock(), -) -def test_ep_all_cluster_handlers(m1, zha_device_mock: Callable[..., ZHADevice]) -> None: - """Test Endpoint adding all cluster handlers.""" - zha_device = zha_device_mock( - { - 1: { - SIG_EP_INPUT: [0, 1, 6, 8], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH, - }, - 2: { - SIG_EP_INPUT: [0, 1, 6, 8, 768], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: 0x0000, - }, - } - ) - assert "1:0x0000" in zha_device._endpoints[1].all_cluster_handlers - assert "1:0x0001" in zha_device._endpoints[1].all_cluster_handlers - assert "1:0x0006" in zha_device._endpoints[1].all_cluster_handlers - assert "1:0x0008" in zha_device._endpoints[1].all_cluster_handlers - assert "1:0x0300" not in zha_device._endpoints[1].all_cluster_handlers - assert "2:0x0000" not in zha_device._endpoints[1].all_cluster_handlers - assert "2:0x0001" not in zha_device._endpoints[1].all_cluster_handlers - assert "2:0x0006" not in zha_device._endpoints[1].all_cluster_handlers - assert "2:0x0008" not in zha_device._endpoints[1].all_cluster_handlers - assert "2:0x0300" not in zha_device._endpoints[1].all_cluster_handlers - assert "1:0x0000" not in zha_device._endpoints[2].all_cluster_handlers - assert "1:0x0001" not in zha_device._endpoints[2].all_cluster_handlers - assert "1:0x0006" not in zha_device._endpoints[2].all_cluster_handlers - assert "1:0x0008" not in zha_device._endpoints[2].all_cluster_handlers - assert "1:0x0300" not in zha_device._endpoints[2].all_cluster_handlers - assert "2:0x0000" in zha_device._endpoints[2].all_cluster_handlers - assert "2:0x0001" in zha_device._endpoints[2].all_cluster_handlers - assert "2:0x0006" in zha_device._endpoints[2].all_cluster_handlers - assert "2:0x0008" in zha_device._endpoints[2].all_cluster_handlers - assert "2:0x0300" in zha_device._endpoints[2].all_cluster_handlers - - zha_device.async_cleanup_handles() - - -@mock.patch( - "homeassistant.components.zha.core.endpoint.Endpoint.add_client_cluster_handlers" -) -@mock.patch( - "homeassistant.components.zha.core.discovery.PROBE.discover_entities", - mock.MagicMock(), -) -def test_cluster_handler_power_config( - m1, zha_device_mock: Callable[..., ZHADevice] -) -> None: - """Test that cluster handlers only get a single power cluster handler.""" - in_clusters = [0, 1, 6, 8] - zha_device = zha_device_mock( - { - 1: {SIG_EP_INPUT: in_clusters, SIG_EP_OUTPUT: [], SIG_EP_TYPE: 0x0000}, - 2: { - SIG_EP_INPUT: [*in_clusters, 768], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: 0x0000, - }, - } - ) - assert "1:0x0000" in zha_device._endpoints[1].all_cluster_handlers - assert "1:0x0001" in zha_device._endpoints[1].all_cluster_handlers - assert "1:0x0006" in zha_device._endpoints[1].all_cluster_handlers - assert "1:0x0008" in zha_device._endpoints[1].all_cluster_handlers - assert "1:0x0300" not in zha_device._endpoints[1].all_cluster_handlers - assert "2:0x0000" in zha_device._endpoints[2].all_cluster_handlers - assert "2:0x0001" in zha_device._endpoints[2].all_cluster_handlers - assert "2:0x0006" in zha_device._endpoints[2].all_cluster_handlers - assert "2:0x0008" in zha_device._endpoints[2].all_cluster_handlers - assert "2:0x0300" in zha_device._endpoints[2].all_cluster_handlers - - zha_device.async_cleanup_handles() - - zha_device = zha_device_mock( - { - 1: {SIG_EP_INPUT: [], SIG_EP_OUTPUT: [], SIG_EP_TYPE: 0x0000}, - 2: {SIG_EP_INPUT: in_clusters, SIG_EP_OUTPUT: [], SIG_EP_TYPE: 0x0000}, - } - ) - assert "1:0x0001" not in zha_device._endpoints[1].all_cluster_handlers - assert "2:0x0001" in zha_device._endpoints[2].all_cluster_handlers - - zha_device.async_cleanup_handles() - - zha_device = zha_device_mock( - {2: {SIG_EP_INPUT: in_clusters, SIG_EP_OUTPUT: [], SIG_EP_TYPE: 0x0000}} - ) - assert "2:0x0001" in zha_device._endpoints[2].all_cluster_handlers - - zha_device.async_cleanup_handles() - - -async def test_ep_cluster_handlers_configure(cluster_handler) -> None: - """Test unclaimed cluster handlers.""" - - ch_1 = cluster_handler(zha_const.CLUSTER_HANDLER_ON_OFF, 6) - ch_2 = cluster_handler(zha_const.CLUSTER_HANDLER_LEVEL, 8) - ch_3 = cluster_handler(zha_const.CLUSTER_HANDLER_COLOR, 768) - ch_3.async_configure = AsyncMock(side_effect=TimeoutError) - ch_3.async_initialize = AsyncMock(side_effect=TimeoutError) - ch_4 = cluster_handler(zha_const.CLUSTER_HANDLER_ON_OFF, 6) - ch_5 = cluster_handler(zha_const.CLUSTER_HANDLER_LEVEL, 8) - ch_5.async_configure = AsyncMock(side_effect=TimeoutError) - ch_5.async_initialize = AsyncMock(side_effect=TimeoutError) - - endpoint_mock = mock.MagicMock(spec_set=ZigpyEndpoint) - type(endpoint_mock).in_clusters = mock.PropertyMock(return_value={}) - type(endpoint_mock).out_clusters = mock.PropertyMock(return_value={}) - endpoint = Endpoint.new(endpoint_mock, mock.MagicMock(spec_set=ZHADevice)) - - claimed = {ch_1.id: ch_1, ch_2.id: ch_2, ch_3.id: ch_3} - client_handlers = {ch_4.id: ch_4, ch_5.id: ch_5} - - with ( - mock.patch.dict(endpoint.claimed_cluster_handlers, claimed, clear=True), - mock.patch.dict(endpoint.client_cluster_handlers, client_handlers, clear=True), - ): - await endpoint.async_configure() - await endpoint.async_initialize(mock.sentinel.from_cache) - - for ch in (*claimed.values(), *client_handlers.values()): - assert ch.async_initialize.call_count == 1 - assert ch.async_initialize.await_count == 1 - assert ch.async_initialize.call_args[0][0] is mock.sentinel.from_cache - assert ch.async_configure.call_count == 1 - assert ch.async_configure.await_count == 1 - - assert ch_3.debug.call_count == 2 - assert ch_5.debug.call_count == 2 - - -async def test_poll_control_configure(poll_control_ch) -> None: - """Test poll control cluster handler configuration.""" - await poll_control_ch.async_configure() - assert poll_control_ch.cluster.write_attributes.call_count == 1 - assert poll_control_ch.cluster.write_attributes.call_args[0][0] == { - "checkin_interval": poll_control_ch.CHECKIN_INTERVAL - } - - -async def test_poll_control_checkin_response(poll_control_ch) -> None: - """Test poll control cluster handler checkin response.""" - rsp_mock = AsyncMock() - set_interval_mock = AsyncMock() - fast_poll_mock = AsyncMock() - cluster = poll_control_ch.cluster - patch_1 = mock.patch.object(cluster, "checkin_response", rsp_mock) - patch_2 = mock.patch.object(cluster, "set_long_poll_interval", set_interval_mock) - patch_3 = mock.patch.object(cluster, "fast_poll_stop", fast_poll_mock) - - with patch_1, patch_2, patch_3: - await poll_control_ch.check_in_response(33) - - assert rsp_mock.call_count == 1 - assert set_interval_mock.call_count == 1 - assert fast_poll_mock.call_count == 1 - - await poll_control_ch.check_in_response(33) - assert cluster.endpoint.request.call_count == 3 - assert cluster.endpoint.request.await_count == 3 - assert cluster.endpoint.request.call_args_list[0][0][1] == 33 - assert cluster.endpoint.request.call_args_list[0][0][0] == 0x0020 - assert cluster.endpoint.request.call_args_list[1][0][0] == 0x0020 - - -async def test_poll_control_cluster_command( - hass: HomeAssistant, poll_control_device -) -> None: - """Test poll control cluster handler response to cluster command.""" - checkin_mock = AsyncMock() - poll_control_ch = poll_control_device._endpoints[1].all_cluster_handlers["1:0x0020"] - cluster = poll_control_ch.cluster - events = async_capture_events(hass, zha_const.ZHA_EVENT) - - with mock.patch.object(poll_control_ch, "check_in_response", checkin_mock): - tsn = 22 - hdr = make_zcl_header(0, global_command=False, tsn=tsn) - assert not events - cluster.handle_message( - hdr, [mock.sentinel.args, mock.sentinel.args2, mock.sentinel.args3] - ) - await hass.async_block_till_done() - - assert checkin_mock.call_count == 1 - assert checkin_mock.await_count == 1 - assert checkin_mock.await_args[0][0] == tsn - assert len(events) == 1 - data = events[0].data - assert data["command"] == "checkin" - assert data["args"][0] is mock.sentinel.args - assert data["args"][1] is mock.sentinel.args2 - assert data["args"][2] is mock.sentinel.args3 - assert data["unique_id"] == "00:11:22:33:44:55:66:77:1:0x0020" - assert data["device_id"] == poll_control_device.device_id - - -async def test_poll_control_ignore_list( - hass: HomeAssistant, poll_control_device -) -> None: - """Test poll control cluster handler ignore list.""" - set_long_poll_mock = AsyncMock() - poll_control_ch = poll_control_device._endpoints[1].all_cluster_handlers["1:0x0020"] - cluster = poll_control_ch.cluster - - with mock.patch.object(cluster, "set_long_poll_interval", set_long_poll_mock): - await poll_control_ch.check_in_response(33) - - assert set_long_poll_mock.call_count == 1 - - set_long_poll_mock.reset_mock() - poll_control_ch.skip_manufacturer_id(4151) - with mock.patch.object(cluster, "set_long_poll_interval", set_long_poll_mock): - await poll_control_ch.check_in_response(33) - - assert set_long_poll_mock.call_count == 0 - - -async def test_poll_control_ikea(hass: HomeAssistant, poll_control_device) -> None: - """Test poll control cluster handler ignore list for ikea.""" - set_long_poll_mock = AsyncMock() - poll_control_ch = poll_control_device._endpoints[1].all_cluster_handlers["1:0x0020"] - cluster = poll_control_ch.cluster - - poll_control_device.device.node_desc.manufacturer_code = 4476 - with mock.patch.object(cluster, "set_long_poll_interval", set_long_poll_mock): - await poll_control_ch.check_in_response(33) - - assert set_long_poll_mock.call_count == 0 - - -@pytest.fixture -def zigpy_zll_device(zigpy_device_mock): - """ZLL device fixture.""" - - return zigpy_device_mock( - {1: {SIG_EP_INPUT: [0x1000], SIG_EP_OUTPUT: [], SIG_EP_TYPE: 0x1234}}, - "00:11:22:33:44:55:66:77", - "test manufacturer", - "test model", - ) - - -async def test_zll_device_groups( - zigpy_zll_device, endpoint, zigpy_coordinator_device -) -> None: - """Test adding coordinator to ZLL groups.""" - - cluster = zigpy_zll_device.endpoints[1].lightlink - cluster_handler = cluster_handlers.lightlink.LightLinkClusterHandler( - cluster, endpoint - ) - - get_group_identifiers_rsp = zigpy.zcl.clusters.lightlink.LightLink.commands_by_name[ - "get_group_identifiers_rsp" - ].schema - - with patch.object( - cluster, - "command", - AsyncMock( - return_value=get_group_identifiers_rsp( - total=0, start_index=0, group_info_records=[] - ) - ), - ) as cmd_mock: - await cluster_handler.async_configure() - assert cmd_mock.await_count == 1 - assert ( - cluster.server_commands[cmd_mock.await_args[0][0]].name - == "get_group_identifiers" - ) - assert cluster.bind.call_count == 0 - assert zigpy_coordinator_device.add_to_group.await_count == 1 - assert zigpy_coordinator_device.add_to_group.await_args[0][0] == 0x0000 - - zigpy_coordinator_device.add_to_group.reset_mock() - group_1 = zigpy.zcl.clusters.lightlink.GroupInfoRecord(0xABCD, 0x00) - group_2 = zigpy.zcl.clusters.lightlink.GroupInfoRecord(0xAABB, 0x00) - with patch.object( - cluster, - "command", - AsyncMock( - return_value=get_group_identifiers_rsp( - total=2, start_index=0, group_info_records=[group_1, group_2] - ) - ), - ) as cmd_mock: - await cluster_handler.async_configure() - assert cmd_mock.await_count == 1 - assert ( - cluster.server_commands[cmd_mock.await_args[0][0]].name - == "get_group_identifiers" - ) - assert cluster.bind.call_count == 0 - assert zigpy_coordinator_device.add_to_group.await_count == 2 - assert ( - zigpy_coordinator_device.add_to_group.await_args_list[0][0][0] - == group_1.group_id - ) - assert ( - zigpy_coordinator_device.add_to_group.await_args_list[1][0][0] - == group_2.group_id - ) - - -@mock.patch( - "homeassistant.components.zha.core.discovery.PROBE.discover_entities", - mock.MagicMock(), -) -async def test_cluster_no_ep_attribute( - zha_device_mock: Callable[..., ZHADevice], -) -> None: - """Test cluster handlers for clusters without ep_attribute.""" - - zha_device = zha_device_mock( - {1: {SIG_EP_INPUT: [0x042E], SIG_EP_OUTPUT: [], SIG_EP_TYPE: 0x1234}}, - ) - - assert "1:0x042e" in zha_device._endpoints[1].all_cluster_handlers - assert zha_device._endpoints[1].all_cluster_handlers["1:0x042e"].name - - zha_device.async_cleanup_handles() - - -async def test_configure_reporting(hass: HomeAssistant, endpoint) -> None: - """Test setting up a cluster handler and configuring attribute reporting in two batches.""" - - class TestZigbeeClusterHandler(cluster_handlers.ClusterHandler): - BIND = True - REPORT_CONFIG = ( - # By name - cluster_handlers.AttrReportConfig(attr="current_x", config=(1, 60, 1)), - cluster_handlers.AttrReportConfig(attr="current_hue", config=(1, 60, 2)), - cluster_handlers.AttrReportConfig( - attr="color_temperature", config=(1, 60, 3) - ), - cluster_handlers.AttrReportConfig(attr="current_y", config=(1, 60, 4)), - ) - - mock_ep = mock.AsyncMock(spec_set=zigpy.endpoint.Endpoint) - mock_ep.device.zdo = AsyncMock() - - cluster = zigpy.zcl.clusters.lighting.Color(mock_ep) - cluster.bind = AsyncMock( - spec_set=cluster.bind, - return_value=[zdo_t.Status.SUCCESS], # ZDOCmd.Bind_rsp - ) - cluster.configure_reporting_multiple = AsyncMock( - spec_set=cluster.configure_reporting_multiple, - return_value=[ - foundation.ConfigureReportingResponseRecord( - status=foundation.Status.SUCCESS - ) - ], - ) - - cluster_handler = TestZigbeeClusterHandler(cluster, endpoint) - await cluster_handler.async_configure() - - # Since we request reporting for five attributes, we need to make two calls (3 + 1) - assert cluster.configure_reporting_multiple.mock_calls == [ - mock.call( - { - "current_x": (1, 60, 1), - "current_hue": (1, 60, 2), - "color_temperature": (1, 60, 3), - } - ), - mock.call( - { - "current_y": (1, 60, 4), - } - ), - ] - - -async def test_invalid_cluster_handler( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: - """Test setting up a cluster handler that fails to match properly.""" - - class TestZigbeeClusterHandler(cluster_handlers.ClusterHandler): - REPORT_CONFIG = ( - cluster_handlers.AttrReportConfig(attr="missing_attr", config=(1, 60, 1)), - ) - - mock_device = mock.AsyncMock(spec_set=zigpy.device.Device) - zigpy_ep = zigpy.endpoint.Endpoint(mock_device, endpoint_id=1) - - cluster = zigpy_ep.add_input_cluster(zigpy.zcl.clusters.lighting.Color.cluster_id) - cluster.configure_reporting_multiple = AsyncMock( - spec_set=cluster.configure_reporting_multiple, - return_value=[ - foundation.ConfigureReportingResponseRecord( - status=foundation.Status.SUCCESS - ) - ], - ) - - mock_zha_device = mock.AsyncMock(spec=ZHADevice) - mock_zha_device.quirk_id = None - zha_endpoint = Endpoint(zigpy_ep, mock_zha_device) - - # The cluster handler throws an error when matching this cluster - with pytest.raises(KeyError): - TestZigbeeClusterHandler(cluster, zha_endpoint) - - # And one is also logged at runtime - with ( - patch.dict( - registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY[cluster.cluster_id], - {None: TestZigbeeClusterHandler}, - ), - caplog.at_level(logging.WARNING), - ): - zha_endpoint.add_all_cluster_handlers() - - assert "missing_attr" in caplog.text - - -async def test_standard_cluster_handler( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: - """Test setting up a cluster handler that matches a standard cluster.""" - - class TestZigbeeClusterHandler(ColorClusterHandler): - pass - - mock_device = mock.AsyncMock(spec_set=zigpy.device.Device) - zigpy_ep = zigpy.endpoint.Endpoint(mock_device, endpoint_id=1) - - cluster = zigpy_ep.add_input_cluster(zigpy.zcl.clusters.lighting.Color.cluster_id) - cluster.configure_reporting_multiple = AsyncMock( - spec_set=cluster.configure_reporting_multiple, - return_value=[ - foundation.ConfigureReportingResponseRecord( - status=foundation.Status.SUCCESS - ) - ], - ) - - mock_zha_device = mock.AsyncMock(spec=ZHADevice) - mock_zha_device.quirk_id = None - zha_endpoint = Endpoint(zigpy_ep, mock_zha_device) - - with patch.dict( - registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY[cluster.cluster_id], - {"__test_quirk_id": TestZigbeeClusterHandler}, - ): - zha_endpoint.add_all_cluster_handlers() - - assert len(zha_endpoint.all_cluster_handlers) == 1 - assert isinstance( - list(zha_endpoint.all_cluster_handlers.values())[0], ColorClusterHandler - ) - - -async def test_quirk_id_cluster_handler( - hass: HomeAssistant, caplog: pytest.LogCaptureFixture -) -> None: - """Test setting up a cluster handler that matches a standard cluster.""" - - class TestZigbeeClusterHandler(ColorClusterHandler): - pass - - mock_device = mock.AsyncMock(spec_set=zigpy.device.Device) - zigpy_ep = zigpy.endpoint.Endpoint(mock_device, endpoint_id=1) - - cluster = zigpy_ep.add_input_cluster(zigpy.zcl.clusters.lighting.Color.cluster_id) - cluster.configure_reporting_multiple = AsyncMock( - spec_set=cluster.configure_reporting_multiple, - return_value=[ - foundation.ConfigureReportingResponseRecord( - status=foundation.Status.SUCCESS - ) - ], - ) - - mock_zha_device = mock.AsyncMock(spec=ZHADevice) - mock_zha_device.quirk_id = "__test_quirk_id" - zha_endpoint = Endpoint(zigpy_ep, mock_zha_device) - - with patch.dict( - registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY[cluster.cluster_id], - {"__test_quirk_id": TestZigbeeClusterHandler}, - ): - zha_endpoint.add_all_cluster_handlers() - - assert len(zha_endpoint.all_cluster_handlers) == 1 - assert isinstance( - list(zha_endpoint.all_cluster_handlers.values())[0], TestZigbeeClusterHandler - ) - - -# parametrize side effects: -@pytest.mark.parametrize( - ("side_effect", "expected_error"), - [ - (zigpy.exceptions.ZigbeeException(), "Failed to send request"), - ( - zigpy.exceptions.ZigbeeException("Zigbee exception"), - "Failed to send request: Zigbee exception", - ), - (TimeoutError(), "Failed to send request: device did not respond"), - ], -) -async def test_retry_request( - side_effect: Exception | None, expected_error: str | None -) -> None: - """Test the `retry_request` decorator's handling of zigpy-internal exceptions.""" - - async def func(arg1: int, arg2: int) -> int: - assert arg1 == 1 - assert arg2 == 2 - - raise side_effect - - func = mock.AsyncMock(wraps=func) - decorated_func = cluster_handlers.retry_request(func) - - with pytest.raises(HomeAssistantError) as exc: - await decorated_func(1, arg2=2) - - assert func.await_count == 3 - assert isinstance(exc.value, HomeAssistantError) - assert str(exc.value) == expected_error - - -async def test_cluster_handler_naming() -> None: - """Test that all cluster handlers are named appropriately.""" - for client_cluster_handler in registries.CLIENT_CLUSTER_HANDLER_REGISTRY.values(): - assert issubclass(client_cluster_handler, cluster_handlers.ClientClusterHandler) - assert client_cluster_handler.__name__.endswith("ClientClusterHandler") - - for cluster_handler_dict in registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.values(): - for cluster_handler in cluster_handler_dict.values(): - assert not issubclass( - cluster_handler, cluster_handlers.ClientClusterHandler - ) - assert cluster_handler.__name__.endswith("ClusterHandler") diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 0c8414f458f..f3104141269 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -9,6 +9,7 @@ import uuid import pytest import serial.tools.list_ports +from zha.application.const import RadioType from zigpy.backups import BackupManager import zigpy.config from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH, SCHEMA_DEVICE @@ -21,13 +22,12 @@ from homeassistant.components import ssdp, usb, zeroconf from homeassistant.components.hassio import AddonState from homeassistant.components.ssdp import ATTR_UPNP_MANUFACTURER_URL, ATTR_UPNP_SERIAL from homeassistant.components.zha import config_flow, radio_manager -from homeassistant.components.zha.core.const import ( +from homeassistant.components.zha.const import ( CONF_BAUDRATE, CONF_FLOW_CONTROL, CONF_RADIO_TYPE, DOMAIN, EZSP_OVERWRITE_EUI64, - RadioType, ) from homeassistant.components.zha.radio_manager import ProbeResult from homeassistant.config_entries import ( diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 5f6dac885f2..afef2aab70f 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -1,12 +1,10 @@ """Test ZHA cover.""" -import asyncio from unittest.mock import patch import pytest -import zigpy.profiles.zha -import zigpy.types -from zigpy.zcl.clusters import closures, general +from zigpy.profiles import zha +from zigpy.zcl.clusters import closures import zigpy.zcl.foundation as zcl_f from homeassistant.components.cover import ( @@ -22,34 +20,27 @@ from homeassistant.components.cover import ( SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, SERVICE_STOP_COVER_TILT, - SERVICE_TOGGLE_COVER_TILT, ) -from homeassistant.components.zha.core.const import ZHA_EVENT +from homeassistant.components.zha.helpers import ( + ZHADeviceProxy, + ZHAGatewayProxy, + get_zha_gateway, + get_zha_gateway_proxy, +) from homeassistant.const import ( - ATTR_COMMAND, STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING, - STATE_UNAVAILABLE, Platform, ) -from homeassistant.core import CoreState, HomeAssistant, State +from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.entity_component import async_update_entity -from .common import ( - async_enable_traffic, - async_test_rejoin, - find_entity_id, - make_zcl_header, - send_attributes_report, - update_attribute_cache, -) +from .common import find_entity_id, send_attributes_report, update_attribute_cache from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE -from tests.common import async_capture_events, mock_restore_cache - Default_Response = zcl_f.GENERAL_COMMANDS[zcl_f.GeneralCommand.Default_Response].schema @@ -68,135 +59,31 @@ def cover_platform_only(): yield -@pytest.fixture -def zigpy_cover_device(zigpy_device_mock): - """Zigpy cover device.""" - - endpoints = { - 1: { - SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.WINDOW_COVERING_DEVICE, - SIG_EP_INPUT: [closures.WindowCovering.cluster_id], - SIG_EP_OUTPUT: [], - } - } - return zigpy_device_mock(endpoints) - - -@pytest.fixture -def zigpy_cover_remote(zigpy_device_mock): - """Zigpy cover remote device.""" - - endpoints = { - 1: { - SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.WINDOW_COVERING_CONTROLLER, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [closures.WindowCovering.cluster_id], - } - } - return zigpy_device_mock(endpoints) - - -@pytest.fixture -def zigpy_shade_device(zigpy_device_mock): - """Zigpy shade device.""" - - endpoints = { - 1: { - SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.SHADE, - SIG_EP_INPUT: [ - closures.Shade.cluster_id, - general.LevelControl.cluster_id, - general.OnOff.cluster_id, - ], - SIG_EP_OUTPUT: [], - } - } - return zigpy_device_mock(endpoints) - - -@pytest.fixture -def zigpy_keen_vent(zigpy_device_mock): - """Zigpy Keen Vent device.""" - - endpoints = { - 1: { - SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.LEVEL_CONTROLLABLE_OUTPUT, - SIG_EP_INPUT: [general.LevelControl.cluster_id, general.OnOff.cluster_id], - SIG_EP_OUTPUT: [], - } - } - return zigpy_device_mock( - endpoints, manufacturer="Keen Home Inc", model="SV02-612-MP-1.3" - ) - - WCAttrs = closures.WindowCovering.AttributeDefs WCCmds = closures.WindowCovering.ServerCommandDefs WCT = closures.WindowCovering.WindowCoveringType WCCS = closures.WindowCovering.ConfigStatus -async def test_cover_non_tilt_initial_state( - hass: HomeAssistant, zha_device_joined_restored, zigpy_cover_device -) -> None: +async def test_cover(hass: HomeAssistant, setup_zha, zigpy_device_mock) -> None: """Test ZHA cover platform.""" + await setup_zha() + gateway = get_zha_gateway(hass) + gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass) + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_PROFILE: zha.PROFILE_ID, + SIG_EP_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE, + SIG_EP_INPUT: [closures.WindowCovering.cluster_id], + SIG_EP_OUTPUT: [], + } + }, + ) # load up cover domain - cluster = zigpy_cover_device.endpoints[1].window_covering - cluster.PLUGGED_ATTR_READS = { - WCAttrs.current_position_lift_percentage.name: 0, - WCAttrs.window_covering_type.name: WCT.Drapery, - WCAttrs.config_status.name: WCCS(~WCCS.Open_up_commands_reversed), - } - update_attribute_cache(cluster) - zha_device = await zha_device_joined_restored(zigpy_cover_device) - assert ( - not zha_device.endpoints[1] - .all_cluster_handlers[f"1:0x{cluster.cluster_id:04x}"] - .inverted - ) - assert cluster.read_attributes.call_count == 3 - assert ( - WCAttrs.current_position_lift_percentage.name - in cluster.read_attributes.call_args[0][0] - ) - assert ( - WCAttrs.current_position_tilt_percentage.name - in cluster.read_attributes.call_args[0][0] - ) - - entity_id = find_entity_id(Platform.COVER, zha_device, hass) - assert entity_id is not None - - await async_enable_traffic(hass, [zha_device], enabled=False) - # test that the cover was created and that it is unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - await hass.async_block_till_done() - - # test update - prev_call_count = cluster.read_attributes.call_count - await async_update_entity(hass, entity_id) - assert cluster.read_attributes.call_count == prev_call_count + 1 - state = hass.states.get(entity_id) - assert state - assert state.state == STATE_OPEN - assert state.attributes[ATTR_CURRENT_POSITION] == 100 - - -async def test_cover( - hass: HomeAssistant, zha_device_joined_restored, zigpy_cover_device -) -> None: - """Test ZHA cover platform.""" - - # load up cover domain - cluster = zigpy_cover_device.endpoints[1].window_covering + cluster = zigpy_device.endpoints[1].window_covering cluster.PLUGGED_ATTR_READS = { WCAttrs.current_position_lift_percentage.name: 0, WCAttrs.current_position_tilt_percentage.name: 42, @@ -204,9 +91,17 @@ async def test_cover( WCAttrs.config_status.name: WCCS(~WCCS.Open_up_commands_reversed), } update_attribute_cache(cluster) - zha_device = await zha_device_joined_restored(zigpy_cover_device) + + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) + + zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee) + entity_id = find_entity_id(Platform.COVER, zha_device_proxy, hass) + assert entity_id is not None + assert ( - not zha_device.endpoints[1] + not zha_device_proxy.device.endpoints[1] .all_cluster_handlers[f"1:0x{cluster.cluster_id:04x}"] .inverted ) @@ -220,21 +115,7 @@ async def test_cover( in cluster.read_attributes.call_args[0][0] ) - entity_id = find_entity_id(Platform.COVER, zha_device, hass) - assert entity_id is not None - - await async_enable_traffic(hass, [zha_device], enabled=False) - # test that the cover was created and that it is unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - await hass.async_block_till_done() - - # test update - prev_call_count = cluster.read_attributes.call_count await async_update_entity(hass, entity_id) - assert cluster.read_attributes.call_count == prev_call_count + 1 state = hass.states.get(entity_id) assert state assert state.state == STATE_OPEN @@ -440,61 +321,41 @@ async def test_cover( assert cluster.request.call_args[0][2].command.name == WCCmds.stop.name assert cluster.request.call_args[1]["expect_reply"] is True - # test rejoin - cluster.PLUGGED_ATTR_READS = {WCAttrs.current_position_lift_percentage.name: 0} - await async_test_rejoin(hass, zigpy_cover_device, [cluster], (1,)) - assert hass.states.get(entity_id).state == STATE_OPEN - - # test toggle - with patch("zigpy.zcl.Cluster.request", return_value=[0x2, zcl_f.Status.SUCCESS]): - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_TOGGLE_COVER_TILT, - {"entity_id": entity_id}, - blocking=True, - ) - assert cluster.request.call_count == 1 - assert cluster.request.call_args[0][0] is False - assert cluster.request.call_args[0][1] == 0x08 - assert ( - cluster.request.call_args[0][2].command.name - == WCCmds.go_to_tilt_percentage.name - ) - assert cluster.request.call_args[0][3] == 100 - assert cluster.request.call_args[1]["expect_reply"] is True - async def test_cover_failures( - hass: HomeAssistant, zha_device_joined_restored, zigpy_cover_device + hass: HomeAssistant, setup_zha, zigpy_device_mock ) -> None: """Test ZHA cover platform failure cases.""" + await setup_zha() + gateway = get_zha_gateway(hass) + gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass) + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_PROFILE: zha.PROFILE_ID, + SIG_EP_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE, + SIG_EP_INPUT: [closures.WindowCovering.cluster_id], + SIG_EP_OUTPUT: [], + } + }, + ) # load up cover domain - cluster = zigpy_cover_device.endpoints[1].window_covering + cluster = zigpy_device.endpoints[1].window_covering cluster.PLUGGED_ATTR_READS = { WCAttrs.current_position_tilt_percentage.name: 42, WCAttrs.window_covering_type.name: WCT.Tilt_blind_tilt_and_lift, } update_attribute_cache(cluster) - zha_device = await zha_device_joined_restored(zigpy_cover_device) - entity_id = find_entity_id(Platform.COVER, zha_device, hass) + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) + + zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee) + entity_id = find_entity_id(Platform.COVER, zha_device_proxy, hass) assert entity_id is not None - await async_enable_traffic(hass, [zha_device], enabled=False) - # test that the cover was created and that it is unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - # test update returned None - prev_call_count = cluster.read_attributes.call_count - await async_update_entity(hass, entity_id) - assert cluster.read_attributes.call_count == prev_call_count + 1 - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - await hass.async_block_till_done() - # test that the state has changed from unavailable to closed await send_attributes_report(hass, cluster, {0: 0, 8: 100, 1: 1}) assert hass.states.get(entity_id).state == STATE_CLOSED @@ -670,319 +531,3 @@ async def test_cover_failures( cluster.request.call_args[0][1] == closures.WindowCovering.ServerCommandDefs.stop.id ) - - -async def test_shade( - hass: HomeAssistant, zha_device_joined_restored, zigpy_shade_device -) -> None: - """Test ZHA cover platform for shade device type.""" - - # load up cover domain - zha_device = await zha_device_joined_restored(zigpy_shade_device) - - cluster_on_off = zigpy_shade_device.endpoints[1].on_off - cluster_level = zigpy_shade_device.endpoints[1].level - entity_id = find_entity_id(Platform.COVER, zha_device, hass) - assert entity_id is not None - - await async_enable_traffic(hass, [zha_device], enabled=False) - # test that the cover was created and that it is unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - await hass.async_block_till_done() - - # test that the state has changed from unavailable to off - await send_attributes_report(hass, cluster_on_off, {8: 0, 0: False, 1: 1}) - assert hass.states.get(entity_id).state == STATE_CLOSED - - # test to see if it opens - await send_attributes_report(hass, cluster_on_off, {8: 0, 0: True, 1: 1}) - assert hass.states.get(entity_id).state == STATE_OPEN - - # close from UI command fails - with patch( - "zigpy.zcl.Cluster.request", - return_value=Default_Response( - command_id=closures.WindowCovering.ServerCommandDefs.down_close.id, - status=zcl_f.Status.UNSUP_CLUSTER_COMMAND, - ), - ): - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_CLOSE_COVER, - {"entity_id": entity_id}, - blocking=True, - ) - assert cluster_on_off.request.call_count == 1 - assert cluster_on_off.request.call_args[0][0] is False - assert cluster_on_off.request.call_args[0][1] == 0x0000 - assert hass.states.get(entity_id).state == STATE_OPEN - - with patch("zigpy.zcl.Cluster.request", return_value=[0x1, zcl_f.Status.SUCCESS]): - await hass.services.async_call( - COVER_DOMAIN, SERVICE_CLOSE_COVER, {"entity_id": entity_id}, blocking=True - ) - assert cluster_on_off.request.call_count == 1 - assert cluster_on_off.request.call_args[0][0] is False - assert cluster_on_off.request.call_args[0][1] == 0x0000 - assert hass.states.get(entity_id).state == STATE_CLOSED - - # open from UI command fails - assert ATTR_CURRENT_POSITION not in hass.states.get(entity_id).attributes - await send_attributes_report(hass, cluster_level, {0: 0}) - with patch( - "zigpy.zcl.Cluster.request", - return_value=Default_Response( - command_id=closures.WindowCovering.ServerCommandDefs.up_open.id, - status=zcl_f.Status.UNSUP_CLUSTER_COMMAND, - ), - ): - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_OPEN_COVER, - {"entity_id": entity_id}, - blocking=True, - ) - assert cluster_on_off.request.call_count == 1 - assert cluster_on_off.request.call_args[0][0] is False - assert cluster_on_off.request.call_args[0][1] == 0x0001 - assert hass.states.get(entity_id).state == STATE_CLOSED - - # stop from UI command fails - with patch( - "zigpy.zcl.Cluster.request", - return_value=Default_Response( - command_id=general.LevelControl.ServerCommandDefs.stop.id, - status=zcl_f.Status.UNSUP_CLUSTER_COMMAND, - ), - ): - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_STOP_COVER, - {"entity_id": entity_id}, - blocking=True, - ) - - assert cluster_level.request.call_count == 1 - assert cluster_level.request.call_args[0][0] is False - assert ( - cluster_level.request.call_args[0][1] - == general.LevelControl.ServerCommandDefs.stop.id - ) - assert hass.states.get(entity_id).state == STATE_CLOSED - - # open from UI succeeds - with patch("zigpy.zcl.Cluster.request", return_value=[0x0, zcl_f.Status.SUCCESS]): - await hass.services.async_call( - COVER_DOMAIN, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True - ) - assert cluster_on_off.request.call_count == 1 - assert cluster_on_off.request.call_args[0][0] is False - assert cluster_on_off.request.call_args[0][1] == 0x0001 - assert hass.states.get(entity_id).state == STATE_OPEN - - # set position UI command fails - with patch( - "zigpy.zcl.Cluster.request", - return_value=Default_Response( - command_id=closures.WindowCovering.ServerCommandDefs.go_to_lift_percentage.id, - status=zcl_f.Status.UNSUP_CLUSTER_COMMAND, - ), - ): - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_SET_COVER_POSITION, - {"entity_id": entity_id, "position": 47}, - blocking=True, - ) - - assert cluster_level.request.call_count == 1 - assert cluster_level.request.call_args[0][0] is False - assert cluster_level.request.call_args[0][1] == 0x0004 - assert int(cluster_level.request.call_args[0][3] * 100 / 255) == 47 - assert hass.states.get(entity_id).attributes[ATTR_CURRENT_POSITION] == 0 - - # set position UI success - with patch("zigpy.zcl.Cluster.request", return_value=[0x5, zcl_f.Status.SUCCESS]): - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_SET_COVER_POSITION, - {"entity_id": entity_id, "position": 47}, - blocking=True, - ) - assert cluster_level.request.call_count == 1 - assert cluster_level.request.call_args[0][0] is False - assert cluster_level.request.call_args[0][1] == 0x0004 - assert int(cluster_level.request.call_args[0][3] * 100 / 255) == 47 - assert hass.states.get(entity_id).attributes[ATTR_CURRENT_POSITION] == 47 - - # report position change - await send_attributes_report(hass, cluster_level, {8: 0, 0: 100, 1: 1}) - assert hass.states.get(entity_id).attributes[ATTR_CURRENT_POSITION] == int( - 100 * 100 / 255 - ) - - # test rejoin - await async_test_rejoin( - hass, zigpy_shade_device, [cluster_level, cluster_on_off], (1,) - ) - assert hass.states.get(entity_id).state == STATE_OPEN - - # test cover stop - with patch("zigpy.zcl.Cluster.request", side_effect=TimeoutError): - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_STOP_COVER, - {"entity_id": entity_id}, - blocking=True, - ) - assert cluster_level.request.call_count == 3 - assert cluster_level.request.call_args[0][0] is False - assert cluster_level.request.call_args[0][1] in (0x0003, 0x0007) - - -async def test_shade_restore_state( - hass: HomeAssistant, zha_device_restored, zigpy_shade_device -) -> None: - """Ensure states are restored on startup.""" - mock_restore_cache( - hass, - ( - State( - "cover.fakemanufacturer_fakemodel_shade", - STATE_OPEN, - {ATTR_CURRENT_POSITION: 50}, - ), - ), - ) - - hass.set_state(CoreState.starting) - - zha_device = await zha_device_restored(zigpy_shade_device) - entity_id = find_entity_id(Platform.COVER, zha_device, hass) - assert entity_id is not None - - # test that the cover was created and that it is available - assert hass.states.get(entity_id).state == STATE_OPEN - assert hass.states.get(entity_id).attributes[ATTR_CURRENT_POSITION] == 50 - - -async def test_cover_restore_state( - hass: HomeAssistant, zha_device_restored, zigpy_cover_device -) -> None: - """Ensure states are restored on startup.""" - cluster = zigpy_cover_device.endpoints[1].window_covering - cluster.PLUGGED_ATTR_READS = { - WCAttrs.current_position_lift_percentage.name: 50, - WCAttrs.current_position_tilt_percentage.name: 42, - WCAttrs.window_covering_type.name: WCT.Tilt_blind_tilt_and_lift, - } - update_attribute_cache(cluster) - - hass.set_state(CoreState.starting) - - zha_device = await zha_device_restored(zigpy_cover_device) - entity_id = find_entity_id(Platform.COVER, zha_device, hass) - assert entity_id is not None - - # test that the cover was created and that it is available - assert hass.states.get(entity_id).state == STATE_OPEN - assert hass.states.get(entity_id).attributes[ATTR_CURRENT_POSITION] == 100 - 50 - assert hass.states.get(entity_id).attributes[ATTR_CURRENT_TILT_POSITION] == 100 - 42 - - -async def test_keen_vent( - hass: HomeAssistant, zha_device_joined_restored, zigpy_keen_vent -) -> None: - """Test keen vent.""" - - # load up cover domain - zha_device = await zha_device_joined_restored(zigpy_keen_vent) - - cluster_on_off = zigpy_keen_vent.endpoints[1].on_off - cluster_level = zigpy_keen_vent.endpoints[1].level - entity_id = find_entity_id(Platform.COVER, zha_device, hass) - assert entity_id is not None - - await async_enable_traffic(hass, [zha_device], enabled=False) - # test that the cover was created and that it is unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - await hass.async_block_till_done() - - # test that the state has changed from unavailable to off - await send_attributes_report(hass, cluster_on_off, {8: 0, 0: False, 1: 1}) - assert hass.states.get(entity_id).state == STATE_CLOSED - - # open from UI command fails - p1 = patch.object(cluster_on_off, "request", side_effect=TimeoutError) - p2 = patch.object(cluster_level, "request", return_value=[4, 0]) - - with p1, p2: - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - COVER_DOMAIN, - SERVICE_OPEN_COVER, - {"entity_id": entity_id}, - blocking=True, - ) - assert cluster_on_off.request.call_count == 3 - assert cluster_on_off.request.call_args[0][0] is False - assert cluster_on_off.request.call_args[0][1] == 0x0001 - assert cluster_level.request.call_count == 1 - assert hass.states.get(entity_id).state == STATE_CLOSED - - # open from UI command success - p1 = patch.object(cluster_on_off, "request", return_value=[1, 0]) - p2 = patch.object(cluster_level, "request", return_value=[4, 0]) - - with p1, p2: - await hass.services.async_call( - COVER_DOMAIN, SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True - ) - await asyncio.sleep(0) - assert cluster_on_off.request.call_count == 1 - assert cluster_on_off.request.call_args[0][0] is False - assert cluster_on_off.request.call_args[0][1] == 0x0001 - assert cluster_level.request.call_count == 1 - assert hass.states.get(entity_id).state == STATE_OPEN - assert hass.states.get(entity_id).attributes[ATTR_CURRENT_POSITION] == 100 - - -async def test_cover_remote( - hass: HomeAssistant, zha_device_joined_restored, zigpy_cover_remote -) -> None: - """Test ZHA cover remote.""" - - # load up cover domain - await zha_device_joined_restored(zigpy_cover_remote) - - cluster = zigpy_cover_remote.endpoints[1].out_clusters[ - closures.WindowCovering.cluster_id - ] - zha_events = async_capture_events(hass, ZHA_EVENT) - - # up command - hdr = make_zcl_header(0, global_command=False) - cluster.handle_message(hdr, []) - await hass.async_block_till_done() - - assert len(zha_events) == 1 - assert zha_events[0].data[ATTR_COMMAND] == "up_open" - - # down command - hdr = make_zcl_header(1, global_command=False) - cluster.handle_message(hdr, []) - await hass.async_block_till_done() - - assert len(zha_events) == 2 - assert zha_events[1].data[ATTR_COMMAND] == "down_close" diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py deleted file mode 100644 index 87acdc5fd1c..00000000000 --- a/tests/components/zha/test_device.py +++ /dev/null @@ -1,363 +0,0 @@ -"""Test ZHA device switch.""" - -from datetime import timedelta -import logging -import time -from unittest import mock -from unittest.mock import patch - -import pytest -import zigpy.profiles.zha -import zigpy.types -from zigpy.zcl.clusters import general -import zigpy.zdo.types as zdo_t - -from homeassistant.components.zha.core.const import ( - CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY, - CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS, -) -from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE, Platform -from homeassistant.core import HomeAssistant -import homeassistant.helpers.device_registry as dr -import homeassistant.util.dt as dt_util - -from .common import async_enable_traffic, make_zcl_header -from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE - -from tests.common import async_fire_time_changed - - -@pytest.fixture(autouse=True) -def required_platforms_only(): - """Only set up the required platform and required base platforms to speed up tests.""" - with patch( - "homeassistant.components.zha.PLATFORMS", - ( - Platform.DEVICE_TRACKER, - Platform.SENSOR, - Platform.SELECT, - Platform.SWITCH, - Platform.BINARY_SENSOR, - ), - ): - yield - - -@pytest.fixture -def zigpy_device(zigpy_device_mock): - """Device tracker zigpy device.""" - - def _dev(with_basic_cluster_handler: bool = True, **kwargs): - in_clusters = [general.OnOff.cluster_id] - if with_basic_cluster_handler: - in_clusters.append(general.Basic.cluster_id) - - endpoints = { - 3: { - SIG_EP_INPUT: in_clusters, - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH, - } - } - return zigpy_device_mock(endpoints, **kwargs) - - return _dev - - -@pytest.fixture -def zigpy_device_mains(zigpy_device_mock): - """Device tracker zigpy device.""" - - def _dev(with_basic_cluster_handler: bool = True): - in_clusters = [general.OnOff.cluster_id] - if with_basic_cluster_handler: - in_clusters.append(general.Basic.cluster_id) - - endpoints = { - 3: { - SIG_EP_INPUT: in_clusters, - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH, - } - } - return zigpy_device_mock( - endpoints, node_descriptor=b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00" - ) - - return _dev - - -@pytest.fixture -def device_with_basic_cluster_handler(zigpy_device_mains): - """Return a ZHA device with a basic cluster handler present.""" - return zigpy_device_mains(with_basic_cluster_handler=True) - - -@pytest.fixture -def device_without_basic_cluster_handler(zigpy_device): - """Return a ZHA device without a basic cluster handler present.""" - return zigpy_device(with_basic_cluster_handler=False) - - -@pytest.fixture -async def ota_zha_device(zha_device_restored, zigpy_device_mock): - """ZHA device with OTA cluster fixture.""" - zigpy_dev = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [general.Basic.cluster_id], - SIG_EP_OUTPUT: [general.Ota.cluster_id], - SIG_EP_TYPE: 0x1234, - } - }, - "00:11:22:33:44:55:66:77", - "test manufacturer", - "test model", - ) - - return await zha_device_restored(zigpy_dev) - - -def _send_time_changed(hass, seconds): - """Send a time changed event.""" - now = dt_util.utcnow() + timedelta(seconds=seconds) - async_fire_time_changed(hass, now) - - -@patch( - "homeassistant.components.zha.core.cluster_handlers.general.BasicClusterHandler.async_initialize", - new=mock.AsyncMock(), -) -async def test_check_available_success( - hass: HomeAssistant, device_with_basic_cluster_handler, zha_device_restored -) -> None: - """Check device availability success on 1st try.""" - zha_device = await zha_device_restored(device_with_basic_cluster_handler) - await async_enable_traffic(hass, [zha_device]) - basic_ch = device_with_basic_cluster_handler.endpoints[3].basic - - basic_ch.read_attributes.reset_mock() - device_with_basic_cluster_handler.last_seen = None - assert zha_device.available is True - _send_time_changed(hass, zha_device.consider_unavailable_time + 2) - await hass.async_block_till_done() - assert zha_device.available is False - assert basic_ch.read_attributes.await_count == 0 - - device_with_basic_cluster_handler.last_seen = ( - time.time() - zha_device.consider_unavailable_time - 2 - ) - _seens = [time.time(), device_with_basic_cluster_handler.last_seen] - - def _update_last_seen(*args, **kwargs): - device_with_basic_cluster_handler.last_seen = _seens.pop() - - basic_ch.read_attributes.side_effect = _update_last_seen - - # successfully ping zigpy device, but zha_device is not yet available - _send_time_changed(hass, 91) - await hass.async_block_till_done() - assert basic_ch.read_attributes.await_count == 1 - assert basic_ch.read_attributes.await_args[0][0] == ["manufacturer"] - assert zha_device.available is False - - # There was traffic from the device: pings, but not yet available - _send_time_changed(hass, 91) - await hass.async_block_till_done() - assert basic_ch.read_attributes.await_count == 2 - assert basic_ch.read_attributes.await_args[0][0] == ["manufacturer"] - assert zha_device.available is False - - # There was traffic from the device: don't try to ping, marked as available - _send_time_changed(hass, 91) - await hass.async_block_till_done() - assert basic_ch.read_attributes.await_count == 2 - assert basic_ch.read_attributes.await_args[0][0] == ["manufacturer"] - assert zha_device.available is True - - -@patch( - "homeassistant.components.zha.core.cluster_handlers.general.BasicClusterHandler.async_initialize", - new=mock.AsyncMock(), -) -async def test_check_available_unsuccessful( - hass: HomeAssistant, device_with_basic_cluster_handler, zha_device_restored -) -> None: - """Check device availability all tries fail.""" - - zha_device = await zha_device_restored(device_with_basic_cluster_handler) - await async_enable_traffic(hass, [zha_device]) - basic_ch = device_with_basic_cluster_handler.endpoints[3].basic - - assert zha_device.available is True - assert basic_ch.read_attributes.await_count == 0 - - device_with_basic_cluster_handler.last_seen = ( - time.time() - zha_device.consider_unavailable_time - 2 - ) - - # unsuccessfully ping zigpy device, but zha_device is still available - _send_time_changed(hass, 91) - await hass.async_block_till_done() - assert basic_ch.read_attributes.await_count == 1 - assert basic_ch.read_attributes.await_args[0][0] == ["manufacturer"] - assert zha_device.available is True - - # still no traffic, but zha_device is still available - _send_time_changed(hass, 91) - await hass.async_block_till_done() - assert basic_ch.read_attributes.await_count == 2 - assert basic_ch.read_attributes.await_args[0][0] == ["manufacturer"] - assert zha_device.available is True - - # not even trying to update, device is unavailable - _send_time_changed(hass, 91) - await hass.async_block_till_done() - assert basic_ch.read_attributes.await_count == 2 - assert basic_ch.read_attributes.await_args[0][0] == ["manufacturer"] - assert zha_device.available is False - - -@patch( - "homeassistant.components.zha.core.cluster_handlers.general.BasicClusterHandler.async_initialize", - new=mock.AsyncMock(), -) -async def test_check_available_no_basic_cluster_handler( - hass: HomeAssistant, - device_without_basic_cluster_handler, - zha_device_restored, - caplog: pytest.LogCaptureFixture, -) -> None: - """Check device availability for a device without basic cluster.""" - caplog.set_level(logging.DEBUG, logger="homeassistant.components.zha") - - zha_device = await zha_device_restored(device_without_basic_cluster_handler) - await async_enable_traffic(hass, [zha_device]) - - assert zha_device.available is True - - device_without_basic_cluster_handler.last_seen = ( - time.time() - zha_device.consider_unavailable_time - 2 - ) - - assert "does not have a mandatory basic cluster" not in caplog.text - _send_time_changed(hass, 91) - await hass.async_block_till_done() - assert zha_device.available is False - assert "does not have a mandatory basic cluster" in caplog.text - - -async def test_ota_sw_version( - hass: HomeAssistant, device_registry: dr.DeviceRegistry, ota_zha_device -) -> None: - """Test device entry gets sw_version updated via OTA cluster handler.""" - - ota_ch = ota_zha_device._endpoints[1].client_cluster_handlers["1:0x0019"] - entry = device_registry.async_get(ota_zha_device.device_id) - assert entry.sw_version is None - - cluster = ota_ch.cluster - hdr = make_zcl_header(1, global_command=False) - sw_version = 0x2345 - cluster.handle_message(hdr, [1, 2, 3, sw_version, None]) - await hass.async_block_till_done() - entry = device_registry.async_get(ota_zha_device.device_id) - assert int(entry.sw_version, base=16) == sw_version - - -@pytest.mark.parametrize( - ("device", "last_seen_delta", "is_available"), - [ - ("zigpy_device", 0, True), - ( - "zigpy_device", - CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS + 2, - True, - ), - ( - "zigpy_device", - CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY - 2, - True, - ), - ( - "zigpy_device", - CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY + 2, - False, - ), - ("zigpy_device_mains", 0, True), - ( - "zigpy_device_mains", - CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS - 2, - True, - ), - ( - "zigpy_device_mains", - CONF_DEFAULT_CONSIDER_UNAVAILABLE_MAINS + 2, - False, - ), - ( - "zigpy_device_mains", - CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY - 2, - False, - ), - ( - "zigpy_device_mains", - CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY + 2, - False, - ), - ], -) -async def test_device_restore_availability( - hass: HomeAssistant, - request: pytest.FixtureRequest, - device, - last_seen_delta, - is_available, - zha_device_restored, -) -> None: - """Test initial availability for restored devices.""" - - zigpy_device = request.getfixturevalue(device)() - zha_device = await zha_device_restored( - zigpy_device, last_seen=time.time() - last_seen_delta - ) - entity_id = "switch.fakemanufacturer_fakemodel_switch" - - await hass.async_block_till_done() - # ensure the switch entity was created - assert hass.states.get(entity_id).state is not None - assert zha_device.available is is_available - if is_available: - assert hass.states.get(entity_id).state == STATE_OFF - else: - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - -async def test_device_is_active_coordinator( - hass: HomeAssistant, zha_device_joined, zigpy_device -) -> None: - """Test that the current coordinator is uniquely detected.""" - - current_coord_dev = zigpy_device(ieee="aa:bb:cc:dd:ee:ff:00:11", nwk=0x0000) - current_coord_dev.node_desc = current_coord_dev.node_desc.replace( - logical_type=zdo_t.LogicalType.Coordinator - ) - - old_coord_dev = zigpy_device(ieee="aa:bb:cc:dd:ee:ff:00:12", nwk=0x0000) - old_coord_dev.node_desc = old_coord_dev.node_desc.replace( - logical_type=zdo_t.LogicalType.Coordinator - ) - - # The two coordinators have different IEEE addresses - assert current_coord_dev.ieee != old_coord_dev.ieee - - current_coordinator = await zha_device_joined(current_coord_dev) - stale_coordinator = await zha_device_joined(old_coord_dev) - - # Ensure the current ApplicationController's IEEE matches our coordinator's - current_coordinator.gateway.application_controller.state.node_info.ieee = ( - current_coord_dev.ieee - ) - - assert current_coordinator.is_active_coordinator - assert not stale_coordinator.is_active_coordinator diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index 13e9d789191..8bee821654d 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -1,23 +1,23 @@ """The test for ZHA device automation actions.""" -from unittest.mock import call, patch +from unittest.mock import patch import pytest from pytest_unordered import unordered -from zhaquirks.inovelli.VZM31SN import InovelliVZM31SNv11 -import zigpy.profiles.zha +from zigpy.profiles import zha from zigpy.zcl.clusters import general, security import zigpy.zcl.foundation as zcl_f from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.zha import DOMAIN +from homeassistant.components.zha.helpers import get_zha_gateway from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component -from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE +from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE from tests.common import async_get_device_automations, async_mock_service @@ -52,66 +52,37 @@ def required_platforms_only(): yield -@pytest.fixture -async def device_ias(hass, zigpy_device_mock, zha_device_joined_restored): - """IAS device fixture.""" +async def test_get_actions( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + entity_registry: er.EntityRegistry, + setup_zha, + zigpy_device_mock, +) -> None: + """Test we get the expected actions from a ZHA device.""" - clusters = [general.Basic, security.IasZone, security.IasWd] - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [c.cluster_id for c in clusters], - SIG_EP_OUTPUT: [general.OnOff.cluster_id], - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH, - } - }, - ) - - zha_device = await zha_device_joined_restored(zigpy_device) - zha_device.update_available(True) - await hass.async_block_till_done() - return zigpy_device, zha_device - - -@pytest.fixture -async def device_inovelli(hass, zigpy_device_mock, zha_device_joined): - """Inovelli device fixture.""" + await setup_zha() + gateway = get_zha_gateway(hass) zigpy_device = zigpy_device_mock( { 1: { SIG_EP_INPUT: [ general.Basic.cluster_id, - general.Identify.cluster_id, - general.OnOff.cluster_id, - general.LevelControl.cluster_id, - 0xFC31, + security.IasZone.cluster_id, + security.IasWd.cluster_id, ], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.DIMMABLE_LIGHT, + SIG_EP_OUTPUT: [general.OnOff.cluster_id], + SIG_EP_TYPE: zha.DeviceType.IAS_WARNING_DEVICE, + SIG_EP_PROFILE: zha.PROFILE_ID, } - }, - ieee="00:1d:8f:08:0c:90:69:6b", - manufacturer="Inovelli", - model="VZM31-SN", - quirk=InovelliVZM31SNv11, + } ) - zha_device = await zha_device_joined(zigpy_device) - zha_device.update_available(True) - await hass.async_block_till_done() - return zigpy_device, zha_device - - -async def test_get_actions( - hass: HomeAssistant, - device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, - device_ias, -) -> None: - """Test we get the expected actions from a ZHA device.""" - - ieee_address = str(device_ias[0].ieee) + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) + ieee_address = str(zigpy_device.ieee) reg_device = device_registry.async_get_device(identifiers={(DOMAIN, ieee_address)}) siren_level_select = entity_registry.async_get( @@ -168,112 +139,40 @@ async def test_get_actions( assert actions == unordered(expected_actions) -async def test_get_inovelli_actions( +async def test_action( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - entity_registry: er.EntityRegistry, - device_inovelli, -) -> None: - """Test we get the expected actions from a ZHA device.""" - - inovelli_ieee_address = str(device_inovelli[0].ieee) - inovelli_reg_device = device_registry.async_get_device( - identifiers={(DOMAIN, inovelli_ieee_address)} - ) - inovelli_button = entity_registry.async_get("button.inovelli_vzm31_sn_identify") - inovelli_light = entity_registry.async_get("light.inovelli_vzm31_sn_light") - - actions = await async_get_device_automations( - hass, DeviceAutomationType.ACTION, inovelli_reg_device.id - ) - - expected_actions = [ - { - "device_id": inovelli_reg_device.id, - "domain": DOMAIN, - "metadata": {}, - "type": "issue_all_led_effect", - }, - { - "device_id": inovelli_reg_device.id, - "domain": DOMAIN, - "metadata": {}, - "type": "issue_individual_led_effect", - }, - { - "device_id": inovelli_reg_device.id, - "domain": Platform.BUTTON, - "entity_id": inovelli_button.id, - "metadata": {"secondary": True}, - "type": "press", - }, - { - "device_id": inovelli_reg_device.id, - "domain": Platform.LIGHT, - "entity_id": inovelli_light.id, - "metadata": {"secondary": False}, - "type": "turn_off", - }, - { - "device_id": inovelli_reg_device.id, - "domain": Platform.LIGHT, - "entity_id": inovelli_light.id, - "metadata": {"secondary": False}, - "type": "turn_on", - }, - { - "device_id": inovelli_reg_device.id, - "domain": Platform.LIGHT, - "entity_id": inovelli_light.id, - "metadata": {"secondary": False}, - "type": "toggle", - }, - { - "device_id": inovelli_reg_device.id, - "domain": Platform.LIGHT, - "entity_id": inovelli_light.id, - "metadata": {"secondary": False}, - "type": "brightness_increase", - }, - { - "device_id": inovelli_reg_device.id, - "domain": Platform.LIGHT, - "entity_id": inovelli_light.id, - "metadata": {"secondary": False}, - "type": "brightness_decrease", - }, - { - "device_id": inovelli_reg_device.id, - "domain": Platform.LIGHT, - "entity_id": inovelli_light.id, - "metadata": {"secondary": False}, - "type": "flash", - }, - ] - - assert actions == unordered(expected_actions) - - -async def test_action( - hass: HomeAssistant, device_registry: dr.DeviceRegistry, device_ias, device_inovelli + setup_zha, + zigpy_device_mock, ) -> None: """Test for executing a ZHA device action.""" - zigpy_device, zha_device = device_ias - inovelli_zigpy_device, inovelli_zha_device = device_inovelli + await setup_zha() + gateway = get_zha_gateway(hass) + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [ + general.Basic.cluster_id, + security.IasZone.cluster_id, + security.IasWd.cluster_id, + ], + SIG_EP_OUTPUT: [general.OnOff.cluster_id], + SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, + SIG_EP_PROFILE: zha.PROFILE_ID, + } + } + ) zigpy_device.device_automation_triggers = { (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE} } - ieee_address = str(zha_device.ieee) - inovelli_ieee_address = str(inovelli_zha_device.ieee) + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) + ieee_address = str(zigpy_device.ieee) reg_device = device_registry.async_get_device(identifiers={(DOMAIN, ieee_address)}) - inovelli_reg_device = device_registry.async_get_device( - identifiers={(DOMAIN, inovelli_ieee_address)} - ) - - cluster = inovelli_zigpy_device.endpoints[1].in_clusters[0xFC31] with patch( "zigpy.zcl.Cluster.request", @@ -298,25 +197,6 @@ async def test_action( "device_id": reg_device.id, "type": "warn", }, - { - "domain": DOMAIN, - "device_id": inovelli_reg_device.id, - "type": "issue_all_led_effect", - "effect_type": "Open_Close", - "duration": 5, - "level": 10, - "color": 41, - }, - { - "domain": DOMAIN, - "device_id": inovelli_reg_device.id, - "type": "issue_individual_led_effect", - "effect_type": "Falling", - "led_number": 1, - "duration": 5, - "level": 10, - "color": 41, - }, ], } ] @@ -326,7 +206,11 @@ async def test_action( await hass.async_block_till_done() calls = async_mock_service(hass, DOMAIN, "warning_device_warn") - cluster_handler = zha_device.endpoints[1].client_cluster_handlers["1:0x0006"] + cluster_handler = ( + gateway.get_device(zigpy_device.ieee) + .endpoints[1] + .client_cluster_handlers["1:0x0006"] + ) cluster_handler.zha_send_event(COMMAND_SINGLE, []) await hass.async_block_till_done() @@ -335,44 +219,41 @@ async def test_action( assert calls[0].service == "warning_device_warn" assert calls[0].data["ieee"] == ieee_address - assert len(cluster.request.mock_calls) == 2 - assert ( - call( - False, - cluster.commands_by_name["led_effect"].id, - cluster.commands_by_name["led_effect"].schema, - 6, - 41, - 10, - 5, - expect_reply=False, - manufacturer=4151, - tsn=None, - ) - in cluster.request.call_args_list - ) - assert ( - call( - False, - cluster.commands_by_name["individual_led_effect"].id, - cluster.commands_by_name["individual_led_effect"].schema, - 1, - 6, - 41, - 10, - 5, - expect_reply=False, - manufacturer=4151, - tsn=None, - ) - in cluster.request.call_args_list - ) - -async def test_invalid_zha_event_type(hass: HomeAssistant, device_ias) -> None: +async def test_invalid_zha_event_type( + hass: HomeAssistant, setup_zha, zigpy_device_mock +) -> None: """Test that unexpected types are not passed to `zha_send_event`.""" - zigpy_device, zha_device = device_ias - cluster_handler = zha_device._endpoints[1].client_cluster_handlers["1:0x0006"] + await setup_zha() + gateway = get_zha_gateway(hass) + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [ + general.Basic.cluster_id, + security.IasZone.cluster_id, + security.IasWd.cluster_id, + ], + SIG_EP_OUTPUT: [general.OnOff.cluster_id], + SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, + SIG_EP_PROFILE: zha.PROFILE_ID, + } + } + ) + zigpy_device.device_automation_triggers = { + (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE} + } + + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) + + cluster_handler = ( + gateway.get_device(zigpy_device.ieee) + .endpoints[1] + .client_cluster_handlers["1:0x0006"] + ) # `zha_send_event` accepts only zigpy responses, lists, and dicts with pytest.raises(TypeError): diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index 64360c8b2ff..ae96de44f17 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -5,23 +5,22 @@ import time from unittest.mock import patch import pytest -import zigpy.profiles.zha +from zha.application.registries import SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE +from zigpy.profiles import zha from zigpy.zcl.clusters import general from homeassistant.components.device_tracker import SourceType -from homeassistant.components.zha.core.registries import ( - SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE, +from homeassistant.components.zha.helpers import ( + ZHADeviceProxy, + ZHAGatewayProxy, + get_zha_gateway, + get_zha_gateway_proxy, ) -from homeassistant.const import STATE_HOME, STATE_NOT_HOME, STATE_UNAVAILABLE, Platform +from homeassistant.const import STATE_HOME, STATE_NOT_HOME, Platform from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util -from .common import ( - async_enable_traffic, - async_test_rejoin, - find_entity_id, - send_attributes_report, -) +from .common import find_entity_id, send_attributes_report from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE from tests.common import async_fire_time_changed @@ -44,49 +43,41 @@ def device_tracker_platforms_only(): yield -@pytest.fixture -def zigpy_device_dt(zigpy_device_mock): - """Device tracker zigpy device.""" - endpoints = { - 1: { - SIG_EP_INPUT: [ - general.Basic.cluster_id, - general.PowerConfiguration.cluster_id, - general.Identify.cluster_id, - general.PollControl.cluster_id, - general.BinaryInput.cluster_id, - ], - SIG_EP_OUTPUT: [general.Identify.cluster_id, general.Ota.cluster_id], - SIG_EP_TYPE: SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE, - SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, - } - } - return zigpy_device_mock(endpoints) - - async def test_device_tracker( - hass: HomeAssistant, zha_device_joined_restored, zigpy_device_dt + hass: HomeAssistant, setup_zha, zigpy_device_mock ) -> None: """Test ZHA device tracker platform.""" - zha_device = await zha_device_joined_restored(zigpy_device_dt) - cluster = zigpy_device_dt.endpoints.get(1).power - entity_id = find_entity_id(Platform.DEVICE_TRACKER, zha_device, hass) + await setup_zha() + gateway = get_zha_gateway(hass) + gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass) + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [ + general.Basic.cluster_id, + general.PowerConfiguration.cluster_id, + general.Identify.cluster_id, + general.PollControl.cluster_id, + general.BinaryInput.cluster_id, + ], + SIG_EP_OUTPUT: [general.Identify.cluster_id, general.Ota.cluster_id], + SIG_EP_TYPE: SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE, + SIG_EP_PROFILE: zha.PROFILE_ID, + } + } + ) + + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) + + zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee) + entity_id = find_entity_id(Platform.DEVICE_TRACKER, zha_device_proxy, hass) + cluster = zigpy_device.endpoints[1].power assert entity_id is not None - assert hass.states.get(entity_id).state == STATE_NOT_HOME - await async_enable_traffic(hass, [zha_device], enabled=False) - # test that the device tracker was created and that it is unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - zigpy_device_dt.last_seen = time.time() - 120 - next_update = dt_util.utcnow() + timedelta(seconds=30) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - # test that the state has changed from unavailable to not home assert hass.states.get(entity_id).state == STATE_NOT_HOME @@ -95,7 +86,7 @@ async def test_device_tracker( hass, cluster, {0x0000: 0, 0x0020: 23, 0x0021: 200, 0x0001: 2} ) - zigpy_device_dt.last_seen = time.time() + 10 + zigpy_device.last_seen = time.time() + 10 next_update = dt_util.utcnow() + timedelta(seconds=30) async_fire_time_changed(hass, next_update) await hass.async_block_till_done() @@ -107,7 +98,3 @@ async def test_device_tracker( assert entity.is_connected is True assert entity.source_type == SourceType.ROUTER assert entity.battery_level == 100 - - # test adding device tracker to the network and HA - await async_test_rejoin(hass, zigpy_device_dt, [cluster], (2,)) - assert hass.states.get(entity_id).state == STATE_HOME diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index b43392af61a..da9ea26299a 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -1,32 +1,27 @@ """ZHA device automation trigger tests.""" -from datetime import timedelta -import time from unittest.mock import patch import pytest +from zha.application.const import ATTR_ENDPOINT_ID from zigpy.application import ControllerApplication +from zigpy.device import Device as ZigpyDevice import zigpy.profiles.zha -from zigpy.zcl.clusters import general +import zigpy.types from homeassistant.components import automation from homeassistant.components.device_automation import DeviceAutomationType from homeassistant.components.device_automation.exceptions import ( InvalidDeviceAutomationConfig, ) -from homeassistant.components.zha.core.const import ATTR_ENDPOINT_ID +from homeassistant.components.zha.helpers import get_zha_gateway from homeassistant.const import Platform from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component -import homeassistant.util.dt as dt_util - -from .common import async_enable_traffic -from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE from tests.common import ( MockConfigEntry, - async_fire_time_changed, async_get_device_automations, async_mock_service, ) @@ -51,16 +46,6 @@ LONG_PRESS = "remote_button_long_press" LONG_RELEASE = "remote_button_long_release" -SWITCH_SIGNATURE = { - 1: { - SIG_EP_INPUT: [general.Basic.cluster_id], - SIG_EP_OUTPUT: [general.OnOff.cluster_id], - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH, - SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, - } -} - - @pytest.fixture(autouse=True) def sensor_platforms_only(): """Only set up the sensor platform and required base platforms to speed up tests.""" @@ -81,25 +66,21 @@ def calls(hass: HomeAssistant) -> list[ServiceCall]: return async_mock_service(hass, "test", "automation") -@pytest.fixture -async def mock_devices(hass, zigpy_device_mock, zha_device_joined_restored): - """IAS device fixture.""" - - zigpy_device = zigpy_device_mock(SWITCH_SIGNATURE) - - zha_device = await zha_device_joined_restored(zigpy_device) - zha_device.update_available(True) - await hass.async_block_till_done() - return zigpy_device, zha_device - - async def test_triggers( - hass: HomeAssistant, device_registry: dr.DeviceRegistry, mock_devices + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + setup_zha, ) -> None: """Test ZHA device triggers.""" - zigpy_device, zha_device = mock_devices + await setup_zha() + gateway = get_zha_gateway(hass) + zigpy_device = ZigpyDevice( + application=gateway.application_controller, + ieee=zigpy.types.EUI64.convert("aa:bb:cc:dd:11:22:33:44"), + nwk=0x1234, + ) zigpy_device.device_automation_triggers = { (SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE}, (DOUBLE_PRESS, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE}, @@ -108,9 +89,13 @@ async def test_triggers( (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, } - ieee_address = str(zha_device.ieee) + zha_device = gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zha_device.device) + await hass.async_block_till_done(wait_background_tasks=True) - reg_device = device_registry.async_get_device(identifiers={("zha", ieee_address)}) + reg_device = device_registry.async_get_device( + identifiers={("zha", str(zha_device.ieee))} + ) triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, reg_device.id @@ -170,14 +155,26 @@ async def test_triggers( async def test_no_triggers( - hass: HomeAssistant, device_registry: dr.DeviceRegistry, mock_devices + hass: HomeAssistant, device_registry: dr.DeviceRegistry, setup_zha ) -> None: """Test ZHA device with no triggers.""" + await setup_zha() + gateway = get_zha_gateway(hass) - _, zha_device = mock_devices - ieee_address = str(zha_device.ieee) + zigpy_device = ZigpyDevice( + application=gateway.application_controller, + ieee=zigpy.types.EUI64.convert("aa:bb:cc:dd:11:22:33:44"), + nwk=0x1234, + ) + zigpy_device.device_automation_triggers = {} - reg_device = device_registry.async_get_device(identifiers={("zha", ieee_address)}) + zha_device = gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zha_device.device) + await hass.async_block_till_done(wait_background_tasks=True) + + reg_device = device_registry.async_get_device( + identifiers={("zha", str(zha_device.ieee))} + ) triggers = await async_get_device_automations( hass, DeviceAutomationType.TRIGGER, reg_device.id @@ -197,12 +194,21 @@ async def test_no_triggers( async def test_if_fires_on_event( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - mock_devices, calls: list[ServiceCall], + setup_zha, ) -> None: """Test for remote triggers firing.""" - zigpy_device, zha_device = mock_devices + await setup_zha() + gateway = get_zha_gateway(hass) + + zigpy_device = ZigpyDevice( + application=gateway.application_controller, + ieee=zigpy.types.EUI64.convert("aa:bb:cc:dd:11:22:33:44"), + nwk=0x1234, + ) + ep = zigpy_device.add_endpoint(1) + ep.add_output_cluster(0x0006) zigpy_device.device_automation_triggers = { (SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE}, @@ -212,8 +218,13 @@ async def test_if_fires_on_event( (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, } - ieee_address = str(zha_device.ieee) - reg_device = device_registry.async_get_device(identifiers={("zha", ieee_address)}) + zha_device = gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zha_device.device) + await hass.async_block_till_done(wait_background_tasks=True) + + reg_device = device_registry.async_get_device( + identifiers={("zha", str(zha_device.ieee))} + ) assert await async_setup_component( hass, @@ -239,8 +250,16 @@ async def test_if_fires_on_event( await hass.async_block_till_done() - cluster_handler = zha_device.endpoints[1].client_cluster_handlers["1:0x0006"] - cluster_handler.zha_send_event(COMMAND_SINGLE, []) + zha_device.emit_zha_event( + { + "unique_id": f"{zha_device.ieee}:1:0x0006", + "endpoint_id": 1, + "cluster_id": 0x0006, + "command": COMMAND_SINGLE, + "args": [], + "params": {}, + }, + ) await hass.async_block_till_done() assert len(calls) == 1 @@ -249,25 +268,28 @@ async def test_if_fires_on_event( async def test_device_offline_fires( hass: HomeAssistant, - zigpy_device_mock, - zha_device_restored, + device_registry: dr.DeviceRegistry, calls: list[ServiceCall], + setup_zha, ) -> None: """Test for device offline triggers firing.""" - zigpy_device = zigpy_device_mock( - { - 1: { - "in_clusters": [general.Basic.cluster_id], - "out_clusters": [general.OnOff.cluster_id], - "device_type": 0, - } - } + await setup_zha() + gateway = get_zha_gateway(hass) + + zigpy_device = ZigpyDevice( + application=gateway.application_controller, + ieee=zigpy.types.EUI64.convert("aa:bb:cc:dd:11:22:33:44"), + nwk=0x1234, ) - zha_device = await zha_device_restored(zigpy_device, last_seen=time.time()) - await async_enable_traffic(hass, [zha_device]) - await hass.async_block_till_done() + zha_device = gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zha_device.device) + await hass.async_block_till_done(wait_background_tasks=True) + + reg_device = device_registry.async_get_device( + identifiers={("zha", str(zha_device.ieee))} + ) assert await async_setup_component( hass, @@ -276,7 +298,7 @@ async def test_device_offline_fires( automation.DOMAIN: [ { "trigger": { - "device_id": zha_device.device_id, + "device_id": reg_device.id, "domain": "zha", "platform": "device", "type": "device_offline", @@ -291,27 +313,10 @@ async def test_device_offline_fires( }, ) - await hass.async_block_till_done() assert zha_device.available is True + zha_device.available = False + zha_device.emit_zha_event({"device_event_type": "device_offline"}) - zigpy_device.last_seen = time.time() - zha_device.consider_unavailable_time - 2 - - # there are 3 checkins to perform before marking the device unavailable - future = dt_util.utcnow() + timedelta(seconds=90) - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - future = dt_util.utcnow() + timedelta(seconds=90) - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - future = dt_util.utcnow() + timedelta( - seconds=zha_device.consider_unavailable_time + 100 - ) - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - assert zha_device.available is False assert len(calls) == 1 assert calls[0].data["message"] == "service called" @@ -319,16 +324,28 @@ async def test_device_offline_fires( async def test_exception_no_triggers( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - mock_devices, calls: list[ServiceCall], caplog: pytest.LogCaptureFixture, + setup_zha, ) -> None: """Test for exception when validating device triggers.""" - _, zha_device = mock_devices + await setup_zha() + gateway = get_zha_gateway(hass) - ieee_address = str(zha_device.ieee) - reg_device = device_registry.async_get_device(identifiers={("zha", ieee_address)}) + zigpy_device = ZigpyDevice( + application=gateway.application_controller, + ieee=zigpy.types.EUI64.convert("aa:bb:cc:dd:11:22:33:44"), + nwk=0x1234, + ) + + zha_device = gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zha_device.device) + await hass.async_block_till_done(wait_background_tasks=True) + + reg_device = device_registry.async_get_device( + identifiers={("zha", str(zha_device.ieee))} + ) await async_setup_component( hass, @@ -361,14 +378,20 @@ async def test_exception_no_triggers( async def test_exception_bad_trigger( hass: HomeAssistant, device_registry: dr.DeviceRegistry, - mock_devices, calls: list[ServiceCall], caplog: pytest.LogCaptureFixture, + setup_zha, ) -> None: """Test for exception when validating device triggers.""" - zigpy_device, zha_device = mock_devices + await setup_zha() + gateway = get_zha_gateway(hass) + zigpy_device = ZigpyDevice( + application=gateway.application_controller, + ieee=zigpy.types.EUI64.convert("aa:bb:cc:dd:11:22:33:44"), + nwk=0x1234, + ) zigpy_device.device_automation_triggers = { (SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE}, (DOUBLE_PRESS, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE}, @@ -377,8 +400,13 @@ async def test_exception_bad_trigger( (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, } - ieee_address = str(zha_device.ieee) - reg_device = device_registry.async_get_device(identifiers={("zha", ieee_address)}) + zha_device = gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zha_device.device) + await hass.async_block_till_done(wait_background_tasks=True) + + reg_device = device_registry.async_get_device( + identifiers={("zha", str(zha_device.ieee))} + ) await async_setup_component( hass, @@ -412,23 +440,37 @@ async def test_validate_trigger_config_missing_info( hass: HomeAssistant, device_registry: dr.DeviceRegistry, config_entry: MockConfigEntry, - zigpy_device_mock, - mock_zigpy_connect: ControllerApplication, - zha_device_joined, caplog: pytest.LogCaptureFixture, + setup_zha, ) -> None: """Test device triggers referring to a missing device.""" - # Join a device - switch = zigpy_device_mock(SWITCH_SIGNATURE) - await zha_device_joined(switch) + await setup_zha() + gateway = get_zha_gateway(hass) + + zigpy_device = ZigpyDevice( + application=gateway.application_controller, + ieee=zigpy.types.EUI64.convert("aa:bb:cc:dd:11:22:33:44"), + nwk=0x1234, + ) + zigpy_device.device_automation_triggers = { + (SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE}, + (DOUBLE_PRESS, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE}, + (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE}, + (LONG_PRESS, LONG_PRESS): {COMMAND: COMMAND_HOLD}, + (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, + } + + zha_device = gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zha_device.device) + await hass.async_block_till_done(wait_background_tasks=True) # After we unload the config entry, trigger info was not cached on startup, nor can # it be pulled from the current device, making it impossible to validate triggers await hass.config_entries.async_unload(config_entry.entry_id) reg_device = device_registry.async_get_device( - identifiers={("zha", str(switch.ieee))} + identifiers={("zha", str(zha_device.ieee))} ) assert await async_setup_component( @@ -465,16 +507,32 @@ async def test_validate_trigger_config_unloaded_bad_info( hass: HomeAssistant, device_registry: dr.DeviceRegistry, config_entry: MockConfigEntry, - zigpy_device_mock, - mock_zigpy_connect: ControllerApplication, - zha_device_joined, caplog: pytest.LogCaptureFixture, + zigpy_app_controller: ControllerApplication, + setup_zha, ) -> None: """Test device triggers referring to a missing device.""" - # Join a device - switch = zigpy_device_mock(SWITCH_SIGNATURE) - await zha_device_joined(switch) + await setup_zha() + gateway = get_zha_gateway(hass) + + zigpy_device = ZigpyDevice( + application=gateway.application_controller, + ieee=zigpy.types.EUI64.convert("aa:bb:cc:dd:11:22:33:44"), + nwk=0x1234, + ) + zigpy_device.device_automation_triggers = { + (SHAKEN, SHAKEN): {COMMAND: COMMAND_SHAKE}, + (DOUBLE_PRESS, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE}, + (SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE}, + (LONG_PRESS, LONG_PRESS): {COMMAND: COMMAND_HOLD}, + (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, + } + + zigpy_app_controller.devices[zigpy_device.ieee] = zigpy_device + zha_device = gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zha_device.device) + await hass.async_block_till_done(wait_background_tasks=True) # After we unload the config entry, trigger info was not cached on startup, nor can # it be pulled from the current device, making it impossible to validate triggers @@ -482,11 +540,12 @@ async def test_validate_trigger_config_unloaded_bad_info( # Reload ZHA to persist the device info in the cache await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() + await hass.async_block_till_done(wait_background_tasks=True) + await hass.config_entries.async_unload(config_entry.entry_id) reg_device = device_registry.async_get_device( - identifiers={("zha", str(switch.ieee))} + identifiers={("zha", str(zha_device.ieee))} ) assert await async_setup_component( diff --git a/tests/components/zha/test_diagnostics.py b/tests/components/zha/test_diagnostics.py index 4bb30a5fc8c..bbdc6271207 100644 --- a/tests/components/zha/test_diagnostics.py +++ b/tests/components/zha/test_diagnostics.py @@ -7,9 +7,13 @@ from zigpy.profiles import zha from zigpy.zcl.clusters import security from homeassistant.components.diagnostics import REDACTED -from homeassistant.components.zha.core.device import ZHADevice -from homeassistant.components.zha.core.helpers import get_zha_gateway from homeassistant.components.zha.diagnostics import KEYS_TO_REDACT +from homeassistant.components.zha.helpers import ( + ZHADeviceProxy, + ZHAGatewayProxy, + get_zha_gateway, + get_zha_gateway_proxy, +) from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -41,33 +45,35 @@ def required_platforms_only(): yield -@pytest.fixture -def zigpy_device(zigpy_device_mock): - """Device tracker zigpy device.""" - endpoints = { - 1: { - SIG_EP_INPUT: [security.IasAce.cluster_id, security.IasZone.cluster_id], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.IAS_ANCILLARY_CONTROL, - SIG_EP_PROFILE: zha.PROFILE_ID, - } - } - return zigpy_device_mock( - endpoints, node_descriptor=b"\x02@\x8c\x02\x10RR\x00\x00\x00R\x00\x00" - ) - - async def test_diagnostics_for_config_entry( hass: HomeAssistant, hass_client: ClientSessionGenerator, config_entry: MockConfigEntry, - zha_device_joined, - zigpy_device, + setup_zha, + zigpy_device_mock, ) -> None: """Test diagnostics for config entry.""" - await zha_device_joined(zigpy_device) + await setup_zha() gateway = get_zha_gateway(hass) + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [security.IasAce.cluster_id, security.IasZone.cluster_id], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zha.DeviceType.IAS_ANCILLARY_CONTROL, + SIG_EP_PROFILE: zha.PROFILE_ID, + } + }, + ieee="01:2d:6f:00:0a:90:69:e8", + node_descriptor=b"\x02@\x8c\x02\x10RR\x00\x00\x00R\x00\x00", + ) + + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) + scan = {c: c for c in range(11, 26 + 1)} with patch.object(gateway.application_controller, "energy_scan", return_value=scan): @@ -106,19 +112,40 @@ async def test_diagnostics_for_device( hass_client: ClientSessionGenerator, device_registry: dr.DeviceRegistry, config_entry: MockConfigEntry, - zha_device_joined, - zigpy_device, + setup_zha, + zigpy_device_mock, ) -> None: """Test diagnostics for device.""" - zha_device: ZHADevice = await zha_device_joined(zigpy_device) + await setup_zha() + gateway = get_zha_gateway(hass) + gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass) + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [security.IasAce.cluster_id, security.IasZone.cluster_id], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zha.DeviceType.IAS_ANCILLARY_CONTROL, + SIG_EP_PROFILE: zha.PROFILE_ID, + } + }, + ieee="01:2d:6f:00:0a:90:69:e8", + node_descriptor=b"\x02@\x8c\x02\x10RR\x00\x00\x00R\x00\x00", + ) + + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) + + zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee) # add unknown unsupported attribute with id and name - zha_device.device.endpoints[1].in_clusters[ + zha_device_proxy.device.device.endpoints[1].in_clusters[ security.IasAce.cluster_id ].unsupported_attributes.update({0x1000, "unknown_attribute_name"}) # add known unsupported attributes with id and name - zha_device.device.endpoints[1].in_clusters[ + zha_device_proxy.device.device.endpoints[1].in_clusters[ security.IasZone.cluster_id ].unsupported_attributes.update( { @@ -128,14 +155,14 @@ async def test_diagnostics_for_device( ) device = device_registry.async_get_device( - identifiers={("zha", str(zha_device.ieee))} + identifiers={("zha", str(zha_device_proxy.device.ieee))} ) assert device diagnostics_data = await get_diagnostics_for_device( hass, hass_client, config_entry, device ) assert diagnostics_data - device_info: dict = zha_device.zha_device_info + device_info: dict = zha_device_proxy.zha_device_info for key in device_info: assert key in diagnostics_data if key not in KEYS_TO_REDACT: diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py deleted file mode 100644 index c59acc3395f..00000000000 --- a/tests/components/zha/test_discover.py +++ /dev/null @@ -1,1100 +0,0 @@ -"""Test ZHA device discovery.""" - -from collections.abc import Callable -import enum -import itertools -import re -from typing import Any -from unittest import mock -from unittest.mock import AsyncMock, Mock, patch - -import pytest -from zhaquirks.ikea import PowerConfig1CRCluster, ScenesCluster -from zhaquirks.xiaomi import ( - BasicCluster, - LocalIlluminanceMeasurementCluster, - XiaomiPowerConfigurationPercent, -) -from zhaquirks.xiaomi.aqara.driver_curtain_e1 import ( - WindowCoveringE1, - XiaomiAqaraDriverE1, -) -from zigpy.const import SIG_ENDPOINTS, SIG_MANUFACTURER, SIG_MODEL, SIG_NODE_DESC -import zigpy.profiles.zha -import zigpy.quirks -from zigpy.quirks.v2 import ( - BinarySensorMetadata, - EntityMetadata, - EntityType, - NumberMetadata, - QuirksV2RegistryEntry, - ZCLCommandButtonMetadata, - ZCLSensorMetadata, - add_to_registry_v2, -) -from zigpy.quirks.v2.homeassistant import UnitOfTime -import zigpy.types -from zigpy.zcl import ClusterType -import zigpy.zcl.clusters.closures -import zigpy.zcl.clusters.general -import zigpy.zcl.clusters.security -import zigpy.zcl.foundation as zcl_f - -from homeassistant.components.zha.core import cluster_handlers -import homeassistant.components.zha.core.const as zha_const -from homeassistant.components.zha.core.device import ZHADevice -import homeassistant.components.zha.core.discovery as disc -from homeassistant.components.zha.core.endpoint import Endpoint -from homeassistant.components.zha.core.helpers import get_zha_gateway -import homeassistant.components.zha.core.registries as zha_regs -from homeassistant.const import STATE_OFF, Platform -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity_platform import EntityPlatform -from homeassistant.util.json import load_json - -from .common import find_entity_id, update_attribute_cache -from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE -from .zha_devices_list import ( - DEV_SIG_ATTRIBUTES, - DEV_SIG_CLUSTER_HANDLERS, - DEV_SIG_ENT_MAP, - DEV_SIG_ENT_MAP_CLASS, - DEV_SIG_ENT_MAP_ID, - DEV_SIG_EVT_CLUSTER_HANDLERS, - DEVICES, -) - -NO_TAIL_ID = re.compile("_\\d$") -UNIQUE_ID_HD = re.compile(r"^(([\da-fA-F]{2}:){7}[\da-fA-F]{2}-\d{1,3})", re.X) - -IGNORE_SUFFIXES = [ - zigpy.zcl.clusters.general.OnOff.StartUpOnOff.__name__, - "on_off_transition_time", - "on_level", - "on_transition_time", - "off_transition_time", - "default_move_rate", - "start_up_current_level", - "counter", -] - - -def contains_ignored_suffix(unique_id: str) -> bool: - """Return true if the unique_id ends with an ignored suffix.""" - return any(suffix.lower() in unique_id.lower() for suffix in IGNORE_SUFFIXES) - - -@patch( - "zigpy.zcl.clusters.general.Identify.request", - new=AsyncMock(return_value=[mock.sentinel.data, zcl_f.Status.SUCCESS]), -) -# We do this here because we are testing ZHA discovery logic. Point being we want to ensure that -# all discovered entities are dispatched for creation. In order to test this we need the entities -# added to HA. So we ensure that they are all enabled even though they won't necessarily be in reality -# at runtime -@patch( - "homeassistant.components.zha.entity.ZhaEntity.entity_registry_enabled_default", - new=Mock(return_value=True), -) -@pytest.mark.parametrize("device", DEVICES) -async def test_devices( - device, - hass_disable_services, - zigpy_device_mock, - zha_device_joined_restored, -) -> None: - """Test device discovery.""" - zigpy_device = zigpy_device_mock( - endpoints=device[SIG_ENDPOINTS], - ieee="00:11:22:33:44:55:66:77", - manufacturer=device[SIG_MANUFACTURER], - model=device[SIG_MODEL], - node_descriptor=device[SIG_NODE_DESC], - attributes=device.get(DEV_SIG_ATTRIBUTES), - patch_cluster=False, - ) - - cluster_identify = _get_first_identify_cluster(zigpy_device) - if cluster_identify: - cluster_identify.request.reset_mock() - - with patch( - "homeassistant.helpers.entity_platform.EntityPlatform._async_schedule_add_entities_for_entry", - side_effect=EntityPlatform._async_schedule_add_entities_for_entry, - autospec=True, - ) as mock_add_entities: - zha_dev = await zha_device_joined_restored(zigpy_device) - await hass_disable_services.async_block_till_done() - - if cluster_identify: - # We only identify on join - should_identify = ( - zha_device_joined_restored.name == "zha_device_joined" - and not zigpy_device.skip_configuration - ) - - if should_identify: - assert cluster_identify.request.mock_calls == [ - mock.call( - False, - cluster_identify.commands_by_name["trigger_effect"].id, - cluster_identify.commands_by_name["trigger_effect"].schema, - effect_id=zigpy.zcl.clusters.general.Identify.EffectIdentifier.Okay, - effect_variant=( - zigpy.zcl.clusters.general.Identify.EffectVariant.Default - ), - expect_reply=True, - manufacturer=None, - tsn=None, - ) - ] - else: - assert cluster_identify.request.mock_calls == [] - - event_cluster_handlers = { - ch.id - for endpoint in zha_dev._endpoints.values() - for ch in endpoint.client_cluster_handlers.values() - } - assert event_cluster_handlers == set(device[DEV_SIG_EVT_CLUSTER_HANDLERS]) - - # Keep track of unhandled entities: they should always be ones we explicitly ignore - created_entities = { - entity.entity_id: entity - for mock_call in mock_add_entities.mock_calls - for entity in mock_call.args[1] - } - unhandled_entities = set(created_entities.keys()) - entity_registry = er.async_get(hass_disable_services) - - for (platform, unique_id), ent_info in device[DEV_SIG_ENT_MAP].items(): - no_tail_id = NO_TAIL_ID.sub("", ent_info[DEV_SIG_ENT_MAP_ID]) - ha_entity_id = entity_registry.async_get_entity_id(platform, "zha", unique_id) - message1 = f"No entity found for platform[{platform}] unique_id[{unique_id}]" - message2 = f"no_tail_id[{no_tail_id}] with entity_id[{ha_entity_id}]" - assert ha_entity_id is not None, f"{message1} {message2}" - assert ha_entity_id.startswith(no_tail_id) - - entity = created_entities[ha_entity_id] - unhandled_entities.remove(ha_entity_id) - - assert entity.platform.domain == platform - assert type(entity).__name__ == ent_info[DEV_SIG_ENT_MAP_CLASS] - # unique_id used for discover is the same for "multi entities" - assert unique_id == entity.unique_id - assert {ch.name for ch in entity.cluster_handlers.values()} == set( - ent_info[DEV_SIG_CLUSTER_HANDLERS] - ) - - # All unhandled entities should be ones we explicitly ignore - for entity_id in unhandled_entities: - domain = entity_id.split(".")[0] - assert domain in zha_const.PLATFORMS - assert contains_ignored_suffix(entity_id) - - -def _get_first_identify_cluster(zigpy_device): - for endpoint in list(zigpy_device.endpoints.values())[1:]: - if hasattr(endpoint, "identify"): - return endpoint.identify - - -@mock.patch( - "homeassistant.components.zha.core.discovery.ProbeEndpoint.discover_by_device_type" -) -@mock.patch( - "homeassistant.components.zha.core.discovery.ProbeEndpoint.discover_by_cluster_id" -) -def test_discover_entities(m1, m2) -> None: - """Test discover endpoint class method.""" - endpoint = mock.MagicMock() - disc.PROBE.discover_entities(endpoint) - assert m1.call_count == 1 - assert m1.call_args[0][0] is endpoint - assert m2.call_count == 1 - assert m2.call_args[0][0] is endpoint - - -@pytest.mark.parametrize( - ("device_type", "platform", "hit"), - [ - (zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, Platform.LIGHT, True), - (zigpy.profiles.zha.DeviceType.ON_OFF_BALLAST, Platform.SWITCH, True), - (zigpy.profiles.zha.DeviceType.SMART_PLUG, Platform.SWITCH, True), - (0xFFFF, None, False), - ], -) -def test_discover_by_device_type(device_type, platform, hit) -> None: - """Test entity discovery by device type.""" - - endpoint = mock.MagicMock(spec_set=Endpoint) - ep_mock = mock.PropertyMock() - ep_mock.return_value.profile_id = 0x0104 - ep_mock.return_value.device_type = device_type - type(endpoint).zigpy_endpoint = ep_mock - - get_entity_mock = mock.MagicMock( - return_value=(mock.sentinel.entity_cls, mock.sentinel.claimed) - ) - with mock.patch( - "homeassistant.components.zha.core.registries.ZHA_ENTITIES.get_entity", - get_entity_mock, - ): - disc.PROBE.discover_by_device_type(endpoint) - if hit: - assert get_entity_mock.call_count == 1 - assert endpoint.claim_cluster_handlers.call_count == 1 - assert endpoint.claim_cluster_handlers.call_args[0][0] is mock.sentinel.claimed - assert endpoint.async_new_entity.call_count == 1 - assert endpoint.async_new_entity.call_args[0][0] == platform - assert endpoint.async_new_entity.call_args[0][1] == mock.sentinel.entity_cls - - -def test_discover_by_device_type_override() -> None: - """Test entity discovery by device type overriding.""" - - endpoint = mock.MagicMock(spec_set=Endpoint) - ep_mock = mock.PropertyMock() - ep_mock.return_value.profile_id = 0x0104 - ep_mock.return_value.device_type = 0x0100 - type(endpoint).zigpy_endpoint = ep_mock - - overrides = {endpoint.unique_id: {"type": Platform.SWITCH}} - get_entity_mock = mock.MagicMock( - return_value=(mock.sentinel.entity_cls, mock.sentinel.claimed) - ) - with ( - mock.patch( - "homeassistant.components.zha.core.registries.ZHA_ENTITIES.get_entity", - get_entity_mock, - ), - mock.patch.dict(disc.PROBE._device_configs, overrides, clear=True), - ): - disc.PROBE.discover_by_device_type(endpoint) - assert get_entity_mock.call_count == 1 - assert endpoint.claim_cluster_handlers.call_count == 1 - assert endpoint.claim_cluster_handlers.call_args[0][0] is mock.sentinel.claimed - assert endpoint.async_new_entity.call_count == 1 - assert endpoint.async_new_entity.call_args[0][0] == Platform.SWITCH - assert endpoint.async_new_entity.call_args[0][1] == mock.sentinel.entity_cls - - -def test_discover_probe_single_cluster() -> None: - """Test entity discovery by single cluster.""" - - endpoint = mock.MagicMock(spec_set=Endpoint) - ep_mock = mock.PropertyMock() - ep_mock.return_value.profile_id = 0x0104 - ep_mock.return_value.device_type = 0x0100 - type(endpoint).zigpy_endpoint = ep_mock - - get_entity_mock = mock.MagicMock( - return_value=(mock.sentinel.entity_cls, mock.sentinel.claimed) - ) - cluster_handler_mock = mock.MagicMock(spec_set=cluster_handlers.ClusterHandler) - with mock.patch( - "homeassistant.components.zha.core.registries.ZHA_ENTITIES.get_entity", - get_entity_mock, - ): - disc.PROBE.probe_single_cluster(Platform.SWITCH, cluster_handler_mock, endpoint) - - assert get_entity_mock.call_count == 1 - assert endpoint.claim_cluster_handlers.call_count == 1 - assert endpoint.claim_cluster_handlers.call_args[0][0] is mock.sentinel.claimed - assert endpoint.async_new_entity.call_count == 1 - assert endpoint.async_new_entity.call_args[0][0] == Platform.SWITCH - assert endpoint.async_new_entity.call_args[0][1] == mock.sentinel.entity_cls - assert endpoint.async_new_entity.call_args[0][3] == mock.sentinel.claimed - - -@pytest.mark.parametrize("device_info", DEVICES) -async def test_discover_endpoint( - device_info: dict[str, Any], - zha_device_mock: Callable[..., ZHADevice], - hass: HomeAssistant, -) -> None: - """Test device discovery.""" - - with mock.patch( - "homeassistant.components.zha.core.endpoint.Endpoint.async_new_entity" - ) as new_ent: - device = zha_device_mock( - device_info[SIG_ENDPOINTS], - manufacturer=device_info[SIG_MANUFACTURER], - model=device_info[SIG_MODEL], - node_desc=device_info[SIG_NODE_DESC], - patch_cluster=True, - ) - - assert device_info[DEV_SIG_EVT_CLUSTER_HANDLERS] == sorted( - ch.id - for endpoint in device._endpoints.values() - for ch in endpoint.client_cluster_handlers.values() - ) - - # build a dict of entity_class -> (platform, unique_id, cluster_handlers) tuple - ha_ent_info = {} - for call in new_ent.call_args_list: - platform, entity_cls, unique_id, cluster_handlers = call[0] - if not contains_ignored_suffix(unique_id): - unique_id_head = UNIQUE_ID_HD.match(unique_id).group( - 0 - ) # ieee + endpoint_id - ha_ent_info[(unique_id_head, entity_cls.__name__)] = ( - platform, - unique_id, - cluster_handlers, - ) - - for platform_id, ent_info in device_info[DEV_SIG_ENT_MAP].items(): - platform, unique_id = platform_id - - test_ent_class = ent_info[DEV_SIG_ENT_MAP_CLASS] - test_unique_id_head = UNIQUE_ID_HD.match(unique_id).group(0) - assert (test_unique_id_head, test_ent_class) in ha_ent_info - - entity_platform, entity_unique_id, entity_cluster_handlers = ha_ent_info[ - (test_unique_id_head, test_ent_class) - ] - assert platform is entity_platform.value - # unique_id used for discover is the same for "multi entities" - assert unique_id.startswith(entity_unique_id) - assert {ch.name for ch in entity_cluster_handlers} == set( - ent_info[DEV_SIG_CLUSTER_HANDLERS] - ) - - device.async_cleanup_handles() - - -def _ch_mock(cluster): - """Return mock of a cluster_handler with a cluster.""" - cluster_handler = mock.MagicMock() - type(cluster_handler).cluster = mock.PropertyMock( - return_value=cluster(mock.MagicMock()) - ) - return cluster_handler - - -@mock.patch( - ( - "homeassistant.components.zha.core.discovery.ProbeEndpoint" - ".handle_on_off_output_cluster_exception" - ), - new=mock.MagicMock(), -) -@mock.patch( - "homeassistant.components.zha.core.discovery.ProbeEndpoint.probe_single_cluster" -) -def _test_single_input_cluster_device_class(probe_mock): - """Test SINGLE_INPUT_CLUSTER_DEVICE_CLASS matching by cluster id or class.""" - - door_ch = _ch_mock(zigpy.zcl.clusters.closures.DoorLock) - cover_ch = _ch_mock(zigpy.zcl.clusters.closures.WindowCovering) - multistate_ch = _ch_mock(zigpy.zcl.clusters.general.MultistateInput) - - class QuirkedIAS(zigpy.quirks.CustomCluster, zigpy.zcl.clusters.security.IasZone): - pass - - ias_ch = _ch_mock(QuirkedIAS) - - class _Analog(zigpy.quirks.CustomCluster, zigpy.zcl.clusters.general.AnalogInput): - pass - - analog_ch = _ch_mock(_Analog) - - endpoint = mock.MagicMock(spec_set=Endpoint) - endpoint.unclaimed_cluster_handlers.return_value = [ - door_ch, - cover_ch, - multistate_ch, - ias_ch, - ] - - disc.ProbeEndpoint().discover_by_cluster_id(endpoint) - assert probe_mock.call_count == len(endpoint.unclaimed_cluster_handlers()) - probes = ( - (Platform.LOCK, door_ch), - (Platform.COVER, cover_ch), - (Platform.SENSOR, multistate_ch), - (Platform.BINARY_SENSOR, ias_ch), - (Platform.SENSOR, analog_ch), - ) - for call, details in zip(probe_mock.call_args_list, probes, strict=False): - platform, ch = details - assert call[0][0] == platform - assert call[0][1] == ch - - -def test_single_input_cluster_device_class_by_cluster_class() -> None: - """Test SINGLE_INPUT_CLUSTER_DEVICE_CLASS matching by cluster id or class.""" - mock_reg = { - zigpy.zcl.clusters.closures.DoorLock.cluster_id: Platform.LOCK, - zigpy.zcl.clusters.closures.WindowCovering.cluster_id: Platform.COVER, - zigpy.zcl.clusters.general.AnalogInput: Platform.SENSOR, - zigpy.zcl.clusters.general.MultistateInput: Platform.SENSOR, - zigpy.zcl.clusters.security.IasZone: Platform.BINARY_SENSOR, - } - - with mock.patch.dict( - zha_regs.SINGLE_INPUT_CLUSTER_DEVICE_CLASS, mock_reg, clear=True - ): - _test_single_input_cluster_device_class() - - -@pytest.mark.parametrize( - ("override", "entity_id"), - [ - (None, "light.manufacturer_model_light"), - ("switch", "switch.manufacturer_model_switch"), - ], -) -async def test_device_override( - hass_disable_services, zigpy_device_mock, setup_zha, override, entity_id -) -> None: - """Test device discovery override.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.COLOR_DIMMABLE_LIGHT, - "endpoint_id": 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 768, 2821, 64513], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - } - }, - "00:11:22:33:44:55:66:77", - "manufacturer", - "model", - patch_cluster=False, - ) - - if override is not None: - override = {"device_config": {"00:11:22:33:44:55:66:77-1": {"type": override}}} - - await setup_zha(override) - assert hass_disable_services.states.get(entity_id) is None - zha_gateway = get_zha_gateway(hass_disable_services) - await zha_gateway.async_device_initialized(zigpy_device) - await hass_disable_services.async_block_till_done() - assert hass_disable_services.states.get(entity_id) is not None - - -async def test_group_probe_cleanup_called( - hass_disable_services, setup_zha, config_entry -) -> None: - """Test cleanup happens when ZHA is unloaded.""" - await setup_zha() - disc.GROUP_PROBE.cleanup = mock.Mock(wraps=disc.GROUP_PROBE.cleanup) - await hass_disable_services.config_entries.async_unload(config_entry.entry_id) - await hass_disable_services.async_block_till_done() - disc.GROUP_PROBE.cleanup.assert_called() - - -async def test_quirks_v2_entity_discovery( - hass: HomeAssistant, - zigpy_device_mock, - zha_device_joined, -) -> None: - """Test quirks v2 discovery.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [ - zigpy.zcl.clusters.general.PowerConfiguration.cluster_id, - zigpy.zcl.clusters.general.Groups.cluster_id, - zigpy.zcl.clusters.general.OnOff.cluster_id, - ], - SIG_EP_OUTPUT: [ - zigpy.zcl.clusters.general.Scenes.cluster_id, - ], - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.NON_COLOR_CONTROLLER, - } - }, - ieee="01:2d:6f:00:0a:90:69:e8", - manufacturer="Ikea of Sweden", - model="TRADFRI remote control", - ) - - ( - add_to_registry_v2( - "Ikea of Sweden", "TRADFRI remote control", zigpy.quirks._DEVICE_REGISTRY - ) - .replaces(PowerConfig1CRCluster) - .replaces(ScenesCluster, cluster_type=ClusterType.Client) - .number( - zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, - zigpy.zcl.clusters.general.OnOff.cluster_id, - min_value=1, - max_value=100, - step=1, - unit=UnitOfTime.SECONDS, - multiplier=1, - translation_key="on_off_transition_time", - ) - ) - - zigpy_device = zigpy.quirks._DEVICE_REGISTRY.get_device(zigpy_device) - zigpy_device.endpoints[1].power.PLUGGED_ATTR_READS = { - "battery_voltage": 3, - "battery_percentage_remaining": 100, - } - update_attribute_cache(zigpy_device.endpoints[1].power) - zigpy_device.endpoints[1].on_off.PLUGGED_ATTR_READS = { - zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name: 3, - } - update_attribute_cache(zigpy_device.endpoints[1].on_off) - - zha_device = await zha_device_joined(zigpy_device) - - entity_id = find_entity_id( - Platform.NUMBER, - zha_device, - hass, - ) - assert entity_id is not None - - state = hass.states.get(entity_id) - assert state is not None - - -async def test_quirks_v2_entity_discovery_e1_curtain( - hass: HomeAssistant, - zigpy_device_mock, - zha_device_joined, -) -> None: - """Test quirks v2 discovery for e1 curtain motor.""" - aqara_E1_device = zigpy_device_mock( - { - 1: { - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.WINDOW_COVERING_DEVICE, - SIG_EP_INPUT: [ - zigpy.zcl.clusters.general.Basic.cluster_id, - zigpy.zcl.clusters.general.PowerConfiguration.cluster_id, - zigpy.zcl.clusters.general.Identify.cluster_id, - zigpy.zcl.clusters.general.Time.cluster_id, - WindowCoveringE1.cluster_id, - XiaomiAqaraDriverE1.cluster_id, - ], - SIG_EP_OUTPUT: [ - zigpy.zcl.clusters.general.Identify.cluster_id, - zigpy.zcl.clusters.general.Time.cluster_id, - zigpy.zcl.clusters.general.Ota.cluster_id, - XiaomiAqaraDriverE1.cluster_id, - ], - } - }, - ieee="01:2d:6f:00:0a:90:69:e8", - manufacturer="LUMI", - model="lumi.curtain.agl006", - ) - - class AqaraE1HookState(zigpy.types.enum8): - """Aqara hook state.""" - - Unlocked = 0x00 - Locked = 0x01 - Locking = 0x02 - Unlocking = 0x03 - - class FakeXiaomiAqaraDriverE1(XiaomiAqaraDriverE1): - """Fake XiaomiAqaraDriverE1 cluster.""" - - attributes = XiaomiAqaraDriverE1.attributes.copy() - attributes.update( - { - 0x9999: ("error_detected", zigpy.types.Bool, True), - } - ) - - ( - add_to_registry_v2("LUMI", "lumi.curtain.agl006") - .adds(LocalIlluminanceMeasurementCluster) - .replaces(BasicCluster) - .replaces(XiaomiPowerConfigurationPercent) - .replaces(WindowCoveringE1) - .replaces(FakeXiaomiAqaraDriverE1) - .removes(FakeXiaomiAqaraDriverE1, cluster_type=ClusterType.Client) - .enum( - BasicCluster.AttributeDefs.power_source.name, - BasicCluster.PowerSource, - BasicCluster.cluster_id, - entity_platform=Platform.SENSOR, - entity_type=EntityType.DIAGNOSTIC, - ) - .enum( - "hooks_state", - AqaraE1HookState, - FakeXiaomiAqaraDriverE1.cluster_id, - entity_platform=Platform.SENSOR, - entity_type=EntityType.DIAGNOSTIC, - ) - .binary_sensor( - "error_detected", - FakeXiaomiAqaraDriverE1.cluster_id, - translation_key="valve_alarm", - ) - ) - - aqara_E1_device = zigpy.quirks._DEVICE_REGISTRY.get_device(aqara_E1_device) - - aqara_E1_device.endpoints[1].opple_cluster.PLUGGED_ATTR_READS = { - "hand_open": 0, - "positions_stored": 0, - "hooks_lock": 0, - "hooks_state": AqaraE1HookState.Unlocked, - "light_level": 0, - "error_detected": 0, - } - update_attribute_cache(aqara_E1_device.endpoints[1].opple_cluster) - - aqara_E1_device.endpoints[1].basic.PLUGGED_ATTR_READS = { - BasicCluster.AttributeDefs.power_source.name: BasicCluster.PowerSource.Mains_single_phase, - } - update_attribute_cache(aqara_E1_device.endpoints[1].basic) - - WCAttrs = zigpy.zcl.clusters.closures.WindowCovering.AttributeDefs - WCT = zigpy.zcl.clusters.closures.WindowCovering.WindowCoveringType - WCCS = zigpy.zcl.clusters.closures.WindowCovering.ConfigStatus - aqara_E1_device.endpoints[1].window_covering.PLUGGED_ATTR_READS = { - WCAttrs.current_position_lift_percentage.name: 0, - WCAttrs.window_covering_type.name: WCT.Drapery, - WCAttrs.config_status.name: WCCS(~WCCS.Open_up_commands_reversed), - } - update_attribute_cache(aqara_E1_device.endpoints[1].window_covering) - - zha_device = await zha_device_joined(aqara_E1_device) - - power_source_entity_id = find_entity_id( - Platform.SENSOR, - zha_device, - hass, - qualifier=BasicCluster.AttributeDefs.power_source.name, - ) - assert power_source_entity_id is not None - state = hass.states.get(power_source_entity_id) - assert state is not None - assert state.state == BasicCluster.PowerSource.Mains_single_phase.name - - hook_state_entity_id = find_entity_id( - Platform.SENSOR, - zha_device, - hass, - qualifier="hooks_state", - ) - assert hook_state_entity_id is not None - state = hass.states.get(hook_state_entity_id) - assert state is not None - assert state.state == AqaraE1HookState.Unlocked.name - - error_detected_entity_id = find_entity_id( - Platform.BINARY_SENSOR, - zha_device, - hass, - ) - assert error_detected_entity_id is not None - state = hass.states.get(error_detected_entity_id) - assert state is not None - assert state.state == STATE_OFF - - -def _get_test_device( - zigpy_device_mock, - manufacturer: str, - model: str, - augment_method: Callable[[QuirksV2RegistryEntry], QuirksV2RegistryEntry] - | None = None, -): - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [ - zigpy.zcl.clusters.general.PowerConfiguration.cluster_id, - zigpy.zcl.clusters.general.Groups.cluster_id, - zigpy.zcl.clusters.general.OnOff.cluster_id, - ], - SIG_EP_OUTPUT: [ - zigpy.zcl.clusters.general.Scenes.cluster_id, - ], - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.NON_COLOR_CONTROLLER, - } - }, - ieee="01:2d:6f:00:0a:90:69:e8", - manufacturer=manufacturer, - model=model, - ) - - v2_quirk = ( - add_to_registry_v2(manufacturer, model, zigpy.quirks._DEVICE_REGISTRY) - .replaces(PowerConfig1CRCluster) - .replaces(ScenesCluster, cluster_type=ClusterType.Client) - .number( - zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, - zigpy.zcl.clusters.general.OnOff.cluster_id, - endpoint_id=3, - min_value=1, - max_value=100, - step=1, - unit=UnitOfTime.SECONDS, - multiplier=1, - translation_key="on_off_transition_time", - ) - .number( - zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, - zigpy.zcl.clusters.general.Time.cluster_id, - min_value=1, - max_value=100, - step=1, - unit=UnitOfTime.SECONDS, - multiplier=1, - translation_key="on_off_transition_time", - ) - .sensor( - zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, - zigpy.zcl.clusters.general.OnOff.cluster_id, - entity_type=EntityType.CONFIG, - translation_key="analog_input", - ) - ) - - if augment_method: - v2_quirk = augment_method(v2_quirk) - - zigpy_device = zigpy.quirks._DEVICE_REGISTRY.get_device(zigpy_device) - zigpy_device.endpoints[1].power.PLUGGED_ATTR_READS = { - "battery_voltage": 3, - "battery_percentage_remaining": 100, - } - update_attribute_cache(zigpy_device.endpoints[1].power) - zigpy_device.endpoints[1].on_off.PLUGGED_ATTR_READS = { - zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name: 3, - } - update_attribute_cache(zigpy_device.endpoints[1].on_off) - return zigpy_device - - -async def test_quirks_v2_entity_no_metadata( - hass: HomeAssistant, - zigpy_device_mock, - zha_device_joined, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test quirks v2 discovery skipped - no metadata.""" - - zigpy_device = _get_test_device( - zigpy_device_mock, "Ikea of Sweden2", "TRADFRI remote control2" - ) - setattr(zigpy_device, "_exposes_metadata", {}) - zha_device = await zha_device_joined(zigpy_device) - assert ( - f"Device: {zigpy_device.ieee!s}-{zha_device.name} does not expose any quirks v2 entities" - in caplog.text - ) - - -async def test_quirks_v2_entity_discovery_errors( - hass: HomeAssistant, - zigpy_device_mock, - zha_device_joined, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test quirks v2 discovery skipped - errors.""" - - zigpy_device = _get_test_device( - zigpy_device_mock, "Ikea of Sweden3", "TRADFRI remote control3" - ) - zha_device = await zha_device_joined(zigpy_device) - - m1 = f"Device: {zigpy_device.ieee!s}-{zha_device.name} does not have an" - m2 = " endpoint with id: 3 - unable to create entity with cluster" - m3 = " details: (3, 6, )" - assert f"{m1}{m2}{m3}" in caplog.text - - time_cluster_id = zigpy.zcl.clusters.general.Time.cluster_id - - m1 = f"Device: {zigpy_device.ieee!s}-{zha_device.name} does not have a" - m2 = f" cluster with id: {time_cluster_id} - unable to create entity with " - m3 = f"cluster details: (1, {time_cluster_id}, )" - assert f"{m1}{m2}{m3}" in caplog.text - - # fmt: off - entity_details = ( - "{'cluster_details': (1, 6, ), 'entity_metadata': " - "ZCLSensorMetadata(entity_platform=, " - "entity_type=, cluster_id=6, endpoint_id=1, " - "cluster_type=, initially_disabled=False, " - "attribute_initialized_from_cache=True, translation_key='analog_input', " - "attribute_name='off_wait_time', divisor=1, multiplier=1, " - "unit=None, device_class=None, state_class=None)}" - ) - # fmt: on - - m1 = f"Device: {zigpy_device.ieee!s}-{zha_device.name} has an entity with " - m2 = f"details: {entity_details} that does not have an entity class mapping - " - m3 = "unable to create entity" - assert f"{m1}{m2}{m3}" in caplog.text - - -DEVICE_CLASS_TYPES = [NumberMetadata, BinarySensorMetadata, ZCLSensorMetadata] - - -def validate_device_class_unit( - quirk: QuirksV2RegistryEntry, - entity_metadata: EntityMetadata, - platform: Platform, - translations: dict, -) -> None: - """Ensure device class and unit are used correctly.""" - if ( - hasattr(entity_metadata, "unit") - and entity_metadata.unit is not None - and hasattr(entity_metadata, "device_class") - and entity_metadata.device_class is not None - ): - m1 = "device_class and unit are both set - unit: " - m2 = f"{entity_metadata.unit} device_class: " - m3 = f"{entity_metadata.device_class} for {platform.name} " - raise ValueError(f"{m1}{m2}{m3}{quirk}") - - -def validate_translation_keys( - quirk: QuirksV2RegistryEntry, - entity_metadata: EntityMetadata, - platform: Platform, - translations: dict, -) -> None: - """Ensure translation keys exist for all v2 quirks.""" - if isinstance(entity_metadata, ZCLCommandButtonMetadata): - default_translation_key = entity_metadata.command_name - else: - default_translation_key = entity_metadata.attribute_name - translation_key = entity_metadata.translation_key or default_translation_key - - if ( - translation_key is not None - and translation_key not in translations["entity"][platform] - ): - raise ValueError( - f"Missing translation key: {translation_key} for {platform.name} {quirk}" - ) - - -def validate_translation_keys_device_class( - quirk: QuirksV2RegistryEntry, - entity_metadata: EntityMetadata, - platform: Platform, - translations: dict, -) -> None: - """Validate translation keys and device class usage.""" - if isinstance(entity_metadata, ZCLCommandButtonMetadata): - default_translation_key = entity_metadata.command_name - else: - default_translation_key = entity_metadata.attribute_name - translation_key = entity_metadata.translation_key or default_translation_key - - metadata_type = type(entity_metadata) - if metadata_type in DEVICE_CLASS_TYPES: - device_class = entity_metadata.device_class - if device_class is not None and translation_key is not None: - m1 = "translation_key and device_class are both set - translation_key: " - m2 = f"{translation_key} device_class: {device_class} for {platform.name} " - raise ValueError(f"{m1}{m2}{quirk}") - - -def validate_metadata(validator: Callable) -> None: - """Ensure v2 quirks metadata does not violate HA rules.""" - all_v2_quirks = itertools.chain.from_iterable( - zigpy.quirks._DEVICE_REGISTRY._registry_v2.values() - ) - translations = load_json("homeassistant/components/zha/strings.json") - for quirk in all_v2_quirks: - for entity_metadata in quirk.entity_metadata: - platform = Platform(entity_metadata.entity_platform.value) - validator(quirk, entity_metadata, platform, translations) - - -def bad_translation_key(v2_quirk: QuirksV2RegistryEntry) -> QuirksV2RegistryEntry: - """Introduce a bad translation key.""" - return v2_quirk.sensor( - zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, - zigpy.zcl.clusters.general.OnOff.cluster_id, - entity_type=EntityType.CONFIG, - translation_key="missing_translation_key", - ) - - -def bad_device_class_unit_combination( - v2_quirk: QuirksV2RegistryEntry, -) -> QuirksV2RegistryEntry: - """Introduce a bad device class and unit combination.""" - return v2_quirk.sensor( - zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, - zigpy.zcl.clusters.general.OnOff.cluster_id, - entity_type=EntityType.CONFIG, - unit="invalid", - device_class="invalid", - translation_key="analog_input", - ) - - -def bad_device_class_translation_key_usage( - v2_quirk: QuirksV2RegistryEntry, -) -> QuirksV2RegistryEntry: - """Introduce a bad device class and translation key combination.""" - return v2_quirk.sensor( - zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, - zigpy.zcl.clusters.general.OnOff.cluster_id, - entity_type=EntityType.CONFIG, - translation_key="invalid", - device_class="invalid", - ) - - -@pytest.mark.parametrize( - ("augment_method", "validate_method", "expected_exception_string"), - [ - ( - bad_translation_key, - validate_translation_keys, - "Missing translation key: missing_translation_key", - ), - ( - bad_device_class_unit_combination, - validate_device_class_unit, - "cannot have both unit and device_class", - ), - ( - bad_device_class_translation_key_usage, - validate_translation_keys_device_class, - "cannot have both a translation_key and a device_class", - ), - ], -) -async def test_quirks_v2_metadata_errors( - hass: HomeAssistant, - zigpy_device_mock, - zha_device_joined, - augment_method: Callable[[QuirksV2RegistryEntry], QuirksV2RegistryEntry], - validate_method: Callable, - expected_exception_string: str, -) -> None: - """Ensure all v2 quirks translation keys exist.""" - - # no error yet - validate_metadata(validate_method) - - # ensure the error is caught and raised - try: - # introduce an error - zigpy_device = _get_test_device( - zigpy_device_mock, - "Ikea of Sweden4", - "TRADFRI remote control4", - augment_method=augment_method, - ) - await zha_device_joined(zigpy_device) - - validate_metadata(validate_method) - # if the device was created we remove it - # so we don't pollute the rest of the tests - zigpy.quirks._DEVICE_REGISTRY.remove(zigpy_device) - except ValueError: - # if the device was not created we remove it - # so we don't pollute the rest of the tests - zigpy.quirks._DEVICE_REGISTRY._registry_v2.pop( - ( - "Ikea of Sweden4", - "TRADFRI remote control4", - ) - ) - with pytest.raises(ValueError, match=expected_exception_string): - raise - - -class BadDeviceClass(enum.Enum): - """Bad device class.""" - - BAD = "bad" - - -def bad_binary_sensor_device_class( - v2_quirk: QuirksV2RegistryEntry, -) -> QuirksV2RegistryEntry: - """Introduce a bad device class on a binary sensor.""" - - return v2_quirk.binary_sensor( - zigpy.zcl.clusters.general.OnOff.AttributeDefs.on_off.name, - zigpy.zcl.clusters.general.OnOff.cluster_id, - device_class=BadDeviceClass.BAD, - ) - - -def bad_sensor_device_class( - v2_quirk: QuirksV2RegistryEntry, -) -> QuirksV2RegistryEntry: - """Introduce a bad device class on a sensor.""" - - return v2_quirk.sensor( - zigpy.zcl.clusters.general.OnOff.AttributeDefs.off_wait_time.name, - zigpy.zcl.clusters.general.OnOff.cluster_id, - device_class=BadDeviceClass.BAD, - ) - - -def bad_number_device_class( - v2_quirk: QuirksV2RegistryEntry, -) -> QuirksV2RegistryEntry: - """Introduce a bad device class on a number.""" - - return v2_quirk.number( - zigpy.zcl.clusters.general.OnOff.AttributeDefs.on_time.name, - zigpy.zcl.clusters.general.OnOff.cluster_id, - device_class=BadDeviceClass.BAD, - ) - - -ERROR_ROOT = "Quirks provided an invalid device class" - - -@pytest.mark.parametrize( - ("augment_method", "expected_exception_string"), - [ - ( - bad_binary_sensor_device_class, - f"{ERROR_ROOT}: BadDeviceClass.BAD for platform binary_sensor", - ), - ( - bad_sensor_device_class, - f"{ERROR_ROOT}: BadDeviceClass.BAD for platform sensor", - ), - ( - bad_number_device_class, - f"{ERROR_ROOT}: BadDeviceClass.BAD for platform number", - ), - ], -) -async def test_quirks_v2_metadata_bad_device_classes( - hass: HomeAssistant, - zigpy_device_mock, - zha_device_joined, - caplog: pytest.LogCaptureFixture, - augment_method: Callable[[QuirksV2RegistryEntry], QuirksV2RegistryEntry], - expected_exception_string: str, -) -> None: - """Test bad quirks v2 device classes.""" - - # introduce an error - zigpy_device = _get_test_device( - zigpy_device_mock, - "Ikea of Sweden4", - "TRADFRI remote control4", - augment_method=augment_method, - ) - await zha_device_joined(zigpy_device) - - assert expected_exception_string in caplog.text - - # remove the device so we don't pollute the rest of the tests - zigpy.quirks._DEVICE_REGISTRY.remove(zigpy_device) diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 095f505876e..0105c569653 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -1,32 +1,25 @@ """Test ZHA fan.""" -from unittest.mock import AsyncMock, call, patch +from unittest.mock import call, patch import pytest -import zhaquirks.ikea.starkvind -from zigpy.device import Device -from zigpy.exceptions import ZigbeeException +from zha.application.platforms.fan.const import PRESET_MODE_ON from zigpy.profiles import zha from zigpy.zcl.clusters import general, hvac -import zigpy.zcl.foundation as zcl_f from homeassistant.components.fan import ( ATTR_PERCENTAGE, - ATTR_PERCENTAGE_STEP, ATTR_PRESET_MODE, DOMAIN as FAN_DOMAIN, SERVICE_SET_PERCENTAGE, SERVICE_SET_PRESET_MODE, NotValidPresetModeError, ) -from homeassistant.components.zha.core.device import ZHADevice -from homeassistant.components.zha.core.discovery import GROUP_PROBE -from homeassistant.components.zha.core.group import GroupMember -from homeassistant.components.zha.core.helpers import get_zha_gateway -from homeassistant.components.zha.fan import ( - PRESET_MODE_AUTO, - PRESET_MODE_ON, - PRESET_MODE_SMART, +from homeassistant.components.zha.helpers import ( + ZHADeviceProxy, + ZHAGatewayProxy, + get_zha_gateway, + get_zha_gateway_proxy, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -34,25 +27,15 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_OFF, STATE_ON, - STATE_UNAVAILABLE, Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.setup import async_setup_component -from .common import ( - async_enable_traffic, - async_find_group_entity_id, - async_test_rejoin, - async_wait_for_updates, - find_entity_id, - send_attributes_report, -) +from .common import find_entity_id, send_attributes_report from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE -IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" -IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" +ON = 1 +OFF = 0 @pytest.fixture(autouse=True) @@ -75,122 +58,49 @@ def fan_platform_only(): yield -@pytest.fixture -def zigpy_device(zigpy_device_mock): - """Fan zigpy device.""" - endpoints = { - 1: { - SIG_EP_INPUT: [hvac.Fan.cluster_id], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, - SIG_EP_PROFILE: zha.PROFILE_ID, - } - } - return zigpy_device_mock( - endpoints, node_descriptor=b"\x02@\x8c\x02\x10RR\x00\x00\x00R\x00\x00" - ) - - -@pytest.fixture -async def coordinator(hass, zigpy_device_mock, zha_device_joined): +async def test_fan(hass: HomeAssistant, setup_zha, zigpy_device_mock) -> None: """Test ZHA fan platform.""" + await setup_zha() + gateway = get_zha_gateway(hass) + gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass) + zigpy_device = zigpy_device_mock( { 1: { - SIG_EP_INPUT: [general.Groups.cluster_id], + SIG_EP_INPUT: [general.Basic.cluster_id, hvac.Fan.cluster_id], SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, + SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, SIG_EP_PROFILE: zha.PROFILE_ID, } }, - ieee="00:15:8d:00:02:32:4f:32", - nwk=0x0000, - node_descriptor=b"\xf8\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff", + ieee="01:2d:6f:00:0a:90:69:e8", + node_descriptor=b"\x02@\x8c\x02\x10RR\x00\x00\x00R\x00\x00", ) - zha_device = await zha_device_joined(zigpy_device) - zha_device.available = True - return zha_device + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) -@pytest.fixture -async def device_fan_1(hass, zigpy_device_mock, zha_device_joined): - """Test ZHA fan platform.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [ - general.Groups.cluster_id, - general.OnOff.cluster_id, - hvac.Fan.cluster_id, - ], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.ON_OFF_LIGHT, - SIG_EP_PROFILE: zha.PROFILE_ID, - }, - }, - ieee=IEEE_GROUPABLE_DEVICE, - ) - zha_device = await zha_device_joined(zigpy_device) - zha_device.available = True - await hass.async_block_till_done() - return zha_device - - -@pytest.fixture -async def device_fan_2(hass, zigpy_device_mock, zha_device_joined): - """Test ZHA fan platform.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [ - general.Groups.cluster_id, - general.OnOff.cluster_id, - hvac.Fan.cluster_id, - general.LevelControl.cluster_id, - ], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.ON_OFF_LIGHT, - SIG_EP_PROFILE: zha.PROFILE_ID, - }, - }, - ieee=IEEE_GROUPABLE_DEVICE2, - ) - zha_device = await zha_device_joined(zigpy_device) - zha_device.available = True - await hass.async_block_till_done() - return zha_device - - -async def test_fan( - hass: HomeAssistant, zha_device_joined_restored, zigpy_device -) -> None: - """Test ZHA fan platform.""" - - zha_device = await zha_device_joined_restored(zigpy_device) - cluster = zigpy_device.endpoints.get(1).fan - entity_id = find_entity_id(Platform.FAN, zha_device, hass) + zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee) + entity_id = find_entity_id(Platform.FAN, zha_device_proxy, hass) + cluster = zigpy_device.endpoints[1].fan assert entity_id is not None - assert hass.states.get(entity_id).state == STATE_OFF - await async_enable_traffic(hass, [zha_device], enabled=False) - # test that the fan was created and that it is unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - - # test that the state has changed from unavailable to off assert hass.states.get(entity_id).state == STATE_OFF # turn on at fan - await send_attributes_report(hass, cluster, {1: 2, 0: 1, 2: 3}) + await send_attributes_report( + hass, + cluster, + {hvac.Fan.AttributeDefs.fan_mode.id: hvac.FanMode.Low}, + ) assert hass.states.get(entity_id).state == STATE_ON # turn off at fan - await send_attributes_report(hass, cluster, {1: 1, 0: 0, 2: 2}) + await send_attributes_report( + hass, cluster, {hvac.Fan.AttributeDefs.fan_mode.id: hvac.FanMode.Off} + ) assert hass.states.get(entity_id).state == STATE_OFF # turn on from HA @@ -230,11 +140,8 @@ async def test_fan( assert exc.value.translation_key == "not_valid_preset_mode" assert len(cluster.write_attributes.mock_calls) == 0 - # test adding new fan to the network and HA - await async_test_rejoin(hass, zigpy_device, [cluster], (1,)) - -async def async_turn_on(hass, entity_id, percentage=None): +async def async_turn_on(hass: HomeAssistant, entity_id, percentage=None): """Turn fan on.""" data = { key: value @@ -245,14 +152,14 @@ async def async_turn_on(hass, entity_id, percentage=None): await hass.services.async_call(Platform.FAN, SERVICE_TURN_ON, data, blocking=True) -async def async_turn_off(hass, entity_id): +async def async_turn_off(hass: HomeAssistant, entity_id): """Turn fan off.""" data = {ATTR_ENTITY_ID: entity_id} if entity_id else {} await hass.services.async_call(Platform.FAN, SERVICE_TURN_OFF, data, blocking=True) -async def async_set_percentage(hass, entity_id, percentage=None): +async def async_set_percentage(hass: HomeAssistant, entity_id, percentage=None): """Set percentage for specified fan.""" data = { key: value @@ -265,7 +172,7 @@ async def async_set_percentage(hass, entity_id, percentage=None): ) -async def async_set_preset_mode(hass, entity_id, preset_mode=None): +async def async_set_preset_mode(hass: HomeAssistant, entity_id, preset_mode=None): """Set preset_mode for specified fan.""" data = { key: value @@ -276,633 +183,3 @@ async def async_set_preset_mode(hass, entity_id, preset_mode=None): await hass.services.async_call( FAN_DOMAIN, SERVICE_SET_PRESET_MODE, data, blocking=True ) - - -@patch( - "zigpy.zcl.clusters.hvac.Fan.write_attributes", - new=AsyncMock(return_value=zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]), -) -@patch( - "homeassistant.components.zha.entity.DEFAULT_UPDATE_GROUP_FROM_CHILD_DELAY", - new=0, -) -async def test_zha_group_fan_entity( - hass: HomeAssistant, device_fan_1, device_fan_2, coordinator -) -> None: - """Test the fan entity for a ZHA group.""" - zha_gateway = get_zha_gateway(hass) - assert zha_gateway is not None - zha_gateway.coordinator_zha_device = coordinator - coordinator._zha_gateway = zha_gateway - device_fan_1._zha_gateway = zha_gateway - device_fan_2._zha_gateway = zha_gateway - member_ieee_addresses = [device_fan_1.ieee, device_fan_2.ieee] - members = [GroupMember(device_fan_1.ieee, 1), GroupMember(device_fan_2.ieee, 1)] - - # test creating a group with 2 members - zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members) - await hass.async_block_till_done() - - assert zha_group is not None - assert len(zha_group.members) == 2 - for member in zha_group.members: - assert member.device.ieee in member_ieee_addresses - assert member.group == zha_group - assert member.endpoint is not None - - entity_domains = GROUP_PROBE.determine_entity_domains(hass, zha_group) - assert len(entity_domains) == 2 - - assert Platform.LIGHT in entity_domains - assert Platform.FAN in entity_domains - - entity_id = async_find_group_entity_id(hass, Platform.FAN, zha_group) - assert hass.states.get(entity_id) is not None - - group_fan_cluster = zha_group.endpoint[hvac.Fan.cluster_id] - - dev1_fan_cluster = device_fan_1.device.endpoints[1].fan - dev2_fan_cluster = device_fan_2.device.endpoints[1].fan - - await async_enable_traffic(hass, [device_fan_1, device_fan_2], enabled=False) - await async_wait_for_updates(hass) - # test that the fans were created and that they are unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [device_fan_1, device_fan_2]) - await async_wait_for_updates(hass) - # test that the fan group entity was created and is off - assert hass.states.get(entity_id).state == STATE_OFF - - # turn on from HA - group_fan_cluster.write_attributes.reset_mock() - await async_turn_on(hass, entity_id) - await hass.async_block_till_done() - assert len(group_fan_cluster.write_attributes.mock_calls) == 1 - assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 2} - - # turn off from HA - group_fan_cluster.write_attributes.reset_mock() - await async_turn_off(hass, entity_id) - assert len(group_fan_cluster.write_attributes.mock_calls) == 1 - assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 0} - - # change speed from HA - group_fan_cluster.write_attributes.reset_mock() - await async_set_percentage(hass, entity_id, percentage=100) - assert len(group_fan_cluster.write_attributes.mock_calls) == 1 - assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 3} - - # change preset mode from HA - group_fan_cluster.write_attributes.reset_mock() - await async_set_preset_mode(hass, entity_id, preset_mode=PRESET_MODE_ON) - assert len(group_fan_cluster.write_attributes.mock_calls) == 1 - assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 4} - - # change preset mode from HA - group_fan_cluster.write_attributes.reset_mock() - await async_set_preset_mode(hass, entity_id, preset_mode=PRESET_MODE_AUTO) - assert len(group_fan_cluster.write_attributes.mock_calls) == 1 - assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 5} - - # change preset mode from HA - group_fan_cluster.write_attributes.reset_mock() - await async_set_preset_mode(hass, entity_id, preset_mode=PRESET_MODE_SMART) - assert len(group_fan_cluster.write_attributes.mock_calls) == 1 - assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 6} - - # test some of the group logic to make sure we key off states correctly - await send_attributes_report(hass, dev1_fan_cluster, {0: 0}) - await send_attributes_report(hass, dev2_fan_cluster, {0: 0}) - await hass.async_block_till_done() - - # test that group fan is off - assert hass.states.get(entity_id).state == STATE_OFF - - await send_attributes_report(hass, dev2_fan_cluster, {0: 2}) - await async_wait_for_updates(hass) - - # test that group fan is speed medium - assert hass.states.get(entity_id).state == STATE_ON - - await send_attributes_report(hass, dev2_fan_cluster, {0: 0}) - await async_wait_for_updates(hass) - - # test that group fan is now off - assert hass.states.get(entity_id).state == STATE_OFF - - -@patch( - "zigpy.zcl.clusters.hvac.Fan.write_attributes", - new=AsyncMock(side_effect=ZigbeeException), -) -@patch( - "homeassistant.components.zha.entity.DEFAULT_UPDATE_GROUP_FROM_CHILD_DELAY", - new=0, -) -async def test_zha_group_fan_entity_failure_state( - hass: HomeAssistant, - device_fan_1, - device_fan_2, - coordinator, - caplog: pytest.LogCaptureFixture, -) -> None: - """Test the fan entity for a ZHA group when writing attributes generates an exception.""" - zha_gateway = get_zha_gateway(hass) - assert zha_gateway is not None - zha_gateway.coordinator_zha_device = coordinator - coordinator._zha_gateway = zha_gateway - device_fan_1._zha_gateway = zha_gateway - device_fan_2._zha_gateway = zha_gateway - member_ieee_addresses = [device_fan_1.ieee, device_fan_2.ieee] - members = [GroupMember(device_fan_1.ieee, 1), GroupMember(device_fan_2.ieee, 1)] - - # test creating a group with 2 members - zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members) - await hass.async_block_till_done() - - assert zha_group is not None - assert len(zha_group.members) == 2 - for member in zha_group.members: - assert member.device.ieee in member_ieee_addresses - assert member.group == zha_group - assert member.endpoint is not None - - entity_domains = GROUP_PROBE.determine_entity_domains(hass, zha_group) - assert len(entity_domains) == 2 - - assert Platform.LIGHT in entity_domains - assert Platform.FAN in entity_domains - - entity_id = async_find_group_entity_id(hass, Platform.FAN, zha_group) - assert hass.states.get(entity_id) is not None - - group_fan_cluster = zha_group.endpoint[hvac.Fan.cluster_id] - - await async_enable_traffic(hass, [device_fan_1, device_fan_2], enabled=False) - await async_wait_for_updates(hass) - # test that the fans were created and that they are unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [device_fan_1, device_fan_2]) - await async_wait_for_updates(hass) - # test that the fan group entity was created and is off - assert hass.states.get(entity_id).state == STATE_OFF - - # turn on from HA - group_fan_cluster.write_attributes.reset_mock() - - with pytest.raises(HomeAssistantError): - await async_turn_on(hass, entity_id) - - await hass.async_block_till_done() - assert len(group_fan_cluster.write_attributes.mock_calls) == 1 - assert group_fan_cluster.write_attributes.call_args[0][0] == {"fan_mode": 2} - - -@pytest.mark.parametrize( - ("plug_read", "expected_state", "expected_percentage"), - [ - (None, STATE_OFF, None), - ({"fan_mode": 0}, STATE_OFF, 0), - ({"fan_mode": 1}, STATE_ON, 33), - ({"fan_mode": 2}, STATE_ON, 66), - ({"fan_mode": 3}, STATE_ON, 100), - ], -) -async def test_fan_init( - hass: HomeAssistant, - zha_device_joined_restored, - zigpy_device, - plug_read, - expected_state, - expected_percentage, -) -> None: - """Test ZHA fan platform.""" - - cluster = zigpy_device.endpoints.get(1).fan - cluster.PLUGGED_ATTR_READS = plug_read - - zha_device = await zha_device_joined_restored(zigpy_device) - entity_id = find_entity_id(Platform.FAN, zha_device, hass) - assert entity_id is not None - assert hass.states.get(entity_id).state == expected_state - assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == expected_percentage - assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None - - -async def test_fan_update_entity( - hass: HomeAssistant, - zha_device_joined_restored, - zigpy_device, -) -> None: - """Test ZHA fan platform.""" - - cluster = zigpy_device.endpoints.get(1).fan - cluster.PLUGGED_ATTR_READS = {"fan_mode": 0} - - zha_device = await zha_device_joined_restored(zigpy_device) - entity_id = find_entity_id(Platform.FAN, zha_device, hass) - assert entity_id is not None - assert hass.states.get(entity_id).state == STATE_OFF - assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == 0 - assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None - assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE_STEP] == 100 / 3 - if zha_device_joined_restored.name == "zha_device_joined": - assert cluster.read_attributes.await_count == 2 - else: - assert cluster.read_attributes.await_count == 4 - - await async_setup_component(hass, "homeassistant", {}) - await hass.async_block_till_done() - - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True - ) - assert hass.states.get(entity_id).state == STATE_OFF - if zha_device_joined_restored.name == "zha_device_joined": - assert cluster.read_attributes.await_count == 3 - else: - assert cluster.read_attributes.await_count == 5 - - cluster.PLUGGED_ATTR_READS = {"fan_mode": 1} - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True - ) - assert hass.states.get(entity_id).state == STATE_ON - assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == 33 - assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None - assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE_STEP] == 100 / 3 - if zha_device_joined_restored.name == "zha_device_joined": - assert cluster.read_attributes.await_count == 4 - else: - assert cluster.read_attributes.await_count == 6 - - -@pytest.fixture -def zigpy_device_ikea(zigpy_device_mock): - """Ikea fan zigpy device.""" - endpoints = { - 1: { - SIG_EP_INPUT: [ - general.Basic.cluster_id, - general.Identify.cluster_id, - general.Groups.cluster_id, - general.Scenes.cluster_id, - 64637, - ], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.COMBINED_INTERFACE, - SIG_EP_PROFILE: zha.PROFILE_ID, - }, - } - return zigpy_device_mock( - endpoints, - manufacturer="IKEA of Sweden", - model="STARKVIND Air purifier", - quirk=zhaquirks.ikea.starkvind.IkeaSTARKVIND, - node_descriptor=b"\x02@\x8c\x02\x10RR\x00\x00\x00R\x00\x00", - ) - - -async def test_fan_ikea( - hass: HomeAssistant, - zha_device_joined_restored: ZHADevice, - zigpy_device_ikea: Device, -) -> None: - """Test ZHA fan Ikea platform.""" - zha_device = await zha_device_joined_restored(zigpy_device_ikea) - cluster = zigpy_device_ikea.endpoints.get(1).ikea_airpurifier - entity_id = find_entity_id(Platform.FAN, zha_device, hass) - assert entity_id is not None - - assert hass.states.get(entity_id).state == STATE_OFF - await async_enable_traffic(hass, [zha_device], enabled=False) - # test that the fan was created and that it is unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - - # test that the state has changed from unavailable to off - assert hass.states.get(entity_id).state == STATE_OFF - - # turn on at fan - await send_attributes_report(hass, cluster, {6: 1}) - assert hass.states.get(entity_id).state == STATE_ON - - # turn off at fan - await send_attributes_report(hass, cluster, {6: 0}) - assert hass.states.get(entity_id).state == STATE_OFF - - # turn on from HA - cluster.write_attributes.reset_mock() - await async_turn_on(hass, entity_id) - assert cluster.write_attributes.mock_calls == [ - call({"fan_mode": 1}, manufacturer=None) - ] - - # turn off from HA - cluster.write_attributes.reset_mock() - await async_turn_off(hass, entity_id) - assert cluster.write_attributes.mock_calls == [ - call({"fan_mode": 0}, manufacturer=None) - ] - - # change speed from HA - cluster.write_attributes.reset_mock() - await async_set_percentage(hass, entity_id, percentage=100) - assert cluster.write_attributes.mock_calls == [ - call({"fan_mode": 10}, manufacturer=None) - ] - - # change preset_mode from HA - cluster.write_attributes.reset_mock() - await async_set_preset_mode(hass, entity_id, preset_mode=PRESET_MODE_AUTO) - assert cluster.write_attributes.mock_calls == [ - call({"fan_mode": 1}, manufacturer=None) - ] - - # set invalid preset_mode from HA - cluster.write_attributes.reset_mock() - with pytest.raises(NotValidPresetModeError) as exc: - await async_set_preset_mode( - hass, entity_id, preset_mode="invalid does not exist" - ) - assert exc.value.translation_key == "not_valid_preset_mode" - assert len(cluster.write_attributes.mock_calls) == 0 - - # test adding new fan to the network and HA - await async_test_rejoin(hass, zigpy_device_ikea, [cluster], (9,)) - - -@pytest.mark.parametrize( - ( - "ikea_plug_read", - "ikea_expected_state", - "ikea_expected_percentage", - "ikea_preset_mode", - ), - [ - (None, STATE_OFF, None, None), - ({"fan_mode": 0}, STATE_OFF, 0, None), - ({"fan_mode": 1}, STATE_ON, 10, PRESET_MODE_AUTO), - ({"fan_mode": 10}, STATE_ON, 20, "Speed 1"), - ({"fan_mode": 15}, STATE_ON, 30, "Speed 1.5"), - ({"fan_mode": 20}, STATE_ON, 40, "Speed 2"), - ({"fan_mode": 25}, STATE_ON, 50, "Speed 2.5"), - ({"fan_mode": 30}, STATE_ON, 60, "Speed 3"), - ({"fan_mode": 35}, STATE_ON, 70, "Speed 3.5"), - ({"fan_mode": 40}, STATE_ON, 80, "Speed 4"), - ({"fan_mode": 45}, STATE_ON, 90, "Speed 4.5"), - ({"fan_mode": 50}, STATE_ON, 100, "Speed 5"), - ], -) -async def test_fan_ikea_init( - hass: HomeAssistant, - zha_device_joined_restored, - zigpy_device_ikea, - ikea_plug_read, - ikea_expected_state, - ikea_expected_percentage, - ikea_preset_mode, -) -> None: - """Test ZHA fan platform.""" - cluster = zigpy_device_ikea.endpoints.get(1).ikea_airpurifier - cluster.PLUGGED_ATTR_READS = ikea_plug_read - - zha_device = await zha_device_joined_restored(zigpy_device_ikea) - entity_id = find_entity_id(Platform.FAN, zha_device, hass) - assert entity_id is not None - assert hass.states.get(entity_id).state == ikea_expected_state - assert ( - hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] - == ikea_expected_percentage - ) - assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] == ikea_preset_mode - - -async def test_fan_ikea_update_entity( - hass: HomeAssistant, - zha_device_joined_restored, - zigpy_device_ikea, -) -> None: - """Test ZHA fan platform.""" - cluster = zigpy_device_ikea.endpoints.get(1).ikea_airpurifier - cluster.PLUGGED_ATTR_READS = {"fan_mode": 0} - - zha_device = await zha_device_joined_restored(zigpy_device_ikea) - entity_id = find_entity_id(Platform.FAN, zha_device, hass) - assert entity_id is not None - assert hass.states.get(entity_id).state == STATE_OFF - assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == 0 - assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None - assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE_STEP] == 100 / 10 - if zha_device_joined_restored.name == "zha_device_joined": - assert cluster.read_attributes.await_count == 3 - else: - assert cluster.read_attributes.await_count == 6 - - await async_setup_component(hass, "homeassistant", {}) - await hass.async_block_till_done() - - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True - ) - assert hass.states.get(entity_id).state == STATE_OFF - if zha_device_joined_restored.name == "zha_device_joined": - assert cluster.read_attributes.await_count == 4 - else: - assert cluster.read_attributes.await_count == 7 - - cluster.PLUGGED_ATTR_READS = {"fan_mode": 1} - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True - ) - assert hass.states.get(entity_id).state == STATE_ON - assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == 10 - assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is PRESET_MODE_AUTO - assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE_STEP] == 100 / 10 - if zha_device_joined_restored.name == "zha_device_joined": - assert cluster.read_attributes.await_count == 5 - else: - assert cluster.read_attributes.await_count == 8 - - -@pytest.fixture -def zigpy_device_kof(zigpy_device_mock): - """Fan by King of Fans zigpy device.""" - endpoints = { - 1: { - SIG_EP_INPUT: [ - general.Basic.cluster_id, - general.Identify.cluster_id, - general.Groups.cluster_id, - general.Scenes.cluster_id, - 64637, - ], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.COMBINED_INTERFACE, - SIG_EP_PROFILE: zha.PROFILE_ID, - }, - } - return zigpy_device_mock( - endpoints, - manufacturer="King Of Fans, Inc.", - model="HBUniversalCFRemote", - quirk=zhaquirks.kof.kof_mr101z.CeilingFan, - node_descriptor=b"\x02@\x8c\x02\x10RR\x00\x00\x00R\x00\x00", - ) - - -async def test_fan_kof( - hass: HomeAssistant, - zha_device_joined_restored: ZHADevice, - zigpy_device_kof: Device, -) -> None: - """Test ZHA fan platform for King of Fans.""" - zha_device = await zha_device_joined_restored(zigpy_device_kof) - cluster = zigpy_device_kof.endpoints.get(1).fan - entity_id = find_entity_id(Platform.FAN, zha_device, hass) - assert entity_id is not None - - assert hass.states.get(entity_id).state == STATE_OFF - await async_enable_traffic(hass, [zha_device], enabled=False) - # test that the fan was created and that it is unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - - # test that the state has changed from unavailable to off - assert hass.states.get(entity_id).state == STATE_OFF - - # turn on at fan - await send_attributes_report(hass, cluster, {1: 2, 0: 1, 2: 3}) - assert hass.states.get(entity_id).state == STATE_ON - - # turn off at fan - await send_attributes_report(hass, cluster, {1: 1, 0: 0, 2: 2}) - assert hass.states.get(entity_id).state == STATE_OFF - - # turn on from HA - cluster.write_attributes.reset_mock() - await async_turn_on(hass, entity_id) - assert cluster.write_attributes.mock_calls == [ - call({"fan_mode": 2}, manufacturer=None) - ] - - # turn off from HA - cluster.write_attributes.reset_mock() - await async_turn_off(hass, entity_id) - assert cluster.write_attributes.mock_calls == [ - call({"fan_mode": 0}, manufacturer=None) - ] - - # change speed from HA - cluster.write_attributes.reset_mock() - await async_set_percentage(hass, entity_id, percentage=100) - assert cluster.write_attributes.mock_calls == [ - call({"fan_mode": 4}, manufacturer=None) - ] - - # change preset_mode from HA - cluster.write_attributes.reset_mock() - await async_set_preset_mode(hass, entity_id, preset_mode=PRESET_MODE_SMART) - assert cluster.write_attributes.mock_calls == [ - call({"fan_mode": 6}, manufacturer=None) - ] - - # set invalid preset_mode from HA - cluster.write_attributes.reset_mock() - with pytest.raises(NotValidPresetModeError) as exc: - await async_set_preset_mode(hass, entity_id, preset_mode=PRESET_MODE_AUTO) - assert exc.value.translation_key == "not_valid_preset_mode" - assert len(cluster.write_attributes.mock_calls) == 0 - - # test adding new fan to the network and HA - await async_test_rejoin(hass, zigpy_device_kof, [cluster], (1,)) - - -@pytest.mark.parametrize( - ("plug_read", "expected_state", "expected_percentage", "expected_preset"), - [ - (None, STATE_OFF, None, None), - ({"fan_mode": 0}, STATE_OFF, 0, None), - ({"fan_mode": 1}, STATE_ON, 25, None), - ({"fan_mode": 2}, STATE_ON, 50, None), - ({"fan_mode": 3}, STATE_ON, 75, None), - ({"fan_mode": 4}, STATE_ON, 100, None), - ({"fan_mode": 6}, STATE_ON, None, PRESET_MODE_SMART), - ], -) -async def test_fan_kof_init( - hass: HomeAssistant, - zha_device_joined_restored, - zigpy_device_kof, - plug_read, - expected_state, - expected_percentage, - expected_preset, -) -> None: - """Test ZHA fan platform for King of Fans.""" - - cluster = zigpy_device_kof.endpoints.get(1).fan - cluster.PLUGGED_ATTR_READS = plug_read - - zha_device = await zha_device_joined_restored(zigpy_device_kof) - entity_id = find_entity_id(Platform.FAN, zha_device, hass) - assert entity_id is not None - assert hass.states.get(entity_id).state == expected_state - assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == expected_percentage - assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] == expected_preset - - -async def test_fan_kof_update_entity( - hass: HomeAssistant, - zha_device_joined_restored, - zigpy_device_kof, -) -> None: - """Test ZHA fan platform for King of Fans.""" - - cluster = zigpy_device_kof.endpoints.get(1).fan - cluster.PLUGGED_ATTR_READS = {"fan_mode": 0} - - zha_device = await zha_device_joined_restored(zigpy_device_kof) - entity_id = find_entity_id(Platform.FAN, zha_device, hass) - assert entity_id is not None - assert hass.states.get(entity_id).state == STATE_OFF - assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == 0 - assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None - assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE_STEP] == 100 / 4 - if zha_device_joined_restored.name == "zha_device_joined": - assert cluster.read_attributes.await_count == 2 - else: - assert cluster.read_attributes.await_count == 4 - - await async_setup_component(hass, "homeassistant", {}) - await hass.async_block_till_done() - - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True - ) - assert hass.states.get(entity_id).state == STATE_OFF - if zha_device_joined_restored.name == "zha_device_joined": - assert cluster.read_attributes.await_count == 3 - else: - assert cluster.read_attributes.await_count == 5 - - cluster.PLUGGED_ATTR_READS = {"fan_mode": 1} - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True - ) - assert hass.states.get(entity_id).state == STATE_ON - assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE] == 25 - assert hass.states.get(entity_id).attributes[ATTR_PRESET_MODE] is None - assert hass.states.get(entity_id).attributes[ATTR_PERCENTAGE_STEP] == 100 / 4 - if zha_device_joined_restored.name == "zha_device_joined": - assert cluster.read_attributes.await_count == 4 - else: - assert cluster.read_attributes.await_count == 6 diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py deleted file mode 100644 index 3a576ed6e55..00000000000 --- a/tests/components/zha/test_gateway.py +++ /dev/null @@ -1,404 +0,0 @@ -"""Test ZHA Gateway.""" - -import asyncio -from unittest.mock import MagicMock, PropertyMock, patch - -import pytest -from zigpy.application import ControllerApplication -from zigpy.profiles import zha -import zigpy.types -from zigpy.zcl.clusters import general, lighting -import zigpy.zdo.types - -from homeassistant.components.zha.core.gateway import ZHAGateway -from homeassistant.components.zha.core.group import GroupMember -from homeassistant.components.zha.core.helpers import get_zha_gateway -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant - -from .common import async_find_group_entity_id -from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE - -from tests.common import MockConfigEntry - -IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" -IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" - - -@pytest.fixture -def zigpy_dev_basic(zigpy_device_mock): - """Zigpy device with just a basic cluster.""" - return zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [general.Basic.cluster_id], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, - SIG_EP_PROFILE: zha.PROFILE_ID, - } - } - ) - - -@pytest.fixture(autouse=True) -def required_platform_only(): - """Only set up the required and required base platforms to speed up tests.""" - with patch( - "homeassistant.components.zha.PLATFORMS", - ( - Platform.SENSOR, - Platform.LIGHT, - Platform.DEVICE_TRACKER, - Platform.NUMBER, - Platform.SELECT, - ), - ): - yield - - -@pytest.fixture -async def zha_dev_basic(hass, zha_device_restored, zigpy_dev_basic): - """ZHA device with just a basic cluster.""" - - return await zha_device_restored(zigpy_dev_basic) - - -@pytest.fixture -async def coordinator(hass, zigpy_device_mock, zha_device_joined): - """Test ZHA light platform.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, - SIG_EP_PROFILE: zha.PROFILE_ID, - } - }, - ieee="00:15:8d:00:02:32:4f:32", - nwk=0x0000, - node_descriptor=b"\xf8\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff", - ) - zha_device = await zha_device_joined(zigpy_device) - zha_device.available = True - return zha_device - - -@pytest.fixture -async def device_light_1(hass, zigpy_device_mock, zha_device_joined): - """Test ZHA light platform.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [ - general.OnOff.cluster_id, - general.LevelControl.cluster_id, - lighting.Color.cluster_id, - general.Groups.cluster_id, - ], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, - SIG_EP_PROFILE: zha.PROFILE_ID, - } - }, - ieee=IEEE_GROUPABLE_DEVICE, - ) - zha_device = await zha_device_joined(zigpy_device) - zha_device.available = True - return zha_device - - -@pytest.fixture -async def device_light_2(hass, zigpy_device_mock, zha_device_joined): - """Test ZHA light platform.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [ - general.OnOff.cluster_id, - general.LevelControl.cluster_id, - lighting.Color.cluster_id, - general.Groups.cluster_id, - ], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, - SIG_EP_PROFILE: zha.PROFILE_ID, - } - }, - ieee=IEEE_GROUPABLE_DEVICE2, - ) - zha_device = await zha_device_joined(zigpy_device) - zha_device.available = True - return zha_device - - -async def test_device_left(hass: HomeAssistant, zigpy_dev_basic, zha_dev_basic) -> None: - """Device leaving the network should become unavailable.""" - - assert zha_dev_basic.available is True - - get_zha_gateway(hass).device_left(zigpy_dev_basic) - await hass.async_block_till_done() - assert zha_dev_basic.available is False - - -async def test_gateway_group_methods( - hass: HomeAssistant, device_light_1, device_light_2, coordinator -) -> None: - """Test creating a group with 2 members.""" - zha_gateway = get_zha_gateway(hass) - assert zha_gateway is not None - zha_gateway.coordinator_zha_device = coordinator - coordinator._zha_gateway = zha_gateway - device_light_1._zha_gateway = zha_gateway - device_light_2._zha_gateway = zha_gateway - member_ieee_addresses = [device_light_1.ieee, device_light_2.ieee] - members = [GroupMember(device_light_1.ieee, 1), GroupMember(device_light_2.ieee, 1)] - - # test creating a group with 2 members - zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members) - await hass.async_block_till_done() - - assert zha_group is not None - assert len(zha_group.members) == 2 - for member in zha_group.members: - assert member.device.ieee in member_ieee_addresses - - entity_id = async_find_group_entity_id(hass, Platform.LIGHT, zha_group) - assert hass.states.get(entity_id) is not None - - # test get group by name - assert zha_group == zha_gateway.async_get_group_by_name(zha_group.name) - - # test removing a group - await zha_gateway.async_remove_zigpy_group(zha_group.group_id) - await hass.async_block_till_done() - - # we shouldn't have the group anymore - assert zha_gateway.async_get_group_by_name(zha_group.name) is None - - # the group entity should be cleaned up - assert entity_id not in hass.states.async_entity_ids(Platform.LIGHT) - - # test creating a group with 1 member - zha_group = await zha_gateway.async_create_zigpy_group( - "Test Group", [GroupMember(device_light_1.ieee, 1)] - ) - await hass.async_block_till_done() - - assert zha_group is not None - assert len(zha_group.members) == 1 - for member in zha_group.members: - assert member.device.ieee in [device_light_1.ieee] - - # the group entity should not have been cleaned up - assert entity_id not in hass.states.async_entity_ids(Platform.LIGHT) - - with patch("zigpy.zcl.Cluster.request", side_effect=TimeoutError): - await zha_group.members[0].async_remove_from_group() - assert len(zha_group.members) == 1 - for member in zha_group.members: - assert member.device.ieee in [device_light_1.ieee] - - -async def test_gateway_create_group_with_id( - hass: HomeAssistant, device_light_1, coordinator -) -> None: - """Test creating a group with a specific ID.""" - zha_gateway = get_zha_gateway(hass) - assert zha_gateway is not None - zha_gateway.coordinator_zha_device = coordinator - coordinator._zha_gateway = zha_gateway - device_light_1._zha_gateway = zha_gateway - - zha_group = await zha_gateway.async_create_zigpy_group( - "Test Group", [GroupMember(device_light_1.ieee, 1)], group_id=0x1234 - ) - await hass.async_block_till_done() - - assert len(zha_group.members) == 1 - assert zha_group.members[0].device is device_light_1 - assert zha_group.group_id == 0x1234 - - -@patch( - "homeassistant.components.zha.core.gateway.ZHAGateway.async_load_devices", - MagicMock(), -) -@patch( - "homeassistant.components.zha.core.gateway.ZHAGateway.async_load_groups", - MagicMock(), -) -@pytest.mark.parametrize( - ("device_path", "thread_state", "config_override"), - [ - ("/dev/ttyUSB0", True, {}), - ("socket://192.168.1.123:9999", False, {}), - ("socket://192.168.1.123:9999", True, {"use_thread": True}), - ], -) -async def test_gateway_initialize_bellows_thread( - device_path: str, - thread_state: bool, - config_override: dict, - hass: HomeAssistant, - zigpy_app_controller: ControllerApplication, - config_entry: MockConfigEntry, -) -> None: - """Test ZHA disabling the UART thread when connecting to a TCP coordinator.""" - data = dict(config_entry.data) - data["device"]["path"] = device_path - config_entry.add_to_hass(hass) - hass.config_entries.async_update_entry(config_entry, data=data) - - zha_gateway = ZHAGateway(hass, {"zigpy_config": config_override}, config_entry) - - with patch( - "bellows.zigbee.application.ControllerApplication.new", - return_value=zigpy_app_controller, - ) as mock_new: - await zha_gateway.async_initialize() - - assert mock_new.mock_calls[-1].kwargs["config"]["use_thread"] is thread_state - - await zha_gateway.shutdown() - - -@pytest.mark.parametrize( - ("device_path", "config_override", "expected_channel"), - [ - ("/dev/ttyUSB0", {}, None), - ("socket://192.168.1.123:9999", {}, None), - ("socket://192.168.1.123:9999", {"network": {"channel": 20}}, 20), - ("socket://core-silabs-multiprotocol:9999", {}, 15), - ("socket://core-silabs-multiprotocol:9999", {"network": {"channel": 20}}, 20), - ], -) -async def test_gateway_force_multi_pan_channel( - device_path: str, - config_override: dict, - expected_channel: int | None, - hass: HomeAssistant, - config_entry: MockConfigEntry, -) -> None: - """Test ZHA disabling the UART thread when connecting to a TCP coordinator.""" - data = dict(config_entry.data) - data["device"]["path"] = device_path - config_entry.add_to_hass(hass) - hass.config_entries.async_update_entry(config_entry, data=data) - - zha_gateway = ZHAGateway(hass, {"zigpy_config": config_override}, config_entry) - - _, config = zha_gateway.get_application_controller_data() - assert config["network"]["channel"] == expected_channel - - -async def test_single_reload_on_multiple_connection_loss( - hass: HomeAssistant, - zigpy_app_controller: ControllerApplication, - config_entry: MockConfigEntry, -) -> None: - """Test that we only reload once when we lose the connection multiple times.""" - config_entry.add_to_hass(hass) - - zha_gateway = ZHAGateway(hass, {}, config_entry) - - with patch( - "bellows.zigbee.application.ControllerApplication.new", - return_value=zigpy_app_controller, - ): - await zha_gateway.async_initialize() - - with patch.object( - hass.config_entries, "async_reload", wraps=hass.config_entries.async_reload - ) as mock_reload: - zha_gateway.connection_lost(RuntimeError()) - zha_gateway.connection_lost(RuntimeError()) - zha_gateway.connection_lost(RuntimeError()) - zha_gateway.connection_lost(RuntimeError()) - zha_gateway.connection_lost(RuntimeError()) - - assert len(mock_reload.mock_calls) == 1 - - await hass.async_block_till_done() - - -@pytest.mark.parametrize("radio_concurrency", [1, 2, 8]) -async def test_startup_concurrency_limit( - radio_concurrency: int, - hass: HomeAssistant, - zigpy_app_controller: ControllerApplication, - config_entry: MockConfigEntry, - zigpy_device_mock, -) -> None: - """Test ZHA gateway limits concurrency on startup.""" - config_entry.add_to_hass(hass) - zha_gateway = ZHAGateway(hass, {}, config_entry) - - with patch( - "bellows.zigbee.application.ControllerApplication.new", - return_value=zigpy_app_controller, - ): - await zha_gateway.async_initialize() - - for i in range(50): - zigpy_dev = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [ - general.OnOff.cluster_id, - general.LevelControl.cluster_id, - lighting.Color.cluster_id, - general.Groups.cluster_id, - ], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, - SIG_EP_PROFILE: zha.PROFILE_ID, - } - }, - ieee=f"11:22:33:44:{i:08x}", - nwk=0x1234 + i, - ) - zigpy_dev.node_desc.mac_capability_flags |= ( - zigpy.zdo.types.NodeDescriptor.MACCapabilityFlags.MainsPowered - ) - - zha_gateway._async_get_or_create_device(zigpy_dev) - - # Keep track of request concurrency during initialization - current_concurrency = 0 - concurrencies = [] - - async def mock_send_packet(*args, **kwargs): - nonlocal current_concurrency - - current_concurrency += 1 - concurrencies.append(current_concurrency) - - await asyncio.sleep(0.001) - - current_concurrency -= 1 - concurrencies.append(current_concurrency) - - type(zha_gateway).radio_concurrency = PropertyMock(return_value=radio_concurrency) - assert zha_gateway.radio_concurrency == radio_concurrency - - with patch( - "homeassistant.components.zha.core.device.ZHADevice.async_initialize", - side_effect=mock_send_packet, - ): - await zha_gateway.async_fetch_updated_state_mains() - - await zha_gateway.shutdown() - - # Make sure concurrency was always limited - assert current_concurrency == 0 - assert min(concurrencies) == 0 - - if radio_concurrency > 1: - assert 1 <= max(concurrencies) < zha_gateway.radio_concurrency - else: - assert 1 == max(concurrencies) == zha_gateway.radio_concurrency diff --git a/tests/components/zha/test_helpers.py b/tests/components/zha/test_helpers.py index 0615fefd644..13c03c17cf7 100644 --- a/tests/components/zha/test_helpers.py +++ b/tests/components/zha/test_helpers.py @@ -1,81 +1,27 @@ """Tests for ZHA helpers.""" -import enum import logging -from unittest.mock import patch +from typing import Any import pytest import voluptuous_serialize -from zigpy.profiles import zha -from zigpy.quirks.v2.homeassistant import UnitOfPower as QuirksUnitOfPower from zigpy.types.basic import uint16_t -from zigpy.zcl.clusters import general, lighting +from zigpy.zcl.clusters import lighting -from homeassistant.components.zha.core.helpers import ( +from homeassistant.components.zha.helpers import ( cluster_command_schema_to_vol_schema, convert_to_zcl_values, - validate_unit, + exclude_none_values, ) -from homeassistant.const import Platform, UnitOfPower from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from .common import async_enable_traffic -from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE - _LOGGER = logging.getLogger(__name__) -@pytest.fixture(autouse=True) -def light_platform_only(): - """Only set up the light and required base platforms to speed up tests.""" - with patch( - "homeassistant.components.zha.PLATFORMS", - ( - Platform.BUTTON, - Platform.LIGHT, - Platform.NUMBER, - Platform.SELECT, - ), - ): - yield - - -@pytest.fixture -async def device_light(hass: HomeAssistant, zigpy_device_mock, zha_device_joined): - """Test light.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [ - general.OnOff.cluster_id, - general.LevelControl.cluster_id, - lighting.Color.cluster_id, - general.Groups.cluster_id, - general.Identify.cluster_id, - ], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, - SIG_EP_PROFILE: zha.PROFILE_ID, - } - } - ) - color_cluster = zigpy_device.endpoints[1].light_color - color_cluster.PLUGGED_ATTR_READS = { - "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature - | lighting.Color.ColorCapabilities.XY_attributes - } - zha_device = await zha_device_joined(zigpy_device) - zha_device.available = True - return color_cluster, zha_device - - -async def test_zcl_schema_conversions(hass: HomeAssistant, device_light) -> None: +async def test_zcl_schema_conversions(hass: HomeAssistant) -> None: """Test ZHA ZCL schema conversion helpers.""" - color_cluster, zha_device = device_light - await async_enable_traffic(hass, [zha_device]) - command_schema = color_cluster.commands_by_name["color_loop_set"].schema + command_schema = lighting.Color.ServerCommandDefs.color_loop_set.schema expected_schema = [ { "type": "multi_select", @@ -215,23 +161,21 @@ async def test_zcl_schema_conversions(hass: HomeAssistant, device_light) -> None assert converted_data["update_flags"] == 0 -def test_unit_validation() -> None: - """Test unit validation.""" +@pytest.mark.parametrize( + ("obj", "expected_output"), + [ + ({"a": 1, "b": 2, "c": None}, {"a": 1, "b": 2}), + ({"a": 1, "b": 2, "c": 0}, {"a": 1, "b": 2, "c": 0}), + ({"a": 1, "b": 2, "c": ""}, {"a": 1, "b": 2, "c": ""}), + ({"a": 1, "b": 2, "c": False}, {"a": 1, "b": 2, "c": False}), + ], +) +def test_exclude_none_values( + obj: dict[str, Any], expected_output: dict[str, Any] +) -> None: + """Test exclude_none_values helper.""" + result = exclude_none_values(obj) + assert result == expected_output - assert validate_unit(QuirksUnitOfPower.WATT) == UnitOfPower.WATT - - class FooUnit(enum.Enum): - """Foo unit.""" - - BAR = "bar" - - class UnitOfMass(enum.Enum): - """UnitOfMass.""" - - BAR = "bar" - - with pytest.raises(KeyError): - validate_unit(FooUnit.BAR) - - with pytest.raises(ValueError): - validate_unit(UnitOfMass.BAR) + for key in expected_output: + assert expected_output[key] == obj[key] diff --git a/tests/components/zha/test_init.py b/tests/components/zha/test_init.py index 4d4956d3978..aa68d688799 100644 --- a/tests/components/zha/test_init.py +++ b/tests/components/zha/test_init.py @@ -9,14 +9,14 @@ from zigpy.application import ControllerApplication from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH from zigpy.exceptions import TransientConnectionError -from homeassistant.components.zha.core.const import ( +from homeassistant.components.zha.const import ( CONF_BAUDRATE, CONF_FLOW_CONTROL, CONF_RADIO_TYPE, CONF_USB_PATH, DOMAIN, ) -from homeassistant.components.zha.core.helpers import get_zha_data +from homeassistant.components.zha.helpers import get_zha_data from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, MAJOR_VERSION, @@ -43,7 +43,7 @@ def disable_platform_only(): @pytest.fixture -def config_entry_v1(hass): +def config_entry_v1(hass: HomeAssistant): """Config entry version 1 fixture.""" return MockConfigEntry( domain=DOMAIN, @@ -139,7 +139,6 @@ async def test_config_depreciation(hass: HomeAssistant, zha_config) -> None: ("socket://[1.2.3.4]:5678 ", "socket://1.2.3.4:5678"), ], ) -@patch("homeassistant.components.zha.setup_quirks", Mock(return_value=True)) @patch( "homeassistant.components.zha.websocket_api.async_load_api", Mock(return_value=True) ) @@ -282,7 +281,7 @@ async def test_shutdown_on_ha_stop( zha_data = get_zha_data(hass) with patch.object( - zha_data.gateway, "shutdown", wraps=zha_data.gateway.shutdown + zha_data.gateway_proxy, "shutdown", wraps=zha_data.gateway_proxy.shutdown ) as mock_shutdown: hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) hass.set_state(CoreState.stopping) diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index a9d32362863..6a7ede341f5 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -1,12 +1,11 @@ """Test ZHA light.""" -from collections.abc import Callable -from datetime import timedelta -from typing import Any from unittest.mock import AsyncMock, call, patch, sentinel import pytest +from zha.application.platforms.light.const import FLASH_EFFECTS from zigpy.profiles import zha +from zigpy.zcl import Cluster from zigpy.zcl.clusters import general, lighting import zigpy.zcl.foundation as zcl_f @@ -16,41 +15,23 @@ from homeassistant.components.light import ( FLASH_SHORT, ColorMode, ) -from homeassistant.components.zha.core.const import ( - CONF_ALWAYS_PREFER_XY_COLOR_MODE, - CONF_GROUP_MEMBERS_ASSUME_STATE, - ZHA_OPTIONS, +from homeassistant.components.zha.helpers import ( + ZHADeviceProxy, + ZHAGatewayProxy, + get_zha_gateway, + get_zha_gateway_proxy, ) -from homeassistant.components.zha.core.group import GroupMember -from homeassistant.components.zha.core.helpers import get_zha_gateway -from homeassistant.components.zha.light import FLASH_EFFECTS -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform +from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er -import homeassistant.util.dt as dt_util from .common import ( - async_enable_traffic, - async_find_group_entity_id, async_shift_time, - async_test_rejoin, - async_wait_for_updates, find_entity_id, - patch_zha_config, send_attributes_report, update_attribute_cache, ) from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE -from tests.common import ( - async_fire_time_changed, - async_mock_load_restore_state_from_storage, -) - -IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" -IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e9" -IEEE_GROUPABLE_DEVICE3 = "03:2d:6f:00:0a:90:69:e7" - LIGHT_ON_OFF = { 1: { SIG_EP_PROFILE: zha.PROFILE_ID, @@ -111,195 +92,6 @@ def light_platform_only(): yield -@pytest.fixture -async def coordinator(hass, zigpy_device_mock, zha_device_joined): - """Test ZHA light platform.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [general.Groups.cluster_id], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, - SIG_EP_PROFILE: zha.PROFILE_ID, - } - }, - ieee="00:15:8d:00:02:32:4f:32", - nwk=0x0000, - node_descriptor=b"\xf8\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff", - ) - zha_device = await zha_device_joined(zigpy_device) - zha_device.available = True - return zha_device - - -@pytest.fixture -async def device_light_1(hass, zigpy_device_mock, zha_device_joined): - """Test ZHA light platform.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [ - general.OnOff.cluster_id, - general.LevelControl.cluster_id, - lighting.Color.cluster_id, - general.Groups.cluster_id, - general.Identify.cluster_id, - ], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, - SIG_EP_PROFILE: zha.PROFILE_ID, - } - }, - ieee=IEEE_GROUPABLE_DEVICE, - nwk=0xB79D, - ) - color_cluster = zigpy_device.endpoints[1].light_color - color_cluster.PLUGGED_ATTR_READS = { - "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature - | lighting.Color.ColorCapabilities.XY_attributes - } - zha_device = await zha_device_joined(zigpy_device) - zha_device.available = True - return zha_device - - -@pytest.fixture -async def device_light_2(hass, zigpy_device_mock, zha_device_joined): - """Test ZHA light platform.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [ - general.OnOff.cluster_id, - general.LevelControl.cluster_id, - lighting.Color.cluster_id, - general.Groups.cluster_id, - general.Identify.cluster_id, - ], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, - SIG_EP_PROFILE: zha.PROFILE_ID, - } - }, - ieee=IEEE_GROUPABLE_DEVICE2, - manufacturer="sengled", - nwk=0xC79E, - ) - color_cluster = zigpy_device.endpoints[1].light_color - color_cluster.PLUGGED_ATTR_READS = { - "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature - | lighting.Color.ColorCapabilities.XY_attributes - } - zha_device = await zha_device_joined(zigpy_device) - zha_device.available = True - return zha_device - - -@pytest.fixture -async def device_light_3(hass, zigpy_device_mock, zha_device_joined): - """Test ZHA light platform.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [ - general.OnOff.cluster_id, - general.LevelControl.cluster_id, - lighting.Color.cluster_id, - general.Groups.cluster_id, - general.Identify.cluster_id, - ], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, - SIG_EP_PROFILE: zha.PROFILE_ID, - } - }, - ieee=IEEE_GROUPABLE_DEVICE3, - nwk=0xB89F, - ) - zha_device = await zha_device_joined(zigpy_device) - zha_device.available = True - return zha_device - - -@pytest.fixture -async def eWeLink_light(hass, zigpy_device_mock, zha_device_joined): - """Mock eWeLink light.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [ - general.OnOff.cluster_id, - general.LevelControl.cluster_id, - lighting.Color.cluster_id, - general.Groups.cluster_id, - general.Identify.cluster_id, - ], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, - SIG_EP_PROFILE: zha.PROFILE_ID, - } - }, - ieee="03:2d:6f:00:0a:90:69:e3", - manufacturer="eWeLink", - nwk=0xB79D, - ) - color_cluster = zigpy_device.endpoints[1].light_color - color_cluster.PLUGGED_ATTR_READS = { - "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature - | lighting.Color.ColorCapabilities.XY_attributes, - "color_temp_physical_min": 0, - "color_temp_physical_max": 0, - } - zha_device = await zha_device_joined(zigpy_device) - zha_device.available = True - return zha_device - - -async def test_light_refresh( - hass: HomeAssistant, zigpy_device_mock, zha_device_joined_restored -) -> None: - """Test ZHA light platform refresh.""" - - # create zigpy devices - zigpy_device = zigpy_device_mock(LIGHT_ON_OFF) - on_off_cluster = zigpy_device.endpoints[1].on_off - on_off_cluster.PLUGGED_ATTR_READS = {"on_off": 0} - zha_device = await zha_device_joined_restored(zigpy_device) - entity_id = find_entity_id(Platform.LIGHT, zha_device, hass) - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - on_off_cluster.read_attributes.reset_mock() - - # not enough time passed - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=20)) - await hass.async_block_till_done() - assert on_off_cluster.read_attributes.call_count == 0 - assert on_off_cluster.read_attributes.await_count == 0 - assert hass.states.get(entity_id).state == STATE_OFF - - # 1 interval - 1 call - on_off_cluster.PLUGGED_ATTR_READS = {"on_off": 1} - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=80)) - await hass.async_block_till_done() - assert on_off_cluster.read_attributes.call_count == 1 - assert on_off_cluster.read_attributes.await_count == 1 - assert hass.states.get(entity_id).state == STATE_ON - - # 2 intervals - 2 calls - on_off_cluster.PLUGGED_ATTR_READS = {"on_off": 0} - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=80)) - await hass.async_block_till_done() - assert on_off_cluster.read_attributes.call_count == 2 - assert on_off_cluster.read_attributes.await_count == 2 - assert hass.states.get(entity_id).state == STATE_OFF - - @patch( "zigpy.zcl.clusters.lighting.Color.request", new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), @@ -322,34 +114,42 @@ async def test_light_refresh( ) async def test_light( hass: HomeAssistant, + setup_zha, zigpy_device_mock, - zha_device_joined_restored, device, reporting, ) -> None: """Test ZHA light platform.""" - # create zigpy devices - zigpy_device = zigpy_device_mock(device) - zha_device = await zha_device_joined_restored(zigpy_device) - entity_id = find_entity_id(Platform.LIGHT, zha_device, hass) + await setup_zha() + gateway = get_zha_gateway(hass) + gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass) + zigpy_device = zigpy_device_mock(device) + cluster_color = getattr(zigpy_device.endpoints[1], "light_color", None) + + if cluster_color: + cluster_color.PLUGGED_ATTR_READS = { + "color_temperature": 100, + "color_temp_physical_min": 0, + "color_temp_physical_max": 600, + "color_capabilities": lighting.ColorCapabilities.XY_attributes + | lighting.ColorCapabilities.Color_temperature, + } + update_attribute_cache(cluster_color) + + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) + + zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee) + entity_id = find_entity_id(Platform.LIGHT, zha_device_proxy, hass) assert entity_id is not None cluster_on_off = zigpy_device.endpoints[1].on_off cluster_level = getattr(zigpy_device.endpoints[1], "level", None) - cluster_color = getattr(zigpy_device.endpoints[1], "light_color", None) cluster_identify = getattr(zigpy_device.endpoints[1], "identify", None) - assert hass.states.get(entity_id).state == STATE_OFF - await async_enable_traffic(hass, [zha_device], enabled=False) - # test that the lights were created and that they are unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - - # test that the lights were created and are off assert hass.states.get(entity_id).state == STATE_OFF # test turning the lights on and off from the light @@ -379,889 +179,6 @@ async def test_light( hass, cluster_level, entity_id, 150, STATE_ON ) - # test rejoin - await async_test_off_from_hass(hass, cluster_on_off, entity_id) - clusters = [c for c in (cluster_on_off, cluster_level, cluster_color) if c] - await async_test_rejoin(hass, zigpy_device, clusters, reporting) - - -@pytest.mark.parametrize( - ("plugged_attr_reads", "config_override", "expected_state"), - [ - # HS light without cached hue or saturation - ( - { - "color_capabilities": ( - lighting.Color.ColorCapabilities.Hue_and_saturation - ), - }, - {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, - {}, - ), - # HS light with cached hue - ( - { - "color_capabilities": ( - lighting.Color.ColorCapabilities.Hue_and_saturation - ), - "current_hue": 100, - }, - {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, - {}, - ), - # HS light with cached saturation - ( - { - "color_capabilities": ( - lighting.Color.ColorCapabilities.Hue_and_saturation - ), - "current_saturation": 100, - }, - {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, - {}, - ), - # HS light with both - ( - { - "color_capabilities": ( - lighting.Color.ColorCapabilities.Hue_and_saturation - ), - "current_hue": 100, - "current_saturation": 100, - }, - {(ZHA_OPTIONS, CONF_ALWAYS_PREFER_XY_COLOR_MODE): False}, - {}, - ), - ], -) -async def test_light_initialization( - hass: HomeAssistant, - zigpy_device_mock, - zha_device_joined_restored, - plugged_attr_reads, - config_override, - expected_state, -) -> None: - """Test ZHA light initialization with cached attributes and color modes.""" - - # create zigpy devices - zigpy_device = zigpy_device_mock(LIGHT_COLOR) - - # mock attribute reads - zigpy_device.endpoints[1].light_color.PLUGGED_ATTR_READS = plugged_attr_reads - - with patch_zha_config("light", config_override): - zha_device = await zha_device_joined_restored(zigpy_device) - entity_id = find_entity_id(Platform.LIGHT, zha_device, hass) - - assert entity_id is not None - - # pylint: disable-next=fixme - # TODO ensure hue and saturation are properly set on startup - - -@patch( - "zigpy.zcl.clusters.lighting.Color.request", - new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), -) -@patch( - "zigpy.zcl.clusters.general.Identify.request", - new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), -) -@patch( - "zigpy.zcl.clusters.general.LevelControl.request", - new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), -) -@patch( - "zigpy.zcl.clusters.general.OnOff.request", - new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), -) -async def test_transitions( - hass: HomeAssistant, device_light_1, device_light_2, eWeLink_light, coordinator -) -> None: - """Test ZHA light transition code.""" - zha_gateway = get_zha_gateway(hass) - assert zha_gateway is not None - zha_gateway.coordinator_zha_device = coordinator - coordinator._zha_gateway = zha_gateway - device_light_1._zha_gateway = zha_gateway - device_light_2._zha_gateway = zha_gateway - member_ieee_addresses = [device_light_1.ieee, device_light_2.ieee] - members = [GroupMember(device_light_1.ieee, 1), GroupMember(device_light_2.ieee, 1)] - - assert coordinator.is_coordinator - - # test creating a group with 2 members - zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members) - await hass.async_block_till_done() - - assert zha_group is not None - assert len(zha_group.members) == 2 - for member in zha_group.members: - assert member.device.ieee in member_ieee_addresses - assert member.group == zha_group - assert member.endpoint is not None - - device_1_entity_id = find_entity_id(Platform.LIGHT, device_light_1, hass) - device_2_entity_id = find_entity_id(Platform.LIGHT, device_light_2, hass) - eWeLink_light_entity_id = find_entity_id(Platform.LIGHT, eWeLink_light, hass) - assert device_1_entity_id != device_2_entity_id - - group_entity_id = async_find_group_entity_id(hass, Platform.LIGHT, zha_group) - assert hass.states.get(group_entity_id) is not None - - assert device_1_entity_id in zha_group.member_entity_ids - assert device_2_entity_id in zha_group.member_entity_ids - - dev1_cluster_on_off = device_light_1.device.endpoints[1].on_off - dev2_cluster_on_off = device_light_2.device.endpoints[1].on_off - eWeLink_cluster_on_off = eWeLink_light.device.endpoints[1].on_off - - dev1_cluster_level = device_light_1.device.endpoints[1].level - dev2_cluster_level = device_light_2.device.endpoints[1].level - eWeLink_cluster_level = eWeLink_light.device.endpoints[1].level - - dev1_cluster_color = device_light_1.device.endpoints[1].light_color - dev2_cluster_color = device_light_2.device.endpoints[1].light_color - eWeLink_cluster_color = eWeLink_light.device.endpoints[1].light_color - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [device_light_1, device_light_2]) - await async_wait_for_updates(hass) - - # test that the lights were created and are off - group_state = hass.states.get(group_entity_id) - assert group_state.state == STATE_OFF - light1_state = hass.states.get(device_1_entity_id) - assert light1_state.state == STATE_OFF - light2_state = hass.states.get(device_2_entity_id) - assert light2_state.state == STATE_OFF - - # first test 0 length transition with no color and no brightness provided - dev1_cluster_on_off.request.reset_mock() - dev1_cluster_level.request.reset_mock() - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_on", - {"entity_id": device_1_entity_id, "transition": 0}, - blocking=True, - ) - assert dev1_cluster_on_off.request.call_count == 0 - assert dev1_cluster_on_off.request.await_count == 0 - assert dev1_cluster_color.request.call_count == 0 - assert dev1_cluster_color.request.await_count == 0 - assert dev1_cluster_level.request.call_count == 1 - assert dev1_cluster_level.request.await_count == 1 - assert dev1_cluster_level.request.call_args == call( - False, - dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].id, - dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, - level=254, # default "full on" brightness - transition_time=0, - expect_reply=True, - manufacturer=None, - tsn=None, - ) - - light1_state = hass.states.get(device_1_entity_id) - assert light1_state.state == STATE_ON - assert light1_state.attributes["brightness"] == 254 - - # test 0 length transition with no color and no brightness provided again, but for "force on" lights - eWeLink_cluster_on_off.request.reset_mock() - eWeLink_cluster_level.request.reset_mock() - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_on", - {"entity_id": eWeLink_light_entity_id, "transition": 0}, - blocking=True, - ) - assert eWeLink_cluster_on_off.request.call_count == 1 - assert eWeLink_cluster_on_off.request.await_count == 1 - assert eWeLink_cluster_on_off.request.call_args_list[0] == call( - False, - eWeLink_cluster_on_off.commands_by_name["on"].id, - eWeLink_cluster_on_off.commands_by_name["on"].schema, - expect_reply=True, - manufacturer=None, - tsn=None, - ) - assert eWeLink_cluster_color.request.call_count == 0 - assert eWeLink_cluster_color.request.await_count == 0 - assert eWeLink_cluster_level.request.call_count == 1 - assert eWeLink_cluster_level.request.await_count == 1 - assert eWeLink_cluster_level.request.call_args == call( - False, - eWeLink_cluster_level.commands_by_name["move_to_level_with_on_off"].id, - eWeLink_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, - level=254, # default "full on" brightness - transition_time=0, - expect_reply=True, - manufacturer=None, - tsn=None, - ) - - eWeLink_state = hass.states.get(eWeLink_light_entity_id) - assert eWeLink_state.state == STATE_ON - assert eWeLink_state.attributes["brightness"] == 254 - - eWeLink_cluster_on_off.request.reset_mock() - eWeLink_cluster_level.request.reset_mock() - - # test 0 length transition with brightness, but no color provided - dev1_cluster_on_off.request.reset_mock() - dev1_cluster_level.request.reset_mock() - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_on", - {"entity_id": device_1_entity_id, "transition": 0, "brightness": 50}, - blocking=True, - ) - assert dev1_cluster_on_off.request.call_count == 0 - assert dev1_cluster_on_off.request.await_count == 0 - assert dev1_cluster_color.request.call_count == 0 - assert dev1_cluster_color.request.await_count == 0 - assert dev1_cluster_level.request.call_count == 1 - assert dev1_cluster_level.request.await_count == 1 - assert dev1_cluster_level.request.call_args == call( - False, - dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].id, - dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, - level=50, - transition_time=0, - expect_reply=True, - manufacturer=None, - tsn=None, - ) - - light1_state = hass.states.get(device_1_entity_id) - assert light1_state.state == STATE_ON - assert light1_state.attributes["brightness"] == 50 - - dev1_cluster_level.request.reset_mock() - - # test non 0 length transition with color provided while light is on - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_on", - { - "entity_id": device_1_entity_id, - "transition": 3.5, - "brightness": 18, - "color_temp": 432, - }, - blocking=True, - ) - assert dev1_cluster_on_off.request.call_count == 0 - assert dev1_cluster_on_off.request.await_count == 0 - assert dev1_cluster_color.request.call_count == 1 - assert dev1_cluster_color.request.await_count == 1 - assert dev1_cluster_level.request.call_count == 1 - assert dev1_cluster_level.request.await_count == 1 - assert dev1_cluster_level.request.call_args == call( - False, - dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].id, - dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, - level=18, - transition_time=35, - expect_reply=True, - manufacturer=None, - tsn=None, - ) - assert dev1_cluster_color.request.call_args == call( - False, - dev1_cluster_color.commands_by_name["move_to_color_temp"].id, - dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, - color_temp_mireds=432, - transition_time=35, - expect_reply=True, - manufacturer=None, - tsn=None, - ) - - light1_state = hass.states.get(device_1_entity_id) - assert light1_state.state == STATE_ON - assert light1_state.attributes["brightness"] == 18 - assert light1_state.attributes["color_temp"] == 432 - assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP - - dev1_cluster_level.request.reset_mock() - dev1_cluster_color.request.reset_mock() - - # test 0 length transition to turn light off - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_off", - { - "entity_id": device_1_entity_id, - "transition": 0, - }, - blocking=True, - ) - assert dev1_cluster_on_off.request.call_count == 0 - assert dev1_cluster_on_off.request.await_count == 0 - assert dev1_cluster_color.request.call_count == 0 - assert dev1_cluster_color.request.await_count == 0 - assert dev1_cluster_level.request.call_count == 1 - assert dev1_cluster_level.request.await_count == 1 - assert dev1_cluster_level.request.call_args == call( - False, - dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].id, - dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, - level=0, - transition_time=0, - expect_reply=True, - manufacturer=None, - tsn=None, - ) - - light1_state = hass.states.get(device_1_entity_id) - assert light1_state.state == STATE_OFF - - dev1_cluster_level.request.reset_mock() - - # test non 0 length transition and color temp while turning light on (new_color_provided_while_off) - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_on", - { - "entity_id": device_1_entity_id, - "transition": 1, - "brightness": 25, - "color_temp": 235, - }, - blocking=True, - ) - assert dev1_cluster_on_off.request.call_count == 0 - assert dev1_cluster_on_off.request.await_count == 0 - assert dev1_cluster_color.request.call_count == 1 - assert dev1_cluster_color.request.await_count == 1 - assert dev1_cluster_level.request.call_count == 2 - assert dev1_cluster_level.request.await_count == 2 - - # first it comes on with no transition at 2 brightness - assert dev1_cluster_level.request.call_args_list[0] == call( - False, - dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].id, - dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, - level=2, - transition_time=0, - expect_reply=True, - manufacturer=None, - tsn=None, - ) - assert dev1_cluster_color.request.call_args == call( - False, - dev1_cluster_color.commands_by_name["move_to_color_temp"].id, - dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, - color_temp_mireds=235, - transition_time=0, # no transition when new_color_provided_while_off - expect_reply=True, - manufacturer=None, - tsn=None, - ) - assert dev1_cluster_level.request.call_args_list[1] == call( - False, - dev1_cluster_level.commands_by_name["move_to_level"].id, - dev1_cluster_level.commands_by_name["move_to_level"].schema, - level=25, - transition_time=10, - expect_reply=True, - manufacturer=None, - tsn=None, - ) - - light1_state = hass.states.get(device_1_entity_id) - assert light1_state.state == STATE_ON - assert light1_state.attributes["brightness"] == 25 - assert light1_state.attributes["color_temp"] == 235 - assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP - - dev1_cluster_level.request.reset_mock() - dev1_cluster_color.request.reset_mock() - - # turn light 1 back off - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_off", - { - "entity_id": device_1_entity_id, - }, - blocking=True, - ) - assert dev1_cluster_on_off.request.call_count == 1 - assert dev1_cluster_on_off.request.await_count == 1 - assert dev1_cluster_color.request.call_count == 0 - assert dev1_cluster_color.request.await_count == 0 - assert dev1_cluster_level.request.call_count == 0 - assert dev1_cluster_level.request.await_count == 0 - group_state = hass.states.get(group_entity_id) - assert group_state.state == STATE_OFF - - dev1_cluster_on_off.request.reset_mock() - dev1_cluster_color.request.reset_mock() - dev1_cluster_level.request.reset_mock() - - # test no transition provided and color temp while turning light on (new_color_provided_while_off) - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_on", - { - "entity_id": device_1_entity_id, - "brightness": 25, - "color_temp": 236, - }, - blocking=True, - ) - assert dev1_cluster_on_off.request.call_count == 0 - assert dev1_cluster_on_off.request.await_count == 0 - assert dev1_cluster_color.request.call_count == 1 - assert dev1_cluster_color.request.await_count == 1 - assert dev1_cluster_level.request.call_count == 2 - assert dev1_cluster_level.request.await_count == 2 - - # first it comes on with no transition at 2 brightness - assert dev1_cluster_level.request.call_args_list[0] == call( - False, - dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].id, - dev1_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, - level=2, - transition_time=0, - expect_reply=True, - manufacturer=None, - tsn=None, - ) - assert dev1_cluster_color.request.call_args == call( - False, - dev1_cluster_color.commands_by_name["move_to_color_temp"].id, - dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, - color_temp_mireds=236, - transition_time=0, # no transition when new_color_provided_while_off - expect_reply=True, - manufacturer=None, - tsn=None, - ) - assert dev1_cluster_level.request.call_args_list[1] == call( - False, - dev1_cluster_level.commands_by_name["move_to_level"].id, - dev1_cluster_level.commands_by_name["move_to_level"].schema, - level=25, - transition_time=0, - expect_reply=True, - manufacturer=None, - tsn=None, - ) - - light1_state = hass.states.get(device_1_entity_id) - assert light1_state.state == STATE_ON - assert light1_state.attributes["brightness"] == 25 - assert light1_state.attributes["color_temp"] == 236 - assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP - - dev1_cluster_level.request.reset_mock() - dev1_cluster_color.request.reset_mock() - - # turn light 1 back off to setup group test - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_off", - { - "entity_id": device_1_entity_id, - }, - blocking=True, - ) - assert dev1_cluster_on_off.request.call_count == 1 - assert dev1_cluster_on_off.request.await_count == 1 - assert dev1_cluster_color.request.call_count == 0 - assert dev1_cluster_color.request.await_count == 0 - assert dev1_cluster_level.request.call_count == 0 - assert dev1_cluster_level.request.await_count == 0 - group_state = hass.states.get(group_entity_id) - assert group_state.state == STATE_OFF - - dev1_cluster_on_off.request.reset_mock() - dev1_cluster_color.request.reset_mock() - dev1_cluster_level.request.reset_mock() - - # test no transition when the same color temp is provided from off - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_on", - { - "entity_id": device_1_entity_id, - "color_temp": 236, - }, - blocking=True, - ) - assert dev1_cluster_on_off.request.call_count == 1 - assert dev1_cluster_on_off.request.await_count == 1 - assert dev1_cluster_color.request.call_count == 1 - assert dev1_cluster_color.request.await_count == 1 - assert dev1_cluster_level.request.call_count == 0 - assert dev1_cluster_level.request.await_count == 0 - - assert dev1_cluster_on_off.request.call_args == call( - False, - dev1_cluster_on_off.commands_by_name["on"].id, - dev1_cluster_on_off.commands_by_name["on"].schema, - expect_reply=True, - manufacturer=None, - tsn=None, - ) - - assert dev1_cluster_color.request.call_args == call( - False, - dev1_cluster_color.commands_by_name["move_to_color_temp"].id, - dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, - color_temp_mireds=236, - transition_time=0, # no transition when new_color_provided_while_off - expect_reply=True, - manufacturer=None, - tsn=None, - ) - - light1_state = hass.states.get(device_1_entity_id) - assert light1_state.state == STATE_ON - assert light1_state.attributes["brightness"] == 25 - assert light1_state.attributes["color_temp"] == 236 - assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP - - dev1_cluster_on_off.request.reset_mock() - dev1_cluster_color.request.reset_mock() - - # turn light 1 back off to setup group test - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_off", - { - "entity_id": device_1_entity_id, - }, - blocking=True, - ) - assert dev1_cluster_on_off.request.call_count == 1 - assert dev1_cluster_on_off.request.await_count == 1 - assert dev1_cluster_color.request.call_count == 0 - assert dev1_cluster_color.request.await_count == 0 - assert dev1_cluster_level.request.call_count == 0 - assert dev1_cluster_level.request.await_count == 0 - group_state = hass.states.get(group_entity_id) - assert group_state.state == STATE_OFF - - dev1_cluster_on_off.request.reset_mock() - dev1_cluster_color.request.reset_mock() - dev1_cluster_level.request.reset_mock() - - # test sengled light uses default minimum transition time - dev2_cluster_on_off.request.reset_mock() - dev2_cluster_color.request.reset_mock() - dev2_cluster_level.request.reset_mock() - - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_on", - {"entity_id": device_2_entity_id, "transition": 0, "brightness": 100}, - blocking=True, - ) - assert dev2_cluster_on_off.request.call_count == 0 - assert dev2_cluster_on_off.request.await_count == 0 - assert dev2_cluster_color.request.call_count == 0 - assert dev2_cluster_color.request.await_count == 0 - assert dev2_cluster_level.request.call_count == 1 - assert dev2_cluster_level.request.await_count == 1 - assert dev2_cluster_level.request.call_args == call( - False, - dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].id, - dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, - level=100, - transition_time=1, # transition time - sengled light uses default minimum - expect_reply=True, - manufacturer=None, - tsn=None, - ) - - light2_state = hass.states.get(device_2_entity_id) - assert light2_state.state == STATE_ON - assert light2_state.attributes["brightness"] == 100 - - dev2_cluster_level.request.reset_mock() - - # turn the sengled light back off - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_off", - { - "entity_id": device_2_entity_id, - }, - blocking=True, - ) - assert dev2_cluster_on_off.request.call_count == 1 - assert dev2_cluster_on_off.request.await_count == 1 - assert dev2_cluster_color.request.call_count == 0 - assert dev2_cluster_color.request.await_count == 0 - assert dev2_cluster_level.request.call_count == 0 - assert dev2_cluster_level.request.await_count == 0 - light2_state = hass.states.get(device_2_entity_id) - assert light2_state.state == STATE_OFF - - dev2_cluster_on_off.request.reset_mock() - - # test non 0 length transition and color temp while turning light on and sengled (new_color_provided_while_off) - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_on", - { - "entity_id": device_2_entity_id, - "transition": 1, - "brightness": 25, - "color_temp": 235, - }, - blocking=True, - ) - assert dev2_cluster_on_off.request.call_count == 0 - assert dev2_cluster_on_off.request.await_count == 0 - assert dev2_cluster_color.request.call_count == 1 - assert dev2_cluster_color.request.await_count == 1 - assert dev2_cluster_level.request.call_count == 2 - assert dev2_cluster_level.request.await_count == 2 - - # first it comes on with no transition at 2 brightness - assert dev2_cluster_level.request.call_args_list[0] == call( - False, - dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].id, - dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, - level=2, - transition_time=1, - expect_reply=True, - manufacturer=None, - tsn=None, - ) - assert dev2_cluster_color.request.call_args == call( - False, - dev2_cluster_color.commands_by_name["move_to_color_temp"].id, - dev2_cluster_color.commands_by_name["move_to_color_temp"].schema, - color_temp_mireds=235, - transition_time=1, # sengled transition == 1 when new_color_provided_while_off - expect_reply=True, - manufacturer=None, - tsn=None, - ) - assert dev2_cluster_level.request.call_args_list[1] == call( - False, - dev2_cluster_level.commands_by_name["move_to_level"].id, - dev2_cluster_level.commands_by_name["move_to_level"].schema, - level=25, - transition_time=10, - expect_reply=True, - manufacturer=None, - tsn=None, - ) - - light2_state = hass.states.get(device_2_entity_id) - assert light2_state.state == STATE_ON - assert light2_state.attributes["brightness"] == 25 - assert light2_state.attributes["color_temp"] == 235 - assert light2_state.attributes["color_mode"] == ColorMode.COLOR_TEMP - - dev2_cluster_level.request.reset_mock() - dev2_cluster_color.request.reset_mock() - - # turn the sengled light back off - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_off", - { - "entity_id": device_2_entity_id, - }, - blocking=True, - ) - assert dev2_cluster_on_off.request.call_count == 1 - assert dev2_cluster_on_off.request.await_count == 1 - assert dev2_cluster_color.request.call_count == 0 - assert dev2_cluster_color.request.await_count == 0 - assert dev2_cluster_level.request.call_count == 0 - assert dev2_cluster_level.request.await_count == 0 - light2_state = hass.states.get(device_2_entity_id) - assert light2_state.state == STATE_OFF - - dev2_cluster_on_off.request.reset_mock() - - # test non 0 length transition and color temp while turning group light on (new_color_provided_while_off) - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_on", - { - "entity_id": group_entity_id, - "transition": 1, - "brightness": 25, - "color_temp": 235, - }, - blocking=True, - ) - - group_on_off_cluster_handler = zha_group.endpoint[general.OnOff.cluster_id] - group_level_cluster_handler = zha_group.endpoint[general.LevelControl.cluster_id] - group_color_cluster_handler = zha_group.endpoint[lighting.Color.cluster_id] - assert group_on_off_cluster_handler.request.call_count == 0 - assert group_on_off_cluster_handler.request.await_count == 0 - assert group_color_cluster_handler.request.call_count == 1 - assert group_color_cluster_handler.request.await_count == 1 - assert group_level_cluster_handler.request.call_count == 1 - assert group_level_cluster_handler.request.await_count == 1 - - # groups are omitted from the 3 call dance for new_color_provided_while_off - assert group_color_cluster_handler.request.call_args == call( - False, - dev2_cluster_color.commands_by_name["move_to_color_temp"].id, - dev2_cluster_color.commands_by_name["move_to_color_temp"].schema, - color_temp_mireds=235, - transition_time=10, # sengled transition == 1 when new_color_provided_while_off - expect_reply=True, - manufacturer=None, - tsn=None, - ) - assert group_level_cluster_handler.request.call_args == call( - False, - dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].id, - dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, - level=25, - transition_time=10, - expect_reply=True, - manufacturer=None, - tsn=None, - ) - - group_state = hass.states.get(group_entity_id) - assert group_state.state == STATE_ON - assert group_state.attributes["brightness"] == 25 - assert group_state.attributes["color_temp"] == 235 - assert group_state.attributes["color_mode"] == ColorMode.COLOR_TEMP - - group_on_off_cluster_handler.request.reset_mock() - group_color_cluster_handler.request.reset_mock() - group_level_cluster_handler.request.reset_mock() - - # turn the sengled light back on - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_on", - { - "entity_id": device_2_entity_id, - }, - blocking=True, - ) - assert dev2_cluster_on_off.request.call_count == 1 - assert dev2_cluster_on_off.request.await_count == 1 - assert dev2_cluster_color.request.call_count == 0 - assert dev2_cluster_color.request.await_count == 0 - assert dev2_cluster_level.request.call_count == 0 - assert dev2_cluster_level.request.await_count == 0 - light2_state = hass.states.get(device_2_entity_id) - assert light2_state.state == STATE_ON - - dev2_cluster_on_off.request.reset_mock() - - # turn the light off with a transition - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_off", - {"entity_id": device_2_entity_id, "transition": 2}, - blocking=True, - ) - assert dev2_cluster_on_off.request.call_count == 0 - assert dev2_cluster_on_off.request.await_count == 0 - assert dev2_cluster_color.request.call_count == 0 - assert dev2_cluster_color.request.await_count == 0 - assert dev2_cluster_level.request.call_count == 1 - assert dev2_cluster_level.request.await_count == 1 - assert dev2_cluster_level.request.call_args == call( - False, - dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].id, - dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, - level=0, - transition_time=20, # transition time - expect_reply=True, - manufacturer=None, - tsn=None, - ) - - light2_state = hass.states.get(device_2_entity_id) - assert light2_state.state == STATE_OFF - - dev2_cluster_level.request.reset_mock() - - # turn the light back on with no args should use a transition and last known brightness - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_on", - {"entity_id": device_2_entity_id}, - blocking=True, - ) - assert dev2_cluster_on_off.request.call_count == 0 - assert dev2_cluster_on_off.request.await_count == 0 - assert dev2_cluster_color.request.call_count == 0 - assert dev2_cluster_color.request.await_count == 0 - assert dev2_cluster_level.request.call_count == 1 - assert dev2_cluster_level.request.await_count == 1 - assert dev2_cluster_level.request.call_args == call( - False, - dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].id, - dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, - level=25, - transition_time=1, # transition time - sengled light uses default minimum - expect_reply=True, - manufacturer=None, - tsn=None, - ) - - light2_state = hass.states.get(device_2_entity_id) - assert light2_state.state == STATE_ON - - dev2_cluster_level.request.reset_mock() - - # test eWeLink color temp while turning light on from off (new_color_provided_while_off) - await hass.services.async_call( - LIGHT_DOMAIN, - "turn_on", - { - "entity_id": eWeLink_light_entity_id, - "color_temp": 235, - }, - blocking=True, - ) - assert eWeLink_cluster_on_off.request.call_count == 1 - assert eWeLink_cluster_on_off.request.await_count == 1 - assert eWeLink_cluster_color.request.call_count == 1 - assert eWeLink_cluster_color.request.await_count == 1 - assert eWeLink_cluster_level.request.call_count == 0 - assert eWeLink_cluster_level.request.await_count == 0 - - # first it comes on - assert eWeLink_cluster_on_off.request.call_args_list[0] == call( - False, - eWeLink_cluster_on_off.commands_by_name["on"].id, - eWeLink_cluster_on_off.commands_by_name["on"].schema, - expect_reply=True, - manufacturer=None, - tsn=None, - ) - assert dev1_cluster_color.request.call_args == call( - False, - dev1_cluster_color.commands_by_name["move_to_color_temp"].id, - dev1_cluster_color.commands_by_name["move_to_color_temp"].schema, - color_temp_mireds=235, - transition_time=0, - expect_reply=True, - manufacturer=None, - tsn=None, - ) - - eWeLink_state = hass.states.get(eWeLink_light_entity_id) - assert eWeLink_state.state == STATE_ON - assert eWeLink_state.attributes["color_temp"] == 235 - assert eWeLink_state.attributes["color_mode"] == ColorMode.COLOR_TEMP - assert eWeLink_state.attributes["min_mireds"] == 153 - assert eWeLink_state.attributes["max_mireds"] == 500 - @patch( "zigpy.zcl.clusters.lighting.Color.request", @@ -1275,13 +192,51 @@ async def test_transitions( "zigpy.zcl.clusters.general.OnOff.request", new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), ) -async def test_on_with_off_color(hass: HomeAssistant, device_light_1) -> None: +async def test_on_with_off_color( + hass: HomeAssistant, setup_zha, zigpy_device_mock +) -> None: """Test turning on the light and sending color commands before on/level commands for supporting lights.""" - device_1_entity_id = find_entity_id(Platform.LIGHT, device_light_1, hass) - dev1_cluster_on_off = device_light_1.device.endpoints[1].on_off - dev1_cluster_level = device_light_1.device.endpoints[1].level - dev1_cluster_color = device_light_1.device.endpoints[1].light_color + await setup_zha() + gateway = get_zha_gateway(hass) + gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass) + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [ + general.OnOff.cluster_id, + general.LevelControl.cluster_id, + lighting.Color.cluster_id, + general.Groups.cluster_id, + general.Identify.cluster_id, + ], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, + SIG_EP_PROFILE: zha.PROFILE_ID, + } + }, + nwk=0xB79D, + ) + + dev1_cluster_color = zigpy_device.endpoints[1].light_color + + dev1_cluster_color.PLUGGED_ATTR_READS = { + "color_capabilities": lighting.Color.ColorCapabilities.Color_temperature + | lighting.Color.ColorCapabilities.XY_attributes + } + + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) + + zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee) + entity_id = find_entity_id(Platform.LIGHT, zha_device_proxy, hass) + assert entity_id is not None + + device_1_entity_id = find_entity_id(Platform.LIGHT, zha_device_proxy, hass) + dev1_cluster_on_off = zigpy_device.endpoints[1].on_off + dev1_cluster_level = zigpy_device.endpoints[1].level # Execute_if_off will override the "enhanced turn on from an off-state" config option that's enabled here dev1_cluster_color.PLUGGED_ATTR_READS = { @@ -1403,28 +358,34 @@ async def test_on_with_off_color(hass: HomeAssistant, device_light_1) -> None: assert light1_state.attributes["color_mode"] == ColorMode.COLOR_TEMP -async def async_test_on_off_from_light(hass, cluster, entity_id): +async def async_test_on_off_from_light( + hass: HomeAssistant, cluster: Cluster, entity_id: str +): """Test on off functionality from the light.""" # turn on at light await send_attributes_report(hass, cluster, {1: 0, 0: 1, 2: 3}) - await async_wait_for_updates(hass) + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == STATE_ON # turn off at light await send_attributes_report(hass, cluster, {1: 1, 0: 0, 2: 3}) - await async_wait_for_updates(hass) + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == STATE_OFF -async def async_test_on_from_light(hass, cluster, entity_id): +async def async_test_on_from_light( + hass: HomeAssistant, cluster: Cluster, entity_id: str +): """Test on off functionality from the light.""" # turn on at light await send_attributes_report(hass, cluster, {1: -1, 0: 1, 2: 2}) - await async_wait_for_updates(hass) + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == STATE_ON -async def async_test_on_off_from_hass(hass, cluster, entity_id): +async def async_test_on_off_from_hass( + hass: HomeAssistant, cluster: Cluster, entity_id: str +): """Test on off functionality from hass.""" # turn on via UI cluster.request.reset_mock() @@ -1445,7 +406,9 @@ async def async_test_on_off_from_hass(hass, cluster, entity_id): await async_test_off_from_hass(hass, cluster, entity_id) -async def async_test_off_from_hass(hass, cluster, entity_id): +async def async_test_off_from_hass( + hass: HomeAssistant, cluster: Cluster, entity_id: str +): """Test turning off the light from Home Assistant.""" # turn off via UI @@ -1467,9 +430,9 @@ async def async_test_off_from_hass(hass, cluster, entity_id): async def async_test_level_on_off_from_hass( hass: HomeAssistant, - on_off_cluster, - level_cluster, - entity_id, + on_off_cluster: Cluster, + level_cluster: Cluster, + entity_id: str, expected_default_transition: int = 0, ): """Test on off functionality from hass.""" @@ -1549,13 +512,19 @@ async def async_test_level_on_off_from_hass( await async_test_off_from_hass(hass, on_off_cluster, entity_id) -async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected_state): +async def async_test_dimmer_from_light( + hass: HomeAssistant, + cluster: Cluster, + entity_id: str, + level: int, + expected_state: str, +): """Test dimmer functionality from the light.""" await send_attributes_report( hass, cluster, {1: level + 10, 0: level, 2: level - 10 or 22} ) - await async_wait_for_updates(hass) + await hass.async_block_till_done(wait_background_tasks=True) assert hass.states.get(entity_id).state == expected_state # hass uses None for brightness of 0 in state attributes if level == 0: @@ -1563,7 +532,9 @@ async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected assert hass.states.get(entity_id).attributes.get("brightness") == level -async def async_test_flash_from_hass(hass, cluster, entity_id, flash): +async def async_test_flash_from_hass( + hass: HomeAssistant, cluster: Cluster, entity_id: str, flash +): """Test flash functionality from hass.""" # turn on via UI cluster.request.reset_mock() @@ -1585,423 +556,3 @@ async def async_test_flash_from_hass(hass, cluster, entity_id, flash): manufacturer=None, tsn=None, ) - - -@patch( - "zigpy.zcl.clusters.lighting.Color.request", - new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), -) -@patch( - "zigpy.zcl.clusters.general.Identify.request", - new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), -) -@patch( - "zigpy.zcl.clusters.general.LevelControl.request", - new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), -) -@patch( - "zigpy.zcl.clusters.general.OnOff.request", - new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), -) -@patch( - "homeassistant.components.zha.entity.DEFAULT_UPDATE_GROUP_FROM_CHILD_DELAY", - new=0, -) -async def test_zha_group_light_entity( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - device_light_1, - device_light_2, - device_light_3, - coordinator, -) -> None: - """Test the light entity for a ZHA group.""" - zha_gateway = get_zha_gateway(hass) - assert zha_gateway is not None - zha_gateway.coordinator_zha_device = coordinator - coordinator._zha_gateway = zha_gateway - device_light_1._zha_gateway = zha_gateway - device_light_2._zha_gateway = zha_gateway - member_ieee_addresses = [device_light_1.ieee, device_light_2.ieee] - members = [GroupMember(device_light_1.ieee, 1), GroupMember(device_light_2.ieee, 1)] - - assert coordinator.is_coordinator - - # test creating a group with 2 members - zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members) - await hass.async_block_till_done() - - assert zha_group is not None - assert len(zha_group.members) == 2 - for member in zha_group.members: - assert member.device.ieee in member_ieee_addresses - assert member.group == zha_group - assert member.endpoint is not None - - device_1_entity_id = find_entity_id(Platform.LIGHT, device_light_1, hass) - device_2_entity_id = find_entity_id(Platform.LIGHT, device_light_2, hass) - device_3_entity_id = find_entity_id(Platform.LIGHT, device_light_3, hass) - - assert device_1_entity_id not in (device_2_entity_id, device_3_entity_id) - assert device_2_entity_id != device_3_entity_id - - group_entity_id = async_find_group_entity_id(hass, Platform.LIGHT, zha_group) - assert hass.states.get(group_entity_id) is not None - - assert device_1_entity_id in zha_group.member_entity_ids - assert device_2_entity_id in zha_group.member_entity_ids - assert device_3_entity_id not in zha_group.member_entity_ids - - group_cluster_on_off = zha_group.endpoint[general.OnOff.cluster_id] - group_cluster_level = zha_group.endpoint[general.LevelControl.cluster_id] - group_cluster_identify = zha_group.endpoint[general.Identify.cluster_id] - - dev1_cluster_on_off = device_light_1.device.endpoints[1].on_off - dev2_cluster_on_off = device_light_2.device.endpoints[1].on_off - dev3_cluster_on_off = device_light_3.device.endpoints[1].on_off - - dev1_cluster_level = device_light_1.device.endpoints[1].level - - await async_enable_traffic( - hass, [device_light_1, device_light_2, device_light_3], enabled=False - ) - await async_wait_for_updates(hass) - # test that the lights were created and that they are unavailable - assert hass.states.get(group_entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [device_light_1, device_light_2, device_light_3]) - await async_wait_for_updates(hass) - - # test that the lights were created and are off - group_state = hass.states.get(group_entity_id) - assert group_state.state == STATE_OFF - assert group_state.attributes["supported_color_modes"] == [ - ColorMode.COLOR_TEMP, - ColorMode.XY, - ] - # Light which is off has no color mode - assert group_state.attributes["color_mode"] is None - - # test turning the lights on and off from the HA - await async_test_on_off_from_hass(hass, group_cluster_on_off, group_entity_id) - - await async_shift_time(hass) - - # test short flashing the lights from the HA - await async_test_flash_from_hass( - hass, group_cluster_identify, group_entity_id, FLASH_SHORT - ) - - await async_shift_time(hass) - - # test turning the lights on and off from the light - await async_test_on_off_from_light(hass, dev1_cluster_on_off, group_entity_id) - - # test turning the lights on and off from the HA - await async_test_level_on_off_from_hass( - hass, - group_cluster_on_off, - group_cluster_level, - group_entity_id, - expected_default_transition=1, # a Sengled light is in that group and needs a minimum 0.1s transition - ) - - await async_shift_time(hass) - - # test getting a brightness change from the network - await async_test_on_from_light(hass, dev1_cluster_on_off, group_entity_id) - await async_test_dimmer_from_light( - hass, dev1_cluster_level, group_entity_id, 150, STATE_ON - ) - # Check state - group_state = hass.states.get(group_entity_id) - assert group_state.state == STATE_ON - assert group_state.attributes["supported_color_modes"] == [ - ColorMode.COLOR_TEMP, - ColorMode.XY, - ] - assert group_state.attributes["color_mode"] == ColorMode.XY - - # test long flashing the lights from the HA - await async_test_flash_from_hass( - hass, group_cluster_identify, group_entity_id, FLASH_LONG - ) - - await async_shift_time(hass) - - assert len(zha_group.members) == 2 - # test some of the group logic to make sure we key off states correctly - await send_attributes_report(hass, dev1_cluster_on_off, {0: 1}) - await send_attributes_report(hass, dev2_cluster_on_off, {0: 1}) - await hass.async_block_till_done() - - # test that group light is on - assert hass.states.get(device_1_entity_id).state == STATE_ON - assert hass.states.get(device_2_entity_id).state == STATE_ON - assert hass.states.get(group_entity_id).state == STATE_ON - - await send_attributes_report(hass, dev1_cluster_on_off, {0: 0}) - await hass.async_block_till_done() - - # test that group light is still on - assert hass.states.get(device_1_entity_id).state == STATE_OFF - assert hass.states.get(device_2_entity_id).state == STATE_ON - assert hass.states.get(group_entity_id).state == STATE_ON - - await send_attributes_report(hass, dev2_cluster_on_off, {0: 0}) - await async_wait_for_updates(hass) - - # test that group light is now off - assert hass.states.get(device_1_entity_id).state == STATE_OFF - assert hass.states.get(device_2_entity_id).state == STATE_OFF - assert hass.states.get(group_entity_id).state == STATE_OFF - - await send_attributes_report(hass, dev1_cluster_on_off, {0: 1}) - await async_wait_for_updates(hass) - - # test that group light is now back on - assert hass.states.get(device_1_entity_id).state == STATE_ON - assert hass.states.get(device_2_entity_id).state == STATE_OFF - assert hass.states.get(group_entity_id).state == STATE_ON - - # turn it off to test a new member add being tracked - await send_attributes_report(hass, dev1_cluster_on_off, {0: 0}) - await async_wait_for_updates(hass) - assert hass.states.get(device_1_entity_id).state == STATE_OFF - assert hass.states.get(device_2_entity_id).state == STATE_OFF - assert hass.states.get(group_entity_id).state == STATE_OFF - - # add a new member and test that his state is also tracked - await zha_group.async_add_members([GroupMember(device_light_3.ieee, 1)]) - await send_attributes_report(hass, dev3_cluster_on_off, {0: 1}) - await async_wait_for_updates(hass) - assert device_3_entity_id in zha_group.member_entity_ids - assert len(zha_group.members) == 3 - - assert hass.states.get(device_1_entity_id).state == STATE_OFF - assert hass.states.get(device_2_entity_id).state == STATE_OFF - assert hass.states.get(device_3_entity_id).state == STATE_ON - assert hass.states.get(group_entity_id).state == STATE_ON - - # make the group have only 1 member and now there should be no entity - await zha_group.async_remove_members( - [GroupMember(device_light_2.ieee, 1), GroupMember(device_light_3.ieee, 1)] - ) - assert len(zha_group.members) == 1 - assert hass.states.get(group_entity_id) is None - assert device_2_entity_id not in zha_group.member_entity_ids - assert device_3_entity_id not in zha_group.member_entity_ids - - # make sure the entity registry entry is still there - assert entity_registry.async_get(group_entity_id) is not None - - # add a member back and ensure that the group entity was created again - await zha_group.async_add_members([GroupMember(device_light_3.ieee, 1)]) - await send_attributes_report(hass, dev3_cluster_on_off, {0: 1}) - await async_wait_for_updates(hass) - assert len(zha_group.members) == 2 - assert hass.states.get(group_entity_id).state == STATE_ON - - # add a 3rd member and ensure we still have an entity and we track the new one - await send_attributes_report(hass, dev1_cluster_on_off, {0: 0}) - await send_attributes_report(hass, dev3_cluster_on_off, {0: 0}) - await async_wait_for_updates(hass) - assert hass.states.get(group_entity_id).state == STATE_OFF - - # this will test that _reprobe_group is used correctly - await zha_group.async_add_members( - [GroupMember(device_light_2.ieee, 1), GroupMember(coordinator.ieee, 1)] - ) - await send_attributes_report(hass, dev2_cluster_on_off, {0: 1}) - await async_wait_for_updates(hass) - assert len(zha_group.members) == 4 - assert hass.states.get(group_entity_id).state == STATE_ON - - await zha_group.async_remove_members([GroupMember(coordinator.ieee, 1)]) - await hass.async_block_till_done() - assert hass.states.get(group_entity_id).state == STATE_ON - assert len(zha_group.members) == 3 - - # remove the group and ensure that there is no entity and that the entity registry is cleaned up - assert entity_registry.async_get(group_entity_id) is not None - await zha_gateway.async_remove_zigpy_group(zha_group.group_id) - assert hass.states.get(group_entity_id) is None - assert entity_registry.async_get(group_entity_id) is None - - -@patch( - "zigpy.zcl.clusters.general.OnOff.request", - new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), -) -@patch( - "homeassistant.components.zha.light.ASSUME_UPDATE_GROUP_FROM_CHILD_DELAY", - new=0, -) -async def test_group_member_assume_state( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - zigpy_device_mock, - zha_device_joined, - coordinator, - device_light_1, - device_light_2, -) -> None: - """Test the group members assume state function.""" - with patch_zha_config( - "light", {(ZHA_OPTIONS, CONF_GROUP_MEMBERS_ASSUME_STATE): True} - ): - zha_gateway = get_zha_gateway(hass) - assert zha_gateway is not None - zha_gateway.coordinator_zha_device = coordinator - coordinator._zha_gateway = zha_gateway - device_light_1._zha_gateway = zha_gateway - device_light_2._zha_gateway = zha_gateway - member_ieee_addresses = [device_light_1.ieee, device_light_2.ieee] - members = [ - GroupMember(device_light_1.ieee, 1), - GroupMember(device_light_2.ieee, 1), - ] - - assert coordinator.is_coordinator - - # test creating a group with 2 members - zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members) - await hass.async_block_till_done() - - assert zha_group is not None - assert len(zha_group.members) == 2 - for member in zha_group.members: - assert member.device.ieee in member_ieee_addresses - assert member.group == zha_group - assert member.endpoint is not None - - device_1_entity_id = find_entity_id(Platform.LIGHT, device_light_1, hass) - device_2_entity_id = find_entity_id(Platform.LIGHT, device_light_2, hass) - - assert device_1_entity_id != device_2_entity_id - - group_entity_id = async_find_group_entity_id(hass, Platform.LIGHT, zha_group) - assert hass.states.get(group_entity_id) is not None - - assert device_1_entity_id in zha_group.member_entity_ids - assert device_2_entity_id in zha_group.member_entity_ids - - group_cluster_on_off = zha_group.endpoint[general.OnOff.cluster_id] - - await async_enable_traffic( - hass, [device_light_1, device_light_2], enabled=False - ) - await async_wait_for_updates(hass) - # test that the lights were created and that they are unavailable - assert hass.states.get(group_entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [device_light_1, device_light_2]) - await async_wait_for_updates(hass) - - # test that the lights were created and are off - group_state = hass.states.get(group_entity_id) - assert group_state.state == STATE_OFF - - group_cluster_on_off.request.reset_mock() - await async_shift_time(hass) - - # turn on via UI - await hass.services.async_call( - LIGHT_DOMAIN, "turn_on", {"entity_id": group_entity_id}, blocking=True - ) - - # members also instantly assume STATE_ON - assert hass.states.get(device_1_entity_id).state == STATE_ON - assert hass.states.get(device_2_entity_id).state == STATE_ON - assert hass.states.get(group_entity_id).state == STATE_ON - - # turn off via UI - await hass.services.async_call( - LIGHT_DOMAIN, "turn_off", {"entity_id": group_entity_id}, blocking=True - ) - - # members also instantly assume STATE_OFF - assert hass.states.get(device_1_entity_id).state == STATE_OFF - assert hass.states.get(device_2_entity_id).state == STATE_OFF - assert hass.states.get(group_entity_id).state == STATE_OFF - - # remove the group and ensure that there is no entity and that the entity registry is cleaned up - assert entity_registry.async_get(group_entity_id) is not None - await zha_gateway.async_remove_zigpy_group(zha_group.group_id) - assert hass.states.get(group_entity_id) is None - assert entity_registry.async_get(group_entity_id) is None - - -@pytest.mark.parametrize( - ("restored_state", "expected_state"), - [ - ( - STATE_ON, - { - "brightness": None, - "off_with_transition": None, - "off_brightness": None, - "color_mode": ColorMode.XY, # color_mode defaults to what the light supports when restored with ON state - "color_temp": None, - "xy_color": None, - "hs_color": None, - "effect": None, - }, - ), - ( - STATE_OFF, - { - "brightness": None, - "off_with_transition": None, - "off_brightness": None, - "color_mode": None, - "color_temp": None, - "xy_color": None, - "hs_color": None, - "effect": None, - }, - ), - ], -) -async def test_restore_light_state( - hass: HomeAssistant, - zigpy_device_mock, - core_rs: Callable[[str, Any, dict[str, Any]], None], - zha_device_restored, - restored_state: str, - expected_state: dict[str, Any], -) -> None: - """Test ZHA light restores without throwing an error when attributes are None.""" - - # restore state with None values - attributes = { - "brightness": None, - "off_with_transition": None, - "off_brightness": None, - "color_mode": None, - "color_temp": None, - "xy_color": None, - "hs_color": None, - "effect": None, - } - - entity_id = "light.fakemanufacturer_fakemodel_light" - core_rs( - entity_id, - state=restored_state, - attributes=attributes, - ) - await async_mock_load_restore_state_from_storage(hass) - - zigpy_device = zigpy_device_mock(LIGHT_COLOR) - zha_device = await zha_device_restored(zigpy_device) - entity_id = find_entity_id(Platform.LIGHT, zha_device, hass) - - assert entity_id is not None - assert hass.states.get(entity_id).state == restored_state - - # compare actual restored state to expected state - for attribute, expected_value in expected_state.items(): - assert hass.states.get(entity_id).attributes.get(attribute) == expected_value diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index b16d7a31828..4e1d092af9b 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -3,27 +3,23 @@ from unittest.mock import patch import pytest -import zigpy.profiles.zha +from zigpy.profiles import zha +from zigpy.zcl import Cluster from zigpy.zcl.clusters import closures, general import zigpy.zcl.foundation as zcl_f from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN -from homeassistant.const import ( - STATE_LOCKED, - STATE_UNAVAILABLE, - STATE_UNLOCKED, - Platform, +from homeassistant.components.zha.helpers import ( + ZHADeviceProxy, + ZHAGatewayProxy, + get_zha_gateway, + get_zha_gateway_proxy, ) +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED, Platform from homeassistant.core import HomeAssistant -from .common import async_enable_traffic, find_entity_id, send_attributes_report -from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE - -LOCK_DOOR = 0 -UNLOCK_DOOR = 1 -SET_PIN_CODE = 5 -CLEAR_PIN_CODE = 7 -SET_USER_STATUS = 9 +from .common import find_entity_id, send_attributes_report +from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE @pytest.fixture(autouse=True) @@ -40,48 +36,51 @@ def lock_platform_only(): yield -@pytest.fixture -async def lock(hass, zigpy_device_mock, zha_device_joined_restored): - """Lock cluster fixture.""" +async def test_lock(hass: HomeAssistant, setup_zha, zigpy_device_mock) -> None: + """Test ZHA lock platform.""" + + await setup_zha() + gateway = get_zha_gateway(hass) + gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass) zigpy_device = zigpy_device_mock( { 1: { SIG_EP_INPUT: [closures.DoorLock.cluster_id, general.Basic.cluster_id], SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.DOOR_LOCK, + SIG_EP_TYPE: zha.DeviceType.DOOR_LOCK, + SIG_EP_PROFILE: zha.PROFILE_ID, } }, + ieee="01:2d:6f:00:0a:90:69:e8", + node_descriptor=b"\x02@\x8c\x02\x10RR\x00\x00\x00R\x00\x00", ) - zha_device = await zha_device_joined_restored(zigpy_device) - return zha_device, zigpy_device.endpoints[1].door_lock + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) - -async def test_lock(hass: HomeAssistant, lock) -> None: - """Test ZHA lock platform.""" - - zha_device, cluster = lock - entity_id = find_entity_id(Platform.LOCK, zha_device, hass) + zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee) + entity_id = find_entity_id(Platform.LOCK, zha_device_proxy, hass) + cluster = zigpy_device.endpoints[1].door_lock assert entity_id is not None - assert hass.states.get(entity_id).state == STATE_UNLOCKED - await async_enable_traffic(hass, [zha_device], enabled=False) - # test that the lock was created and that it is unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - - # test that the state has changed from unavailable to unlocked assert hass.states.get(entity_id).state == STATE_UNLOCKED # set state to locked - await send_attributes_report(hass, cluster, {1: 0, 0: 1, 2: 2}) + await send_attributes_report( + hass, + cluster, + {closures.DoorLock.AttributeDefs.lock_state.id: closures.LockState.Locked}, + ) assert hass.states.get(entity_id).state == STATE_LOCKED # set state to unlocked - await send_attributes_report(hass, cluster, {1: 0, 0: 2, 2: 3}) + await send_attributes_report( + hass, + cluster, + {closures.DoorLock.AttributeDefs.lock_state.id: closures.LockState.Unlocked}, + ) assert hass.states.get(entity_id).state == STATE_UNLOCKED # lock from HA @@ -103,7 +102,7 @@ async def test_lock(hass: HomeAssistant, lock) -> None: await async_disable_user_code(hass, cluster, entity_id) -async def async_lock(hass, cluster, entity_id): +async def async_lock(hass: HomeAssistant, cluster: Cluster, entity_id: str): """Test lock functionality from hass.""" with patch("zigpy.zcl.Cluster.request", return_value=[zcl_f.Status.SUCCESS]): # lock via UI @@ -112,10 +111,13 @@ async def async_lock(hass, cluster, entity_id): ) assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False - assert cluster.request.call_args[0][1] == LOCK_DOOR + assert ( + cluster.request.call_args[0][1] + == closures.DoorLock.ServerCommandDefs.lock_door.id + ) -async def async_unlock(hass, cluster, entity_id): +async def async_unlock(hass: HomeAssistant, cluster: Cluster, entity_id: str): """Test lock functionality from hass.""" with patch("zigpy.zcl.Cluster.request", return_value=[zcl_f.Status.SUCCESS]): # lock via UI @@ -124,10 +126,13 @@ async def async_unlock(hass, cluster, entity_id): ) assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False - assert cluster.request.call_args[0][1] == UNLOCK_DOOR + assert ( + cluster.request.call_args[0][1] + == closures.DoorLock.ServerCommandDefs.unlock_door.id + ) -async def async_set_user_code(hass, cluster, entity_id): +async def async_set_user_code(hass: HomeAssistant, cluster: Cluster, entity_id: str): """Test set lock code functionality from hass.""" with patch("zigpy.zcl.Cluster.request", return_value=[zcl_f.Status.SUCCESS]): # set lock code via service call @@ -139,7 +144,10 @@ async def async_set_user_code(hass, cluster, entity_id): ) assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False - assert cluster.request.call_args[0][1] == SET_PIN_CODE + assert ( + cluster.request.call_args[0][1] + == closures.DoorLock.ServerCommandDefs.set_pin_code.id + ) assert cluster.request.call_args[0][3] == 2 # user slot 3 => internal slot 2 assert cluster.request.call_args[0][4] == closures.DoorLock.UserStatus.Enabled assert ( @@ -148,7 +156,7 @@ async def async_set_user_code(hass, cluster, entity_id): assert cluster.request.call_args[0][6] == "13246579" -async def async_clear_user_code(hass, cluster, entity_id): +async def async_clear_user_code(hass: HomeAssistant, cluster: Cluster, entity_id: str): """Test clear lock code functionality from hass.""" with patch("zigpy.zcl.Cluster.request", return_value=[zcl_f.Status.SUCCESS]): # set lock code via service call @@ -163,11 +171,14 @@ async def async_clear_user_code(hass, cluster, entity_id): ) assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False - assert cluster.request.call_args[0][1] == CLEAR_PIN_CODE + assert ( + cluster.request.call_args[0][1] + == closures.DoorLock.ServerCommandDefs.clear_pin_code.id + ) assert cluster.request.call_args[0][3] == 2 # user slot 3 => internal slot 2 -async def async_enable_user_code(hass, cluster, entity_id): +async def async_enable_user_code(hass: HomeAssistant, cluster: Cluster, entity_id: str): """Test enable lock code functionality from hass.""" with patch("zigpy.zcl.Cluster.request", return_value=[zcl_f.Status.SUCCESS]): # set lock code via service call @@ -182,12 +193,17 @@ async def async_enable_user_code(hass, cluster, entity_id): ) assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False - assert cluster.request.call_args[0][1] == SET_USER_STATUS + assert ( + cluster.request.call_args[0][1] + == closures.DoorLock.ServerCommandDefs.set_user_status.id + ) assert cluster.request.call_args[0][3] == 2 # user slot 3 => internal slot 2 assert cluster.request.call_args[0][4] == closures.DoorLock.UserStatus.Enabled -async def async_disable_user_code(hass, cluster, entity_id): +async def async_disable_user_code( + hass: HomeAssistant, cluster: Cluster, entity_id: str +): """Test disable lock code functionality from hass.""" with patch("zigpy.zcl.Cluster.request", return_value=[zcl_f.Status.SUCCESS]): # set lock code via service call @@ -202,6 +218,9 @@ async def async_disable_user_code(hass, cluster, entity_id): ) assert cluster.request.call_count == 1 assert cluster.request.call_args[0][0] is False - assert cluster.request.call_args[0][1] == SET_USER_STATUS + assert ( + cluster.request.call_args[0][1] + == closures.DoorLock.ServerCommandDefs.set_user_status.id + ) assert cluster.request.call_args[0][3] == 2 # user slot 3 => internal slot 2 assert cluster.request.call_args[0][4] == closures.DoorLock.UserStatus.Disabled diff --git a/tests/components/zha/test_logbook.py b/tests/components/zha/test_logbook.py index 19a6f9d359f..0b27cd095a9 100644 --- a/tests/components/zha/test_logbook.py +++ b/tests/components/zha/test_logbook.py @@ -3,10 +3,16 @@ from unittest.mock import patch import pytest +from zha.application.const import ZHA_EVENT import zigpy.profiles.zha from zigpy.zcl.clusters import general -from homeassistant.components.zha.core.const import ZHA_EVENT +from homeassistant.components.zha.helpers import ( + ZHADeviceProxy, + ZHAGatewayProxy, + get_zha_gateway, + get_zha_gateway_proxy, +) from homeassistant.const import CONF_DEVICE_ID, CONF_UNIQUE_ID, Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -40,9 +46,13 @@ def sensor_platform_only(): @pytest.fixture -async def mock_devices(hass, zigpy_device_mock, zha_device_joined): +async def mock_devices(hass: HomeAssistant, setup_zha, zigpy_device_mock): """IAS device fixture.""" + await setup_zha() + gateway = get_zha_gateway(hass) + gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass) + zigpy_device = zigpy_device_mock( { 1: { @@ -54,10 +64,13 @@ async def mock_devices(hass, zigpy_device_mock, zha_device_joined): } ) - zha_device = await zha_device_joined(zigpy_device) - zha_device.update_available(True) - await hass.async_block_till_done() - return zigpy_device, zha_device + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) + + zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee) + + return zigpy_device, zha_device_proxy async def test_zha_logbook_event_device_with_triggers( @@ -76,7 +89,7 @@ async def test_zha_logbook_event_device_with_triggers( (LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_HOLD}, } - ieee_address = str(zha_device.ieee) + ieee_address = str(zha_device.device.ieee) reg_device = device_registry.async_get_device(identifiers={("zha", ieee_address)}) @@ -153,7 +166,7 @@ async def test_zha_logbook_event_device_no_triggers( """Test ZHA logbook events with device and without triggers.""" zigpy_device, zha_device = mock_devices - ieee_address = str(zha_device.ieee) + ieee_address = str(zha_device.device.ieee) reg_device = device_registry.async_get_device(identifiers={("zha", ieee_address)}) hass.config.components.add("recorder") diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py index 6b302f9cbd9..180f16e9ae2 100644 --- a/tests/components/zha/test_number.py +++ b/tests/components/zha/test_number.py @@ -3,26 +3,22 @@ from unittest.mock import call, patch import pytest -from zigpy.exceptions import ZigbeeException from zigpy.profiles import zha -from zigpy.zcl.clusters import general, lighting +from zigpy.zcl.clusters import general import zigpy.zcl.foundation as zcl_f from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN -from homeassistant.components.zha.core.device import ZHADevice -from homeassistant.const import STATE_UNAVAILABLE, EntityCategory, Platform +from homeassistant.components.zha.helpers import ( + ZHADeviceProxy, + ZHAGatewayProxy, + get_zha_gateway, + get_zha_gateway_proxy, +) +from homeassistant.const import Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from .common import ( - async_enable_traffic, - async_test_rejoin, - find_entity_id, - send_attributes_report, - update_attribute_cache, -) +from .common import find_entity_id, send_attributes_report, update_attribute_cache from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE @@ -43,49 +39,28 @@ def number_platform_only(): yield -@pytest.fixture -def zigpy_analog_output_device(zigpy_device_mock): - """Zigpy analog_output device.""" - - endpoints = { - 1: { - SIG_EP_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, - SIG_EP_INPUT: [general.AnalogOutput.cluster_id, general.Basic.cluster_id], - SIG_EP_OUTPUT: [], - } - } - return zigpy_device_mock(endpoints) - - -@pytest.fixture -async def light(zigpy_device_mock): - """Siren fixture.""" - - return zigpy_device_mock( - { - 1: { - SIG_EP_PROFILE: zha.PROFILE_ID, - SIG_EP_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT, - SIG_EP_INPUT: [ - general.Basic.cluster_id, - general.Identify.cluster_id, - general.OnOff.cluster_id, - general.LevelControl.cluster_id, - lighting.Color.cluster_id, - ], - SIG_EP_OUTPUT: [general.Ota.cluster_id], - } - }, - node_descriptor=b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", - ) - - -async def test_number( - hass: HomeAssistant, zha_device_joined_restored, zigpy_analog_output_device -) -> None: +async def test_number(hass: HomeAssistant, setup_zha, zigpy_device_mock) -> None: """Test ZHA number platform.""" - cluster = zigpy_analog_output_device.endpoints.get(1).analog_output + await setup_zha() + gateway = get_zha_gateway(hass) + gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass) + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, + SIG_EP_INPUT: [ + general.AnalogOutput.cluster_id, + general.Basic.cluster_id, + ], + SIG_EP_OUTPUT: [], + SIG_EP_PROFILE: zha.PROFILE_ID, + } + } + ) + + cluster = zigpy_device.endpoints[1].analog_output cluster.PLUGGED_ATTR_READS = { "max_present_value": 100.0, "min_present_value": 1.0, @@ -98,34 +73,14 @@ async def test_number( update_attribute_cache(cluster) cluster.PLUGGED_ATTR_READS["present_value"] = 15.0 - zha_device = await zha_device_joined_restored(zigpy_analog_output_device) - # one for present_value and one for the rest configuration attributes - assert cluster.read_attributes.call_count == 3 - attr_reads = set() - for call_args in cluster.read_attributes.call_args_list: - attr_reads |= set(call_args[0][0]) - assert "max_present_value" in attr_reads - assert "min_present_value" in attr_reads - assert "relinquish_default" in attr_reads - assert "resolution" in attr_reads - assert "description" in attr_reads - assert "engineering_units" in attr_reads - assert "application_type" in attr_reads + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) - entity_id = find_entity_id(Platform.NUMBER, zha_device, hass) + zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee) + entity_id = find_entity_id(Platform.NUMBER, zha_device_proxy, hass) assert entity_id is not None - await async_enable_traffic(hass, [zha_device], enabled=False) - # test that the number was created and that it is unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and device - assert cluster.read_attributes.call_count == 3 - await async_enable_traffic(hass, [zha_device]) - await hass.async_block_till_done() - assert cluster.read_attributes.call_count == 6 - - # test that the state has changed from unavailable to 15.0 assert hass.states.get(entity_id).state == "15.0" # test attributes @@ -134,13 +89,13 @@ async def test_number( assert hass.states.get(entity_id).attributes.get("step") == 1.1 assert hass.states.get(entity_id).attributes.get("icon") == "mdi:percent" assert hass.states.get(entity_id).attributes.get("unit_of_measurement") == "%" + assert ( hass.states.get(entity_id).attributes.get("friendly_name") == "FakeManufacturer FakeModel Number PWM1" ) # change value from device - assert cluster.read_attributes.call_count == 6 await send_attributes_report(hass, cluster, {0x0055: 15}) assert hass.states.get(entity_id).state == "15.0" @@ -165,16 +120,8 @@ async def test_number( ] cluster.PLUGGED_ATTR_READS["present_value"] = 30.0 - # test rejoin - assert cluster.read_attributes.call_count == 6 - await async_test_rejoin(hass, zigpy_analog_output_device, [cluster], (1,)) - assert hass.states.get(entity_id).state == "30.0" - assert cluster.read_attributes.call_count == 9 - # update device value with failed attribute report cluster.PLUGGED_ATTR_READS["present_value"] = 40.0 - # validate the entity still contains old value - assert hass.states.get(entity_id).state == "30.0" await async_setup_component(hass, "homeassistant", {}) await hass.async_block_till_done() @@ -183,251 +130,4 @@ async def test_number( "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True ) assert hass.states.get(entity_id).state == "40.0" - assert cluster.read_attributes.call_count == 10 assert "present_value" in cluster.read_attributes.call_args[0][0] - - -@pytest.mark.parametrize( - ("attr", "initial_value", "new_value"), - [ - ("on_off_transition_time", 20, 5), - ("on_level", 255, 50), - ("on_transition_time", 5, 1), - ("off_transition_time", 5, 1), - ("default_move_rate", 1, 5), - ("start_up_current_level", 254, 125), - ], -) -async def test_level_control_number( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - light: ZHADevice, - zha_device_joined, - attr: str, - initial_value: int, - new_value: int, -) -> None: - """Test ZHA level control number entities - new join.""" - level_control_cluster = light.endpoints[1].level - level_control_cluster.PLUGGED_ATTR_READS = { - attr: initial_value, - } - zha_device = await zha_device_joined(light) - - entity_id = find_entity_id( - Platform.NUMBER, - zha_device, - hass, - qualifier=attr, - ) - assert entity_id is not None - - assert level_control_cluster.read_attributes.mock_calls == [ - call( - [ - "on_off_transition_time", - "on_level", - "on_transition_time", - "off_transition_time", - "default_move_rate", - ], - allow_cache=True, - only_cache=False, - manufacturer=None, - ), - call( - ["start_up_current_level"], - allow_cache=True, - only_cache=False, - manufacturer=None, - ), - call( - [ - "current_level", - ], - allow_cache=False, - only_cache=False, - manufacturer=None, - ), - ] - - state = hass.states.get(entity_id) - assert state - assert state.state == str(initial_value) - - entity_entry = entity_registry.async_get(entity_id) - assert entity_entry - assert entity_entry.entity_category == EntityCategory.CONFIG - - # Test number set_value - await hass.services.async_call( - "number", - "set_value", - { - "entity_id": entity_id, - "value": new_value, - }, - blocking=True, - ) - - assert level_control_cluster.write_attributes.mock_calls == [ - call({attr: new_value}, manufacturer=None) - ] - - state = hass.states.get(entity_id) - assert state - assert state.state == str(new_value) - - level_control_cluster.read_attributes.reset_mock() - await async_setup_component(hass, "homeassistant", {}) - await hass.async_block_till_done() - - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True - ) - # the mocking doesn't update the attr cache so this flips back to initial value - assert hass.states.get(entity_id).state == str(initial_value) - assert level_control_cluster.read_attributes.mock_calls == [ - call( - [attr], - allow_cache=False, - only_cache=False, - manufacturer=None, - ) - ] - - level_control_cluster.write_attributes.reset_mock() - level_control_cluster.write_attributes.side_effect = ZigbeeException - - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - "number", - "set_value", - { - "entity_id": entity_id, - "value": new_value, - }, - blocking=True, - ) - - assert level_control_cluster.write_attributes.mock_calls == [ - call({attr: new_value}, manufacturer=None), - call({attr: new_value}, manufacturer=None), - call({attr: new_value}, manufacturer=None), - ] - assert hass.states.get(entity_id).state == str(initial_value) - - -@pytest.mark.parametrize( - ("attr", "initial_value", "new_value"), - [("start_up_color_temperature", 500, 350)], -) -async def test_color_number( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - light: ZHADevice, - zha_device_joined, - attr: str, - initial_value: int, - new_value: int, -) -> None: - """Test ZHA color number entities - new join.""" - color_cluster = light.endpoints[1].light_color - color_cluster.PLUGGED_ATTR_READS = { - attr: initial_value, - } - zha_device = await zha_device_joined(light) - - entity_id = find_entity_id( - Platform.NUMBER, - zha_device, - hass, - qualifier=attr, - ) - assert entity_id is not None - - assert color_cluster.read_attributes.call_count == 3 - assert ( - call( - [ - "color_temp_physical_min", - "color_temp_physical_max", - "color_capabilities", - "start_up_color_temperature", - "options", - ], - allow_cache=True, - only_cache=False, - manufacturer=None, - ) - in color_cluster.read_attributes.call_args_list - ) - - state = hass.states.get(entity_id) - assert state - assert state.state == str(initial_value) - - entity_entry = entity_registry.async_get(entity_id) - assert entity_entry - assert entity_entry.entity_category == EntityCategory.CONFIG - - # Test number set_value - await hass.services.async_call( - "number", - "set_value", - { - "entity_id": entity_id, - "value": new_value, - }, - blocking=True, - ) - - assert color_cluster.write_attributes.call_count == 1 - assert color_cluster.write_attributes.call_args[0][0] == { - attr: new_value, - } - - state = hass.states.get(entity_id) - assert state - assert state.state == str(new_value) - - color_cluster.read_attributes.reset_mock() - await async_setup_component(hass, "homeassistant", {}) - await hass.async_block_till_done() - - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True - ) - # the mocking doesn't update the attr cache so this flips back to initial value - assert hass.states.get(entity_id).state == str(initial_value) - assert color_cluster.read_attributes.call_count == 1 - assert ( - call( - [attr], - allow_cache=False, - only_cache=False, - manufacturer=None, - ) - in color_cluster.read_attributes.call_args_list - ) - - color_cluster.write_attributes.reset_mock() - color_cluster.write_attributes.side_effect = ZigbeeException - - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - "number", - "set_value", - { - "entity_id": entity_id, - "value": new_value, - }, - blocking=True, - ) - - assert color_cluster.write_attributes.mock_calls == [ - call({attr: new_value}, manufacturer=None), - call({attr: new_value}, manufacturer=None), - call({attr: new_value}, manufacturer=None), - ] - assert hass.states.get(entity_id).state == str(initial_value) diff --git a/tests/components/zha/test_radio_manager.py b/tests/components/zha/test_radio_manager.py index 5e92991cbb4..0a51aaa6dba 100644 --- a/tests/components/zha/test_radio_manager.py +++ b/tests/components/zha/test_radio_manager.py @@ -5,6 +5,7 @@ from unittest.mock import AsyncMock, MagicMock, create_autospec, patch import pytest import serial.tools.list_ports +from zha.application.const import RadioType from zigpy.backups import BackupManager import zigpy.config from zigpy.config import CONF_DEVICE_PATH @@ -12,7 +13,7 @@ import zigpy.types from homeassistant.components.usb import UsbServiceInfo from homeassistant.components.zha import radio_manager -from homeassistant.components.zha.core.const import DOMAIN, RadioType +from homeassistant.components.zha.const import DOMAIN from homeassistant.components.zha.radio_manager import ProbeResult, ZhaRadioManager from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant diff --git a/tests/components/zha/test_registries.py b/tests/components/zha/test_registries.py deleted file mode 100644 index 7d1831650f3..00000000000 --- a/tests/components/zha/test_registries.py +++ /dev/null @@ -1,602 +0,0 @@ -"""Test ZHA registries.""" - -from __future__ import annotations - -from collections.abc import Generator -from unittest import mock - -import pytest -import zigpy.quirks as zigpy_quirks - -from homeassistant.components.zha.binary_sensor import IASZone -from homeassistant.components.zha.core import registries -from homeassistant.components.zha.core.const import ATTR_QUIRK_ID -from homeassistant.components.zha.entity import ZhaEntity -from homeassistant.helpers import entity_registry as er - -MANUFACTURER = "mock manufacturer" -MODEL = "mock model" -QUIRK_CLASS = "mock.test.quirk.class" -QUIRK_ID = "quirk_id" - - -@pytest.fixture -def zha_device(): - """Return a mock of ZHA device.""" - dev = mock.MagicMock() - dev.manufacturer = MANUFACTURER - dev.model = MODEL - dev.quirk_class = QUIRK_CLASS - dev.quirk_id = QUIRK_ID - return dev - - -@pytest.fixture -def cluster_handlers(cluster_handler): - """Return a mock of cluster_handlers.""" - - return [cluster_handler("level", 8), cluster_handler("on_off", 6)] - - -@pytest.mark.parametrize( - ("rule", "matched"), - [ - (registries.MatchRule(), False), - (registries.MatchRule(cluster_handler_names={"level"}), True), - (registries.MatchRule(cluster_handler_names={"level", "no match"}), False), - (registries.MatchRule(cluster_handler_names={"on_off"}), True), - (registries.MatchRule(cluster_handler_names={"on_off", "no match"}), False), - (registries.MatchRule(cluster_handler_names={"on_off", "level"}), True), - ( - registries.MatchRule(cluster_handler_names={"on_off", "level", "no match"}), - False, - ), - # test generic_id matching - (registries.MatchRule(generic_ids={"cluster_handler_0x0006"}), True), - (registries.MatchRule(generic_ids={"cluster_handler_0x0008"}), True), - ( - registries.MatchRule( - generic_ids={"cluster_handler_0x0006", "cluster_handler_0x0008"} - ), - True, - ), - ( - registries.MatchRule( - generic_ids={ - "cluster_handler_0x0006", - "cluster_handler_0x0008", - "cluster_handler_0x0009", - } - ), - False, - ), - ( - registries.MatchRule( - generic_ids={"cluster_handler_0x0006", "cluster_handler_0x0008"}, - cluster_handler_names={"on_off", "level"}, - ), - True, - ), - # manufacturer matching - (registries.MatchRule(manufacturers="no match"), False), - (registries.MatchRule(manufacturers=MANUFACTURER), True), - ( - registries.MatchRule( - manufacturers="no match", aux_cluster_handlers="aux_cluster_handler" - ), - False, - ), - ( - registries.MatchRule( - manufacturers=MANUFACTURER, aux_cluster_handlers="aux_cluster_handler" - ), - True, - ), - (registries.MatchRule(models=MODEL), True), - (registries.MatchRule(models="no match"), False), - ( - registries.MatchRule( - models=MODEL, aux_cluster_handlers="aux_cluster_handler" - ), - True, - ), - ( - registries.MatchRule( - models="no match", aux_cluster_handlers="aux_cluster_handler" - ), - False, - ), - (registries.MatchRule(quirk_ids=QUIRK_ID), True), - (registries.MatchRule(quirk_ids="no match"), False), - ( - registries.MatchRule( - quirk_ids=QUIRK_ID, aux_cluster_handlers="aux_cluster_handler" - ), - True, - ), - ( - registries.MatchRule( - quirk_ids="no match", aux_cluster_handlers="aux_cluster_handler" - ), - False, - ), - # match everything - ( - registries.MatchRule( - generic_ids={"cluster_handler_0x0006", "cluster_handler_0x0008"}, - cluster_handler_names={"on_off", "level"}, - manufacturers=MANUFACTURER, - models=MODEL, - quirk_ids=QUIRK_ID, - ), - True, - ), - ( - registries.MatchRule( - cluster_handler_names="on_off", - manufacturers={"random manuf", MANUFACTURER}, - ), - True, - ), - ( - registries.MatchRule( - cluster_handler_names="on_off", - manufacturers={"random manuf", "Another manuf"}, - ), - False, - ), - ( - registries.MatchRule( - cluster_handler_names="on_off", - manufacturers=lambda x: x == MANUFACTURER, - ), - True, - ), - ( - registries.MatchRule( - cluster_handler_names="on_off", - manufacturers=lambda x: x != MANUFACTURER, - ), - False, - ), - ( - registries.MatchRule( - cluster_handler_names="on_off", models={"random model", MODEL} - ), - True, - ), - ( - registries.MatchRule( - cluster_handler_names="on_off", models={"random model", "Another model"} - ), - False, - ), - ( - registries.MatchRule( - cluster_handler_names="on_off", models=lambda x: x == MODEL - ), - True, - ), - ( - registries.MatchRule( - cluster_handler_names="on_off", models=lambda x: x != MODEL - ), - False, - ), - ( - registries.MatchRule( - cluster_handler_names="on_off", - quirk_ids={"random quirk", QUIRK_ID}, - ), - True, - ), - ( - registries.MatchRule( - cluster_handler_names="on_off", - quirk_ids={"random quirk", "another quirk"}, - ), - False, - ), - ( - registries.MatchRule( - cluster_handler_names="on_off", quirk_ids=lambda x: x == QUIRK_ID - ), - True, - ), - ( - registries.MatchRule( - cluster_handler_names="on_off", quirk_ids=lambda x: x != QUIRK_ID - ), - False, - ), - ( - registries.MatchRule(cluster_handler_names="on_off", quirk_ids=QUIRK_ID), - True, - ), - ], -) -def test_registry_matching(rule, matched, cluster_handlers) -> None: - """Test strict rule matching.""" - assert ( - rule.strict_matched(MANUFACTURER, MODEL, cluster_handlers, QUIRK_ID) is matched - ) - - -@pytest.mark.parametrize( - ("rule", "matched"), - [ - (registries.MatchRule(), False), - (registries.MatchRule(cluster_handler_names={"level"}), True), - (registries.MatchRule(cluster_handler_names={"level", "no match"}), False), - (registries.MatchRule(cluster_handler_names={"on_off"}), True), - (registries.MatchRule(cluster_handler_names={"on_off", "no match"}), False), - (registries.MatchRule(cluster_handler_names={"on_off", "level"}), True), - ( - registries.MatchRule(cluster_handler_names={"on_off", "level", "no match"}), - False, - ), - ( - registries.MatchRule( - cluster_handler_names={"on_off", "level"}, models="no match" - ), - True, - ), - ( - registries.MatchRule( - cluster_handler_names={"on_off", "level"}, - models="no match", - manufacturers="no match", - ), - True, - ), - ( - registries.MatchRule( - cluster_handler_names={"on_off", "level"}, - models="no match", - manufacturers=MANUFACTURER, - ), - True, - ), - # test generic_id matching - (registries.MatchRule(generic_ids={"cluster_handler_0x0006"}), True), - (registries.MatchRule(generic_ids={"cluster_handler_0x0008"}), True), - ( - registries.MatchRule( - generic_ids={"cluster_handler_0x0006", "cluster_handler_0x0008"} - ), - True, - ), - ( - registries.MatchRule( - generic_ids={ - "cluster_handler_0x0006", - "cluster_handler_0x0008", - "cluster_handler_0x0009", - } - ), - False, - ), - ( - registries.MatchRule( - generic_ids={ - "cluster_handler_0x0006", - "cluster_handler_0x0008", - "cluster_handler_0x0009", - }, - models="mo match", - ), - False, - ), - ( - registries.MatchRule( - generic_ids={ - "cluster_handler_0x0006", - "cluster_handler_0x0008", - "cluster_handler_0x0009", - }, - models=MODEL, - ), - True, - ), - ( - registries.MatchRule( - generic_ids={"cluster_handler_0x0006", "cluster_handler_0x0008"}, - cluster_handler_names={"on_off", "level"}, - ), - True, - ), - # manufacturer matching - (registries.MatchRule(manufacturers="no match"), False), - (registries.MatchRule(manufacturers=MANUFACTURER), True), - (registries.MatchRule(models=MODEL), True), - (registries.MatchRule(models="no match"), False), - (registries.MatchRule(quirk_ids=QUIRK_ID), True), - (registries.MatchRule(quirk_ids="no match"), False), - # match everything - ( - registries.MatchRule( - generic_ids={"cluster_handler_0x0006", "cluster_handler_0x0008"}, - cluster_handler_names={"on_off", "level"}, - manufacturers=MANUFACTURER, - models=MODEL, - quirk_ids=QUIRK_ID, - ), - True, - ), - ], -) -def test_registry_loose_matching(rule, matched, cluster_handlers) -> None: - """Test loose rule matching.""" - assert ( - rule.loose_matched(MANUFACTURER, MODEL, cluster_handlers, QUIRK_ID) is matched - ) - - -def test_match_rule_claim_cluster_handlers_color(cluster_handler) -> None: - """Test cluster handler claiming.""" - ch_color = cluster_handler("color", 0x300) - ch_level = cluster_handler("level", 8) - ch_onoff = cluster_handler("on_off", 6) - - rule = registries.MatchRule( - cluster_handler_names="on_off", aux_cluster_handlers={"color", "level"} - ) - claimed = rule.claim_cluster_handlers([ch_color, ch_level, ch_onoff]) - assert {"color", "level", "on_off"} == {ch.name for ch in claimed} - - -@pytest.mark.parametrize( - ("rule", "match"), - [ - (registries.MatchRule(cluster_handler_names={"level"}), {"level"}), - (registries.MatchRule(cluster_handler_names={"level", "no match"}), {"level"}), - (registries.MatchRule(cluster_handler_names={"on_off"}), {"on_off"}), - (registries.MatchRule(generic_ids="cluster_handler_0x0000"), {"basic"}), - ( - registries.MatchRule( - cluster_handler_names="level", generic_ids="cluster_handler_0x0000" - ), - {"basic", "level"}, - ), - ( - registries.MatchRule(cluster_handler_names={"level", "power"}), - {"level", "power"}, - ), - ( - registries.MatchRule( - cluster_handler_names={"level", "on_off"}, - aux_cluster_handlers={"basic", "power"}, - ), - {"basic", "level", "on_off", "power"}, - ), - (registries.MatchRule(cluster_handler_names={"color"}), set()), - ], -) -def test_match_rule_claim_cluster_handlers( - rule, match, cluster_handler, cluster_handlers -) -> None: - """Test cluster handler claiming.""" - ch_basic = cluster_handler("basic", 0) - cluster_handlers.append(ch_basic) - ch_power = cluster_handler("power", 1) - cluster_handlers.append(ch_power) - - claimed = rule.claim_cluster_handlers(cluster_handlers) - assert match == {ch.name for ch in claimed} - - -@pytest.fixture -def entity_registry(): - """Registry fixture.""" - return registries.ZHAEntityRegistry() - - -@pytest.mark.parametrize( - ("manufacturer", "model", "quirk_id", "match_name"), - [ - ("random manufacturer", "random model", "random.class", "OnOff"), - ("random manufacturer", MODEL, "random.class", "OnOffModel"), - (MANUFACTURER, "random model", "random.class", "OnOffManufacturer"), - ("random manufacturer", "random model", QUIRK_ID, "OnOffQuirk"), - (MANUFACTURER, MODEL, "random.class", "OnOffModelManufacturer"), - (MANUFACTURER, "some model", "random.class", "OnOffMultimodel"), - ], -) -def test_weighted_match( - cluster_handler, - entity_registry: er.EntityRegistry, - manufacturer, - model, - quirk_id, - match_name, -) -> None: - """Test weightedd match.""" - - s = mock.sentinel - - @entity_registry.strict_match( - s.component, - cluster_handler_names="on_off", - models={MODEL, "another model", "some model"}, - ) - class OnOffMultimodel: - pass - - @entity_registry.strict_match(s.component, cluster_handler_names="on_off") - class OnOff: - pass - - @entity_registry.strict_match( - s.component, cluster_handler_names="on_off", manufacturers=MANUFACTURER - ) - class OnOffManufacturer: - pass - - @entity_registry.strict_match( - s.component, cluster_handler_names="on_off", models=MODEL - ) - class OnOffModel: - pass - - @entity_registry.strict_match( - s.component, - cluster_handler_names="on_off", - models=MODEL, - manufacturers=MANUFACTURER, - ) - class OnOffModelManufacturer: - pass - - @entity_registry.strict_match( - s.component, cluster_handler_names="on_off", quirk_ids=QUIRK_ID - ) - class OnOffQuirk: - pass - - ch_on_off = cluster_handler("on_off", 6) - ch_level = cluster_handler("level", 8) - - match, claimed = entity_registry.get_entity( - s.component, manufacturer, model, [ch_on_off, ch_level], quirk_id - ) - - assert match.__name__ == match_name - assert claimed == [ch_on_off] - - -def test_multi_sensor_match( - cluster_handler, entity_registry: er.EntityRegistry -) -> None: - """Test multi-entity match.""" - - s = mock.sentinel - - @entity_registry.multipass_match( - s.binary_sensor, - cluster_handler_names="smartenergy_metering", - ) - class SmartEnergySensor2: - pass - - ch_se = cluster_handler("smartenergy_metering", 0x0702) - ch_illuminati = cluster_handler("illuminance", 0x0401) - - match, claimed = entity_registry.get_multi_entity( - "manufacturer", - "model", - cluster_handlers=[ch_se, ch_illuminati], - quirk_id="quirk_id", - ) - - assert s.binary_sensor in match - assert s.component not in match - assert set(claimed) == {ch_se} - assert {cls.entity_class.__name__ for cls in match[s.binary_sensor]} == { - SmartEnergySensor2.__name__ - } - - @entity_registry.multipass_match( - s.component, - cluster_handler_names="smartenergy_metering", - aux_cluster_handlers="illuminance", - ) - class SmartEnergySensor1: - pass - - @entity_registry.multipass_match( - s.binary_sensor, - cluster_handler_names="smartenergy_metering", - aux_cluster_handlers="illuminance", - ) - class SmartEnergySensor3: - pass - - match, claimed = entity_registry.get_multi_entity( - "manufacturer", - "model", - cluster_handlers={ch_se, ch_illuminati}, - quirk_id="quirk_id", - ) - - assert s.binary_sensor in match - assert s.component in match - assert set(claimed) == {ch_se, ch_illuminati} - assert {cls.entity_class.__name__ for cls in match[s.binary_sensor]} == { - SmartEnergySensor2.__name__, - SmartEnergySensor3.__name__, - } - assert {cls.entity_class.__name__ for cls in match[s.component]} == { - SmartEnergySensor1.__name__ - } - - -def iter_all_rules() -> Generator[tuple[registries.MatchRule, list[type[ZhaEntity]]]]: - """Iterate over all match rules and their corresponding entities.""" - - for rules in registries.ZHA_ENTITIES._strict_registry.values(): - for rule, entity in rules.items(): - yield rule, [entity] - - for rules in registries.ZHA_ENTITIES._multi_entity_registry.values(): - for multi in rules.values(): - for rule, entities in multi.items(): - yield rule, entities - - for rules in registries.ZHA_ENTITIES._config_diagnostic_entity_registry.values(): - for multi in rules.values(): - for rule, entities in multi.items(): - yield rule, entities - - -def test_quirk_classes() -> None: - """Make sure that all quirk IDs in components matches exist.""" - - def quirk_class_validator(value): - """Validate quirk IDs during self test.""" - if callable(value): - # Callables cannot be tested - return - - if isinstance(value, (frozenset, set, list)): - for v in value: - # Unpack the value if needed - quirk_class_validator(v) - return - - if value not in all_quirk_ids: - raise ValueError(f"Quirk ID '{value}' does not exist.") - - # get all quirk ID from zigpy quirks registry - all_quirk_ids = [] - for manufacturer in zigpy_quirks._DEVICE_REGISTRY._registry.values(): - for model_quirk_list in manufacturer.values(): - for quirk in model_quirk_list: - quirk_id = getattr(quirk, ATTR_QUIRK_ID, None) - if quirk_id is not None and quirk_id not in all_quirk_ids: - all_quirk_ids.append(quirk_id) - # pylint: disable-next=undefined-loop-variable - del quirk, model_quirk_list, manufacturer - - # validate all quirk IDs used in component match rules - for rule, _ in iter_all_rules(): - quirk_class_validator(rule.quirk_ids) - - -def test_entity_names() -> None: - """Make sure that all handlers expose entities with valid names.""" - - for _, entity_classes in iter_all_rules(): - for entity_class in entity_classes: - if hasattr(entity_class, "__attr_name"): - # The entity has a name - assert (name := entity_class.__attr_name) and isinstance(name, str) - elif hasattr(entity_class, "__attr_translation_key"): - assert ( - isinstance(entity_class.__attr_translation_key, str) - and entity_class.__attr_translation_key - ) - elif hasattr(entity_class, "__attr_device_class"): - assert entity_class.__attr_device_class - else: - # The only exception (for now) is IASZone - assert entity_class is IASZone diff --git a/tests/components/zha/test_repairs.py b/tests/components/zha/test_repairs.py index c093fe266bd..7f9b2b4a016 100644 --- a/tests/components/zha/test_repairs.py +++ b/tests/components/zha/test_repairs.py @@ -16,7 +16,7 @@ from homeassistant.components.homeassistant_sky_connect.const import ( # pylint DOMAIN as SKYCONNECT_DOMAIN, ) from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN -from homeassistant.components.zha.core.const import DOMAIN +from homeassistant.components.zha.const import DOMAIN from homeassistant.components.zha.repairs.network_settings_inconsistent import ( ISSUE_INCONSISTENT_NETWORK_SETTINGS, ) @@ -148,7 +148,7 @@ async def test_multipan_firmware_repair( autospec=True, ), patch( - "homeassistant.components.zha.core.gateway.ZHAGateway.async_initialize", + "homeassistant.components.zha.Gateway.async_from_config", side_effect=RuntimeError(), ), patch( @@ -199,7 +199,7 @@ async def test_multipan_firmware_no_repair_on_probe_failure( autospec=True, ), patch( - "homeassistant.components.zha.core.gateway.ZHAGateway.async_initialize", + "homeassistant.components.zha.Gateway.async_from_config", side_effect=RuntimeError(), ), ): @@ -236,7 +236,7 @@ async def test_multipan_firmware_retry_on_probe_ezsp( autospec=True, ), patch( - "homeassistant.components.zha.core.gateway.ZHAGateway.async_initialize", + "homeassistant.components.zha.Gateway.async_from_config", side_effect=RuntimeError(), ), ): @@ -311,7 +311,7 @@ async def test_inconsistent_settings_keep_new( old_state = network_backup with patch( - "homeassistant.components.zha.core.gateway.ZHAGateway.async_initialize", + "homeassistant.components.zha.Gateway.async_from_config", side_effect=NetworkSettingsInconsistent( message="Network settings are inconsistent", new_state=new_state, @@ -390,7 +390,7 @@ async def test_inconsistent_settings_restore_old( old_state = network_backup with patch( - "homeassistant.components.zha.core.gateway.ZHAGateway.async_initialize", + "homeassistant.components.zha.Gateway.async_from_config", side_effect=NetworkSettingsInconsistent( message="Network settings are inconsistent", new_state=new_state, diff --git a/tests/components/zha/test_select.py b/tests/components/zha/test_select.py index 70f58ee4e6d..a39172b850e 100644 --- a/tests/components/zha/test_select.py +++ b/tests/components/zha/test_select.py @@ -1,34 +1,23 @@ """Test ZHA select entities.""" -from typing import Any -from unittest.mock import call, patch +from unittest.mock import patch import pytest -from zhaquirks import ( - DEVICE_TYPE, - ENDPOINTS, - INPUT_CLUSTERS, - OUTPUT_CLUSTERS, - PROFILE_ID, -) -from zigpy.const import SIG_EP_PROFILE +from zigpy.const import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE from zigpy.profiles import zha -from zigpy.quirks import CustomCluster, CustomDevice -from zigpy.quirks.v2 import CustomDeviceV2, add_to_registry_v2 -import zigpy.types as t from zigpy.zcl.clusters import general, security -from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster -from homeassistant.components.zha.select import AqaraMotionSensitivities +from homeassistant.components.zha.helpers import ( + ZHADeviceProxy, + ZHAGatewayProxy, + get_zha_gateway, + get_zha_gateway_proxy, +) from homeassistant.const import STATE_UNKNOWN, EntityCategory, Platform from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er, restore_state -from homeassistant.util import dt as dt_util +from homeassistant.helpers import entity_registry as er -from .common import async_enable_traffic, find_entity_id, send_attributes_report -from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE - -from tests.common import async_mock_load_restore_state_from_storage +from .common import find_entity_id @pytest.fixture(autouse=True) @@ -50,9 +39,17 @@ def select_select_only(): yield -@pytest.fixture -async def siren(hass, zigpy_device_mock, zha_device_joined_restored): - """Siren fixture.""" +async def test_select( + hass: HomeAssistant, + entity_registry: er.EntityRegistry, + setup_zha, + zigpy_device_mock, +) -> None: + """Test ZHA select platform.""" + + await setup_zha() + gateway = get_zha_gateway(hass) + gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass) zigpy_device = zigpy_device_mock( { @@ -62,75 +59,16 @@ async def siren(hass, zigpy_device_mock, zha_device_joined_restored): SIG_EP_TYPE: zha.DeviceType.IAS_WARNING_DEVICE, SIG_EP_PROFILE: zha.PROFILE_ID, } - }, - ) - - zha_device = await zha_device_joined_restored(zigpy_device) - return zha_device, zigpy_device.endpoints[1].ias_wd - - -@pytest.fixture -async def light(hass, zigpy_device_mock): - """Siren fixture.""" - - return zigpy_device_mock( - { - 1: { - SIG_EP_PROFILE: zha.PROFILE_ID, - SIG_EP_TYPE: zha.DeviceType.ON_OFF_LIGHT, - SIG_EP_INPUT: [ - general.Basic.cluster_id, - general.Identify.cluster_id, - general.OnOff.cluster_id, - ], - SIG_EP_OUTPUT: [general.Ota.cluster_id], - } - }, - node_descriptor=b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", - ) - - -@pytest.fixture -def core_rs(hass_storage: dict[str, Any]): - """Core.restore_state fixture.""" - - def _storage(entity_id, state): - now = dt_util.utcnow().isoformat() - - hass_storage[restore_state.STORAGE_KEY] = { - "version": restore_state.STORAGE_VERSION, - "key": restore_state.STORAGE_KEY, - "data": [ - { - "state": { - "entity_id": entity_id, - "state": str(state), - "last_changed": now, - "last_updated": now, - "context": { - "id": "3c2243ff5f30447eb12e7348cfd5b8ff", - "user_id": None, - }, - }, - "last_seen": now, - } - ], } + ) - return _storage + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) - -async def test_select( - hass: HomeAssistant, entity_registry: er.EntityRegistry, siren -) -> None: - """Test ZHA select platform.""" - zha_device, cluster = siren - assert cluster is not None + zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee) entity_id = find_entity_id( - Platform.SELECT, - zha_device, - hass, - qualifier="tone", + Platform.SELECT, zha_device_proxy, hass, qualifier="tone" ) assert entity_id is not None @@ -165,329 +103,3 @@ async def test_select( state = hass.states.get(entity_id) assert state assert state.state == security.IasWd.Warning.WarningMode.Burglar.name - - -async def test_select_restore_state( - hass: HomeAssistant, - zigpy_device_mock, - core_rs, - zha_device_restored, -) -> None: - """Test ZHA select entity restore state.""" - - entity_id = "select.fakemanufacturer_fakemodel_default_siren_tone" - core_rs(entity_id, state="Burglar") - await async_mock_load_restore_state_from_storage(hass) - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [general.Basic.cluster_id, security.IasWd.cluster_id], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.IAS_WARNING_DEVICE, - SIG_EP_PROFILE: zha.PROFILE_ID, - } - }, - ) - - zha_device = await zha_device_restored(zigpy_device) - cluster = zigpy_device.endpoints[1].ias_wd - assert cluster is not None - entity_id = find_entity_id( - Platform.SELECT, - zha_device, - hass, - qualifier="tone", - ) - - assert entity_id is not None - state = hass.states.get(entity_id) - assert state - assert state.state == security.IasWd.Warning.WarningMode.Burglar.name - - -async def test_on_off_select_new_join( - hass: HomeAssistant, entity_registry: er.EntityRegistry, light, zha_device_joined -) -> None: - """Test ZHA on off select - new join.""" - on_off_cluster = light.endpoints[1].on_off - on_off_cluster.PLUGGED_ATTR_READS = { - "start_up_on_off": general.OnOff.StartUpOnOff.On - } - zha_device = await zha_device_joined(light) - select_name = "start_up_behavior" - entity_id = find_entity_id( - Platform.SELECT, - zha_device, - hass, - qualifier=select_name, - ) - assert entity_id is not None - - assert on_off_cluster.read_attributes.call_count == 2 - assert ( - call(["start_up_on_off"], allow_cache=True, only_cache=False, manufacturer=None) - in on_off_cluster.read_attributes.call_args_list - ) - assert ( - call(["on_off"], allow_cache=False, only_cache=False, manufacturer=None) - in on_off_cluster.read_attributes.call_args_list - ) - - state = hass.states.get(entity_id) - assert state - assert state.state == general.OnOff.StartUpOnOff.On.name - - assert state.attributes["options"] == ["Off", "On", "Toggle", "PreviousValue"] - - entity_entry = entity_registry.async_get(entity_id) - assert entity_entry - assert entity_entry.entity_category == EntityCategory.CONFIG - - # Test select option with string value - await hass.services.async_call( - "select", - "select_option", - { - "entity_id": entity_id, - "option": general.OnOff.StartUpOnOff.Off.name, - }, - blocking=True, - ) - - assert on_off_cluster.write_attributes.call_count == 1 - assert on_off_cluster.write_attributes.call_args[0][0] == { - "start_up_on_off": general.OnOff.StartUpOnOff.Off - } - - state = hass.states.get(entity_id) - assert state - assert state.state == general.OnOff.StartUpOnOff.Off.name - - -async def test_on_off_select_restored( - hass: HomeAssistant, entity_registry: er.EntityRegistry, light, zha_device_restored -) -> None: - """Test ZHA on off select - restored.""" - on_off_cluster = light.endpoints[1].on_off - on_off_cluster.PLUGGED_ATTR_READS = { - "start_up_on_off": general.OnOff.StartUpOnOff.On - } - zha_device = await zha_device_restored(light) - - assert zha_device.is_mains_powered - - assert on_off_cluster.read_attributes.call_count == 4 - # first 2 calls hit cache only - assert ( - call(["start_up_on_off"], allow_cache=True, only_cache=True, manufacturer=None) - in on_off_cluster.read_attributes.call_args_list - ) - assert ( - call(["on_off"], allow_cache=True, only_cache=True, manufacturer=None) - in on_off_cluster.read_attributes.call_args_list - ) - - # 2nd set of calls can actually read from the device - assert ( - call(["start_up_on_off"], allow_cache=True, only_cache=False, manufacturer=None) - in on_off_cluster.read_attributes.call_args_list - ) - assert ( - call(["on_off"], allow_cache=False, only_cache=False, manufacturer=None) - in on_off_cluster.read_attributes.call_args_list - ) - - select_name = "start_up_behavior" - entity_id = find_entity_id( - Platform.SELECT, - zha_device, - hass, - qualifier=select_name, - ) - assert entity_id is not None - - state = hass.states.get(entity_id) - assert state - assert state.state == general.OnOff.StartUpOnOff.On.name - assert state.attributes["options"] == ["Off", "On", "Toggle", "PreviousValue"] - - entity_entry = entity_registry.async_get(entity_id) - assert entity_entry - assert entity_entry.entity_category == EntityCategory.CONFIG - - -async def test_on_off_select_unsupported( - hass: HomeAssistant, light, zha_device_joined_restored -) -> None: - """Test ZHA on off select unsupported.""" - - on_off_cluster = light.endpoints[1].on_off - on_off_cluster.add_unsupported_attribute("start_up_on_off") - zha_device = await zha_device_joined_restored(light) - select_name = general.OnOff.StartUpOnOff.__name__ - entity_id = find_entity_id( - Platform.SELECT, - zha_device, - hass, - qualifier=select_name.lower(), - ) - assert entity_id is None - - -class MotionSensitivityQuirk(CustomDevice): - """Quirk with motion sensitivity attribute.""" - - class OppleCluster(CustomCluster, ManufacturerSpecificCluster): - """Aqara manufacturer specific cluster.""" - - cluster_id = 0xFCC0 - ep_attribute = "opple_cluster" - attributes = { - 0x010C: ("motion_sensitivity", t.uint8_t, True), - 0x020C: ("motion_sensitivity_disabled", t.uint8_t, True), - } - - def __init__(self, *args, **kwargs): - """Initialize.""" - super().__init__(*args, **kwargs) - # populate cache to create config entity - self._attr_cache.update( - { - 0x010C: AqaraMotionSensitivities.Medium, - 0x020C: AqaraMotionSensitivities.Medium, - } - ) - - replacement = { - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, - INPUT_CLUSTERS: [general.Basic.cluster_id, OppleCluster], - OUTPUT_CLUSTERS: [], - }, - } - } - - -@pytest.fixture -async def zigpy_device_aqara_sensor(hass, zigpy_device_mock, zha_device_joined): - """Device tracker zigpy Aqara motion sensor device.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [general.Basic.cluster_id], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, - } - }, - manufacturer="LUMI", - model="lumi.motion.ac02", - quirk=MotionSensitivityQuirk, - ) - - zha_device = await zha_device_joined(zigpy_device) - zha_device.available = True - await hass.async_block_till_done() - return zigpy_device - - -async def test_on_off_select_attribute_report( - hass: HomeAssistant, light, zha_device_restored, zigpy_device_aqara_sensor -) -> None: - """Test ZHA attribute report parsing for select platform.""" - - zha_device = await zha_device_restored(zigpy_device_aqara_sensor) - cluster = zigpy_device_aqara_sensor.endpoints.get(1).opple_cluster - entity_id = find_entity_id(Platform.SELECT, zha_device, hass) - assert entity_id is not None - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - - # test that the state is in default medium state - assert hass.states.get(entity_id).state == AqaraMotionSensitivities.Medium.name - - # send attribute report from device - await send_attributes_report( - hass, cluster, {"motion_sensitivity": AqaraMotionSensitivities.Low} - ) - assert hass.states.get(entity_id).state == AqaraMotionSensitivities.Low.name - - -( - add_to_registry_v2("Fake_Manufacturer", "Fake_Model") - .replaces(MotionSensitivityQuirk.OppleCluster) - .enum( - "motion_sensitivity", - AqaraMotionSensitivities, - MotionSensitivityQuirk.OppleCluster.cluster_id, - ) - .enum( - "motion_sensitivity_disabled", - AqaraMotionSensitivities, - MotionSensitivityQuirk.OppleCluster.cluster_id, - translation_key="motion_sensitivity", - initially_disabled=True, - ) -) - - -@pytest.fixture -async def zigpy_device_aqara_sensor_v2( - hass: HomeAssistant, zigpy_device_mock, zha_device_joined_restored -): - """Device tracker zigpy Aqara motion sensor device.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [ - general.Basic.cluster_id, - MotionSensitivityQuirk.OppleCluster.cluster_id, - ], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, - } - }, - manufacturer="Fake_Manufacturer", - model="Fake_Model", - ) - - zha_device = await zha_device_joined_restored(zigpy_device) - return zha_device, zigpy_device.endpoints[1].opple_cluster - - -async def test_on_off_select_attribute_report_v2( - hass: HomeAssistant, - entity_registry: er.EntityRegistry, - zigpy_device_aqara_sensor_v2, -) -> None: - """Test ZHA attribute report parsing for select platform.""" - - zha_device, cluster = zigpy_device_aqara_sensor_v2 - assert isinstance(zha_device.device, CustomDeviceV2) - entity_id = find_entity_id( - Platform.SELECT, zha_device, hass, qualifier="motion_sensitivity" - ) - assert entity_id is not None - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - - # test that the state is in default medium state - assert hass.states.get(entity_id).state == AqaraMotionSensitivities.Medium.name - - # send attribute report from device - await send_attributes_report( - hass, cluster, {"motion_sensitivity": AqaraMotionSensitivities.Low} - ) - assert hass.states.get(entity_id).state == AqaraMotionSensitivities.Low.name - - entity_entry = entity_registry.async_get(entity_id) - assert entity_entry - assert entity_entry.entity_category == EntityCategory.CONFIG - assert entity_entry.disabled is False - assert entity_entry.translation_key == "motion_sensitivity" diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index 8443c4ced07..2d69cf1ff36 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -1,33 +1,20 @@ """Test ZHA sensor.""" -from collections.abc import Callable -from datetime import timedelta -import math -from typing import Any -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest -from zhaquirks.danfoss import thermostat as danfoss_thermostat -import zigpy.profiles.zha -from zigpy.quirks import CustomCluster -from zigpy.quirks.v2 import CustomDeviceV2, add_to_registry_v2 -from zigpy.quirks.v2.homeassistant import UnitOfMass -import zigpy.types as t +from zigpy.profiles import zha +from zigpy.zcl import Cluster from zigpy.zcl.clusters import general, homeautomation, hvac, measurement, smartenergy from zigpy.zcl.clusters.hvac import Thermostat -from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster from homeassistant.components.sensor import SensorDeviceClass -from homeassistant.components.zha.core import ZHADevice -from homeassistant.components.zha.core.const import ZHA_CLUSTER_HANDLER_READS_PER_REQ -import homeassistant.config as config_util +from homeassistant.components.zha.helpers import get_zha_gateway from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT, - CONF_UNIT_SYSTEM, LIGHT_LUX, PERCENTAGE, - STATE_UNAVAILABLE, STATE_UNKNOWN, Platform, UnitOfApparentPower, @@ -37,29 +24,12 @@ from homeassistant.const import ( UnitOfPower, UnitOfPressure, UnitOfTemperature, - UnitOfVolume, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er, restore_state -from homeassistant.helpers.entity_component import async_update_entity -from homeassistant.util import dt as dt_util -from .common import ( - async_enable_traffic, - async_test_rejoin, - find_entity_id, - find_entity_ids, - send_attribute_report, - send_attributes_report, -) +from .common import send_attributes_report from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE -from tests.common import ( - MockConfigEntry, - async_fire_time_changed, - async_mock_load_restore_state_from_storage, -) - ENTITY_ID_PREFIX = "sensor.fakemanufacturer_fakemodel_{}" @@ -76,60 +46,19 @@ def sensor_platform_only(): yield -@pytest.fixture -async def elec_measurement_zigpy_dev(hass: HomeAssistant, zigpy_device_mock): - """Electric Measurement zigpy device.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [ - general.Basic.cluster_id, - homeautomation.ElectricalMeasurement.cluster_id, - ], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.SIMPLE_SENSOR, - SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, - } - }, - ) - zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100 - zigpy_device.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS = { - "ac_current_divisor": 10, - "ac_current_multiplier": 1, - "ac_power_divisor": 10, - "ac_power_multiplier": 1, - "ac_voltage_divisor": 10, - "ac_voltage_multiplier": 1, - "measurement_type": 8, - "power_divisor": 10, - "power_multiplier": 1, - } - return zigpy_device - - -@pytest.fixture -async def elec_measurement_zha_dev(elec_measurement_zigpy_dev, zha_device_joined): - """Electric Measurement ZHA device.""" - - zha_dev = await zha_device_joined(elec_measurement_zigpy_dev) - zha_dev.available = True - return zha_dev - - -async def async_test_humidity(hass: HomeAssistant, cluster, entity_id): +async def async_test_humidity(hass: HomeAssistant, cluster: Cluster, entity_id: str): """Test humidity sensor.""" await send_attributes_report(hass, cluster, {1: 1, 0: 1000, 2: 100}) assert_state(hass, entity_id, "10.0", PERCENTAGE) -async def async_test_temperature(hass: HomeAssistant, cluster, entity_id): +async def async_test_temperature(hass: HomeAssistant, cluster: Cluster, entity_id: str): """Test temperature sensor.""" await send_attributes_report(hass, cluster, {1: 1, 0: 2900, 2: 100}) assert_state(hass, entity_id, "29.0", UnitOfTemperature.CELSIUS) -async def async_test_pressure(hass: HomeAssistant, cluster, entity_id): +async def async_test_pressure(hass: HomeAssistant, cluster: Cluster, entity_id: str): """Test pressure sensor.""" await send_attributes_report(hass, cluster, {1: 1, 0: 1000, 2: 10000}) assert_state(hass, entity_id, "1000", UnitOfPressure.HPA) @@ -138,7 +67,7 @@ async def async_test_pressure(hass: HomeAssistant, cluster, entity_id): assert_state(hass, entity_id, "1000", UnitOfPressure.HPA) -async def async_test_illuminance(hass: HomeAssistant, cluster, entity_id): +async def async_test_illuminance(hass: HomeAssistant, cluster: Cluster, entity_id: str): """Test illuminance sensor.""" await send_attributes_report(hass, cluster, {1: 1, 0: 10, 2: 20}) assert_state(hass, entity_id, "1", LIGHT_LUX) @@ -150,7 +79,7 @@ async def async_test_illuminance(hass: HomeAssistant, cluster, entity_id): assert_state(hass, entity_id, "unknown", LIGHT_LUX) -async def async_test_metering(hass: HomeAssistant, cluster, entity_id): +async def async_test_metering(hass: HomeAssistant, cluster: Cluster, entity_id: str): """Test Smart Energy metering sensor.""" await send_attributes_report(hass, cluster, {1025: 1, 1024: 12345, 1026: 100}) assert_state(hass, entity_id, "12345.0", None) @@ -159,13 +88,14 @@ async def async_test_metering(hass: HomeAssistant, cluster, entity_id): await send_attributes_report(hass, cluster, {1024: 12346, "status": 64 + 8}) assert_state(hass, entity_id, "12346.0", None) + assert hass.states.get(entity_id).attributes["status"] in ( "SERVICE_DISCONNECT|POWER_FAILURE", "POWER_FAILURE|SERVICE_DISCONNECT", ) await send_attributes_report( - hass, cluster, {"status": 64 + 8, "metering_device_type": 1} + hass, cluster, {"metering_device_type": 1, "status": 64 + 8} ) assert hass.states.get(entity_id).attributes["status"] in ( "SERVICE_DISCONNECT|NOT_DEFINED", @@ -173,7 +103,7 @@ async def async_test_metering(hass: HomeAssistant, cluster, entity_id): ) await send_attributes_report( - hass, cluster, {"status": 64 + 8, "metering_device_type": 2} + hass, cluster, {"metering_device_type": 2, "status": 64 + 8} ) assert hass.states.get(entity_id).attributes["status"] in ( "SERVICE_DISCONNECT|PIPE_EMPTY", @@ -181,7 +111,7 @@ async def async_test_metering(hass: HomeAssistant, cluster, entity_id): ) await send_attributes_report( - hass, cluster, {"status": 64 + 8, "metering_device_type": 5} + hass, cluster, {"metering_device_type": 5, "status": 64 + 8} ) assert hass.states.get(entity_id).attributes["status"] in ( "SERVICE_DISCONNECT|TEMPERATURE_SENSOR", @@ -190,13 +120,13 @@ async def async_test_metering(hass: HomeAssistant, cluster, entity_id): # Status for other meter types await send_attributes_report( - hass, cluster, {"status": 32, "metering_device_type": 4} + hass, cluster, {"metering_device_type": 4, "status": 32} ) assert hass.states.get(entity_id).attributes["status"] in ("", "32") async def async_test_smart_energy_summation_delivered( - hass: HomeAssistant, cluster, entity_id + hass: HomeAssistant, cluster: Cluster, entity_id: str ): """Test SmartEnergy Summation delivered sensor.""" @@ -213,7 +143,7 @@ async def async_test_smart_energy_summation_delivered( async def async_test_smart_energy_summation_received( - hass: HomeAssistant, cluster, entity_id + hass: HomeAssistant, cluster: Cluster, entity_id: str ): """Test SmartEnergy Summation received sensor.""" @@ -229,7 +159,9 @@ async def async_test_smart_energy_summation_received( ) -async def async_test_electrical_measurement(hass: HomeAssistant, cluster, entity_id): +async def async_test_electrical_measurement( + hass: HomeAssistant, cluster: Cluster, entity_id: str +): """Test electrical measurement sensor.""" # update divisor cached value await send_attributes_report(hass, cluster, {"ac_power_divisor": 1}) @@ -248,10 +180,12 @@ async def async_test_electrical_measurement(hass: HomeAssistant, cluster, entity assert "active_power_max" not in hass.states.get(entity_id).attributes await send_attributes_report(hass, cluster, {0: 1, 0x050D: 88, 10: 5000}) - assert hass.states.get(entity_id).attributes["active_power_max"] == "8.8" + assert hass.states.get(entity_id).attributes["active_power_max"] == 8.8 -async def async_test_em_apparent_power(hass: HomeAssistant, cluster, entity_id): +async def async_test_em_apparent_power( + hass: HomeAssistant, cluster: Cluster, entity_id: str +): """Test electrical measurement Apparent Power sensor.""" # update divisor cached value await send_attributes_report(hass, cluster, {"ac_power_divisor": 1}) @@ -269,7 +203,9 @@ async def async_test_em_apparent_power(hass: HomeAssistant, cluster, entity_id): assert_state(hass, entity_id, "9.9", UnitOfApparentPower.VOLT_AMPERE) -async def async_test_em_power_factor(hass: HomeAssistant, cluster, entity_id): +async def async_test_em_power_factor( + hass: HomeAssistant, cluster: Cluster, entity_id: str +): """Test electrical measurement Power Factor sensor.""" # update divisor cached value await send_attributes_report(hass, cluster, {"ac_power_divisor": 1}) @@ -287,7 +223,9 @@ async def async_test_em_power_factor(hass: HomeAssistant, cluster, entity_id): assert_state(hass, entity_id, "99", PERCENTAGE) -async def async_test_em_rms_current(hass: HomeAssistant, cluster, entity_id): +async def async_test_em_rms_current( + hass: HomeAssistant, cluster: Cluster, entity_id: str +): """Test electrical measurement RMS Current sensor.""" await send_attributes_report(hass, cluster, {0: 1, 0x0508: 1234, 10: 1000}) @@ -302,10 +240,12 @@ async def async_test_em_rms_current(hass: HomeAssistant, cluster, entity_id): assert "rms_current_max" not in hass.states.get(entity_id).attributes await send_attributes_report(hass, cluster, {0: 1, 0x050A: 88, 10: 5000}) - assert hass.states.get(entity_id).attributes["rms_current_max"] == "8.8" + assert hass.states.get(entity_id).attributes["rms_current_max"] == 8.8 -async def async_test_em_rms_voltage(hass: HomeAssistant, cluster, entity_id): +async def async_test_em_rms_voltage( + hass: HomeAssistant, cluster: Cluster, entity_id: str +): """Test electrical measurement RMS Voltage sensor.""" await send_attributes_report(hass, cluster, {0: 1, 0x0505: 1234, 10: 1000}) @@ -320,10 +260,12 @@ async def async_test_em_rms_voltage(hass: HomeAssistant, cluster, entity_id): assert "rms_voltage_max" not in hass.states.get(entity_id).attributes await send_attributes_report(hass, cluster, {0: 1, 0x0507: 888, 10: 5000}) - assert hass.states.get(entity_id).attributes["rms_voltage_max"] == "8.9" + assert hass.states.get(entity_id).attributes["rms_voltage_max"] == 8.9 -async def async_test_powerconfiguration(hass: HomeAssistant, cluster, entity_id): +async def async_test_powerconfiguration( + hass: HomeAssistant, cluster: Cluster, entity_id: str +): """Test powerconfiguration/battery sensor.""" await send_attributes_report(hass, cluster, {33: 98}) assert_state(hass, entity_id, "49", "%") @@ -334,7 +276,9 @@ async def async_test_powerconfiguration(hass: HomeAssistant, cluster, entity_id) assert hass.states.get(entity_id).attributes["battery_voltage"] == 2.0 -async def async_test_powerconfiguration2(hass: HomeAssistant, cluster, entity_id): +async def async_test_powerconfiguration2( + hass: HomeAssistant, cluster: Cluster, entity_id: str +): """Test powerconfiguration/battery sensor.""" await send_attributes_report(hass, cluster, {33: -1}) assert_state(hass, entity_id, STATE_UNKNOWN, "%") @@ -346,13 +290,17 @@ async def async_test_powerconfiguration2(hass: HomeAssistant, cluster, entity_id assert_state(hass, entity_id, "49", "%") -async def async_test_device_temperature(hass: HomeAssistant, cluster, entity_id): +async def async_test_device_temperature( + hass: HomeAssistant, cluster: Cluster, entity_id: str +): """Test temperature sensor.""" await send_attributes_report(hass, cluster, {0: 2900}) assert_state(hass, entity_id, "29.0", UnitOfTemperature.CELSIUS) -async def async_test_setpoint_change_source(hass, cluster, entity_id): +async def async_test_setpoint_change_source( + hass: HomeAssistant, cluster: Cluster, entity_id: str +): """Test the translation of numerical state into enum text.""" await send_attributes_report( hass, cluster, {Thermostat.AttributeDefs.setpoint_change_source.id: 0x01} @@ -361,7 +309,9 @@ async def async_test_setpoint_change_source(hass, cluster, entity_id): assert hass_state.state == "Schedule" -async def async_test_pi_heating_demand(hass, cluster, entity_id): +async def async_test_pi_heating_demand( + hass: HomeAssistant, cluster: Cluster, entity_id: str +): """Test pi heating demand is correctly returned.""" await send_attributes_report( hass, cluster, {Thermostat.AttributeDefs.pi_heating_demand.id: 1} @@ -568,8 +518,8 @@ async def async_test_pi_heating_demand(hass, cluster, entity_id): ) async def test_sensor( hass: HomeAssistant, + setup_zha, zigpy_device_mock, - zha_device_joined_restored, cluster_id, entity_suffix, test_func, @@ -580,14 +530,18 @@ async def test_sensor( ) -> None: """Test ZHA sensor platform.""" + await setup_zha() + gateway = get_zha_gateway(hass) + zigpy_device = zigpy_device_mock( { 1: { SIG_EP_INPUT: [cluster_id, general.Basic.cluster_id], SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH, + SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, + SIG_EP_PROFILE: zha.PROFILE_ID, } - } + }, ) cluster = zigpy_device.endpoints[1].in_clusters[cluster_id] if unsupported_attrs: @@ -600,26 +554,27 @@ async def test_sensor( # this one is mains powered zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100 cluster.PLUGGED_ATTR_READS = read_plug - zha_device = await zha_device_joined_restored(zigpy_device) + + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) entity_id = ENTITY_ID_PREFIX.format(entity_suffix) - await async_enable_traffic(hass, [zha_device], enabled=False) - await hass.async_block_till_done() - # ensure the sensor entity was created - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [cluster_id, general.Basic.cluster_id], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, + } + } + ) - # allow traffic to flow through the gateway and devices - await async_enable_traffic(hass, [zha_device]) - - # test that the sensor now have their correct initial state (mostly unknown) assert hass.states.get(entity_id).state == initial_sensor_state # test sensor associated logic await test_func(hass, cluster, entity_id) - # test rejoin - await async_test_rejoin(hass, zigpy_device, [cluster], (report_count,)) - def assert_state(hass: HomeAssistant, entity_id, state, unit_of_measurement): """Check that the state is what is expected. @@ -630,748 +585,3 @@ def assert_state(hass: HomeAssistant, entity_id, state, unit_of_measurement): hass_state = hass.states.get(entity_id) assert hass_state.state == state assert hass_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == unit_of_measurement - - -@pytest.fixture -def hass_ms(hass: HomeAssistant) -> Callable[[str], HomeAssistant]: - """Hass instance with measurement system.""" - - async def _hass_ms(meas_sys: str) -> HomeAssistant: - await config_util.async_process_ha_core_config( - hass, {CONF_UNIT_SYSTEM: meas_sys} - ) - await hass.async_block_till_done() - return hass - - return _hass_ms - - -@pytest.fixture -def core_rs(hass_storage: dict[str, Any]): - """Core.restore_state fixture.""" - - def _storage(entity_id, uom, state): - now = dt_util.utcnow().isoformat() - - hass_storage[restore_state.STORAGE_KEY] = { - "version": restore_state.STORAGE_VERSION, - "key": restore_state.STORAGE_KEY, - "data": [ - { - "state": { - "entity_id": entity_id, - "state": str(state), - "attributes": {ATTR_UNIT_OF_MEASUREMENT: uom}, - "last_changed": now, - "last_updated": now, - "context": { - "id": "3c2243ff5f30447eb12e7348cfd5b8ff", - "user_id": None, - }, - }, - "last_seen": now, - } - ], - } - - return _storage - - -@pytest.mark.parametrize( - ("uom", "raw_temp", "expected", "restore"), - [ - (UnitOfTemperature.CELSIUS, 2900, 29, False), - (UnitOfTemperature.CELSIUS, 2900, 29, True), - (UnitOfTemperature.FAHRENHEIT, 2900, 84, False), - (UnitOfTemperature.FAHRENHEIT, 2900, 84, True), - ], -) -async def test_temp_uom( - hass: HomeAssistant, - uom: UnitOfTemperature, - raw_temp: int, - expected: int, - restore: bool, - hass_ms: Callable[[str], HomeAssistant], - core_rs, - zigpy_device_mock, - zha_device_restored, -) -> None: - """Test ZHA temperature sensor unit of measurement.""" - - entity_id = "sensor.fake1026_fakemodel1026_004f3202_temperature" - if restore: - core_rs(entity_id, uom, state=(expected - 2)) - await async_mock_load_restore_state_from_storage(hass) - - hass = await hass_ms("metric" if uom == UnitOfTemperature.CELSIUS else "imperial") - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [ - measurement.TemperatureMeasurement.cluster_id, - general.Basic.cluster_id, - ], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH, - } - } - ) - cluster = zigpy_device.endpoints[1].temperature - zha_device = await zha_device_restored(zigpy_device) - entity_id = find_entity_id(Platform.SENSOR, zha_device, hass) - - if not restore: - await async_enable_traffic(hass, [zha_device], enabled=False) - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and devices - await async_enable_traffic(hass, [zha_device]) - - # test that the sensors now have a state of unknown - if not restore: - assert hass.states.get(entity_id).state == STATE_UNKNOWN - - await send_attribute_report(hass, cluster, 0, raw_temp) - await hass.async_block_till_done() - state = hass.states.get(entity_id) - assert state is not None - assert round(float(state.state)) == expected - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == uom - - -@patch( - "zigpy.zcl.ClusterPersistingListener", - MagicMock(), -) -async def test_electrical_measurement_init( - hass: HomeAssistant, - zigpy_device_mock, - zha_device_joined, -) -> None: - """Test proper initialization of the electrical measurement cluster.""" - - cluster_id = homeautomation.ElectricalMeasurement.cluster_id - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [cluster_id, general.Basic.cluster_id], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH, - } - } - ) - cluster = zigpy_device.endpoints[1].in_clusters[cluster_id] - zha_device = await zha_device_joined(zigpy_device) - entity_id = "sensor.fakemanufacturer_fakemodel_power" - - # allow traffic to flow through the gateway and devices - await async_enable_traffic(hass, [zha_device]) - - # test that the sensor now have a state of unknown - assert hass.states.get(entity_id).state == STATE_UNKNOWN - - await send_attributes_report(hass, cluster, {0: 1, 1291: 100, 10: 1000}) - assert int(hass.states.get(entity_id).state) == 100 - - cluster_handler = zha_device._endpoints[1].all_cluster_handlers["1:0x0b04"] - assert cluster_handler.ac_power_divisor == 1 - assert cluster_handler.ac_power_multiplier == 1 - - # update power divisor - await send_attributes_report(hass, cluster, {0: 1, 1291: 20, 0x0403: 5, 10: 1000}) - assert cluster_handler.ac_power_divisor == 5 - assert cluster_handler.ac_power_multiplier == 1 - assert hass.states.get(entity_id).state == "4.0" - - await send_attributes_report(hass, cluster, {0: 1, 1291: 30, 0x0605: 10, 10: 1000}) - assert cluster_handler.ac_power_divisor == 10 - assert cluster_handler.ac_power_multiplier == 1 - assert hass.states.get(entity_id).state == "3.0" - - # update power multiplier - await send_attributes_report(hass, cluster, {0: 1, 1291: 20, 0x0402: 6, 10: 1000}) - assert cluster_handler.ac_power_divisor == 10 - assert cluster_handler.ac_power_multiplier == 6 - assert hass.states.get(entity_id).state == "12.0" - - await send_attributes_report(hass, cluster, {0: 1, 1291: 30, 0x0604: 20, 10: 1000}) - assert cluster_handler.ac_power_divisor == 10 - assert cluster_handler.ac_power_multiplier == 20 - assert hass.states.get(entity_id).state == "60.0" - - -@pytest.mark.parametrize( - ("cluster_id", "unsupported_attributes", "entity_ids", "missing_entity_ids"), - [ - ( - homeautomation.ElectricalMeasurement.cluster_id, - {"apparent_power", "rms_voltage", "rms_current"}, - { - "power", - "ac_frequency", - "power_factor", - }, - { - "apparent_power", - "voltage", - "current", - }, - ), - ( - homeautomation.ElectricalMeasurement.cluster_id, - {"apparent_power", "rms_current", "ac_frequency", "power_factor"}, - {"voltage", "power"}, - { - "apparent_power", - "current", - "ac_frequency", - "power_factor", - }, - ), - ( - homeautomation.ElectricalMeasurement.cluster_id, - set(), - { - "voltage", - "power", - "apparent_power", - "current", - "ac_frequency", - "power_factor", - }, - set(), - ), - ( - smartenergy.Metering.cluster_id, - { - "instantaneous_demand", - }, - { - "summation_delivered", - }, - { - "instantaneous_demand", - }, - ), - ( - smartenergy.Metering.cluster_id, - {"instantaneous_demand", "current_summ_delivered"}, - {}, - { - "instantaneous_demand", - "summation_delivered", - }, - ), - ( - smartenergy.Metering.cluster_id, - {}, - { - "instantaneous_demand", - "summation_delivered", - }, - {}, - ), - ], -) -async def test_unsupported_attributes_sensor( - hass: HomeAssistant, - zigpy_device_mock, - zha_device_joined_restored, - cluster_id, - unsupported_attributes, - entity_ids, - missing_entity_ids, -) -> None: - """Test ZHA sensor platform.""" - - entity_ids = {ENTITY_ID_PREFIX.format(e) for e in entity_ids} - missing_entity_ids = {ENTITY_ID_PREFIX.format(e) for e in missing_entity_ids} - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [cluster_id, general.Basic.cluster_id], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.ON_OFF_SWITCH, - } - } - ) - cluster = zigpy_device.endpoints[1].in_clusters[cluster_id] - if cluster_id == smartenergy.Metering.cluster_id: - # this one is mains powered - zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100 - for attr in unsupported_attributes: - cluster.add_unsupported_attribute(attr) - zha_device = await zha_device_joined_restored(zigpy_device) - - await async_enable_traffic(hass, [zha_device], enabled=False) - await hass.async_block_till_done() - present_entity_ids = set(find_entity_ids(Platform.SENSOR, zha_device, hass)) - assert present_entity_ids == entity_ids - assert missing_entity_ids not in present_entity_ids - - -@pytest.mark.parametrize( - ("raw_uom", "raw_value", "expected_state", "expected_uom"), - [ - ( - 1, - 12320, - "1.23", - UnitOfVolume.CUBIC_METERS, - ), - ( - 1, - 1232000, - "123.2", - UnitOfVolume.CUBIC_METERS, - ), - ( - 3, - 2340, - "0.65", - UnitOfVolume.CUBIC_METERS, - ), - ( - 3, - 2360, - "0.68", - UnitOfVolume.CUBIC_METERS, - ), - ( - 8, - 23660, - "2.37", - UnitOfPressure.KPA, - ), - ( - 0, - 9366, - "0.937", - UnitOfEnergy.KILO_WATT_HOUR, - ), - ( - 0, - 999, - "0.1", - UnitOfEnergy.KILO_WATT_HOUR, - ), - ( - 0, - 10091, - "1.009", - UnitOfEnergy.KILO_WATT_HOUR, - ), - ( - 0, - 10099, - "1.01", - UnitOfEnergy.KILO_WATT_HOUR, - ), - ( - 0, - 100999, - "10.1", - UnitOfEnergy.KILO_WATT_HOUR, - ), - ( - 0, - 100023, - "10.002", - UnitOfEnergy.KILO_WATT_HOUR, - ), - ( - 0, - 102456, - "10.246", - UnitOfEnergy.KILO_WATT_HOUR, - ), - ( - 5, - 102456, - "10.25", - "IMP gal", - ), - ( - 7, - 50124, - "5.01", - UnitOfVolume.LITERS, - ), - ], -) -async def test_se_summation_uom( - hass: HomeAssistant, - zigpy_device_mock, - zha_device_joined, - raw_uom, - raw_value, - expected_state, - expected_uom, -) -> None: - """Test ZHA smart energy summation.""" - - entity_id = ENTITY_ID_PREFIX.format("summation_delivered") - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [ - smartenergy.Metering.cluster_id, - general.Basic.cluster_id, - ], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.SIMPLE_SENSOR, - } - } - ) - zigpy_device.node_desc.mac_capability_flags |= 0b_0000_0100 - - cluster = zigpy_device.endpoints[1].in_clusters[smartenergy.Metering.cluster_id] - for attr in ("instanteneous_demand",): - cluster.add_unsupported_attribute(attr) - cluster.PLUGGED_ATTR_READS = { - "current_summ_delivered": raw_value, - "demand_formatting": 0xF9, - "divisor": 10000, - "metering_device_type": 0x00, - "multiplier": 1, - "status": 0x00, - "summation_formatting": 0b1_0111_010, - "unit_of_measure": raw_uom, - } - await zha_device_joined(zigpy_device) - - assert_state(hass, entity_id, expected_state, expected_uom) - - -@pytest.mark.parametrize( - ("raw_measurement_type", "expected_type"), - [ - (1, "ACTIVE_MEASUREMENT"), - (8, "PHASE_A_MEASUREMENT"), - (9, "ACTIVE_MEASUREMENT, PHASE_A_MEASUREMENT"), - ( - 15, - ( - "ACTIVE_MEASUREMENT, REACTIVE_MEASUREMENT, APPARENT_MEASUREMENT," - " PHASE_A_MEASUREMENT" - ), - ), - ], -) -async def test_elec_measurement_sensor_type( - hass: HomeAssistant, - elec_measurement_zigpy_dev, - raw_measurement_type, - expected_type, - zha_device_joined, -) -> None: - """Test ZHA electrical measurement sensor type.""" - - entity_id = ENTITY_ID_PREFIX.format("power") - zigpy_dev = elec_measurement_zigpy_dev - zigpy_dev.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS[ - "measurement_type" - ] = raw_measurement_type - - await zha_device_joined(zigpy_dev) - - state = hass.states.get(entity_id) - assert state is not None - assert state.attributes["measurement_type"] == expected_type - - -async def test_elec_measurement_sensor_polling( - hass: HomeAssistant, - elec_measurement_zigpy_dev, - zha_device_joined_restored, -) -> None: - """Test ZHA electrical measurement sensor polling.""" - - entity_id = ENTITY_ID_PREFIX.format("power") - zigpy_dev = elec_measurement_zigpy_dev - zigpy_dev.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS["active_power"] = ( - 20 - ) - - await zha_device_joined_restored(zigpy_dev) - - # test that the sensor has an initial state of 2.0 - state = hass.states.get(entity_id) - assert state.state == "2.0" - - # update the value for the power reading - zigpy_dev.endpoints[1].electrical_measurement.PLUGGED_ATTR_READS["active_power"] = ( - 60 - ) - - # ensure the state is still 2.0 - state = hass.states.get(entity_id) - assert state.state == "2.0" - - # let the polling happen - future = dt_util.utcnow() + timedelta(seconds=90) - async_fire_time_changed(hass, future) - await hass.async_block_till_done(wait_background_tasks=True) - - # ensure the state has been updated to 6.0 - state = hass.states.get(entity_id) - assert state.state == "6.0" - - -@pytest.mark.parametrize( - "supported_attributes", - [ - set(), - { - "active_power", - "active_power_max", - "rms_current", - "rms_current_max", - "rms_voltage", - "rms_voltage_max", - }, - { - "active_power", - }, - { - "active_power", - "active_power_max", - }, - { - "rms_current", - "rms_current_max", - }, - { - "rms_voltage", - "rms_voltage_max", - }, - ], -) -async def test_elec_measurement_skip_unsupported_attribute( - hass: HomeAssistant, - elec_measurement_zha_dev, - supported_attributes, -) -> None: - """Test ZHA electrical measurement skipping update of unsupported attributes.""" - - entity_id = ENTITY_ID_PREFIX.format("power") - zha_dev = elec_measurement_zha_dev - - cluster = zha_dev.device.endpoints[1].electrical_measurement - - all_attrs = { - "active_power", - "active_power_max", - "apparent_power", - "rms_current", - "rms_current_max", - "rms_voltage", - "rms_voltage_max", - "power_factor", - "ac_frequency", - "ac_frequency_max", - } - for attr in all_attrs - supported_attributes: - cluster.add_unsupported_attribute(attr) - cluster.read_attributes.reset_mock() - - await async_update_entity(hass, entity_id) - await hass.async_block_till_done() - assert cluster.read_attributes.call_count == math.ceil( - len(supported_attributes) / ZHA_CLUSTER_HANDLER_READS_PER_REQ - ) - read_attrs = { - a for call in cluster.read_attributes.call_args_list for a in call[0][0] - } - assert read_attrs == supported_attributes - - -class OppleCluster(CustomCluster, ManufacturerSpecificCluster): - """Aqara manufacturer specific cluster.""" - - cluster_id = 0xFCC0 - ep_attribute = "opple_cluster" - attributes = { - 0x010C: ("last_feeding_size", t.uint16_t, True), - } - - def __init__(self, *args, **kwargs) -> None: - """Initialize.""" - super().__init__(*args, **kwargs) - # populate cache to create config entity - self._attr_cache.update({0x010C: 10}) - - -( - add_to_registry_v2("Fake_Manufacturer_sensor", "Fake_Model_sensor") - .replaces(OppleCluster) - .sensor( - "last_feeding_size", - OppleCluster.cluster_id, - divisor=1, - multiplier=1, - unit=UnitOfMass.GRAMS, - ) -) - - -@pytest.fixture -async def zigpy_device_aqara_sensor_v2( - hass: HomeAssistant, zigpy_device_mock, zha_device_joined_restored -): - """Device tracker zigpy Aqara motion sensor device.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [ - general.Basic.cluster_id, - OppleCluster.cluster_id, - ], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.OCCUPANCY_SENSOR, - } - }, - manufacturer="Fake_Manufacturer_sensor", - model="Fake_Model_sensor", - ) - - zha_device = await zha_device_joined_restored(zigpy_device) - return zha_device, zigpy_device.endpoints[1].opple_cluster - - -async def test_last_feeding_size_sensor_v2( - hass: HomeAssistant, zigpy_device_aqara_sensor_v2 -) -> None: - """Test quirks defined sensor.""" - - zha_device, cluster = zigpy_device_aqara_sensor_v2 - assert isinstance(zha_device.device, CustomDeviceV2) - entity_id = find_entity_id( - Platform.SENSOR, zha_device, hass, qualifier="last_feeding_size" - ) - assert entity_id is not None - - await send_attributes_report(hass, cluster, {0x010C: 1}) - assert_state(hass, entity_id, "1.0", UnitOfMass.GRAMS.value) - - await send_attributes_report(hass, cluster, {0x010C: 5}) - assert_state(hass, entity_id, "5.0", UnitOfMass.GRAMS.value) - - -@pytest.fixture -async def coordinator(hass: HomeAssistant, zigpy_device_mock, zha_device_joined): - """Test ZHA fan platform.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [general.Groups.cluster_id], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.CONTROL_BRIDGE, - SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, - } - }, - ieee="00:15:8d:00:02:32:4f:32", - nwk=0x0000, - node_descriptor=b"\xf8\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff", - ) - zha_device = await zha_device_joined(zigpy_device) - zha_device.available = True - return zha_device - - -async def test_device_counter_sensors( - hass: HomeAssistant, - coordinator: ZHADevice, - entity_registry: er.EntityRegistry, - config_entry: MockConfigEntry, -) -> None: - """Test quirks defined sensor.""" - - entity_id = "sensor.coordinator_manufacturer_coordinator_model_counter_1" - state = hass.states.get(entity_id) - assert state is None - - # Enable the entity. - entity_registry.async_update_entity(entity_id, disabled_by=None) - await hass.config_entries.async_reload(config_entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state is not None - assert state.state == "1" - - # simulate counter increment on application - coordinator.device.application.state.counters["ezsp_counters"][ - "counter_1" - ].increment() - - next_update = dt_util.utcnow() + timedelta(seconds=60) - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state is not None - assert state.state == "2" - - -@pytest.fixture -async def zigpy_device_danfoss_thermostat( - hass: HomeAssistant, zigpy_device_mock, zha_device_joined_restored -): - """Device tracker zigpy danfoss thermostat device.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [ - general.Basic.cluster_id, - general.PowerConfiguration.cluster_id, - general.Identify.cluster_id, - general.Time.cluster_id, - general.PollControl.cluster_id, - Thermostat.cluster_id, - hvac.UserInterface.cluster_id, - homeautomation.Diagnostic.cluster_id, - ], - SIG_EP_OUTPUT: [general.Basic.cluster_id, general.Ota.cluster_id], - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.THERMOSTAT, - } - }, - manufacturer="Danfoss", - model="eTRV0100", - ) - - zha_device = await zha_device_joined_restored(zigpy_device) - return zha_device, zigpy_device - - -async def test_danfoss_thermostat_sw_error( - hass: HomeAssistant, zigpy_device_danfoss_thermostat -) -> None: - """Test quirks defined thermostat.""" - - zha_device, zigpy_device = zigpy_device_danfoss_thermostat - - entity_id = find_entity_id( - Platform.SENSOR, zha_device, hass, qualifier="software_error" - ) - assert entity_id is not None - - cluster = zigpy_device.endpoints[1].diagnostic - - await send_attributes_report( - hass, - cluster, - { - danfoss_thermostat.DanfossDiagnosticCluster.AttributeDefs.sw_error_code.id: 0x0001 - }, - ) - - hass_state = hass.states.get(entity_id) - assert hass_state.state == "something" - assert hass_state.attributes["Top_pcb_sensor_error"] diff --git a/tests/components/zha/test_silabs_multiprotocol.py b/tests/components/zha/test_silabs_multiprotocol.py index 03c845269e0..a5f2db22ce5 100644 --- a/tests/components/zha/test_silabs_multiprotocol.py +++ b/tests/components/zha/test_silabs_multiprotocol.py @@ -11,7 +11,7 @@ import zigpy.state from homeassistant.components import zha from homeassistant.components.zha import silabs_multiprotocol -from homeassistant.components.zha.core.helpers import get_zha_gateway +from homeassistant.components.zha.helpers import get_zha_data from homeassistant.core import HomeAssistant if TYPE_CHECKING: @@ -38,8 +38,7 @@ async def test_async_get_channel_missing( """Test reading channel with an inactive ZHA installation, no valid channel.""" await setup_zha() - gateway = get_zha_gateway(hass) - await zha.async_unload_entry(hass, gateway.config_entry) + await zha.async_unload_entry(hass, get_zha_data(hass).config_entry) # Network settings were never loaded for whatever reason zigpy_app_controller.state.network_info = zigpy.state.NetworkInfo() diff --git a/tests/components/zha/test_siren.py b/tests/components/zha/test_siren.py index 652955ef98d..f9837a7d016 100644 --- a/tests/components/zha/test_siren.py +++ b/tests/components/zha/test_siren.py @@ -4,7 +4,11 @@ from datetime import timedelta from unittest.mock import ANY, call, patch import pytest -from zigpy.const import SIG_EP_PROFILE +from zha.application.const import ( + WARNING_DEVICE_MODE_EMERGENCY_PANIC, + WARNING_DEVICE_SOUND_MEDIUM, +) +from zigpy.const import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE from zigpy.profiles import zha import zigpy.zcl from zigpy.zcl.clusters import general, security @@ -16,16 +20,17 @@ from homeassistant.components.siren import ( ATTR_VOLUME_LEVEL, DOMAIN as SIREN_DOMAIN, ) -from homeassistant.components.zha.core.const import ( - WARNING_DEVICE_MODE_EMERGENCY_PANIC, - WARNING_DEVICE_SOUND_MEDIUM, +from homeassistant.components.zha.helpers import ( + ZHADeviceProxy, + ZHAGatewayProxy, + get_zha_gateway, + get_zha_gateway_proxy, ) -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform +from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util -from .common import async_enable_traffic, find_entity_id -from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE +from .common import find_entity_id from tests.common import async_fire_time_changed @@ -46,9 +51,12 @@ def siren_platform_only(): yield -@pytest.fixture -async def siren(hass, zigpy_device_mock, zha_device_joined_restored): - """Siren fixture.""" +async def test_siren(hass: HomeAssistant, setup_zha, zigpy_device_mock) -> None: + """Test zha siren platform.""" + + await setup_zha() + gateway = get_zha_gateway(hass) + gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass) zigpy_device = zigpy_device_mock( { @@ -58,30 +66,18 @@ async def siren(hass, zigpy_device_mock, zha_device_joined_restored): SIG_EP_TYPE: zha.DeviceType.IAS_WARNING_DEVICE, SIG_EP_PROFILE: zha.PROFILE_ID, } - }, + } ) - zha_device = await zha_device_joined_restored(zigpy_device) - return zha_device, zigpy_device.endpoints[1].ias_wd + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) - -async def test_siren(hass: HomeAssistant, siren) -> None: - """Test zha siren platform.""" - - zha_device, cluster = siren - assert cluster is not None - entity_id = find_entity_id(Platform.SIREN, zha_device, hass) + zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee) + entity_id = find_entity_id(Platform.SIREN, zha_device_proxy, hass) + cluster = zigpy_device.endpoints[1].ias_wd assert entity_id is not None - assert hass.states.get(entity_id).state == STATE_OFF - await async_enable_traffic(hass, [zha_device], enabled=False) - # test that the switch was created and that its state is unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - - # test that the state has changed from unavailable to off assert hass.states.get(entity_id).state == STATE_OFF # turn on from HA diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index c8c2842c400..cc4e41485f9 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -1,51 +1,28 @@ """Test ZHA switch.""" -from unittest.mock import AsyncMock, call, patch +from unittest.mock import call, patch import pytest -from zhaquirks.const import ( - DEVICE_TYPE, - ENDPOINTS, - INPUT_CLUSTERS, - OUTPUT_CLUSTERS, - PROFILE_ID, -) -from zigpy.exceptions import ZigbeeException from zigpy.profiles import zha -from zigpy.quirks import _DEVICE_REGISTRY, CustomCluster, CustomDevice -from zigpy.quirks.v2 import CustomDeviceV2, add_to_registry_v2 -import zigpy.types as t -from zigpy.zcl.clusters import closures, general -from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster +from zigpy.zcl.clusters import general import zigpy.zcl.foundation as zcl_f from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.components.zha.core.group import GroupMember -from homeassistant.components.zha.core.helpers import get_zha_gateway -from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, Platform +from homeassistant.components.zha.helpers import ( + ZHADeviceProxy, + ZHAGatewayProxy, + get_zha_gateway, + get_zha_gateway_proxy, +) +from homeassistant.const import STATE_OFF, STATE_ON, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.entity_component import async_update_entity from homeassistant.setup import async_setup_component -from .common import ( - async_enable_traffic, - async_find_group_entity_id, - async_test_rejoin, - async_wait_for_updates, - find_entity_id, - send_attributes_report, - update_attribute_cache, -) +from .common import find_entity_id, send_attributes_report from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE -from tests.common import MockConfigEntry - ON = 1 OFF = 0 -IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" -IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" @pytest.fixture(autouse=True) @@ -63,104 +40,51 @@ def switch_platform_only(): yield -@pytest.fixture -def zigpy_device(zigpy_device_mock): - """Device tracker zigpy device.""" - endpoints = { - 1: { - SIG_EP_INPUT: [general.Basic.cluster_id, general.OnOff.cluster_id], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, - } - } - return zigpy_device_mock(endpoints) - - -@pytest.fixture -def zigpy_cover_device(zigpy_device_mock): - """Zigpy cover device.""" - - endpoints = { - 1: { - SIG_EP_PROFILE: zha.PROFILE_ID, - SIG_EP_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE, - SIG_EP_INPUT: [ - general.Basic.cluster_id, - closures.WindowCovering.cluster_id, - ], - SIG_EP_OUTPUT: [], - } - } - return zigpy_device_mock(endpoints) - - -@pytest.fixture -async def device_switch_1(hass, zigpy_device_mock, zha_device_joined): +async def test_switch(hass: HomeAssistant, setup_zha, zigpy_device_mock) -> None: """Test ZHA switch platform.""" + await setup_zha() + gateway = get_zha_gateway(hass) + gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass) + zigpy_device = zigpy_device_mock( { 1: { - SIG_EP_INPUT: [general.OnOff.cluster_id, general.Groups.cluster_id], + SIG_EP_INPUT: [ + general.Basic.cluster_id, + general.OnOff.cluster_id, + general.Groups.cluster_id, + ], SIG_EP_OUTPUT: [], SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, + SIG_EP_PROFILE: zha.PROFILE_ID, } }, - ieee=IEEE_GROUPABLE_DEVICE, + ieee="01:2d:6f:00:0a:90:69:e8", + node_descriptor=b"\x02@\x8c\x02\x10RR\x00\x00\x00R\x00\x00", ) - zha_device = await zha_device_joined(zigpy_device) - zha_device.available = True - await hass.async_block_till_done() - return zha_device + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) -@pytest.fixture -async def device_switch_2(hass, zigpy_device_mock, zha_device_joined): - """Test ZHA switch platform.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [general.OnOff.cluster_id, general.Groups.cluster_id], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, - } - }, - ieee=IEEE_GROUPABLE_DEVICE2, - ) - zha_device = await zha_device_joined(zigpy_device) - zha_device.available = True - await hass.async_block_till_done() - return zha_device - - -async def test_switch( - hass: HomeAssistant, zha_device_joined_restored, zigpy_device -) -> None: - """Test ZHA switch platform.""" - - zha_device = await zha_device_joined_restored(zigpy_device) + zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee) + entity_id = find_entity_id(Platform.SWITCH, zha_device_proxy, hass) cluster = zigpy_device.endpoints[1].on_off - entity_id = find_entity_id(Platform.SWITCH, zha_device, hass) assert entity_id is not None - assert hass.states.get(entity_id).state == STATE_OFF - await async_enable_traffic(hass, [zha_device], enabled=False) - # test that the switch was created and that its state is unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - - # test that the state has changed from unavailable to off assert hass.states.get(entity_id).state == STATE_OFF # turn on at switch - await send_attributes_report(hass, cluster, {1: 0, 0: 1, 2: 2}) + await send_attributes_report( + hass, cluster, {general.OnOff.AttributeDefs.on_off.id: ON} + ) assert hass.states.get(entity_id).state == STATE_ON # turn off at switch - await send_attributes_report(hass, cluster, {1: 1, 0: 0, 2: 2}) + await send_attributes_report( + hass, cluster, {general.OnOff.AttributeDefs.on_off.id: OFF} + ) assert hass.states.get(entity_id).state == STATE_OFF # turn on from HA @@ -217,765 +141,3 @@ async def test_switch( assert cluster.read_attributes.call_args == call( ["on_off"], allow_cache=False, only_cache=False, manufacturer=None ) - - # test joining a new switch to the network and HA - await async_test_rejoin(hass, zigpy_device, [cluster], (1,)) - - -class WindowDetectionFunctionQuirk(CustomDevice): - """Quirk with window detection function attribute.""" - - class TuyaManufCluster(CustomCluster, ManufacturerSpecificCluster): - """Tuya manufacturer specific cluster.""" - - cluster_id = 0xEF00 - ep_attribute = "tuya_manufacturer" - - attributes = { - 0xEF01: ("window_detection_function", t.Bool), - 0xEF02: ("window_detection_function_inverter", t.Bool), - } - - def __init__(self, *args, **kwargs): - """Initialize with task.""" - super().__init__(*args, **kwargs) - self._attr_cache.update( - {0xEF01: False} - ) # entity won't be created without this - - replacement = { - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH, - INPUT_CLUSTERS: [general.Basic.cluster_id, TuyaManufCluster], - OUTPUT_CLUSTERS: [], - }, - } - } - - -@pytest.fixture -async def zigpy_device_tuya(hass, zigpy_device_mock, zha_device_joined): - """Device tracker zigpy tuya device.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [general.Basic.cluster_id], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, - } - }, - manufacturer="_TZE200_b6wax7g0", - quirk=WindowDetectionFunctionQuirk, - ) - - zha_device = await zha_device_joined(zigpy_device) - zha_device.available = True - await hass.async_block_till_done() - return zigpy_device - - -@patch( - "homeassistant.components.zha.entity.DEFAULT_UPDATE_GROUP_FROM_CHILD_DELAY", - new=0, -) -async def test_zha_group_switch_entity( - hass: HomeAssistant, - device_switch_1, - device_switch_2, - entity_registry: er.EntityRegistry, - config_entry: MockConfigEntry, -) -> None: - """Test the switch entity for a ZHA group.""" - - # make sure we can still get groups when counter entities exist - entity_id = "sensor.coordinator_manufacturer_coordinator_model_counter_1" - state = hass.states.get(entity_id) - assert state is None - - # Enable the entity. - entity_registry.async_update_entity(entity_id, disabled_by=None) - await hass.config_entries.async_reload(config_entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get(entity_id) - assert state is not None - assert state.state == "1" - - zha_gateway = get_zha_gateway(hass) - assert zha_gateway is not None - device_switch_1._zha_gateway = zha_gateway - device_switch_2._zha_gateway = zha_gateway - member_ieee_addresses = [ - device_switch_1.ieee, - device_switch_2.ieee, - zha_gateway.coordinator_zha_device.ieee, - ] - members = [ - GroupMember(device_switch_1.ieee, 1), - GroupMember(device_switch_2.ieee, 1), - GroupMember(zha_gateway.coordinator_zha_device.ieee, 1), - ] - - # test creating a group with 2 members - zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members) - await hass.async_block_till_done() - - assert zha_group is not None - assert len(zha_group.members) == 3 - for member in zha_group.members: - assert member.device.ieee in member_ieee_addresses - assert member.group == zha_group - assert member.endpoint is not None - - entity_id = async_find_group_entity_id(hass, Platform.SWITCH, zha_group) - assert hass.states.get(entity_id) is not None - - group_cluster_on_off = zha_group.endpoint[general.OnOff.cluster_id] - dev1_cluster_on_off = device_switch_1.device.endpoints[1].on_off - dev2_cluster_on_off = device_switch_2.device.endpoints[1].on_off - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [device_switch_1, device_switch_2]) - await async_wait_for_updates(hass) - - # test that the switches were created and are off - assert hass.states.get(entity_id).state == STATE_OFF - - # turn on from HA - with patch( - "zigpy.zcl.Cluster.request", - return_value=[0x00, zcl_f.Status.SUCCESS], - ): - # turn on via UI - await hass.services.async_call( - SWITCH_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True - ) - assert len(group_cluster_on_off.request.mock_calls) == 1 - assert group_cluster_on_off.request.call_args == call( - False, - ON, - group_cluster_on_off.commands_by_name["on"].schema, - expect_reply=True, - manufacturer=None, - tsn=None, - ) - assert hass.states.get(entity_id).state == STATE_ON - - # test turn off failure case - hold_off = group_cluster_on_off.off - group_cluster_on_off.off = AsyncMock(return_value=[0x01, zcl_f.Status.FAILURE]) - # turn off via UI - await hass.services.async_call( - SWITCH_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True - ) - assert len(group_cluster_on_off.off.mock_calls) == 1 - - state = hass.states.get(entity_id) - assert state - assert state.state == STATE_ON - group_cluster_on_off.off = hold_off - - # turn off from HA - with patch( - "zigpy.zcl.Cluster.request", - return_value=[0x01, zcl_f.Status.SUCCESS], - ): - # turn off via UI - await hass.services.async_call( - SWITCH_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True - ) - assert len(group_cluster_on_off.request.mock_calls) == 1 - assert group_cluster_on_off.request.call_args == call( - False, - OFF, - group_cluster_on_off.commands_by_name["off"].schema, - expect_reply=True, - manufacturer=None, - tsn=None, - ) - assert hass.states.get(entity_id).state == STATE_OFF - - # test turn on failure case - hold_on = group_cluster_on_off.on - group_cluster_on_off.on = AsyncMock(return_value=[0x01, zcl_f.Status.FAILURE]) - # turn on via UI - await hass.services.async_call( - SWITCH_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True - ) - assert len(group_cluster_on_off.on.mock_calls) == 1 - - state = hass.states.get(entity_id) - assert state - assert state.state == STATE_OFF - group_cluster_on_off.on = hold_on - - # test some of the group logic to make sure we key off states correctly - await send_attributes_report(hass, dev1_cluster_on_off, {0: 1}) - await send_attributes_report(hass, dev2_cluster_on_off, {0: 1}) - await async_wait_for_updates(hass) - - # test that group switch is on - assert hass.states.get(entity_id).state == STATE_ON - - await send_attributes_report(hass, dev1_cluster_on_off, {0: 0}) - await async_wait_for_updates(hass) - - # test that group switch is still on - assert hass.states.get(entity_id).state == STATE_ON - - await send_attributes_report(hass, dev2_cluster_on_off, {0: 0}) - await async_wait_for_updates(hass) - - # test that group switch is now off - assert hass.states.get(entity_id).state == STATE_OFF - - await send_attributes_report(hass, dev1_cluster_on_off, {0: 1}) - await async_wait_for_updates(hass) - - # test that group switch is now back on - assert hass.states.get(entity_id).state == STATE_ON - - -async def test_switch_configurable( - hass: HomeAssistant, zha_device_joined_restored, zigpy_device_tuya -) -> None: - """Test ZHA configurable switch platform.""" - - zha_device = await zha_device_joined_restored(zigpy_device_tuya) - cluster = zigpy_device_tuya.endpoints[1].tuya_manufacturer - entity_id = find_entity_id(Platform.SWITCH, zha_device, hass) - assert entity_id is not None - - assert hass.states.get(entity_id).state == STATE_OFF - await async_enable_traffic(hass, [zha_device], enabled=False) - # test that the switch was created and that its state is unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - - # test that the state has changed from unavailable to off - assert hass.states.get(entity_id).state == STATE_OFF - - # turn on at switch - await send_attributes_report(hass, cluster, {"window_detection_function": True}) - assert hass.states.get(entity_id).state == STATE_ON - - # turn off at switch - await send_attributes_report(hass, cluster, {"window_detection_function": False}) - assert hass.states.get(entity_id).state == STATE_OFF - - # turn on from HA - with patch( - "zigpy.zcl.Cluster.write_attributes", - return_value=[zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS], - ): - # turn on via UI - await hass.services.async_call( - SWITCH_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True - ) - assert cluster.write_attributes.mock_calls == [ - call({"window_detection_function": True}, manufacturer=None) - ] - - cluster.write_attributes.reset_mock() - - # turn off from HA - with patch( - "zigpy.zcl.Cluster.write_attributes", - return_value=[zcl_f.Status.SUCCESS, zcl_f.Status.SUCCESS], - ): - # turn off via UI - await hass.services.async_call( - SWITCH_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True - ) - assert cluster.write_attributes.mock_calls == [ - call({"window_detection_function": False}, manufacturer=None) - ] - - cluster.read_attributes.reset_mock() - await async_setup_component(hass, "homeassistant", {}) - await hass.async_block_till_done() - - await hass.services.async_call( - "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True - ) - # the mocking doesn't update the attr cache so this flips back to initial value - assert cluster.read_attributes.call_count == 2 - assert [ - call( - [ - "window_detection_function", - ], - allow_cache=False, - only_cache=False, - manufacturer=None, - ), - call( - [ - "window_detection_function_inverter", - ], - allow_cache=False, - only_cache=False, - manufacturer=None, - ), - ] == cluster.read_attributes.call_args_list - - cluster.write_attributes.reset_mock() - cluster.write_attributes.side_effect = ZigbeeException - - with pytest.raises(HomeAssistantError): - await hass.services.async_call( - SWITCH_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True - ) - - assert cluster.write_attributes.mock_calls == [ - call({"window_detection_function": False}, manufacturer=None), - call({"window_detection_function": False}, manufacturer=None), - call({"window_detection_function": False}, manufacturer=None), - ] - - cluster.write_attributes.side_effect = None - - # test inverter - cluster.write_attributes.reset_mock() - cluster._attr_cache.update({0xEF02: True}) - - await hass.services.async_call( - SWITCH_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True - ) - assert cluster.write_attributes.mock_calls == [ - call({"window_detection_function": True}, manufacturer=None) - ] - - cluster.write_attributes.reset_mock() - await hass.services.async_call( - SWITCH_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True - ) - assert cluster.write_attributes.mock_calls == [ - call({"window_detection_function": False}, manufacturer=None) - ] - - # test joining a new switch to the network and HA - await async_test_rejoin(hass, zigpy_device_tuya, [cluster], (0,)) - - -async def test_switch_configurable_custom_on_off_values( - hass: HomeAssistant, zha_device_joined_restored, zigpy_device_mock -) -> None: - """Test ZHA configurable switch platform.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [general.Basic.cluster_id], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, - } - }, - manufacturer="manufacturer", - model="model", - ) - - ( - add_to_registry_v2(zigpy_device.manufacturer, zigpy_device.model) - .adds(WindowDetectionFunctionQuirk.TuyaManufCluster) - .switch( - "window_detection_function", - WindowDetectionFunctionQuirk.TuyaManufCluster.cluster_id, - on_value=3, - off_value=5, - ) - ) - - zigpy_device = _DEVICE_REGISTRY.get_device(zigpy_device) - - assert isinstance(zigpy_device, CustomDeviceV2) - cluster = zigpy_device.endpoints[1].tuya_manufacturer - cluster.PLUGGED_ATTR_READS = {"window_detection_function": 5} - update_attribute_cache(cluster) - - zha_device = await zha_device_joined_restored(zigpy_device) - - entity_id = find_entity_id(Platform.SWITCH, zha_device, hass) - assert entity_id is not None - - assert hass.states.get(entity_id).state == STATE_OFF - await async_enable_traffic(hass, [zha_device], enabled=False) - # test that the switch was created and that its state is unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - - # test that the state has changed from unavailable to off - assert hass.states.get(entity_id).state == STATE_OFF - - # turn on at switch - await send_attributes_report(hass, cluster, {"window_detection_function": 3}) - assert hass.states.get(entity_id).state == STATE_ON - - # turn off at switch - await send_attributes_report(hass, cluster, {"window_detection_function": 5}) - assert hass.states.get(entity_id).state == STATE_OFF - - # turn on from HA - with patch( - "zigpy.zcl.Cluster.write_attributes", - return_value=[zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]], - ): - # turn on via UI - await hass.services.async_call( - SWITCH_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True - ) - assert cluster.write_attributes.mock_calls == [ - call({"window_detection_function": 3}, manufacturer=None) - ] - cluster.write_attributes.reset_mock() - - # turn off from HA - with patch( - "zigpy.zcl.Cluster.write_attributes", - return_value=[zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]], - ): - # turn off via UI - await hass.services.async_call( - SWITCH_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True - ) - assert cluster.write_attributes.mock_calls == [ - call({"window_detection_function": 5}, manufacturer=None) - ] - - -async def test_switch_configurable_custom_on_off_values_force_inverted( - hass: HomeAssistant, zha_device_joined_restored, zigpy_device_mock -) -> None: - """Test ZHA configurable switch platform.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [general.Basic.cluster_id], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, - } - }, - manufacturer="manufacturer2", - model="model2", - ) - - ( - add_to_registry_v2(zigpy_device.manufacturer, zigpy_device.model) - .adds(WindowDetectionFunctionQuirk.TuyaManufCluster) - .switch( - "window_detection_function", - WindowDetectionFunctionQuirk.TuyaManufCluster.cluster_id, - on_value=3, - off_value=5, - force_inverted=True, - ) - ) - - zigpy_device = _DEVICE_REGISTRY.get_device(zigpy_device) - - assert isinstance(zigpy_device, CustomDeviceV2) - cluster = zigpy_device.endpoints[1].tuya_manufacturer - cluster.PLUGGED_ATTR_READS = {"window_detection_function": 5} - update_attribute_cache(cluster) - - zha_device = await zha_device_joined_restored(zigpy_device) - - entity_id = find_entity_id(Platform.SWITCH, zha_device, hass) - assert entity_id is not None - - assert hass.states.get(entity_id).state == STATE_ON - await async_enable_traffic(hass, [zha_device], enabled=False) - # test that the switch was created and that its state is unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - - # test that the state has changed from unavailable to off - assert hass.states.get(entity_id).state == STATE_ON - - # turn on at switch - await send_attributes_report(hass, cluster, {"window_detection_function": 3}) - assert hass.states.get(entity_id).state == STATE_OFF - - # turn off at switch - await send_attributes_report(hass, cluster, {"window_detection_function": 5}) - assert hass.states.get(entity_id).state == STATE_ON - - # turn on from HA - with patch( - "zigpy.zcl.Cluster.write_attributes", - return_value=[zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]], - ): - # turn on via UI - await hass.services.async_call( - SWITCH_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True - ) - assert cluster.write_attributes.mock_calls == [ - call({"window_detection_function": 5}, manufacturer=None) - ] - cluster.write_attributes.reset_mock() - - # turn off from HA - with patch( - "zigpy.zcl.Cluster.write_attributes", - return_value=[zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]], - ): - # turn off via UI - await hass.services.async_call( - SWITCH_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True - ) - assert cluster.write_attributes.mock_calls == [ - call({"window_detection_function": 3}, manufacturer=None) - ] - - -async def test_switch_configurable_custom_on_off_values_inverter_attribute( - hass: HomeAssistant, zha_device_joined_restored, zigpy_device_mock -) -> None: - """Test ZHA configurable switch platform.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [general.Basic.cluster_id], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, - } - }, - manufacturer="manufacturer3", - model="model3", - ) - - ( - add_to_registry_v2(zigpy_device.manufacturer, zigpy_device.model) - .adds(WindowDetectionFunctionQuirk.TuyaManufCluster) - .switch( - "window_detection_function", - WindowDetectionFunctionQuirk.TuyaManufCluster.cluster_id, - on_value=3, - off_value=5, - invert_attribute_name="window_detection_function_inverter", - ) - ) - - zigpy_device = _DEVICE_REGISTRY.get_device(zigpy_device) - - assert isinstance(zigpy_device, CustomDeviceV2) - cluster = zigpy_device.endpoints[1].tuya_manufacturer - cluster.PLUGGED_ATTR_READS = { - "window_detection_function": 5, - "window_detection_function_inverter": t.Bool(True), - } - update_attribute_cache(cluster) - - zha_device = await zha_device_joined_restored(zigpy_device) - - entity_id = find_entity_id(Platform.SWITCH, zha_device, hass) - assert entity_id is not None - - assert hass.states.get(entity_id).state == STATE_ON - await async_enable_traffic(hass, [zha_device], enabled=False) - # test that the switch was created and that its state is unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - - # test that the state has changed from unavailable to off - assert hass.states.get(entity_id).state == STATE_ON - - # turn on at switch - await send_attributes_report(hass, cluster, {"window_detection_function": 3}) - assert hass.states.get(entity_id).state == STATE_OFF - - # turn off at switch - await send_attributes_report(hass, cluster, {"window_detection_function": 5}) - assert hass.states.get(entity_id).state == STATE_ON - - # turn on from HA - with patch( - "zigpy.zcl.Cluster.write_attributes", - return_value=[zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]], - ): - # turn on via UI - await hass.services.async_call( - SWITCH_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True - ) - assert cluster.write_attributes.mock_calls == [ - call({"window_detection_function": 5}, manufacturer=None) - ] - cluster.write_attributes.reset_mock() - - # turn off from HA - with patch( - "zigpy.zcl.Cluster.write_attributes", - return_value=[zcl_f.WriteAttributesResponse.deserialize(b"\x00")[0]], - ): - # turn off via UI - await hass.services.async_call( - SWITCH_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True - ) - assert cluster.write_attributes.mock_calls == [ - call({"window_detection_function": 3}, manufacturer=None) - ] - - -WCAttrs = closures.WindowCovering.AttributeDefs -WCT = closures.WindowCovering.WindowCoveringType -WCCS = closures.WindowCovering.ConfigStatus -WCM = closures.WindowCovering.WindowCoveringMode - - -async def test_cover_inversion_switch( - hass: HomeAssistant, zha_device_joined_restored, zigpy_cover_device -) -> None: - """Test ZHA cover platform.""" - - # load up cover domain - cluster = zigpy_cover_device.endpoints[1].window_covering - cluster.PLUGGED_ATTR_READS = { - WCAttrs.current_position_lift_percentage.name: 65, - WCAttrs.current_position_tilt_percentage.name: 42, - WCAttrs.window_covering_type.name: WCT.Tilt_blind_tilt_and_lift, - WCAttrs.config_status.name: WCCS(~WCCS.Open_up_commands_reversed), - WCAttrs.window_covering_mode.name: WCM(WCM.LEDs_display_feedback), - } - update_attribute_cache(cluster) - zha_device = await zha_device_joined_restored(zigpy_cover_device) - assert ( - not zha_device.endpoints[1] - .all_cluster_handlers[f"1:0x{cluster.cluster_id:04x}"] - .inverted - ) - assert cluster.read_attributes.call_count == 3 - assert ( - WCAttrs.current_position_lift_percentage.name - in cluster.read_attributes.call_args[0][0] - ) - assert ( - WCAttrs.current_position_tilt_percentage.name - in cluster.read_attributes.call_args[0][0] - ) - - entity_id = find_entity_id(Platform.SWITCH, zha_device, hass) - assert entity_id is not None - - await async_enable_traffic(hass, [zha_device], enabled=False) - # test that the cover was created and that it is unavailable - state = hass.states.get(entity_id) - assert state - assert state.state == STATE_UNAVAILABLE - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - await hass.async_block_till_done() - - # test update - prev_call_count = cluster.read_attributes.call_count - await async_update_entity(hass, entity_id) - assert cluster.read_attributes.call_count == prev_call_count + 1 - state = hass.states.get(entity_id) - assert state - assert state.state == STATE_OFF - - # test to see the state remains after tilting to 0% - await send_attributes_report( - hass, cluster, {WCAttrs.current_position_tilt_percentage.id: 0} - ) - state = hass.states.get(entity_id) - assert state - assert state.state == STATE_OFF - - with patch( - "zigpy.zcl.Cluster.write_attributes", return_value=[0x1, zcl_f.Status.SUCCESS] - ): - cluster.PLUGGED_ATTR_READS = { - WCAttrs.config_status.name: WCCS.Operational - | WCCS.Open_up_commands_reversed, - } - # turn on from UI - await hass.services.async_call( - SWITCH_DOMAIN, "turn_on", {"entity_id": entity_id}, blocking=True - ) - assert cluster.write_attributes.call_count == 1 - assert cluster.write_attributes.call_args_list[0] == call( - { - WCAttrs.window_covering_mode.name: WCM.Motor_direction_reversed - | WCM.LEDs_display_feedback - }, - manufacturer=None, - ) - - state = hass.states.get(entity_id) - assert state - assert state.state == STATE_ON - - cluster.write_attributes.reset_mock() - - # turn off from UI - cluster.PLUGGED_ATTR_READS = { - WCAttrs.config_status.name: WCCS.Operational, - } - await hass.services.async_call( - SWITCH_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True - ) - assert cluster.write_attributes.call_count == 1 - assert cluster.write_attributes.call_args_list[0] == call( - {WCAttrs.window_covering_mode.name: WCM.LEDs_display_feedback}, - manufacturer=None, - ) - - state = hass.states.get(entity_id) - assert state - assert state.state == STATE_OFF - - cluster.write_attributes.reset_mock() - - # test that sending the command again does not result in a write - await hass.services.async_call( - SWITCH_DOMAIN, "turn_off", {"entity_id": entity_id}, blocking=True - ) - assert cluster.write_attributes.call_count == 0 - - state = hass.states.get(entity_id) - assert state - assert state.state == STATE_OFF - - -async def test_cover_inversion_switch_not_created( - hass: HomeAssistant, zha_device_joined_restored, zigpy_cover_device -) -> None: - """Test ZHA cover platform.""" - - # load up cover domain - cluster = zigpy_cover_device.endpoints[1].window_covering - cluster.PLUGGED_ATTR_READS = { - WCAttrs.current_position_lift_percentage.name: 65, - WCAttrs.current_position_tilt_percentage.name: 42, - WCAttrs.config_status.name: WCCS(~WCCS.Open_up_commands_reversed), - } - update_attribute_cache(cluster) - zha_device = await zha_device_joined_restored(zigpy_cover_device) - - assert cluster.read_attributes.call_count == 3 - assert ( - WCAttrs.current_position_lift_percentage.name - in cluster.read_attributes.call_args[0][0] - ) - assert ( - WCAttrs.current_position_tilt_percentage.name - in cluster.read_attributes.call_args[0][0] - ) - - # entity should not be created when mode or config status aren't present - entity_id = find_entity_id(Platform.SWITCH, zha_device, hass) - assert entity_id is None diff --git a/tests/components/zha/test_update.py b/tests/components/zha/test_update.py index 32be013e673..6a1a19b407f 100644 --- a/tests/components/zha/test_update.py +++ b/tests/components/zha/test_update.py @@ -23,13 +23,25 @@ from homeassistant.components.update import ( DOMAIN as UPDATE_DOMAIN, SERVICE_INSTALL, ) -from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON, Platform +from homeassistant.components.zha.helpers import ( + ZHADeviceProxy, + ZHAGatewayProxy, + get_zha_gateway, + get_zha_gateway_proxy, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + STATE_OFF, + STATE_ON, + STATE_UNKNOWN, + Platform, +) from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -from .common import async_enable_traffic, find_entity_id, update_attribute_cache -from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_TYPE +from .common import find_entity_id, update_attribute_cache +from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE @pytest.fixture(autouse=True) @@ -47,28 +59,32 @@ def update_platform_only(): yield -@pytest.fixture -def zigpy_device(zigpy_device_mock): - """Device tracker zigpy device.""" - endpoints = { - 1: { - SIG_EP_INPUT: [general.Basic.cluster_id, general.OnOff.cluster_id], - SIG_EP_OUTPUT: [general.Ota.cluster_id], - SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, - } - } - return zigpy_device_mock( - endpoints, node_descriptor=b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00" - ) - - async def setup_test_data( - zha_device_joined_restored, - zigpy_device, + hass: HomeAssistant, + zigpy_device_mock, skip_attribute_plugs=False, file_not_found=False, ): """Set up test data for the tests.""" + gateway = get_zha_gateway(hass) + gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass) + + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [general.Basic.cluster_id, general.OnOff.cluster_id], + SIG_EP_OUTPUT: [general.Ota.cluster_id], + SIG_EP_TYPE: zha.DeviceType.ON_OFF_SWITCH, + SIG_EP_PROFILE: zha.PROFILE_ID, + } + }, + node_descriptor=b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", + ) + + gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) + fw_version = 0x12345678 installed_fw_version = fw_version - 10 cluster = zigpy_device.endpoints[1].out_clusters[general.Ota.cluster_id] @@ -106,31 +122,28 @@ async def setup_test_data( cluster.endpoint.device.application.ota.get_ota_image = AsyncMock( return_value=None if file_not_found else fw_image ) + zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy(zigpy_device.ieee) + zha_device_proxy.device.async_update_sw_build_id(installed_fw_version) - zha_device = await zha_device_joined_restored(zigpy_device) - zha_device.async_update_sw_build_id(installed_fw_version) - - return zha_device, cluster, fw_image, installed_fw_version + return zha_device_proxy, cluster, fw_image, installed_fw_version async def test_firmware_update_notification_from_zigpy( hass: HomeAssistant, - zha_device_joined_restored, - zigpy_device, + setup_zha, + zigpy_device_mock, ) -> None: """Test ZHA update platform - firmware update notification.""" + await setup_zha() zha_device, cluster, fw_image, installed_fw_version = await setup_test_data( - zha_device_joined_restored, - zigpy_device, + hass, + zigpy_device_mock, ) entity_id = find_entity_id(Platform.UPDATE, zha_device, hass) assert entity_id is not None - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - - assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(entity_id).state == STATE_UNKNOWN # simulate an image available notification await cluster._handle_query_next_image( @@ -139,7 +152,7 @@ async def test_firmware_update_notification_from_zigpy( ), general.QueryNextImageCommand( fw_image.firmware.header.field_control, - zha_device.manufacturer_code, + zha_device.device.manufacturer_code, fw_image.firmware.header.image_type, installed_fw_version, fw_image.firmware.header.header_version, @@ -158,20 +171,20 @@ async def test_firmware_update_notification_from_zigpy( async def test_firmware_update_notification_from_service_call( - hass: HomeAssistant, zha_device_joined_restored, zigpy_device + hass: HomeAssistant, + setup_zha, + zigpy_device_mock, ) -> None: """Test ZHA update platform - firmware update manual check.""" + await setup_zha() zha_device, cluster, fw_image, installed_fw_version = await setup_test_data( - zha_device_joined_restored, zigpy_device + hass, + zigpy_device_mock, ) entity_id = find_entity_id(Platform.UPDATE, zha_device, hass) assert entity_id is not None - - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - - assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(entity_id).state == STATE_UNKNOWN async def _async_image_notify_side_effect(*args, **kwargs): await cluster._handle_query_next_image( @@ -180,7 +193,7 @@ async def test_firmware_update_notification_from_service_call( ), general.QueryNextImageCommand( fw_image.firmware.header.field_control, - zha_device.manufacturer_code, + zha_device.device.manufacturer_code, fw_image.firmware.header.image_type, installed_fw_version, fw_image.firmware.header.header_version, @@ -245,11 +258,14 @@ def make_packet(zigpy_device, cluster, cmd_name: str, **kwargs): @patch("zigpy.device.AFTER_OTA_ATTR_READ_DELAY", 0.01) async def test_firmware_update_success( - hass: HomeAssistant, zha_device_joined_restored, zigpy_device + hass: HomeAssistant, + setup_zha, + zigpy_device_mock, ) -> None: """Test ZHA update platform - firmware update success.""" + await setup_zha() zha_device, cluster, fw_image, installed_fw_version = await setup_test_data( - zha_device_joined_restored, zigpy_device + hass, zigpy_device_mock ) assert installed_fw_version < fw_image.firmware.header.file_version @@ -257,10 +273,7 @@ async def test_firmware_update_success( entity_id = find_entity_id(Platform.UPDATE, zha_device, hass) assert entity_id is not None - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - - assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(entity_id).state == STATE_UNKNOWN # simulate an image available notification await cluster._handle_query_next_image( @@ -269,7 +282,7 @@ async def test_firmware_update_success( ), general.QueryNextImageCommand( field_control=fw_image.firmware.header.field_control, - manufacturer_code=zha_device.manufacturer_code, + manufacturer_code=zha_device.device.manufacturer_code, image_type=fw_image.firmware.header.image_type, current_file_version=installed_fw_version, ), @@ -289,9 +302,9 @@ async def test_firmware_update_success( if cluster_id == general.Ota.cluster_id: hdr, cmd = cluster.deserialize(data) if isinstance(cmd, general.Ota.ImageNotifyCommand): - zigpy_device.packet_received( + zha_device.device.device.packet_received( make_packet( - zigpy_device, + zha_device.device.device, cluster, general.Ota.ServerCommandDefs.query_next_image.name, field_control=general.Ota.QueryNextImageCommand.FieldControl.HardwareVersion, @@ -309,9 +322,9 @@ async def test_firmware_update_success( assert cmd.image_type == fw_image.firmware.header.image_type assert cmd.file_version == fw_image.firmware.header.file_version assert cmd.image_size == fw_image.firmware.header.image_size - zigpy_device.packet_received( + zha_device.device.device.packet_received( make_packet( - zigpy_device, + zha_device.device.device, cluster, general.Ota.ServerCommandDefs.image_block.name, field_control=general.Ota.ImageBlockCommand.FieldControl.RequestNodeAddr, @@ -320,7 +333,7 @@ async def test_firmware_update_success( file_version=fw_image.firmware.header.file_version, file_offset=0, maximum_data_size=40, - request_node_addr=zigpy_device.ieee, + request_node_addr=zha_device.device.device.ieee, ) ) elif isinstance( @@ -336,9 +349,9 @@ async def test_firmware_update_success( assert cmd.file_version == fw_image.firmware.header.file_version assert cmd.file_offset == 0 assert cmd.image_data == fw_image.firmware.serialize()[0:40] - zigpy_device.packet_received( + zha_device.device.device.packet_received( make_packet( - zigpy_device, + zha_device.device.device, cluster, general.Ota.ServerCommandDefs.image_block.name, field_control=general.Ota.ImageBlockCommand.FieldControl.RequestNodeAddr, @@ -347,7 +360,7 @@ async def test_firmware_update_success( file_version=fw_image.firmware.header.file_version, file_offset=40, maximum_data_size=40, - request_node_addr=zigpy_device.ieee, + request_node_addr=zha_device.device.device.ieee, ) ) elif cmd.file_offset == 40: @@ -374,9 +387,9 @@ async def test_firmware_update_success( == f"0x{fw_image.firmware.header.file_version:08x}" ) - zigpy_device.packet_received( + zha_device.device.device.packet_received( make_packet( - zigpy_device, + zha_device.device.device, cluster, general.Ota.ServerCommandDefs.upgrade_end.name, status=foundation.Status.SUCCESS, @@ -430,7 +443,7 @@ async def test_firmware_update_success( # If we send a progress notification incorrectly, it won't be handled entity = hass.data[UPDATE_DOMAIN].get_entity(entity_id) - entity._update_progress(50, 100, 0.50) + entity.entity_data.entity._update_progress(50, 100, 0.50) state = hass.states.get(entity_id) assert not attrs[ATTR_IN_PROGRESS] @@ -438,20 +451,20 @@ async def test_firmware_update_success( async def test_firmware_update_raises( - hass: HomeAssistant, zha_device_joined_restored, zigpy_device + hass: HomeAssistant, + setup_zha, + zigpy_device_mock, ) -> None: """Test ZHA update platform - firmware update raises.""" + await setup_zha() zha_device, cluster, fw_image, installed_fw_version = await setup_test_data( - zha_device_joined_restored, zigpy_device + hass, zigpy_device_mock ) entity_id = find_entity_id(Platform.UPDATE, zha_device, hass) assert entity_id is not None - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - - assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(entity_id).state == STATE_UNKNOWN # simulate an image available notification await cluster._handle_query_next_image( @@ -460,7 +473,7 @@ async def test_firmware_update_raises( ), general.QueryNextImageCommand( fw_image.firmware.header.field_control, - zha_device.manufacturer_code, + zha_device.device.manufacturer_code, fw_image.firmware.header.image_type, installed_fw_version, fw_image.firmware.header.header_version, @@ -481,9 +494,9 @@ async def test_firmware_update_raises( if cluster_id == general.Ota.cluster_id: hdr, cmd = cluster.deserialize(data) if isinstance(cmd, general.Ota.ImageNotifyCommand): - zigpy_device.packet_received( + zha_device.device.device.packet_received( make_packet( - zigpy_device, + zha_device.device.device, cluster, general.Ota.ServerCommandDefs.query_next_image.name, field_control=general.Ota.QueryNextImageCommand.FieldControl.HardwareVersion, @@ -532,20 +545,20 @@ async def test_firmware_update_raises( async def test_firmware_update_no_longer_compatible( - hass: HomeAssistant, zha_device_joined_restored, zigpy_device + hass: HomeAssistant, + setup_zha, + zigpy_device_mock, ) -> None: """Test ZHA update platform - firmware update is no longer valid.""" + await setup_zha() zha_device, cluster, fw_image, installed_fw_version = await setup_test_data( - zha_device_joined_restored, zigpy_device + hass, zigpy_device_mock ) entity_id = find_entity_id(Platform.UPDATE, zha_device, hass) assert entity_id is not None - # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, [zha_device]) - - assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(entity_id).state == STATE_UNKNOWN # simulate an image available notification await cluster._handle_query_next_image( @@ -554,7 +567,7 @@ async def test_firmware_update_no_longer_compatible( ), general.QueryNextImageCommand( fw_image.firmware.header.field_control, - zha_device.manufacturer_code, + zha_device.device.manufacturer_code, fw_image.firmware.header.image_type, installed_fw_version, fw_image.firmware.header.header_version, @@ -577,9 +590,9 @@ async def test_firmware_update_no_longer_compatible( if cluster_id == general.Ota.cluster_id: hdr, cmd = cluster.deserialize(data) if isinstance(cmd, general.Ota.ImageNotifyCommand): - zigpy_device.packet_received( + zha_device.device.device.packet_received( make_packet( - zigpy_device, + zha_device.device.device, cluster, general.Ota.ServerCommandDefs.query_next_image.name, field_control=general.Ota.QueryNextImageCommand.FieldControl.HardwareVersion, diff --git a/tests/components/zha/test_websocket_api.py b/tests/components/zha/test_websocket_api.py index 80b9f6accd0..ea8ea39aed9 100644 --- a/tests/components/zha/test_websocket_api.py +++ b/tests/components/zha/test_websocket_api.py @@ -10,12 +10,27 @@ from unittest.mock import ANY, AsyncMock, MagicMock, call, patch from freezegun import freeze_time import pytest import voluptuous as vol +from zha.application.const import ( + ATTR_CLUSTER_ID, + ATTR_CLUSTER_TYPE, + ATTR_ENDPOINT_ID, + ATTR_ENDPOINT_NAMES, + ATTR_IEEE, + ATTR_MANUFACTURER, + ATTR_NEIGHBORS, + ATTR_QUIRK_APPLIED, + ATTR_TYPE, + CLUSTER_TYPE_IN, +) +from zha.zigbee.cluster_handlers import ClusterBindEvent, ClusterConfigureReportingEvent +from zha.zigbee.device import ClusterHandlerConfigurationComplete import zigpy.backups +from zigpy.const import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE import zigpy.profiles.zha import zigpy.types from zigpy.types.named import EUI64 import zigpy.util -from zigpy.zcl.clusters import general, security +from zigpy.zcl.clusters import closures, general, security from zigpy.zcl.clusters.general import Groups import zigpy.zdo.types as zdo_types @@ -25,23 +40,12 @@ from homeassistant.components.websocket_api import ( TYPE_RESULT, ) from homeassistant.components.zha import DOMAIN -from homeassistant.components.zha.core.const import ( - ATTR_CLUSTER_ID, - ATTR_CLUSTER_TYPE, - ATTR_ENDPOINT_ID, - ATTR_ENDPOINT_NAMES, - ATTR_IEEE, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_NEIGHBORS, - ATTR_QUIRK_APPLIED, - ATTR_TYPE, - BINDINGS, - CLUSTER_TYPE_IN, - EZSP_OVERWRITE_EUI64, - GROUP_ID, - GROUP_IDS, - GROUP_NAME, +from homeassistant.components.zha.const import EZSP_OVERWRITE_EUI64 +from homeassistant.components.zha.helpers import ( + ZHADeviceProxy, + ZHAGatewayProxy, + get_zha_gateway, + get_zha_gateway_proxy, ) from homeassistant.components.zha.websocket_api import ( ATTR_DURATION, @@ -49,22 +53,19 @@ from homeassistant.components.zha.websocket_api import ( ATTR_QR_CODE, ATTR_SOURCE_IEEE, ATTR_TARGET_IEEE, + BINDINGS, + GROUP_ID, + GROUP_IDS, + GROUP_NAME, ID, SERVICE_PERMIT, TYPE, async_load_api, ) -from homeassistant.const import ATTR_NAME, Platform +from homeassistant.const import ATTR_MODEL, ATTR_NAME, Platform from homeassistant.core import Context, HomeAssistant -from .conftest import ( - FIXTURE_GRP_ID, - FIXTURE_GRP_NAME, - SIG_EP_INPUT, - SIG_EP_OUTPUT, - SIG_EP_PROFILE, - SIG_EP_TYPE, -) +from .conftest import FIXTURE_GRP_ID, FIXTURE_GRP_NAME from .data import BASE_CUSTOM_CONFIGURATION, CONFIG_WITH_ALARM_OPTIONS from tests.common import MockConfigEntry, MockUser @@ -93,10 +94,18 @@ def required_platform_only(): @pytest.fixture -async def device_switch(hass, zigpy_device_mock, zha_device_joined): - """Test ZHA switch platform.""" +async def zha_client( + hass: HomeAssistant, + hass_ws_client: WebSocketGenerator, + setup_zha, + zigpy_device_mock, +) -> MockHAClientWebSocket: + """Get ZHA WebSocket client.""" - zigpy_device = zigpy_device_mock( + await setup_zha() + gateway = get_zha_gateway(hass) + + zigpy_device_switch = zigpy_device_mock( { 1: { SIG_EP_INPUT: [general.OnOff.cluster_id, general.Basic.cluster_id], @@ -107,35 +116,8 @@ async def device_switch(hass, zigpy_device_mock, zha_device_joined): }, ieee=IEEE_SWITCH_DEVICE, ) - zha_device = await zha_device_joined(zigpy_device) - zha_device.available = True - return zha_device - -@pytest.fixture -async def device_ias_ace(hass, zigpy_device_mock, zha_device_joined): - """Test alarm control panel device.""" - - zigpy_device = zigpy_device_mock( - { - 1: { - SIG_EP_INPUT: [security.IasAce.cluster_id], - SIG_EP_OUTPUT: [], - SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.IAS_ANCILLARY_CONTROL, - SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, - } - }, - ) - zha_device = await zha_device_joined(zigpy_device) - zha_device.available = True - return zha_device - - -@pytest.fixture -async def device_groupable(hass, zigpy_device_mock, zha_device_joined): - """Test ZHA light platform.""" - - zigpy_device = zigpy_device_mock( + zigpy_device_groupable = zigpy_device_mock( { 1: { SIG_EP_INPUT: [ @@ -150,19 +132,14 @@ async def device_groupable(hass, zigpy_device_mock, zha_device_joined): }, ieee=IEEE_GROUPABLE_DEVICE, ) - zha_device = await zha_device_joined(zigpy_device) - zha_device.available = True - return zha_device + gateway.get_or_create_device(zigpy_device_switch) + await gateway.async_device_initialized(zigpy_device_switch) + await hass.async_block_till_done(wait_background_tasks=True) -@pytest.fixture -async def zha_client( - hass: HomeAssistant, - hass_ws_client: WebSocketGenerator, - device_switch, - device_groupable, -) -> MockHAClientWebSocket: - """Get ZHA WebSocket client.""" + gateway.get_or_create_device(zigpy_device_groupable) + await gateway.async_device_initialized(zigpy_device_groupable) + await hass.async_block_till_done(wait_background_tasks=True) # load the ZHA API async_load_api(hass) @@ -247,7 +224,7 @@ async def test_list_devices(zha_client) -> None: msg = await zha_client.receive_json() devices = msg["result"] - assert len(devices) == 2 + 1 # the coordinator is included as well + assert len(devices) == 3 # the coordinator is included as well msg_id = 100 for device in devices: @@ -284,9 +261,31 @@ async def test_get_zha_config(zha_client) -> None: async def test_get_zha_config_with_alarm( - hass: HomeAssistant, zha_client, device_ias_ace + hass: HomeAssistant, zha_client, zigpy_device_mock ) -> None: """Test getting ZHA custom configuration.""" + + gateway = get_zha_gateway(hass) + gateway_proxy: ZHAGatewayProxy = get_zha_gateway_proxy(hass) + + zigpy_device_ias = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [security.IasAce.cluster_id], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.IAS_ANCILLARY_CONTROL, + SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, + } + }, + ) + + gateway.get_or_create_device(zigpy_device_ias) + await gateway.async_device_initialized(zigpy_device_ias) + await hass.async_block_till_done(wait_background_tasks=True) + zha_device_proxy: ZHADeviceProxy = gateway_proxy.get_device_proxy( + zigpy_device_ias.ieee + ) + await zha_client.send_json({ID: 5, TYPE: "zha/configuration"}) msg = await zha_client.receive_json() @@ -295,7 +294,7 @@ async def test_get_zha_config_with_alarm( assert configuration == CONFIG_WITH_ALARM_OPTIONS # test that the alarm options are not in the config when we remove the device - device_ias_ace.gateway.device_removed(device_ias_ace.device) + zha_device_proxy.gateway_proxy.gateway.device_removed(zha_device_proxy.device) await hass.async_block_till_done() await zha_client.send_json({ID: 6, TYPE: "zha/configuration"}) @@ -390,11 +389,12 @@ async def test_get_group_not_found(zha_client) -> None: async def test_list_groupable_devices( - zha_client, device_groupable, zigpy_app_controller + hass: HomeAssistant, zha_client, zigpy_app_controller ) -> None: """Test getting ZHA devices that have a group cluster.""" # Ensure the coordinator doesn't have a group cluster coordinator = zigpy_app_controller.get_device(nwk=0x0000) + del coordinator.endpoints[1].in_clusters[Groups.cluster_id] await zha_client.send_json({ID: 10, TYPE: "zha/devices/groupable"}) @@ -425,7 +425,10 @@ async def test_list_groupable_devices( # Make sure there are no groupable devices when the device is unavailable # Make device unavailable - device_groupable.available = False + get_zha_gateway_proxy(hass).device_proxies[ + EUI64.convert(IEEE_GROUPABLE_DEVICE) + ].device.available = False + await hass.async_block_till_done(wait_background_tasks=True) await zha_client.send_json({ID: 11, TYPE: "zha/devices/groupable"}) @@ -1037,3 +1040,101 @@ async def test_websocket_bind_unbind_group( assert bind_mock.mock_calls == [call(test_group_id, ANY)] elif command_type == "unbind": assert unbind_mock.mock_calls == [call(test_group_id, ANY)] + + +async def test_websocket_reconfigure( + hass: HomeAssistant, zha_client: MockHAClientWebSocket, zigpy_device_mock +) -> None: + """Test websocket API to reconfigure a device.""" + gateway = get_zha_gateway(hass) + zigpy_device = zigpy_device_mock( + { + 1: { + SIG_EP_INPUT: [closures.WindowCovering.cluster_id], + SIG_EP_OUTPUT: [], + SIG_EP_TYPE: zigpy.profiles.zha.DeviceType.SHADE, + SIG_EP_PROFILE: zigpy.profiles.zha.PROFILE_ID, + } + }, + ) + + zha_device = gateway.get_or_create_device(zigpy_device) + await gateway.async_device_initialized(zigpy_device) + await hass.async_block_till_done(wait_background_tasks=True) + + zha_device_proxy = get_zha_gateway_proxy(hass).get_device_proxy(zha_device.ieee) + + def mock_reconfigure() -> None: + zha_device_proxy.handle_zha_channel_configure_reporting( + ClusterConfigureReportingEvent( + cluster_name="Window Covering", + cluster_id=258, + attributes={ + "current_position_lift_percentage": { + "min": 0, + "max": 900, + "id": "current_position_lift_percentage", + "name": "current_position_lift_percentage", + "change": 1, + "status": "SUCCESS", + }, + "current_position_tilt_percentage": { + "min": 0, + "max": 900, + "id": "current_position_tilt_percentage", + "name": "current_position_tilt_percentage", + "change": 1, + "status": "SUCCESS", + }, + }, + cluster_handler_unique_id="28:2c:02:bf:ff:ea:05:68:1:0x0102", + event_type="zha_channel_message", + event="zha_channel_configure_reporting", + ) + ) + + zha_device_proxy.handle_zha_channel_bind( + ClusterBindEvent( + cluster_name="Window Covering", + cluster_id=1, + success=True, + cluster_handler_unique_id="28:2c:02:bf:ff:ea:05:68:1:0x0012", + event_type="zha_channel_message", + event="zha_channel_bind", + ) + ) + + zha_device_proxy.handle_zha_channel_cfg_done( + ClusterHandlerConfigurationComplete( + device_ieee="28:2c:02:bf:ff:ea:05:68", + unique_id="28:2c:02:bf:ff:ea:05:68", + event_type="zha_channel_message", + event="zha_channel_cfg_done", + ) + ) + + with patch.object( + zha_device_proxy.device, "async_configure", side_effect=mock_reconfigure + ): + await zha_client.send_json( + { + ID: 6, + TYPE: "zha/devices/reconfigure", + ATTR_IEEE: str(zha_device_proxy.device.ieee), + } + ) + + messages = [] + + while len(messages) != 3: + msg = await zha_client.receive_json() + + if msg[ID] == 6: + messages.append(msg) + + # Ensure the frontend receives progress events + assert {m["event"]["type"] for m in messages} == { + "zha_channel_configure_reporting", + "zha_channel_bind", + "zha_channel_cfg_done", + } diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py deleted file mode 100644 index 4c23244c5e0..00000000000 --- a/tests/components/zha/zha_devices_list.py +++ /dev/null @@ -1,5922 +0,0 @@ -"""Example Zigbee Devices.""" - -from zigpy.const import ( - SIG_ENDPOINTS, - SIG_EP_INPUT, - SIG_EP_OUTPUT, - SIG_EP_PROFILE, - SIG_EP_TYPE, - SIG_MANUFACTURER, - SIG_MODEL, - SIG_NODE_DESC, -) -from zigpy.profiles import zha, zll -from zigpy.types import Bool, uint8_t -from zigpy.zcl.clusters.closures import DoorLock -from zigpy.zcl.clusters.general import ( - Basic, - Groups, - Identify, - LevelControl, - MultistateInput, - OnOff, - Ota, - PowerConfiguration, - Scenes, -) -from zigpy.zcl.clusters.lighting import Color -from zigpy.zcl.clusters.measurement import ( - IlluminanceMeasurement, - OccupancySensing, - TemperatureMeasurement, -) - -DEV_SIG_CLUSTER_HANDLERS = "cluster_handlers" -DEV_SIG_DEV_NO = "device_no" -DEV_SIG_ENT_MAP = "entity_map" -DEV_SIG_ENT_MAP_CLASS = "entity_class" -DEV_SIG_ENT_MAP_ID = "entity_id" -DEV_SIG_EP_ID = "endpoint_id" -DEV_SIG_EVT_CLUSTER_HANDLERS = "event_cluster_handlers" -DEV_SIG_ZHA_QUIRK = "zha_quirk" -DEV_SIG_ATTRIBUTES = "attributes" - - -PROFILE_ID = SIG_EP_PROFILE -DEVICE_TYPE = SIG_EP_TYPE -INPUT_CLUSTERS = SIG_EP_INPUT -OUTPUT_CLUSTERS = SIG_EP_OUTPUT - -DEVICES = [ - { - DEV_SIG_DEV_NO: 0, - SIG_MANUFACTURER: "ADUROLIGHT", - SIG_MODEL: "Adurolight_NCC", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 2080, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4096, 64716], - SIG_EP_OUTPUT: [3, 4, 6, 8, 4096, 64716], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0006", "1:0x0008"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.adurolight_adurolight_ncc_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.adurolight_adurolight_ncc_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.adurolight_adurolight_ncc_lqi", - }, - }, - }, - { - DEV_SIG_DEV_NO: 1, - SIG_MANUFACTURER: "Bosch", - SIG_MODEL: "ISW-ZPR1-WP13", - SIG_NODE_DESC: b"\x02@\x08\x00\x00l\x00\x00\x00\x00\x00\x00\x00", - SIG_ENDPOINTS: { - 5: { - SIG_EP_TYPE: 1026, - DEV_SIG_EP_ID: 5, - SIG_EP_INPUT: [0, 1, 3, 32, 1026, 1280, 2821], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["5:0x0019"], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-5-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.bosch_isw_zpr1_wp13_ias_zone", - }, - ("button", "00:11:22:33:44:55:66:77-5-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.bosch_isw_zpr1_wp13_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-5-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-5-1026"): { - DEV_SIG_CLUSTER_HANDLERS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_temperature", - }, - ("sensor", "00:11:22:33:44:55:66:77-5-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-5-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.bosch_isw_zpr1_wp13_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-5-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.bosch_isw_zpr1_wp13_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 2, - SIG_MANUFACTURER: "CentraLite", - SIG_MODEL: "3130", - SIG_NODE_DESC: b"\x02@\x80N\x10RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 1, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 32, 2821], - SIG_EP_OUTPUT: [3, 6, 8, 25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0006", "1:0x0008", "1:0x0019"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3130_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3130_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.centralite_3130_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 3, - SIG_MANUFACTURER: "CentraLite", - SIG_MODEL: "3210-L", - SIG_NODE_DESC: b"\x01@\x8eN\x10RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 81, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 1794, 2820, 2821, 64515], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("switch", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.centralite_3210_l_switch", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3210_l_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "PolledElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_power", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_apparent_power", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_current", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_voltage", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_ac_frequency", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_power_factor", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_instantaneous_demand", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_summation_delivered", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3210_l_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.centralite_3210_l_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 4, - SIG_MANUFACTURER: "CentraLite", - SIG_MODEL: "3310-S", - SIG_NODE_DESC: b"\x02@\x80\xdf\xc2RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 770, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 32, 1026, 2821, 64581], - SIG_EP_OUTPUT: [3, 25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3310_s_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CLUSTER_HANDLERS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_temperature", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_lqi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-64581"): { - DEV_SIG_CLUSTER_HANDLERS: ["humidity"], - DEV_SIG_ENT_MAP_CLASS: "Humidity", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3310_s_humidity", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.centralite_3310_s_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 5, - SIG_MANUFACTURER: "CentraLite", - SIG_MODEL: "3315-S", - SIG_NODE_DESC: b"\x02@\x80\xdf\xc2RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 1026, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 32, 1026, 1280, 2821], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - 2: { - SIG_EP_TYPE: 12, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [0, 3, 2821, 64527], - SIG_EP_OUTPUT: [3], - SIG_EP_PROFILE: 49887, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3315_s_ias_zone", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3315_s_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CLUSTER_HANDLERS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_temperature", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3315_s_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.centralite_3315_s_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 6, - SIG_MANUFACTURER: "CentraLite", - SIG_MODEL: "3320-L", - SIG_NODE_DESC: b"\x02@\x80\xdf\xc2RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 1026, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 32, 1026, 1280, 2821], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - 2: { - SIG_EP_TYPE: 12, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [0, 3, 2821, 64527], - SIG_EP_OUTPUT: [3], - SIG_EP_PROFILE: 49887, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3320_l_ias_zone", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3320_l_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CLUSTER_HANDLERS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_temperature", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3320_l_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.centralite_3320_l_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 7, - SIG_MANUFACTURER: "CentraLite", - SIG_MODEL: "3326-L", - SIG_NODE_DESC: b"\x02@\x80\xdf\xc2RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 1026, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 32, 1026, 1280, 2821], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - 2: { - SIG_EP_TYPE: 263, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [0, 3, 2821, 64582], - SIG_EP_OUTPUT: [3], - SIG_EP_PROFILE: 49887, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_3326_l_ias_zone", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_3326_l_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CLUSTER_HANDLERS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_temperature", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_3326_l_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.centralite_3326_l_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 8, - SIG_MANUFACTURER: "CentraLite", - SIG_MODEL: "Motion Sensor-A", - SIG_NODE_DESC: b"\x02@\x80N\x10RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 1026, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 32, 1026, 1280, 2821], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - 2: { - SIG_EP_TYPE: 263, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [0, 3, 1030, 2821], - SIG_EP_OUTPUT: [3], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.centralite_motion_sensor_a_ias_zone", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.centralite_motion_sensor_a_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CLUSTER_HANDLERS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_temperature", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.centralite_motion_sensor_a_lqi", - }, - ("binary_sensor", "00:11:22:33:44:55:66:77-2-1030"): { - DEV_SIG_CLUSTER_HANDLERS: ["occupancy"], - DEV_SIG_ENT_MAP_CLASS: "Occupancy", - DEV_SIG_ENT_MAP_ID: ( - "binary_sensor.centralite_motion_sensor_a_occupancy" - ), - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.centralite_motion_sensor_a_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 9, - SIG_MANUFACTURER: "ClimaxTechnology", - SIG_MODEL: "PSMP5_00.00.02.02TC", - SIG_NODE_DESC: b"\x01@\x8e\x00\x00P\xa0\x00\x00\x00\xa0\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 81, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 1794], - SIG_EP_OUTPUT: [0], - SIG_EP_PROFILE: 260, - }, - 4: { - SIG_EP_TYPE: 9, - DEV_SIG_EP_ID: 4, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["4:0x0019"], - DEV_SIG_ENT_MAP: { - ("switch", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: ( - "switch.climaxtechnology_psmp5_00_00_02_02tc_switch" - ), - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: ( - "button.climaxtechnology_psmp5_00_00_02_02tc_identify" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: ( - "sensor.climaxtechnology_psmp5_00_00_02_02tc_instantaneous_demand" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: ( - "sensor.climaxtechnology_psmp5_00_00_02_02tc_summation_delivered" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_psmp5_00_00_02_02tc_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-4-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.climaxtechnology_psmp5_00_00_02_02tc_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 10, - SIG_MANUFACTURER: "ClimaxTechnology", - SIG_MODEL: "SD8SC_00.00.03.12TC", - SIG_NODE_DESC: b"\x02@\x80\x00\x00P\xa0\x00\x00\x00\xa0\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 1026, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 1280, 1282], - SIG_EP_OUTPUT: [0], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: [], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: ( - "binary_sensor.climaxtechnology_sd8sc_00_00_03_12tc_ias_zone" - ), - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: ( - "button.climaxtechnology_sd8sc_00_00_03_12tc_identify" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_sd8sc_00_00_03_12tc_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_sd8sc_00_00_03_12tc_lqi", - }, - ("select", "00:11:22:33:44:55:66:77-1-1282-WarningMode"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_wd"], - DEV_SIG_ENT_MAP_CLASS: "ZHADefaultToneSelectEntity", - DEV_SIG_ENT_MAP_ID: ( - "select.climaxtechnology_sd8sc_00_00_03_12tc_default_siren_tone" - ), - }, - ("select", "00:11:22:33:44:55:66:77-1-1282-SirenLevel"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_wd"], - DEV_SIG_ENT_MAP_CLASS: "ZHADefaultSirenLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: ( - "select.climaxtechnology_sd8sc_00_00_03_12tc_default_siren_level" - ), - }, - ("select", "00:11:22:33:44:55:66:77-1-1282-StrobeLevel"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_wd"], - DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: ( - "select.climaxtechnology_sd8sc_00_00_03_12tc_default_strobe_level" - ), - }, - ("select", "00:11:22:33:44:55:66:77-1-1282-Strobe"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_wd"], - DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeSelectEntity", - DEV_SIG_ENT_MAP_ID: ( - "select.climaxtechnology_sd8sc_00_00_03_12tc_default_strobe" - ), - }, - ("siren", "00:11:22:33:44:55:66:77-1-1282"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_wd"], - DEV_SIG_ENT_MAP_CLASS: "ZHASiren", - DEV_SIG_ENT_MAP_ID: "siren.climaxtechnology_sd8sc_00_00_03_12tc_siren", - }, - }, - }, - { - DEV_SIG_DEV_NO: 11, - SIG_MANUFACTURER: "ClimaxTechnology", - SIG_MODEL: "WS15_00.00.03.03TC", - SIG_NODE_DESC: b"\x02@\x80\x00\x00P\xa0\x00\x00\x00\xa0\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 1026, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 1280], - SIG_EP_OUTPUT: [0], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: [], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: ( - "binary_sensor.climaxtechnology_ws15_00_00_03_03tc_ias_zone" - ), - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: ( - "button.climaxtechnology_ws15_00_00_03_03tc_identify" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_ws15_00_00_03_03tc_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.climaxtechnology_ws15_00_00_03_03tc_lqi", - }, - }, - }, - { - DEV_SIG_DEV_NO: 12, - SIG_MANUFACTURER: "Feibit Inc co.", - SIG_MODEL: "FB56-ZCW08KU1.1", - SIG_NODE_DESC: b"\x01@\x8e\x00\x00P\xa0\x00\x00\x00\xa0\x00\x00", - SIG_ENDPOINTS: { - 11: { - SIG_EP_TYPE: 528, - DEV_SIG_EP_ID: 11, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 768], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 49246, - }, - 13: { - SIG_EP_TYPE: 57694, - DEV_SIG_EP_ID: 13, - SIG_EP_INPUT: [4096], - SIG_EP_OUTPUT: [4096], - SIG_EP_PROFILE: 49246, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: [], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-11"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off", "light_color", "level"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.feibit_inc_co_fb56_zcw08ku1_1_light", - }, - ("button", "00:11:22:33:44:55:66:77-11-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.feibit_inc_co_fb56_zcw08ku1_1_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-11-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.feibit_inc_co_fb56_zcw08ku1_1_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-11-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.feibit_inc_co_fb56_zcw08ku1_1_lqi", - }, - }, - }, - { - DEV_SIG_DEV_NO: 13, - SIG_MANUFACTURER: "HEIMAN", - SIG_MODEL: "SmokeSensor-EM", - SIG_NODE_DESC: b"\x02@\x80\x0b\x12RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 1026, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 1280, 1282], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_smokesensor_em_ias_zone", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.heiman_smokesensor_em_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_smokesensor_em_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.heiman_smokesensor_em_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 14, - SIG_MANUFACTURER: "Heiman", - SIG_MODEL: "CO_V16", - SIG_NODE_DESC: b"\x02@\x84\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 1026, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 9, 1280], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_co_v16_ias_zone", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.heiman_co_v16_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_co_v16_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_co_v16_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.heiman_co_v16_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 15, - SIG_MANUFACTURER: "Heiman", - SIG_MODEL: "WarningDevice", - SIG_NODE_DESC: b"\x01@\x8e\x0b\x12RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 1027, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 4, 9, 1280, 1282], - SIG_EP_OUTPUT: [3, 25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("select", "00:11:22:33:44:55:66:77-1-1282-WarningMode"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_wd"], - DEV_SIG_ENT_MAP_CLASS: "ZHADefaultToneSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_default_siren_tone", - }, - ("select", "00:11:22:33:44:55:66:77-1-1282-SirenLevel"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_wd"], - DEV_SIG_ENT_MAP_CLASS: "ZHADefaultSirenLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_default_siren_level", - }, - ("select", "00:11:22:33:44:55:66:77-1-1282-StrobeLevel"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_wd"], - DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeLevelSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_default_strobe_level", - }, - ("select", "00:11:22:33:44:55:66:77-1-1282-Strobe"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_wd"], - DEV_SIG_ENT_MAP_CLASS: "ZHADefaultStrobeSelectEntity", - DEV_SIG_ENT_MAP_ID: "select.heiman_warningdevice_default_strobe", - }, - ("siren", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_wd"], - DEV_SIG_ENT_MAP_CLASS: "ZHASiren", - DEV_SIG_ENT_MAP_ID: "siren.heiman_warningdevice_siren", - }, - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.heiman_warningdevice_ias_zone", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.heiman_warningdevice_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_warningdevice_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.heiman_warningdevice_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.heiman_warningdevice_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 16, - SIG_MANUFACTURER: "HiveHome.com", - SIG_MODEL: "MOT003", - SIG_NODE_DESC: b"\x02@\x809\x10PP\x00\x00\x00P\x00\x00", - SIG_ENDPOINTS: { - 6: { - SIG_EP_TYPE: 1026, - DEV_SIG_EP_ID: 6, - SIG_EP_INPUT: [0, 1, 3, 32, 1024, 1026, 1280], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["6:0x0019"], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-6-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.hivehome_com_mot003_ias_zone", - }, - ("button", "00:11:22:33:44:55:66:77-6-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.hivehome_com_mot003_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-6-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-6-1024"): { - DEV_SIG_CLUSTER_HANDLERS: ["illuminance"], - DEV_SIG_ENT_MAP_CLASS: "Illuminance", - DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_illuminance", - }, - ("sensor", "00:11:22:33:44:55:66:77-6-1026"): { - DEV_SIG_CLUSTER_HANDLERS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_temperature", - }, - ("sensor", "00:11:22:33:44:55:66:77-6-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-6-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.hivehome_com_mot003_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-6-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.hivehome_com_mot003_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 17, - SIG_MANUFACTURER: "IKEA of Sweden", - SIG_MODEL: "TRADFRI bulb E12 WS opal 600lm", - SIG_NODE_DESC: b"\x01@\x8e|\x11RR\x00\x00,R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 268, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 768, 4096, 64636], - SIG_EP_OUTPUT: [5, 25, 32, 4096], - SIG_EP_PROFILE: 260, - }, - 242: { - SIG_EP_TYPE: 97, - DEV_SIG_EP_ID: 242, - SIG_EP_INPUT: [33], - SIG_EP_OUTPUT: [33], - SIG_EP_PROFILE: 41440, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0005", "1:0x0019"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off", "level", "light_color"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: ( - "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_light" - ), - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: ( - "button.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_identify" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: ( - "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_rssi" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: ( - "sensor.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_lqi" - ), - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 18, - SIG_MANUFACTURER: "IKEA of Sweden", - SIG_MODEL: "TRADFRI bulb E26 CWS opal 600lm", - SIG_NODE_DESC: b"\x01@\x8e|\x11RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 512, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 768, 2821, 4096], - SIG_EP_OUTPUT: [5, 25, 32, 4096], - SIG_EP_PROFILE: 49246, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0005", "1:0x0019"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off", "level", "light_color"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: ( - "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_light" - ), - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: ( - "button.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_identify" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: ( - "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_rssi" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: ( - "sensor.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_lqi" - ), - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 19, - SIG_MANUFACTURER: "IKEA of Sweden", - SIG_MODEL: "TRADFRI bulb E26 W opal 1000lm", - SIG_NODE_DESC: b"\x01@\x8e|\x11RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 256, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 2821, 4096], - SIG_EP_OUTPUT: [5, 25, 32, 4096], - SIG_EP_PROFILE: 49246, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0005", "1:0x0019"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off", "level"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: ( - "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_light" - ), - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: ( - "button.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_identify" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: ( - "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_rssi" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: ( - "sensor.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_lqi" - ), - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 20, - SIG_MANUFACTURER: "IKEA of Sweden", - SIG_MODEL: "TRADFRI bulb E26 WS opal 980lm", - SIG_NODE_DESC: b"\x01@\x8e|\x11RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 544, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 768, 2821, 4096], - SIG_EP_OUTPUT: [5, 25, 32, 4096], - SIG_EP_PROFILE: 49246, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0005", "1:0x0019"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off", "level", "light_color"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: ( - "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_light" - ), - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: ( - "button.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_identify" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: ( - "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_rssi" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: ( - "sensor.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_lqi" - ), - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 21, - SIG_MANUFACTURER: "IKEA of Sweden", - SIG_MODEL: "TRADFRI bulb E26 opal 1000lm", - SIG_NODE_DESC: b"\x01@\x8e|\x11RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 256, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 2821, 4096], - SIG_EP_OUTPUT: [5, 25, 32, 4096], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0005", "1:0x0019"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off", "level"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: ( - "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_light" - ), - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: ( - "button.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_identify" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: ( - "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_rssi" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: ( - "sensor.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_lqi" - ), - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 22, - SIG_MANUFACTURER: "IKEA of Sweden", - SIG_MODEL: "TRADFRI control outlet", - SIG_NODE_DESC: b"\x01@\x8e|\x11RR\x00\x00,R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 266, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 64636], - SIG_EP_OUTPUT: [5, 25, 32], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0005", "1:0x0019"], - DEV_SIG_ENT_MAP: { - ("switch", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: ( - "switch.ikea_of_sweden_tradfri_control_outlet_switch" - ), - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: ( - "button.ikea_of_sweden_tradfri_control_outlet_identify" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_control_outlet_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_control_outlet_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.ikea_of_sweden_tradfri_control_outlet_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 23, - SIG_MANUFACTURER: "IKEA of Sweden", - SIG_MODEL: "TRADFRI motion sensor", - SIG_NODE_DESC: b"\x02@\x80|\x11RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 2128, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 9, 2821, 4096], - SIG_EP_OUTPUT: [3, 4, 6, 25, 4096], - SIG_EP_PROFILE: 49246, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0006", "1:0x0019"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: ( - "button.ikea_of_sweden_tradfri_motion_sensor_identify" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: ( - "sensor.ikea_of_sweden_tradfri_motion_sensor_battery" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_motion_sensor_lqi", - }, - ("binary_sensor", "00:11:22:33:44:55:66:77-1-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Motion", - DEV_SIG_ENT_MAP_ID: ( - "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_motion" - ), - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.ikea_of_sweden_tradfri_motion_sensor_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 24, - SIG_MANUFACTURER: "IKEA of Sweden", - SIG_MODEL: "TRADFRI on/off switch", - SIG_NODE_DESC: b"\x02@\x80|\x11RR\x00\x00,R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 2080, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 9, 32, 4096, 64636], - SIG_EP_OUTPUT: [3, 4, 6, 8, 25, 258, 4096], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0006", "1:0x0008", "1:0x0019", "1:0x0102"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: ( - "button.ikea_of_sweden_tradfri_on_off_switch_identify" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: ( - "sensor.ikea_of_sweden_tradfri_on_off_switch_battery" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_on_off_switch_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.ikea_of_sweden_tradfri_on_off_switch_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 25, - SIG_MANUFACTURER: "IKEA of Sweden", - SIG_MODEL: "TRADFRI remote control", - SIG_NODE_DESC: b"\x02@\x80|\x11RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 2096, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 9, 2821, 4096], - SIG_EP_OUTPUT: [3, 4, 5, 6, 8, 25, 4096], - SIG_EP_PROFILE: 49246, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: ( - "button.ikea_of_sweden_tradfri_remote_control_identify" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: ( - "sensor.ikea_of_sweden_tradfri_remote_control_battery" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_remote_control_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.ikea_of_sweden_tradfri_remote_control_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 26, - SIG_MANUFACTURER: "IKEA of Sweden", - SIG_MODEL: "TRADFRI signal repeater", - SIG_NODE_DESC: b"\x01@\x8e|\x11RR\x00\x00,R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 8, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 9, 2821, 4096, 64636], - SIG_EP_OUTPUT: [25, 32, 4096], - SIG_EP_PROFILE: 260, - }, - 242: { - SIG_EP_TYPE: 97, - DEV_SIG_EP_ID: 242, - SIG_EP_INPUT: [33], - SIG_EP_OUTPUT: [33], - SIG_EP_PROFILE: 41440, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: ( - "button.ikea_of_sweden_tradfri_signal_repeater_identify" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: ( - "sensor.ikea_of_sweden_tradfri_signal_repeater_rssi" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_signal_repeater_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.ikea_of_sweden_tradfri_signal_repeater_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 27, - SIG_MANUFACTURER: "IKEA of Sweden", - SIG_MODEL: "TRADFRI wireless dimmer", - SIG_NODE_DESC: b"\x02@\x80|\x11RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 2064, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 9, 2821, 4096], - SIG_EP_OUTPUT: [3, 4, 6, 8, 25, 4096], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0006", "1:0x0008", "1:0x0019"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: ( - "button.ikea_of_sweden_tradfri_wireless_dimmer_identify" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: ( - "sensor.ikea_of_sweden_tradfri_wireless_dimmer_battery" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: ( - "sensor.ikea_of_sweden_tradfri_wireless_dimmer_rssi" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ikea_of_sweden_tradfri_wireless_dimmer_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.ikea_of_sweden_tradfri_wireless_dimmer_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 28, - SIG_MANUFACTURER: "Jasco Products", - SIG_MODEL: "45852", - SIG_NODE_DESC: b"\x01@\x8e$\x11R\xff\x00\x00\x00\xff\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 257, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 1794, 2821], - SIG_EP_OUTPUT: [10, 25], - SIG_EP_PROFILE: 260, - }, - 2: { - SIG_EP_TYPE: 260, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [0, 3, 2821], - SIG_EP_OUTPUT: [3, 6, 8], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019", "2:0x0006", "2:0x0008"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off", "level"], - DEV_SIG_ENT_MAP_CLASS: "ForceOnLight", - DEV_SIG_ENT_MAP_ID: "light.jasco_products_45852_light", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.jasco_products_45852_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_instantaneous_demand", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_summation_delivered", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45852_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.jasco_products_45852_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 29, - SIG_MANUFACTURER: "Jasco Products", - SIG_MODEL: "45856", - SIG_NODE_DESC: b"\x01@\x8e$\x11R\xff\x00\x00\x00\xff\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 256, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 1794, 2821], - SIG_EP_OUTPUT: [10, 25], - SIG_EP_PROFILE: 260, - }, - 2: { - SIG_EP_TYPE: 259, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [0, 3, 2821], - SIG_EP_OUTPUT: [3, 6], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019", "2:0x0006"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "ForceOnLight", - DEV_SIG_ENT_MAP_ID: "light.jasco_products_45856_light", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.jasco_products_45856_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_instantaneous_demand", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_summation_delivered", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45856_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.jasco_products_45856_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 30, - SIG_MANUFACTURER: "Jasco Products", - SIG_MODEL: "45857", - SIG_NODE_DESC: b"\x01@\x8e$\x11R\xff\x00\x00\x00\xff\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 257, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 1794, 2821], - SIG_EP_OUTPUT: [10, 25], - SIG_EP_PROFILE: 260, - }, - 2: { - SIG_EP_TYPE: 260, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [0, 3, 2821], - SIG_EP_OUTPUT: [3, 6, 8], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019", "2:0x0006", "2:0x0008"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off", "level"], - DEV_SIG_ENT_MAP_CLASS: "ForceOnLight", - DEV_SIG_ENT_MAP_ID: "light.jasco_products_45857_light", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.jasco_products_45857_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_instantaneous_demand", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_summation_delivered", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.jasco_products_45857_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.jasco_products_45857_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 31, - SIG_MANUFACTURER: "Keen Home Inc", - SIG_MODEL: "SV02-610-MP-1.3", - SIG_NODE_DESC: b"\x02@\x80[\x11RR\x00\x00*R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 3, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 4, 5, 6, 8, 32, 1026, 1027, 2821, 64513, 64514], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_610_mp_1_3_identify", - }, - ("cover", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["level", "on_off"], - DEV_SIG_ENT_MAP_CLASS: "KeenVent", - DEV_SIG_ENT_MAP_ID: "cover.keen_home_inc_sv02_610_mp_1_3_keen_vent", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { - DEV_SIG_CLUSTER_HANDLERS: ["pressure"], - DEV_SIG_ENT_MAP_CLASS: "Pressure", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_pressure", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CLUSTER_HANDLERS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_temperature", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_610_mp_1_3_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.keen_home_inc_sv02_610_mp_1_3_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 32, - SIG_MANUFACTURER: "Keen Home Inc", - SIG_MODEL: "SV02-612-MP-1.2", - SIG_NODE_DESC: b"\x02@\x80[\x11RR\x00\x00*R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 3, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 4, 5, 6, 8, 32, 1026, 1027, 2821, 64513, 64514], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_612_mp_1_2_identify", - }, - ("cover", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["level", "on_off"], - DEV_SIG_ENT_MAP_CLASS: "KeenVent", - DEV_SIG_ENT_MAP_ID: "cover.keen_home_inc_sv02_612_mp_1_2_keen_vent", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { - DEV_SIG_CLUSTER_HANDLERS: ["pressure"], - DEV_SIG_ENT_MAP_CLASS: "Pressure", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_pressure", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CLUSTER_HANDLERS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_temperature", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_2_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.keen_home_inc_sv02_612_mp_1_2_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 33, - SIG_MANUFACTURER: "Keen Home Inc", - SIG_MODEL: "SV02-612-MP-1.3", - SIG_NODE_DESC: b"\x02@\x80[\x11RR\x00\x00*R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 3, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 4, 5, 6, 8, 32, 1026, 1027, 2821, 64513, 64514], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.keen_home_inc_sv02_612_mp_1_3_identify", - }, - ("cover", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["level", "on_off"], - DEV_SIG_ENT_MAP_CLASS: "KeenVent", - DEV_SIG_ENT_MAP_ID: "cover.keen_home_inc_sv02_612_mp_1_3_keen_vent", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { - DEV_SIG_CLUSTER_HANDLERS: ["pressure"], - DEV_SIG_ENT_MAP_CLASS: "Pressure", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_pressure", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CLUSTER_HANDLERS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_temperature", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.keen_home_inc_sv02_612_mp_1_3_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.keen_home_inc_sv02_612_mp_1_3_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 34, - SIG_MANUFACTURER: "King Of Fans, Inc.", - SIG_MODEL: "HBUniversalCFRemote", - SIG_NODE_DESC: b"\x02@\x8c\x02\x10RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 257, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 514], - SIG_EP_OUTPUT: [3, 25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off", "level"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.king_of_fans_inc_hbuniversalcfremote_light", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: ( - "button.king_of_fans_inc_hbuniversalcfremote_identify" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.king_of_fans_inc_hbuniversalcfremote_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.king_of_fans_inc_hbuniversalcfremote_lqi", - }, - ("fan", "00:11:22:33:44:55:66:77-1-514"): { - DEV_SIG_CLUSTER_HANDLERS: ["fan"], - DEV_SIG_ENT_MAP_CLASS: "KofFan", - DEV_SIG_ENT_MAP_ID: "fan.king_of_fans_inc_hbuniversalcfremote_fan", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.king_of_fans_inc_hbuniversalcfremote_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 35, - SIG_MANUFACTURER: "LDS", - SIG_MODEL: "ZBT-CCTSwitch-D0001", - SIG_NODE_DESC: b"\x02@\x80h\x11RR\x00\x00,R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 2048, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 4096, 64769], - SIG_EP_OUTPUT: [3, 4, 6, 8, 25, 768, 4096], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0006", "1:0x0008", "1:0x0019", "1:0x0300"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lds_zbt_cctswitch_d0001_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lds_zbt_cctswitch_d0001_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.lds_zbt_cctswitch_d0001_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 36, - SIG_MANUFACTURER: "LEDVANCE", - SIG_MODEL: "A19 RGBW", - SIG_NODE_DESC: b"\x01@\x8e\x89\x11RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 258, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 768, 2821, 64513], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off", "level", "light_color"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ledvance_a19_rgbw_light", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ledvance_a19_rgbw_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_a19_rgbw_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_a19_rgbw_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.ledvance_a19_rgbw_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 37, - SIG_MANUFACTURER: "LEDVANCE", - SIG_MODEL: "FLEX RGBW", - SIG_NODE_DESC: b"\x01@\x8e\x89\x11RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 258, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 768, 2821, 64513], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off", "level", "light_color"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ledvance_flex_rgbw_light", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ledvance_flex_rgbw_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_flex_rgbw_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_flex_rgbw_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.ledvance_flex_rgbw_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 38, - SIG_MANUFACTURER: "LEDVANCE", - SIG_MODEL: "PLUG", - SIG_NODE_DESC: b"\x01@\x8e\x89\x11RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 81, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 2821, 64513, 64520], - SIG_EP_OUTPUT: [3, 25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("switch", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.ledvance_plug_switch", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ledvance_plug_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_plug_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_plug_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.ledvance_plug_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 39, - SIG_MANUFACTURER: "LEDVANCE", - SIG_MODEL: "RT RGBW", - SIG_NODE_DESC: b"\x01@\x8e\x89\x11RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 258, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 768, 2821, 64513], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off", "level", "light_color"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.ledvance_rt_rgbw_light", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.ledvance_rt_rgbw_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_rt_rgbw_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.ledvance_rt_rgbw_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.ledvance_rt_rgbw_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 40, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.plug.maus01", - SIG_NODE_DESC: b"\x01@\x8e_\x11\x7fd\x00\x00\x00d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 81, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 2, 3, 4, 5, 6, 10, 16, 2820], - SIG_EP_OUTPUT: [10, 25], - SIG_EP_PROFILE: 260, - }, - 2: { - SIG_EP_TYPE: 9, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [12], - SIG_EP_OUTPUT: [4, 12], - SIG_EP_PROFILE: 260, - }, - 3: { - SIG_EP_TYPE: 83, - DEV_SIG_EP_ID: 3, - SIG_EP_INPUT: [12], - SIG_EP_OUTPUT: [12], - SIG_EP_PROFILE: 260, - }, - 100: { - SIG_EP_TYPE: 263, - DEV_SIG_EP_ID: 100, - SIG_EP_INPUT: [15], - SIG_EP_OUTPUT: [4, 15], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("switch", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.lumi_lumi_plug_maus01_switch", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2"): { - DEV_SIG_CLUSTER_HANDLERS: ["device_temperature"], - DEV_SIG_ENT_MAP_CLASS: "DeviceTemperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_device_temperature", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_plug_maus01_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "PolledElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_power", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_voltage", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_lqi", - }, - ("binary_sensor", "00:11:22:33:44:55:66:77-100-15"): { - DEV_SIG_CLUSTER_HANDLERS: ["binary_input"], - DEV_SIG_ENT_MAP_CLASS: "BinaryInput", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_plug_maus01_binary_input", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_plug_maus01_summation_delivered", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_plug_maus01_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 41, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.relay.c2acn01", - SIG_NODE_DESC: b"\x01@\x8e7\x10\x7fd\x00\x00\x00d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 257, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 2, 3, 4, 5, 6, 10, 12, 16, 2820], - SIG_EP_OUTPUT: [10, 25], - SIG_EP_PROFILE: 260, - }, - 2: { - SIG_EP_TYPE: 257, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [4, 5, 6, 16], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_relay_c2acn01_light", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2"): { - DEV_SIG_CLUSTER_HANDLERS: ["device_temperature"], - DEV_SIG_ENT_MAP_CLASS: "DeviceTemperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_device_temperature", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_relay_c2acn01_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "PolledElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_power", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_apparent_power", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_current", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_voltage", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_ac_frequency", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_power_factor", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_summation_delivered", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_instantaneous_demand", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_relay_c2acn01_lqi", - }, - ("light", "00:11:22:33:44:55:66:77-2"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_relay_c2acn01_light_2", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_relay_c2acn01_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 42, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.remote.b186acn01", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 24321, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 18, 25, 65535], - SIG_EP_OUTPUT: [0, 3, 4, 5, 18, 25, 65535], - SIG_EP_PROFILE: 260, - }, - 2: { - SIG_EP_TYPE: 24322, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [3, 18], - SIG_EP_OUTPUT: [3, 4, 5, 18], - SIG_EP_PROFILE: 260, - }, - 3: { - SIG_EP_TYPE: 24323, - DEV_SIG_EP_ID: 3, - SIG_EP_INPUT: [3, 18], - SIG_EP_OUTPUT: [3, 4, 5, 12, 18], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b186acn01_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b186acn01_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_remote_b186acn01_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 43, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.remote.b286acn01", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 24321, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 18, 25, 65535], - SIG_EP_OUTPUT: [0, 3, 4, 5, 18, 25, 65535], - SIG_EP_PROFILE: 260, - }, - 2: { - SIG_EP_TYPE: 24322, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [3, 18], - SIG_EP_OUTPUT: [3, 4, 5, 18], - SIG_EP_PROFILE: 260, - }, - 3: { - SIG_EP_TYPE: 24323, - DEV_SIG_EP_ID: 3, - SIG_EP_INPUT: [3, 18], - SIG_EP_OUTPUT: [3, 4, 5, 12, 18], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b286acn01_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286acn01_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_remote_b286acn01_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 44, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.remote.b286opcn01", - SIG_NODE_DESC: b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 261, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3], - SIG_EP_OUTPUT: [3, 6, 8, 768], - SIG_EP_PROFILE: 260, - }, - 2: { - SIG_EP_TYPE: -1, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: -1, - }, - 3: { - SIG_EP_TYPE: -1, - DEV_SIG_EP_ID: 3, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: -1, - }, - 4: { - SIG_EP_TYPE: -1, - DEV_SIG_EP_ID: 4, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: -1, - }, - 5: { - SIG_EP_TYPE: -1, - DEV_SIG_EP_ID: 5, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: -1, - }, - 6: { - SIG_EP_TYPE: -1, - DEV_SIG_EP_ID: 6, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: -1, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0006", "1:0x0008", "1:0x0300", "2:0x0006"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b286opcn01_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286opcn01_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286opcn01_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b286opcn01_lqi", - }, - }, - }, - { - DEV_SIG_DEV_NO: 45, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.remote.b486opcn01", - SIG_NODE_DESC: b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 261, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3], - SIG_EP_OUTPUT: [3, 6, 8, 768], - SIG_EP_PROFILE: 260, - }, - 2: { - SIG_EP_TYPE: 259, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [3], - SIG_EP_OUTPUT: [3, 6], - SIG_EP_PROFILE: 260, - }, - 3: { - SIG_EP_TYPE: -1, - DEV_SIG_EP_ID: 3, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: -1, - }, - 4: { - SIG_EP_TYPE: -1, - DEV_SIG_EP_ID: 4, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: -1, - }, - 5: { - SIG_EP_TYPE: -1, - DEV_SIG_EP_ID: 5, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: -1, - }, - 6: { - SIG_EP_TYPE: -1, - DEV_SIG_EP_ID: 6, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: -1, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0006", "1:0x0008", "1:0x0300", "2:0x0006"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b486opcn01_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b486opcn01_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b486opcn01_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b486opcn01_lqi", - }, - }, - }, - { - DEV_SIG_DEV_NO: 46, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.remote.b686opcn01", - SIG_NODE_DESC: b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 261, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3], - SIG_EP_OUTPUT: [3, 6, 8, 768], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0006", "1:0x0008", "1:0x0300", "2:0x0006"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b686opcn01_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_lqi", - }, - }, - }, - { - DEV_SIG_DEV_NO: 47, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.remote.b686opcn01", - SIG_NODE_DESC: b"\x02@\x84_\x11\x7fd\x00\x00,d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 261, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3], - SIG_EP_OUTPUT: [3, 6, 8, 768], - SIG_EP_PROFILE: 260, - }, - 2: { - SIG_EP_TYPE: 259, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [3], - SIG_EP_OUTPUT: [3, 6], - SIG_EP_PROFILE: 260, - }, - 3: { - SIG_EP_TYPE: None, - DEV_SIG_EP_ID: 3, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: None, - }, - 4: { - SIG_EP_TYPE: None, - DEV_SIG_EP_ID: 4, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: None, - }, - 5: { - SIG_EP_TYPE: None, - DEV_SIG_EP_ID: 5, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: None, - }, - 6: { - SIG_EP_TYPE: None, - DEV_SIG_EP_ID: 6, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: None, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0006", "1:0x0008", "1:0x0300", "2:0x0006"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_remote_b686opcn01_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_remote_b686opcn01_lqi", - }, - }, - }, - { - DEV_SIG_DEV_NO: 48, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.router", - SIG_NODE_DESC: b"\x01@\x8e_\x11P\xa0\x00\x00\x00\xa0\x00\x00", - SIG_ENDPOINTS: { - 8: { - SIG_EP_TYPE: 256, - DEV_SIG_EP_ID: 8, - SIG_EP_INPUT: [0, 6], - SIG_EP_OUTPUT: [0, 6], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["8:0x0006"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-8"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_light", - }, - ("sensor", "00:11:22:33:44:55:66:77-8-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-8-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_lqi", - }, - ("binary_sensor", "00:11:22:33:44:55:66:77-8-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_opening", - }, - }, - }, - { - DEV_SIG_DEV_NO: 49, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.router", - SIG_NODE_DESC: b"\x01@\x8e_\x11P\xa0\x00\x00\x00\xa0\x00\x00", - SIG_ENDPOINTS: { - 8: { - SIG_EP_TYPE: 256, - DEV_SIG_EP_ID: 8, - SIG_EP_INPUT: [0, 6, 11, 17], - SIG_EP_OUTPUT: [0, 6], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["8:0x0006"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-8"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_light", - }, - ("sensor", "00:11:22:33:44:55:66:77-8-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-8-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_lqi", - }, - ("binary_sensor", "00:11:22:33:44:55:66:77-8-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_opening", - }, - }, - }, - { - DEV_SIG_DEV_NO: 50, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.router", - SIG_NODE_DESC: b"\x01@\x8e_\x11P\xa0\x00\x00\x00\xa0\x00\x00", - SIG_ENDPOINTS: { - 8: { - SIG_EP_TYPE: 256, - DEV_SIG_EP_ID: 8, - SIG_EP_INPUT: [0, 6, 17], - SIG_EP_OUTPUT: [0, 6], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["8:0x0006"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-8"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.lumi_lumi_router_light", - }, - ("sensor", "00:11:22:33:44:55:66:77-8-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-8-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_router_lqi", - }, - ("binary_sensor", "00:11:22:33:44:55:66:77-8-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_router_opening", - }, - }, - }, - { - DEV_SIG_DEV_NO: 51, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sen_ill.mgl01", - SIG_NODE_DESC: b"\x02@\x84n\x12\x7fd\x00\x00,d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 262, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 1024], - SIG_EP_OUTPUT: [3], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: [], - DEV_SIG_ENT_MAP: { - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_battery", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sen_ill_mgl01_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1024"): { - DEV_SIG_CLUSTER_HANDLERS: ["illuminance"], - DEV_SIG_ENT_MAP_CLASS: "Illuminance", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_illuminance", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sen_ill_mgl01_lqi", - }, - }, - }, - { - DEV_SIG_DEV_NO: 52, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sensor_86sw1", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 24321, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 18, 25, 65535], - SIG_EP_OUTPUT: [0, 3, 4, 5, 18, 25, 65535], - SIG_EP_PROFILE: 260, - }, - 2: { - SIG_EP_TYPE: 24322, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [3, 18], - SIG_EP_OUTPUT: [3, 4, 5, 18], - SIG_EP_PROFILE: 260, - }, - 3: { - SIG_EP_TYPE: 24323, - DEV_SIG_EP_ID: 3, - SIG_EP_INPUT: [3, 18], - SIG_EP_OUTPUT: [3, 4, 5, 12, 18], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_86sw1_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_86sw1_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_sensor_86sw1_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 53, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sensor_cube.aqgl01", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 28417, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 25], - SIG_EP_OUTPUT: [0, 3, 4, 5, 18, 25], - SIG_EP_PROFILE: 260, - }, - 2: { - SIG_EP_TYPE: 28418, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [3, 18], - SIG_EP_OUTPUT: [3, 4, 5, 18], - SIG_EP_PROFILE: 260, - }, - 3: { - SIG_EP_TYPE: 28419, - DEV_SIG_EP_ID: 3, - SIG_EP_INPUT: [3, 12], - SIG_EP_OUTPUT: [3, 4, 5, 12], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_cube_aqgl01_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_cube_aqgl01_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_sensor_cube_aqgl01_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 54, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sensor_ht", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 24322, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 25, 1026, 1029, 65535], - SIG_EP_OUTPUT: [0, 3, 4, 5, 18, 25, 65535], - SIG_EP_PROFILE: 260, - }, - 2: { - SIG_EP_TYPE: 24322, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [3], - SIG_EP_OUTPUT: [3, 4, 5, 18], - SIG_EP_PROFILE: 260, - }, - 3: { - SIG_EP_TYPE: 24323, - DEV_SIG_EP_ID: 3, - SIG_EP_INPUT: [3], - SIG_EP_OUTPUT: [3, 4, 5, 12], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_ht_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CLUSTER_HANDLERS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_temperature", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_lqi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1029"): { - DEV_SIG_CLUSTER_HANDLERS: ["humidity"], - DEV_SIG_ENT_MAP_CLASS: "Humidity", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_ht_humidity", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_sensor_ht_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 55, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sensor_magnet", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 2128, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 25, 65535], - SIG_EP_OUTPUT: [0, 3, 4, 5, 6, 8, 25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_magnet_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_lqi", - }, - ("binary_sensor", "00:11:22:33:44:55:66:77-1-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_magnet_opening", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_sensor_magnet_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 56, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sensor_magnet.aq2", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 24321, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 65535], - SIG_EP_OUTPUT: [0, 4, 6, 65535], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0006"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_magnet_aq2_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_magnet_aq2_lqi", - }, - ("binary_sensor", "00:11:22:33:44:55:66:77-1-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Opening", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_magnet_aq2_opening", - }, - }, - }, - { - DEV_SIG_DEV_NO: 57, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sensor_motion.aq2", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 263, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 1024, 1030, 1280, 65535], - SIG_EP_OUTPUT: [0, 25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1030"): { - DEV_SIG_CLUSTER_HANDLERS: ["occupancy"], - DEV_SIG_ENT_MAP_CLASS: "Occupancy", - DEV_SIG_ENT_MAP_ID: ( - "binary_sensor.lumi_lumi_sensor_motion_aq2_occupancy" - ), - }, - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_motion_aq2_motion", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_motion_aq2_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1024"): { - DEV_SIG_CLUSTER_HANDLERS: ["illuminance"], - DEV_SIG_ENT_MAP_CLASS: "Illuminance", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_illuminance", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2"): { - DEV_SIG_CLUSTER_HANDLERS: ["device_temperature"], - DEV_SIG_ENT_MAP_CLASS: "DeviceTemperature", - DEV_SIG_ENT_MAP_ID: ( - "sensor.lumi_lumi_sensor_motion_aq2_device_temperature" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_motion_aq2_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_sensor_motion_aq2_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 58, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sensor_smoke", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 1026, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 12, 18, 1280], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_smoke_smoke", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_smoke_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2"): { - DEV_SIG_CLUSTER_HANDLERS: ["device_temperature"], - DEV_SIG_ENT_MAP_CLASS: "DeviceTemperature", - DEV_SIG_ENT_MAP_ID: ( - "sensor.lumi_lumi_sensor_smoke_device_temperature" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_smoke_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_sensor_smoke_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 59, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sensor_switch", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 6, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3], - SIG_EP_OUTPUT: [0, 4, 5, 6, 8, 25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_switch_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_sensor_switch_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 60, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sensor_switch.aq2", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 6, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 65535], - SIG_EP_OUTPUT: [0, 4, 6, 65535], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0006"], - DEV_SIG_ENT_MAP: { - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq2_lqi", - }, - }, - }, - { - DEV_SIG_DEV_NO: 61, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sensor_switch.aq3", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 6, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 18], - SIG_EP_OUTPUT: [0, 6], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0006"], - DEV_SIG_ENT_MAP: { - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_switch_aq3_lqi", - }, - }, - }, - { - DEV_SIG_DEV_NO: 62, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.sensor_wleak.aq1", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 1026, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 2, 3, 1280], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_sensor_wleak_aq1_ias_zone", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2"): { - DEV_SIG_CLUSTER_HANDLERS: ["device_temperature"], - DEV_SIG_ENT_MAP_CLASS: "DeviceTemperature", - DEV_SIG_ENT_MAP_ID: ( - "sensor.lumi_lumi_sensor_wleak_aq1_device_temperature" - ), - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_sensor_wleak_aq1_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_sensor_wleak_aq1_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_sensor_wleak_aq1_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 63, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.vibration.aq1", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - SIG_ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.DOOR_LOCK, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - Ota.cluster_id, - DoorLock.cluster_id, - ], - OUTPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - Groups.cluster_id, - Scenes.cluster_id, - Ota.cluster_id, - DoorLock.cluster_id, - ], - }, - 2: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: 0x5F02, - INPUT_CLUSTERS: [Identify.cluster_id, MultistateInput.cluster_id], - OUTPUT_CLUSTERS: [ - Identify.cluster_id, - Groups.cluster_id, - Scenes.cluster_id, - MultistateInput.cluster_id, - ], - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0005", "1:0x0019", "2:0x0005"], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.lumi_lumi_vibration_aq1_vibration", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_vibration_aq1_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_lqi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2"): { - DEV_SIG_CLUSTER_HANDLERS: ["device_temperature"], - DEV_SIG_ENT_MAP_CLASS: "DeviceTemperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_vibration_aq1_device_temperature", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.lumi_lumi_vibration_aq1_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 64, - SIG_MANUFACTURER: "LUMI", - SIG_MODEL: "lumi.weather", - SIG_NODE_DESC: b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 24321, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 1026, 1027, 1029, 65535], - SIG_EP_OUTPUT: [0, 4, 65535], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: [], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.lumi_lumi_weather_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1027"): { - DEV_SIG_CLUSTER_HANDLERS: ["pressure"], - DEV_SIG_ENT_MAP_CLASS: "Pressure", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_pressure", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CLUSTER_HANDLERS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_temperature", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_lqi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1029"): { - DEV_SIG_CLUSTER_HANDLERS: ["humidity"], - DEV_SIG_ENT_MAP_CLASS: "Humidity", - DEV_SIG_ENT_MAP_ID: "sensor.lumi_lumi_weather_humidity", - }, - }, - }, - { - DEV_SIG_DEV_NO: 65, - SIG_MANUFACTURER: "NYCE", - SIG_MODEL: "3010", - SIG_NODE_DESC: b"\x02@\x80\xb9\x10RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 1026, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 32, 1280], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: [], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.nyce_3010_ias_zone", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.nyce_3010_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3010_lqi", - }, - }, - }, - { - DEV_SIG_DEV_NO: 66, - SIG_MANUFACTURER: "NYCE", - SIG_MODEL: "3014", - SIG_NODE_DESC: b"\x02@\x80\xb9\x10RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 1026, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 32, 1280], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: [], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.nyce_3014_ias_zone", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.nyce_3014_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.nyce_3014_lqi", - }, - }, - }, - { - DEV_SIG_DEV_NO: 67, - SIG_MANUFACTURER: None, - SIG_MODEL: None, - SIG_NODE_DESC: b"\x10@\x0f5\x11Y=\x00@\x00=\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 5, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [10, 25], - SIG_EP_OUTPUT: [1280], - SIG_EP_PROFILE: 260, - }, - 242: { - SIG_EP_TYPE: 100, - DEV_SIG_EP_ID: 242, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [33], - SIG_EP_PROFILE: 41440, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: [], - DEV_SIG_ENT_MAP: {}, - }, - { - DEV_SIG_DEV_NO: 68, - SIG_MANUFACTURER: None, - SIG_MODEL: None, - SIG_NODE_DESC: b"\x00@\x8f\xcd\xabR\x80\x00\x00\x00\x80\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 48879, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [], - SIG_EP_OUTPUT: [1280], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: [], - DEV_SIG_ENT_MAP: {}, - }, - { - DEV_SIG_DEV_NO: 69, - SIG_MANUFACTURER: "OSRAM", - SIG_MODEL: "LIGHTIFY A19 RGBW", - SIG_NODE_DESC: b"\x01@\x8e\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", - SIG_ENDPOINTS: { - 3: { - SIG_EP_TYPE: 258, - DEV_SIG_EP_ID: 3, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 768, 64527], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["3:0x0019"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off", "light_color", "level"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.osram_lightify_a19_rgbw_light", - }, - ("button", "00:11:22:33:44:55:66:77-3-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.osram_lightify_a19_rgbw_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_a19_rgbw_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-3-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_a19_rgbw_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-3-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.osram_lightify_a19_rgbw_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 70, - SIG_MANUFACTURER: "OSRAM", - SIG_MODEL: "LIGHTIFY Dimming Switch", - SIG_NODE_DESC: b"\x02@\x80\x0c\x11RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 1, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 32, 2821], - SIG_EP_OUTPUT: [3, 6, 8, 25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0006", "1:0x0008", "1:0x0019"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.osram_lightify_dimming_switch_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_dimming_switch_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.osram_lightify_dimming_switch_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 71, - SIG_MANUFACTURER: "OSRAM", - SIG_MODEL: "LIGHTIFY Flex RGBW", - SIG_NODE_DESC: b"\x19@\x8e\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", - SIG_ENDPOINTS: { - 3: { - SIG_EP_TYPE: 258, - DEV_SIG_EP_ID: 3, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 768, 64527], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["3:0x0019"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off", "light_color", "level"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.osram_lightify_flex_rgbw_light", - }, - ("button", "00:11:22:33:44:55:66:77-3-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.osram_lightify_flex_rgbw_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_flex_rgbw_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-3-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_flex_rgbw_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-3-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.osram_lightify_flex_rgbw_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 72, - SIG_MANUFACTURER: "OSRAM", - SIG_MODEL: "LIGHTIFY RT Tunable White", - SIG_NODE_DESC: b"\x01@\x8e\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", - SIG_ENDPOINTS: { - 3: { - SIG_EP_TYPE: 258, - DEV_SIG_EP_ID: 3, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 768, 2820, 64527], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["3:0x0019"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off", "light_color", "level"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.osram_lightify_rt_tunable_white_light", - }, - ("button", "00:11:22:33:44:55:66:77-3-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.osram_lightify_rt_tunable_white_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-3-2820"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "PolledElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: ("sensor.osram_lightify_rt_tunable_white_power"), - }, - ("sensor", "00:11:22:33:44:55:66:77-3-2820-apparent_power"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: ( - "sensor.osram_lightify_rt_tunable_white_apparent_power" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_current"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: ("sensor.osram_lightify_rt_tunable_white_current"), - }, - ("sensor", "00:11:22:33:44:55:66:77-3-2820-rms_voltage"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: ("sensor.osram_lightify_rt_tunable_white_voltage"), - }, - ("sensor", "00:11:22:33:44:55:66:77-3-2820-ac_frequency"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: ( - "sensor.osram_lightify_rt_tunable_white_ac_frequency" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-3-2820-power_factor"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: ( - "sensor.osram_lightify_rt_tunable_white_power_factor" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-3-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_lightify_rt_tunable_white_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-3-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.osram_lightify_rt_tunable_white_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 73, - SIG_MANUFACTURER: "OSRAM", - SIG_MODEL: "Plug 01", - SIG_NODE_DESC: b"\x01@\x8e\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", - SIG_ENDPOINTS: { - 3: { - SIG_EP_TYPE: 16, - DEV_SIG_EP_ID: 3, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 2820, 4096, 64527], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 49246, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["3:0x0019"], - DEV_SIG_ENT_MAP: { - ("switch", "00:11:22:33:44:55:66:77-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.osram_plug_01_switch", - }, - ("button", "00:11:22:33:44:55:66:77-3-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.osram_plug_01_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-3-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-3-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_plug_01_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-3-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.osram_plug_01_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 74, - SIG_MANUFACTURER: "OSRAM", - SIG_MODEL: "Switch 4x-LIGHTIFY", - SIG_NODE_DESC: b"\x02@\x80\x0c\x11RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 2064, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 32, 4096, 64768], - SIG_EP_OUTPUT: [3, 4, 5, 6, 8, 25, 768, 4096], - SIG_EP_PROFILE: 260, - }, - 2: { - SIG_EP_TYPE: 2064, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [0, 4096, 64768], - SIG_EP_OUTPUT: [3, 4, 5, 6, 8, 768, 4096], - SIG_EP_PROFILE: 260, - }, - 3: { - SIG_EP_TYPE: 2064, - DEV_SIG_EP_ID: 3, - SIG_EP_INPUT: [0, 4096, 64768], - SIG_EP_OUTPUT: [3, 4, 5, 6, 8, 768, 4096], - SIG_EP_PROFILE: 260, - }, - 4: { - SIG_EP_TYPE: 2064, - DEV_SIG_EP_ID: 4, - SIG_EP_INPUT: [0, 4096, 64768], - SIG_EP_OUTPUT: [3, 4, 5, 6, 8, 768, 4096], - SIG_EP_PROFILE: 260, - }, - 5: { - SIG_EP_TYPE: 2064, - DEV_SIG_EP_ID: 5, - SIG_EP_INPUT: [0, 4096, 64768], - SIG_EP_OUTPUT: [3, 4, 5, 6, 8, 768, 4096], - SIG_EP_PROFILE: 260, - }, - 6: { - SIG_EP_TYPE: 2064, - DEV_SIG_EP_ID: 6, - SIG_EP_INPUT: [0, 4096, 64768], - SIG_EP_OUTPUT: [3, 4, 5, 6, 8, 768, 4096], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: [ - "1:0x0005", - "1:0x0006", - "1:0x0008", - "1:0x0019", - "1:0x0300", - "2:0x0005", - "2:0x0006", - "2:0x0008", - "2:0x0300", - "3:0x0005", - "3:0x0006", - "3:0x0008", - "3:0x0300", - "4:0x0005", - "4:0x0006", - "4:0x0008", - "4:0x0300", - "5:0x0005", - "5:0x0006", - "5:0x0008", - "5:0x0300", - "6:0x0005", - "6:0x0006", - "6:0x0008", - "6:0x0300", - ], - DEV_SIG_ENT_MAP: { - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.osram_switch_4x_lightify_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.osram_switch_4x_lightify_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 75, - SIG_MANUFACTURER: "Philips", - SIG_MODEL: "RWL020", - SIG_NODE_DESC: b"\x02@\x80\x0b\x10G-\x00\x00\x00-\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 2096, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0], - SIG_EP_OUTPUT: [0, 3, 4, 5, 6, 8], - SIG_EP_PROFILE: 49246, - }, - 2: { - SIG_EP_TYPE: 12, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [0, 1, 3, 15, 64512], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0005", "1:0x0006", "1:0x0008", "2:0x0019"], - DEV_SIG_ENT_MAP: { - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_lqi", - }, - ("binary_sensor", "00:11:22:33:44:55:66:77-2-15"): { - DEV_SIG_CLUSTER_HANDLERS: ["binary_input"], - DEV_SIG_ENT_MAP_CLASS: "BinaryInput", - DEV_SIG_ENT_MAP_ID: "binary_sensor.philips_rwl020_binary_input", - }, - ("button", "00:11:22:33:44:55:66:77-2-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.philips_rwl020_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-2-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.philips_rwl020_battery", - }, - ("update", "00:11:22:33:44:55:66:77-2-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.philips_rwl020_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 76, - SIG_MANUFACTURER: "Samjin", - SIG_MODEL: "button", - SIG_NODE_DESC: b"\x02@\x80A\x12RR\x00\x00,R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 1026, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 32, 1026, 1280, 2821], - SIG_EP_OUTPUT: [3, 25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_button_ias_zone", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.samjin_button_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CLUSTER_HANDLERS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_temperature", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_button_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.samjin_button_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 77, - SIG_MANUFACTURER: "Samjin", - SIG_MODEL: "multi", - SIG_NODE_DESC: b"\x02@\x80A\x12RR\x00\x00,R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 1026, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 32, 1026, 1280, 64514], - SIG_EP_OUTPUT: [3, 25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_multi_ias_zone", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.samjin_multi_identify", - }, - ("binary_sensor", "00:11:22:33:44:55:66:77-1-64514"): { - DEV_SIG_CLUSTER_HANDLERS: ["accelerometer"], - DEV_SIG_ENT_MAP_CLASS: "Accelerometer", - DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_multi_accelerometer", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CLUSTER_HANDLERS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_temperature", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_multi_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.samjin_multi_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 78, - SIG_MANUFACTURER: "Samjin", - SIG_MODEL: "water", - SIG_NODE_DESC: b"\x02@\x80A\x12RR\x00\x00,R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 1026, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 32, 1026, 1280], - SIG_EP_OUTPUT: [3, 25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.samjin_water_ias_zone", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.samjin_water_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CLUSTER_HANDLERS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_temperature", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.samjin_water_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.samjin_water_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 79, - SIG_MANUFACTURER: "Securifi Ltd.", - SIG_MODEL: None, - SIG_NODE_DESC: b"\x01@\x8e\x02\x10RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 0, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 4, 5, 6, 2820, 2821], - SIG_EP_OUTPUT: [0, 1, 3, 4, 5, 6, 25, 2820, 2821], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0005", "1:0x0006", "1:0x0019"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.securifi_ltd_unk_model_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "PolledElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_power", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_apparent_power", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_current", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_voltage", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_ac_frequency", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_power_factor", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.securifi_ltd_unk_model_lqi", - }, - ("switch", "00:11:22:33:44:55:66:77-1-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.securifi_ltd_unk_model_switch", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.securifi_ltd_unk_model_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 80, - SIG_MANUFACTURER: "Sercomm Corp.", - SIG_MODEL: "SZ-DWS04N_SF", - SIG_NODE_DESC: b"\x02@\x801\x11R\xff\x00\x00\x00\xff\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 1026, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 32, 1026, 1280, 2821], - SIG_EP_OUTPUT: [3, 25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.sercomm_corp_sz_dws04n_sf_ias_zone", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_dws04n_sf_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CLUSTER_HANDLERS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_temperature", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_dws04n_sf_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.sercomm_corp_sz_dws04n_sf_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 81, - SIG_MANUFACTURER: "Sercomm Corp.", - SIG_MODEL: "SZ-ESW01", - SIG_NODE_DESC: b"\x01@\x8e1\x11RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 256, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 4, 5, 6, 1794, 2820, 2821], - SIG_EP_OUTPUT: [3, 10, 25, 2821], - SIG_EP_PROFILE: 260, - }, - 2: { - SIG_EP_TYPE: 259, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [0, 1, 3], - SIG_EP_OUTPUT: [3, 6], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019", "2:0x0006"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.sercomm_corp_sz_esw01_light", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_esw01_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "PolledElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_power", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_apparent_power", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_current", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_voltage", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_ac_frequency", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_power_factor", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_instantaneous_demand", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_summation_delivered", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_esw01_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.sercomm_corp_sz_esw01_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 82, - SIG_MANUFACTURER: "Sercomm Corp.", - SIG_MODEL: "SZ-PIR04", - SIG_NODE_DESC: b"\x02@\x801\x11RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 1026, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 32, 1024, 1026, 1280, 2821], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.sercomm_corp_sz_pir04_ias_zone", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sercomm_corp_sz_pir04_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1024"): { - DEV_SIG_CLUSTER_HANDLERS: ["illuminance"], - DEV_SIG_ENT_MAP_CLASS: "Illuminance", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_illuminance", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CLUSTER_HANDLERS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_temperature", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sercomm_corp_sz_pir04_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.sercomm_corp_sz_pir04_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 83, - SIG_MANUFACTURER: "Sinope Technologies", - SIG_MODEL: "RM3250ZB", - SIG_NODE_DESC: b"\x11@\x8e\x9c\x11G+\x00\x00*+\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 2, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 2820, 2821, 65281], - SIG_EP_OUTPUT: [3, 4, 25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_rm3250zb_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "PolledElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_power", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: ( - "sensor.sinope_technologies_rm3250zb_apparent_power" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_current", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_voltage", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_ac_frequency", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_power_factor", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_rm3250zb_lqi", - }, - ("switch", "00:11:22:33:44:55:66:77-1-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.sinope_technologies_rm3250zb_switch", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.sinope_technologies_rm3250zb_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 84, - SIG_MANUFACTURER: "Sinope Technologies", - SIG_MODEL: "TH1123ZB", - SIG_NODE_DESC: b"\x12@\x8c\x9c\x11G+\x00\x00\x00+\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 769, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 513, 516, 1026, 2820, 2821, 65281], - SIG_EP_OUTPUT: [25, 65281], - SIG_EP_PROFILE: 260, - }, - 196: { - SIG_EP_TYPE: 769, - DEV_SIG_EP_ID: 196, - SIG_EP_INPUT: [1], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 49757, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_th1123zb_identify", - }, - ("climate", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: [ - "thermostat", - "sinope_manufacturer_specific", - ], - DEV_SIG_ENT_MAP_CLASS: "SinopeTechnologiesThermostat", - DEV_SIG_ENT_MAP_ID: "climate.sinope_technologies_th1123zb_thermostat", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "PolledElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_power", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: ( - "sensor.sinope_technologies_th1123zb_apparent_power" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_current", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_voltage", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_ac_frequency", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_power_factor", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CLUSTER_HANDLERS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_temperature", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_lqi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { - DEV_SIG_CLUSTER_HANDLERS: ["thermostat"], - DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_hvac_action", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-513-pi_heating_demand"): { - DEV_SIG_CLUSTER_HANDLERS: ["thermostat"], - DEV_SIG_ENT_MAP_CLASS: "PiHeatingDemand", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_pi_heating_demand", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-513-setpoint_change_source"): { - DEV_SIG_CLUSTER_HANDLERS: ["thermostat"], - DEV_SIG_ENT_MAP_CLASS: "SetpointChangeSource", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1123zb_setpoint_change_source", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.sinope_technologies_th1123zb_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 85, - SIG_MANUFACTURER: "Sinope Technologies", - SIG_MODEL: "TH1124ZB", - SIG_NODE_DESC: b"\x11@\x8e\x9c\x11G+\x00\x00\x00+\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 769, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 513, 516, 1026, 2820, 2821, 65281], - SIG_EP_OUTPUT: [25, 65281], - SIG_EP_PROFILE: 260, - }, - 196: { - SIG_EP_TYPE: 769, - DEV_SIG_EP_ID: 196, - SIG_EP_INPUT: [1], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 49757, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sinope_technologies_th1124zb_identify", - }, - ("climate", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: [ - "thermostat", - "sinope_manufacturer_specific", - ], - DEV_SIG_ENT_MAP_CLASS: "SinopeTechnologiesThermostat", - DEV_SIG_ENT_MAP_ID: "climate.sinope_technologies_th1124zb_thermostat", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "PolledElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_power", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: ( - "sensor.sinope_technologies_th1124zb_apparent_power" - ), - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_current", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_voltage", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_ac_frequency", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_power_factor", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CLUSTER_HANDLERS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_temperature", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_lqi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { - DEV_SIG_CLUSTER_HANDLERS: ["thermostat"], - DEV_SIG_ENT_MAP_CLASS: "SinopeHVACAction", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_hvac_action", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-513-pi_heating_demand"): { - DEV_SIG_CLUSTER_HANDLERS: ["thermostat"], - DEV_SIG_ENT_MAP_CLASS: "PiHeatingDemand", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_pi_heating_demand", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-513-setpoint_change_source"): { - DEV_SIG_CLUSTER_HANDLERS: ["thermostat"], - DEV_SIG_ENT_MAP_CLASS: "SetpointChangeSource", - DEV_SIG_ENT_MAP_ID: "sensor.sinope_technologies_th1124zb_setpoint_change_source", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.sinope_technologies_th1124zb_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 86, - SIG_MANUFACTURER: "SmartThings", - SIG_MODEL: "outletv4", - SIG_NODE_DESC: b"\x01@\x8e\n\x11RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 2, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 9, 15, 2820], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-15"): { - DEV_SIG_CLUSTER_HANDLERS: ["binary_input"], - DEV_SIG_ENT_MAP_CLASS: "BinaryInput", - DEV_SIG_ENT_MAP_ID: "binary_sensor.smartthings_outletv4_binary_input", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.smartthings_outletv4_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurement", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_power", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-apparent_power"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementApparentPower", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_apparent_power", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_current"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSCurrent", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_current", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-rms_voltage"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementRMSVoltage", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_voltage", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-ac_frequency"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementFrequency", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_ac_frequency", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-2820-power_factor"): { - DEV_SIG_CLUSTER_HANDLERS: ["electrical_measurement"], - DEV_SIG_ENT_MAP_CLASS: "ElectricalMeasurementPowerFactor", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_power_factor", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_outletv4_lqi", - }, - ("switch", "00:11:22:33:44:55:66:77-1-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.smartthings_outletv4_switch", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.smartthings_outletv4_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 87, - SIG_MANUFACTURER: "SmartThings", - SIG_MODEL: "tagv4", - SIG_NODE_DESC: b"\x02@\x80\n\x11RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 32768, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 15, 32], - SIG_EP_OUTPUT: [3, 25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("device_tracker", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "ZHADeviceScannerEntity", - DEV_SIG_ENT_MAP_ID: "device_tracker.smartthings_tagv4_device_scanner", - }, - ("binary_sensor", "00:11:22:33:44:55:66:77-1-15"): { - DEV_SIG_CLUSTER_HANDLERS: ["binary_input"], - DEV_SIG_ENT_MAP_CLASS: "BinaryInput", - DEV_SIG_ENT_MAP_ID: "binary_sensor.smartthings_tagv4_binary_input", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.smartthings_tagv4_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_tagv4_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.smartthings_tagv4_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.smartthings_tagv4_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 88, - SIG_MANUFACTURER: "Third Reality, Inc", - SIG_MODEL: "3RSS007Z", - SIG_NODE_DESC: b"\x02@\x803\x12\x7fd\x00\x00,d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 2, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 25], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: [], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.third_reality_inc_3rss007z_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss007z_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss007z_lqi", - }, - ("switch", "00:11:22:33:44:55:66:77-1-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.third_reality_inc_3rss007z_switch", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.third_reality_inc_3rss007z_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 89, - SIG_MANUFACTURER: "Third Reality, Inc", - SIG_MODEL: "3RSS008Z", - SIG_NODE_DESC: b"\x02@\x803\x12\x7fd\x00\x00,d\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 2, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 4, 5, 6, 25], - SIG_EP_OUTPUT: [1], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: [], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.third_reality_inc_3rss008z_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.third_reality_inc_3rss008z_lqi", - }, - ("switch", "00:11:22:33:44:55:66:77-1-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.third_reality_inc_3rss008z_switch", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.third_reality_inc_3rss008z_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 90, - SIG_MANUFACTURER: "Visonic", - SIG_MODEL: "MCT-340 E", - SIG_NODE_DESC: b"\x02@\x80\x11\x10RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 1026, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 32, 1026, 1280, 2821], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.visonic_mct_340_e_ias_zone", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.visonic_mct_340_e_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CLUSTER_HANDLERS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_temperature", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.visonic_mct_340_e_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.visonic_mct_340_e_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 91, - SIG_MANUFACTURER: "Zen Within", - SIG_MODEL: "Zen-01", - SIG_NODE_DESC: b"\x02@\x80X\x11R\x80\x00\x00\x00\x80\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 769, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 4, 5, 32, 513, 514, 516, 2821], - SIG_EP_OUTPUT: [10, 25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.zen_within_zen_01_identify", - }, - ("climate", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["thermostat", "fan"], - DEV_SIG_ENT_MAP_CLASS: "ZenWithinThermostat", - DEV_SIG_ENT_MAP_ID: "climate.zen_within_zen_01_thermostat", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_lqi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-513-hvac_action"): { - DEV_SIG_CLUSTER_HANDLERS: ["thermostat"], - DEV_SIG_ENT_MAP_CLASS: "ThermostatHVACAction", - DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_hvac_action", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-513-pi_heating_demand"): { - DEV_SIG_CLUSTER_HANDLERS: ["thermostat"], - DEV_SIG_ENT_MAP_CLASS: "PiHeatingDemand", - DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_pi_heating_demand", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-513-setpoint_change_source"): { - DEV_SIG_CLUSTER_HANDLERS: ["thermostat"], - DEV_SIG_ENT_MAP_CLASS: "SetpointChangeSource", - DEV_SIG_ENT_MAP_ID: "sensor.zen_within_zen_01_setpoint_change_source", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.zen_within_zen_01_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 92, - SIG_MANUFACTURER: "_TYZB01_ns1ndbww", - SIG_MODEL: "TS0004", - SIG_NODE_DESC: b"\x01@\x8e\x02\x10R\x00\x02\x00,\x00\x02\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 256, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 4, 5, 6, 10], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - 2: { - SIG_EP_TYPE: 256, - DEV_SIG_EP_ID: 2, - SIG_EP_INPUT: [4, 5, 6], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 260, - }, - 3: { - SIG_EP_TYPE: 256, - DEV_SIG_EP_ID: 3, - SIG_EP_INPUT: [4, 5, 6], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 260, - }, - 4: { - SIG_EP_TYPE: 256, - DEV_SIG_EP_ID: 4, - SIG_EP_INPUT: [4, 5, 6], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_light", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.tyzb01_ns1ndbww_ts0004_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.tyzb01_ns1ndbww_ts0004_lqi", - }, - ("light", "00:11:22:33:44:55:66:77-2"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_light_2", - }, - ("light", "00:11:22:33:44:55:66:77-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_light_3", - }, - ("light", "00:11:22:33:44:55:66:77-4"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Light", - DEV_SIG_ENT_MAP_ID: "light.tyzb01_ns1ndbww_ts0004_light_4", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.tyzb01_ns1ndbww_ts0004_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 93, - SIG_MANUFACTURER: "netvox", - SIG_MODEL: "Z308E3ED", - SIG_NODE_DESC: b"\x02@\x80\x9f\x10RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 1026, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 3, 21, 32, 1280, 2821], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: [], - DEV_SIG_ENT_MAP: { - ("binary_sensor", "00:11:22:33:44:55:66:77-1-1280"): { - DEV_SIG_CLUSTER_HANDLERS: ["ias_zone"], - DEV_SIG_ENT_MAP_CLASS: "IASZone", - DEV_SIG_ENT_MAP_ID: "binary_sensor.netvox_z308e3ed_ias_zone", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.netvox_z308e3ed_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.netvox_z308e3ed_lqi", - }, - }, - }, - { - DEV_SIG_DEV_NO: 94, - SIG_MANUFACTURER: "sengled", - SIG_MODEL: "E11-G13", - SIG_NODE_DESC: b"\x02@\x8c`\x11RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 257, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 1794, 2821], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off", "level"], - DEV_SIG_ENT_MAP_CLASS: "MinTransitionLight", - DEV_SIG_ENT_MAP_ID: "light.sengled_e11_g13_light", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sengled_e11_g13_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_instantaneous_demand", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_summation_delivered", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e11_g13_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.sengled_e11_g13_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 95, - SIG_MANUFACTURER: "sengled", - SIG_MODEL: "E12-N14", - SIG_NODE_DESC: b"\x02@\x8c`\x11RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 257, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 1794, 2821], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off", "level"], - DEV_SIG_ENT_MAP_CLASS: "MinTransitionLight", - DEV_SIG_ENT_MAP_ID: "light.sengled_e12_n14_light", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sengled_e12_n14_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_instantaneous_demand", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_summation_delivered", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_e12_n14_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.sengled_e12_n14_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 96, - SIG_MANUFACTURER: "sengled", - SIG_MODEL: "Z01-A19NAE26", - SIG_NODE_DESC: b"\x02@\x8c`\x11RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 257, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 768, 1794, 2821], - SIG_EP_OUTPUT: [25], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["1:0x0019"], - DEV_SIG_ENT_MAP: { - ("light", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off", "level", "light_color"], - DEV_SIG_ENT_MAP_CLASS: "MinTransitionLight", - DEV_SIG_ENT_MAP_ID: "light.sengled_z01_a19nae26_light", - }, - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.sengled_z01_a19nae26_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergyMetering", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_instantaneous_demand", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1794-summation_delivered"): { - DEV_SIG_CLUSTER_HANDLERS: ["smartenergy_metering"], - DEV_SIG_ENT_MAP_CLASS: "SmartEnergySummation", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_summation_delivered", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.sengled_z01_a19nae26_lqi", - }, - ("update", "00:11:22:33:44:55:66:77-1-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.sengled_z01_a19nae26_firmware", - }, - }, - }, - { - DEV_SIG_DEV_NO: 97, - SIG_MANUFACTURER: "unk_manufacturer", - SIG_MODEL: "unk_model", - SIG_NODE_DESC: b"\x01@\x8e\x10\x11RR\x00\x00\x00R\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 512, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 3, 4, 5, 6, 8, 10, 21, 256, 64544, 64545], - SIG_EP_OUTPUT: [3, 64544], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: [], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.unk_manufacturer_unk_model_identify", - }, - ("cover", "00:11:22:33:44:55:66:77-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["level", "on_off", "shade"], - DEV_SIG_ENT_MAP_CLASS: "Shade", - DEV_SIG_ENT_MAP_ID: "cover.unk_manufacturer_unk_model_shade", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.unk_manufacturer_unk_model_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.unk_manufacturer_unk_model_lqi", - }, - }, - }, - { - DEV_SIG_DEV_NO: 98, - SIG_MANUFACTURER: "Digi", - SIG_MODEL: "XBee3", - SIG_NODE_DESC: b"\x01@\x8e\x1e\x10R\xff\x00\x00,\xff\x00\x00", - SIG_ENDPOINTS: { - 208: { - SIG_EP_TYPE: 1, - DEV_SIG_EP_ID: 208, - SIG_EP_INPUT: [6, 12], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 49413, - }, - 209: { - SIG_EP_TYPE: 1, - DEV_SIG_EP_ID: 209, - SIG_EP_INPUT: [6, 12], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 49413, - }, - 210: { - SIG_EP_TYPE: 1, - DEV_SIG_EP_ID: 210, - SIG_EP_INPUT: [6, 12], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 49413, - }, - 211: { - SIG_EP_TYPE: 1, - DEV_SIG_EP_ID: 211, - SIG_EP_INPUT: [6, 12], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 49413, - }, - 212: { - SIG_EP_TYPE: 1, - DEV_SIG_EP_ID: 212, - SIG_EP_INPUT: [6], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 49413, - }, - 213: { - SIG_EP_TYPE: 1, - DEV_SIG_EP_ID: 213, - SIG_EP_INPUT: [6], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 49413, - }, - 214: { - SIG_EP_TYPE: 1, - DEV_SIG_EP_ID: 214, - SIG_EP_INPUT: [6], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 49413, - }, - 215: { - SIG_EP_TYPE: 1, - DEV_SIG_EP_ID: 215, - SIG_EP_INPUT: [6, 12], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 49413, - }, - 216: { - SIG_EP_TYPE: 1, - DEV_SIG_EP_ID: 216, - SIG_EP_INPUT: [6], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 49413, - }, - 217: { - SIG_EP_TYPE: 1, - DEV_SIG_EP_ID: 217, - SIG_EP_INPUT: [6], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 49413, - }, - 218: { - SIG_EP_TYPE: 1, - DEV_SIG_EP_ID: 218, - SIG_EP_INPUT: [6, 13], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 49413, - }, - 219: { - SIG_EP_TYPE: 1, - DEV_SIG_EP_ID: 219, - SIG_EP_INPUT: [6, 13], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 49413, - }, - 220: { - SIG_EP_TYPE: 1, - DEV_SIG_EP_ID: 220, - SIG_EP_INPUT: [6], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 49413, - }, - 221: { - SIG_EP_TYPE: 1, - DEV_SIG_EP_ID: 221, - SIG_EP_INPUT: [6], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 49413, - }, - 222: { - SIG_EP_TYPE: 1, - DEV_SIG_EP_ID: 222, - SIG_EP_INPUT: [6], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 49413, - }, - 232: { - SIG_EP_TYPE: 1, - DEV_SIG_EP_ID: 232, - SIG_EP_INPUT: [17, 146], - SIG_EP_OUTPUT: [8, 17], - SIG_EP_PROFILE: 49413, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: ["232:0x0008"], - DEV_SIG_ENT_MAP: { - ("sensor", "00:11:22:33:44:55:66:77-208-12"): { - DEV_SIG_CLUSTER_HANDLERS: ["analog_input"], - DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_analog_input", - }, - ("switch", "00:11:22:33:44:55:66:77-208-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch", - }, - ("sensor", "00:11:22:33:44:55:66:77-209-12"): { - DEV_SIG_CLUSTER_HANDLERS: ["analog_input"], - DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_analog_input_2", - }, - ("switch", "00:11:22:33:44:55:66:77-209-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_2", - }, - ("sensor", "00:11:22:33:44:55:66:77-210-12"): { - DEV_SIG_CLUSTER_HANDLERS: ["analog_input"], - DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_analog_input_3", - }, - ("switch", "00:11:22:33:44:55:66:77-210-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_3", - }, - ("sensor", "00:11:22:33:44:55:66:77-211-12"): { - DEV_SIG_CLUSTER_HANDLERS: ["analog_input"], - DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_analog_input_4", - }, - ("switch", "00:11:22:33:44:55:66:77-211-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_4", - }, - ("switch", "00:11:22:33:44:55:66:77-212-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_5", - }, - ("switch", "00:11:22:33:44:55:66:77-213-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_6", - }, - ("switch", "00:11:22:33:44:55:66:77-214-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_7", - }, - ("sensor", "00:11:22:33:44:55:66:77-215-12"): { - DEV_SIG_CLUSTER_HANDLERS: ["analog_input"], - DEV_SIG_ENT_MAP_CLASS: "AnalogInput", - DEV_SIG_ENT_MAP_ID: "sensor.digi_xbee3_analog_input_5", - }, - ("switch", "00:11:22:33:44:55:66:77-215-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_8", - }, - ("switch", "00:11:22:33:44:55:66:77-216-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_9", - }, - ("switch", "00:11:22:33:44:55:66:77-217-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_10", - }, - ("number", "00:11:22:33:44:55:66:77-218-13"): { - DEV_SIG_CLUSTER_HANDLERS: ["analog_output"], - DEV_SIG_ENT_MAP_CLASS: "ZhaNumber", - DEV_SIG_ENT_MAP_ID: "number.digi_xbee3_number", - }, - ("switch", "00:11:22:33:44:55:66:77-218-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_11", - }, - ("switch", "00:11:22:33:44:55:66:77-219-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_12", - }, - ("number", "00:11:22:33:44:55:66:77-219-13"): { - DEV_SIG_CLUSTER_HANDLERS: ["analog_output"], - DEV_SIG_ENT_MAP_CLASS: "ZhaNumber", - DEV_SIG_ENT_MAP_ID: "number.digi_xbee3_number_2", - }, - ("switch", "00:11:22:33:44:55:66:77-220-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_13", - }, - ("switch", "00:11:22:33:44:55:66:77-221-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_14", - }, - ("switch", "00:11:22:33:44:55:66:77-222-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Switch", - DEV_SIG_ENT_MAP_ID: "switch.digi_xbee3_switch_15", - }, - }, - }, - { - DEV_SIG_DEV_NO: 99, - SIG_MANUFACTURER: "efektalab.ru", - SIG_MODEL: "EFEKTA_PWS", - SIG_NODE_DESC: b"\x02@\x80\x00\x00P\xa0\x00\x00\x00\xa0\x00\x00", - SIG_ENDPOINTS: { - 1: { - SIG_EP_TYPE: 12, - DEV_SIG_EP_ID: 1, - SIG_EP_INPUT: [0, 1, 1026, 1032], - SIG_EP_OUTPUT: [], - SIG_EP_PROFILE: 260, - }, - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: [], - DEV_SIG_ENT_MAP: { - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1032"): { - DEV_SIG_CLUSTER_HANDLERS: ["soil_moisture"], - DEV_SIG_ENT_MAP_CLASS: "SoilMoisture", - DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_soil_moisture", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1026"): { - DEV_SIG_CLUSTER_HANDLERS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_temperature", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.efektalab_ru_efekta_pws_lqi", - }, - }, - }, - { - DEV_SIG_DEV_NO: 100, - SIG_MANUFACTURER: "Konke", - SIG_MODEL: "3AFE170100510001", - SIG_NODE_DESC: b"\x02@\x80\x02\x10RR\x00\x00,R\x00\x00", - SIG_ENDPOINTS: { - 1: { - PROFILE_ID: 260, - DEVICE_TYPE: zha.DeviceType.ON_OFF_OUTPUT, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - Identify.cluster_id, - Groups.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - ], - OUTPUT_CLUSTERS: [ - Identify.cluster_id, - ], - } - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: [], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-1-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.konke_3afe170100510001_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.konke_3afe170100510001_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.konke_3afe170100510001_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.konke_3afe170100510001_lqi", - }, - }, - }, - { - DEV_SIG_DEV_NO: 101, - SIG_MANUFACTURER: "Philips", - SIG_MODEL: "SML001", - SIG_NODE_DESC: b"\x02@\x80\x0b\x10Y?\x00\x00\x00?\x00\x00", - SIG_ENDPOINTS: { - 1: { - PROFILE_ID: zll.PROFILE_ID, - DEVICE_TYPE: zll.DeviceType.ON_OFF_SENSOR, - INPUT_CLUSTERS: [Basic.cluster_id], - OUTPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - Groups.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - LevelControl.cluster_id, - Color.cluster_id, - ], - }, - 2: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.OCCUPANCY_SENSOR, - INPUT_CLUSTERS: [ - Basic.cluster_id, - PowerConfiguration.cluster_id, - Identify.cluster_id, - IlluminanceMeasurement.cluster_id, - TemperatureMeasurement.cluster_id, - OccupancySensing.cluster_id, - ], - OUTPUT_CLUSTERS: [ - Ota.cluster_id, - ], - }, - }, - DEV_SIG_ATTRIBUTES: { - 2: { - "basic": { - "trigger_indicator": Bool(False), - }, - "philips_occupancy": { - "sensitivity": uint8_t(1), - }, - } - }, - DEV_SIG_EVT_CLUSTER_HANDLERS: [ - "1:0x0005", - "1:0x0006", - "1:0x0008", - "1:0x0300", - "2:0x0019", - ], - DEV_SIG_ENT_MAP: { - ("button", "00:11:22:33:44:55:66:77-2-3"): { - DEV_SIG_CLUSTER_HANDLERS: ["identify"], - DEV_SIG_ENT_MAP_CLASS: "ZHAIdentifyButton", - DEV_SIG_ENT_MAP_ID: "button.philips_sml001_identify", - }, - ("sensor", "00:11:22:33:44:55:66:77-2-1"): { - DEV_SIG_CLUSTER_HANDLERS: ["power"], - DEV_SIG_ENT_MAP_CLASS: "Battery", - DEV_SIG_ENT_MAP_ID: "sensor.philips_sml001_battery", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-rssi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "RSSISensor", - DEV_SIG_ENT_MAP_ID: "sensor.philips_sml001_rssi", - }, - ("sensor", "00:11:22:33:44:55:66:77-1-0-lqi"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "LQISensor", - DEV_SIG_ENT_MAP_ID: "sensor.philips_sml001_lqi", - }, - ("binary_sensor", "00:11:22:33:44:55:66:77-1-6"): { - DEV_SIG_CLUSTER_HANDLERS: ["on_off"], - DEV_SIG_ENT_MAP_CLASS: "Motion", - DEV_SIG_ENT_MAP_ID: "binary_sensor.philips_sml001_motion", - }, - ("sensor", "00:11:22:33:44:55:66:77-2-1024"): { - DEV_SIG_CLUSTER_HANDLERS: ["illuminance"], - DEV_SIG_ENT_MAP_CLASS: "Illuminance", - DEV_SIG_ENT_MAP_ID: "sensor.philips_sml001_illuminance", - }, - ("binary_sensor", "00:11:22:33:44:55:66:77-2-1030"): { - DEV_SIG_CLUSTER_HANDLERS: ["philips_occupancy"], - DEV_SIG_ENT_MAP_CLASS: "HueOccupancy", - DEV_SIG_ENT_MAP_ID: "binary_sensor.philips_sml001_occupancy", - }, - ("sensor", "00:11:22:33:44:55:66:77-2-1026"): { - DEV_SIG_CLUSTER_HANDLERS: ["temperature"], - DEV_SIG_ENT_MAP_CLASS: "Temperature", - DEV_SIG_ENT_MAP_ID: "sensor.philips_sml001_temperature", - }, - ("switch", "00:11:22:33:44:55:66:77-2-0-trigger_indicator"): { - DEV_SIG_CLUSTER_HANDLERS: ["basic"], - DEV_SIG_ENT_MAP_CLASS: "HueMotionTriggerIndicatorSwitch", - DEV_SIG_ENT_MAP_ID: "switch.philips_sml001_led_trigger_indicator", - }, - ("select", "00:11:22:33:44:55:66:77-2-1030-motion_sensitivity"): { - DEV_SIG_CLUSTER_HANDLERS: ["philips_occupancy"], - DEV_SIG_ENT_MAP_CLASS: "HueV1MotionSensitivity", - DEV_SIG_ENT_MAP_ID: "select.philips_sml001_motion_sensitivity", - }, - ("update", "00:11:22:33:44:55:66:77-2-25-firmware_update"): { - DEV_SIG_CLUSTER_HANDLERS: ["ota"], - DEV_SIG_ENT_MAP_CLASS: "ZHAFirmwareUpdateEntity", - DEV_SIG_ENT_MAP_ID: "update.philips_sml001_firmware", - }, - }, - }, -]