hass-core/homeassistant/components/zha/binary_sensor.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

351 lines
12 KiB
Python

"""Binary sensors on Zigbee Home Automation networks."""
from __future__ import annotations
import functools
from typing import Any
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.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_ZONE,
SIGNAL_ADD_ENTITIES,
SIGNAL_ATTR_UPDATED,
)
from .core.helpers import get_zha_data
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
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Zigbee Home Automation binary sensor from config entry."""
zha_data = get_zha_data(hass)
entities_to_create = zha_data.platforms[Platform.BINARY_SENSOR]
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 BinarySensor(ZhaEntity, BinarySensorEntity):
"""ZHA BinarySensor."""
_attribute_name: str
def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs) -> None:
"""Initialize the ZHA binary sensor."""
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
self._cluster_handler = cluster_handlers[0]
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
# temporary code to migrate old IasZone sensors to update attribute cache state once
# remove in 2024.4.0
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return state attributes."""
return {"migrated_to_cache": True} # writing new state means we're migrated
# temporary migration code
@callback
def async_restore_last_state(self, last_state):
"""Restore previous state."""
# trigger migration if extra state attribute is not present
if "migrated_to_cache" not in last_state.attributes:
self.migrate_to_zigpy_cache(last_state)
# temporary migration code
@callback
def migrate_to_zigpy_cache(self, last_state):
"""Save old IasZone sensor state to attribute cache."""
# previous HA versions did not update the attribute cache for IasZone sensors, so do it once here
# a HA state write is triggered shortly afterwards and writes the "migrated_to_cache" extra state attribute
if last_state.state == STATE_ON:
migrated_state = IasZone.ZoneStatus.Alarm_1
else:
migrated_state = IasZone.ZoneStatus(0)
self._cluster_handler.cluster.update_attribute(
IasZone.attributes_by_name[self._attribute_name].id, migrated_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_icon = "mdi:hand-wave"
_attr_entity_category = EntityCategory.DIAGNOSTIC