hass-core/homeassistant/components/zha/select.py
David F. Mulcahey ac8f555a70
Add additional entities for the Aqara E1 curtain motor to ZHA (#108243)
* aqara curtain motor opened by hand binary sensor

add icon and translation key for identify button

remove previous inversion entity

add window covering type sensor and aqara curtain motor sensors

add aqara curtain motor hook lock switch

add aqara curtain motor attributes zcl_init_attrs

add aqara curtain motor zcl_init_attrs

translations

* update translation string

* review comments

* use enum sensor after rebase

* remove button change
2024-01-30 22:40:33 -05:00

681 lines
20 KiB
Python

"""Support for ZHA controls using the select platform."""
from __future__ import annotations
from enum import Enum
import functools
import logging
from typing import TYPE_CHECKING, Any, Self
from zhaquirks.quirk_ids import 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.zcl.clusters.general import OnOff
from zigpy.zcl.clusters.security import IasWd
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.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,
SIGNAL_ADD_ENTITIES,
SIGNAL_ATTR_UPDATED,
Strobe,
)
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__)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Zigbee Home Automation siren from config entry."""
zha_data = get_zha_data(hass)
entities_to_create = zha_data.platforms[Platform.SELECT]
unsub = async_dispatcher_connect(
hass,
SIGNAL_ADD_ENTITIES,
functools.partial(
discovery.async_add_entities,
async_add_entities,
entities_to_create,
),
)
config_entry.async_on_unload(unsub)
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._attribute_name = self._enum.__name__
self._attr_options = [entry.name.replace("_", " ") for entry in self._enum]
self._cluster_handler: ClusterHandler = cluster_handlers[0]
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
@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("_", " ")
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(" ", "_")
]
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 (
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 select entity."""
self._attr_options = [entry.name.replace("_", " ") for entry in self._enum]
self._cluster_handler: ClusterHandler = cluster_handlers[0]
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
@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"
_attr_icon: str = "mdi:wrench-clock"
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"
_attr_icon: str = "mdi:lock"