mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Refactor ZHA (#91476)
* rename channel -> cluster handler * remove refs to channels and create endpoint class * remove remaining references to channels * fix filter * take in latest changes from #91403 * missed one * missed a reference
This commit is contained in:
parent
090f59aaa2
commit
9c784ac622
@ -1509,7 +1509,7 @@ omit =
|
||||
homeassistant/components/zeversolar/entity.py
|
||||
homeassistant/components/zeversolar/sensor.py
|
||||
homeassistant/components/zha/websocket_api.py
|
||||
homeassistant/components/zha/core/channels/*
|
||||
homeassistant/components/zha/core/cluster_handlers/*
|
||||
homeassistant/components/zha/core/device.py
|
||||
homeassistant/components/zha/core/gateway.py
|
||||
homeassistant/components/zha/core/helpers.py
|
||||
|
@ -25,13 +25,13 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .core import discovery
|
||||
from .core.channels.security import (
|
||||
from .core.cluster_handlers.security import (
|
||||
SIGNAL_ALARM_TRIGGERED,
|
||||
SIGNAL_ARMED_STATE_CHANGED,
|
||||
IasAce as AceChannel,
|
||||
IasAce as AceClusterHandler,
|
||||
)
|
||||
from .core.const import (
|
||||
CHANNEL_IAS_ACE,
|
||||
CLUSTER_HANDLER_IAS_ACE,
|
||||
CONF_ALARM_ARM_REQUIRES_CODE,
|
||||
CONF_ALARM_FAILED_TRIES,
|
||||
CONF_ALARM_MASTER_CODE,
|
||||
@ -77,7 +77,7 @@ async def async_setup_entry(
|
||||
config_entry.async_on_unload(unsub)
|
||||
|
||||
|
||||
@STRICT_MATCH(channel_names=CHANNEL_IAS_ACE)
|
||||
@STRICT_MATCH(cluster_handler_names=CLUSTER_HANDLER_IAS_ACE)
|
||||
class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity):
|
||||
"""Entity for ZHA alarm control devices."""
|
||||
|
||||
@ -89,18 +89,20 @@ class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity):
|
||||
| AlarmControlPanelEntityFeature.TRIGGER
|
||||
)
|
||||
|
||||
def __init__(self, unique_id, zha_device: ZHADevice, channels, **kwargs) -> None:
|
||||
def __init__(
|
||||
self, unique_id, zha_device: ZHADevice, cluster_handlers, **kwargs
|
||||
) -> None:
|
||||
"""Initialize the ZHA alarm control device."""
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
|
||||
cfg_entry = zha_device.gateway.config_entry
|
||||
self._channel: AceChannel = channels[0]
|
||||
self._channel.panel_code = async_get_zha_config_value(
|
||||
self._cluster_handler: AceClusterHandler = cluster_handlers[0]
|
||||
self._cluster_handler.panel_code = async_get_zha_config_value(
|
||||
cfg_entry, ZHA_ALARM_OPTIONS, CONF_ALARM_MASTER_CODE, "1234"
|
||||
)
|
||||
self._channel.code_required_arm_actions = async_get_zha_config_value(
|
||||
self._cluster_handler.code_required_arm_actions = async_get_zha_config_value(
|
||||
cfg_entry, ZHA_ALARM_OPTIONS, CONF_ALARM_ARM_REQUIRES_CODE, False
|
||||
)
|
||||
self._channel.max_invalid_tries = async_get_zha_config_value(
|
||||
self._cluster_handler.max_invalid_tries = async_get_zha_config_value(
|
||||
cfg_entry, ZHA_ALARM_OPTIONS, CONF_ALARM_FAILED_TRIES, 3
|
||||
)
|
||||
|
||||
@ -108,10 +110,10 @@ class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity):
|
||||
"""Run when about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_accept_signal(
|
||||
self._channel, SIGNAL_ARMED_STATE_CHANGED, self.async_set_armed_mode
|
||||
self._cluster_handler, SIGNAL_ARMED_STATE_CHANGED, self.async_set_armed_mode
|
||||
)
|
||||
self.async_accept_signal(
|
||||
self._channel, SIGNAL_ALARM_TRIGGERED, self.async_alarm_trigger
|
||||
self._cluster_handler, SIGNAL_ALARM_TRIGGERED, self.async_alarm_trigger
|
||||
)
|
||||
|
||||
@callback
|
||||
@ -122,26 +124,26 @@ class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity):
|
||||
@property
|
||||
def code_arm_required(self) -> bool:
|
||||
"""Whether the code is required for arm actions."""
|
||||
return self._channel.code_required_arm_actions
|
||||
return self._cluster_handler.code_required_arm_actions
|
||||
|
||||
async def async_alarm_disarm(self, code: str | None = None) -> None:
|
||||
"""Send disarm command."""
|
||||
self._channel.arm(IasAce.ArmMode.Disarm, code, 0)
|
||||
self._cluster_handler.arm(IasAce.ArmMode.Disarm, code, 0)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_alarm_arm_home(self, code: str | None = None) -> None:
|
||||
"""Send arm home command."""
|
||||
self._channel.arm(IasAce.ArmMode.Arm_Day_Home_Only, code, 0)
|
||||
self._cluster_handler.arm(IasAce.ArmMode.Arm_Day_Home_Only, code, 0)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_alarm_arm_away(self, code: str | None = None) -> None:
|
||||
"""Send arm away command."""
|
||||
self._channel.arm(IasAce.ArmMode.Arm_All_Zones, code, 0)
|
||||
self._cluster_handler.arm(IasAce.ArmMode.Arm_All_Zones, code, 0)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_alarm_arm_night(self, code: str | None = None) -> None:
|
||||
"""Send arm night command."""
|
||||
self._channel.arm(IasAce.ArmMode.Arm_Night_Sleep_Only, code, 0)
|
||||
self._cluster_handler.arm(IasAce.ArmMode.Arm_Night_Sleep_Only, code, 0)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_alarm_trigger(self, code: str | None = None) -> None:
|
||||
@ -151,4 +153,4 @@ class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity):
|
||||
@property
|
||||
def state(self) -> str | None:
|
||||
"""Return the state of the entity."""
|
||||
return IAS_ACE_STATE_MAP.get(self._channel.armed_state)
|
||||
return IAS_ACE_STATE_MAP.get(self._cluster_handler.armed_state)
|
||||
|
@ -20,11 +20,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .core import discovery
|
||||
from .core.const import (
|
||||
CHANNEL_ACCELEROMETER,
|
||||
CHANNEL_BINARY_INPUT,
|
||||
CHANNEL_OCCUPANCY,
|
||||
CHANNEL_ON_OFF,
|
||||
CHANNEL_ZONE,
|
||||
CLUSTER_HANDLER_ACCELEROMETER,
|
||||
CLUSTER_HANDLER_BINARY_INPUT,
|
||||
CLUSTER_HANDLER_OCCUPANCY,
|
||||
CLUSTER_HANDLER_ON_OFF,
|
||||
CLUSTER_HANDLER_ZONE,
|
||||
DATA_ZHA,
|
||||
SIGNAL_ADD_ENTITIES,
|
||||
SIGNAL_ATTR_UPDATED,
|
||||
@ -72,22 +72,22 @@ class BinarySensor(ZhaEntity, BinarySensorEntity):
|
||||
|
||||
SENSOR_ATTR: str | None = None
|
||||
|
||||
def __init__(self, unique_id, zha_device, channels, **kwargs):
|
||||
def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs):
|
||||
"""Initialize the ZHA binary sensor."""
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
self._channel = channels[0]
|
||||
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._channel, SIGNAL_ATTR_UPDATED, self.async_set_state
|
||||
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._channel.cluster.get(self.SENSOR_ATTR)
|
||||
raw_state = self._cluster_handler.cluster.get(self.SENSOR_ATTR)
|
||||
if raw_state is None:
|
||||
return False
|
||||
return self.parse(raw_state)
|
||||
@ -103,7 +103,7 @@ class BinarySensor(ZhaEntity, BinarySensorEntity):
|
||||
return bool(value)
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names=CHANNEL_ACCELEROMETER)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ACCELEROMETER)
|
||||
class Accelerometer(BinarySensor):
|
||||
"""ZHA BinarySensor."""
|
||||
|
||||
@ -111,7 +111,7 @@ class Accelerometer(BinarySensor):
|
||||
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.MOVING
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names=CHANNEL_OCCUPANCY)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_OCCUPANCY)
|
||||
class Occupancy(BinarySensor):
|
||||
"""ZHA BinarySensor."""
|
||||
|
||||
@ -119,7 +119,7 @@ class Occupancy(BinarySensor):
|
||||
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.OCCUPANCY
|
||||
|
||||
|
||||
@STRICT_MATCH(channel_names=CHANNEL_ON_OFF)
|
||||
@STRICT_MATCH(cluster_handler_names=CLUSTER_HANDLER_ON_OFF)
|
||||
class Opening(BinarySensor):
|
||||
"""ZHA OnOff BinarySensor."""
|
||||
|
||||
@ -131,13 +131,13 @@ class Opening(BinarySensor):
|
||||
@callback
|
||||
def async_restore_last_state(self, last_state):
|
||||
"""Restore previous state to zigpy cache."""
|
||||
self._channel.cluster.update_attribute(
|
||||
self._cluster_handler.cluster.update_attribute(
|
||||
OnOff.attributes_by_name[self.SENSOR_ATTR].id,
|
||||
t.Bool.true if last_state.state == STATE_ON else t.Bool.false,
|
||||
)
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names=CHANNEL_BINARY_INPUT)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_BINARY_INPUT)
|
||||
class BinaryInput(BinarySensor):
|
||||
"""ZHA BinarySensor."""
|
||||
|
||||
@ -145,14 +145,14 @@ class BinaryInput(BinarySensor):
|
||||
|
||||
|
||||
@STRICT_MATCH(
|
||||
channel_names=CHANNEL_ON_OFF,
|
||||
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(
|
||||
channel_names=CHANNEL_ON_OFF,
|
||||
cluster_handler_names=CLUSTER_HANDLER_ON_OFF,
|
||||
manufacturers="Philips",
|
||||
models={"SML001", "SML002"},
|
||||
)
|
||||
@ -162,7 +162,7 @@ class Motion(Opening):
|
||||
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.MOTION
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names=CHANNEL_ZONE)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ZONE)
|
||||
class IASZone(BinarySensor):
|
||||
"""ZHA IAS BinarySensor."""
|
||||
|
||||
@ -171,7 +171,7 @@ class IASZone(BinarySensor):
|
||||
@property
|
||||
def device_class(self) -> BinarySensorDeviceClass | None:
|
||||
"""Return device class from component DEVICE_CLASSES."""
|
||||
return CLASS_MAPPING.get(self._channel.cluster.get("zone_type"))
|
||||
return CLASS_MAPPING.get(self._cluster_handler.cluster.get("zone_type"))
|
||||
|
||||
@staticmethod
|
||||
def parse(value: bool | int) -> bool:
|
||||
@ -204,13 +204,13 @@ class IASZone(BinarySensor):
|
||||
else:
|
||||
migrated_state = IasZone.ZoneStatus(0)
|
||||
|
||||
self._channel.cluster.update_attribute(
|
||||
self._cluster_handler.cluster.update_attribute(
|
||||
IasZone.attributes_by_name[self.SENSOR_ATTR].id, migrated_state
|
||||
)
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names="tuya_manufacturer",
|
||||
cluster_handler_names="tuya_manufacturer",
|
||||
manufacturers={
|
||||
"_TZE200_htnnfasr",
|
||||
},
|
||||
@ -222,7 +222,7 @@ class FrostLock(BinarySensor, id_suffix="frost_lock"):
|
||||
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.LOCK
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names="ikea_airpurifier")
|
||||
@MULTI_MATCH(cluster_handler_names="ikea_airpurifier")
|
||||
class ReplaceFilter(BinarySensor, id_suffix="replace_filter"):
|
||||
"""ZHA BinarySensor."""
|
||||
|
||||
@ -230,7 +230,7 @@ class ReplaceFilter(BinarySensor, id_suffix="replace_filter"):
|
||||
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.PROBLEM
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"})
|
||||
@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"})
|
||||
class AqaraPetFeederErrorDetected(BinarySensor, id_suffix="error_detected"):
|
||||
"""ZHA aqara pet feeder error detected binary sensor."""
|
||||
|
||||
@ -240,7 +240,8 @@ class AqaraPetFeederErrorDetected(BinarySensor, id_suffix="error_detected"):
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names="opple_cluster", models={"lumi.plug.mmeu01", "lumi.plug.maeu01"}
|
||||
cluster_handler_names="opple_cluster",
|
||||
models={"lumi.plug.mmeu01", "lumi.plug.maeu01"},
|
||||
)
|
||||
class XiaomiPlugConsumerConnected(BinarySensor, id_suffix="consumer_connected"):
|
||||
"""ZHA Xiaomi plug consumer connected binary sensor."""
|
||||
@ -250,7 +251,7 @@ class XiaomiPlugConsumerConnected(BinarySensor, id_suffix="consumer_connected"):
|
||||
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.PLUG
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names="opple_cluster", models={"lumi.airrtc.agl001"})
|
||||
@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"lumi.airrtc.agl001"})
|
||||
class AqaraThermostatWindowOpen(BinarySensor, id_suffix="window_open"):
|
||||
"""ZHA Aqara thermostat window open binary sensor."""
|
||||
|
||||
@ -259,7 +260,7 @@ class AqaraThermostatWindowOpen(BinarySensor, id_suffix="window_open"):
|
||||
_attr_name: str = "Window open"
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names="opple_cluster", models={"lumi.airrtc.agl001"})
|
||||
@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"lumi.airrtc.agl001"})
|
||||
class AqaraThermostatValveAlarm(BinarySensor, id_suffix="valve_alarm"):
|
||||
"""ZHA Aqara thermostat valve alarm binary sensor."""
|
||||
|
||||
@ -268,7 +269,9 @@ class AqaraThermostatValveAlarm(BinarySensor, id_suffix="valve_alarm"):
|
||||
_attr_name: str = "Valve alarm"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"lumi.airrtc.agl001"})
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
cluster_handler_names="opple_cluster", models={"lumi.airrtc.agl001"}
|
||||
)
|
||||
class AqaraThermostatCalibrated(BinarySensor, id_suffix="calibrated"):
|
||||
"""ZHA Aqara thermostat calibrated binary sensor."""
|
||||
|
||||
@ -277,7 +280,9 @@ class AqaraThermostatCalibrated(BinarySensor, id_suffix="calibrated"):
|
||||
_attr_name: str = "Calibrated"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"lumi.airrtc.agl001"})
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
cluster_handler_names="opple_cluster", models={"lumi.airrtc.agl001"}
|
||||
)
|
||||
class AqaraThermostatExternalSensor(BinarySensor, id_suffix="sensor"):
|
||||
"""ZHA Aqara thermostat external sensor binary sensor."""
|
||||
|
||||
@ -286,7 +291,7 @@ class AqaraThermostatExternalSensor(BinarySensor, id_suffix="sensor"):
|
||||
_attr_name: str = "External sensor"
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names="opple_cluster", models={"lumi.sensor_smoke.acn03"})
|
||||
@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"lumi.sensor_smoke.acn03"})
|
||||
class AqaraLinkageAlarmState(BinarySensor, id_suffix="linkage_alarm_state"):
|
||||
"""ZHA Aqara linkage alarm state binary sensor."""
|
||||
|
||||
|
@ -18,12 +18,12 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .core import discovery
|
||||
from .core.const import CHANNEL_IDENTIFY, DATA_ZHA, SIGNAL_ADD_ENTITIES
|
||||
from .core.const import CLUSTER_HANDLER_IDENTIFY, DATA_ZHA, SIGNAL_ADD_ENTITIES
|
||||
from .core.registries import ZHA_ENTITIES
|
||||
from .entity import ZhaEntity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .core.channels.base import ZigbeeChannel
|
||||
from .core.cluster_handlers import ClusterHandler
|
||||
from .core.device import ZHADevice
|
||||
|
||||
|
||||
@ -65,12 +65,12 @@ class ZHAButton(ZhaEntity, ButtonEntity):
|
||||
self,
|
||||
unique_id: str,
|
||||
zha_device: ZHADevice,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Init this button."""
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
self._channel: ZigbeeChannel = channels[0]
|
||||
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
|
||||
self._cluster_handler: ClusterHandler = cluster_handlers[0]
|
||||
|
||||
@abc.abstractmethod
|
||||
def get_args(self) -> list[Any]:
|
||||
@ -78,12 +78,12 @@ class ZHAButton(ZhaEntity, ButtonEntity):
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Send out a update command."""
|
||||
command = getattr(self._channel, self._command_name)
|
||||
command = getattr(self._cluster_handler, self._command_name)
|
||||
arguments = self.get_args()
|
||||
await command(*arguments)
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names=CHANNEL_IDENTIFY)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_IDENTIFY)
|
||||
class ZHAIdentifyButton(ZHAButton):
|
||||
"""Defines a ZHA identify button."""
|
||||
|
||||
@ -92,7 +92,7 @@ class ZHAIdentifyButton(ZHAButton):
|
||||
cls,
|
||||
unique_id: str,
|
||||
zha_device: ZHADevice,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
**kwargs: Any,
|
||||
) -> Self | None:
|
||||
"""Entity Factory.
|
||||
@ -100,10 +100,10 @@ class ZHAIdentifyButton(ZHAButton):
|
||||
Return entity if it is a supported configuration, otherwise return None
|
||||
"""
|
||||
if ZHA_ENTITIES.prevent_entity_creation(
|
||||
Platform.BUTTON, zha_device.ieee, CHANNEL_IDENTIFY
|
||||
Platform.BUTTON, zha_device.ieee, CLUSTER_HANDLER_IDENTIFY
|
||||
):
|
||||
return None
|
||||
return cls(unique_id, zha_device, channels, **kwargs)
|
||||
return cls(unique_id, zha_device, cluster_handlers, **kwargs)
|
||||
|
||||
_attr_device_class: ButtonDeviceClass = ButtonDeviceClass.UPDATE
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
@ -126,17 +126,17 @@ class ZHAAttributeButton(ZhaEntity, ButtonEntity):
|
||||
self,
|
||||
unique_id: str,
|
||||
zha_device: ZHADevice,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Init this button."""
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
self._channel: ZigbeeChannel = channels[0]
|
||||
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
|
||||
self._cluster_handler: ClusterHandler = cluster_handlers[0]
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Write attribute with defined value."""
|
||||
try:
|
||||
result = await self._channel.cluster.write_attributes(
|
||||
result = await self._cluster_handler.cluster.write_attributes(
|
||||
{self._attribute_name: self._attribute_value}
|
||||
)
|
||||
except zigpy.exceptions.ZigbeeException as ex:
|
||||
@ -149,7 +149,7 @@ class ZHAAttributeButton(ZhaEntity, ButtonEntity):
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names="tuya_manufacturer",
|
||||
cluster_handler_names="tuya_manufacturer",
|
||||
manufacturers={
|
||||
"_TZE200_htnnfasr",
|
||||
},
|
||||
@ -164,7 +164,9 @@ class FrostLockResetButton(ZHAAttributeButton, id_suffix="reset_frost_lock"):
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"lumi.motion.ac01"})
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
cluster_handler_names="opple_cluster", models={"lumi.motion.ac01"}
|
||||
)
|
||||
class NoPresenceStatusResetButton(
|
||||
ZHAAttributeButton, id_suffix="reset_no_presence_status"
|
||||
):
|
||||
@ -177,7 +179,7 @@ class NoPresenceStatusResetButton(
|
||||
_attr_entity_category = EntityCategory.CONFIG
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"})
|
||||
@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"})
|
||||
class AqaraPetFeederFeedButton(ZHAAttributeButton, id_suffix="feeding"):
|
||||
"""Defines a feed button for the aqara c1 pet feeder."""
|
||||
|
||||
@ -187,7 +189,7 @@ class AqaraPetFeederFeedButton(ZHAAttributeButton, id_suffix="feeding"):
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names="opple_cluster", models={"lumi.sensor_smoke.acn03"}
|
||||
cluster_handler_names="opple_cluster", models={"lumi.sensor_smoke.acn03"}
|
||||
)
|
||||
class AqaraSelfTestButton(ZHAAttributeButton, id_suffix="self_test"):
|
||||
"""Defines a ZHA self-test button for Aqara smoke sensors."""
|
||||
|
@ -43,8 +43,8 @@ import homeassistant.util.dt as dt_util
|
||||
|
||||
from .core import discovery
|
||||
from .core.const import (
|
||||
CHANNEL_FAN,
|
||||
CHANNEL_THERMOSTAT,
|
||||
CLUSTER_HANDLER_FAN,
|
||||
CLUSTER_HANDLER_THERMOSTAT,
|
||||
DATA_ZHA,
|
||||
PRESET_COMPLEX,
|
||||
PRESET_SCHEDULE,
|
||||
@ -127,9 +127,9 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names=CHANNEL_THERMOSTAT,
|
||||
aux_channels=CHANNEL_FAN,
|
||||
stop_on_match_group=CHANNEL_THERMOSTAT,
|
||||
cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
|
||||
aux_cluster_handlers=CLUSTER_HANDLER_FAN,
|
||||
stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT,
|
||||
)
|
||||
class Thermostat(ZhaEntity, ClimateEntity):
|
||||
"""Representation of a ZHA Thermostat device."""
|
||||
@ -140,14 +140,14 @@ class Thermostat(ZhaEntity, ClimateEntity):
|
||||
_attr_precision = PRECISION_TENTHS
|
||||
_attr_temperature_unit = UnitOfTemperature.CELSIUS
|
||||
|
||||
def __init__(self, unique_id, zha_device, channels, **kwargs):
|
||||
def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs):
|
||||
"""Initialize ZHA Thermostat instance."""
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
self._thrm = self.cluster_channels.get(CHANNEL_THERMOSTAT)
|
||||
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
|
||||
self._fan = self.cluster_channels.get(CHANNEL_FAN)
|
||||
self._fan = self.cluster_handlers.get(CLUSTER_HANDLER_FAN)
|
||||
|
||||
@property
|
||||
def current_temperature(self):
|
||||
@ -480,9 +480,9 @@ class Thermostat(ZhaEntity, ClimateEntity):
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names={CHANNEL_THERMOSTAT, "sinope_manufacturer_specific"},
|
||||
cluster_handler_names={CLUSTER_HANDLER_THERMOSTAT, "sinope_manufacturer_specific"},
|
||||
manufacturers="Sinope Technologies",
|
||||
stop_on_match_group=CHANNEL_THERMOSTAT,
|
||||
stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT,
|
||||
)
|
||||
class SinopeTechnologiesThermostat(Thermostat):
|
||||
"""Sinope Technologies Thermostat."""
|
||||
@ -490,12 +490,12 @@ class SinopeTechnologiesThermostat(Thermostat):
|
||||
manufacturer = 0x119C
|
||||
update_time_interval = timedelta(minutes=randint(45, 75))
|
||||
|
||||
def __init__(self, unique_id, zha_device, channels, **kwargs):
|
||||
def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs):
|
||||
"""Initialize ZHA Thermostat instance."""
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
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_channels["sinope_manufacturer_specific"]
|
||||
self._manufacturer_ch = self.cluster_handlers["sinope_manufacturer_specific"]
|
||||
|
||||
@property
|
||||
def _rm_rs_action(self) -> HVACAction:
|
||||
@ -553,28 +553,28 @@ class SinopeTechnologiesThermostat(Thermostat):
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names=CHANNEL_THERMOSTAT,
|
||||
aux_channels=CHANNEL_FAN,
|
||||
cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
|
||||
aux_cluster_handlers=CLUSTER_HANDLER_FAN,
|
||||
manufacturers={"Zen Within", "LUX"},
|
||||
stop_on_match_group=CHANNEL_THERMOSTAT,
|
||||
stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT,
|
||||
)
|
||||
class ZenWithinThermostat(Thermostat):
|
||||
"""Zen Within Thermostat implementation."""
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names=CHANNEL_THERMOSTAT,
|
||||
aux_channels=CHANNEL_FAN,
|
||||
cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
|
||||
aux_cluster_handlers=CLUSTER_HANDLER_FAN,
|
||||
manufacturers="Centralite",
|
||||
models={"3157100", "3157100-E"},
|
||||
stop_on_match_group=CHANNEL_THERMOSTAT,
|
||||
stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT,
|
||||
)
|
||||
class CentralitePearl(ZenWithinThermostat):
|
||||
"""Centralite Pearl Thermostat implementation."""
|
||||
|
||||
|
||||
@STRICT_MATCH(
|
||||
channel_names=CHANNEL_THERMOSTAT,
|
||||
cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
|
||||
manufacturers={
|
||||
"_TZE200_ckud7u2l",
|
||||
"_TZE200_ywdxldoj",
|
||||
@ -594,9 +594,9 @@ class CentralitePearl(ZenWithinThermostat):
|
||||
class MoesThermostat(Thermostat):
|
||||
"""Moes Thermostat implementation."""
|
||||
|
||||
def __init__(self, unique_id, zha_device, channels, **kwargs):
|
||||
def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs):
|
||||
"""Initialize ZHA Thermostat instance."""
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
|
||||
self._presets = [
|
||||
PRESET_NONE,
|
||||
PRESET_AWAY,
|
||||
@ -668,7 +668,7 @@ class MoesThermostat(Thermostat):
|
||||
|
||||
|
||||
@STRICT_MATCH(
|
||||
channel_names=CHANNEL_THERMOSTAT,
|
||||
cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
|
||||
manufacturers={
|
||||
"_TZE200_b6wax7g0",
|
||||
},
|
||||
@ -676,9 +676,9 @@ class MoesThermostat(Thermostat):
|
||||
class BecaThermostat(Thermostat):
|
||||
"""Beca Thermostat implementation."""
|
||||
|
||||
def __init__(self, unique_id, zha_device, channels, **kwargs):
|
||||
def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs):
|
||||
"""Initialize ZHA Thermostat instance."""
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
|
||||
self._presets = [
|
||||
PRESET_NONE,
|
||||
PRESET_AWAY,
|
||||
@ -743,10 +743,10 @@ class BecaThermostat(Thermostat):
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names=CHANNEL_THERMOSTAT,
|
||||
cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
|
||||
manufacturers="Stelpro",
|
||||
models={"SORB"},
|
||||
stop_on_match_group=CHANNEL_THERMOSTAT,
|
||||
stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT,
|
||||
)
|
||||
class StelproFanHeater(Thermostat):
|
||||
"""Stelpro Fan Heater implementation."""
|
||||
@ -758,7 +758,7 @@ class StelproFanHeater(Thermostat):
|
||||
|
||||
|
||||
@STRICT_MATCH(
|
||||
channel_names=CHANNEL_THERMOSTAT,
|
||||
cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
|
||||
manufacturers={
|
||||
"_TZE200_7yoranx2",
|
||||
"_TZE200_e9ba97vf", # TV01-ZG
|
||||
@ -780,9 +780,9 @@ class ZONNSMARTThermostat(Thermostat):
|
||||
PRESET_HOLIDAY = "holiday"
|
||||
PRESET_FROST = "frost protect"
|
||||
|
||||
def __init__(self, unique_id, zha_device, channels, **kwargs):
|
||||
def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs):
|
||||
"""Initialize ZHA Thermostat instance."""
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
|
||||
self._presets = [
|
||||
PRESET_NONE,
|
||||
self.PRESET_HOLIDAY,
|
||||
|
@ -1,385 +0,0 @@
|
||||
"""Channels module for Zigbee Home Automation."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from typing_extensions import Self
|
||||
import zigpy.endpoint
|
||||
import zigpy.zcl.clusters.closures
|
||||
|
||||
from homeassistant.const import ATTR_DEVICE_ID
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from . import ( # noqa: F401
|
||||
base,
|
||||
closures,
|
||||
general,
|
||||
homeautomation,
|
||||
hvac,
|
||||
lighting,
|
||||
lightlink,
|
||||
manufacturerspecific,
|
||||
measurement,
|
||||
protocol,
|
||||
security,
|
||||
smartenergy,
|
||||
)
|
||||
from .. import (
|
||||
const,
|
||||
device as zha_core_device,
|
||||
discovery as zha_disc,
|
||||
registries as zha_regs,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ...entity import ZhaEntity
|
||||
from ..device import ZHADevice
|
||||
|
||||
_ChannelsDictType = dict[str, base.ZigbeeChannel]
|
||||
|
||||
|
||||
class Channels:
|
||||
"""All discovered channels of a device."""
|
||||
|
||||
def __init__(self, zha_device: ZHADevice) -> None:
|
||||
"""Initialize instance."""
|
||||
self._pools: list[ChannelPool] = []
|
||||
self._power_config: base.ZigbeeChannel | None = None
|
||||
self._identify: base.ZigbeeChannel | None = None
|
||||
self._unique_id = str(zha_device.ieee)
|
||||
self._zdo_channel = base.ZDOChannel(zha_device.device.endpoints[0], zha_device)
|
||||
self._zha_device = zha_device
|
||||
|
||||
@property
|
||||
def pools(self) -> list[ChannelPool]:
|
||||
"""Return channel pools list."""
|
||||
return self._pools
|
||||
|
||||
@property
|
||||
def power_configuration_ch(self) -> base.ZigbeeChannel | None:
|
||||
"""Return power configuration channel."""
|
||||
return self._power_config
|
||||
|
||||
@power_configuration_ch.setter
|
||||
def power_configuration_ch(self, channel: base.ZigbeeChannel) -> None:
|
||||
"""Power configuration channel setter."""
|
||||
if self._power_config is None:
|
||||
self._power_config = channel
|
||||
|
||||
@property
|
||||
def identify_ch(self) -> base.ZigbeeChannel | None:
|
||||
"""Return power configuration channel."""
|
||||
return self._identify
|
||||
|
||||
@identify_ch.setter
|
||||
def identify_ch(self, channel: base.ZigbeeChannel) -> None:
|
||||
"""Power configuration channel setter."""
|
||||
if self._identify is None:
|
||||
self._identify = channel
|
||||
|
||||
@property
|
||||
def zdo_channel(self) -> base.ZDOChannel:
|
||||
"""Return ZDO channel."""
|
||||
return self._zdo_channel
|
||||
|
||||
@property
|
||||
def zha_device(self) -> ZHADevice:
|
||||
"""Return parent ZHA device."""
|
||||
return self._zha_device
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique id for this channel."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def zigbee_signature(self) -> dict[int, dict[str, Any]]:
|
||||
"""Get the zigbee signatures for the pools in channels."""
|
||||
return {
|
||||
signature[0]: signature[1]
|
||||
for signature in [pool.zigbee_signature for pool in self.pools]
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def new(cls, zha_device: ZHADevice) -> Self:
|
||||
"""Create new instance."""
|
||||
channels = cls(zha_device)
|
||||
for ep_id in sorted(zha_device.device.endpoints):
|
||||
channels.add_pool(ep_id)
|
||||
return channels
|
||||
|
||||
def add_pool(self, ep_id: int) -> None:
|
||||
"""Add channels for a specific endpoint."""
|
||||
if ep_id == 0:
|
||||
return
|
||||
self._pools.append(ChannelPool.new(self, ep_id))
|
||||
|
||||
async def async_initialize(self, from_cache: bool = False) -> None:
|
||||
"""Initialize claimed channels."""
|
||||
await self.zdo_channel.async_initialize(from_cache)
|
||||
self.zdo_channel.debug("'async_initialize' stage succeeded")
|
||||
await asyncio.gather(
|
||||
*(pool.async_initialize(from_cache) for pool in self.pools)
|
||||
)
|
||||
|
||||
async def async_configure(self) -> None:
|
||||
"""Configure claimed channels."""
|
||||
await self.zdo_channel.async_configure()
|
||||
self.zdo_channel.debug("'async_configure' stage succeeded")
|
||||
await asyncio.gather(*(pool.async_configure() for pool in self.pools))
|
||||
async_dispatcher_send(
|
||||
self.zha_device.hass,
|
||||
const.ZHA_CHANNEL_MSG,
|
||||
{
|
||||
const.ATTR_TYPE: const.ZHA_CHANNEL_CFG_DONE,
|
||||
},
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_new_entity(
|
||||
self,
|
||||
component: str,
|
||||
entity_class: type[ZhaEntity],
|
||||
unique_id: str,
|
||||
channels: list[base.ZigbeeChannel],
|
||||
):
|
||||
"""Signal new entity addition."""
|
||||
if self.zha_device.status == zha_core_device.DeviceStatus.INITIALIZED:
|
||||
return
|
||||
|
||||
self.zha_device.hass.data[const.DATA_ZHA][component].append(
|
||||
(entity_class, (unique_id, self.zha_device, channels))
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_send_signal(self, signal: str, *args: Any) -> None:
|
||||
"""Send a signal through hass dispatcher."""
|
||||
async_dispatcher_send(self.zha_device.hass, signal, *args)
|
||||
|
||||
@callback
|
||||
def zha_send_event(self, event_data: dict[str, str | int]) -> None:
|
||||
"""Relay events to hass."""
|
||||
self.zha_device.hass.bus.async_fire(
|
||||
const.ZHA_EVENT,
|
||||
{
|
||||
const.ATTR_DEVICE_IEEE: str(self.zha_device.ieee),
|
||||
const.ATTR_UNIQUE_ID: self.unique_id,
|
||||
ATTR_DEVICE_ID: self.zha_device.device_id,
|
||||
**event_data,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class ChannelPool:
|
||||
"""All channels of an endpoint."""
|
||||
|
||||
def __init__(self, channels: Channels, ep_id: int) -> None:
|
||||
"""Initialize instance."""
|
||||
self._all_channels: _ChannelsDictType = {}
|
||||
self._channels = channels
|
||||
self._claimed_channels: _ChannelsDictType = {}
|
||||
self._id = ep_id
|
||||
self._client_channels: dict[str, base.ClientChannel] = {}
|
||||
self._unique_id = f"{channels.unique_id}-{ep_id}"
|
||||
|
||||
@property
|
||||
def all_channels(self) -> _ChannelsDictType:
|
||||
"""All server channels of an endpoint."""
|
||||
return self._all_channels
|
||||
|
||||
@property
|
||||
def claimed_channels(self) -> _ChannelsDictType:
|
||||
"""Channels in use."""
|
||||
return self._claimed_channels
|
||||
|
||||
@property
|
||||
def client_channels(self) -> dict[str, base.ClientChannel]:
|
||||
"""Return a dict of client channels."""
|
||||
return self._client_channels
|
||||
|
||||
@property
|
||||
def endpoint(self) -> zigpy.endpoint.Endpoint:
|
||||
"""Return endpoint of zigpy device."""
|
||||
return self._channels.zha_device.device.endpoints[self.id]
|
||||
|
||||
@property
|
||||
def id(self) -> int:
|
||||
"""Return endpoint id."""
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def nwk(self) -> int:
|
||||
"""Device NWK for logging."""
|
||||
return self._channels.zha_device.nwk
|
||||
|
||||
@property
|
||||
def is_mains_powered(self) -> bool | None:
|
||||
"""Device is_mains_powered."""
|
||||
return self._channels.zha_device.is_mains_powered
|
||||
|
||||
@property
|
||||
def manufacturer(self) -> str:
|
||||
"""Return device manufacturer."""
|
||||
return self._channels.zha_device.manufacturer
|
||||
|
||||
@property
|
||||
def manufacturer_code(self) -> int | None:
|
||||
"""Return device manufacturer."""
|
||||
return self._channels.zha_device.manufacturer_code
|
||||
|
||||
@property
|
||||
def hass(self) -> HomeAssistant:
|
||||
"""Return hass."""
|
||||
return self._channels.zha_device.hass
|
||||
|
||||
@property
|
||||
def model(self) -> str:
|
||||
"""Return device model."""
|
||||
return self._channels.zha_device.model
|
||||
|
||||
@property
|
||||
def quirk_class(self) -> str:
|
||||
"""Return device quirk class."""
|
||||
return self._channels.zha_device.quirk_class
|
||||
|
||||
@property
|
||||
def skip_configuration(self) -> bool:
|
||||
"""Return True if device does not require channel configuration."""
|
||||
return self._channels.zha_device.skip_configuration
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return the unique id for this channel."""
|
||||
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.endpoint.endpoint_id,
|
||||
{
|
||||
const.ATTR_PROFILE_ID: self.endpoint.profile_id,
|
||||
const.ATTR_DEVICE_TYPE: f"0x{self.endpoint.device_type:04x}"
|
||||
if self.endpoint.device_type is not None
|
||||
else "",
|
||||
const.ATTR_IN_CLUSTERS: [
|
||||
f"0x{cluster_id:04x}"
|
||||
for cluster_id in sorted(self.endpoint.in_clusters)
|
||||
],
|
||||
const.ATTR_OUT_CLUSTERS: [
|
||||
f"0x{cluster_id:04x}"
|
||||
for cluster_id in sorted(self.endpoint.out_clusters)
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def new(cls, channels: Channels, ep_id: int) -> Self:
|
||||
"""Create new channels for an endpoint."""
|
||||
pool = cls(channels, ep_id)
|
||||
pool.add_all_channels()
|
||||
pool.add_client_channels()
|
||||
if not channels.zha_device.is_coordinator:
|
||||
zha_disc.PROBE.discover_entities(pool)
|
||||
return pool
|
||||
|
||||
@callback
|
||||
def add_all_channels(self) -> None:
|
||||
"""Create and add channels for all input clusters."""
|
||||
for cluster_id, cluster in self.endpoint.in_clusters.items():
|
||||
channel_class = zha_regs.ZIGBEE_CHANNEL_REGISTRY.get(
|
||||
cluster_id, base.ZigbeeChannel
|
||||
)
|
||||
# really ugly hack to deal with xiaomi using the door lock cluster
|
||||
# incorrectly.
|
||||
if (
|
||||
hasattr(cluster, "ep_attribute")
|
||||
and cluster_id == zigpy.zcl.clusters.closures.DoorLock.cluster_id
|
||||
and cluster.ep_attribute == "multistate_input"
|
||||
):
|
||||
channel_class = general.MultistateInput
|
||||
# end of ugly hack
|
||||
channel = channel_class(cluster, self)
|
||||
if channel.name == const.CHANNEL_POWER_CONFIGURATION:
|
||||
if (
|
||||
self._channels.power_configuration_ch
|
||||
or self._channels.zha_device.is_mains_powered
|
||||
):
|
||||
# on power configuration channel per device
|
||||
continue
|
||||
self._channels.power_configuration_ch = channel
|
||||
elif channel.name == const.CHANNEL_IDENTIFY:
|
||||
self._channels.identify_ch = channel
|
||||
|
||||
self.all_channels[channel.id] = channel
|
||||
|
||||
@callback
|
||||
def add_client_channels(self) -> None:
|
||||
"""Create client channels for all output clusters if in the registry."""
|
||||
for cluster_id, channel_class in zha_regs.CLIENT_CHANNELS_REGISTRY.items():
|
||||
cluster = self.endpoint.out_clusters.get(cluster_id)
|
||||
if cluster is not None:
|
||||
channel = channel_class(cluster, self)
|
||||
self.client_channels[channel.id] = channel
|
||||
|
||||
async def async_initialize(self, from_cache: bool = False) -> None:
|
||||
"""Initialize claimed channels."""
|
||||
await self._execute_channel_tasks("async_initialize", from_cache)
|
||||
|
||||
async def async_configure(self) -> None:
|
||||
"""Configure claimed channels."""
|
||||
await self._execute_channel_tasks("async_configure")
|
||||
|
||||
async def _execute_channel_tasks(self, func_name: str, *args: Any) -> None:
|
||||
"""Add a throttled channel task and swallow exceptions."""
|
||||
channels = [*self.claimed_channels.values(), *self.client_channels.values()]
|
||||
tasks = [getattr(ch, func_name)(*args) for ch in channels]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
for channel, outcome in zip(channels, results):
|
||||
if isinstance(outcome, Exception):
|
||||
channel.warning(
|
||||
"'%s' stage failed: %s", func_name, str(outcome), exc_info=outcome
|
||||
)
|
||||
continue
|
||||
channel.debug("'%s' stage succeeded", func_name)
|
||||
|
||||
@callback
|
||||
def async_new_entity(
|
||||
self,
|
||||
component: str,
|
||||
entity_class: type[ZhaEntity],
|
||||
unique_id: str,
|
||||
channels: list[base.ZigbeeChannel],
|
||||
):
|
||||
"""Signal new entity addition."""
|
||||
self._channels.async_new_entity(component, entity_class, unique_id, channels)
|
||||
|
||||
@callback
|
||||
def async_send_signal(self, signal: str, *args: Any) -> None:
|
||||
"""Send a signal through hass dispatcher."""
|
||||
self._channels.async_send_signal(signal, *args)
|
||||
|
||||
@callback
|
||||
def claim_channels(self, channels: list[base.ZigbeeChannel]) -> None:
|
||||
"""Claim a channel."""
|
||||
self.claimed_channels.update({ch.id: ch for ch in channels})
|
||||
|
||||
@callback
|
||||
def unclaimed_channels(self) -> list[base.ZigbeeChannel]:
|
||||
"""Return a list of available (unclaimed) channels."""
|
||||
claimed = set(self.claimed_channels)
|
||||
available = set(self.all_channels)
|
||||
return [self.all_channels[chan_id] for chan_id in (available - claimed)]
|
||||
|
||||
@callback
|
||||
def zha_send_event(self, event_data: dict[str, Any]) -> None:
|
||||
"""Relay events to hass."""
|
||||
self._channels.zha_send_event(
|
||||
{
|
||||
const.ATTR_UNIQUE_ID: self.unique_id,
|
||||
const.ATTR_ENDPOINT_ID: self.id,
|
||||
**event_data,
|
||||
}
|
||||
)
|
@ -1,15 +0,0 @@
|
||||
"""Helpers for use with ZHA Zigbee channels."""
|
||||
from .base import ZigbeeChannel
|
||||
|
||||
|
||||
def is_hue_motion_sensor(channel: ZigbeeChannel) -> bool:
|
||||
"""Return true if the manufacturer and model match known Hue motion sensor models."""
|
||||
return channel.cluster.endpoint.manufacturer in (
|
||||
"Philips",
|
||||
"Signify Netherlands B.V.",
|
||||
) and channel.cluster.endpoint.model in (
|
||||
"SML001",
|
||||
"SML002",
|
||||
"SML003",
|
||||
"SML004",
|
||||
)
|
@ -1,113 +0,0 @@
|
||||
"""Protocol channels module for Zigbee Home Automation."""
|
||||
from zigpy.zcl.clusters import protocol
|
||||
|
||||
from .. import registries
|
||||
from .base import ZigbeeChannel
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogInputExtended.cluster_id)
|
||||
class AnalogInputExtended(ZigbeeChannel):
|
||||
"""Analog Input Extended channel."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogInputRegular.cluster_id)
|
||||
class AnalogInputRegular(ZigbeeChannel):
|
||||
"""Analog Input Regular channel."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogOutputExtended.cluster_id)
|
||||
class AnalogOutputExtended(ZigbeeChannel):
|
||||
"""Analog Output Regular channel."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogOutputRegular.cluster_id)
|
||||
class AnalogOutputRegular(ZigbeeChannel):
|
||||
"""Analog Output Regular channel."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogValueExtended.cluster_id)
|
||||
class AnalogValueExtended(ZigbeeChannel):
|
||||
"""Analog Value Extended edition channel."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.AnalogValueRegular.cluster_id)
|
||||
class AnalogValueRegular(ZigbeeChannel):
|
||||
"""Analog Value Regular channel."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.BacnetProtocolTunnel.cluster_id)
|
||||
class BacnetProtocolTunnel(ZigbeeChannel):
|
||||
"""Bacnet Protocol Tunnel channel."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryInputExtended.cluster_id)
|
||||
class BinaryInputExtended(ZigbeeChannel):
|
||||
"""Binary Input Extended channel."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryInputRegular.cluster_id)
|
||||
class BinaryInputRegular(ZigbeeChannel):
|
||||
"""Binary Input Regular channel."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryOutputExtended.cluster_id)
|
||||
class BinaryOutputExtended(ZigbeeChannel):
|
||||
"""Binary Output Extended channel."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryOutputRegular.cluster_id)
|
||||
class BinaryOutputRegular(ZigbeeChannel):
|
||||
"""Binary Output Regular channel."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryValueExtended.cluster_id)
|
||||
class BinaryValueExtended(ZigbeeChannel):
|
||||
"""Binary Value Extended channel."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.BinaryValueRegular.cluster_id)
|
||||
class BinaryValueRegular(ZigbeeChannel):
|
||||
"""Binary Value Regular channel."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.GenericTunnel.cluster_id)
|
||||
class GenericTunnel(ZigbeeChannel):
|
||||
"""Generic Tunnel channel."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(
|
||||
protocol.MultistateInputExtended.cluster_id
|
||||
)
|
||||
class MultiStateInputExtended(ZigbeeChannel):
|
||||
"""Multistate Input Extended channel."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.MultistateInputRegular.cluster_id)
|
||||
class MultiStateInputRegular(ZigbeeChannel):
|
||||
"""Multistate Input Regular channel."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(
|
||||
protocol.MultistateOutputExtended.cluster_id
|
||||
)
|
||||
class MultiStateOutputExtended(ZigbeeChannel):
|
||||
"""Multistate Output Extended channel."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(
|
||||
protocol.MultistateOutputRegular.cluster_id
|
||||
)
|
||||
class MultiStateOutputRegular(ZigbeeChannel):
|
||||
"""Multistate Output Regular channel."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(
|
||||
protocol.MultistateValueExtended.cluster_id
|
||||
)
|
||||
class MultiStateValueExtended(ZigbeeChannel):
|
||||
"""Multistate Value Extended channel."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(protocol.MultistateValueRegular.cluster_id)
|
||||
class MultiStateValueRegular(ZigbeeChannel):
|
||||
"""Multistate Value Regular channel."""
|
@ -1,4 +1,4 @@
|
||||
"""Base classes for channels."""
|
||||
"""Cluster handlers module for Zigbee Home Automation."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
@ -29,19 +29,19 @@ from ..const import (
|
||||
ATTR_TYPE,
|
||||
ATTR_UNIQUE_ID,
|
||||
ATTR_VALUE,
|
||||
CHANNEL_ZDO,
|
||||
CLUSTER_HANDLER_ZDO,
|
||||
REPORT_CONFIG_ATTR_PER_REQ,
|
||||
SIGNAL_ATTR_UPDATED,
|
||||
ZHA_CHANNEL_MSG,
|
||||
ZHA_CHANNEL_MSG_BIND,
|
||||
ZHA_CHANNEL_MSG_CFG_RPT,
|
||||
ZHA_CHANNEL_MSG_DATA,
|
||||
ZHA_CHANNEL_READS_PER_REQ,
|
||||
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, retryable_req, safe_read
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ChannelPool
|
||||
from ..endpoint import Endpoint
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -56,31 +56,31 @@ class AttrReportConfig(TypedDict, total=True):
|
||||
config: tuple[int, int, int | float]
|
||||
|
||||
|
||||
def parse_and_log_command(channel, tsn, command_id, args):
|
||||
def parse_and_log_command(cluster_handler, tsn, command_id, args):
|
||||
"""Parse and log a zigbee cluster command."""
|
||||
try:
|
||||
name = channel.cluster.server_commands[command_id].name
|
||||
name = cluster_handler.cluster.server_commands[command_id].name
|
||||
except KeyError:
|
||||
name = f"0x{command_id:02X}"
|
||||
|
||||
channel.debug(
|
||||
cluster_handler.debug(
|
||||
"received '%s' command with %s args on cluster_id '%s' tsn '%s'",
|
||||
name,
|
||||
args,
|
||||
channel.cluster.cluster_id,
|
||||
cluster_handler.cluster.cluster_id,
|
||||
tsn,
|
||||
)
|
||||
return name
|
||||
|
||||
|
||||
def decorate_command(channel, command):
|
||||
def decorate_command(cluster_handler, command):
|
||||
"""Wrap a cluster command to make it safe."""
|
||||
|
||||
@wraps(command)
|
||||
async def wrapper(*args, **kwds):
|
||||
try:
|
||||
result = await command(*args, **kwds)
|
||||
channel.debug(
|
||||
cluster_handler.debug(
|
||||
"executed '%s' command with args: '%s' kwargs: '%s' result: %s",
|
||||
command.__name__,
|
||||
args,
|
||||
@ -90,7 +90,7 @@ def decorate_command(channel, command):
|
||||
return result
|
||||
|
||||
except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex:
|
||||
channel.debug(
|
||||
cluster_handler.debug(
|
||||
"command failed: '%s' args: '%s' kwargs '%s' exception: '%s'",
|
||||
command.__name__,
|
||||
args,
|
||||
@ -102,32 +102,32 @@ def decorate_command(channel, command):
|
||||
return wrapper
|
||||
|
||||
|
||||
class ChannelStatus(Enum):
|
||||
"""Status of a channel."""
|
||||
class ClusterHandlerStatus(Enum):
|
||||
"""Status of a cluster handler."""
|
||||
|
||||
CREATED = 1
|
||||
CONFIGURED = 2
|
||||
INITIALIZED = 3
|
||||
|
||||
|
||||
class ZigbeeChannel(LogMixin):
|
||||
"""Base channel for a Zigbee cluster."""
|
||||
class ClusterHandler(LogMixin):
|
||||
"""Base cluster handler for a Zigbee cluster."""
|
||||
|
||||
REPORT_CONFIG: tuple[AttrReportConfig, ...] = ()
|
||||
BIND: bool = True
|
||||
|
||||
# Dict of attributes to read on channel initialization.
|
||||
# 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[int | str, bool] = {}
|
||||
|
||||
def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None:
|
||||
"""Initialize ZigbeeChannel."""
|
||||
self._generic_id = f"channel_0x{cluster.cluster_id:04x}"
|
||||
self._ch_pool = ch_pool
|
||||
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"{ch_pool.id}:0x{cluster.cluster_id:04x}"
|
||||
unique_id = ch_pool.unique_id.replace("-", ":")
|
||||
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 | None = self.cluster.attributes_by_name.get(
|
||||
@ -137,28 +137,28 @@ class ZigbeeChannel(LogMixin):
|
||||
self.value_attribute = attr_def.id
|
||||
else:
|
||||
self.value_attribute = None
|
||||
self._status = ChannelStatus.CREATED
|
||||
self._status = ClusterHandlerStatus.CREATED
|
||||
self._cluster.add_listener(self)
|
||||
self.data_cache: dict[str, Enum] = {}
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
"""Return channel id unique for this device only."""
|
||||
"""Return cluster handler id unique for this device only."""
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def generic_id(self):
|
||||
"""Return the generic id for this channel."""
|
||||
"""Return the generic id for this cluster handler."""
|
||||
return self._generic_id
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique id for this channel."""
|
||||
"""Return the unique id for this cluster handler."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def cluster(self):
|
||||
"""Return the zigpy cluster for this channel."""
|
||||
"""Return the zigpy cluster for this cluster handler."""
|
||||
return self._cluster
|
||||
|
||||
@property
|
||||
@ -168,7 +168,7 @@ class ZigbeeChannel(LogMixin):
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""Return the status of the channel."""
|
||||
"""Return the status of the cluster handler."""
|
||||
return self._status
|
||||
|
||||
def __hash__(self) -> int:
|
||||
@ -178,7 +178,7 @@ class ZigbeeChannel(LogMixin):
|
||||
@callback
|
||||
def async_send_signal(self, signal: str, *args: Any) -> None:
|
||||
"""Send a signal through hass dispatcher."""
|
||||
self._ch_pool.async_send_signal(signal, *args)
|
||||
self._endpoint.async_send_signal(signal, *args)
|
||||
|
||||
async def bind(self):
|
||||
"""Bind a zigbee cluster.
|
||||
@ -190,11 +190,11 @@ class ZigbeeChannel(LogMixin):
|
||||
res = await self.cluster.bind()
|
||||
self.debug("bound '%s' cluster: %s", self.cluster.ep_attribute, res[0])
|
||||
async_dispatcher_send(
|
||||
self._ch_pool.hass,
|
||||
ZHA_CHANNEL_MSG,
|
||||
self._endpoint.device.hass,
|
||||
ZHA_CLUSTER_HANDLER_MSG,
|
||||
{
|
||||
ATTR_TYPE: ZHA_CHANNEL_MSG_BIND,
|
||||
ZHA_CHANNEL_MSG_DATA: {
|
||||
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,
|
||||
@ -206,11 +206,11 @@ class ZigbeeChannel(LogMixin):
|
||||
"Failed to bind '%s' cluster: %s", self.cluster.ep_attribute, str(ex)
|
||||
)
|
||||
async_dispatcher_send(
|
||||
self._ch_pool.hass,
|
||||
ZHA_CHANNEL_MSG,
|
||||
self._endpoint.device.hass,
|
||||
ZHA_CLUSTER_HANDLER_MSG,
|
||||
{
|
||||
ATTR_TYPE: ZHA_CHANNEL_MSG_BIND,
|
||||
ZHA_CHANNEL_MSG_DATA: {
|
||||
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,
|
||||
@ -226,8 +226,11 @@ class ZigbeeChannel(LogMixin):
|
||||
"""
|
||||
event_data = {}
|
||||
kwargs = {}
|
||||
if self.cluster.cluster_id >= 0xFC00 and self._ch_pool.manufacturer_code:
|
||||
kwargs["manufacturer"] = self._ch_pool.manufacturer_code
|
||||
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"]
|
||||
@ -272,11 +275,11 @@ class ZigbeeChannel(LogMixin):
|
||||
)
|
||||
|
||||
async_dispatcher_send(
|
||||
self._ch_pool.hass,
|
||||
ZHA_CHANNEL_MSG,
|
||||
self._endpoint.device.hass,
|
||||
ZHA_CLUSTER_HANDLER_MSG,
|
||||
{
|
||||
ATTR_TYPE: ZHA_CHANNEL_MSG_CFG_RPT,
|
||||
ZHA_CHANNEL_MSG_DATA: {
|
||||
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,
|
||||
@ -311,7 +314,6 @@ class ZigbeeChannel(LogMixin):
|
||||
for record in res
|
||||
if record.status != Status.SUCCESS
|
||||
]
|
||||
|
||||
self.debug(
|
||||
"Successfully configured reporting for '%s' on '%s' cluster",
|
||||
set(attrs) - set(failed),
|
||||
@ -326,43 +328,45 @@ class ZigbeeChannel(LogMixin):
|
||||
|
||||
async def async_configure(self) -> None:
|
||||
"""Set cluster binding and attribute reporting."""
|
||||
if not self._ch_pool.skip_configuration:
|
||||
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_channel_specific", None)
|
||||
ch_specific_cfg = getattr(
|
||||
self, "async_configure_cluster_handler_specific", None
|
||||
)
|
||||
if ch_specific_cfg:
|
||||
self.debug("Performing channel specific configuration")
|
||||
self.debug("Performing cluster handler specific configuration")
|
||||
await ch_specific_cfg()
|
||||
self.debug("finished channel configuration")
|
||||
self.debug("finished cluster handler configuration")
|
||||
else:
|
||||
self.debug("skipping channel configuration")
|
||||
self._status = ChannelStatus.CONFIGURED
|
||||
self.debug("skipping cluster handler configuration")
|
||||
self._status = ClusterHandlerStatus.CONFIGURED
|
||||
|
||||
@retryable_req(delays=(1, 1, 3))
|
||||
async def async_initialize(self, from_cache: bool) -> None:
|
||||
"""Initialize channel."""
|
||||
if not from_cache and self._ch_pool.skip_configuration:
|
||||
self.debug("Skipping channel initialization")
|
||||
self._status = ChannelStatus.INITIALIZED
|
||||
"""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 channel: from_cache: %s", from_cache)
|
||||
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 channel attributes: %s", 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 channel attributes: %s - from cache[%s]",
|
||||
"initializing uncached cluster handler attributes: %s - from cache[%s]",
|
||||
uncached,
|
||||
from_cache,
|
||||
)
|
||||
@ -370,13 +374,17 @@ class ZigbeeChannel(LogMixin):
|
||||
True, uncached, from_cache=from_cache, only_cache=from_cache
|
||||
)
|
||||
|
||||
ch_specific_init = getattr(self, "async_initialize_channel_specific", None)
|
||||
ch_specific_init = getattr(
|
||||
self, "async_initialize_cluster_handler_specific", None
|
||||
)
|
||||
if ch_specific_init:
|
||||
self.debug("Performing channel specific initialization: %s", uncached)
|
||||
self.debug(
|
||||
"Performing cluster handler specific initialization: %s", uncached
|
||||
)
|
||||
await ch_specific_init(from_cache=from_cache)
|
||||
|
||||
self.debug("finished channel initialization")
|
||||
self._status = ChannelStatus.INITIALIZED
|
||||
self.debug("finished cluster handler initialization")
|
||||
self._status = ClusterHandlerStatus.INITIALIZED
|
||||
|
||||
@callback
|
||||
def cluster_command(self, tsn, command_id, args):
|
||||
@ -411,13 +419,13 @@ class ZigbeeChannel(LogMixin):
|
||||
else:
|
||||
raise TypeError(f"Unexpected zha_send_event {command!r} argument: {arg!r}")
|
||||
|
||||
self._ch_pool.zha_send_event(
|
||||
self._endpoint.device.zha_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_ARGS: args, # type: ignore[dict-item]
|
||||
ATTR_PARAMS: params,
|
||||
}
|
||||
)
|
||||
@ -434,7 +442,7 @@ class ZigbeeChannel(LogMixin):
|
||||
async def get_attribute_value(self, attribute, from_cache=True):
|
||||
"""Get the value for an attribute."""
|
||||
manufacturer = None
|
||||
manufacturer_code = self._ch_pool.manufacturer_code
|
||||
manufacturer_code = self._endpoint.device.manufacturer_code
|
||||
if self.cluster.cluster_id >= 0xFC00 and manufacturer_code:
|
||||
manufacturer = manufacturer_code
|
||||
result = await safe_read(
|
||||
@ -455,11 +463,11 @@ class ZigbeeChannel(LogMixin):
|
||||
) -> dict[int | str, Any]:
|
||||
"""Get the values for a list of attributes."""
|
||||
manufacturer = None
|
||||
manufacturer_code = self._ch_pool.manufacturer_code
|
||||
manufacturer_code = self._endpoint.device.manufacturer_code
|
||||
if self.cluster.cluster_id >= 0xFC00 and manufacturer_code:
|
||||
manufacturer = manufacturer_code
|
||||
chunk = attributes[:ZHA_CHANNEL_READS_PER_REQ]
|
||||
rest = attributes[ZHA_CHANNEL_READS_PER_REQ:]
|
||||
chunk = attributes[:ZHA_CLUSTER_HANDLER_READS_PER_REQ]
|
||||
rest = attributes[ZHA_CLUSTER_HANDLER_READS_PER_REQ:]
|
||||
result = {}
|
||||
while chunk:
|
||||
try:
|
||||
@ -480,8 +488,8 @@ class ZigbeeChannel(LogMixin):
|
||||
)
|
||||
if raise_exceptions:
|
||||
raise
|
||||
chunk = rest[:ZHA_CHANNEL_READS_PER_REQ]
|
||||
rest = rest[ZHA_CHANNEL_READS_PER_REQ:]
|
||||
chunk = rest[:ZHA_CLUSTER_HANDLER_READS_PER_REQ]
|
||||
rest = rest[ZHA_CLUSTER_HANDLER_READS_PER_REQ:]
|
||||
return result
|
||||
|
||||
get_attributes = partialmethod(_get_attributes, False)
|
||||
@ -489,7 +497,7 @@ class ZigbeeChannel(LogMixin):
|
||||
def log(self, level, msg, *args, **kwargs):
|
||||
"""Log a message."""
|
||||
msg = f"[%s:%s]: {msg}"
|
||||
args = (self._ch_pool.nwk, self._id) + args
|
||||
args = (self._endpoint.device.nwk, self._id) + args
|
||||
_LOGGER.log(level, msg, *args, **kwargs)
|
||||
|
||||
def __getattr__(self, name):
|
||||
@ -501,31 +509,31 @@ class ZigbeeChannel(LogMixin):
|
||||
return self.__getattribute__(name)
|
||||
|
||||
|
||||
class ZDOChannel(LogMixin):
|
||||
"""Channel for ZDO events."""
|
||||
class ZDOClusterHandler(LogMixin):
|
||||
"""Cluster handler for ZDO events."""
|
||||
|
||||
def __init__(self, cluster, device):
|
||||
"""Initialize ZDOChannel."""
|
||||
self.name = CHANNEL_ZDO
|
||||
self._cluster = cluster
|
||||
def __init__(self, device):
|
||||
"""Initialize ZDOClusterHandler."""
|
||||
self.name = CLUSTER_HANDLER_ZDO
|
||||
self._cluster = device.device.endpoints[0]
|
||||
self._zha_device = device
|
||||
self._status = ChannelStatus.CREATED
|
||||
self._status = ClusterHandlerStatus.CREATED
|
||||
self._unique_id = f"{str(device.ieee)}:{device.name}_ZDO"
|
||||
self._cluster.add_listener(self)
|
||||
|
||||
@property
|
||||
def unique_id(self):
|
||||
"""Return the unique id for this channel."""
|
||||
"""Return the unique id for this cluster handler."""
|
||||
return self._unique_id
|
||||
|
||||
@property
|
||||
def cluster(self):
|
||||
"""Return the aigpy cluster for this channel."""
|
||||
"""Return the aigpy cluster for this cluster handler."""
|
||||
return self._cluster
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
"""Return the status of the channel."""
|
||||
"""Return the status of the cluster handler."""
|
||||
return self._status
|
||||
|
||||
@callback
|
||||
@ -537,12 +545,12 @@ class ZDOChannel(LogMixin):
|
||||
"""Permit handler."""
|
||||
|
||||
async def async_initialize(self, from_cache):
|
||||
"""Initialize channel."""
|
||||
self._status = ChannelStatus.INITIALIZED
|
||||
"""Initialize cluster handler."""
|
||||
self._status = ClusterHandlerStatus.INITIALIZED
|
||||
|
||||
async def async_configure(self):
|
||||
"""Configure channel."""
|
||||
self._status = ChannelStatus.CONFIGURED
|
||||
"""Configure cluster handler."""
|
||||
self._status = ClusterHandlerStatus.CONFIGURED
|
||||
|
||||
def log(self, level, msg, *args, **kwargs):
|
||||
"""Log a message."""
|
||||
@ -551,8 +559,8 @@ class ZDOChannel(LogMixin):
|
||||
_LOGGER.log(level, msg, *args, **kwargs)
|
||||
|
||||
|
||||
class ClientChannel(ZigbeeChannel):
|
||||
"""Channel listener for Zigbee client (output) clusters."""
|
||||
class ClientClusterHandler(ClusterHandler):
|
||||
"""ClusterHandler for Zigbee client (output) clusters."""
|
||||
|
||||
@callback
|
||||
def attribute_updated(self, attrid, value):
|
@ -1,16 +1,16 @@
|
||||
"""Closures channels module for Zigbee Home Automation."""
|
||||
"""Closures cluster handlers module for Zigbee Home Automation."""
|
||||
from zigpy.zcl.clusters import closures
|
||||
|
||||
from homeassistant.core import callback
|
||||
|
||||
from . import AttrReportConfig, ClientClusterHandler, ClusterHandler
|
||||
from .. import registries
|
||||
from ..const import REPORT_CONFIG_IMMEDIATE, SIGNAL_ATTR_UPDATED
|
||||
from .base import AttrReportConfig, ClientChannel, ZigbeeChannel
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(closures.DoorLock.cluster_id)
|
||||
class DoorLockChannel(ZigbeeChannel):
|
||||
"""Door lock channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(closures.DoorLock.cluster_id)
|
||||
class DoorLockClusterHandler(ClusterHandler):
|
||||
"""Door lock cluster handler."""
|
||||
|
||||
_value_attribute = 0
|
||||
REPORT_CONFIG = (
|
||||
@ -107,19 +107,19 @@ class DoorLockChannel(ZigbeeChannel):
|
||||
return result
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(closures.Shade.cluster_id)
|
||||
class Shade(ZigbeeChannel):
|
||||
"""Shade channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(closures.Shade.cluster_id)
|
||||
class Shade(ClusterHandler):
|
||||
"""Shade cluster handler."""
|
||||
|
||||
|
||||
@registries.CLIENT_CHANNELS_REGISTRY.register(closures.WindowCovering.cluster_id)
|
||||
class WindowCoveringClient(ClientChannel):
|
||||
"""Window client channel."""
|
||||
@registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(closures.WindowCovering.cluster_id)
|
||||
class WindowCoveringClient(ClientClusterHandler):
|
||||
"""Window client cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(closures.WindowCovering.cluster_id)
|
||||
class WindowCovering(ZigbeeChannel):
|
||||
"""Window channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(closures.WindowCovering.cluster_id)
|
||||
class WindowCovering(ClusterHandler):
|
||||
"""Window cluster handler."""
|
||||
|
||||
_value_attribute = 8
|
||||
REPORT_CONFIG = (
|
@ -1,4 +1,4 @@
|
||||
"""General channels module for Zigbee Home Automation."""
|
||||
"""General cluster handlers module for Zigbee Home Automation."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
@ -14,6 +14,12 @@ from zigpy.zcl.foundation import Status
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
from . import (
|
||||
AttrReportConfig,
|
||||
ClientClusterHandler,
|
||||
ClusterHandler,
|
||||
parse_and_log_command,
|
||||
)
|
||||
from .. import registries
|
||||
from ..const import (
|
||||
REPORT_CONFIG_ASAP,
|
||||
@ -27,21 +33,20 @@ from ..const import (
|
||||
SIGNAL_SET_LEVEL,
|
||||
SIGNAL_UPDATE_DEVICE,
|
||||
)
|
||||
from .base import AttrReportConfig, ClientChannel, ZigbeeChannel, parse_and_log_command
|
||||
from .helpers import is_hue_motion_sensor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ChannelPool
|
||||
from ..endpoint import Endpoint
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Alarms.cluster_id)
|
||||
class Alarms(ZigbeeChannel):
|
||||
"""Alarms channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.Alarms.cluster_id)
|
||||
class Alarms(ClusterHandler):
|
||||
"""Alarms cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogInput.cluster_id)
|
||||
class AnalogInput(ZigbeeChannel):
|
||||
"""Analog Input channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.AnalogInput.cluster_id)
|
||||
class AnalogInput(ClusterHandler):
|
||||
"""Analog Input cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT),
|
||||
@ -49,9 +54,9 @@ class AnalogInput(ZigbeeChannel):
|
||||
|
||||
|
||||
@registries.BINDABLE_CLUSTERS.register(general.AnalogOutput.cluster_id)
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogOutput.cluster_id)
|
||||
class AnalogOutput(ZigbeeChannel):
|
||||
"""Analog Output channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.AnalogOutput.cluster_id)
|
||||
class AnalogOutput(ClusterHandler):
|
||||
"""Analog Output cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT),
|
||||
@ -120,24 +125,26 @@ class AnalogOutput(ZigbeeChannel):
|
||||
return False
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogValue.cluster_id)
|
||||
class AnalogValue(ZigbeeChannel):
|
||||
"""Analog Value channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.AnalogValue.cluster_id)
|
||||
class AnalogValue(ClusterHandler):
|
||||
"""Analog Value cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT),
|
||||
)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.ApplianceControl.cluster_id)
|
||||
class ApplianceContorl(ZigbeeChannel):
|
||||
"""Appliance Control channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
general.ApplianceControl.cluster_id
|
||||
)
|
||||
class ApplianceContorl(ClusterHandler):
|
||||
"""Appliance Control cluster handler."""
|
||||
|
||||
|
||||
@registries.CHANNEL_ONLY_CLUSTERS.register(general.Basic.cluster_id)
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Basic.cluster_id)
|
||||
class BasicChannel(ZigbeeChannel):
|
||||
"""Channel to interact with the basic cluster."""
|
||||
@registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(general.Basic.cluster_id)
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.Basic.cluster_id)
|
||||
class BasicClusterHandler(ClusterHandler):
|
||||
"""Cluster handler to interact with the basic cluster."""
|
||||
|
||||
UNKNOWN = 0
|
||||
BATTERY = 3
|
||||
@ -153,9 +160,9 @@ class BasicChannel(ZigbeeChannel):
|
||||
6: "Emergency mains and transfer switch",
|
||||
}
|
||||
|
||||
def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None:
|
||||
"""Initialize Basic channel."""
|
||||
super().__init__(cluster, ch_pool)
|
||||
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 = ( # pylint: disable=invalid-name
|
||||
self.ZCL_INIT_ATTRS.copy()
|
||||
@ -169,41 +176,43 @@ class BasicChannel(ZigbeeChannel):
|
||||
self.ZCL_INIT_ATTRS["transmit_power"] = True
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryInput.cluster_id)
|
||||
class BinaryInput(ZigbeeChannel):
|
||||
"""Binary Input channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.BinaryInput.cluster_id)
|
||||
class BinaryInput(ClusterHandler):
|
||||
"""Binary Input cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT),
|
||||
)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryOutput.cluster_id)
|
||||
class BinaryOutput(ZigbeeChannel):
|
||||
"""Binary Output channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.BinaryOutput.cluster_id)
|
||||
class BinaryOutput(ClusterHandler):
|
||||
"""Binary Output cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT),
|
||||
)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryValue.cluster_id)
|
||||
class BinaryValue(ZigbeeChannel):
|
||||
"""Binary Value channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.BinaryValue.cluster_id)
|
||||
class BinaryValue(ClusterHandler):
|
||||
"""Binary Value cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT),
|
||||
)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Commissioning.cluster_id)
|
||||
class Commissioning(ZigbeeChannel):
|
||||
"""Commissioning channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.Commissioning.cluster_id)
|
||||
class Commissioning(ClusterHandler):
|
||||
"""Commissioning cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.DeviceTemperature.cluster_id)
|
||||
class DeviceTemperature(ZigbeeChannel):
|
||||
"""Device Temperature channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
general.DeviceTemperature.cluster_id
|
||||
)
|
||||
class DeviceTemperature(ClusterHandler):
|
||||
"""Device Temperature cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
{
|
||||
@ -213,23 +222,23 @@ class DeviceTemperature(ZigbeeChannel):
|
||||
)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.GreenPowerProxy.cluster_id)
|
||||
class GreenPowerProxy(ZigbeeChannel):
|
||||
"""Green Power Proxy channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.GreenPowerProxy.cluster_id)
|
||||
class GreenPowerProxy(ClusterHandler):
|
||||
"""Green Power Proxy cluster handler."""
|
||||
|
||||
BIND: bool = False
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Groups.cluster_id)
|
||||
class Groups(ZigbeeChannel):
|
||||
"""Groups channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.Groups.cluster_id)
|
||||
class Groups(ClusterHandler):
|
||||
"""Groups cluster handler."""
|
||||
|
||||
BIND: bool = False
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Identify.cluster_id)
|
||||
class Identify(ZigbeeChannel):
|
||||
"""Identify channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.Identify.cluster_id)
|
||||
class Identify(ClusterHandler):
|
||||
"""Identify cluster handler."""
|
||||
|
||||
BIND: bool = False
|
||||
|
||||
@ -242,15 +251,15 @@ class Identify(ZigbeeChannel):
|
||||
self.async_send_signal(f"{self.unique_id}_{cmd}", args[0])
|
||||
|
||||
|
||||
@registries.CLIENT_CHANNELS_REGISTRY.register(general.LevelControl.cluster_id)
|
||||
class LevelControlClientChannel(ClientChannel):
|
||||
@registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(general.LevelControl.cluster_id)
|
||||
class LevelControlClientClusterHandler(ClientClusterHandler):
|
||||
"""LevelControl client cluster."""
|
||||
|
||||
|
||||
@registries.BINDABLE_CLUSTERS.register(general.LevelControl.cluster_id)
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.LevelControl.cluster_id)
|
||||
class LevelControlChannel(ZigbeeChannel):
|
||||
"""Channel for the LevelControl Zigbee cluster."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.LevelControl.cluster_id)
|
||||
class LevelControlClusterHandler(ClusterHandler):
|
||||
"""Cluster handler for the LevelControl Zigbee cluster."""
|
||||
|
||||
CURRENT_LEVEL = 0
|
||||
REPORT_CONFIG = (AttrReportConfig(attr="current_level", config=REPORT_CONFIG_ASAP),)
|
||||
@ -299,42 +308,44 @@ class LevelControlChannel(ZigbeeChannel):
|
||||
self.async_send_signal(f"{self.unique_id}_{command}", level)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.MultistateInput.cluster_id)
|
||||
class MultistateInput(ZigbeeChannel):
|
||||
"""Multistate Input channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.MultistateInput.cluster_id)
|
||||
class MultistateInput(ClusterHandler):
|
||||
"""Multistate Input cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT),
|
||||
)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.MultistateOutput.cluster_id)
|
||||
class MultistateOutput(ZigbeeChannel):
|
||||
"""Multistate Output channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
general.MultistateOutput.cluster_id
|
||||
)
|
||||
class MultistateOutput(ClusterHandler):
|
||||
"""Multistate Output cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT),
|
||||
)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.MultistateValue.cluster_id)
|
||||
class MultistateValue(ZigbeeChannel):
|
||||
"""Multistate Value channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.MultistateValue.cluster_id)
|
||||
class MultistateValue(ClusterHandler):
|
||||
"""Multistate Value cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT),
|
||||
)
|
||||
|
||||
|
||||
@registries.CLIENT_CHANNELS_REGISTRY.register(general.OnOff.cluster_id)
|
||||
class OnOffClientChannel(ClientChannel):
|
||||
"""OnOff client channel."""
|
||||
@registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(general.OnOff.cluster_id)
|
||||
class OnOffClientClusterHandler(ClientClusterHandler):
|
||||
"""OnOff client cluster handler."""
|
||||
|
||||
|
||||
@registries.BINDABLE_CLUSTERS.register(general.OnOff.cluster_id)
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.OnOff.cluster_id)
|
||||
class OnOffChannel(ZigbeeChannel):
|
||||
"""Channel for the OnOff Zigbee cluster."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.OnOff.cluster_id)
|
||||
class OnOffClusterHandler(ClusterHandler):
|
||||
"""Cluster handler for the OnOff Zigbee cluster."""
|
||||
|
||||
ON_OFF = 0
|
||||
REPORT_CONFIG = (AttrReportConfig(attr="on_off", config=REPORT_CONFIG_IMMEDIATE),)
|
||||
@ -342,9 +353,9 @@ class OnOffChannel(ZigbeeChannel):
|
||||
"start_up_on_off": True,
|
||||
}
|
||||
|
||||
def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None:
|
||||
"""Initialize OnOffChannel."""
|
||||
super().__init__(cluster, ch_pool)
|
||||
def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None:
|
||||
"""Initialize OnOffClusterHandler."""
|
||||
super().__init__(cluster, endpoint)
|
||||
self._off_listener = None
|
||||
|
||||
if self.cluster.endpoint.model in (
|
||||
@ -404,7 +415,7 @@ class OnOffChannel(ZigbeeChannel):
|
||||
self.cluster.update_attribute(self.ON_OFF, t.Bool.true)
|
||||
if on_time > 0:
|
||||
self._off_listener = async_call_later(
|
||||
self._ch_pool.hass,
|
||||
self._endpoint.device.hass,
|
||||
(on_time / 10), # value is in 10ths of a second
|
||||
self.set_to_off,
|
||||
)
|
||||
@ -426,24 +437,26 @@ class OnOffChannel(ZigbeeChannel):
|
||||
)
|
||||
|
||||
async def async_update(self):
|
||||
"""Initialize channel."""
|
||||
"""Initialize cluster handler."""
|
||||
if self.cluster.is_client:
|
||||
return
|
||||
from_cache = not self._ch_pool.is_mains_powered
|
||||
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(self.ON_OFF, from_cache=from_cache)
|
||||
await super().async_update()
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.OnOffConfiguration.cluster_id)
|
||||
class OnOffConfiguration(ZigbeeChannel):
|
||||
"""OnOff Configuration channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
general.OnOffConfiguration.cluster_id
|
||||
)
|
||||
class OnOffConfiguration(ClusterHandler):
|
||||
"""OnOff Configuration cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Ota.cluster_id)
|
||||
@registries.CLIENT_CHANNELS_REGISTRY.register(general.Ota.cluster_id)
|
||||
class Ota(ClientChannel):
|
||||
"""OTA Channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.Ota.cluster_id)
|
||||
@registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(general.Ota.cluster_id)
|
||||
class Ota(ClientClusterHandler):
|
||||
"""OTA cluster handler."""
|
||||
|
||||
BIND: bool = False
|
||||
|
||||
@ -457,21 +470,21 @@ class Ota(ClientChannel):
|
||||
else:
|
||||
cmd_name = command_id
|
||||
|
||||
signal_id = self._ch_pool.unique_id.split("-")[0]
|
||||
signal_id = self._endpoint.unique_id.split("-")[0]
|
||||
if cmd_name == "query_next_image":
|
||||
assert args
|
||||
self.async_send_signal(SIGNAL_UPDATE_DEVICE.format(signal_id), args[3])
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Partition.cluster_id)
|
||||
class Partition(ZigbeeChannel):
|
||||
"""Partition channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.Partition.cluster_id)
|
||||
class Partition(ClusterHandler):
|
||||
"""Partition cluster handler."""
|
||||
|
||||
|
||||
@registries.CHANNEL_ONLY_CLUSTERS.register(general.PollControl.cluster_id)
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PollControl.cluster_id)
|
||||
class PollControl(ZigbeeChannel):
|
||||
"""Poll Control channel."""
|
||||
@registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(general.PollControl.cluster_id)
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.PollControl.cluster_id)
|
||||
class PollControl(ClusterHandler):
|
||||
"""Poll Control cluster handler."""
|
||||
|
||||
CHECKIN_INTERVAL = 55 * 60 * 4 # 55min
|
||||
CHECKIN_FAST_POLL_TIMEOUT = 2 * 4 # 2s
|
||||
@ -480,8 +493,8 @@ class PollControl(ZigbeeChannel):
|
||||
4476,
|
||||
} # IKEA
|
||||
|
||||
async def async_configure_channel_specific(self) -> None:
|
||||
"""Configure channel: set check-in interval."""
|
||||
async def async_configure_cluster_handler_specific(self) -> None:
|
||||
"""Configure cluster handler: set check-in interval."""
|
||||
try:
|
||||
res = await self.cluster.write_attributes(
|
||||
{"checkin_interval": self.CHECKIN_INTERVAL}
|
||||
@ -508,7 +521,7 @@ class PollControl(ZigbeeChannel):
|
||||
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._ch_pool.manufacturer_code not in self._IGNORED_MANUFACTURER_ID:
|
||||
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()
|
||||
|
||||
@ -518,9 +531,11 @@ class PollControl(ZigbeeChannel):
|
||||
self._IGNORED_MANUFACTURER_ID.add(manufacturer_code)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PowerConfiguration.cluster_id)
|
||||
class PowerConfigurationChannel(ZigbeeChannel):
|
||||
"""Channel for the zigbee power configuration cluster."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
general.PowerConfiguration.cluster_id
|
||||
)
|
||||
class PowerConfigurationCLusterHandler(ClusterHandler):
|
||||
"""Cluster handler for the zigbee power configuration cluster."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(attr="battery_voltage", config=REPORT_CONFIG_BATTERY_SAVE),
|
||||
@ -529,8 +544,8 @@ class PowerConfigurationChannel(ZigbeeChannel):
|
||||
),
|
||||
)
|
||||
|
||||
def async_initialize_channel_specific(self, from_cache: bool) -> Coroutine:
|
||||
"""Initialize channel specific attrs."""
|
||||
def async_initialize_cluster_handler_specific(self, from_cache: bool) -> Coroutine:
|
||||
"""Initialize cluster handler specific attrs."""
|
||||
attributes = [
|
||||
"battery_size",
|
||||
"battery_quantity",
|
||||
@ -540,26 +555,26 @@ class PowerConfigurationChannel(ZigbeeChannel):
|
||||
)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PowerProfile.cluster_id)
|
||||
class PowerProfile(ZigbeeChannel):
|
||||
"""Power Profile channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.PowerProfile.cluster_id)
|
||||
class PowerProfile(ClusterHandler):
|
||||
"""Power Profile cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.RSSILocation.cluster_id)
|
||||
class RSSILocation(ZigbeeChannel):
|
||||
"""RSSI Location channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.RSSILocation.cluster_id)
|
||||
class RSSILocation(ClusterHandler):
|
||||
"""RSSI Location cluster handler."""
|
||||
|
||||
|
||||
@registries.CLIENT_CHANNELS_REGISTRY.register(general.Scenes.cluster_id)
|
||||
class ScenesClientChannel(ClientChannel):
|
||||
"""Scenes channel."""
|
||||
@registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(general.Scenes.cluster_id)
|
||||
class ScenesClientClusterHandler(ClientClusterHandler):
|
||||
"""Scenes cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Scenes.cluster_id)
|
||||
class Scenes(ZigbeeChannel):
|
||||
"""Scenes channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.Scenes.cluster_id)
|
||||
class Scenes(ClusterHandler):
|
||||
"""Scenes cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Time.cluster_id)
|
||||
class Time(ZigbeeChannel):
|
||||
"""Time channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.Time.cluster_id)
|
||||
class Time(ClusterHandler):
|
||||
"""Time cluster handler."""
|
@ -0,0 +1,15 @@
|
||||
"""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",
|
||||
)
|
@ -1,53 +1,55 @@
|
||||
"""Home automation channels module for Zigbee Home Automation."""
|
||||
"""Home automation cluster handlers module for Zigbee Home Automation."""
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
|
||||
from zigpy.zcl.clusters import homeautomation
|
||||
|
||||
from . import AttrReportConfig, ClusterHandler
|
||||
from .. import registries
|
||||
from ..const import (
|
||||
CHANNEL_ELECTRICAL_MEASUREMENT,
|
||||
CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT,
|
||||
REPORT_CONFIG_DEFAULT,
|
||||
REPORT_CONFIG_OP,
|
||||
SIGNAL_ATTR_UPDATED,
|
||||
)
|
||||
from .base import AttrReportConfig, ZigbeeChannel
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
homeautomation.ApplianceEventAlerts.cluster_id
|
||||
)
|
||||
class ApplianceEventAlerts(ZigbeeChannel):
|
||||
"""Appliance Event Alerts channel."""
|
||||
class ApplianceEventAlerts(ClusterHandler):
|
||||
"""Appliance Event Alerts cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
homeautomation.ApplianceIdentification.cluster_id
|
||||
)
|
||||
class ApplianceIdentification(ZigbeeChannel):
|
||||
"""Appliance Identification channel."""
|
||||
class ApplianceIdentification(ClusterHandler):
|
||||
"""Appliance Identification cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
homeautomation.ApplianceStatistics.cluster_id
|
||||
)
|
||||
class ApplianceStatistics(ZigbeeChannel):
|
||||
"""Appliance Statistics channel."""
|
||||
class ApplianceStatistics(ClusterHandler):
|
||||
"""Appliance Statistics cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(homeautomation.Diagnostic.cluster_id)
|
||||
class Diagnostic(ZigbeeChannel):
|
||||
"""Diagnostic channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
homeautomation.Diagnostic.cluster_id
|
||||
)
|
||||
class Diagnostic(ClusterHandler):
|
||||
"""Diagnostic cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
homeautomation.ElectricalMeasurement.cluster_id
|
||||
)
|
||||
class ElectricalMeasurementChannel(ZigbeeChannel):
|
||||
"""Channel that polls active power level."""
|
||||
class ElectricalMeasurementClusterHandler(ClusterHandler):
|
||||
"""Cluster handler that polls active power level."""
|
||||
|
||||
CHANNEL_NAME = CHANNEL_ELECTRICAL_MEASUREMENT
|
||||
CLUSTER_HANDLER_NAME = CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT
|
||||
|
||||
class MeasurementType(enum.IntFlag):
|
||||
"""Measurement types."""
|
||||
@ -91,7 +93,7 @@ class ElectricalMeasurementChannel(ZigbeeChannel):
|
||||
"""Retrieve latest state."""
|
||||
self.debug("async_update")
|
||||
|
||||
# This is a polling channel. Don't allow cache.
|
||||
# This is a polling cluster handler. Don't allow cache.
|
||||
attrs = [
|
||||
a["attr"]
|
||||
for a in self.REPORT_CONFIG
|
||||
@ -165,8 +167,8 @@ class ElectricalMeasurementChannel(ZigbeeChannel):
|
||||
)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
homeautomation.MeterIdentification.cluster_id
|
||||
)
|
||||
class MeterIdentification(ZigbeeChannel):
|
||||
"""Metering Identification channel."""
|
||||
class MeterIdentification(ClusterHandler):
|
||||
"""Metering Identification cluster handler."""
|
@ -1,4 +1,4 @@
|
||||
"""HVAC channels module for Zigbee Home Automation.
|
||||
"""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/
|
||||
@ -14,6 +14,7 @@ from zigpy.zcl.foundation import Status
|
||||
|
||||
from homeassistant.core import callback
|
||||
|
||||
from . import AttrReportConfig, ClusterHandler
|
||||
from .. import registries
|
||||
from ..const import (
|
||||
REPORT_CONFIG_MAX_INT,
|
||||
@ -21,7 +22,6 @@ from ..const import (
|
||||
REPORT_CONFIG_OP,
|
||||
SIGNAL_ATTR_UPDATED,
|
||||
)
|
||||
from .base import AttrReportConfig, ZigbeeChannel
|
||||
|
||||
AttributeUpdateRecord = namedtuple("AttributeUpdateRecord", "attr_id, attr_name, value")
|
||||
REPORT_CONFIG_CLIMATE = (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 25)
|
||||
@ -29,14 +29,14 @@ 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_CHANNEL_REGISTRY.register(hvac.Dehumidification.cluster_id)
|
||||
class Dehumidification(ZigbeeChannel):
|
||||
"""Dehumidification channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(hvac.Dehumidification.cluster_id)
|
||||
class Dehumidification(ClusterHandler):
|
||||
"""Dehumidification cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.Fan.cluster_id)
|
||||
class FanChannel(ZigbeeChannel):
|
||||
"""Fan channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(hvac.Fan.cluster_id)
|
||||
class FanClusterHandler(ClusterHandler):
|
||||
"""Fan cluster handler."""
|
||||
|
||||
_value_attribute = 0
|
||||
|
||||
@ -79,14 +79,14 @@ class FanChannel(ZigbeeChannel):
|
||||
)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.Pump.cluster_id)
|
||||
class Pump(ZigbeeChannel):
|
||||
"""Pump channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(hvac.Pump.cluster_id)
|
||||
class Pump(ClusterHandler):
|
||||
"""Pump cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.Thermostat.cluster_id)
|
||||
class ThermostatChannel(ZigbeeChannel):
|
||||
"""Thermostat channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(hvac.Thermostat.cluster_id)
|
||||
class ThermostatClusterHandler(ClusterHandler):
|
||||
"""Thermostat cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(attr="local_temperature", config=REPORT_CONFIG_CLIMATE),
|
||||
@ -314,6 +314,6 @@ class ThermostatChannel(ZigbeeChannel):
|
||||
return all(record.status == Status.SUCCESS for record in res[0])
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.UserInterface.cluster_id)
|
||||
class UserInterface(ZigbeeChannel):
|
||||
"""User interface (thermostat) channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(hvac.UserInterface.cluster_id)
|
||||
class UserInterface(ClusterHandler):
|
||||
"""User interface (thermostat) cluster handler."""
|
@ -1,29 +1,29 @@
|
||||
"""Lighting channels module for Zigbee Home Automation."""
|
||||
"""Lighting cluster handlers module for Zigbee Home Automation."""
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import cached_property
|
||||
|
||||
from zigpy.zcl.clusters import lighting
|
||||
|
||||
from . import AttrReportConfig, ClientClusterHandler, ClusterHandler
|
||||
from .. import registries
|
||||
from ..const import REPORT_CONFIG_DEFAULT
|
||||
from .base import AttrReportConfig, ClientChannel, ZigbeeChannel
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(lighting.Ballast.cluster_id)
|
||||
class Ballast(ZigbeeChannel):
|
||||
"""Ballast channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(lighting.Ballast.cluster_id)
|
||||
class Ballast(ClusterHandler):
|
||||
"""Ballast cluster handler."""
|
||||
|
||||
|
||||
@registries.CLIENT_CHANNELS_REGISTRY.register(lighting.Color.cluster_id)
|
||||
class ColorClientChannel(ClientChannel):
|
||||
"""Color client channel."""
|
||||
@registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(lighting.Color.cluster_id)
|
||||
class ColorClientClusterHandler(ClientClusterHandler):
|
||||
"""Color client cluster handler."""
|
||||
|
||||
|
||||
@registries.BINDABLE_CLUSTERS.register(lighting.Color.cluster_id)
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(lighting.Color.cluster_id)
|
||||
class ColorChannel(ZigbeeChannel):
|
||||
"""Color channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(lighting.Color.cluster_id)
|
||||
class ColorClusterHandler(ClusterHandler):
|
||||
"""Color cluster handler."""
|
||||
|
||||
CAPABILITIES_COLOR_XY = 0x08
|
||||
CAPABILITIES_COLOR_TEMP = 0x10
|
||||
@ -98,7 +98,7 @@ class ColorChannel(ZigbeeChannel):
|
||||
|
||||
@property
|
||||
def min_mireds(self) -> int:
|
||||
"""Return the coldest color_temp that this channel supports."""
|
||||
"""Return the coldest color_temp that this cluster handler supports."""
|
||||
min_mireds = self.cluster.get("color_temp_physical_min", self.MIN_MIREDS)
|
||||
if min_mireds == 0:
|
||||
self.warning(
|
||||
@ -113,7 +113,7 @@ class ColorChannel(ZigbeeChannel):
|
||||
|
||||
@property
|
||||
def max_mireds(self) -> int:
|
||||
"""Return the warmest color_temp that this channel supports."""
|
||||
"""Return the warmest color_temp that this cluster handler supports."""
|
||||
max_mireds = self.cluster.get("color_temp_physical_max", self.MAX_MIREDS)
|
||||
if max_mireds == 0:
|
||||
self.warning(
|
||||
@ -128,7 +128,7 @@ class ColorChannel(ZigbeeChannel):
|
||||
|
||||
@property
|
||||
def hs_supported(self) -> bool:
|
||||
"""Return True if the channel supports hue and saturation."""
|
||||
"""Return True if the cluster handler supports hue and saturation."""
|
||||
return (
|
||||
self.color_capabilities is not None
|
||||
and lighting.Color.ColorCapabilities.Hue_and_saturation
|
||||
@ -137,7 +137,7 @@ class ColorChannel(ZigbeeChannel):
|
||||
|
||||
@property
|
||||
def enhanced_hue_supported(self) -> bool:
|
||||
"""Return True if the channel supports enhanced hue and saturation."""
|
||||
"""Return True if the cluster handler supports enhanced hue and saturation."""
|
||||
return (
|
||||
self.color_capabilities is not None
|
||||
and lighting.Color.ColorCapabilities.Enhanced_hue in self.color_capabilities
|
||||
@ -145,7 +145,7 @@ class ColorChannel(ZigbeeChannel):
|
||||
|
||||
@property
|
||||
def xy_supported(self) -> bool:
|
||||
"""Return True if the channel supports xy."""
|
||||
"""Return True if the cluster handler supports xy."""
|
||||
return (
|
||||
self.color_capabilities is not None
|
||||
and lighting.Color.ColorCapabilities.XY_attributes
|
||||
@ -154,7 +154,7 @@ class ColorChannel(ZigbeeChannel):
|
||||
|
||||
@property
|
||||
def color_temp_supported(self) -> bool:
|
||||
"""Return True if the channel supports color temperature."""
|
||||
"""Return True if the cluster handler supports color temperature."""
|
||||
return (
|
||||
self.color_capabilities is not None
|
||||
and lighting.Color.ColorCapabilities.Color_temperature
|
||||
@ -163,7 +163,7 @@ class ColorChannel(ZigbeeChannel):
|
||||
|
||||
@property
|
||||
def color_loop_supported(self) -> bool:
|
||||
"""Return True if the channel supports color loop."""
|
||||
"""Return True if the cluster handler supports color loop."""
|
||||
return (
|
||||
self.color_capabilities is not None
|
||||
and lighting.Color.ColorCapabilities.Color_loop in self.color_capabilities
|
||||
@ -171,10 +171,10 @@ class ColorChannel(ZigbeeChannel):
|
||||
|
||||
@property
|
||||
def options(self) -> lighting.Color.Options:
|
||||
"""Return ZCL options of the channel."""
|
||||
"""Return ZCL options of the cluster handler."""
|
||||
return lighting.Color.Options(self.cluster.get("options", 0))
|
||||
|
||||
@property
|
||||
def execute_if_off_supported(self) -> bool:
|
||||
"""Return True if the channel can execute commands when off."""
|
||||
"""Return True if the cluster handler can execute commands when off."""
|
||||
return lighting.Color.Options.Execute_if_off in self.options
|
@ -1,29 +1,29 @@
|
||||
"""Lightlink channels module for Zigbee Home Automation."""
|
||||
"""Lightlink cluster handlers module for Zigbee Home Automation."""
|
||||
import asyncio
|
||||
|
||||
import zigpy.exceptions
|
||||
from zigpy.zcl.clusters import lightlink
|
||||
from zigpy.zcl.foundation import GENERAL_COMMANDS, GeneralCommand
|
||||
|
||||
from . import ClusterHandler, ClusterHandlerStatus
|
||||
from .. import registries
|
||||
from .base import ChannelStatus, ZigbeeChannel
|
||||
|
||||
|
||||
@registries.CHANNEL_ONLY_CLUSTERS.register(lightlink.LightLink.cluster_id)
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(lightlink.LightLink.cluster_id)
|
||||
class LightLink(ZigbeeChannel):
|
||||
"""Lightlink channel."""
|
||||
@registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(lightlink.LightLink.cluster_id)
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(lightlink.LightLink.cluster_id)
|
||||
class LightLink(ClusterHandler):
|
||||
"""Lightlink cluster handler."""
|
||||
|
||||
BIND: bool = False
|
||||
|
||||
async def async_configure(self) -> None:
|
||||
"""Add Coordinator to LightLink group."""
|
||||
|
||||
if self._ch_pool.skip_configuration:
|
||||
self._status = ChannelStatus.CONFIGURED
|
||||
if self._endpoint.device.skip_configuration:
|
||||
self._status = ClusterHandlerStatus.CONFIGURED
|
||||
return
|
||||
|
||||
application = self._ch_pool.endpoint.device.application
|
||||
application = self._endpoint.zigpy_endpoint.device.application
|
||||
try:
|
||||
coordinator = application.get_device(application.state.node_info.ieee)
|
||||
except KeyError:
|
@ -1,4 +1,4 @@
|
||||
"""Manufacturer specific channels module for Zigbee Home Automation."""
|
||||
"""Manufacturer specific cluster handlers module for Zigbee Home Automation."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
@ -10,6 +10,7 @@ import zigpy.zcl
|
||||
|
||||
from homeassistant.core import callback
|
||||
|
||||
from . import AttrReportConfig, ClientClusterHandler, ClusterHandler
|
||||
from .. import registries
|
||||
from ..const import (
|
||||
ATTR_ATTRIBUTE_ID,
|
||||
@ -23,17 +24,18 @@ from ..const import (
|
||||
SIGNAL_ATTR_UPDATED,
|
||||
UNKNOWN,
|
||||
)
|
||||
from .base import AttrReportConfig, ClientChannel, ZigbeeChannel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ChannelPool
|
||||
from ..endpoint import Endpoint
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(registries.SMARTTHINGS_HUMIDITY_CLUSTER)
|
||||
class SmartThingsHumidity(ZigbeeChannel):
|
||||
"""Smart Things Humidity channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
registries.SMARTTHINGS_HUMIDITY_CLUSTER
|
||||
)
|
||||
class SmartThingsHumidity(ClusterHandler):
|
||||
"""Smart Things Humidity cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
{
|
||||
@ -43,32 +45,34 @@ class SmartThingsHumidity(ZigbeeChannel):
|
||||
)
|
||||
|
||||
|
||||
@registries.CHANNEL_ONLY_CLUSTERS.register(0xFD00)
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(0xFD00)
|
||||
class OsramButton(ZigbeeChannel):
|
||||
"""Osram button channel."""
|
||||
@registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(0xFD00)
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(0xFD00)
|
||||
class OsramButton(ClusterHandler):
|
||||
"""Osram button cluster handler."""
|
||||
|
||||
REPORT_CONFIG = ()
|
||||
|
||||
|
||||
@registries.CHANNEL_ONLY_CLUSTERS.register(registries.PHILLIPS_REMOTE_CLUSTER)
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(registries.PHILLIPS_REMOTE_CLUSTER)
|
||||
class PhillipsRemote(ZigbeeChannel):
|
||||
"""Phillips remote channel."""
|
||||
@registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(registries.PHILLIPS_REMOTE_CLUSTER)
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(registries.PHILLIPS_REMOTE_CLUSTER)
|
||||
class PhillipsRemote(ClusterHandler):
|
||||
"""Phillips remote cluster handler."""
|
||||
|
||||
REPORT_CONFIG = ()
|
||||
|
||||
|
||||
@registries.CHANNEL_ONLY_CLUSTERS.register(registries.TUYA_MANUFACTURER_CLUSTER)
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(registries.TUYA_MANUFACTURER_CLUSTER)
|
||||
class TuyaChannel(ZigbeeChannel):
|
||||
"""Channel for the Tuya manufacturer Zigbee cluster."""
|
||||
@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, ch_pool: ChannelPool) -> None:
|
||||
"""Initialize TuyaChannel."""
|
||||
super().__init__(cluster, ch_pool)
|
||||
def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None:
|
||||
"""Initialize TuyaClusterHandler."""
|
||||
super().__init__(cluster, endpoint)
|
||||
|
||||
if self.cluster.endpoint.manufacturer in (
|
||||
"_TZE200_7tdtqgwv",
|
||||
@ -94,16 +98,16 @@ class TuyaChannel(ZigbeeChannel):
|
||||
}
|
||||
|
||||
|
||||
@registries.CHANNEL_ONLY_CLUSTERS.register(0xFCC0)
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(0xFCC0)
|
||||
class OppleRemote(ZigbeeChannel):
|
||||
"""Opple channel."""
|
||||
@registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(0xFCC0)
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(0xFCC0)
|
||||
class OppleRemote(ClusterHandler):
|
||||
"""Opple cluster handler."""
|
||||
|
||||
REPORT_CONFIG = ()
|
||||
|
||||
def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None:
|
||||
"""Initialize Opple channel."""
|
||||
super().__init__(cluster, ch_pool)
|
||||
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 = { # pylint: disable=invalid-name
|
||||
"detection_interval": True,
|
||||
@ -162,8 +166,8 @@ class OppleRemote(ZigbeeChannel):
|
||||
"linkage_alarm": True,
|
||||
}
|
||||
|
||||
async def async_initialize_channel_specific(self, from_cache: bool) -> None:
|
||||
"""Initialize channel specific."""
|
||||
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:
|
||||
@ -171,11 +175,11 @@ class OppleRemote(ZigbeeChannel):
|
||||
self.cluster.endpoint.ias_zone.reset_s = int(interval)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
registries.SMARTTHINGS_ACCELERATION_CLUSTER
|
||||
)
|
||||
class SmartThingsAcceleration(ZigbeeChannel):
|
||||
"""Smart Things Acceleration channel."""
|
||||
class SmartThingsAcceleration(ClusterHandler):
|
||||
"""Smart Things Acceleration cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(attr="acceleration", config=REPORT_CONFIG_ASAP),
|
||||
@ -211,9 +215,9 @@ class SmartThingsAcceleration(ZigbeeChannel):
|
||||
)
|
||||
|
||||
|
||||
@registries.CLIENT_CHANNELS_REGISTRY.register(0xFC31)
|
||||
class InovelliNotificationChannel(ClientChannel):
|
||||
"""Inovelli Notification channel."""
|
||||
@registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(0xFC31)
|
||||
class InovelliNotificationClusterHandler(ClientClusterHandler):
|
||||
"""Inovelli Notification cluster handler."""
|
||||
|
||||
@callback
|
||||
def attribute_updated(self, attrid, value):
|
||||
@ -224,9 +228,9 @@ class InovelliNotificationChannel(ClientChannel):
|
||||
"""Handle a cluster command received on this cluster."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(0xFC31)
|
||||
class InovelliConfigEntityChannel(ZigbeeChannel):
|
||||
"""Inovelli Configuration Entity channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(0xFC31)
|
||||
class InovelliConfigEntityClusterHandler(ClusterHandler):
|
||||
"""Inovelli Configuration Entity cluster handler."""
|
||||
|
||||
REPORT_CONFIG = ()
|
||||
ZCL_INIT_ATTRS = {
|
||||
@ -307,10 +311,12 @@ class InovelliConfigEntityChannel(ZigbeeChannel):
|
||||
)
|
||||
|
||||
|
||||
@registries.CHANNEL_ONLY_CLUSTERS.register(registries.IKEA_AIR_PURIFIER_CLUSTER)
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(registries.IKEA_AIR_PURIFIER_CLUSTER)
|
||||
class IkeaAirPurifierChannel(ZigbeeChannel):
|
||||
"""IKEA Air Purifier channel."""
|
||||
@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),
|
||||
@ -360,9 +366,9 @@ class IkeaAirPurifierChannel(ZigbeeChannel):
|
||||
)
|
||||
|
||||
|
||||
@registries.CHANNEL_ONLY_CLUSTERS.register(0xFC80)
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(0xFC80)
|
||||
class IkeaRemote(ZigbeeChannel):
|
||||
"""Ikea Matter remote channel."""
|
||||
@registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(0xFC80)
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(0xFC80)
|
||||
class IkeaRemote(ClusterHandler):
|
||||
"""Ikea Matter remote cluster handler."""
|
||||
|
||||
REPORT_CONFIG = ()
|
@ -1,4 +1,4 @@
|
||||
"""Measurement channels module for Zigbee Home Automation."""
|
||||
"""Measurement cluster handlers module for Zigbee Home Automation."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
@ -6,6 +6,7 @@ from typing import TYPE_CHECKING
|
||||
import zigpy.zcl
|
||||
from zigpy.zcl.clusters import measurement
|
||||
|
||||
from . import AttrReportConfig, ClusterHandler
|
||||
from .. import registries
|
||||
from ..const import (
|
||||
REPORT_CONFIG_DEFAULT,
|
||||
@ -13,55 +14,58 @@ from ..const import (
|
||||
REPORT_CONFIG_MAX_INT,
|
||||
REPORT_CONFIG_MIN_INT,
|
||||
)
|
||||
from .base import AttrReportConfig, ZigbeeChannel
|
||||
from .helpers import is_hue_motion_sensor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ChannelPool
|
||||
from ..endpoint import Endpoint
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.FlowMeasurement.cluster_id)
|
||||
class FlowMeasurement(ZigbeeChannel):
|
||||
"""Flow Measurement channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
measurement.FlowMeasurement.cluster_id
|
||||
)
|
||||
class FlowMeasurement(ClusterHandler):
|
||||
"""Flow Measurement cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(attr="measured_value", config=REPORT_CONFIG_DEFAULT),
|
||||
)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
measurement.IlluminanceLevelSensing.cluster_id
|
||||
)
|
||||
class IlluminanceLevelSensing(ZigbeeChannel):
|
||||
"""Illuminance Level Sensing channel."""
|
||||
class IlluminanceLevelSensing(ClusterHandler):
|
||||
"""Illuminance Level Sensing cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(attr="level_status", config=REPORT_CONFIG_DEFAULT),
|
||||
)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
measurement.IlluminanceMeasurement.cluster_id
|
||||
)
|
||||
class IlluminanceMeasurement(ZigbeeChannel):
|
||||
"""Illuminance Measurement channel."""
|
||||
class IlluminanceMeasurement(ClusterHandler):
|
||||
"""Illuminance Measurement cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(attr="measured_value", config=REPORT_CONFIG_DEFAULT),
|
||||
)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.OccupancySensing.cluster_id)
|
||||
class OccupancySensing(ZigbeeChannel):
|
||||
"""Occupancy Sensing channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
measurement.OccupancySensing.cluster_id
|
||||
)
|
||||
class OccupancySensing(ClusterHandler):
|
||||
"""Occupancy Sensing cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(attr="occupancy", config=REPORT_CONFIG_IMMEDIATE),
|
||||
)
|
||||
|
||||
def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None:
|
||||
"""Initialize Occupancy channel."""
|
||||
super().__init__(cluster, ch_pool)
|
||||
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 = ( # pylint: disable=invalid-name
|
||||
self.ZCL_INIT_ATTRS.copy()
|
||||
@ -69,18 +73,22 @@ class OccupancySensing(ZigbeeChannel):
|
||||
self.ZCL_INIT_ATTRS["sensitivity"] = True
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.PressureMeasurement.cluster_id)
|
||||
class PressureMeasurement(ZigbeeChannel):
|
||||
"""Pressure measurement channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
measurement.PressureMeasurement.cluster_id
|
||||
)
|
||||
class PressureMeasurement(ClusterHandler):
|
||||
"""Pressure measurement cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(attr="measured_value", config=REPORT_CONFIG_DEFAULT),
|
||||
)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.RelativeHumidity.cluster_id)
|
||||
class RelativeHumidity(ZigbeeChannel):
|
||||
"""Relative Humidity measurement channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
measurement.RelativeHumidity.cluster_id
|
||||
)
|
||||
class RelativeHumidity(ClusterHandler):
|
||||
"""Relative Humidity measurement cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(
|
||||
@ -90,9 +98,11 @@ class RelativeHumidity(ZigbeeChannel):
|
||||
)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.SoilMoisture.cluster_id)
|
||||
class SoilMoisture(ZigbeeChannel):
|
||||
"""Soil Moisture measurement channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
measurement.SoilMoisture.cluster_id
|
||||
)
|
||||
class SoilMoisture(ClusterHandler):
|
||||
"""Soil Moisture measurement cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(
|
||||
@ -102,9 +112,9 @@ class SoilMoisture(ZigbeeChannel):
|
||||
)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.LeafWetness.cluster_id)
|
||||
class LeafWetness(ZigbeeChannel):
|
||||
"""Leaf Wetness measurement channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(measurement.LeafWetness.cluster_id)
|
||||
class LeafWetness(ClusterHandler):
|
||||
"""Leaf Wetness measurement cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(
|
||||
@ -114,11 +124,11 @@ class LeafWetness(ZigbeeChannel):
|
||||
)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
measurement.TemperatureMeasurement.cluster_id
|
||||
)
|
||||
class TemperatureMeasurement(ZigbeeChannel):
|
||||
"""Temperature measurement channel."""
|
||||
class TemperatureMeasurement(ClusterHandler):
|
||||
"""Temperature measurement cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(
|
||||
@ -128,11 +138,11 @@ class TemperatureMeasurement(ZigbeeChannel):
|
||||
)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
measurement.CarbonMonoxideConcentration.cluster_id
|
||||
)
|
||||
class CarbonMonoxideConcentration(ZigbeeChannel):
|
||||
"""Carbon Monoxide measurement channel."""
|
||||
class CarbonMonoxideConcentration(ClusterHandler):
|
||||
"""Carbon Monoxide measurement cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(
|
||||
@ -142,11 +152,11 @@ class CarbonMonoxideConcentration(ZigbeeChannel):
|
||||
)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
measurement.CarbonDioxideConcentration.cluster_id
|
||||
)
|
||||
class CarbonDioxideConcentration(ZigbeeChannel):
|
||||
"""Carbon Dioxide measurement channel."""
|
||||
class CarbonDioxideConcentration(ClusterHandler):
|
||||
"""Carbon Dioxide measurement cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(
|
||||
@ -156,9 +166,9 @@ class CarbonDioxideConcentration(ZigbeeChannel):
|
||||
)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.PM25.cluster_id)
|
||||
class PM25(ZigbeeChannel):
|
||||
"""Particulate Matter 2.5 microns or less measurement channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(measurement.PM25.cluster_id)
|
||||
class PM25(ClusterHandler):
|
||||
"""Particulate Matter 2.5 microns or less measurement cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(
|
||||
@ -168,11 +178,11 @@ class PM25(ZigbeeChannel):
|
||||
)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
measurement.FormaldehydeConcentration.cluster_id
|
||||
)
|
||||
class FormaldehydeConcentration(ZigbeeChannel):
|
||||
"""Formaldehyde measurement channel."""
|
||||
class FormaldehydeConcentration(ClusterHandler):
|
||||
"""Formaldehyde measurement cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(
|
143
homeassistant/components/zha/core/cluster_handlers/protocol.py
Normal file
143
homeassistant/components/zha/core/cluster_handlers/protocol.py
Normal file
@ -0,0 +1,143 @@
|
||||
"""Protocol cluster handlers module for Zigbee Home Automation."""
|
||||
from zigpy.zcl.clusters import protocol
|
||||
|
||||
from . import ClusterHandler
|
||||
from .. import registries
|
||||
|
||||
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
protocol.AnalogInputExtended.cluster_id
|
||||
)
|
||||
class AnalogInputExtended(ClusterHandler):
|
||||
"""Analog Input Extended cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
protocol.AnalogInputRegular.cluster_id
|
||||
)
|
||||
class AnalogInputRegular(ClusterHandler):
|
||||
"""Analog Input Regular cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
protocol.AnalogOutputExtended.cluster_id
|
||||
)
|
||||
class AnalogOutputExtended(ClusterHandler):
|
||||
"""Analog Output Regular cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
protocol.AnalogOutputRegular.cluster_id
|
||||
)
|
||||
class AnalogOutputRegular(ClusterHandler):
|
||||
"""Analog Output Regular cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
protocol.AnalogValueExtended.cluster_id
|
||||
)
|
||||
class AnalogValueExtended(ClusterHandler):
|
||||
"""Analog Value Extended edition cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
protocol.AnalogValueRegular.cluster_id
|
||||
)
|
||||
class AnalogValueRegular(ClusterHandler):
|
||||
"""Analog Value Regular cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
protocol.BacnetProtocolTunnel.cluster_id
|
||||
)
|
||||
class BacnetProtocolTunnel(ClusterHandler):
|
||||
"""Bacnet Protocol Tunnel cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
protocol.BinaryInputExtended.cluster_id
|
||||
)
|
||||
class BinaryInputExtended(ClusterHandler):
|
||||
"""Binary Input Extended cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
protocol.BinaryInputRegular.cluster_id
|
||||
)
|
||||
class BinaryInputRegular(ClusterHandler):
|
||||
"""Binary Input Regular cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
protocol.BinaryOutputExtended.cluster_id
|
||||
)
|
||||
class BinaryOutputExtended(ClusterHandler):
|
||||
"""Binary Output Extended cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
protocol.BinaryOutputRegular.cluster_id
|
||||
)
|
||||
class BinaryOutputRegular(ClusterHandler):
|
||||
"""Binary Output Regular cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
protocol.BinaryValueExtended.cluster_id
|
||||
)
|
||||
class BinaryValueExtended(ClusterHandler):
|
||||
"""Binary Value Extended cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
protocol.BinaryValueRegular.cluster_id
|
||||
)
|
||||
class BinaryValueRegular(ClusterHandler):
|
||||
"""Binary Value Regular cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(protocol.GenericTunnel.cluster_id)
|
||||
class GenericTunnel(ClusterHandler):
|
||||
"""Generic Tunnel cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
protocol.MultistateInputExtended.cluster_id
|
||||
)
|
||||
class MultiStateInputExtended(ClusterHandler):
|
||||
"""Multistate Input Extended cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
protocol.MultistateInputRegular.cluster_id
|
||||
)
|
||||
class MultiStateInputRegular(ClusterHandler):
|
||||
"""Multistate Input Regular cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
protocol.MultistateOutputExtended.cluster_id
|
||||
)
|
||||
class MultiStateOutputExtended(ClusterHandler):
|
||||
"""Multistate Output Extended cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
protocol.MultistateOutputRegular.cluster_id
|
||||
)
|
||||
class MultiStateOutputRegular(ClusterHandler):
|
||||
"""Multistate Output Regular cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
protocol.MultistateValueExtended.cluster_id
|
||||
)
|
||||
class MultiStateValueExtended(ClusterHandler):
|
||||
"""Multistate Value Extended cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
protocol.MultistateValueRegular.cluster_id
|
||||
)
|
||||
class MultiStateValueRegular(ClusterHandler):
|
||||
"""Multistate Value Regular cluster handler."""
|
@ -1,4 +1,4 @@
|
||||
"""Security channels module for Zigbee Home Automation.
|
||||
"""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/
|
||||
@ -15,6 +15,7 @@ from zigpy.zcl.clusters.security import IasAce as AceCluster, IasZone
|
||||
|
||||
from homeassistant.core import callback
|
||||
|
||||
from . import ClusterHandler, ClusterHandlerStatus
|
||||
from .. import registries
|
||||
from ..const import (
|
||||
SIGNAL_ATTR_UPDATED,
|
||||
@ -24,10 +25,9 @@ from ..const import (
|
||||
WARNING_DEVICE_STROBE_HIGH,
|
||||
WARNING_DEVICE_STROBE_YES,
|
||||
)
|
||||
from .base import ChannelStatus, ZigbeeChannel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ChannelPool
|
||||
from ..endpoint import Endpoint
|
||||
|
||||
IAS_ACE_ARM = 0x0000 # ("arm", (t.enum8, t.CharacterString, t.uint8_t), False),
|
||||
IAS_ACE_BYPASS = 0x0001 # ("bypass", (t.LVList(t.uint8_t), t.CharacterString), False),
|
||||
@ -46,13 +46,13 @@ SIGNAL_ARMED_STATE_CHANGED = "zha_armed_state_changed"
|
||||
SIGNAL_ALARM_TRIGGERED = "zha_armed_triggered"
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(AceCluster.cluster_id)
|
||||
class IasAce(ZigbeeChannel):
|
||||
"""IAS Ancillary Control Equipment channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(AceCluster.cluster_id)
|
||||
class IasAce(ClusterHandler):
|
||||
"""IAS Ancillary Control Equipment cluster handler."""
|
||||
|
||||
def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None:
|
||||
"""Initialize IAS Ancillary Control Equipment channel."""
|
||||
super().__init__(cluster, ch_pool)
|
||||
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]] = {
|
||||
IAS_ACE_ARM: self.arm,
|
||||
IAS_ACE_BYPASS: self._bypass,
|
||||
@ -105,7 +105,7 @@ class IasAce(ZigbeeChannel):
|
||||
)
|
||||
|
||||
zigbee_reply = self.arm_map[mode](code)
|
||||
self._ch_pool.hass.async_create_task(zigbee_reply)
|
||||
self._endpoint.device.hass.async_create_task(zigbee_reply)
|
||||
|
||||
if self.invalid_tries >= self.max_invalid_tries:
|
||||
self.alarm_status = AceCluster.AlarmStatus.Emergency
|
||||
@ -228,7 +228,7 @@ class IasAce(ZigbeeChannel):
|
||||
AceCluster.AudibleNotification.Default_Sound,
|
||||
self.alarm_status,
|
||||
)
|
||||
self._ch_pool.hass.async_create_task(response)
|
||||
self._endpoint.device.hass.async_create_task(response)
|
||||
|
||||
def _send_panel_status_changed(self) -> None:
|
||||
"""Handle the IAS ACE panel status changed command."""
|
||||
@ -238,7 +238,7 @@ class IasAce(ZigbeeChannel):
|
||||
AceCluster.AudibleNotification.Default_Sound,
|
||||
self.alarm_status,
|
||||
)
|
||||
self._ch_pool.hass.async_create_task(response)
|
||||
self._endpoint.device.hass.async_create_task(response)
|
||||
|
||||
def _get_bypassed_zone_list(self):
|
||||
"""Handle the IAS ACE bypassed zone list command."""
|
||||
@ -249,10 +249,10 @@ class IasAce(ZigbeeChannel):
|
||||
"""Handle the IAS ACE zone status command."""
|
||||
|
||||
|
||||
@registries.CHANNEL_ONLY_CLUSTERS.register(security.IasWd.cluster_id)
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(security.IasWd.cluster_id)
|
||||
class IasWd(ZigbeeChannel):
|
||||
"""IAS Warning Device channel."""
|
||||
@registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(security.IasWd.cluster_id)
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(security.IasWd.cluster_id)
|
||||
class IasWd(ClusterHandler):
|
||||
"""IAS Warning Device cluster handler."""
|
||||
|
||||
@staticmethod
|
||||
def set_bit(destination_value, destination_bit, source_value, source_bit):
|
||||
@ -332,9 +332,9 @@ class IasWd(ZigbeeChannel):
|
||||
)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(IasZone.cluster_id)
|
||||
class IASZoneChannel(ZigbeeChannel):
|
||||
"""Channel for the IASZone Zigbee cluster."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(IasZone.cluster_id)
|
||||
class IASZoneClusterHandler(ClusterHandler):
|
||||
"""Cluster handler for the IASZone Zigbee cluster."""
|
||||
|
||||
ZCL_INIT_ATTRS = {"zone_status": False, "zone_state": True, "zone_type": True}
|
||||
|
||||
@ -356,11 +356,11 @@ class IASZoneChannel(ZigbeeChannel):
|
||||
async def async_configure(self):
|
||||
"""Configure IAS device."""
|
||||
await self.get_attribute_value("zone_type", from_cache=False)
|
||||
if self._ch_pool.skip_configuration:
|
||||
self.debug("skipping IASZoneChannel configuration")
|
||||
if self._endpoint.device.skip_configuration:
|
||||
self.debug("skipping IASZoneClusterHandler configuration")
|
||||
return
|
||||
|
||||
self.debug("started IASZoneChannel configuration")
|
||||
self.debug("started IASZoneClusterHandler configuration")
|
||||
|
||||
await self.bind()
|
||||
ieee = self.cluster.endpoint.device.application.state.node_info.ieee
|
||||
@ -384,8 +384,8 @@ class IASZoneChannel(ZigbeeChannel):
|
||||
self.debug("Sending pro-active IAS enroll response")
|
||||
self._cluster.create_catching_task(self._cluster.enroll_response(0, 0))
|
||||
|
||||
self._status = ChannelStatus.CONFIGURED
|
||||
self.debug("finished IASZoneChannel configuration")
|
||||
self._status = ClusterHandlerStatus.CONFIGURED
|
||||
self.debug("finished IASZoneClusterHandler configuration")
|
||||
|
||||
@callback
|
||||
def attribute_updated(self, attrid, value):
|
@ -1,4 +1,4 @@
|
||||
"""Smart energy channels module for Zigbee Home Automation."""
|
||||
"""Smart energy cluster handlers module for Zigbee Home Automation."""
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
|
||||
import zigpy.zcl
|
||||
from zigpy.zcl.clusters import smartenergy
|
||||
|
||||
from . import AttrReportConfig, ClusterHandler
|
||||
from .. import registries
|
||||
from ..const import (
|
||||
REPORT_CONFIG_ASAP,
|
||||
@ -15,55 +16,60 @@ from ..const import (
|
||||
REPORT_CONFIG_OP,
|
||||
SIGNAL_ATTR_UPDATED,
|
||||
)
|
||||
from .base import AttrReportConfig, ZigbeeChannel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ChannelPool
|
||||
from ..endpoint import Endpoint
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Calendar.cluster_id)
|
||||
class Calendar(ZigbeeChannel):
|
||||
"""Calendar channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(smartenergy.Calendar.cluster_id)
|
||||
class Calendar(ClusterHandler):
|
||||
"""Calendar cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.DeviceManagement.cluster_id)
|
||||
class DeviceManagement(ZigbeeChannel):
|
||||
"""Device Management channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
smartenergy.DeviceManagement.cluster_id
|
||||
)
|
||||
class DeviceManagement(ClusterHandler):
|
||||
"""Device Management cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Drlc.cluster_id)
|
||||
class Drlc(ZigbeeChannel):
|
||||
"""Demand Response and Load Control channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(smartenergy.Drlc.cluster_id)
|
||||
class Drlc(ClusterHandler):
|
||||
"""Demand Response and Load Control cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.EnergyManagement.cluster_id)
|
||||
class EnergyManagement(ZigbeeChannel):
|
||||
"""Energy Management channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
smartenergy.EnergyManagement.cluster_id
|
||||
)
|
||||
class EnergyManagement(ClusterHandler):
|
||||
"""Energy Management cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Events.cluster_id)
|
||||
class Events(ZigbeeChannel):
|
||||
"""Event channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(smartenergy.Events.cluster_id)
|
||||
class Events(ClusterHandler):
|
||||
"""Event cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.KeyEstablishment.cluster_id)
|
||||
class KeyEstablishment(ZigbeeChannel):
|
||||
"""Key Establishment channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
|
||||
smartenergy.KeyEstablishment.cluster_id
|
||||
)
|
||||
class KeyEstablishment(ClusterHandler):
|
||||
"""Key Establishment cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.MduPairing.cluster_id)
|
||||
class MduPairing(ZigbeeChannel):
|
||||
"""Pairing channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(smartenergy.MduPairing.cluster_id)
|
||||
class MduPairing(ClusterHandler):
|
||||
"""Pairing cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Messaging.cluster_id)
|
||||
class Messaging(ZigbeeChannel):
|
||||
"""Messaging channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(smartenergy.Messaging.cluster_id)
|
||||
class Messaging(ClusterHandler):
|
||||
"""Messaging cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Metering.cluster_id)
|
||||
class Metering(ZigbeeChannel):
|
||||
"""Metering channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(smartenergy.Metering.cluster_id)
|
||||
class Metering(ClusterHandler):
|
||||
"""Metering cluster handler."""
|
||||
|
||||
REPORT_CONFIG = (
|
||||
AttrReportConfig(attr="instantaneous_demand", config=REPORT_CONFIG_OP),
|
||||
@ -137,9 +143,9 @@ class Metering(ZigbeeChannel):
|
||||
DEMAND = 0
|
||||
SUMMATION = 1
|
||||
|
||||
def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None:
|
||||
def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None:
|
||||
"""Initialize Metering."""
|
||||
super().__init__(cluster, ch_pool)
|
||||
super().__init__(cluster, endpoint)
|
||||
self._format_spec: str | None = None
|
||||
self._summa_format: str | None = None
|
||||
|
||||
@ -176,7 +182,7 @@ class Metering(ZigbeeChannel):
|
||||
"""Return unit of measurement."""
|
||||
return self.cluster.get("unit_of_measure")
|
||||
|
||||
async def async_initialize_channel_specific(self, from_cache: bool) -> None:
|
||||
async def async_initialize_cluster_handler_specific(self, from_cache: bool) -> None:
|
||||
"""Fetch config from device and updates format specifier."""
|
||||
|
||||
fmting = self.cluster.get(
|
||||
@ -249,16 +255,16 @@ class Metering(ZigbeeChannel):
|
||||
summa_formatter = partialmethod(_formatter_function, FormatSelector.SUMMATION)
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Prepayment.cluster_id)
|
||||
class Prepayment(ZigbeeChannel):
|
||||
"""Prepayment channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(smartenergy.Prepayment.cluster_id)
|
||||
class Prepayment(ClusterHandler):
|
||||
"""Prepayment cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Price.cluster_id)
|
||||
class Price(ZigbeeChannel):
|
||||
"""Price channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(smartenergy.Price.cluster_id)
|
||||
class Price(ClusterHandler):
|
||||
"""Price cluster handler."""
|
||||
|
||||
|
||||
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Tunneling.cluster_id)
|
||||
class Tunneling(ZigbeeChannel):
|
||||
"""Tunneling channel."""
|
||||
@registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(smartenergy.Tunneling.cluster_id)
|
||||
class Tunneling(ClusterHandler):
|
||||
"""Tunneling cluster handler."""
|
@ -64,39 +64,39 @@ ATTR_WARNING_DEVICE_STROBE_INTENSITY = "intensity"
|
||||
BAUD_RATES = [2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200, 128000, 256000]
|
||||
BINDINGS = "bindings"
|
||||
|
||||
CHANNEL_ACCELEROMETER = "accelerometer"
|
||||
CHANNEL_BINARY_INPUT = "binary_input"
|
||||
CHANNEL_ANALOG_INPUT = "analog_input"
|
||||
CHANNEL_ANALOG_OUTPUT = "analog_output"
|
||||
CHANNEL_ATTRIBUTE = "attribute"
|
||||
CHANNEL_BASIC = "basic"
|
||||
CHANNEL_COLOR = "light_color"
|
||||
CHANNEL_COVER = "window_covering"
|
||||
CHANNEL_DEVICE_TEMPERATURE = "device_temperature"
|
||||
CHANNEL_DOORLOCK = "door_lock"
|
||||
CHANNEL_ELECTRICAL_MEASUREMENT = "electrical_measurement"
|
||||
CHANNEL_EVENT_RELAY = "event_relay"
|
||||
CHANNEL_FAN = "fan"
|
||||
CHANNEL_HUMIDITY = "humidity"
|
||||
CHANNEL_SOIL_MOISTURE = "soil_moisture"
|
||||
CHANNEL_LEAF_WETNESS = "leaf_wetness"
|
||||
CHANNEL_IAS_ACE = "ias_ace"
|
||||
CHANNEL_IAS_WD = "ias_wd"
|
||||
CHANNEL_IDENTIFY = "identify"
|
||||
CHANNEL_ILLUMINANCE = "illuminance"
|
||||
CHANNEL_LEVEL = ATTR_LEVEL
|
||||
CHANNEL_MULTISTATE_INPUT = "multistate_input"
|
||||
CHANNEL_OCCUPANCY = "occupancy"
|
||||
CHANNEL_ON_OFF = "on_off"
|
||||
CHANNEL_POWER_CONFIGURATION = "power"
|
||||
CHANNEL_PRESSURE = "pressure"
|
||||
CHANNEL_SHADE = "shade"
|
||||
CHANNEL_SMARTENERGY_METERING = "smartenergy_metering"
|
||||
CHANNEL_TEMPERATURE = "temperature"
|
||||
CHANNEL_THERMOSTAT = "thermostat"
|
||||
CHANNEL_ZDO = "zdo"
|
||||
CHANNEL_ZONE = ZONE = "ias_zone"
|
||||
CHANNEL_INOVELLI = "inovelli_vzm31sn_cluster"
|
||||
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_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_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"
|
||||
@ -330,15 +330,15 @@ REPORT_CONFIG_OP = (
|
||||
|
||||
SENSOR_ACCELERATION = "acceleration"
|
||||
SENSOR_BATTERY = "battery"
|
||||
SENSOR_ELECTRICAL_MEASUREMENT = CHANNEL_ELECTRICAL_MEASUREMENT
|
||||
SENSOR_ELECTRICAL_MEASUREMENT = CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT
|
||||
SENSOR_GENERIC = "generic"
|
||||
SENSOR_HUMIDITY = CHANNEL_HUMIDITY
|
||||
SENSOR_ILLUMINANCE = CHANNEL_ILLUMINANCE
|
||||
SENSOR_HUMIDITY = CLUSTER_HANDLER_HUMIDITY
|
||||
SENSOR_ILLUMINANCE = CLUSTER_HANDLER_ILLUMINANCE
|
||||
SENSOR_METERING = "metering"
|
||||
SENSOR_OCCUPANCY = CHANNEL_OCCUPANCY
|
||||
SENSOR_OCCUPANCY = CLUSTER_HANDLER_OCCUPANCY
|
||||
SENSOR_OPENING = "opening"
|
||||
SENSOR_PRESSURE = CHANNEL_PRESSURE
|
||||
SENSOR_TEMPERATURE = CHANNEL_TEMPERATURE
|
||||
SENSOR_PRESSURE = CLUSTER_HANDLER_PRESSURE
|
||||
SENSOR_TEMPERATURE = CLUSTER_HANDLER_TEMPERATURE
|
||||
SENSOR_TYPE = "sensor_type"
|
||||
|
||||
SIGNAL_ADD_ENTITIES = "zha_add_new_entities"
|
||||
@ -381,12 +381,12 @@ WARNING_DEVICE_SQUAWK_MODE_ARMED = 0
|
||||
WARNING_DEVICE_SQUAWK_MODE_DISARMED = 1
|
||||
|
||||
ZHA_DISCOVERY_NEW = "zha_discovery_new_{}"
|
||||
ZHA_CHANNEL_MSG = "zha_channel_message"
|
||||
ZHA_CHANNEL_MSG_BIND = "zha_channel_bind"
|
||||
ZHA_CHANNEL_MSG_CFG_RPT = "zha_channel_configure_reporting"
|
||||
ZHA_CHANNEL_MSG_DATA = "zha_channel_msg_data"
|
||||
ZHA_CHANNEL_CFG_DONE = "zha_channel_cfg_done"
|
||||
ZHA_CHANNEL_READS_PER_REQ = 5
|
||||
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"
|
||||
|
@ -13,10 +13,10 @@ class DictRegistry(dict[int | str, _TypeT]):
|
||||
def register(self, name: int | str) -> Callable[[_TypeT], _TypeT]:
|
||||
"""Return decorator to register item with a specific name."""
|
||||
|
||||
def decorator(channel: _TypeT) -> _TypeT:
|
||||
"""Register decorated channel or item."""
|
||||
self[name] = channel
|
||||
return channel
|
||||
def decorator(cluster_handler: _TypeT) -> _TypeT:
|
||||
"""Register decorated cluster handler or item."""
|
||||
self[name] = cluster_handler
|
||||
return cluster_handler
|
||||
|
||||
return decorator
|
||||
|
||||
@ -27,9 +27,9 @@ class SetRegistry(set[int | str]):
|
||||
def register(self, name: int | str) -> Callable[[_TypeT], _TypeT]:
|
||||
"""Return decorator to register item with a specific name."""
|
||||
|
||||
def decorator(channel: _TypeT) -> _TypeT:
|
||||
"""Register decorated channel or item."""
|
||||
def decorator(cluster_handler: _TypeT) -> _TypeT:
|
||||
"""Register decorated cluster handler or item."""
|
||||
self.add(name)
|
||||
return channel
|
||||
return cluster_handler
|
||||
|
||||
return decorator
|
||||
|
@ -23,7 +23,7 @@ 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_NAME
|
||||
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.dispatcher import (
|
||||
@ -32,7 +32,8 @@ from homeassistant.helpers.dispatcher import (
|
||||
)
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
from . import channels
|
||||
from . import const
|
||||
from .cluster_handlers import ClusterHandler, ZDOClusterHandler
|
||||
from .const import (
|
||||
ATTR_ACTIVE_COORDINATOR,
|
||||
ATTR_ARGS,
|
||||
@ -81,6 +82,7 @@ from .const import (
|
||||
UNKNOWN_MODEL,
|
||||
ZHA_OPTIONS,
|
||||
)
|
||||
from .endpoint import Endpoint
|
||||
from .helpers import LogMixin, async_get_zha_config_value, convert_to_zcl_values
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -139,14 +141,26 @@ class ZHADevice(LogMixin):
|
||||
CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY,
|
||||
)
|
||||
|
||||
keep_alive_interval = random.randint(*_UPDATE_ALIVE_INTERVAL)
|
||||
self.unsubs.append(
|
||||
async_track_time_interval(
|
||||
self.hass, self._check_available, timedelta(seconds=keep_alive_interval)
|
||||
)
|
||||
)
|
||||
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._channels = channels.Channels(self)
|
||||
|
||||
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.unsubs.append(
|
||||
async_track_time_interval(
|
||||
self.hass,
|
||||
self._check_available,
|
||||
timedelta(seconds=keep_alive_interval),
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def device_id(self) -> str:
|
||||
@ -162,17 +176,6 @@ class ZHADevice(LogMixin):
|
||||
"""Return underlying Zigpy device."""
|
||||
return self._zigpy_device
|
||||
|
||||
@property
|
||||
def channels(self) -> channels.Channels:
|
||||
"""Return ZHA channels."""
|
||||
return self._channels
|
||||
|
||||
@channels.setter
|
||||
def channels(self, value: channels.Channels) -> None:
|
||||
"""Channels setter."""
|
||||
assert isinstance(value, channels.Channels)
|
||||
self._channels = value
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return device name."""
|
||||
@ -335,12 +338,62 @@ class ZHADevice(LogMixin):
|
||||
"""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: self._channels.zigbee_signature,
|
||||
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,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
@ -353,11 +406,10 @@ class ZHADevice(LogMixin):
|
||||
) -> Self:
|
||||
"""Create new device."""
|
||||
zha_dev = cls(hass, zigpy_dev, gateway)
|
||||
zha_dev.channels = channels.Channels.new(zha_dev)
|
||||
zha_dev.unsubs.append(
|
||||
async_dispatcher_connect(
|
||||
hass,
|
||||
SIGNAL_UPDATE_DEVICE.format(zha_dev.channels.unique_id),
|
||||
SIGNAL_UPDATE_DEVICE.format(str(zha_dev.ieee)),
|
||||
zha_dev.async_update_sw_build_id,
|
||||
)
|
||||
)
|
||||
@ -393,7 +445,7 @@ class ZHADevice(LogMixin):
|
||||
if (
|
||||
self._checkins_missed_count >= _CHECKIN_GRACE_PERIODS
|
||||
or self.manufacturer == "LUMI"
|
||||
or not self._channels.pools
|
||||
or not self._endpoints
|
||||
):
|
||||
self.debug(
|
||||
(
|
||||
@ -410,14 +462,13 @@ class ZHADevice(LogMixin):
|
||||
"Attempting to checkin with device - missed checkins: %s",
|
||||
self._checkins_missed_count,
|
||||
)
|
||||
try:
|
||||
pool = self._channels.pools[0]
|
||||
basic_ch = pool.all_channels[f"{pool.id}:0x0000"]
|
||||
except KeyError:
|
||||
if not self.basic_ch:
|
||||
self.debug("does not have a mandatory basic cluster")
|
||||
self.update_available(False)
|
||||
return
|
||||
res = await basic_ch.get_attribute_value(ATTR_MANUFACTURER, from_cache=False)
|
||||
res = await self.basic_ch.get_attribute_value(
|
||||
ATTR_MANUFACTURER, from_cache=False
|
||||
)
|
||||
if res is not None:
|
||||
self._checkins_missed_count = 0
|
||||
|
||||
@ -435,22 +486,35 @@ class ZHADevice(LogMixin):
|
||||
availability_changed = self.available ^ available
|
||||
self.available = available
|
||||
if availability_changed and available:
|
||||
# reinit channels then signal entities
|
||||
# reinit cluster handlers then signal entities
|
||||
self.debug(
|
||||
"Device availability changed and device became available,"
|
||||
" reinitializing channels"
|
||||
" 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._channels.zha_send_event(
|
||||
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)
|
||||
@ -489,23 +553,41 @@ class ZHADevice(LogMixin):
|
||||
True,
|
||||
)
|
||||
self.debug("started configuration")
|
||||
await self._channels.async_configure()
|
||||
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())
|
||||
)
|
||||
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._channels.identify_ch is not None
|
||||
and self.identify_ch is not None
|
||||
and not self.skip_configuration
|
||||
):
|
||||
await self._channels.identify_ch.trigger_effect(
|
||||
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 channels."""
|
||||
"""Initialize cluster handlers."""
|
||||
self.debug("started initialization")
|
||||
await self._channels.async_initialize(from_cache)
|
||||
await self._zdo_handler.async_initialize(from_cache)
|
||||
self._zdo_handler.debug("'async_initialize' stage succeeded")
|
||||
await asyncio.gather(
|
||||
*(
|
||||
endpoint.async_initialize(from_cache)
|
||||
for endpoint in self._endpoints.values()
|
||||
)
|
||||
)
|
||||
self.debug("power source: %s", self.power_source)
|
||||
self.status = DeviceStatus.INITIALIZED
|
||||
self.debug("completed initialization")
|
||||
|
@ -33,12 +33,27 @@ from .. import ( # noqa: F401 pylint: disable=unused-import,
|
||||
siren,
|
||||
switch,
|
||||
)
|
||||
from .channels import base
|
||||
|
||||
# importing cluster handlers updates registries
|
||||
from .cluster_handlers import ( # noqa: F401 pylint: disable=unused-import,
|
||||
ClusterHandler,
|
||||
closures,
|
||||
general,
|
||||
homeautomation,
|
||||
hvac,
|
||||
lighting,
|
||||
lightlink,
|
||||
manufacturerspecific,
|
||||
measurement,
|
||||
protocol,
|
||||
security,
|
||||
smartenergy,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..entity import ZhaEntity
|
||||
from .channels import ChannelPool
|
||||
from .device import ZHADevice
|
||||
from .endpoint import Endpoint
|
||||
from .gateway import ZHAGateway
|
||||
from .group import ZHAGroup
|
||||
|
||||
@ -51,7 +66,7 @@ async def async_add_entities(
|
||||
entities: list[
|
||||
tuple[
|
||||
type[ZhaEntity],
|
||||
tuple[str, ZHADevice, list[base.ZigbeeChannel]],
|
||||
tuple[str, ZHADevice, list[ClusterHandler]],
|
||||
]
|
||||
],
|
||||
) -> None:
|
||||
@ -65,49 +80,56 @@ async def async_add_entities(
|
||||
|
||||
|
||||
class ProbeEndpoint:
|
||||
"""All discovered channels and entities of an endpoint."""
|
||||
"""All discovered cluster handlers and entities of an endpoint."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize instance."""
|
||||
self._device_configs: ConfigType = {}
|
||||
|
||||
@callback
|
||||
def discover_entities(self, channel_pool: ChannelPool) -> None:
|
||||
def discover_entities(self, endpoint: Endpoint) -> None:
|
||||
"""Process an endpoint on a zigpy device."""
|
||||
self.discover_by_device_type(channel_pool)
|
||||
self.discover_multi_entities(channel_pool)
|
||||
self.discover_by_cluster_id(channel_pool)
|
||||
self.discover_multi_entities(channel_pool, config_diagnostic_entities=True)
|
||||
_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_by_device_type(self, channel_pool: ChannelPool) -> None:
|
||||
def discover_by_device_type(self, endpoint: Endpoint) -> None:
|
||||
"""Process an endpoint on a zigpy device."""
|
||||
|
||||
unique_id = channel_pool.unique_id
|
||||
unique_id = endpoint.unique_id
|
||||
|
||||
component: str | None = self._device_configs.get(unique_id, {}).get(CONF_TYPE)
|
||||
if component is None:
|
||||
ep_profile_id = channel_pool.endpoint.profile_id
|
||||
ep_device_type = channel_pool.endpoint.device_type
|
||||
component = zha_regs.DEVICE_CLASS[ep_profile_id].get(ep_device_type)
|
||||
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 component and component in zha_const.PLATFORMS:
|
||||
channels = channel_pool.unclaimed_channels()
|
||||
entity_class, claimed = zha_regs.ZHA_ENTITIES.get_entity(
|
||||
component,
|
||||
channel_pool.manufacturer,
|
||||
channel_pool.model,
|
||||
channels,
|
||||
channel_pool.quirk_class,
|
||||
if platform and platform in zha_const.PLATFORMS:
|
||||
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_class,
|
||||
)
|
||||
if entity_class is None:
|
||||
if platform_entity_class is None:
|
||||
return
|
||||
channel_pool.claim_channels(claimed)
|
||||
channel_pool.async_new_entity(component, entity_class, unique_id, claimed)
|
||||
endpoint.claim_cluster_handlers(claimed)
|
||||
endpoint.async_new_entity(
|
||||
platform, platform_entity_class, unique_id, claimed
|
||||
)
|
||||
|
||||
@callback
|
||||
def discover_by_cluster_id(self, channel_pool: ChannelPool) -> None:
|
||||
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()
|
||||
@ -116,124 +138,127 @@ class ProbeEndpoint:
|
||||
for cluster_class, match in items
|
||||
if not isinstance(cluster_class, int)
|
||||
}
|
||||
remaining_channels = channel_pool.unclaimed_channels()
|
||||
for channel in remaining_channels:
|
||||
if channel.cluster.cluster_id in zha_regs.CHANNEL_ONLY_CLUSTERS:
|
||||
channel_pool.claim_channels([channel])
|
||||
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
|
||||
|
||||
component = zha_regs.SINGLE_INPUT_CLUSTER_DEVICE_CLASS.get(
|
||||
channel.cluster.cluster_id
|
||||
platform = zha_regs.SINGLE_INPUT_CLUSTER_DEVICE_CLASS.get(
|
||||
cluster_handler.cluster.cluster_id
|
||||
)
|
||||
if component is None:
|
||||
if platform is None:
|
||||
for cluster_class, match in single_input_clusters.items():
|
||||
if isinstance(channel.cluster, cluster_class):
|
||||
component = match
|
||||
if isinstance(cluster_handler.cluster, cluster_class):
|
||||
platform = match
|
||||
break
|
||||
|
||||
self.probe_single_cluster(component, channel, channel_pool)
|
||||
self.probe_single_cluster(platform, cluster_handler, endpoint)
|
||||
|
||||
# until we can get rid of registries
|
||||
self.handle_on_off_output_cluster_exception(channel_pool)
|
||||
self.handle_on_off_output_cluster_exception(endpoint)
|
||||
|
||||
@staticmethod
|
||||
def probe_single_cluster(
|
||||
component: Platform | None,
|
||||
channel: base.ZigbeeChannel,
|
||||
ep_channels: ChannelPool,
|
||||
platform: Platform | None,
|
||||
cluster_handler: ClusterHandler,
|
||||
endpoint: Endpoint,
|
||||
) -> None:
|
||||
"""Probe specified cluster for specific component."""
|
||||
if component is None or component not in zha_const.PLATFORMS:
|
||||
if platform is None or platform not in zha_const.PLATFORMS:
|
||||
return
|
||||
channel_list = [channel]
|
||||
unique_id = f"{ep_channels.unique_id}-{channel.cluster.cluster_id}"
|
||||
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(
|
||||
component,
|
||||
ep_channels.manufacturer,
|
||||
ep_channels.model,
|
||||
channel_list,
|
||||
ep_channels.quirk_class,
|
||||
platform,
|
||||
endpoint.device.manufacturer,
|
||||
endpoint.device.model,
|
||||
cluster_handler_list,
|
||||
endpoint.device.quirk_class,
|
||||
)
|
||||
if entity_class is None:
|
||||
return
|
||||
ep_channels.claim_channels(claimed)
|
||||
ep_channels.async_new_entity(component, entity_class, unique_id, claimed)
|
||||
endpoint.claim_cluster_handlers(claimed)
|
||||
endpoint.async_new_entity(platform, entity_class, unique_id, claimed)
|
||||
|
||||
def handle_on_off_output_cluster_exception(self, ep_channels: ChannelPool) -> None:
|
||||
def handle_on_off_output_cluster_exception(self, endpoint: Endpoint) -> None:
|
||||
"""Process output clusters of the endpoint."""
|
||||
|
||||
profile_id = ep_channels.endpoint.profile_id
|
||||
device_type = ep_channels.endpoint.device_type
|
||||
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 ep_channels.endpoint.out_clusters.items():
|
||||
component = zha_regs.SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.get(
|
||||
for cluster_id, cluster in endpoint.zigpy_endpoint.out_clusters.items():
|
||||
platform = zha_regs.SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.get(
|
||||
cluster.cluster_id
|
||||
)
|
||||
if component is None:
|
||||
if platform is None:
|
||||
continue
|
||||
|
||||
channel_class = zha_regs.ZIGBEE_CHANNEL_REGISTRY.get(
|
||||
cluster_id, base.ZigbeeChannel
|
||||
cluster_handler_class = zha_regs.ZIGBEE_CLUSTER_HANDLER_REGISTRY.get(
|
||||
cluster_id, ClusterHandler
|
||||
)
|
||||
channel = channel_class(cluster, ep_channels)
|
||||
self.probe_single_cluster(component, channel, ep_channels)
|
||||
cluster_handler = cluster_handler_class(cluster, endpoint)
|
||||
self.probe_single_cluster(platform, cluster_handler, endpoint)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def discover_multi_entities(
|
||||
channel_pool: ChannelPool,
|
||||
endpoint: Endpoint,
|
||||
config_diagnostic_entities: bool = False,
|
||||
) -> None:
|
||||
"""Process an endpoint on and discover multiple entities."""
|
||||
|
||||
ep_profile_id = channel_pool.endpoint.profile_id
|
||||
ep_device_type = channel_pool.endpoint.device_type
|
||||
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:
|
||||
matches, claimed = zha_regs.ZHA_ENTITIES.get_config_diagnostic_entity(
|
||||
channel_pool.manufacturer,
|
||||
channel_pool.model,
|
||||
list(channel_pool.all_channels.values()),
|
||||
channel_pool.quirk_class,
|
||||
endpoint.device.manufacturer,
|
||||
endpoint.device.model,
|
||||
list(endpoint.all_cluster_handlers.values()),
|
||||
endpoint.device.quirk_class,
|
||||
)
|
||||
else:
|
||||
matches, claimed = zha_regs.ZHA_ENTITIES.get_multi_entity(
|
||||
channel_pool.manufacturer,
|
||||
channel_pool.model,
|
||||
channel_pool.unclaimed_channels(),
|
||||
channel_pool.quirk_class,
|
||||
endpoint.device.manufacturer,
|
||||
endpoint.device.model,
|
||||
endpoint.unclaimed_cluster_handlers(),
|
||||
endpoint.device.quirk_class,
|
||||
)
|
||||
|
||||
channel_pool.claim_channels(claimed)
|
||||
for component, ent_n_chan_list in matches.items():
|
||||
for entity_and_channel in ent_n_chan_list:
|
||||
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' component -> '%s' using %s",
|
||||
component,
|
||||
entity_and_channel.entity_class.__name__,
|
||||
[ch.name for ch in entity_and_channel.claimed_channel],
|
||||
platform,
|
||||
entity_and_handler.entity_class.__name__,
|
||||
[ch.name for ch in entity_and_handler.claimed_cluster_handlers],
|
||||
)
|
||||
for component, ent_n_chan_list in matches.items():
|
||||
for entity_and_channel in ent_n_chan_list:
|
||||
if component == cmpt_by_dev_type:
|
||||
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
|
||||
channel_pool.async_new_entity(
|
||||
component,
|
||||
entity_and_channel.entity_class,
|
||||
channel_pool.unique_id,
|
||||
entity_and_channel.claimed_channel,
|
||||
endpoint.async_new_entity(
|
||||
platform,
|
||||
entity_and_handler.entity_class,
|
||||
endpoint.unique_id,
|
||||
entity_and_handler.claimed_cluster_handlers,
|
||||
)
|
||||
break
|
||||
first_ch = entity_and_channel.claimed_channel[0]
|
||||
channel_pool.async_new_entity(
|
||||
component,
|
||||
entity_and_channel.entity_class,
|
||||
f"{channel_pool.unique_id}-{first_ch.cluster.cluster_id}",
|
||||
entity_and_channel.claimed_channel,
|
||||
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:
|
||||
|
220
homeassistant/components/zha/core/endpoint.py
Normal file
220
homeassistant/components/zha/core/endpoint.py
Normal file
@ -0,0 +1,220 @@
|
||||
"""Representation of a Zigbee endpoint for zha."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Final, TypeVar
|
||||
|
||||
import zigpy
|
||||
from zigpy.typing import EndpointType as ZigpyEndpointType
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
|
||||
from . import const, discovery, registries
|
||||
from .cluster_handlers import ClusterHandler
|
||||
from .cluster_handlers.general import MultistateInput
|
||||
|
||||
if TYPE_CHECKING:
|
||||
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__)
|
||||
CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name
|
||||
|
||||
|
||||
class Endpoint:
|
||||
"""Endpoint for a zha device."""
|
||||
|
||||
def __init__(self, zigpy_endpoint: ZigpyEndpointType, device: ZHADevice) -> None:
|
||||
"""Initialize instance."""
|
||||
assert zigpy_endpoint is not None
|
||||
assert device is not None
|
||||
self._zigpy_endpoint: ZigpyEndpointType = 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"{str(device.ieee)}-{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) -> ZigpyEndpointType:
|
||||
"""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: ZigpyEndpointType, 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_class = registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.get(
|
||||
cluster_id, ClusterHandler
|
||||
)
|
||||
_LOGGER.info(
|
||||
"Creating cluster handler for cluster id: %s class: %s",
|
||||
cluster_id,
|
||||
cluster_handler_class,
|
||||
)
|
||||
# really ugly hack to deal with xiaomi using the door lock cluster
|
||||
# incorrectly.
|
||||
if (
|
||||
hasattr(cluster, "ep_attribute")
|
||||
and cluster_id == zigpy.zcl.clusters.closures.DoorLock.cluster_id
|
||||
and cluster.ep_attribute == "multistate_input"
|
||||
):
|
||||
cluster_handler_class = MultistateInput
|
||||
# end of ugly hack
|
||||
cluster_handler = cluster_handler_class(cluster, self)
|
||||
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)
|
||||
|
||||
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) -> 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]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
for cluster_handler, outcome in zip(cluster_handlers, results):
|
||||
if isinstance(outcome, Exception):
|
||||
cluster_handler.warning(
|
||||
"'%s' stage failed: %s", func_name, str(outcome), exc_info=outcome
|
||||
)
|
||||
continue
|
||||
cluster_handler.debug("'%s' stage succeeded", func_name)
|
||||
|
||||
def async_new_entity(
|
||||
self,
|
||||
platform: Platform | str,
|
||||
entity_class: CALLABLE_T,
|
||||
unique_id: str,
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
) -> None:
|
||||
"""Create a new entity."""
|
||||
from .device import DeviceStatus # pylint: disable=import-outside-toplevel
|
||||
|
||||
if self.device.status == DeviceStatus.INITIALIZED:
|
||||
return
|
||||
|
||||
self.device.hass.data[const.DATA_ZHA][platform].append(
|
||||
(entity_class, (unique_id, self.device, cluster_handlers))
|
||||
)
|
||||
|
||||
@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."""
|
||||
signal["endpoint"] = {
|
||||
"id": self.id,
|
||||
"unique_id": self.unique_id,
|
||||
}
|
||||
self.device.zha_send_event(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)
|
||||
]
|
@ -93,7 +93,7 @@ if TYPE_CHECKING:
|
||||
from logging import Filter, LogRecord
|
||||
|
||||
from ..entity import ZhaEntity
|
||||
from .channels.base import ZigbeeChannel
|
||||
from .cluster_handlers import ClusterHandler
|
||||
|
||||
_LogFilterType = Filter | Callable[[LogRecord], bool]
|
||||
|
||||
@ -105,7 +105,7 @@ class EntityReference(NamedTuple):
|
||||
|
||||
reference_id: str
|
||||
zha_device: ZHADevice
|
||||
cluster_channels: dict[str, ZigbeeChannel]
|
||||
cluster_handlers: dict[str, ClusterHandler]
|
||||
device_info: DeviceInfo
|
||||
remove_future: asyncio.Future[Any]
|
||||
|
||||
@ -520,7 +520,7 @@ class ZHAGateway:
|
||||
ieee: EUI64,
|
||||
reference_id: str,
|
||||
zha_device: ZHADevice,
|
||||
cluster_channels: dict[str, ZigbeeChannel],
|
||||
cluster_handlers: dict[str, ClusterHandler],
|
||||
device_info: DeviceInfo,
|
||||
remove_future: asyncio.Future[Any],
|
||||
):
|
||||
@ -529,7 +529,7 @@ class ZHAGateway:
|
||||
EntityReference(
|
||||
reference_id=reference_id,
|
||||
zha_device=zha_device,
|
||||
cluster_channels=cluster_channels,
|
||||
cluster_handlers=cluster_handlers,
|
||||
device_info=device_info,
|
||||
remove_future=remove_future,
|
||||
)
|
||||
|
@ -89,7 +89,7 @@ class ZHAGroupMember(LogMixin):
|
||||
entity_ref.reference_id,
|
||||
)._asdict()
|
||||
for entity_ref in zha_device_registry.get(self.device.ieee)
|
||||
if list(entity_ref.cluster_channels.values())[
|
||||
if list(entity_ref.cluster_handlers.values())[
|
||||
0
|
||||
].cluster.endpoint.endpoint_id
|
||||
== self.endpoint_id
|
||||
|
@ -336,17 +336,17 @@ def retryable_req(
|
||||
|
||||
def decorator(func):
|
||||
@functools.wraps(func)
|
||||
async def wrapper(channel, *args, **kwargs):
|
||||
async def wrapper(cluster_handler, *args, **kwargs):
|
||||
exceptions = (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError)
|
||||
try_count, errors = 1, []
|
||||
for delay in itertools.chain(delays, [None]):
|
||||
try:
|
||||
return await func(channel, *args, **kwargs)
|
||||
return await func(cluster_handler, *args, **kwargs)
|
||||
except exceptions as ex:
|
||||
errors.append(ex)
|
||||
if delay:
|
||||
delay = uniform(delay * 0.75, delay * 1.25)
|
||||
channel.debug(
|
||||
cluster_handler.debug(
|
||||
"%s: retryable request #%d failed: %s. Retrying in %ss",
|
||||
func.__name__,
|
||||
try_count,
|
||||
@ -356,7 +356,7 @@ def retryable_req(
|
||||
try_count += 1
|
||||
await asyncio.sleep(delay)
|
||||
else:
|
||||
channel.warning(
|
||||
cluster_handler.warning(
|
||||
"%s: all attempts have failed: %s", func.__name__, errors
|
||||
)
|
||||
if raise_:
|
||||
|
@ -14,13 +14,11 @@ from zigpy.types.named import EUI64
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
# importing channels updates registries
|
||||
from . import channels as zha_channels # noqa: F401 pylint: disable=unused-import
|
||||
from .decorators import DictRegistry, SetRegistry
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..entity import ZhaEntity, ZhaGroupEntity
|
||||
from .channels.base import ClientChannel, ZigbeeChannel
|
||||
from .cluster_handlers import ClientClusterHandler, ClusterHandler
|
||||
|
||||
|
||||
_ZhaEntityT = TypeVar("_ZhaEntityT", bound=type["ZhaEntity"])
|
||||
@ -75,7 +73,6 @@ SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {
|
||||
}
|
||||
|
||||
BINDABLE_CLUSTERS = SetRegistry()
|
||||
CHANNEL_ONLY_CLUSTERS = SetRegistry()
|
||||
|
||||
DEVICE_CLASS = {
|
||||
zigpy.profiles.zha.PROFILE_ID: {
|
||||
@ -108,8 +105,11 @@ DEVICE_CLASS = {
|
||||
}
|
||||
DEVICE_CLASS = collections.defaultdict(dict, DEVICE_CLASS)
|
||||
|
||||
CLIENT_CHANNELS_REGISTRY: DictRegistry[type[ClientChannel]] = DictRegistry()
|
||||
ZIGBEE_CHANNEL_REGISTRY: DictRegistry[type[ZigbeeChannel]] = DictRegistry()
|
||||
CLUSTER_HANDLER_ONLY_CLUSTERS = SetRegistry()
|
||||
CLIENT_CLUSTER_HANDLER_REGISTRY: DictRegistry[
|
||||
type[ClientClusterHandler]
|
||||
] = DictRegistry()
|
||||
ZIGBEE_CLUSTER_HANDLER_REGISTRY: DictRegistry[type[ClusterHandler]] = DictRegistry()
|
||||
|
||||
|
||||
def set_or_callable(value) -> frozenset[str] | Callable:
|
||||
@ -129,9 +129,9 @@ def _get_empty_frozenset() -> frozenset[str]:
|
||||
|
||||
@attr.s(frozen=True)
|
||||
class MatchRule:
|
||||
"""Match a ZHA Entity to a channel name or generic id."""
|
||||
"""Match a ZHA Entity to a cluster handler name or generic id."""
|
||||
|
||||
channel_names: frozenset[str] = attr.ib(
|
||||
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)
|
||||
@ -141,7 +141,7 @@ class MatchRule:
|
||||
models: frozenset[str] | Callable = attr.ib(
|
||||
factory=_get_empty_frozenset, converter=set_or_callable
|
||||
)
|
||||
aux_channels: frozenset[str] | Callable = attr.ib(
|
||||
aux_cluster_handlers: frozenset[str] | Callable = attr.ib(
|
||||
factory=_get_empty_frozenset, converter=set_or_callable
|
||||
)
|
||||
quirk_classes: frozenset[str] | Callable = attr.ib(
|
||||
@ -157,9 +157,9 @@ class MatchRule:
|
||||
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 channels.
|
||||
But in case of a channel name/channel id matching, we give rules matching
|
||||
multiple channels a better priority over rules matching a single channel.
|
||||
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_classes:
|
||||
@ -175,51 +175,57 @@ class MatchRule:
|
||||
1 if callable(self.manufacturers) else len(self.manufacturers)
|
||||
)
|
||||
|
||||
weight += 10 * len(self.channel_names)
|
||||
weight += 10 * len(self.cluster_handler_names)
|
||||
weight += 5 * len(self.generic_ids)
|
||||
if isinstance(self.aux_channels, frozenset):
|
||||
weight += 1 * len(self.aux_channels)
|
||||
if isinstance(self.aux_cluster_handlers, frozenset):
|
||||
weight += 1 * len(self.aux_cluster_handlers)
|
||||
return weight
|
||||
|
||||
def claim_channels(self, channel_pool: list[ZigbeeChannel]) -> list[ZigbeeChannel]:
|
||||
"""Return a list of channels this rule matches + aux channels."""
|
||||
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.channel_names, frozenset):
|
||||
claimed.extend([ch for ch in channel_pool if ch.name in self.channel_names])
|
||||
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 channel_pool if ch.generic_id in self.generic_ids]
|
||||
[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]
|
||||
)
|
||||
if isinstance(self.aux_channels, frozenset):
|
||||
claimed.extend([ch for ch in channel_pool if ch.name in self.aux_channels])
|
||||
return claimed
|
||||
|
||||
def strict_matched(
|
||||
self, manufacturer: str, model: str, channels: list, quirk_class: str
|
||||
self, manufacturer: str, model: str, cluster_handlers: list, quirk_class: str
|
||||
) -> bool:
|
||||
"""Return True if this device matches the criteria."""
|
||||
return all(self._matched(manufacturer, model, channels, quirk_class))
|
||||
return all(self._matched(manufacturer, model, cluster_handlers, quirk_class))
|
||||
|
||||
def loose_matched(
|
||||
self, manufacturer: str, model: str, channels: list, quirk_class: str
|
||||
self, manufacturer: str, model: str, cluster_handlers: list, quirk_class: str
|
||||
) -> bool:
|
||||
"""Return True if this device matches the criteria."""
|
||||
return any(self._matched(manufacturer, model, channels, quirk_class))
|
||||
return any(self._matched(manufacturer, model, cluster_handlers, quirk_class))
|
||||
|
||||
def _matched(
|
||||
self, manufacturer: str, model: str, channels: list, quirk_class: str
|
||||
self, manufacturer: str, model: str, cluster_handlers: list, quirk_class: str
|
||||
) -> list:
|
||||
"""Return a list of field matches."""
|
||||
if not any(attr.asdict(self).values()):
|
||||
return [False]
|
||||
|
||||
matches = []
|
||||
if self.channel_names:
|
||||
channel_names = {ch.name for ch in channels}
|
||||
matches.append(self.channel_names.issubset(channel_names))
|
||||
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 channels}
|
||||
all_generic_ids = {ch.generic_id for ch in cluster_handlers}
|
||||
matches.append(self.generic_ids.issubset(all_generic_ids))
|
||||
|
||||
if self.manufacturers:
|
||||
@ -244,15 +250,15 @@ class MatchRule:
|
||||
|
||||
|
||||
@dataclasses.dataclass
|
||||
class EntityClassAndChannels:
|
||||
"""Container for entity class and corresponding channels."""
|
||||
class EntityClassAndClusterHandlers:
|
||||
"""Container for entity class and corresponding cluster handlers."""
|
||||
|
||||
entity_class: type[ZhaEntity]
|
||||
claimed_channel: list[ZigbeeChannel]
|
||||
claimed_cluster_handlers: list[ClusterHandler]
|
||||
|
||||
|
||||
class ZHAEntityRegistry:
|
||||
"""Channel to ZHA Entity mapping."""
|
||||
"""Cluster handler to ZHA Entity mapping."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize Registry instance."""
|
||||
@ -279,15 +285,15 @@ class ZHAEntityRegistry:
|
||||
component: str,
|
||||
manufacturer: str,
|
||||
model: str,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
quirk_class: str,
|
||||
default: type[ZhaEntity] | None = None,
|
||||
) -> tuple[type[ZhaEntity] | None, list[ZigbeeChannel]]:
|
||||
"""Match a ZHA Channels to a ZHA Entity class."""
|
||||
) -> 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=lambda x: x.weight, reverse=True):
|
||||
if match.strict_matched(manufacturer, model, channels, quirk_class):
|
||||
claimed = match.claim_channels(channels)
|
||||
if match.strict_matched(manufacturer, model, cluster_handlers, quirk_class):
|
||||
claimed = match.claim_cluster_handlers(cluster_handlers)
|
||||
return self._strict_registry[component][match], claimed
|
||||
|
||||
return default, []
|
||||
@ -296,21 +302,27 @@ class ZHAEntityRegistry:
|
||||
self,
|
||||
manufacturer: str,
|
||||
model: str,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
quirk_class: str,
|
||||
) -> tuple[dict[str, list[EntityClassAndChannels]], list[ZigbeeChannel]]:
|
||||
"""Match ZHA Channels to potentially multiple ZHA Entity classes."""
|
||||
result: dict[str, list[EntityClassAndChannels]] = collections.defaultdict(list)
|
||||
all_claimed: set[ZigbeeChannel] = set()
|
||||
) -> tuple[dict[str, list[EntityClassAndClusterHandlers]], list[ClusterHandler]]:
|
||||
"""Match ZHA cluster handlers to potentially multiple ZHA Entity classes."""
|
||||
result: dict[
|
||||
str, 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=lambda x: x.weight, reverse=True)
|
||||
for match in sorted_matches:
|
||||
if match.strict_matched(manufacturer, model, channels, quirk_class):
|
||||
claimed = match.claim_channels(channels)
|
||||
if match.strict_matched(
|
||||
manufacturer, model, cluster_handlers, quirk_class
|
||||
):
|
||||
claimed = match.claim_cluster_handlers(cluster_handlers)
|
||||
for ent_class in stop_match_groups[stop_match_grp][match]:
|
||||
ent_n_channels = EntityClassAndChannels(ent_class, claimed)
|
||||
result[component].append(ent_n_channels)
|
||||
ent_n_cluster_handlers = EntityClassAndClusterHandlers(
|
||||
ent_class, claimed
|
||||
)
|
||||
result[component].append(ent_n_cluster_handlers)
|
||||
all_claimed |= set(claimed)
|
||||
if stop_match_grp:
|
||||
break
|
||||
@ -321,12 +333,14 @@ class ZHAEntityRegistry:
|
||||
self,
|
||||
manufacturer: str,
|
||||
model: str,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
quirk_class: str,
|
||||
) -> tuple[dict[str, list[EntityClassAndChannels]], list[ZigbeeChannel]]:
|
||||
"""Match ZHA Channels to potentially multiple ZHA Entity classes."""
|
||||
result: dict[str, list[EntityClassAndChannels]] = collections.defaultdict(list)
|
||||
all_claimed: set[ZigbeeChannel] = set()
|
||||
) -> tuple[dict[str, list[EntityClassAndClusterHandlers]], list[ClusterHandler]]:
|
||||
"""Match ZHA cluster handlers to potentially multiple ZHA Entity classes."""
|
||||
result: dict[
|
||||
str, list[EntityClassAndClusterHandlers]
|
||||
] = collections.defaultdict(list)
|
||||
all_claimed: set[ClusterHandler] = set()
|
||||
for (
|
||||
component,
|
||||
stop_match_groups,
|
||||
@ -334,11 +348,15 @@ class ZHAEntityRegistry:
|
||||
for stop_match_grp, matches in stop_match_groups.items():
|
||||
sorted_matches = sorted(matches, key=lambda x: x.weight, reverse=True)
|
||||
for match in sorted_matches:
|
||||
if match.strict_matched(manufacturer, model, channels, quirk_class):
|
||||
claimed = match.claim_channels(channels)
|
||||
if match.strict_matched(
|
||||
manufacturer, model, cluster_handlers, quirk_class
|
||||
):
|
||||
claimed = match.claim_cluster_handlers(cluster_handlers)
|
||||
for ent_class in stop_match_groups[stop_match_grp][match]:
|
||||
ent_n_channels = EntityClassAndChannels(ent_class, claimed)
|
||||
result[component].append(ent_n_channels)
|
||||
ent_n_cluster_handlers = EntityClassAndClusterHandlers(
|
||||
ent_class, claimed
|
||||
)
|
||||
result[component].append(ent_n_cluster_handlers)
|
||||
all_claimed |= set(claimed)
|
||||
if stop_match_grp:
|
||||
break
|
||||
@ -352,21 +370,21 @@ class ZHAEntityRegistry:
|
||||
def strict_match(
|
||||
self,
|
||||
component: str,
|
||||
channel_names: set[str] | str | None = None,
|
||||
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_channels: Callable | set[str] | str | None = None,
|
||||
aux_cluster_handlers: Callable | set[str] | str | None = None,
|
||||
quirk_classes: set[str] | str | None = None,
|
||||
) -> Callable[[_ZhaEntityT], _ZhaEntityT]:
|
||||
"""Decorate a strict match rule."""
|
||||
|
||||
rule = MatchRule(
|
||||
channel_names,
|
||||
cluster_handler_names,
|
||||
generic_ids,
|
||||
manufacturers,
|
||||
models,
|
||||
aux_channels,
|
||||
aux_cluster_handlers,
|
||||
quirk_classes,
|
||||
)
|
||||
|
||||
@ -383,22 +401,22 @@ class ZHAEntityRegistry:
|
||||
def multipass_match(
|
||||
self,
|
||||
component: str,
|
||||
channel_names: set[str] | str | None = None,
|
||||
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_channels: Callable | set[str] | str | None = None,
|
||||
aux_cluster_handlers: Callable | set[str] | str | None = None,
|
||||
stop_on_match_group: int | str | None = None,
|
||||
quirk_classes: set[str] | str | None = None,
|
||||
) -> Callable[[_ZhaEntityT], _ZhaEntityT]:
|
||||
"""Decorate a loose match rule."""
|
||||
|
||||
rule = MatchRule(
|
||||
channel_names,
|
||||
cluster_handler_names,
|
||||
generic_ids,
|
||||
manufacturers,
|
||||
models,
|
||||
aux_channels,
|
||||
aux_cluster_handlers,
|
||||
quirk_classes,
|
||||
)
|
||||
|
||||
@ -407,7 +425,7 @@ class ZHAEntityRegistry:
|
||||
|
||||
All non empty fields of a match rule must match.
|
||||
"""
|
||||
# group the rules by channels
|
||||
# group the rules by cluster handlers
|
||||
self._multi_entity_registry[component][stop_on_match_group][rule].append(
|
||||
zha_entity
|
||||
)
|
||||
@ -418,22 +436,22 @@ class ZHAEntityRegistry:
|
||||
def config_diagnostic_match(
|
||||
self,
|
||||
component: str,
|
||||
channel_names: set[str] | str | None = None,
|
||||
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_channels: Callable | set[str] | str | None = None,
|
||||
aux_cluster_handlers: Callable | set[str] | str | None = None,
|
||||
stop_on_match_group: int | str | None = None,
|
||||
quirk_classes: set[str] | str | None = None,
|
||||
) -> Callable[[_ZhaEntityT], _ZhaEntityT]:
|
||||
"""Decorate a loose match rule."""
|
||||
|
||||
rule = MatchRule(
|
||||
channel_names,
|
||||
cluster_handler_names,
|
||||
generic_ids,
|
||||
manufacturers,
|
||||
models,
|
||||
aux_channels,
|
||||
aux_cluster_handlers,
|
||||
quirk_classes,
|
||||
)
|
||||
|
||||
@ -442,7 +460,7 @@ class ZHAEntityRegistry:
|
||||
|
||||
All non-empty fields of a match rule must match.
|
||||
"""
|
||||
# group the rules by channels
|
||||
# group the rules by cluster handlers
|
||||
self._config_diagnostic_entity_registry[component][stop_on_match_group][
|
||||
rule
|
||||
].append(zha_entity)
|
||||
|
@ -28,10 +28,10 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .core import discovery
|
||||
from .core.const import (
|
||||
CHANNEL_COVER,
|
||||
CHANNEL_LEVEL,
|
||||
CHANNEL_ON_OFF,
|
||||
CHANNEL_SHADE,
|
||||
CLUSTER_HANDLER_COVER,
|
||||
CLUSTER_HANDLER_LEVEL,
|
||||
CLUSTER_HANDLER_ON_OFF,
|
||||
CLUSTER_HANDLER_SHADE,
|
||||
DATA_ZHA,
|
||||
SIGNAL_ADD_ENTITIES,
|
||||
SIGNAL_ATTR_UPDATED,
|
||||
@ -41,7 +41,7 @@ from .core.registries import ZHA_ENTITIES
|
||||
from .entity import ZhaEntity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .core.channels.base import ZigbeeChannel
|
||||
from .core.cluster_handlers import ClusterHandler
|
||||
from .core.device import ZHADevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -67,21 +67,21 @@ async def async_setup_entry(
|
||||
config_entry.async_on_unload(unsub)
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names=CHANNEL_COVER)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_COVER)
|
||||
class ZhaCover(ZhaEntity, CoverEntity):
|
||||
"""Representation of a ZHA cover."""
|
||||
|
||||
def __init__(self, unique_id, zha_device, channels, **kwargs):
|
||||
def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs):
|
||||
"""Init this sensor."""
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
self._cover_channel = self.cluster_channels.get(CHANNEL_COVER)
|
||||
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
|
||||
self._cover_cluster_handler = self.cluster_handlers.get(CLUSTER_HANDLER_COVER)
|
||||
self._current_position = None
|
||||
|
||||
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._cover_channel, SIGNAL_ATTR_UPDATED, self.async_set_position
|
||||
self._cover_cluster_handler, SIGNAL_ATTR_UPDATED, self.async_set_position
|
||||
)
|
||||
|
||||
@callback
|
||||
@ -118,7 +118,7 @@ class ZhaCover(ZhaEntity, CoverEntity):
|
||||
|
||||
@callback
|
||||
def async_set_position(self, attr_id, attr_name, value):
|
||||
"""Handle position update from channel."""
|
||||
"""Handle position update from cluster handler."""
|
||||
_LOGGER.debug("setting position: %s", value)
|
||||
self._current_position = 100 - value
|
||||
if self._current_position == 0:
|
||||
@ -129,27 +129,27 @@ class ZhaCover(ZhaEntity, CoverEntity):
|
||||
|
||||
@callback
|
||||
def async_update_state(self, state):
|
||||
"""Handle state update from channel."""
|
||||
"""Handle state update from cluster handler."""
|
||||
_LOGGER.debug("state=%s", state)
|
||||
self._state = state
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the window cover."""
|
||||
res = await self._cover_channel.up_open()
|
||||
res = await self._cover_cluster_handler.up_open()
|
||||
if not isinstance(res, Exception) and res[1] is Status.SUCCESS:
|
||||
self.async_update_state(STATE_OPENING)
|
||||
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close the window cover."""
|
||||
res = await self._cover_channel.down_close()
|
||||
res = await self._cover_cluster_handler.down_close()
|
||||
if not isinstance(res, Exception) and res[1] is Status.SUCCESS:
|
||||
self.async_update_state(STATE_CLOSING)
|
||||
|
||||
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._cover_channel.go_to_lift_percentage(100 - new_pos)
|
||||
res = await self._cover_cluster_handler.go_to_lift_percentage(100 - new_pos)
|
||||
if not isinstance(res, Exception) and res[1] is Status.SUCCESS:
|
||||
self.async_update_state(
|
||||
STATE_CLOSING if new_pos < self._current_position else STATE_OPENING
|
||||
@ -157,7 +157,7 @@ class ZhaCover(ZhaEntity, CoverEntity):
|
||||
|
||||
async def async_stop_cover(self, **kwargs: Any) -> None:
|
||||
"""Stop the window cover."""
|
||||
res = await self._cover_channel.stop()
|
||||
res = await self._cover_cluster_handler.stop()
|
||||
if not isinstance(res, Exception) and res[1] is Status.SUCCESS:
|
||||
self._state = STATE_OPEN if self._current_position > 0 else STATE_CLOSED
|
||||
self.async_write_ha_state()
|
||||
@ -170,8 +170,8 @@ class ZhaCover(ZhaEntity, CoverEntity):
|
||||
async def async_get_state(self, from_cache=True):
|
||||
"""Fetch the current state."""
|
||||
_LOGGER.debug("polling current state")
|
||||
if self._cover_channel:
|
||||
pos = await self._cover_channel.get_attribute_value(
|
||||
if self._cover_cluster_handler:
|
||||
pos = await self._cover_cluster_handler.get_attribute_value(
|
||||
"current_position_lift_percentage", from_cache=from_cache
|
||||
)
|
||||
_LOGGER.debug("read pos=%s", pos)
|
||||
@ -186,7 +186,13 @@ class ZhaCover(ZhaEntity, CoverEntity):
|
||||
self._state = None
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names={CHANNEL_LEVEL, CHANNEL_ON_OFF, CHANNEL_SHADE})
|
||||
@MULTI_MATCH(
|
||||
cluster_handler_names={
|
||||
CLUSTER_HANDLER_LEVEL,
|
||||
CLUSTER_HANDLER_ON_OFF,
|
||||
CLUSTER_HANDLER_SHADE,
|
||||
}
|
||||
)
|
||||
class Shade(ZhaEntity, CoverEntity):
|
||||
"""ZHA Shade."""
|
||||
|
||||
@ -196,13 +202,13 @@ class Shade(ZhaEntity, CoverEntity):
|
||||
self,
|
||||
unique_id: str,
|
||||
zha_device: ZHADevice,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Initialize the ZHA light."""
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
self._on_off_channel = self.cluster_channels[CHANNEL_ON_OFF]
|
||||
self._level_channel = self.cluster_channels[CHANNEL_LEVEL]
|
||||
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
|
||||
|
||||
@ -225,10 +231,12 @@ class Shade(ZhaEntity, CoverEntity):
|
||||
"""Run when about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_accept_signal(
|
||||
self._on_off_channel, SIGNAL_ATTR_UPDATED, self.async_set_open_closed
|
||||
self._on_off_cluster_handler,
|
||||
SIGNAL_ATTR_UPDATED,
|
||||
self.async_set_open_closed,
|
||||
)
|
||||
self.async_accept_signal(
|
||||
self._level_channel, SIGNAL_SET_LEVEL, self.async_set_level
|
||||
self._level_cluster_handler, SIGNAL_SET_LEVEL, self.async_set_level
|
||||
)
|
||||
|
||||
@callback
|
||||
@ -253,7 +261,7 @@ class Shade(ZhaEntity, CoverEntity):
|
||||
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the window cover."""
|
||||
res = await self._on_off_channel.on()
|
||||
res = await self._on_off_cluster_handler.on()
|
||||
if isinstance(res, Exception) or res[1] != Status.SUCCESS:
|
||||
self.debug("couldn't open cover: %s", res)
|
||||
return
|
||||
@ -263,7 +271,7 @@ class Shade(ZhaEntity, CoverEntity):
|
||||
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close the window cover."""
|
||||
res = await self._on_off_channel.off()
|
||||
res = await self._on_off_cluster_handler.off()
|
||||
if isinstance(res, Exception) or res[1] != Status.SUCCESS:
|
||||
self.debug("couldn't open cover: %s", res)
|
||||
return
|
||||
@ -274,7 +282,7 @@ class Shade(ZhaEntity, CoverEntity):
|
||||
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_channel.move_to_level_with_on_off(
|
||||
res = await self._level_cluster_handler.move_to_level_with_on_off(
|
||||
new_pos * 255 / 100, 1
|
||||
)
|
||||
|
||||
@ -287,14 +295,15 @@ class Shade(ZhaEntity, CoverEntity):
|
||||
|
||||
async def async_stop_cover(self, **kwargs: Any) -> None:
|
||||
"""Stop the cover."""
|
||||
res = await self._level_channel.stop()
|
||||
res = await self._level_cluster_handler.stop()
|
||||
if isinstance(res, Exception) or res[1] != Status.SUCCESS:
|
||||
self.debug("couldn't stop cover: %s", res)
|
||||
return
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names={CHANNEL_LEVEL, CHANNEL_ON_OFF}, manufacturers="Keen Home Inc"
|
||||
cluster_handler_names={CLUSTER_HANDLER_LEVEL, CLUSTER_HANDLER_ON_OFF},
|
||||
manufacturers="Keen Home Inc",
|
||||
)
|
||||
class KeenVent(Shade):
|
||||
"""Keen vent cover."""
|
||||
@ -305,8 +314,10 @@ class KeenVent(Shade):
|
||||
"""Open the cover."""
|
||||
position = self._position or 100
|
||||
tasks = [
|
||||
self._level_channel.move_to_level_with_on_off(position * 255 / 100, 1),
|
||||
self._on_off_channel.on(),
|
||||
self._level_cluster_handler.move_to_level_with_on_off(
|
||||
position * 255 / 100, 1
|
||||
),
|
||||
self._on_off_cluster_handler.on(),
|
||||
]
|
||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||
if any(isinstance(result, Exception) for result in results):
|
||||
|
@ -12,8 +12,11 @@ from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
|
||||
|
||||
from . import DOMAIN
|
||||
from .core.channels.manufacturerspecific import AllLEDEffectType, SingleLEDEffectType
|
||||
from .core.const import CHANNEL_IAS_WD, CHANNEL_INOVELLI
|
||||
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 .websocket_api import SERVICE_WARNING_DEVICE_SQUAWK, SERVICE_WARNING_DEVICE_WARN
|
||||
|
||||
@ -25,7 +28,7 @@ ATTR_DATA = "data"
|
||||
ATTR_IEEE = "ieee"
|
||||
CONF_ZHA_ACTION_TYPE = "zha_action_type"
|
||||
ZHA_ACTION_TYPE_SERVICE_CALL = "service_call"
|
||||
ZHA_ACTION_TYPE_CHANNEL_COMMAND = "channel_command"
|
||||
ZHA_ACTION_TYPE_CLUSTER_HANDLER_COMMAND = "cluster_handler_command"
|
||||
INOVELLI_ALL_LED_EFFECT = "issue_all_led_effect"
|
||||
INOVELLI_INDIVIDUAL_LED_EFFECT = "issue_individual_led_effect"
|
||||
|
||||
@ -67,11 +70,11 @@ ACTION_SCHEMA = vol.Any(
|
||||
)
|
||||
|
||||
DEVICE_ACTIONS = {
|
||||
CHANNEL_IAS_WD: [
|
||||
CLUSTER_HANDLER_IAS_WD: [
|
||||
{CONF_TYPE: ACTION_SQUAWK, CONF_DOMAIN: DOMAIN},
|
||||
{CONF_TYPE: ACTION_WARN, CONF_DOMAIN: DOMAIN},
|
||||
],
|
||||
CHANNEL_INOVELLI: [
|
||||
CLUSTER_HANDLER_INOVELLI: [
|
||||
{CONF_TYPE: INOVELLI_ALL_LED_EFFECT, CONF_DOMAIN: DOMAIN},
|
||||
{CONF_TYPE: INOVELLI_INDIVIDUAL_LED_EFFECT, CONF_DOMAIN: DOMAIN},
|
||||
],
|
||||
@ -80,8 +83,8 @@ DEVICE_ACTIONS = {
|
||||
DEVICE_ACTION_TYPES = {
|
||||
ACTION_SQUAWK: ZHA_ACTION_TYPE_SERVICE_CALL,
|
||||
ACTION_WARN: ZHA_ACTION_TYPE_SERVICE_CALL,
|
||||
INOVELLI_ALL_LED_EFFECT: ZHA_ACTION_TYPE_CHANNEL_COMMAND,
|
||||
INOVELLI_INDIVIDUAL_LED_EFFECT: ZHA_ACTION_TYPE_CHANNEL_COMMAND,
|
||||
INOVELLI_ALL_LED_EFFECT: ZHA_ACTION_TYPE_CLUSTER_HANDLER_COMMAND,
|
||||
INOVELLI_INDIVIDUAL_LED_EFFECT: ZHA_ACTION_TYPE_CLUSTER_HANDLER_COMMAND,
|
||||
}
|
||||
|
||||
DEVICE_ACTION_SCHEMAS = {
|
||||
@ -109,9 +112,9 @@ SERVICE_NAMES = {
|
||||
ACTION_WARN: SERVICE_WARNING_DEVICE_WARN,
|
||||
}
|
||||
|
||||
CHANNEL_MAPPINGS = {
|
||||
INOVELLI_ALL_LED_EFFECT: CHANNEL_INOVELLI,
|
||||
INOVELLI_INDIVIDUAL_LED_EFFECT: CHANNEL_INOVELLI,
|
||||
CLUSTER_HANDLER_MAPPINGS = {
|
||||
INOVELLI_ALL_LED_EFFECT: CLUSTER_HANDLER_INOVELLI,
|
||||
INOVELLI_INDIVIDUAL_LED_EFFECT: CLUSTER_HANDLER_INOVELLI,
|
||||
}
|
||||
|
||||
|
||||
@ -144,16 +147,16 @@ async def async_get_actions(
|
||||
zha_device = async_get_zha_device(hass, device_id)
|
||||
except (KeyError, AttributeError):
|
||||
return []
|
||||
cluster_channels = [
|
||||
cluster_handlers = [
|
||||
ch.name
|
||||
for pool in zha_device.channels.pools
|
||||
for ch in pool.claimed_channels.values()
|
||||
for endpoint in zha_device.endpoints.values()
|
||||
for ch in endpoint.claimed_cluster_handlers.values()
|
||||
]
|
||||
actions = [
|
||||
action
|
||||
for channel, channel_actions in DEVICE_ACTIONS.items()
|
||||
for action in channel_actions
|
||||
if channel in cluster_channels
|
||||
for cluster_handler, cluster_handler_actions in DEVICE_ACTIONS.items()
|
||||
for action in cluster_handler_actions
|
||||
if cluster_handler in cluster_handlers
|
||||
]
|
||||
for action in actions:
|
||||
action[CONF_DEVICE_ID] = device_id
|
||||
@ -188,42 +191,42 @@ async def _execute_service_based_action(
|
||||
)
|
||||
|
||||
|
||||
async def _execute_channel_command_based_action(
|
||||
async def _execute_cluster_handler_command_based_action(
|
||||
hass: HomeAssistant,
|
||||
config: dict[str, Any],
|
||||
variables: TemplateVarsType,
|
||||
context: Context | None,
|
||||
) -> None:
|
||||
action_type = config[CONF_TYPE]
|
||||
channel_name = CHANNEL_MAPPINGS[action_type]
|
||||
cluster_handler_name = CLUSTER_HANDLER_MAPPINGS[action_type]
|
||||
try:
|
||||
zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID])
|
||||
except (KeyError, AttributeError):
|
||||
return
|
||||
|
||||
action_channel = None
|
||||
for pool in zha_device.channels.pools:
|
||||
for channel in pool.all_channels.values():
|
||||
if channel.name == channel_name:
|
||||
action_channel = channel
|
||||
action_cluster_handler = None
|
||||
for endpoint in zha_device.endpoints.values():
|
||||
for cluster_handler in endpoint.all_cluster_handlers.values():
|
||||
if cluster_handler.name == cluster_handler_name:
|
||||
action_cluster_handler = cluster_handler
|
||||
break
|
||||
|
||||
if action_channel is None:
|
||||
if action_cluster_handler is None:
|
||||
raise InvalidDeviceAutomationConfig(
|
||||
f"Unable to execute channel action - channel: {channel_name} action:"
|
||||
f"Unable to execute cluster handler action - cluster handler: {cluster_handler_name} action:"
|
||||
f" {action_type}"
|
||||
)
|
||||
|
||||
if not hasattr(action_channel, action_type):
|
||||
if not hasattr(action_cluster_handler, action_type):
|
||||
raise InvalidDeviceAutomationConfig(
|
||||
f"Unable to execute channel action - channel: {channel_name} action:"
|
||||
f"Unable to execute cluster handler - cluster handler: {cluster_handler_name} action:"
|
||||
f" {action_type}"
|
||||
)
|
||||
|
||||
await getattr(action_channel, action_type)(**config)
|
||||
await getattr(action_cluster_handler, action_type)(**config)
|
||||
|
||||
|
||||
ZHA_ACTION_TYPES = {
|
||||
ZHA_ACTION_TYPE_SERVICE_CALL: _execute_service_based_action,
|
||||
ZHA_ACTION_TYPE_CHANNEL_COMMAND: _execute_channel_command_based_action,
|
||||
ZHA_ACTION_TYPE_CLUSTER_HANDLER_COMMAND: _execute_cluster_handler_command_based_action,
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .core import discovery
|
||||
from .core.const import (
|
||||
CHANNEL_POWER_CONFIGURATION,
|
||||
CLUSTER_HANDLER_POWER_CONFIGURATION,
|
||||
DATA_ZHA,
|
||||
SIGNAL_ADD_ENTITIES,
|
||||
SIGNAL_ATTR_UPDATED,
|
||||
@ -44,16 +44,18 @@ async def async_setup_entry(
|
||||
config_entry.async_on_unload(unsub)
|
||||
|
||||
|
||||
@STRICT_MATCH(channel_names=CHANNEL_POWER_CONFIGURATION)
|
||||
@STRICT_MATCH(cluster_handler_names=CLUSTER_HANDLER_POWER_CONFIGURATION)
|
||||
class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity):
|
||||
"""Represent a tracked device."""
|
||||
|
||||
_attr_should_poll = True # BaseZhaEntity defaults to False
|
||||
|
||||
def __init__(self, unique_id, zha_device, channels, **kwargs):
|
||||
def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs):
|
||||
"""Initialize the ZHA device tracker."""
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
self._battery_channel = self.cluster_channels.get(CHANNEL_POWER_CONFIGURATION)
|
||||
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
|
||||
@ -61,9 +63,9 @@ class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity):
|
||||
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_channel:
|
||||
if self._battery_cluster_handler:
|
||||
self.async_accept_signal(
|
||||
self._battery_channel,
|
||||
self._battery_cluster_handler,
|
||||
SIGNAL_ATTR_UPDATED,
|
||||
self.async_battery_percentage_remaining_updated,
|
||||
)
|
||||
|
@ -34,7 +34,7 @@ from .core.const import (
|
||||
from .core.helpers import LogMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .core.channels.base import ZigbeeChannel
|
||||
from .core.cluster_handlers import ClusterHandler
|
||||
from .core.device import ZHADevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -122,19 +122,19 @@ class BaseZhaEntity(LogMixin, entity.Entity):
|
||||
@callback
|
||||
def async_accept_signal(
|
||||
self,
|
||||
channel: ZigbeeChannel | None,
|
||||
cluster_handler: ClusterHandler | None,
|
||||
signal: str,
|
||||
func: Callable[..., Any],
|
||||
signal_override=False,
|
||||
):
|
||||
"""Accept a signal from a channel."""
|
||||
"""Accept a signal from a cluster handler."""
|
||||
unsub = None
|
||||
if signal_override:
|
||||
unsub = async_dispatcher_connect(self.hass, signal, func)
|
||||
else:
|
||||
assert channel
|
||||
assert cluster_handler
|
||||
unsub = async_dispatcher_connect(
|
||||
self.hass, f"{channel.unique_id}_{signal}", func
|
||||
self.hass, f"{cluster_handler.unique_id}_{signal}", func
|
||||
)
|
||||
self._unsubs.append(unsub)
|
||||
|
||||
@ -152,7 +152,7 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity):
|
||||
"""Initialize subclass.
|
||||
|
||||
:param id_suffix: suffix to add to the unique_id of the entity. Used for multi
|
||||
entities using the same channel/cluster id for the entity.
|
||||
entities using the same cluster handler/cluster id for the entity.
|
||||
"""
|
||||
super().__init_subclass__(**kwargs)
|
||||
if id_suffix:
|
||||
@ -162,7 +162,7 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity):
|
||||
self,
|
||||
unique_id: str,
|
||||
zha_device: ZHADevice,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Init ZHA entity."""
|
||||
@ -174,23 +174,23 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity):
|
||||
.replace("sensor", "")
|
||||
.capitalize()
|
||||
)
|
||||
self.cluster_channels: dict[str, ZigbeeChannel] = {}
|
||||
for channel in channels:
|
||||
self.cluster_channels[channel.name] = channel
|
||||
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,
|
||||
channels: list[ZigbeeChannel],
|
||||
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, channels, **kwargs)
|
||||
return cls(unique_id, zha_device, cluster_handlers, **kwargs)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
@ -220,7 +220,7 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity):
|
||||
self._zha_device.ieee,
|
||||
self.entity_id,
|
||||
self._zha_device,
|
||||
self.cluster_channels,
|
||||
self.cluster_handlers,
|
||||
self.device_info,
|
||||
self.remove_future,
|
||||
)
|
||||
@ -238,9 +238,9 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity):
|
||||
async def async_update(self) -> None:
|
||||
"""Retrieve latest state."""
|
||||
tasks = [
|
||||
channel.async_update()
|
||||
for channel in self.cluster_channels.values()
|
||||
if hasattr(channel, "async_update")
|
||||
cluster_handler.async_update()
|
||||
for cluster_handler in self.cluster_handlers.values()
|
||||
if hasattr(cluster_handler, "async_update")
|
||||
]
|
||||
if tasks:
|
||||
await asyncio.gather(*tasks)
|
||||
|
@ -28,7 +28,12 @@ from homeassistant.util.percentage import (
|
||||
)
|
||||
|
||||
from .core import discovery
|
||||
from .core.const import CHANNEL_FAN, DATA_ZHA, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED
|
||||
from .core.const import (
|
||||
CLUSTER_HANDLER_FAN,
|
||||
DATA_ZHA,
|
||||
SIGNAL_ADD_ENTITIES,
|
||||
SIGNAL_ATTR_UPDATED,
|
||||
)
|
||||
from .core.registries import ZHA_ENTITIES
|
||||
from .entity import ZhaEntity, ZhaGroupEntity
|
||||
|
||||
@ -124,50 +129,52 @@ class BaseFan(FanEntity):
|
||||
|
||||
@callback
|
||||
def async_set_state(self, attr_id, attr_name, value):
|
||||
"""Handle state update from channel."""
|
||||
"""Handle state update from cluster handler."""
|
||||
|
||||
|
||||
@STRICT_MATCH(channel_names=CHANNEL_FAN)
|
||||
@STRICT_MATCH(cluster_handler_names=CLUSTER_HANDLER_FAN)
|
||||
class ZhaFan(BaseFan, ZhaEntity):
|
||||
"""Representation of a ZHA fan."""
|
||||
|
||||
def __init__(self, unique_id, zha_device, channels, **kwargs):
|
||||
def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs):
|
||||
"""Init this sensor."""
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
self._fan_channel = self.cluster_channels.get(CHANNEL_FAN)
|
||||
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_channel, SIGNAL_ATTR_UPDATED, self.async_set_state
|
||||
self._fan_cluster_handler, SIGNAL_ATTR_UPDATED, self.async_set_state
|
||||
)
|
||||
|
||||
@property
|
||||
def percentage(self) -> int | None:
|
||||
"""Return the current speed percentage."""
|
||||
if (
|
||||
self._fan_channel.fan_mode is None
|
||||
or self._fan_channel.fan_mode > SPEED_RANGE[1]
|
||||
self._fan_cluster_handler.fan_mode is None
|
||||
or self._fan_cluster_handler.fan_mode > SPEED_RANGE[1]
|
||||
):
|
||||
return None
|
||||
if self._fan_channel.fan_mode == 0:
|
||||
if self._fan_cluster_handler.fan_mode == 0:
|
||||
return 0
|
||||
return ranged_value_to_percentage(SPEED_RANGE, self._fan_channel.fan_mode)
|
||||
return ranged_value_to_percentage(
|
||||
SPEED_RANGE, self._fan_cluster_handler.fan_mode
|
||||
)
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the current preset mode."""
|
||||
return PRESET_MODES_TO_NAME.get(self._fan_channel.fan_mode)
|
||||
return 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 channel."""
|
||||
"""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_channel.async_set_speed(fan_mode)
|
||||
await self._fan_cluster_handler.async_set_speed(fan_mode)
|
||||
self.async_set_state(0, "fan_mode", fan_mode)
|
||||
|
||||
|
||||
@ -182,7 +189,7 @@ class FanGroup(BaseFan, ZhaGroupEntity):
|
||||
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_channel = group.endpoint[hvac.Fan.cluster_id]
|
||||
self._fan_cluster_handler = group.endpoint[hvac.Fan.cluster_id]
|
||||
self._percentage = None
|
||||
self._preset_mode = None
|
||||
|
||||
@ -199,7 +206,7 @@ class FanGroup(BaseFan, ZhaGroupEntity):
|
||||
async def _async_set_fan_mode(self, fan_mode: int) -> None:
|
||||
"""Set the fan mode for the group."""
|
||||
try:
|
||||
await self._fan_channel.write_attributes({"fan_mode": fan_mode})
|
||||
await self._fan_cluster_handler.write_attributes({"fan_mode": fan_mode})
|
||||
except ZigbeeException as ex:
|
||||
self.error("Could not set fan mode: %s", ex)
|
||||
self.async_set_state(0, "fan_mode", fan_mode)
|
||||
@ -250,22 +257,22 @@ IKEA_PRESET_MODES = list(IKEA_NAME_TO_PRESET_MODE)
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names="ikea_airpurifier",
|
||||
cluster_handler_names="ikea_airpurifier",
|
||||
models={"STARKVIND Air purifier", "STARKVIND Air purifier table"},
|
||||
)
|
||||
class IkeaFan(BaseFan, ZhaEntity):
|
||||
"""Representation of a ZHA fan."""
|
||||
|
||||
def __init__(self, unique_id, zha_device, channels, **kwargs):
|
||||
def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs):
|
||||
"""Init this sensor."""
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
self._fan_channel = self.cluster_channels.get("ikea_airpurifier")
|
||||
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
|
||||
self._fan_cluster_handler = self.cluster_handlers.get("ikea_airpurifier")
|
||||
|
||||
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_channel, SIGNAL_ATTR_UPDATED, self.async_set_state
|
||||
self._fan_cluster_handler, SIGNAL_ATTR_UPDATED, self.async_set_state
|
||||
)
|
||||
|
||||
@property
|
||||
@ -296,18 +303,20 @@ class IkeaFan(BaseFan, ZhaEntity):
|
||||
def percentage(self) -> int | None:
|
||||
"""Return the current speed percentage."""
|
||||
if (
|
||||
self._fan_channel.fan_mode is None
|
||||
or self._fan_channel.fan_mode > IKEA_SPEED_RANGE[1]
|
||||
self._fan_cluster_handler.fan_mode is None
|
||||
or self._fan_cluster_handler.fan_mode > IKEA_SPEED_RANGE[1]
|
||||
):
|
||||
return None
|
||||
if self._fan_channel.fan_mode == 0:
|
||||
if self._fan_cluster_handler.fan_mode == 0:
|
||||
return 0
|
||||
return ranged_value_to_percentage(IKEA_SPEED_RANGE, self._fan_channel.fan_mode)
|
||||
return ranged_value_to_percentage(
|
||||
IKEA_SPEED_RANGE, self._fan_cluster_handler.fan_mode
|
||||
)
|
||||
|
||||
@property
|
||||
def preset_mode(self) -> str | None:
|
||||
"""Return the current preset mode."""
|
||||
return IKEA_PRESET_MODES_TO_NAME.get(self._fan_channel.fan_mode)
|
||||
return IKEA_PRESET_MODES_TO_NAME.get(self._fan_cluster_handler.fan_mode)
|
||||
|
||||
async def async_turn_on(
|
||||
self,
|
||||
@ -328,10 +337,10 @@ class IkeaFan(BaseFan, ZhaEntity):
|
||||
|
||||
@callback
|
||||
def async_set_state(self, attr_id, attr_name, value):
|
||||
"""Handle state update from channel."""
|
||||
"""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_channel.async_set_speed(fan_mode)
|
||||
await self._fan_cluster_handler.async_set_speed(fan_mode)
|
||||
self.async_set_state(0, "fan_mode", fan_mode)
|
||||
|
@ -39,9 +39,9 @@ from homeassistant.helpers.event import async_call_later, async_track_time_inter
|
||||
|
||||
from .core import discovery, helpers
|
||||
from .core.const import (
|
||||
CHANNEL_COLOR,
|
||||
CHANNEL_LEVEL,
|
||||
CHANNEL_ON_OFF,
|
||||
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,
|
||||
@ -130,10 +130,10 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
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_channel = None
|
||||
self._level_channel = None
|
||||
self._color_channel = None
|
||||
self._identify_channel = None
|
||||
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
|
||||
@ -193,7 +193,8 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
execute_if_off_supported = (
|
||||
self._GROUP_SUPPORTS_EXECUTE_IF_OFF
|
||||
if isinstance(self, LightGroup)
|
||||
else self._color_channel and self._color_channel.execute_if_off_supported
|
||||
else self._color_cluster_handler
|
||||
and self._color_cluster_handler.execute_if_off_supported
|
||||
)
|
||||
|
||||
set_transition_flag = (
|
||||
@ -289,7 +290,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
# 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_channel.move_to_level_with_on_off(
|
||||
result = await self._level_cluster_handler.move_to_level_with_on_off(
|
||||
level=DEFAULT_MIN_BRIGHTNESS,
|
||||
transition_time=self._DEFAULT_MIN_TRANSITION_TIME,
|
||||
)
|
||||
@ -329,7 +330,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
and not new_color_provided_while_off
|
||||
and brightness_supported(self._attr_supported_color_modes)
|
||||
):
|
||||
result = await self._level_channel.move_to_level_with_on_off(
|
||||
result = await self._level_cluster_handler.move_to_level_with_on_off(
|
||||
level=level,
|
||||
transition_time=duration,
|
||||
)
|
||||
@ -353,7 +354,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
# since some lights don't always 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_channel.on()
|
||||
result = await self._on_off_cluster_handler.on()
|
||||
t_log["on_off"] = result
|
||||
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
|
||||
# 'On' call failed, but as brightness may still transition
|
||||
@ -383,7 +384,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
if new_color_provided_while_off:
|
||||
# The light is has the correct color, so we can now transition
|
||||
# it to the correct brightness level.
|
||||
result = await self._level_channel.move_to_level(
|
||||
result = await self._level_cluster_handler.move_to_level(
|
||||
level=level, transition_time=duration
|
||||
)
|
||||
t_log["move_to_level_if_color"] = result
|
||||
@ -400,7 +401,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
self.async_transition_start_timer(transition_time)
|
||||
|
||||
if effect == light.EFFECT_COLORLOOP:
|
||||
result = await self._color_channel.color_loop_set(
|
||||
result = await self._color_cluster_handler.color_loop_set(
|
||||
update_flags=(
|
||||
Color.ColorLoopUpdateFlags.Action
|
||||
| Color.ColorLoopUpdateFlags.Direction
|
||||
@ -417,7 +418,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
self._attr_effect == light.EFFECT_COLORLOOP
|
||||
and effect != light.EFFECT_COLORLOOP
|
||||
):
|
||||
result = await self._color_channel.color_loop_set(
|
||||
result = await self._color_cluster_handler.color_loop_set(
|
||||
update_flags=Color.ColorLoopUpdateFlags.Action,
|
||||
action=Color.ColorLoopAction.Deactivate,
|
||||
direction=Color.ColorLoopDirection.Decrement,
|
||||
@ -428,7 +429,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
self._attr_effect = None
|
||||
|
||||
if flash is not None:
|
||||
result = await self._identify_channel.trigger_effect(
|
||||
result = await self._identify_cluster_handler.trigger_effect(
|
||||
effect_id=FLASH_EFFECTS[flash],
|
||||
effect_variant=Identify.EffectVariant.Default,
|
||||
)
|
||||
@ -457,12 +458,12 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
# 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_channel.move_to_level_with_on_off(
|
||||
result = await self._level_cluster_handler.move_to_level_with_on_off(
|
||||
level=0,
|
||||
transition_time=(transition * 10 or self._DEFAULT_MIN_TRANSITION_TIME),
|
||||
)
|
||||
else:
|
||||
result = await self._on_off_channel.off()
|
||||
result = await self._on_off_cluster_handler.off()
|
||||
|
||||
# Pause parsing attribute reports until transition is complete
|
||||
if self._zha_config_enable_light_transitioning_flag:
|
||||
@ -503,7 +504,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
)
|
||||
|
||||
if temperature is not None:
|
||||
result = await self._color_channel.move_to_color_temp(
|
||||
result = await self._color_cluster_handler.move_to_color_temp(
|
||||
color_temp_mireds=temperature,
|
||||
transition_time=transition_time,
|
||||
)
|
||||
@ -518,16 +519,16 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
if hs_color is not None:
|
||||
if (
|
||||
not isinstance(self, LightGroup)
|
||||
and self._color_channel.enhanced_hue_supported
|
||||
and self._color_cluster_handler.enhanced_hue_supported
|
||||
):
|
||||
result = await self._color_channel.enhanced_move_to_hue_and_saturation(
|
||||
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=transition_time,
|
||||
)
|
||||
t_log["enhanced_move_to_hue_and_saturation"] = result
|
||||
else:
|
||||
result = await self._color_channel.move_to_hue_and_saturation(
|
||||
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=transition_time,
|
||||
@ -542,7 +543,7 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
xy_color = None # don't set xy_color if it is also present
|
||||
|
||||
if xy_color is not None:
|
||||
result = await self._color_channel.move_to_color(
|
||||
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=transition_time,
|
||||
@ -620,24 +621,29 @@ class BaseLight(LogMixin, light.LightEntity):
|
||||
)
|
||||
|
||||
|
||||
@STRICT_MATCH(channel_names=CHANNEL_ON_OFF, aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL})
|
||||
@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]
|
||||
_REFRESH_INTERVAL = (45, 75)
|
||||
|
||||
def __init__(self, unique_id, zha_device: ZHADevice, channels, **kwargs) -> None:
|
||||
def __init__(
|
||||
self, unique_id, zha_device: ZHADevice, cluster_handlers, **kwargs
|
||||
) -> None:
|
||||
"""Initialize the ZHA light."""
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
self._on_off_channel = self.cluster_channels[CHANNEL_ON_OFF]
|
||||
self._attr_state = bool(self._on_off_channel.on_off)
|
||||
self._level_channel = self.cluster_channels.get(CHANNEL_LEVEL)
|
||||
self._color_channel = self.cluster_channels.get(CHANNEL_COLOR)
|
||||
self._identify_channel = self.zha_device.channels.identify_ch
|
||||
if self._color_channel:
|
||||
self._attr_min_mireds: int = self._color_channel.min_mireds
|
||||
self._attr_max_mireds: int = self._color_channel.max_mireds
|
||||
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 = []
|
||||
|
||||
@ -649,44 +655,48 @@ class Light(BaseLight, ZhaEntity):
|
||||
)
|
||||
|
||||
self._attr_supported_color_modes = {ColorMode.ONOFF}
|
||||
if self._level_channel:
|
||||
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_channel.current_level
|
||||
self._attr_brightness = self._level_cluster_handler.current_level
|
||||
|
||||
if self._color_channel:
|
||||
if self._color_channel.color_temp_supported:
|
||||
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_channel.color_temperature
|
||||
self._attr_color_temp = self._color_cluster_handler.color_temperature
|
||||
|
||||
if self._color_channel.xy_supported and (
|
||||
if self._color_cluster_handler.xy_supported and (
|
||||
self._zha_config_always_prefer_xy_color_mode
|
||||
or not self._color_channel.hs_supported
|
||||
or not self._color_cluster_handler.hs_supported
|
||||
):
|
||||
self._attr_supported_color_modes.add(ColorMode.XY)
|
||||
curr_x = self._color_channel.current_x
|
||||
curr_y = self._color_channel.current_y
|
||||
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_channel.hs_supported
|
||||
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_channel.enhanced_hue_supported
|
||||
and self._color_channel.enhanced_current_hue is not None
|
||||
self._color_cluster_handler.enhanced_hue_supported
|
||||
and self._color_cluster_handler.enhanced_current_hue is not None
|
||||
):
|
||||
curr_hue = self._color_channel.enhanced_current_hue * 65535 / 360
|
||||
elif self._color_channel.current_hue is not None:
|
||||
curr_hue = self._color_channel.current_hue * 254 / 360
|
||||
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_channel.current_saturation) is None:
|
||||
if (
|
||||
curr_saturation := self._color_cluster_handler.current_saturation
|
||||
) is None:
|
||||
curr_saturation = 0
|
||||
|
||||
self._attr_hs_color = (
|
||||
@ -694,10 +704,10 @@ class Light(BaseLight, ZhaEntity):
|
||||
int(curr_saturation * 2.54),
|
||||
)
|
||||
|
||||
if self._color_channel.color_loop_supported:
|
||||
if self._color_cluster_handler.color_loop_supported:
|
||||
self._attr_supported_features |= light.LightEntityFeature.EFFECT
|
||||
effect_list.append(light.EFFECT_COLORLOOP)
|
||||
if self._color_channel.color_loop_active == 1:
|
||||
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
|
||||
@ -705,13 +715,16 @@ class Light(BaseLight, ZhaEntity):
|
||||
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_channel
|
||||
if self._color_channel.color_mode == Color.ColorMode.Color_temperature:
|
||||
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_channel:
|
||||
if self._identify_cluster_handler:
|
||||
self._attr_supported_features |= light.LightEntityFeature.FLASH
|
||||
|
||||
if effect_list:
|
||||
@ -755,11 +768,11 @@ class Light(BaseLight, ZhaEntity):
|
||||
"""Run when about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_accept_signal(
|
||||
self._on_off_channel, SIGNAL_ATTR_UPDATED, self.async_set_state
|
||||
self._on_off_cluster_handler, SIGNAL_ATTR_UPDATED, self.async_set_state
|
||||
)
|
||||
if self._level_channel:
|
||||
if self._level_cluster_handler:
|
||||
self.async_accept_signal(
|
||||
self._level_channel, SIGNAL_SET_LEVEL, self.set_level
|
||||
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(
|
||||
@ -844,8 +857,8 @@ class Light(BaseLight, ZhaEntity):
|
||||
return
|
||||
self.debug("polling current state")
|
||||
|
||||
if self._on_off_channel:
|
||||
state = await self._on_off_channel.get_attribute_value(
|
||||
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
|
||||
@ -858,8 +871,8 @@ class Light(BaseLight, ZhaEntity):
|
||||
self._off_with_transition = False
|
||||
self._off_brightness = None
|
||||
|
||||
if self._level_channel:
|
||||
level = await self._level_channel.get_attribute_value(
|
||||
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
|
||||
@ -868,7 +881,7 @@ class Light(BaseLight, ZhaEntity):
|
||||
if level is not None:
|
||||
self._attr_brightness = level
|
||||
|
||||
if self._color_channel:
|
||||
if self._color_cluster_handler:
|
||||
attributes = [
|
||||
"color_mode",
|
||||
"current_x",
|
||||
@ -876,23 +889,23 @@ class Light(BaseLight, ZhaEntity):
|
||||
]
|
||||
if (
|
||||
not self._zha_config_always_prefer_xy_color_mode
|
||||
and self._color_channel.enhanced_hue_supported
|
||||
and self._color_cluster_handler.enhanced_hue_supported
|
||||
):
|
||||
attributes.append("enhanced_current_hue")
|
||||
attributes.append("current_saturation")
|
||||
if (
|
||||
self._color_channel.hs_supported
|
||||
and not self._color_channel.enhanced_hue_supported
|
||||
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_channel.color_temp_supported:
|
||||
if self._color_cluster_handler.color_temp_supported:
|
||||
attributes.append("color_temperature")
|
||||
if self._color_channel.color_loop_supported:
|
||||
if self._color_cluster_handler.color_loop_supported:
|
||||
attributes.append("color_loop_active")
|
||||
|
||||
results = await self._color_channel.get_attributes(
|
||||
results = await self._color_cluster_handler.get_attributes(
|
||||
attributes, from_cache=False, only_cache=False
|
||||
)
|
||||
|
||||
@ -915,7 +928,7 @@ class Light(BaseLight, ZhaEntity):
|
||||
and not self._zha_config_always_prefer_xy_color_mode
|
||||
):
|
||||
self._attr_color_mode = ColorMode.HS
|
||||
if self._color_channel.enhanced_hue_supported:
|
||||
if self._color_cluster_handler.enhanced_hue_supported:
|
||||
current_hue = results.get("enhanced_current_hue")
|
||||
else:
|
||||
current_hue = results.get("current_hue")
|
||||
@ -923,7 +936,7 @@ class Light(BaseLight, ZhaEntity):
|
||||
if current_hue is not None and current_saturation is not None:
|
||||
self._attr_hs_color = (
|
||||
int(current_hue * 360 / 65535)
|
||||
if self._color_channel.enhanced_hue_supported
|
||||
if self._color_cluster_handler.enhanced_hue_supported
|
||||
else int(current_hue * 360 / 254),
|
||||
int(current_saturation / 2.54),
|
||||
)
|
||||
@ -1036,8 +1049,8 @@ class Light(BaseLight, ZhaEntity):
|
||||
|
||||
|
||||
@STRICT_MATCH(
|
||||
channel_names=CHANNEL_ON_OFF,
|
||||
aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL},
|
||||
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):
|
||||
@ -1047,8 +1060,8 @@ class HueLight(Light):
|
||||
|
||||
|
||||
@STRICT_MATCH(
|
||||
channel_names=CHANNEL_ON_OFF,
|
||||
aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL},
|
||||
cluster_handler_names=CLUSTER_HANDLER_ON_OFF,
|
||||
aux_cluster_handlers={CLUSTER_HANDLER_COLOR, CLUSTER_HANDLER_LEVEL},
|
||||
manufacturers={"Jasco", "Quotra-Vision", "eWeLight", "eWeLink"},
|
||||
)
|
||||
class ForceOnLight(Light):
|
||||
@ -1058,8 +1071,8 @@ class ForceOnLight(Light):
|
||||
|
||||
|
||||
@STRICT_MATCH(
|
||||
channel_names=CHANNEL_ON_OFF,
|
||||
aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL},
|
||||
cluster_handler_names=CLUSTER_HANDLER_ON_OFF,
|
||||
aux_cluster_handlers={CLUSTER_HANDLER_COLOR, CLUSTER_HANDLER_LEVEL},
|
||||
manufacturers=DEFAULT_MIN_TRANSITION_MANUFACTURERS,
|
||||
)
|
||||
class MinTransitionLight(Light):
|
||||
@ -1089,11 +1102,13 @@ class LightGroup(BaseLight, ZhaGroupEntity):
|
||||
# If at least one member has a color cluster and doesn't support it,
|
||||
# it's not used.
|
||||
for member in group.members:
|
||||
for pool in member.device.channels.pools:
|
||||
for channel in pool.all_channels.values():
|
||||
for (
|
||||
endpoint
|
||||
) in member.device._endpoints.values(): # pylint: disable=protected-access
|
||||
for cluster_handler in endpoint.all_cluster_handlers.values():
|
||||
if (
|
||||
channel.name == CHANNEL_COLOR
|
||||
and not channel.execute_if_off_supported
|
||||
cluster_handler.name == CLUSTER_HANDLER_COLOR
|
||||
and not cluster_handler.execute_if_off_supported
|
||||
):
|
||||
self._GROUP_SUPPORTS_EXECUTE_IF_OFF = False
|
||||
break
|
||||
@ -1102,10 +1117,10 @@ class LightGroup(BaseLight, ZhaGroupEntity):
|
||||
member.device.manufacturer in DEFAULT_MIN_TRANSITION_MANUFACTURERS
|
||||
for member in group.members
|
||||
)
|
||||
self._on_off_channel = group.endpoint[OnOff.cluster_id]
|
||||
self._level_channel = group.endpoint[LevelControl.cluster_id]
|
||||
self._color_channel = group.endpoint[Color.cluster_id]
|
||||
self._identify_channel = group.endpoint[Identify.cluster_id]
|
||||
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,
|
||||
|
@ -19,7 +19,7 @@ from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .core import discovery
|
||||
from .core.const import (
|
||||
CHANNEL_DOORLOCK,
|
||||
CLUSTER_HANDLER_DOORLOCK,
|
||||
DATA_ZHA,
|
||||
SIGNAL_ADD_ENTITIES,
|
||||
SIGNAL_ATTR_UPDATED,
|
||||
@ -92,20 +92,22 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names=CHANNEL_DOORLOCK)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_DOORLOCK)
|
||||
class ZhaDoorLock(ZhaEntity, LockEntity):
|
||||
"""Representation of a ZHA lock."""
|
||||
|
||||
def __init__(self, unique_id, zha_device, channels, **kwargs):
|
||||
def __init__(self, unique_id, zha_device, cluster_handlers, **kwargs):
|
||||
"""Init this sensor."""
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
self._doorlock_channel = self.cluster_channels.get(CHANNEL_DOORLOCK)
|
||||
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_channel, SIGNAL_ATTR_UPDATED, self.async_set_state
|
||||
self._doorlock_cluster_handler, SIGNAL_ATTR_UPDATED, self.async_set_state
|
||||
)
|
||||
|
||||
@callback
|
||||
@ -127,7 +129,7 @@ class ZhaDoorLock(ZhaEntity, LockEntity):
|
||||
|
||||
async def async_lock(self, **kwargs: Any) -> None:
|
||||
"""Lock the lock."""
|
||||
result = await self._doorlock_channel.lock_door()
|
||||
result = await self._doorlock_cluster_handler.lock_door()
|
||||
if isinstance(result, Exception) or result[0] is not Status.SUCCESS:
|
||||
self.error("Error with lock_door: %s", result)
|
||||
return
|
||||
@ -135,7 +137,7 @@ class ZhaDoorLock(ZhaEntity, LockEntity):
|
||||
|
||||
async def async_unlock(self, **kwargs: Any) -> None:
|
||||
"""Unlock the lock."""
|
||||
result = await self._doorlock_channel.unlock_door()
|
||||
result = await self._doorlock_cluster_handler.unlock_door()
|
||||
if isinstance(result, Exception) or result[0] is not Status.SUCCESS:
|
||||
self.error("Error with unlock_door: %s", result)
|
||||
return
|
||||
@ -148,14 +150,14 @@ class ZhaDoorLock(ZhaEntity, LockEntity):
|
||||
|
||||
@callback
|
||||
def async_set_state(self, attr_id, attr_name, value):
|
||||
"""Handle state update from channel."""
|
||||
"""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_channel:
|
||||
state = await self._doorlock_channel.get_attribute_value(
|
||||
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:
|
||||
@ -167,24 +169,26 @@ class ZhaDoorLock(ZhaEntity, LockEntity):
|
||||
|
||||
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_channel:
|
||||
await self._doorlock_channel.async_set_user_code(code_slot, user_code)
|
||||
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)
|
||||
|
||||
async def async_enable_lock_user_code(self, code_slot: int) -> None:
|
||||
"""Enable user_code at index X on the lock."""
|
||||
if self._doorlock_channel:
|
||||
await self._doorlock_channel.async_enable_user_code(code_slot)
|
||||
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)
|
||||
|
||||
async def async_disable_lock_user_code(self, code_slot: int) -> None:
|
||||
"""Disable user_code at index X on the lock."""
|
||||
if self._doorlock_channel:
|
||||
await self._doorlock_channel.async_disable_user_code(code_slot)
|
||||
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)
|
||||
|
||||
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_channel:
|
||||
await self._doorlock_channel.async_clear_user_code(code_slot)
|
||||
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)
|
||||
|
@ -18,11 +18,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .core import discovery
|
||||
from .core.const import (
|
||||
CHANNEL_ANALOG_OUTPUT,
|
||||
CHANNEL_BASIC,
|
||||
CHANNEL_COLOR,
|
||||
CHANNEL_INOVELLI,
|
||||
CHANNEL_LEVEL,
|
||||
CLUSTER_HANDLER_ANALOG_OUTPUT,
|
||||
CLUSTER_HANDLER_BASIC,
|
||||
CLUSTER_HANDLER_COLOR,
|
||||
CLUSTER_HANDLER_INOVELLI,
|
||||
CLUSTER_HANDLER_LEVEL,
|
||||
DATA_ZHA,
|
||||
SIGNAL_ADD_ENTITIES,
|
||||
SIGNAL_ATTR_UPDATED,
|
||||
@ -31,7 +31,7 @@ from .core.registries import ZHA_ENTITIES
|
||||
from .entity import ZhaEntity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .core.channels.base import ZigbeeChannel
|
||||
from .core.cluster_handlers import ClusterHandler
|
||||
from .core.device import ZHADevice
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -275,7 +275,7 @@ async def async_setup_entry(
|
||||
config_entry.async_on_unload(unsub)
|
||||
|
||||
|
||||
@STRICT_MATCH(channel_names=CHANNEL_ANALOG_OUTPUT)
|
||||
@STRICT_MATCH(cluster_handler_names=CLUSTER_HANDLER_ANALOG_OUTPUT)
|
||||
class ZhaNumber(ZhaEntity, NumberEntity):
|
||||
"""Representation of a ZHA Number entity."""
|
||||
|
||||
@ -283,29 +283,33 @@ class ZhaNumber(ZhaEntity, NumberEntity):
|
||||
self,
|
||||
unique_id: str,
|
||||
zha_device: ZHADevice,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Init this entity."""
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
self._analog_output_channel = self.cluster_channels[CHANNEL_ANALOG_OUTPUT]
|
||||
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_channel, SIGNAL_ATTR_UPDATED, self.async_set_state
|
||||
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_channel.present_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_channel.min_present_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
|
||||
@ -313,7 +317,7 @@ class ZhaNumber(ZhaEntity, NumberEntity):
|
||||
@property
|
||||
def native_max_value(self) -> float:
|
||||
"""Return the maximum value."""
|
||||
max_present_value = self._analog_output_channel.max_present_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
|
||||
@ -321,7 +325,7 @@ class ZhaNumber(ZhaEntity, NumberEntity):
|
||||
@property
|
||||
def native_step(self) -> float | None:
|
||||
"""Return the value step."""
|
||||
resolution = self._analog_output_channel.resolution
|
||||
resolution = self._analog_output_cluster_handler.resolution
|
||||
if resolution is not None:
|
||||
return resolution
|
||||
return super().native_step
|
||||
@ -329,7 +333,7 @@ class ZhaNumber(ZhaEntity, NumberEntity):
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the number entity."""
|
||||
description = self._analog_output_channel.description
|
||||
description = self._analog_output_cluster_handler.description
|
||||
if description is not None and len(description) > 0:
|
||||
return f"{super().name} {description}"
|
||||
return super().name
|
||||
@ -337,7 +341,7 @@ class ZhaNumber(ZhaEntity, NumberEntity):
|
||||
@property
|
||||
def icon(self) -> str | None:
|
||||
"""Return the icon to be used for this entity."""
|
||||
application_type = self._analog_output_channel.application_type
|
||||
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
|
||||
@ -345,26 +349,26 @@ class ZhaNumber(ZhaEntity, NumberEntity):
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the unit the value is expressed in."""
|
||||
engineering_units = self._analog_output_channel.engineering_units
|
||||
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 channel."""
|
||||
"""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."""
|
||||
num_value = float(value)
|
||||
if await self._analog_output_channel.async_set_present_value(num_value):
|
||||
if await self._analog_output_cluster_handler.async_set_present_value(num_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_channel:
|
||||
value = await self._analog_output_channel.get_attribute_value(
|
||||
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)
|
||||
@ -383,17 +387,17 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity):
|
||||
cls,
|
||||
unique_id: str,
|
||||
zha_device: ZHADevice,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
**kwargs: Any,
|
||||
) -> Self | None:
|
||||
"""Entity Factory.
|
||||
|
||||
Return entity if it is a supported configuration, otherwise return None
|
||||
"""
|
||||
channel = channels[0]
|
||||
cluster_handler = cluster_handlers[0]
|
||||
if (
|
||||
cls._zcl_attribute in channel.cluster.unsupported_attributes
|
||||
or channel.cluster.get(cls._zcl_attribute) is None
|
||||
cls._zcl_attribute in cluster_handler.cluster.unsupported_attributes
|
||||
or cluster_handler.cluster.get(cls._zcl_attribute) is None
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s is not supported - skipping %s entity creation",
|
||||
@ -402,28 +406,31 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity):
|
||||
)
|
||||
return None
|
||||
|
||||
return cls(unique_id, zha_device, channels, **kwargs)
|
||||
return cls(unique_id, zha_device, cluster_handlers, **kwargs)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
unique_id: str,
|
||||
zha_device: ZHADevice,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Init this number configuration entity."""
|
||||
self._channel: ZigbeeChannel = channels[0]
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
self._cluster_handler: ClusterHandler = cluster_handlers[0]
|
||||
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float:
|
||||
"""Return the current value."""
|
||||
return self._channel.cluster.get(self._zcl_attribute) * self._attr_multiplier
|
||||
return (
|
||||
self._cluster_handler.cluster.get(self._zcl_attribute)
|
||||
* self._attr_multiplier
|
||||
)
|
||||
|
||||
async def async_set_native_value(self, value: float) -> None:
|
||||
"""Update the current value from HA."""
|
||||
try:
|
||||
res = await self._channel.cluster.write_attributes(
|
||||
res = await self._cluster_handler.cluster.write_attributes(
|
||||
{self._zcl_attribute: int(value / self._attr_multiplier)}
|
||||
)
|
||||
except zigpy.exceptions.ZigbeeException as ex:
|
||||
@ -438,15 +445,16 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity):
|
||||
"""Attempt to retrieve the state of the entity."""
|
||||
await super().async_update()
|
||||
_LOGGER.debug("polling current state")
|
||||
if self._channel:
|
||||
value = await self._channel.get_attribute_value(
|
||||
if self._cluster_handler:
|
||||
value = await self._cluster_handler.get_attribute_value(
|
||||
self._zcl_attribute, from_cache=False
|
||||
)
|
||||
_LOGGER.debug("read value=%s", value)
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names="opple_cluster", models={"lumi.motion.ac02", "lumi.motion.agl04"}
|
||||
cluster_handler_names="opple_cluster",
|
||||
models={"lumi.motion.ac02", "lumi.motion.agl04"},
|
||||
)
|
||||
class AqaraMotionDetectionInterval(
|
||||
ZHANumberConfigurationEntity, id_suffix="detection_interval"
|
||||
@ -459,7 +467,7 @@ class AqaraMotionDetectionInterval(
|
||||
_attr_name = "Detection interval"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_LEVEL)
|
||||
class OnOffTransitionTimeConfigurationEntity(
|
||||
ZHANumberConfigurationEntity, id_suffix="on_off_transition_time"
|
||||
):
|
||||
@ -471,7 +479,7 @@ class OnOffTransitionTimeConfigurationEntity(
|
||||
_attr_name = "On/Off transition time"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_LEVEL)
|
||||
class OnLevelConfigurationEntity(ZHANumberConfigurationEntity, id_suffix="on_level"):
|
||||
"""Representation of a ZHA on level configuration entity."""
|
||||
|
||||
@ -481,7 +489,7 @@ class OnLevelConfigurationEntity(ZHANumberConfigurationEntity, id_suffix="on_lev
|
||||
_attr_name = "On level"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_LEVEL)
|
||||
class OnTransitionTimeConfigurationEntity(
|
||||
ZHANumberConfigurationEntity, id_suffix="on_transition_time"
|
||||
):
|
||||
@ -493,7 +501,7 @@ class OnTransitionTimeConfigurationEntity(
|
||||
_attr_name = "On transition time"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_LEVEL)
|
||||
class OffTransitionTimeConfigurationEntity(
|
||||
ZHANumberConfigurationEntity, id_suffix="off_transition_time"
|
||||
):
|
||||
@ -505,7 +513,7 @@ class OffTransitionTimeConfigurationEntity(
|
||||
_attr_name = "Off transition time"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_LEVEL)
|
||||
class DefaultMoveRateConfigurationEntity(
|
||||
ZHANumberConfigurationEntity, id_suffix="default_move_rate"
|
||||
):
|
||||
@ -517,7 +525,7 @@ class DefaultMoveRateConfigurationEntity(
|
||||
_attr_name = "Default move rate"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_LEVEL)
|
||||
class StartUpCurrentLevelConfigurationEntity(
|
||||
ZHANumberConfigurationEntity, id_suffix="start_up_current_level"
|
||||
):
|
||||
@ -529,7 +537,7 @@ class StartUpCurrentLevelConfigurationEntity(
|
||||
_attr_name = "Start-up current level"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_COLOR)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_COLOR)
|
||||
class StartUpColorTemperatureConfigurationEntity(
|
||||
ZHANumberConfigurationEntity, id_suffix="start_up_color_temperature"
|
||||
):
|
||||
@ -544,18 +552,18 @@ class StartUpColorTemperatureConfigurationEntity(
|
||||
self,
|
||||
unique_id: str,
|
||||
zha_device: ZHADevice,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Init this ZHA startup color temperature entity."""
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
if self._channel:
|
||||
self._attr_native_min_value: float = self._channel.min_mireds
|
||||
self._attr_native_max_value: float = self._channel.max_mireds
|
||||
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(
|
||||
channel_names="tuya_manufacturer",
|
||||
cluster_handler_names="tuya_manufacturer",
|
||||
manufacturers={
|
||||
"_TZE200_htnnfasr",
|
||||
},
|
||||
@ -572,7 +580,7 @@ class TimerDurationMinutes(ZHANumberConfigurationEntity, id_suffix="timer_durati
|
||||
_attr_name = "Timer duration"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names="ikea_airpurifier")
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names="ikea_airpurifier")
|
||||
class FilterLifeTime(ZHANumberConfigurationEntity, id_suffix="filter_life_time"):
|
||||
"""Representation of a ZHA filter lifetime configuration entity."""
|
||||
|
||||
@ -586,7 +594,7 @@ class FilterLifeTime(ZHANumberConfigurationEntity, id_suffix="filter_life_time")
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_BASIC,
|
||||
cluster_handler_names=CLUSTER_HANDLER_BASIC,
|
||||
manufacturers={"TexasInstruments"},
|
||||
models={"ti.router"},
|
||||
)
|
||||
@ -599,7 +607,7 @@ class TiRouterTransmitPower(ZHANumberConfigurationEntity, id_suffix="transmit_po
|
||||
_attr_name = "Transmit power"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI)
|
||||
class InovelliRemoteDimmingUpSpeed(
|
||||
ZHANumberConfigurationEntity, id_suffix="dimming_speed_up_remote"
|
||||
):
|
||||
@ -613,7 +621,7 @@ class InovelliRemoteDimmingUpSpeed(
|
||||
_attr_name: str = "Remote dimming up speed"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI)
|
||||
class InovelliButtonDelay(ZHANumberConfigurationEntity, id_suffix="button_delay"):
|
||||
"""Inovelli button delay configuration entity."""
|
||||
|
||||
@ -625,7 +633,7 @@ class InovelliButtonDelay(ZHANumberConfigurationEntity, id_suffix="button_delay"
|
||||
_attr_name: str = "Button delay"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI)
|
||||
class InovelliLocalDimmingUpSpeed(
|
||||
ZHANumberConfigurationEntity, id_suffix="dimming_speed_up_local"
|
||||
):
|
||||
@ -639,7 +647,7 @@ class InovelliLocalDimmingUpSpeed(
|
||||
_attr_name: str = "Local dimming up speed"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI)
|
||||
class InovelliLocalRampRateOffToOn(
|
||||
ZHANumberConfigurationEntity, id_suffix="ramp_rate_off_to_on_local"
|
||||
):
|
||||
@ -653,7 +661,7 @@ class InovelliLocalRampRateOffToOn(
|
||||
_attr_name: str = "Local ramp rate off to on"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI)
|
||||
class InovelliRemoteDimmingSpeedOffToOn(
|
||||
ZHANumberConfigurationEntity, id_suffix="ramp_rate_off_to_on_remote"
|
||||
):
|
||||
@ -667,7 +675,7 @@ class InovelliRemoteDimmingSpeedOffToOn(
|
||||
_attr_name: str = "Remote ramp rate off to on"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI)
|
||||
class InovelliRemoteDimmingDownSpeed(
|
||||
ZHANumberConfigurationEntity, id_suffix="dimming_speed_down_remote"
|
||||
):
|
||||
@ -681,7 +689,7 @@ class InovelliRemoteDimmingDownSpeed(
|
||||
_attr_name: str = "Remote dimming down speed"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI)
|
||||
class InovelliLocalDimmingDownSpeed(
|
||||
ZHANumberConfigurationEntity, id_suffix="dimming_speed_down_local"
|
||||
):
|
||||
@ -695,7 +703,7 @@ class InovelliLocalDimmingDownSpeed(
|
||||
_attr_name: str = "Local dimming down speed"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI)
|
||||
class InovelliLocalRampRateOnToOff(
|
||||
ZHANumberConfigurationEntity, id_suffix="ramp_rate_on_to_off_local"
|
||||
):
|
||||
@ -709,7 +717,7 @@ class InovelliLocalRampRateOnToOff(
|
||||
_attr_name: str = "Local ramp rate on to off"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI)
|
||||
class InovelliRemoteDimmingSpeedOnToOff(
|
||||
ZHANumberConfigurationEntity, id_suffix="ramp_rate_on_to_off_remote"
|
||||
):
|
||||
@ -723,7 +731,7 @@ class InovelliRemoteDimmingSpeedOnToOff(
|
||||
_attr_name: str = "Remote ramp rate on to off"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI)
|
||||
class InovelliMinimumLoadDimmingLevel(
|
||||
ZHANumberConfigurationEntity, id_suffix="minimum_level"
|
||||
):
|
||||
@ -737,7 +745,7 @@ class InovelliMinimumLoadDimmingLevel(
|
||||
_attr_name: str = "Minimum load dimming level"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI)
|
||||
class InovelliMaximumLoadDimmingLevel(
|
||||
ZHANumberConfigurationEntity, id_suffix="maximum_level"
|
||||
):
|
||||
@ -751,7 +759,7 @@ class InovelliMaximumLoadDimmingLevel(
|
||||
_attr_name: str = "Maximum load dimming level"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI)
|
||||
class InovelliAutoShutoffTimer(
|
||||
ZHANumberConfigurationEntity, id_suffix="auto_off_timer"
|
||||
):
|
||||
@ -765,7 +773,7 @@ class InovelliAutoShutoffTimer(
|
||||
_attr_name: str = "Automatic switch shutoff timer"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI)
|
||||
class InovelliLoadLevelIndicatorTimeout(
|
||||
ZHANumberConfigurationEntity, id_suffix="load_level_indicator_timeout"
|
||||
):
|
||||
@ -779,7 +787,7 @@ class InovelliLoadLevelIndicatorTimeout(
|
||||
_attr_name: str = "Load level indicator timeout"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI)
|
||||
class InovelliDefaultAllLEDOnColor(
|
||||
ZHANumberConfigurationEntity, id_suffix="led_color_when_on"
|
||||
):
|
||||
@ -793,7 +801,7 @@ class InovelliDefaultAllLEDOnColor(
|
||||
_attr_name: str = "Default all LED on color"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI)
|
||||
class InovelliDefaultAllLEDOffColor(
|
||||
ZHANumberConfigurationEntity, id_suffix="led_color_when_off"
|
||||
):
|
||||
@ -807,7 +815,7 @@ class InovelliDefaultAllLEDOffColor(
|
||||
_attr_name: str = "Default all LED off color"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI)
|
||||
class InovelliDefaultAllLEDOnIntensity(
|
||||
ZHANumberConfigurationEntity, id_suffix="led_intensity_when_on"
|
||||
):
|
||||
@ -821,7 +829,7 @@ class InovelliDefaultAllLEDOnIntensity(
|
||||
_attr_name: str = "Default all LED on intensity"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI)
|
||||
class InovelliDefaultAllLEDOffIntensity(
|
||||
ZHANumberConfigurationEntity, id_suffix="led_intensity_when_off"
|
||||
):
|
||||
@ -835,7 +843,7 @@ class InovelliDefaultAllLEDOffIntensity(
|
||||
_attr_name: str = "Default all LED off intensity"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI)
|
||||
class InovelliDoubleTapUpLevel(
|
||||
ZHANumberConfigurationEntity, id_suffix="double_tap_up_level"
|
||||
):
|
||||
@ -849,7 +857,7 @@ class InovelliDoubleTapUpLevel(
|
||||
_attr_name: str = "Double tap up level"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI)
|
||||
class InovelliDoubleTapDownLevel(
|
||||
ZHANumberConfigurationEntity, id_suffix="double_tap_down_level"
|
||||
):
|
||||
@ -863,7 +871,9 @@ class InovelliDoubleTapDownLevel(
|
||||
_attr_name: str = "Double tap down level"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"})
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"}
|
||||
)
|
||||
class AqaraPetFeederServingSize(ZHANumberConfigurationEntity, id_suffix="serving_size"):
|
||||
"""Aqara pet feeder serving size configuration entity."""
|
||||
|
||||
@ -876,7 +886,9 @@ class AqaraPetFeederServingSize(ZHANumberConfigurationEntity, id_suffix="serving
|
||||
_attr_icon: str = "mdi:counter"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"})
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"}
|
||||
)
|
||||
class AqaraPetFeederPortionWeight(
|
||||
ZHANumberConfigurationEntity, id_suffix="portion_weight"
|
||||
):
|
||||
@ -892,7 +904,9 @@ class AqaraPetFeederPortionWeight(
|
||||
_attr_icon: str = "mdi:weight-gram"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"lumi.airrtc.agl001"})
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
cluster_handler_names="opple_cluster", models={"lumi.airrtc.agl001"}
|
||||
)
|
||||
class AqaraThermostatAwayTemp(
|
||||
ZHANumberConfigurationEntity, id_suffix="away_preset_temperature"
|
||||
):
|
||||
|
@ -20,10 +20,10 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .core import discovery
|
||||
from .core.const import (
|
||||
CHANNEL_IAS_WD,
|
||||
CHANNEL_INOVELLI,
|
||||
CHANNEL_OCCUPANCY,
|
||||
CHANNEL_ON_OFF,
|
||||
CLUSTER_HANDLER_IAS_WD,
|
||||
CLUSTER_HANDLER_INOVELLI,
|
||||
CLUSTER_HANDLER_OCCUPANCY,
|
||||
CLUSTER_HANDLER_ON_OFF,
|
||||
DATA_ZHA,
|
||||
SIGNAL_ADD_ENTITIES,
|
||||
SIGNAL_ATTR_UPDATED,
|
||||
@ -33,7 +33,7 @@ from .core.registries import ZHA_ENTITIES
|
||||
from .entity import ZhaEntity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .core.channels.base import ZigbeeChannel
|
||||
from .core.cluster_handlers import ClusterHandler
|
||||
from .core.device import ZHADevice
|
||||
|
||||
|
||||
@ -74,33 +74,35 @@ class ZHAEnumSelectEntity(ZhaEntity, SelectEntity):
|
||||
self,
|
||||
unique_id: str,
|
||||
zha_device: ZHADevice,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Init this select entity."""
|
||||
self._attribute = self._enum.__name__
|
||||
self._attr_options = [entry.name.replace("_", " ") for entry in self._enum]
|
||||
self._channel: ZigbeeChannel = channels[0]
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
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._channel.data_cache.get(self._attribute)
|
||||
option = self._cluster_handler.data_cache.get(self._attribute)
|
||||
if option is None:
|
||||
return None
|
||||
return option.name.replace("_", " ")
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
self._channel.data_cache[self._attribute] = self._enum[option.replace(" ", "_")]
|
||||
self._cluster_handler.data_cache[self._attribute] = 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._channel.data_cache[self._attribute] = self._enum[
|
||||
self._cluster_handler.data_cache[self._attribute] = self._enum[
|
||||
last_state.state.replace(" ", "_")
|
||||
]
|
||||
|
||||
@ -114,7 +116,7 @@ class ZHANonZCLSelectEntity(ZHAEnumSelectEntity):
|
||||
return True
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_IAS_WD)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_IAS_WD)
|
||||
class ZHADefaultToneSelectEntity(
|
||||
ZHANonZCLSelectEntity, id_suffix=IasWd.Warning.WarningMode.__name__
|
||||
):
|
||||
@ -124,7 +126,7 @@ class ZHADefaultToneSelectEntity(
|
||||
_attr_name = "Default siren tone"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_IAS_WD)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_IAS_WD)
|
||||
class ZHADefaultSirenLevelSelectEntity(
|
||||
ZHANonZCLSelectEntity, id_suffix=IasWd.Warning.SirenLevel.__name__
|
||||
):
|
||||
@ -134,7 +136,7 @@ class ZHADefaultSirenLevelSelectEntity(
|
||||
_attr_name = "Default siren level"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_IAS_WD)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_IAS_WD)
|
||||
class ZHADefaultStrobeLevelSelectEntity(
|
||||
ZHANonZCLSelectEntity, id_suffix=IasWd.StrobeLevel.__name__
|
||||
):
|
||||
@ -144,7 +146,7 @@ class ZHADefaultStrobeLevelSelectEntity(
|
||||
_attr_name = "Default strobe level"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_IAS_WD)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_IAS_WD)
|
||||
class ZHADefaultStrobeSelectEntity(ZHANonZCLSelectEntity, id_suffix=Strobe.__name__):
|
||||
"""Representation of a ZHA default siren strobe select entity."""
|
||||
|
||||
@ -164,17 +166,17 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity):
|
||||
cls,
|
||||
unique_id: str,
|
||||
zha_device: ZHADevice,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
**kwargs: Any,
|
||||
) -> Self | None:
|
||||
"""Entity Factory.
|
||||
|
||||
Return entity if it is a supported configuration, otherwise return None
|
||||
"""
|
||||
channel = channels[0]
|
||||
cluster_handler = cluster_handlers[0]
|
||||
if (
|
||||
cls._select_attr in channel.cluster.unsupported_attributes
|
||||
or channel.cluster.get(cls._select_attr) is None
|
||||
cls._select_attr in cluster_handler.cluster.unsupported_attributes
|
||||
or cluster_handler.cluster.get(cls._select_attr) is None
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s is not supported - skipping %s entity creation",
|
||||
@ -183,24 +185,24 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity):
|
||||
)
|
||||
return None
|
||||
|
||||
return cls(unique_id, zha_device, channels, **kwargs)
|
||||
return cls(unique_id, zha_device, cluster_handlers, **kwargs)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
unique_id: str,
|
||||
zha_device: ZHADevice,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Init this select entity."""
|
||||
self._attr_options = [entry.name.replace("_", " ") for entry in self._enum]
|
||||
self._channel: ZigbeeChannel = channels[0]
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
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._channel.cluster.get(self._select_attr)
|
||||
option = self._cluster_handler.cluster.get(self._select_attr)
|
||||
if option is None:
|
||||
return None
|
||||
option = self._enum(option)
|
||||
@ -208,7 +210,7 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity):
|
||||
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Change the selected option."""
|
||||
await self._channel.cluster.write_attributes(
|
||||
await self._cluster_handler.cluster.write_attributes(
|
||||
{self._select_attr: self._enum[option.replace(" ", "_")]}
|
||||
)
|
||||
self.async_write_ha_state()
|
||||
@ -217,16 +219,16 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity):
|
||||
"""Run when about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_accept_signal(
|
||||
self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state
|
||||
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 channel."""
|
||||
"""Handle state update from cluster handler."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_ON_OFF)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_ON_OFF)
|
||||
class ZHAStartupOnOffSelectEntity(
|
||||
ZCLEnumSelectEntity, id_suffix=OnOff.StartUpOnOff.__name__
|
||||
):
|
||||
@ -246,11 +248,11 @@ class TuyaPowerOnState(types.enum8):
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_ON_OFF,
|
||||
cluster_handler_names=CLUSTER_HANDLER_ON_OFF,
|
||||
models={"TS011F", "TS0121", "TS0001", "TS0002", "TS0003", "TS0004"},
|
||||
)
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names="tuya_manufacturer",
|
||||
cluster_handler_names="tuya_manufacturer",
|
||||
manufacturers={
|
||||
"_TZE200_7tdtqgwv",
|
||||
"_TZE200_amp6tsvy",
|
||||
@ -287,7 +289,7 @@ class TuyaBacklightMode(types.enum8):
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_ON_OFF,
|
||||
cluster_handler_names=CLUSTER_HANDLER_ON_OFF,
|
||||
models={"TS011F", "TS0121", "TS0001", "TS0002", "TS0003", "TS0004"},
|
||||
)
|
||||
class TuyaBacklightModeSelectEntity(ZCLEnumSelectEntity, id_suffix="backlight_mode"):
|
||||
@ -308,7 +310,7 @@ class MoesBacklightMode(types.enum8):
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names="tuya_manufacturer",
|
||||
cluster_handler_names="tuya_manufacturer",
|
||||
manufacturers={
|
||||
"_TZE200_7tdtqgwv",
|
||||
"_TZE200_amp6tsvy",
|
||||
@ -345,7 +347,7 @@ class AqaraMotionSensitivities(types.enum8):
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names="opple_cluster",
|
||||
cluster_handler_names="opple_cluster",
|
||||
models={"lumi.motion.ac01", "lumi.motion.ac02", "lumi.motion.agl04"},
|
||||
)
|
||||
class AqaraMotionSensitivity(ZCLEnumSelectEntity, id_suffix="motion_sensitivity"):
|
||||
@ -365,7 +367,7 @@ class HueV1MotionSensitivities(types.enum8):
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_OCCUPANCY,
|
||||
cluster_handler_names=CLUSTER_HANDLER_OCCUPANCY,
|
||||
manufacturers={"Philips", "Signify Netherlands B.V."},
|
||||
models={"SML001"},
|
||||
)
|
||||
@ -388,7 +390,7 @@ class HueV2MotionSensitivities(types.enum8):
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_OCCUPANCY,
|
||||
cluster_handler_names=CLUSTER_HANDLER_OCCUPANCY,
|
||||
manufacturers={"Philips", "Signify Netherlands B.V."},
|
||||
models={"SML002", "SML003", "SML004"},
|
||||
)
|
||||
@ -407,7 +409,9 @@ class AqaraMonitoringModess(types.enum8):
|
||||
Left_Right = 0x01
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"lumi.motion.ac01"})
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
cluster_handler_names="opple_cluster", models={"lumi.motion.ac01"}
|
||||
)
|
||||
class AqaraMonitoringMode(ZCLEnumSelectEntity, id_suffix="monitoring_mode"):
|
||||
"""Representation of a ZHA monitoring mode configuration entity."""
|
||||
|
||||
@ -424,7 +428,9 @@ class AqaraApproachDistances(types.enum8):
|
||||
Near = 0x02
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"lumi.motion.ac01"})
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
cluster_handler_names="opple_cluster", models={"lumi.motion.ac01"}
|
||||
)
|
||||
class AqaraApproachDistance(ZCLEnumSelectEntity, id_suffix="approach_distance"):
|
||||
"""Representation of a ZHA approach distance configuration entity."""
|
||||
|
||||
@ -441,7 +447,7 @@ class AqaraE1ReverseDirection(types.enum8):
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names="window_covering", models={"lumi.curtain.agl001"}
|
||||
cluster_handler_names="window_covering", models={"lumi.curtain.agl001"}
|
||||
)
|
||||
class AqaraCurtainMode(ZCLEnumSelectEntity, id_suffix="window_covering_mode"):
|
||||
"""Representation of a ZHA curtain mode configuration entity."""
|
||||
@ -459,7 +465,7 @@ class InovelliOutputMode(types.enum1):
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_INOVELLI,
|
||||
cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
|
||||
)
|
||||
class InovelliOutputModeEntity(ZCLEnumSelectEntity, id_suffix="output_mode"):
|
||||
"""Inovelli output mode control."""
|
||||
@ -479,7 +485,7 @@ class InovelliSwitchType(types.enum8):
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_INOVELLI,
|
||||
cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
|
||||
)
|
||||
class InovelliSwitchTypeEntity(ZCLEnumSelectEntity, id_suffix="switch_type"):
|
||||
"""Inovelli switch type control."""
|
||||
@ -497,7 +503,7 @@ class InovelliLedScalingMode(types.enum1):
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_INOVELLI,
|
||||
cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
|
||||
)
|
||||
class InovelliLedScalingModeEntity(ZCLEnumSelectEntity, id_suffix="led_scaling_mode"):
|
||||
"""Inovelli led mode control."""
|
||||
@ -515,7 +521,7 @@ class InovelliNonNeutralOutput(types.enum1):
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_INOVELLI,
|
||||
cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
|
||||
)
|
||||
class InovelliNonNeutralOutputEntity(
|
||||
ZCLEnumSelectEntity, id_suffix="increased_non_neutral_output"
|
||||
@ -534,7 +540,9 @@ class AqaraFeedingMode(types.enum8):
|
||||
Schedule = 0x01
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"})
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"}
|
||||
)
|
||||
class AqaraPetFeederMode(ZCLEnumSelectEntity, id_suffix="feeding_mode"):
|
||||
"""Representation of an Aqara pet feeder mode configuration entity."""
|
||||
|
||||
@ -552,7 +560,9 @@ class AqaraThermostatPresetMode(types.enum8):
|
||||
Away = 0x02
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"lumi.airrtc.agl001"})
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
cluster_handler_names="opple_cluster", models={"lumi.airrtc.agl001"}
|
||||
)
|
||||
class AqaraThermostatPreset(ZCLEnumSelectEntity, id_suffix="preset"):
|
||||
"""Representation of an Aqara thermostat preset configuration entity."""
|
||||
|
||||
|
@ -46,19 +46,19 @@ from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .core import discovery
|
||||
from .core.const import (
|
||||
CHANNEL_ANALOG_INPUT,
|
||||
CHANNEL_BASIC,
|
||||
CHANNEL_DEVICE_TEMPERATURE,
|
||||
CHANNEL_ELECTRICAL_MEASUREMENT,
|
||||
CHANNEL_HUMIDITY,
|
||||
CHANNEL_ILLUMINANCE,
|
||||
CHANNEL_LEAF_WETNESS,
|
||||
CHANNEL_POWER_CONFIGURATION,
|
||||
CHANNEL_PRESSURE,
|
||||
CHANNEL_SMARTENERGY_METERING,
|
||||
CHANNEL_SOIL_MOISTURE,
|
||||
CHANNEL_TEMPERATURE,
|
||||
CHANNEL_THERMOSTAT,
|
||||
CLUSTER_HANDLER_ANALOG_INPUT,
|
||||
CLUSTER_HANDLER_BASIC,
|
||||
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,
|
||||
SIGNAL_ADD_ENTITIES,
|
||||
SIGNAL_ATTR_UPDATED,
|
||||
@ -67,7 +67,7 @@ from .core.registries import SMARTTHINGS_HUMIDITY_CLUSTER, ZHA_ENTITIES
|
||||
from .entity import ZhaEntity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .core.channels.base import ZigbeeChannel
|
||||
from .core.cluster_handlers import ClusterHandler
|
||||
from .core.device import ZHADevice
|
||||
|
||||
PARALLEL_UPDATES = 5
|
||||
@ -88,7 +88,9 @@ BATTERY_SIZES = {
|
||||
255: "Unknown",
|
||||
}
|
||||
|
||||
CHANNEL_ST_HUMIDITY_CLUSTER = f"channel_0x{SMARTTHINGS_HUMIDITY_CLUSTER:04x}"
|
||||
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)
|
||||
|
||||
@ -125,50 +127,50 @@ class Sensor(ZhaEntity, SensorEntity):
|
||||
self,
|
||||
unique_id: str,
|
||||
zha_device: ZHADevice,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Init this sensor."""
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
self._channel: ZigbeeChannel = channels[0]
|
||||
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
|
||||
self._cluster_handler: ClusterHandler = cluster_handlers[0]
|
||||
|
||||
@classmethod
|
||||
def create_entity(
|
||||
cls,
|
||||
unique_id: str,
|
||||
zha_device: ZHADevice,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
**kwargs: Any,
|
||||
) -> Self | None:
|
||||
"""Entity Factory.
|
||||
|
||||
Return entity if it is a supported configuration, otherwise return None
|
||||
"""
|
||||
channel = channels[0]
|
||||
if cls.SENSOR_ATTR in channel.cluster.unsupported_attributes:
|
||||
cluster_handler = cluster_handlers[0]
|
||||
if cls.SENSOR_ATTR in cluster_handler.cluster.unsupported_attributes:
|
||||
return None
|
||||
|
||||
return cls(unique_id, zha_device, channels, **kwargs)
|
||||
return cls(unique_id, zha_device, cluster_handlers, **kwargs)
|
||||
|
||||
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._channel, SIGNAL_ATTR_UPDATED, self.async_set_state
|
||||
self._cluster_handler, SIGNAL_ATTR_UPDATED, self.async_set_state
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the entity."""
|
||||
assert self.SENSOR_ATTR is not None
|
||||
raw_state = self._channel.cluster.get(self.SENSOR_ATTR)
|
||||
raw_state = self._cluster_handler.cluster.get(self.SENSOR_ATTR)
|
||||
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 channel."""
|
||||
"""Handle state update from cluster handler."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
def formatter(self, value: int | enum.IntEnum) -> int | float | str | None:
|
||||
@ -181,9 +183,9 @@ class Sensor(ZhaEntity, SensorEntity):
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names=CHANNEL_ANALOG_INPUT,
|
||||
cluster_handler_names=CLUSTER_HANDLER_ANALOG_INPUT,
|
||||
manufacturers="Digi",
|
||||
stop_on_match_group=CHANNEL_ANALOG_INPUT,
|
||||
stop_on_match_group=CLUSTER_HANDLER_ANALOG_INPUT,
|
||||
)
|
||||
class AnalogInput(Sensor):
|
||||
"""Sensor that displays analog input values."""
|
||||
@ -191,7 +193,7 @@ class AnalogInput(Sensor):
|
||||
SENSOR_ATTR = "present_value"
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names=CHANNEL_POWER_CONFIGURATION)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_POWER_CONFIGURATION)
|
||||
class Battery(Sensor):
|
||||
"""Battery sensor of power configuration cluster."""
|
||||
|
||||
@ -207,7 +209,7 @@ class Battery(Sensor):
|
||||
cls,
|
||||
unique_id: str,
|
||||
zha_device: ZHADevice,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
**kwargs: Any,
|
||||
) -> Self | None:
|
||||
"""Entity Factory.
|
||||
@ -216,7 +218,9 @@ class Battery(Sensor):
|
||||
battery_percent_remaining attribute, but zha-device-handlers takes care of it
|
||||
so create the entity regardless
|
||||
"""
|
||||
return cls(unique_id, zha_device, channels, **kwargs)
|
||||
if zha_device.is_mains_powered:
|
||||
return None
|
||||
return cls(unique_id, zha_device, cluster_handlers, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def formatter(value: int) -> int | None: # pylint: disable=arguments-differ
|
||||
@ -231,19 +235,19 @@ class Battery(Sensor):
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return device state attrs for battery sensors."""
|
||||
state_attrs = {}
|
||||
battery_size = self._channel.cluster.get("battery_size")
|
||||
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._channel.cluster.get("battery_quantity")
|
||||
battery_quantity = self._cluster_handler.cluster.get("battery_quantity")
|
||||
if battery_quantity is not None:
|
||||
state_attrs["battery_quantity"] = battery_quantity
|
||||
battery_voltage = self._channel.cluster.get("battery_voltage")
|
||||
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(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
|
||||
class ElectricalMeasurement(Sensor):
|
||||
"""Active power measurement."""
|
||||
|
||||
@ -259,19 +263,21 @@ class ElectricalMeasurement(Sensor):
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return device state attrs for sensor."""
|
||||
attrs = {}
|
||||
if self._channel.measurement_type is not None:
|
||||
attrs["measurement_type"] = self._channel.measurement_type
|
||||
if self._cluster_handler.measurement_type is not None:
|
||||
attrs["measurement_type"] = self._cluster_handler.measurement_type
|
||||
|
||||
max_attr_name = f"{self.SENSOR_ATTR}_max"
|
||||
if (max_v := self._channel.cluster.get(max_attr_name)) is not None:
|
||||
if (max_v := self._cluster_handler.cluster.get(max_attr_name)) is not None:
|
||||
attrs[max_attr_name] = str(self.formatter(max_v))
|
||||
|
||||
return attrs
|
||||
|
||||
def formatter(self, value: int) -> int | float:
|
||||
"""Return 'normalized' value."""
|
||||
multiplier = getattr(self._channel, f"{self._div_mul_prefix}_multiplier")
|
||||
divisor = getattr(self._channel, f"{self._div_mul_prefix}_divisor")
|
||||
multiplier = getattr(
|
||||
self._cluster_handler, f"{self._div_mul_prefix}_multiplier"
|
||||
)
|
||||
divisor = getattr(self._cluster_handler, f"{self._div_mul_prefix}_divisor")
|
||||
value = float(value * multiplier) / divisor
|
||||
if value < 100 and divisor > 1:
|
||||
return round(value, self._decimals)
|
||||
@ -284,7 +290,7 @@ class ElectricalMeasurement(Sensor):
|
||||
await super().async_update()
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
|
||||
class ElectricalMeasurementApparentPower(
|
||||
ElectricalMeasurement, id_suffix="apparent_power"
|
||||
):
|
||||
@ -298,7 +304,7 @@ class ElectricalMeasurementApparentPower(
|
||||
_div_mul_prefix = "ac_power"
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
|
||||
class ElectricalMeasurementRMSCurrent(ElectricalMeasurement, id_suffix="rms_current"):
|
||||
"""RMS current measurement."""
|
||||
|
||||
@ -310,7 +316,7 @@ class ElectricalMeasurementRMSCurrent(ElectricalMeasurement, id_suffix="rms_curr
|
||||
_div_mul_prefix = "ac_current"
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
|
||||
class ElectricalMeasurementRMSVoltage(ElectricalMeasurement, id_suffix="rms_voltage"):
|
||||
"""RMS Voltage measurement."""
|
||||
|
||||
@ -322,7 +328,7 @@ class ElectricalMeasurementRMSVoltage(ElectricalMeasurement, id_suffix="rms_volt
|
||||
_div_mul_prefix = "ac_voltage"
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
|
||||
class ElectricalMeasurementFrequency(ElectricalMeasurement, id_suffix="ac_frequency"):
|
||||
"""Frequency measurement."""
|
||||
|
||||
@ -334,7 +340,7 @@ class ElectricalMeasurementFrequency(ElectricalMeasurement, id_suffix="ac_freque
|
||||
_div_mul_prefix = "ac_frequency"
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
|
||||
class ElectricalMeasurementPowerFactor(ElectricalMeasurement, id_suffix="power_factor"):
|
||||
"""Frequency measurement."""
|
||||
|
||||
@ -346,9 +352,13 @@ class ElectricalMeasurementPowerFactor(ElectricalMeasurement, id_suffix="power_f
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
generic_ids=CHANNEL_ST_HUMIDITY_CLUSTER, stop_on_match_group=CHANNEL_HUMIDITY
|
||||
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,
|
||||
)
|
||||
@MULTI_MATCH(channel_names=CHANNEL_HUMIDITY, stop_on_match_group=CHANNEL_HUMIDITY)
|
||||
class Humidity(Sensor):
|
||||
"""Humidity sensor."""
|
||||
|
||||
@ -360,7 +370,7 @@ class Humidity(Sensor):
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names=CHANNEL_SOIL_MOISTURE)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_SOIL_MOISTURE)
|
||||
class SoilMoisture(Sensor):
|
||||
"""Soil Moisture sensor."""
|
||||
|
||||
@ -372,7 +382,7 @@ class SoilMoisture(Sensor):
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names=CHANNEL_LEAF_WETNESS)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_LEAF_WETNESS)
|
||||
class LeafWetness(Sensor):
|
||||
"""Leaf Wetness sensor."""
|
||||
|
||||
@ -384,7 +394,7 @@ class LeafWetness(Sensor):
|
||||
_attr_native_unit_of_measurement = PERCENTAGE
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names=CHANNEL_ILLUMINANCE)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ILLUMINANCE)
|
||||
class Illuminance(Sensor):
|
||||
"""Illuminance Sensor."""
|
||||
|
||||
@ -400,8 +410,8 @@ class Illuminance(Sensor):
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names=CHANNEL_SMARTENERGY_METERING,
|
||||
stop_on_match_group=CHANNEL_SMARTENERGY_METERING,
|
||||
cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
|
||||
stop_on_match_group=CLUSTER_HANDLER_SMARTENERGY_METERING,
|
||||
)
|
||||
class SmartEnergyMetering(Sensor):
|
||||
"""Metering sensor."""
|
||||
@ -428,21 +438,21 @@ class SmartEnergyMetering(Sensor):
|
||||
}
|
||||
|
||||
def formatter(self, value: int) -> int | float:
|
||||
"""Pass through channel formatter."""
|
||||
return self._channel.demand_formatter(value)
|
||||
"""Pass through cluster handler formatter."""
|
||||
return self._cluster_handler.demand_formatter(value)
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return Unit of measurement."""
|
||||
return self.unit_of_measure_map.get(self._channel.unit_of_measurement)
|
||||
return self.unit_of_measure_map.get(self._cluster_handler.unit_of_measurement)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return device state attrs for battery sensors."""
|
||||
attrs = {}
|
||||
if self._channel.device_type is not None:
|
||||
attrs["device_type"] = self._channel.device_type
|
||||
if (status := self._channel.status) is not None:
|
||||
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) and sys.version_info >= (3, 11):
|
||||
attrs["status"] = str(
|
||||
status.name if status.name is not None else status.value
|
||||
@ -453,8 +463,8 @@ class SmartEnergyMetering(Sensor):
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names=CHANNEL_SMARTENERGY_METERING,
|
||||
stop_on_match_group=CHANNEL_SMARTENERGY_METERING,
|
||||
cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
|
||||
stop_on_match_group=CLUSTER_HANDLER_SMARTENERGY_METERING,
|
||||
)
|
||||
class SmartEnergySummation(SmartEnergyMetering, id_suffix="summation_delivered"):
|
||||
"""Smart Energy Metering summation sensor."""
|
||||
@ -482,17 +492,20 @@ class SmartEnergySummation(SmartEnergyMetering, id_suffix="summation_delivered")
|
||||
|
||||
def formatter(self, value: int) -> int | float:
|
||||
"""Numeric pass-through formatter."""
|
||||
if self._channel.unit_of_measurement != 0:
|
||||
return self._channel.summa_formatter(value)
|
||||
if self._cluster_handler.unit_of_measurement != 0:
|
||||
return self._cluster_handler.summa_formatter(value)
|
||||
|
||||
cooked = float(self._channel.multiplier * value) / self._channel.divisor
|
||||
cooked = (
|
||||
float(self._cluster_handler.multiplier * value)
|
||||
/ self._cluster_handler.divisor
|
||||
)
|
||||
return round(cooked, 3)
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names=CHANNEL_SMARTENERGY_METERING,
|
||||
cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
|
||||
models={"TS011F", "ZLinky_TIC"},
|
||||
stop_on_match_group=CHANNEL_SMARTENERGY_METERING,
|
||||
stop_on_match_group=CLUSTER_HANDLER_SMARTENERGY_METERING,
|
||||
)
|
||||
class PolledSmartEnergySummation(SmartEnergySummation):
|
||||
"""Polled Smart Energy Metering summation sensor."""
|
||||
@ -503,11 +516,11 @@ class PolledSmartEnergySummation(SmartEnergySummation):
|
||||
"""Retrieve latest state."""
|
||||
if not self.available:
|
||||
return
|
||||
await self._channel.async_force_update()
|
||||
await self._cluster_handler.async_force_update()
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names=CHANNEL_SMARTENERGY_METERING,
|
||||
cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
|
||||
models={"ZLinky_TIC"},
|
||||
)
|
||||
class Tier1SmartEnergySummation(
|
||||
@ -520,7 +533,7 @@ class Tier1SmartEnergySummation(
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names=CHANNEL_SMARTENERGY_METERING,
|
||||
cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
|
||||
models={"ZLinky_TIC"},
|
||||
)
|
||||
class Tier2SmartEnergySummation(
|
||||
@ -533,7 +546,7 @@ class Tier2SmartEnergySummation(
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names=CHANNEL_SMARTENERGY_METERING,
|
||||
cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
|
||||
models={"ZLinky_TIC"},
|
||||
)
|
||||
class Tier3SmartEnergySummation(
|
||||
@ -546,7 +559,7 @@ class Tier3SmartEnergySummation(
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names=CHANNEL_SMARTENERGY_METERING,
|
||||
cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
|
||||
models={"ZLinky_TIC"},
|
||||
)
|
||||
class Tier4SmartEnergySummation(
|
||||
@ -559,7 +572,7 @@ class Tier4SmartEnergySummation(
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names=CHANNEL_SMARTENERGY_METERING,
|
||||
cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
|
||||
models={"ZLinky_TIC"},
|
||||
)
|
||||
class Tier5SmartEnergySummation(
|
||||
@ -572,7 +585,7 @@ class Tier5SmartEnergySummation(
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names=CHANNEL_SMARTENERGY_METERING,
|
||||
cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
|
||||
models={"ZLinky_TIC"},
|
||||
)
|
||||
class Tier6SmartEnergySummation(
|
||||
@ -584,7 +597,7 @@ class Tier6SmartEnergySummation(
|
||||
_attr_name: str = "Tier 6 summation delivered"
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names=CHANNEL_PRESSURE)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_PRESSURE)
|
||||
class Pressure(Sensor):
|
||||
"""Pressure sensor."""
|
||||
|
||||
@ -596,7 +609,7 @@ class Pressure(Sensor):
|
||||
_attr_native_unit_of_measurement = UnitOfPressure.HPA
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names=CHANNEL_TEMPERATURE)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_TEMPERATURE)
|
||||
class Temperature(Sensor):
|
||||
"""Temperature Sensor."""
|
||||
|
||||
@ -608,7 +621,7 @@ class Temperature(Sensor):
|
||||
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names=CHANNEL_DEVICE_TEMPERATURE)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_DEVICE_TEMPERATURE)
|
||||
class DeviceTemperature(Sensor):
|
||||
"""Device Temperature Sensor."""
|
||||
|
||||
@ -621,7 +634,7 @@ class DeviceTemperature(Sensor):
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names="carbon_dioxide_concentration")
|
||||
@MULTI_MATCH(cluster_handler_names="carbon_dioxide_concentration")
|
||||
class CarbonDioxideConcentration(Sensor):
|
||||
"""Carbon Dioxide Concentration sensor."""
|
||||
|
||||
@ -634,7 +647,7 @@ class CarbonDioxideConcentration(Sensor):
|
||||
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names="carbon_monoxide_concentration")
|
||||
@MULTI_MATCH(cluster_handler_names="carbon_monoxide_concentration")
|
||||
class CarbonMonoxideConcentration(Sensor):
|
||||
"""Carbon Monoxide Concentration sensor."""
|
||||
|
||||
@ -647,8 +660,8 @@ class CarbonMonoxideConcentration(Sensor):
|
||||
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
|
||||
|
||||
|
||||
@MULTI_MATCH(generic_ids="channel_0x042e", stop_on_match_group="voc_level")
|
||||
@MULTI_MATCH(channel_names="voc_level", stop_on_match_group="voc_level")
|
||||
@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")
|
||||
class VOCLevel(Sensor):
|
||||
"""VOC Level sensor."""
|
||||
|
||||
@ -662,7 +675,7 @@ class VOCLevel(Sensor):
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names="voc_level",
|
||||
cluster_handler_names="voc_level",
|
||||
models="lumi.airmonitor.acn01",
|
||||
stop_on_match_group="voc_level",
|
||||
)
|
||||
@ -678,7 +691,7 @@ class PPBVOCLevel(Sensor):
|
||||
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_BILLION
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names="pm25")
|
||||
@MULTI_MATCH(cluster_handler_names="pm25")
|
||||
class PM25(Sensor):
|
||||
"""Particulate Matter 2.5 microns or less sensor."""
|
||||
|
||||
@ -690,7 +703,7 @@ class PM25(Sensor):
|
||||
_attr_native_unit_of_measurement = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names="formaldehyde_concentration")
|
||||
@MULTI_MATCH(cluster_handler_names="formaldehyde_concentration")
|
||||
class FormaldehydeConcentration(Sensor):
|
||||
"""Formaldehyde Concentration sensor."""
|
||||
|
||||
@ -702,7 +715,10 @@ class FormaldehydeConcentration(Sensor):
|
||||
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names=CHANNEL_THERMOSTAT, stop_on_match_group=CHANNEL_THERMOSTAT)
|
||||
@MULTI_MATCH(
|
||||
cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
|
||||
stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT,
|
||||
)
|
||||
class ThermostatHVACAction(Sensor, id_suffix="hvac_action"):
|
||||
"""Thermostat HVAC action sensor."""
|
||||
|
||||
@ -713,7 +729,7 @@ class ThermostatHVACAction(Sensor, id_suffix="hvac_action"):
|
||||
cls,
|
||||
unique_id: str,
|
||||
zha_device: ZHADevice,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
**kwargs: Any,
|
||||
) -> Self | None:
|
||||
"""Entity Factory.
|
||||
@ -721,14 +737,14 @@ class ThermostatHVACAction(Sensor, id_suffix="hvac_action"):
|
||||
Return entity if it is a supported configuration, otherwise return None
|
||||
"""
|
||||
|
||||
return cls(unique_id, zha_device, channels, **kwargs)
|
||||
return cls(unique_id, zha_device, cluster_handlers, **kwargs)
|
||||
|
||||
@property
|
||||
def native_value(self) -> str | None:
|
||||
"""Return the current HVAC action."""
|
||||
if (
|
||||
self._channel.pi_heating_demand is None
|
||||
and self._channel.pi_cooling_demand is None
|
||||
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
|
||||
@ -737,36 +753,36 @@ class ThermostatHVACAction(Sensor, id_suffix="hvac_action"):
|
||||
def _rm_rs_action(self) -> HVACAction | None:
|
||||
"""Return the current HVAC action based on running mode and running state."""
|
||||
|
||||
if (running_state := self._channel.running_state) is None:
|
||||
if (running_state := self._cluster_handler.running_state) is None:
|
||||
return None
|
||||
|
||||
rs_heat = (
|
||||
self._channel.RunningState.Heat_State_On
|
||||
| self._channel.RunningState.Heat_2nd_Stage_On
|
||||
self._cluster_handler.RunningState.Heat_State_On
|
||||
| self._cluster_handler.RunningState.Heat_2nd_Stage_On
|
||||
)
|
||||
if running_state & rs_heat:
|
||||
return HVACAction.HEATING
|
||||
|
||||
rs_cool = (
|
||||
self._channel.RunningState.Cool_State_On
|
||||
| self._channel.RunningState.Cool_2nd_Stage_On
|
||||
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._channel.running_state
|
||||
running_state = self._cluster_handler.running_state
|
||||
if running_state and running_state & (
|
||||
self._channel.RunningState.Fan_State_On
|
||||
| self._channel.RunningState.Fan_2nd_Stage_On
|
||||
| self._channel.RunningState.Fan_3rd_Stage_On
|
||||
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._channel.running_state
|
||||
if running_state and running_state & self._channel.RunningState.Idle:
|
||||
running_state = self._cluster_handler.running_state
|
||||
if running_state and running_state & self._cluster_handler.RunningState.Idle:
|
||||
return HVACAction.IDLE
|
||||
|
||||
if self._channel.system_mode != self._channel.SystemMode.Off:
|
||||
if self._cluster_handler.system_mode != self._cluster_handler.SystemMode.Off:
|
||||
return HVACAction.IDLE
|
||||
return HVACAction.OFF
|
||||
|
||||
@ -774,27 +790,27 @@ class ThermostatHVACAction(Sensor, id_suffix="hvac_action"):
|
||||
def _pi_demand_action(self) -> HVACAction:
|
||||
"""Return the current HVAC action based on pi_demands."""
|
||||
|
||||
heating_demand = self._channel.pi_heating_demand
|
||||
heating_demand = self._cluster_handler.pi_heating_demand
|
||||
if heating_demand is not None and heating_demand > 0:
|
||||
return HVACAction.HEATING
|
||||
cooling_demand = self._channel.pi_cooling_demand
|
||||
cooling_demand = self._cluster_handler.pi_cooling_demand
|
||||
if cooling_demand is not None and cooling_demand > 0:
|
||||
return HVACAction.COOLING
|
||||
|
||||
if self._channel.system_mode != self._channel.SystemMode.Off:
|
||||
if self._cluster_handler.system_mode != self._cluster_handler.SystemMode.Off:
|
||||
return HVACAction.IDLE
|
||||
return HVACAction.OFF
|
||||
|
||||
@callback
|
||||
def async_set_state(self, *args, **kwargs) -> None:
|
||||
"""Handle state update from channel."""
|
||||
"""Handle state update from cluster handler."""
|
||||
self.async_write_ha_state()
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names={CHANNEL_THERMOSTAT},
|
||||
cluster_handler_names={CLUSTER_HANDLER_THERMOSTAT},
|
||||
manufacturers="Sinope Technologies",
|
||||
stop_on_match_group=CHANNEL_THERMOSTAT,
|
||||
stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT,
|
||||
)
|
||||
class SinopeHVACAction(ThermostatHVACAction):
|
||||
"""Sinope Thermostat HVAC action sensor."""
|
||||
@ -803,28 +819,28 @@ class SinopeHVACAction(ThermostatHVACAction):
|
||||
def _rm_rs_action(self) -> HVACAction:
|
||||
"""Return the current HVAC action based on running mode and running state."""
|
||||
|
||||
running_mode = self._channel.running_mode
|
||||
if running_mode == self._channel.RunningMode.Heat:
|
||||
running_mode = self._cluster_handler.running_mode
|
||||
if running_mode == self._cluster_handler.RunningMode.Heat:
|
||||
return HVACAction.HEATING
|
||||
if running_mode == self._channel.RunningMode.Cool:
|
||||
if running_mode == self._cluster_handler.RunningMode.Cool:
|
||||
return HVACAction.COOLING
|
||||
|
||||
running_state = self._channel.running_state
|
||||
running_state = self._cluster_handler.running_state
|
||||
if running_state and running_state & (
|
||||
self._channel.RunningState.Fan_State_On
|
||||
| self._channel.RunningState.Fan_2nd_Stage_On
|
||||
| self._channel.RunningState.Fan_3rd_Stage_On
|
||||
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._channel.system_mode != self._channel.SystemMode.Off
|
||||
and running_mode == self._channel.SystemMode.Off
|
||||
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(channel_names=CHANNEL_BASIC)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_BASIC)
|
||||
class RSSISensor(Sensor, id_suffix="rssi"):
|
||||
"""RSSI sensor for a device."""
|
||||
|
||||
@ -842,17 +858,17 @@ class RSSISensor(Sensor, id_suffix="rssi"):
|
||||
cls,
|
||||
unique_id: str,
|
||||
zha_device: ZHADevice,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
**kwargs: Any,
|
||||
) -> Self | None:
|
||||
"""Entity Factory.
|
||||
|
||||
Return entity if it is a supported configuration, otherwise return None
|
||||
"""
|
||||
key = f"{CHANNEL_BASIC}_{cls.unique_id_suffix}"
|
||||
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, channels, **kwargs)
|
||||
return cls(unique_id, zha_device, cluster_handlers, **kwargs)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
@ -860,7 +876,7 @@ class RSSISensor(Sensor, id_suffix="rssi"):
|
||||
return getattr(self._zha_device.device, self.unique_id_suffix)
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names=CHANNEL_BASIC)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_BASIC)
|
||||
class LQISensor(RSSISensor, id_suffix="lqi"):
|
||||
"""LQI sensor for a device."""
|
||||
|
||||
@ -870,7 +886,7 @@ class LQISensor(RSSISensor, id_suffix="lqi"):
|
||||
|
||||
|
||||
@MULTI_MATCH(
|
||||
channel_names="tuya_manufacturer",
|
||||
cluster_handler_names="tuya_manufacturer",
|
||||
manufacturers={
|
||||
"_TZE200_htnnfasr",
|
||||
},
|
||||
@ -885,7 +901,7 @@ class TimeLeft(Sensor, id_suffix="time_left"):
|
||||
_attr_native_unit_of_measurement = UnitOfTime.MINUTES
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names="ikea_airpurifier")
|
||||
@MULTI_MATCH(cluster_handler_names="ikea_airpurifier")
|
||||
class IkeaDeviceRunTime(Sensor, id_suffix="device_run_time"):
|
||||
"""Sensor that displays device run time (in minutes)."""
|
||||
|
||||
@ -896,7 +912,7 @@ class IkeaDeviceRunTime(Sensor, id_suffix="device_run_time"):
|
||||
_attr_native_unit_of_measurement = UnitOfTime.MINUTES
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names="ikea_airpurifier")
|
||||
@MULTI_MATCH(cluster_handler_names="ikea_airpurifier")
|
||||
class IkeaFilterRunTime(Sensor, id_suffix="filter_run_time"):
|
||||
"""Sensor that displays run time of the current filter (in minutes)."""
|
||||
|
||||
@ -914,7 +930,7 @@ class AqaraFeedingSource(types.enum8):
|
||||
HomeAssistant = 0x02
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"})
|
||||
@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"})
|
||||
class AqaraPetFeederLastFeedingSource(Sensor, id_suffix="last_feeding_source"):
|
||||
"""Sensor that displays the last feeding source of pet feeder."""
|
||||
|
||||
@ -927,7 +943,7 @@ class AqaraPetFeederLastFeedingSource(Sensor, id_suffix="last_feeding_source"):
|
||||
return AqaraFeedingSource(value).name
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"})
|
||||
@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"})
|
||||
class AqaraPetFeederLastFeedingSize(Sensor, id_suffix="last_feeding_size"):
|
||||
"""Sensor that displays the last feeding size of the pet feeder."""
|
||||
|
||||
@ -936,7 +952,7 @@ class AqaraPetFeederLastFeedingSize(Sensor, id_suffix="last_feeding_size"):
|
||||
_attr_icon: str = "mdi:counter"
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"})
|
||||
@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"})
|
||||
class AqaraPetFeederPortionsDispensed(Sensor, id_suffix="portions_dispensed"):
|
||||
"""Sensor that displays the number of portions dispensed by the pet feeder."""
|
||||
|
||||
@ -946,7 +962,7 @@ class AqaraPetFeederPortionsDispensed(Sensor, id_suffix="portions_dispensed"):
|
||||
_attr_icon: str = "mdi:counter"
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"})
|
||||
@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"})
|
||||
class AqaraPetFeederWeightDispensed(Sensor, id_suffix="weight_dispensed"):
|
||||
"""Sensor that displays the weight dispensed by the pet feeder."""
|
||||
|
||||
@ -957,7 +973,7 @@ class AqaraPetFeederWeightDispensed(Sensor, id_suffix="weight_dispensed"):
|
||||
_attr_icon: str = "mdi:weight-gram"
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names="opple_cluster", models={"lumi.sensor_smoke.acn03"})
|
||||
@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"lumi.sensor_smoke.acn03"})
|
||||
class AqaraSmokeDensityDbm(Sensor, id_suffix="smoke_density_dbm"):
|
||||
"""Sensor that displays the smoke density of an Aqara smoke sensor in dB/m."""
|
||||
|
||||
|
@ -22,9 +22,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_call_later
|
||||
|
||||
from .core import discovery
|
||||
from .core.channels.security import IasWd
|
||||
from .core.cluster_handlers.security import IasWd
|
||||
from .core.const import (
|
||||
CHANNEL_IAS_WD,
|
||||
CLUSTER_HANDLER_IAS_WD,
|
||||
DATA_ZHA,
|
||||
SIGNAL_ADD_ENTITIES,
|
||||
WARNING_DEVICE_MODE_BURGLAR,
|
||||
@ -43,7 +43,7 @@ from .core.registries import ZHA_ENTITIES
|
||||
from .entity import ZhaEntity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .core.channels.base import ZigbeeChannel
|
||||
from .core.cluster_handlers import ClusterHandler
|
||||
from .core.device import ZHADevice
|
||||
|
||||
MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.SIREN)
|
||||
@ -70,7 +70,7 @@ async def async_setup_entry(
|
||||
config_entry.async_on_unload(unsub)
|
||||
|
||||
|
||||
@MULTI_MATCH(channel_names=CHANNEL_IAS_WD)
|
||||
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_IAS_WD)
|
||||
class ZHASiren(ZhaEntity, SirenEntity):
|
||||
"""Representation of a ZHA siren."""
|
||||
|
||||
@ -78,7 +78,7 @@ class ZHASiren(ZhaEntity, SirenEntity):
|
||||
self,
|
||||
unique_id: str,
|
||||
zha_device: ZHADevice,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
**kwargs,
|
||||
) -> None:
|
||||
"""Init this siren."""
|
||||
@ -97,8 +97,8 @@ class ZHASiren(ZhaEntity, SirenEntity):
|
||||
WARNING_DEVICE_MODE_FIRE_PANIC: "Fire Panic",
|
||||
WARNING_DEVICE_MODE_EMERGENCY_PANIC: "Emergency Panic",
|
||||
}
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
self._channel: IasWd = cast(IasWd, channels[0])
|
||||
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
|
||||
self._cluster_handler: IasWd = cast(IasWd, cluster_handlers[0])
|
||||
self._attr_is_on: bool = False
|
||||
self._off_listener: Callable[[], None] | None = None
|
||||
|
||||
@ -107,22 +107,28 @@ class ZHASiren(ZhaEntity, SirenEntity):
|
||||
if self._off_listener:
|
||||
self._off_listener()
|
||||
self._off_listener = None
|
||||
tone_cache = self._channel.data_cache.get(WD.Warning.WarningMode.__name__)
|
||||
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._channel.data_cache.get(WD.Warning.SirenLevel.__name__)
|
||||
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._channel.data_cache.get(Strobe.__name__)
|
||||
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._channel.data_cache.get(WD.StrobeLevel.__name__)
|
||||
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
|
||||
@ -134,7 +140,7 @@ class ZHASiren(ZhaEntity, SirenEntity):
|
||||
siren_tone = tone
|
||||
if (level := kwargs.get(ATTR_VOLUME_LEVEL)) is not None:
|
||||
siren_level = int(level)
|
||||
await self._channel.issue_start_warning(
|
||||
await self._cluster_handler.issue_start_warning(
|
||||
mode=siren_tone,
|
||||
warning_duration=siren_duration,
|
||||
siren_level=siren_level,
|
||||
@ -150,7 +156,7 @@ class ZHASiren(ZhaEntity, SirenEntity):
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off siren."""
|
||||
await self._channel.issue_start_warning(
|
||||
await self._cluster_handler.issue_start_warning(
|
||||
mode=WARNING_DEVICE_MODE_STOP, strobe=WARNING_DEVICE_STROBE_NO
|
||||
)
|
||||
self._attr_is_on = False
|
||||
|
@ -19,9 +19,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
|
||||
from .core import discovery
|
||||
from .core.const import (
|
||||
CHANNEL_BASIC,
|
||||
CHANNEL_INOVELLI,
|
||||
CHANNEL_ON_OFF,
|
||||
CLUSTER_HANDLER_BASIC,
|
||||
CLUSTER_HANDLER_INOVELLI,
|
||||
CLUSTER_HANDLER_ON_OFF,
|
||||
DATA_ZHA,
|
||||
SIGNAL_ADD_ENTITIES,
|
||||
SIGNAL_ATTR_UPDATED,
|
||||
@ -30,7 +30,7 @@ from .core.registries import ZHA_ENTITIES
|
||||
from .entity import ZhaEntity, ZhaGroupEntity
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .core.channels.base import ZigbeeChannel
|
||||
from .core.cluster_handlers import ClusterHandler
|
||||
from .core.device import ZHADevice
|
||||
|
||||
STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.SWITCH)
|
||||
@ -60,7 +60,7 @@ async def async_setup_entry(
|
||||
config_entry.async_on_unload(unsub)
|
||||
|
||||
|
||||
@STRICT_MATCH(channel_names=CHANNEL_ON_OFF)
|
||||
@STRICT_MATCH(cluster_handler_names=CLUSTER_HANDLER_ON_OFF)
|
||||
class Switch(ZhaEntity, SwitchEntity):
|
||||
"""ZHA switch."""
|
||||
|
||||
@ -68,51 +68,53 @@ class Switch(ZhaEntity, SwitchEntity):
|
||||
self,
|
||||
unique_id: str,
|
||||
zha_device: ZHADevice,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize the ZHA switch."""
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
self._on_off_channel = self.cluster_channels[CHANNEL_ON_OFF]
|
||||
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_channel.on_off is None:
|
||||
if self._on_off_cluster_handler.on_off is None:
|
||||
return False
|
||||
return self._on_off_channel.on_off
|
||||
return self._on_off_cluster_handler.on_off
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
result = await self._on_off_channel.turn_on()
|
||||
result = await self._on_off_cluster_handler.turn_on()
|
||||
if not result:
|
||||
return
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
result = await self._on_off_channel.turn_off()
|
||||
result = await self._on_off_cluster_handler.turn_off()
|
||||
if not result:
|
||||
return
|
||||
self.async_write_ha_state()
|
||||
|
||||
@callback
|
||||
def async_set_state(self, attr_id: int, attr_name: str, value: Any):
|
||||
"""Handle state update from channel."""
|
||||
"""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_channel, SIGNAL_ATTR_UPDATED, self.async_set_state
|
||||
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."""
|
||||
await super().async_update()
|
||||
if self._on_off_channel:
|
||||
await self._on_off_channel.get_attribute_value("on_off", from_cache=False)
|
||||
if self._on_off_cluster_handler:
|
||||
await self._on_off_cluster_handler.get_attribute_value(
|
||||
"on_off", from_cache=False
|
||||
)
|
||||
|
||||
|
||||
@GROUP_MATCH()
|
||||
@ -132,7 +134,7 @@ class SwitchGroup(ZhaGroupEntity, SwitchEntity):
|
||||
self._available: bool
|
||||
self._state: bool
|
||||
group = self.zha_device.gateway.get_group(self._group_id)
|
||||
self._on_off_channel = group.endpoint[OnOff.cluster_id]
|
||||
self._on_off_cluster_handler = group.endpoint[OnOff.cluster_id]
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
@ -141,7 +143,7 @@ class SwitchGroup(ZhaGroupEntity, SwitchEntity):
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
result = await self._on_off_channel.on()
|
||||
result = await self._on_off_cluster_handler.on()
|
||||
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
|
||||
return
|
||||
self._state = True
|
||||
@ -149,7 +151,7 @@ class SwitchGroup(ZhaGroupEntity, SwitchEntity):
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
result = await self._on_off_channel.off()
|
||||
result = await self._on_off_cluster_handler.off()
|
||||
if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
|
||||
return
|
||||
self._state = False
|
||||
@ -178,17 +180,17 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity):
|
||||
cls,
|
||||
unique_id: str,
|
||||
zha_device: ZHADevice,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
**kwargs: Any,
|
||||
) -> Self | None:
|
||||
"""Entity Factory.
|
||||
|
||||
Return entity if it is a supported configuration, otherwise return None
|
||||
"""
|
||||
channel = channels[0]
|
||||
cluster_handler = cluster_handlers[0]
|
||||
if (
|
||||
cls._zcl_attribute in channel.cluster.unsupported_attributes
|
||||
or channel.cluster.get(cls._zcl_attribute) is None
|
||||
cls._zcl_attribute in cluster_handler.cluster.unsupported_attributes
|
||||
or cluster_handler.cluster.get(cls._zcl_attribute) is None
|
||||
):
|
||||
_LOGGER.debug(
|
||||
"%s is not supported - skipping %s entity creation",
|
||||
@ -197,48 +199,48 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity):
|
||||
)
|
||||
return None
|
||||
|
||||
return cls(unique_id, zha_device, channels, **kwargs)
|
||||
return cls(unique_id, zha_device, cluster_handlers, **kwargs)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
unique_id: str,
|
||||
zha_device: ZHADevice,
|
||||
channels: list[ZigbeeChannel],
|
||||
cluster_handlers: list[ClusterHandler],
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Init this number configuration entity."""
|
||||
self._channel: ZigbeeChannel = channels[0]
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
self._cluster_handler: ClusterHandler = cluster_handlers[0]
|
||||
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
|
||||
|
||||
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._channel, SIGNAL_ATTR_UPDATED, self.async_set_state
|
||||
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 channel."""
|
||||
"""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._zcl_inverter_attribute:
|
||||
return bool(self._channel.cluster.get(self._zcl_inverter_attribute))
|
||||
return bool(self._cluster_handler.cluster.get(self._zcl_inverter_attribute))
|
||||
return self._force_inverted
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return if the switch is on based on the statemachine."""
|
||||
val = bool(self._channel.cluster.get(self._zcl_attribute))
|
||||
val = bool(self._cluster_handler.cluster.get(self._zcl_attribute))
|
||||
return (not val) if self.inverted else val
|
||||
|
||||
async def async_turn_on_off(self, state: bool) -> None:
|
||||
"""Turn the entity on or off."""
|
||||
try:
|
||||
result = await self._channel.cluster.write_attributes(
|
||||
result = await self._cluster_handler.cluster.write_attributes(
|
||||
{self._zcl_attribute: not state if self.inverted else state}
|
||||
)
|
||||
except zigpy.exceptions.ZigbeeException as ex:
|
||||
@ -261,18 +263,18 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity):
|
||||
"""Attempt to retrieve the state of the entity."""
|
||||
await super().async_update()
|
||||
self.error("Polling current state")
|
||||
if self._channel:
|
||||
value = await self._channel.get_attribute_value(
|
||||
if self._cluster_handler:
|
||||
value = await self._cluster_handler.get_attribute_value(
|
||||
self._zcl_attribute, from_cache=False
|
||||
)
|
||||
await self._channel.get_attribute_value(
|
||||
await self._cluster_handler.get_attribute_value(
|
||||
self._zcl_inverter_attribute, from_cache=False
|
||||
)
|
||||
self.debug("read value=%s, inverted=%s", value, self.inverted)
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names="tuya_manufacturer",
|
||||
cluster_handler_names="tuya_manufacturer",
|
||||
manufacturers={
|
||||
"_TZE200_b6wax7g0",
|
||||
},
|
||||
@ -286,7 +288,9 @@ class OnOffWindowDetectionFunctionConfigurationEntity(
|
||||
_zcl_inverter_attribute: str = "window_detection_function_inverter"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"lumi.motion.ac02"})
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
cluster_handler_names="opple_cluster", models={"lumi.motion.ac02"}
|
||||
)
|
||||
class P1MotionTriggerIndicatorSwitch(
|
||||
ZHASwitchConfigurationEntity, id_suffix="trigger_indicator"
|
||||
):
|
||||
@ -297,7 +301,8 @@ class P1MotionTriggerIndicatorSwitch(
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names="opple_cluster", models={"lumi.plug.mmeu01", "lumi.plug.maeu01"}
|
||||
cluster_handler_names="opple_cluster",
|
||||
models={"lumi.plug.mmeu01", "lumi.plug.maeu01"},
|
||||
)
|
||||
class XiaomiPlugPowerOutageMemorySwitch(
|
||||
ZHASwitchConfigurationEntity, id_suffix="power_outage_memory"
|
||||
@ -309,7 +314,7 @@ class XiaomiPlugPowerOutageMemorySwitch(
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_BASIC,
|
||||
cluster_handler_names=CLUSTER_HANDLER_BASIC,
|
||||
manufacturers={"Philips", "Signify Netherlands B.V."},
|
||||
models={"SML001", "SML002", "SML003", "SML004"},
|
||||
)
|
||||
@ -323,7 +328,7 @@ class HueMotionTriggerIndicatorSwitch(
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names="ikea_airpurifier",
|
||||
cluster_handler_names="ikea_airpurifier",
|
||||
models={"STARKVIND Air purifier", "STARKVIND Air purifier table"},
|
||||
)
|
||||
class ChildLock(ZHASwitchConfigurationEntity, id_suffix="child_lock"):
|
||||
@ -334,7 +339,7 @@ class ChildLock(ZHASwitchConfigurationEntity, id_suffix="child_lock"):
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names="ikea_airpurifier",
|
||||
cluster_handler_names="ikea_airpurifier",
|
||||
models={"STARKVIND Air purifier", "STARKVIND Air purifier table"},
|
||||
)
|
||||
class DisableLed(ZHASwitchConfigurationEntity, id_suffix="disable_led"):
|
||||
@ -345,7 +350,7 @@ class DisableLed(ZHASwitchConfigurationEntity, id_suffix="disable_led"):
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_INOVELLI,
|
||||
cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
|
||||
)
|
||||
class InovelliInvertSwitch(ZHASwitchConfigurationEntity, id_suffix="invert_switch"):
|
||||
"""Inovelli invert switch control."""
|
||||
@ -355,7 +360,7 @@ class InovelliInvertSwitch(ZHASwitchConfigurationEntity, id_suffix="invert_switc
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_INOVELLI,
|
||||
cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
|
||||
)
|
||||
class InovelliSmartBulbMode(ZHASwitchConfigurationEntity, id_suffix="smart_bulb_mode"):
|
||||
"""Inovelli smart bulb mode control."""
|
||||
@ -365,7 +370,7 @@ class InovelliSmartBulbMode(ZHASwitchConfigurationEntity, id_suffix="smart_bulb_
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_INOVELLI,
|
||||
cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
|
||||
)
|
||||
class InovelliDoubleTapUpEnabled(
|
||||
ZHASwitchConfigurationEntity, id_suffix="double_tap_up_enabled"
|
||||
@ -377,7 +382,7 @@ class InovelliDoubleTapUpEnabled(
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_INOVELLI,
|
||||
cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
|
||||
)
|
||||
class InovelliDoubleTapDownEnabled(
|
||||
ZHASwitchConfigurationEntity, id_suffix="double_tap_down_enabled"
|
||||
@ -389,7 +394,7 @@ class InovelliDoubleTapDownEnabled(
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_INOVELLI,
|
||||
cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
|
||||
)
|
||||
class InovelliAuxSwitchScenes(
|
||||
ZHASwitchConfigurationEntity, id_suffix="aux_switch_scenes"
|
||||
@ -401,7 +406,7 @@ class InovelliAuxSwitchScenes(
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_INOVELLI,
|
||||
cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
|
||||
)
|
||||
class InovelliBindingOffToOnSyncLevel(
|
||||
ZHASwitchConfigurationEntity, id_suffix="binding_off_to_on_sync_level"
|
||||
@ -413,7 +418,7 @@ class InovelliBindingOffToOnSyncLevel(
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_INOVELLI,
|
||||
cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
|
||||
)
|
||||
class InovelliLocalProtection(
|
||||
ZHASwitchConfigurationEntity, id_suffix="local_protection"
|
||||
@ -425,7 +430,7 @@ class InovelliLocalProtection(
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_INOVELLI,
|
||||
cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
|
||||
)
|
||||
class InovelliOnOffLEDMode(ZHASwitchConfigurationEntity, id_suffix="on_off_led_mode"):
|
||||
"""Inovelli only 1 LED mode control."""
|
||||
@ -435,7 +440,7 @@ class InovelliOnOffLEDMode(ZHASwitchConfigurationEntity, id_suffix="on_off_led_m
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_INOVELLI,
|
||||
cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
|
||||
)
|
||||
class InovelliFirmwareProgressLED(
|
||||
ZHASwitchConfigurationEntity, id_suffix="firmware_progress_led"
|
||||
@ -447,7 +452,7 @@ class InovelliFirmwareProgressLED(
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_INOVELLI,
|
||||
cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
|
||||
)
|
||||
class InovelliRelayClickInOnOffMode(
|
||||
ZHASwitchConfigurationEntity, id_suffix="relay_click_in_on_off_mode"
|
||||
@ -459,7 +464,7 @@ class InovelliRelayClickInOnOffMode(
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_INOVELLI,
|
||||
cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
|
||||
)
|
||||
class InovelliDisableDoubleTapClearNotificationsMode(
|
||||
ZHASwitchConfigurationEntity, id_suffix="disable_clear_notifications_double_tap"
|
||||
@ -470,7 +475,9 @@ class InovelliDisableDoubleTapClearNotificationsMode(
|
||||
_attr_name: str = "Disable config 2x tap to clear notifications"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"})
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"}
|
||||
)
|
||||
class AqaraPetFeederLEDIndicator(
|
||||
ZHASwitchConfigurationEntity, id_suffix="disable_led_indicator"
|
||||
):
|
||||
@ -482,7 +489,9 @@ class AqaraPetFeederLEDIndicator(
|
||||
_attr_icon: str = "mdi:led-on"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"aqara.feeder.acn001"})
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"}
|
||||
)
|
||||
class AqaraPetFeederChildLock(ZHASwitchConfigurationEntity, id_suffix="child_lock"):
|
||||
"""Representation of a child lock configuration entity."""
|
||||
|
||||
@ -492,7 +501,7 @@ class AqaraPetFeederChildLock(ZHASwitchConfigurationEntity, id_suffix="child_loc
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names=CHANNEL_ON_OFF,
|
||||
cluster_handler_names=CLUSTER_HANDLER_ON_OFF,
|
||||
models={"TS011F"},
|
||||
)
|
||||
class TuyaChildLockSwitch(ZHASwitchConfigurationEntity, id_suffix="child_lock"):
|
||||
@ -503,7 +512,9 @@ class TuyaChildLockSwitch(ZHASwitchConfigurationEntity, id_suffix="child_lock"):
|
||||
_attr_icon: str = "mdi:account-lock"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"lumi.airrtc.agl001"})
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
cluster_handler_names="opple_cluster", models={"lumi.airrtc.agl001"}
|
||||
)
|
||||
class AqaraThermostatWindowDetection(
|
||||
ZHASwitchConfigurationEntity, id_suffix="window_detection"
|
||||
):
|
||||
@ -513,7 +524,9 @@ class AqaraThermostatWindowDetection(
|
||||
_attr_name = "Window detection"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"lumi.airrtc.agl001"})
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
cluster_handler_names="opple_cluster", models={"lumi.airrtc.agl001"}
|
||||
)
|
||||
class AqaraThermostatValveDetection(
|
||||
ZHASwitchConfigurationEntity, id_suffix="valve_detection"
|
||||
):
|
||||
@ -523,7 +536,9 @@ class AqaraThermostatValveDetection(
|
||||
_attr_name = "Valve detection"
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"lumi.airrtc.agl001"})
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
cluster_handler_names="opple_cluster", models={"lumi.airrtc.agl001"}
|
||||
)
|
||||
class AqaraThermostatChildLock(ZHASwitchConfigurationEntity, id_suffix="child_lock"):
|
||||
"""Representation of an Aqara thermostat child lock configuration entity."""
|
||||
|
||||
@ -533,7 +548,7 @@ class AqaraThermostatChildLock(ZHASwitchConfigurationEntity, id_suffix="child_lo
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names="opple_cluster", models={"lumi.sensor_smoke.acn03"}
|
||||
cluster_handler_names="opple_cluster", models={"lumi.sensor_smoke.acn03"}
|
||||
)
|
||||
class AqaraHeartbeatIndicator(
|
||||
ZHASwitchConfigurationEntity, id_suffix="heartbeat_indicator"
|
||||
@ -546,7 +561,7 @@ class AqaraHeartbeatIndicator(
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names="opple_cluster", models={"lumi.sensor_smoke.acn03"}
|
||||
cluster_handler_names="opple_cluster", models={"lumi.sensor_smoke.acn03"}
|
||||
)
|
||||
class AqaraLinkageAlarm(ZHASwitchConfigurationEntity, id_suffix="linkage_alarm"):
|
||||
"""Representation of a linkage alarm configuration entity for Aqara smoke sensors."""
|
||||
@ -557,7 +572,7 @@ class AqaraLinkageAlarm(ZHASwitchConfigurationEntity, id_suffix="linkage_alarm")
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names="opple_cluster", models={"lumi.sensor_smoke.acn03"}
|
||||
cluster_handler_names="opple_cluster", models={"lumi.sensor_smoke.acn03"}
|
||||
)
|
||||
class AqaraBuzzerManualMute(
|
||||
ZHASwitchConfigurationEntity, id_suffix="buzzer_manual_mute"
|
||||
@ -570,7 +585,7 @@ class AqaraBuzzerManualMute(
|
||||
|
||||
|
||||
@CONFIG_DIAGNOSTIC_MATCH(
|
||||
channel_names="opple_cluster", models={"lumi.sensor_smoke.acn03"}
|
||||
cluster_handler_names="opple_cluster", models={"lumi.sensor_smoke.acn03"}
|
||||
)
|
||||
class AqaraBuzzerManualAlarm(
|
||||
ZHASwitchConfigurationEntity, id_suffix="buzzer_manual_alarm"
|
||||
|
@ -40,10 +40,10 @@ from .core.const import (
|
||||
ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE,
|
||||
ATTR_WARNING_DEVICE_STROBE_INTENSITY,
|
||||
BINDINGS,
|
||||
CHANNEL_IAS_WD,
|
||||
CLUSTER_COMMAND_SERVER,
|
||||
CLUSTER_COMMANDS_CLIENT,
|
||||
CLUSTER_COMMANDS_SERVER,
|
||||
CLUSTER_HANDLER_IAS_WD,
|
||||
CLUSTER_TYPE_IN,
|
||||
CLUSTER_TYPE_OUT,
|
||||
CUSTOM_CONFIGURATION,
|
||||
@ -61,7 +61,7 @@ from .core.const import (
|
||||
WARNING_DEVICE_STROBE_HIGH,
|
||||
WARNING_DEVICE_STROBE_YES,
|
||||
ZHA_ALARM_OPTIONS,
|
||||
ZHA_CHANNEL_MSG,
|
||||
ZHA_CLUSTER_HANDLER_MSG,
|
||||
ZHA_CONFIG_SCHEMAS,
|
||||
)
|
||||
from .core.gateway import EntityReference
|
||||
@ -389,7 +389,7 @@ async def websocket_get_groupable_devices(
|
||||
),
|
||||
}
|
||||
for entity_ref in entity_refs
|
||||
if list(entity_ref.cluster_channels.values())[
|
||||
if list(entity_ref.cluster_handlers.values())[
|
||||
0
|
||||
].cluster.endpoint.endpoint_id
|
||||
== ep_id
|
||||
@ -597,7 +597,7 @@ async def websocket_reconfigure_node(
|
||||
connection.send_message(websocket_api.event_message(msg["id"], data))
|
||||
|
||||
remove_dispatcher_function = async_dispatcher_connect(
|
||||
hass, ZHA_CHANNEL_MSG, forward_messages
|
||||
hass, ZHA_CLUSTER_HANDLER_MSG, forward_messages
|
||||
)
|
||||
|
||||
@callback
|
||||
@ -1406,14 +1406,14 @@ def async_load_api(hass: HomeAssistant) -> None:
|
||||
schema=SERVICE_SCHEMAS[SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND],
|
||||
)
|
||||
|
||||
def _get_ias_wd_channel(zha_device):
|
||||
"""Get the IASWD channel for a device."""
|
||||
cluster_channels = {
|
||||
def _get_ias_wd_cluster_handler(zha_device):
|
||||
"""Get the IASWD cluster handler for a device."""
|
||||
cluster_handlers = {
|
||||
ch.name: ch
|
||||
for pool in zha_device.channels.pools
|
||||
for ch in pool.claimed_channels.values()
|
||||
for endpoint in zha_device.endpoints.values()
|
||||
for ch in endpoint.claimed_cluster_handlers.values()
|
||||
}
|
||||
return cluster_channels.get(CHANNEL_IAS_WD)
|
||||
return cluster_handlers.get(CLUSTER_HANDLER_IAS_WD)
|
||||
|
||||
async def warning_device_squawk(service: ServiceCall) -> None:
|
||||
"""Issue the squawk command for an IAS warning device."""
|
||||
@ -1423,11 +1423,11 @@ def async_load_api(hass: HomeAssistant) -> None:
|
||||
level: int = service.data[ATTR_LEVEL]
|
||||
|
||||
if (zha_device := zha_gateway.get_device(ieee)) is not None:
|
||||
if channel := _get_ias_wd_channel(zha_device):
|
||||
await channel.issue_squawk(mode, strobe, level)
|
||||
if cluster_handler := _get_ias_wd_cluster_handler(zha_device):
|
||||
await cluster_handler.issue_squawk(mode, strobe, level)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Squawking IASWD: %s: [%s] is missing the required IASWD channel!",
|
||||
"Squawking IASWD: %s: [%s] is missing the required IASWD cluster handler!",
|
||||
ATTR_IEEE,
|
||||
str(ieee),
|
||||
)
|
||||
@ -1466,13 +1466,13 @@ def async_load_api(hass: HomeAssistant) -> None:
|
||||
intensity: int = service.data[ATTR_WARNING_DEVICE_STROBE_INTENSITY]
|
||||
|
||||
if (zha_device := zha_gateway.get_device(ieee)) is not None:
|
||||
if channel := _get_ias_wd_channel(zha_device):
|
||||
await channel.issue_start_warning(
|
||||
if cluster_handler := _get_ias_wd_cluster_handler(zha_device):
|
||||
await cluster_handler.issue_start_warning(
|
||||
mode, strobe, level, duration, duty_mode, intensity
|
||||
)
|
||||
else:
|
||||
_LOGGER.error(
|
||||
"Warning IASWD: %s: [%s] is missing the required IASWD channel!",
|
||||
"Warning IASWD: %s: [%s] is missing the required IASWD cluster handler!",
|
||||
ATTR_IEEE,
|
||||
str(ieee),
|
||||
)
|
||||
|
@ -121,19 +121,19 @@ def setup_zha(hass, config_entry, zigpy_app_controller):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def channel():
|
||||
"""Channel mock factory fixture."""
|
||||
def cluster_handler():
|
||||
"""ClusterHandler mock factory fixture."""
|
||||
|
||||
def channel(name: str, cluster_id: int, endpoint_id: int = 1):
|
||||
def cluster_handler(name: str, cluster_id: int, endpoint_id: int = 1):
|
||||
ch = MagicMock()
|
||||
ch.name = name
|
||||
ch.generic_id = f"channel_0x{cluster_id:04x}"
|
||||
ch.generic_id = f"cluster_handler_0x{cluster_id:04x}"
|
||||
ch.id = f"{endpoint_id}:0x{cluster_id:04x}"
|
||||
ch.async_configure = AsyncMock()
|
||||
ch.async_initialize = AsyncMock()
|
||||
return ch
|
||||
|
||||
return channel
|
||||
return cluster_handler
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -162,7 +162,7 @@ def zigpy_device_mock(zigpy_app_controller):
|
||||
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)
|
||||
endpoint.profile_id = ep.get(SIG_EP_PROFILE, 0x0104)
|
||||
endpoint.request = AsyncMock(return_value=[0])
|
||||
|
||||
for cluster_id in ep.get(SIG_EP_INPUT, []):
|
||||
@ -171,6 +171,8 @@ def zigpy_device_mock(zigpy_app_controller):
|
||||
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)
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
"""Test ZHA base channel module."""
|
||||
"""Test ZHA base cluster handlers module."""
|
||||
|
||||
from homeassistant.components.zha.core.channels.base import parse_and_log_command
|
||||
from homeassistant.components.zha.core.cluster_handlers import parse_and_log_command
|
||||
|
||||
from tests.components.zha.test_channels import ( # noqa: F401
|
||||
channel_pool,
|
||||
from tests.components.zha.test_cluster_handlers import ( # noqa: F401
|
||||
endpoint,
|
||||
poll_control_ch,
|
||||
zigpy_coordinator_device,
|
||||
)
|
||||
|
@ -1,4 +1,4 @@
|
||||
"""Test ZHA Core channels."""
|
||||
"""Test ZHA Core cluster handlers."""
|
||||
import asyncio
|
||||
import math
|
||||
from unittest import mock
|
||||
@ -6,15 +6,17 @@ from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
import zigpy.endpoint
|
||||
from zigpy.endpoint import Endpoint as ZigpyEndpoint
|
||||
import zigpy.profiles.zha
|
||||
import zigpy.types as t
|
||||
from zigpy.zcl import foundation
|
||||
import zigpy.zcl.clusters
|
||||
import zigpy.zdo.types as zdo_t
|
||||
|
||||
import homeassistant.components.zha.core.channels as zha_channels
|
||||
import homeassistant.components.zha.core.channels.base as base_channels
|
||||
import homeassistant.components.zha.core.cluster_handlers as cluster_handlers
|
||||
import homeassistant.components.zha.core.const as zha_const
|
||||
from homeassistant.components.zha.core.device import ZHADevice as Device
|
||||
from homeassistant.components.zha.core.endpoint import Endpoint
|
||||
import homeassistant.components.zha.core.registries as registries
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
@ -65,20 +67,22 @@ def zigpy_coordinator_device(zigpy_device_mock):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def channel_pool(zigpy_coordinator_device):
|
||||
"""Endpoint Channels fixture."""
|
||||
ch_pool_mock = mock.MagicMock(spec_set=zha_channels.ChannelPool)
|
||||
ch_pool_mock.endpoint.device.application.get_device.return_value = (
|
||||
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(ch_pool_mock).skip_configuration = mock.PropertyMock(return_value=False)
|
||||
ch_pool_mock.id = 1
|
||||
return ch_pool_mock
|
||||
type(endpoint_mock.device).skip_configuration = mock.PropertyMock(
|
||||
return_value=False
|
||||
)
|
||||
endpoint_mock.id = 1
|
||||
return endpoint_mock
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def poll_control_ch(channel_pool, zigpy_device_mock):
|
||||
"""Poll control channel 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}},
|
||||
@ -88,8 +92,8 @@ def poll_control_ch(channel_pool, zigpy_device_mock):
|
||||
)
|
||||
|
||||
cluster = zigpy_dev.endpoints[1].in_clusters[cluster_id]
|
||||
channel_class = registries.ZIGBEE_CHANNEL_REGISTRY.get(cluster_id)
|
||||
return channel_class(cluster, channel_pool)
|
||||
cluster_handler_class = registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.get(cluster_id)
|
||||
return cluster_handler_class(cluster, endpoint)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@ -236,10 +240,10 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock):
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_in_channel_config(
|
||||
cluster_id, bind_count, attrs, channel_pool, zigpy_device_mock, zha_gateway
|
||||
async def test_in_cluster_handler_config(
|
||||
cluster_id, bind_count, attrs, endpoint, zigpy_device_mock, zha_gateway
|
||||
) -> None:
|
||||
"""Test ZHA core channel configuration for input clusters."""
|
||||
"""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",
|
||||
@ -248,12 +252,12 @@ async def test_in_channel_config(
|
||||
)
|
||||
|
||||
cluster = zigpy_dev.endpoints[1].in_clusters[cluster_id]
|
||||
channel_class = registries.ZIGBEE_CHANNEL_REGISTRY.get(
|
||||
cluster_id, base_channels.ZigbeeChannel
|
||||
cluster_handler_class = registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.get(
|
||||
cluster_id, cluster_handlers.ClusterHandler
|
||||
)
|
||||
channel = channel_class(cluster, channel_pool)
|
||||
cluster_handler = cluster_handler_class(cluster, endpoint)
|
||||
|
||||
await channel.async_configure()
|
||||
await cluster_handler.async_configure()
|
||||
|
||||
assert cluster.bind.call_count == bind_count
|
||||
assert cluster.configure_reporting.call_count == 0
|
||||
@ -299,10 +303,10 @@ async def test_in_channel_config(
|
||||
(0x0B04, 1),
|
||||
],
|
||||
)
|
||||
async def test_out_channel_config(
|
||||
cluster_id, bind_count, channel_pool, zigpy_device_mock, zha_gateway
|
||||
async def test_out_cluster_handler_config(
|
||||
cluster_id, bind_count, endpoint, zigpy_device_mock, zha_gateway
|
||||
) -> None:
|
||||
"""Test ZHA core channel configuration for output clusters."""
|
||||
"""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",
|
||||
@ -312,102 +316,109 @@ async def test_out_channel_config(
|
||||
|
||||
cluster = zigpy_dev.endpoints[1].out_clusters[cluster_id]
|
||||
cluster.bind_only = True
|
||||
channel_class = registries.ZIGBEE_CHANNEL_REGISTRY.get(
|
||||
cluster_id, base_channels.ZigbeeChannel
|
||||
cluster_handler_class = registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.get(
|
||||
cluster_id, cluster_handlers.ClusterHandler
|
||||
)
|
||||
channel = channel_class(cluster, channel_pool)
|
||||
cluster_handler = cluster_handler_class(cluster, endpoint)
|
||||
|
||||
await channel.async_configure()
|
||||
await cluster_handler.async_configure()
|
||||
|
||||
assert cluster.bind.call_count == bind_count
|
||||
assert cluster.configure_reporting.call_count == 0
|
||||
|
||||
|
||||
def test_channel_registry() -> None:
|
||||
"""Test ZIGBEE Channel Registry."""
|
||||
for cluster_id, channel in registries.ZIGBEE_CHANNEL_REGISTRY.items():
|
||||
def test_cluster_handler_registry() -> None:
|
||||
"""Test ZIGBEE cluster handler Registry."""
|
||||
for (
|
||||
cluster_id,
|
||||
cluster_handler,
|
||||
) in registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.items():
|
||||
assert isinstance(cluster_id, int)
|
||||
assert 0 <= cluster_id <= 0xFFFF
|
||||
assert issubclass(channel, base_channels.ZigbeeChannel)
|
||||
assert issubclass(cluster_handler, cluster_handlers.ClusterHandler)
|
||||
|
||||
|
||||
def test_epch_unclaimed_channels(channel) -> None:
|
||||
"""Test unclaimed channels."""
|
||||
def test_epch_unclaimed_cluster_handlers(cluster_handler) -> None:
|
||||
"""Test unclaimed cluster handlers."""
|
||||
|
||||
ch_1 = channel(zha_const.CHANNEL_ON_OFF, 6)
|
||||
ch_2 = channel(zha_const.CHANNEL_LEVEL, 8)
|
||||
ch_3 = channel(zha_const.CHANNEL_COLOR, 768)
|
||||
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_channels = zha_channels.ChannelPool(
|
||||
mock.MagicMock(spec_set=zha_channels.Channels), mock.sentinel.ep
|
||||
ep_cluster_handlers = Endpoint(
|
||||
mock.MagicMock(spec_set=ZigpyEndpoint), mock.MagicMock(spec_set=Device)
|
||||
)
|
||||
all_channels = {ch_1.id: ch_1, ch_2.id: ch_2, ch_3.id: ch_3}
|
||||
with mock.patch.dict(ep_channels.all_channels, all_channels, clear=True):
|
||||
available = ep_channels.unclaimed_channels()
|
||||
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_channels.claimed_channels[ch_2.id] = ch_2
|
||||
available = ep_channels.unclaimed_channels()
|
||||
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_channels.claimed_channels[ch_1.id] = ch_1
|
||||
available = ep_channels.unclaimed_channels()
|
||||
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_channels.claimed_channels[ch_3.id] = ch_3
|
||||
available = ep_channels.unclaimed_channels()
|
||||
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_channels(channel) -> None:
|
||||
"""Test channel claiming."""
|
||||
def test_epch_claim_cluster_handlers(cluster_handler) -> None:
|
||||
"""Test cluster handler claiming."""
|
||||
|
||||
ch_1 = channel(zha_const.CHANNEL_ON_OFF, 6)
|
||||
ch_2 = channel(zha_const.CHANNEL_LEVEL, 8)
|
||||
ch_3 = channel(zha_const.CHANNEL_COLOR, 768)
|
||||
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_channels = zha_channels.ChannelPool(
|
||||
mock.MagicMock(spec_set=zha_channels.Channels), mock.sentinel.ep
|
||||
ep_cluster_handlers = Endpoint(
|
||||
mock.MagicMock(spec_set=ZigpyEndpoint), mock.MagicMock(spec_set=Device)
|
||||
)
|
||||
all_channels = {ch_1.id: ch_1, ch_2.id: ch_2, ch_3.id: ch_3}
|
||||
with mock.patch.dict(ep_channels.all_channels, all_channels, clear=True):
|
||||
assert ch_1.id not in ep_channels.claimed_channels
|
||||
assert ch_2.id not in ep_channels.claimed_channels
|
||||
assert ch_3.id not in ep_channels.claimed_channels
|
||||
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_channels.claim_channels([ch_2])
|
||||
assert ch_1.id not in ep_channels.claimed_channels
|
||||
assert ch_2.id in ep_channels.claimed_channels
|
||||
assert ep_channels.claimed_channels[ch_2.id] is ch_2
|
||||
assert ch_3.id not in ep_channels.claimed_channels
|
||||
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_channels.claim_channels([ch_3, ch_1])
|
||||
assert ch_1.id in ep_channels.claimed_channels
|
||||
assert ep_channels.claimed_channels[ch_1.id] is ch_1
|
||||
assert ch_2.id in ep_channels.claimed_channels
|
||||
assert ep_channels.claimed_channels[ch_2.id] is ch_2
|
||||
assert ch_3.id in ep_channels.claimed_channels
|
||||
assert ep_channels.claimed_channels[ch_3.id] is ch_3
|
||||
assert "1:0x0300" in ep_channels.claimed_channels
|
||||
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.channels.ChannelPool.add_client_channels"
|
||||
"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_channels_all_channels(m1, zha_device_mock) -> None:
|
||||
"""Test EndpointChannels adding all channels."""
|
||||
def test_ep_all_cluster_handlers(m1, zha_device_mock) -> None:
|
||||
"""Test Endpoint adding all cluster handlers."""
|
||||
zha_device = zha_device_mock(
|
||||
{
|
||||
1: {
|
||||
@ -422,43 +433,37 @@ def test_ep_channels_all_channels(m1, zha_device_mock) -> None:
|
||||
},
|
||||
}
|
||||
)
|
||||
channels = zha_channels.Channels(zha_device)
|
||||
|
||||
ep_channels = zha_channels.ChannelPool.new(channels, 1)
|
||||
assert "1:0x0000" in ep_channels.all_channels
|
||||
assert "1:0x0001" in ep_channels.all_channels
|
||||
assert "1:0x0006" in ep_channels.all_channels
|
||||
assert "1:0x0008" in ep_channels.all_channels
|
||||
assert "1:0x0300" not in ep_channels.all_channels
|
||||
assert "2:0x0000" not in ep_channels.all_channels
|
||||
assert "2:0x0001" not in ep_channels.all_channels
|
||||
assert "2:0x0006" not in ep_channels.all_channels
|
||||
assert "2:0x0008" not in ep_channels.all_channels
|
||||
assert "2:0x0300" not in ep_channels.all_channels
|
||||
|
||||
channels = zha_channels.Channels(zha_device)
|
||||
ep_channels = zha_channels.ChannelPool.new(channels, 2)
|
||||
assert "1:0x0000" not in ep_channels.all_channels
|
||||
assert "1:0x0001" not in ep_channels.all_channels
|
||||
assert "1:0x0006" not in ep_channels.all_channels
|
||||
assert "1:0x0008" not in ep_channels.all_channels
|
||||
assert "1:0x0300" not in ep_channels.all_channels
|
||||
assert "2:0x0000" in ep_channels.all_channels
|
||||
assert "2:0x0001" in ep_channels.all_channels
|
||||
assert "2:0x0006" in ep_channels.all_channels
|
||||
assert "2:0x0008" in ep_channels.all_channels
|
||||
assert "2:0x0300" in ep_channels.all_channels
|
||||
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
|
||||
|
||||
|
||||
@mock.patch(
|
||||
"homeassistant.components.zha.core.channels.ChannelPool.add_client_channels"
|
||||
"homeassistant.components.zha.core.endpoint.Endpoint.add_client_cluster_handlers"
|
||||
)
|
||||
@mock.patch(
|
||||
"homeassistant.components.zha.core.discovery.PROBE.discover_entities",
|
||||
mock.MagicMock(),
|
||||
)
|
||||
def test_channel_power_config(m1, zha_device_mock) -> None:
|
||||
"""Test that channels only get a single power channel."""
|
||||
def test_cluster_handler_power_config(m1, zha_device_mock) -> None:
|
||||
"""Test that cluster handlers only get a single power cluster handler."""
|
||||
in_clusters = [0, 1, 6, 8]
|
||||
zha_device = zha_device_mock(
|
||||
{
|
||||
@ -470,18 +475,16 @@ def test_channel_power_config(m1, zha_device_mock) -> None:
|
||||
},
|
||||
}
|
||||
)
|
||||
channels = zha_channels.Channels.new(zha_device)
|
||||
pools = {pool.id: pool for pool in channels.pools}
|
||||
assert "1:0x0000" in pools[1].all_channels
|
||||
assert "1:0x0001" in pools[1].all_channels
|
||||
assert "1:0x0006" in pools[1].all_channels
|
||||
assert "1:0x0008" in pools[1].all_channels
|
||||
assert "1:0x0300" not in pools[1].all_channels
|
||||
assert "2:0x0000" in pools[2].all_channels
|
||||
assert "2:0x0001" not in pools[2].all_channels
|
||||
assert "2:0x0006" in pools[2].all_channels
|
||||
assert "2:0x0008" in pools[2].all_channels
|
||||
assert "2:0x0300" in pools[2].all_channels
|
||||
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 = zha_device_mock(
|
||||
{
|
||||
@ -489,46 +492,43 @@ def test_channel_power_config(m1, zha_device_mock) -> None:
|
||||
2: {SIG_EP_INPUT: in_clusters, SIG_EP_OUTPUT: [], SIG_EP_TYPE: 0x0000},
|
||||
}
|
||||
)
|
||||
channels = zha_channels.Channels.new(zha_device)
|
||||
pools = {pool.id: pool for pool in channels.pools}
|
||||
assert "1:0x0001" not in pools[1].all_channels
|
||||
assert "2:0x0001" in pools[2].all_channels
|
||||
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 = zha_device_mock(
|
||||
{2: {SIG_EP_INPUT: in_clusters, SIG_EP_OUTPUT: [], SIG_EP_TYPE: 0x0000}}
|
||||
)
|
||||
channels = zha_channels.Channels.new(zha_device)
|
||||
pools = {pool.id: pool for pool in channels.pools}
|
||||
assert "2:0x0001" in pools[2].all_channels
|
||||
assert "2:0x0001" in zha_device._endpoints[2].all_cluster_handlers
|
||||
|
||||
|
||||
async def test_ep_channels_configure(channel) -> None:
|
||||
"""Test unclaimed channels."""
|
||||
async def test_ep_cluster_handlers_configure(cluster_handler) -> None:
|
||||
"""Test unclaimed cluster handlers."""
|
||||
|
||||
ch_1 = channel(zha_const.CHANNEL_ON_OFF, 6)
|
||||
ch_2 = channel(zha_const.CHANNEL_LEVEL, 8)
|
||||
ch_3 = channel(zha_const.CHANNEL_COLOR, 768)
|
||||
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=asyncio.TimeoutError)
|
||||
ch_3.async_initialize = AsyncMock(side_effect=asyncio.TimeoutError)
|
||||
ch_4 = channel(zha_const.CHANNEL_ON_OFF, 6)
|
||||
ch_5 = channel(zha_const.CHANNEL_LEVEL, 8)
|
||||
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=asyncio.TimeoutError)
|
||||
ch_5.async_initialize = AsyncMock(side_effect=asyncio.TimeoutError)
|
||||
|
||||
channels = mock.MagicMock(spec_set=zha_channels.Channels)
|
||||
type(channels).semaphore = mock.PropertyMock(return_value=asyncio.Semaphore(3))
|
||||
ep_channels = zha_channels.ChannelPool(channels, mock.sentinel.ep)
|
||||
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=Device))
|
||||
|
||||
claimed = {ch_1.id: ch_1, ch_2.id: ch_2, ch_3.id: ch_3}
|
||||
client_chans = {ch_4.id: ch_4, ch_5.id: ch_5}
|
||||
client_handlers = {ch_4.id: ch_4, ch_5.id: ch_5}
|
||||
|
||||
with mock.patch.dict(
|
||||
ep_channels.claimed_channels, claimed, clear=True
|
||||
), mock.patch.dict(ep_channels.client_channels, client_chans, clear=True):
|
||||
await ep_channels.async_configure()
|
||||
await ep_channels.async_initialize(mock.sentinel.from_cache)
|
||||
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_chans.values()]:
|
||||
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
|
||||
@ -540,7 +540,7 @@ async def test_ep_channels_configure(channel) -> None:
|
||||
|
||||
|
||||
async def test_poll_control_configure(poll_control_ch) -> None:
|
||||
"""Test poll control channel configuration."""
|
||||
"""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] == {
|
||||
@ -549,7 +549,7 @@ async def test_poll_control_configure(poll_control_ch) -> None:
|
||||
|
||||
|
||||
async def test_poll_control_checkin_response(poll_control_ch) -> None:
|
||||
"""Test poll control channel checkin response."""
|
||||
"""Test poll control cluster handler checkin response."""
|
||||
rsp_mock = AsyncMock()
|
||||
set_interval_mock = AsyncMock()
|
||||
fast_poll_mock = AsyncMock()
|
||||
@ -576,9 +576,9 @@ async def test_poll_control_checkin_response(poll_control_ch) -> None:
|
||||
async def test_poll_control_cluster_command(
|
||||
hass: HomeAssistant, poll_control_device
|
||||
) -> None:
|
||||
"""Test poll control channel response to cluster command."""
|
||||
"""Test poll control cluster handler response to cluster command."""
|
||||
checkin_mock = AsyncMock()
|
||||
poll_control_ch = poll_control_device.channels.pools[0].all_channels["1:0x0020"]
|
||||
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)
|
||||
|
||||
@ -607,9 +607,9 @@ async def test_poll_control_cluster_command(
|
||||
async def test_poll_control_ignore_list(
|
||||
hass: HomeAssistant, poll_control_device
|
||||
) -> None:
|
||||
"""Test poll control channel ignore list."""
|
||||
"""Test poll control cluster handler ignore list."""
|
||||
set_long_poll_mock = AsyncMock()
|
||||
poll_control_ch = poll_control_device.channels.pools[0].all_channels["1:0x0020"]
|
||||
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):
|
||||
@ -626,9 +626,9 @@ async def test_poll_control_ignore_list(
|
||||
|
||||
|
||||
async def test_poll_control_ikea(hass: HomeAssistant, poll_control_device) -> None:
|
||||
"""Test poll control channel ignore list for ikea."""
|
||||
"""Test poll control cluster handler ignore list for ikea."""
|
||||
set_long_poll_mock = AsyncMock()
|
||||
poll_control_ch = poll_control_device.channels.pools[0].all_channels["1:0x0020"]
|
||||
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
|
||||
@ -651,12 +651,12 @@ def zigpy_zll_device(zigpy_device_mock):
|
||||
|
||||
|
||||
async def test_zll_device_groups(
|
||||
zigpy_zll_device, channel_pool, zigpy_coordinator_device
|
||||
zigpy_zll_device, endpoint, zigpy_coordinator_device
|
||||
) -> None:
|
||||
"""Test adding coordinator to ZLL groups."""
|
||||
|
||||
cluster = zigpy_zll_device.endpoints[1].lightlink
|
||||
channel = zha_channels.lightlink.LightLink(cluster, channel_pool)
|
||||
cluster_handler = cluster_handlers.lightlink.LightLink(cluster, endpoint)
|
||||
|
||||
get_group_identifiers_rsp = zigpy.zcl.clusters.lightlink.LightLink.commands_by_name[
|
||||
"get_group_identifiers_rsp"
|
||||
@ -671,7 +671,7 @@ async def test_zll_device_groups(
|
||||
)
|
||||
),
|
||||
) as cmd_mock:
|
||||
await channel.async_configure()
|
||||
await cluster_handler.async_configure()
|
||||
assert cmd_mock.await_count == 1
|
||||
assert (
|
||||
cluster.server_commands[cmd_mock.await_args[0][0]].name
|
||||
@ -693,7 +693,7 @@ async def test_zll_device_groups(
|
||||
)
|
||||
),
|
||||
) as cmd_mock:
|
||||
await channel.async_configure()
|
||||
await cluster_handler.async_configure()
|
||||
assert cmd_mock.await_count == 1
|
||||
assert (
|
||||
cluster.server_commands[cmd_mock.await_args[0][0]].name
|
||||
@ -711,37 +711,34 @@ async def test_zll_device_groups(
|
||||
)
|
||||
|
||||
|
||||
@mock.patch(
|
||||
"homeassistant.components.zha.core.channels.ChannelPool.add_client_channels"
|
||||
)
|
||||
@mock.patch(
|
||||
"homeassistant.components.zha.core.discovery.PROBE.discover_entities",
|
||||
mock.MagicMock(),
|
||||
)
|
||||
async def test_cluster_no_ep_attribute(m1, zha_device_mock) -> None:
|
||||
"""Test channels for clusters without ep_attribute."""
|
||||
async def test_cluster_no_ep_attribute(zha_device_mock) -> 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}},
|
||||
)
|
||||
|
||||
channels = zha_channels.Channels.new(zha_device)
|
||||
pools = {pool.id: pool for pool in channels.pools}
|
||||
assert "1:0x042e" in pools[1].all_channels
|
||||
assert pools[1].all_channels["1:0x042e"].name
|
||||
assert "1:0x042e" in zha_device._endpoints[1].all_cluster_handlers
|
||||
assert zha_device._endpoints[1].all_cluster_handlers["1:0x042e"].name
|
||||
|
||||
|
||||
async def test_configure_reporting(hass: HomeAssistant) -> None:
|
||||
"""Test setting up a channel and configuring attribute reporting in two batches."""
|
||||
async def test_configure_reporting(hass: HomeAssistant, endpoint) -> None:
|
||||
"""Test setting up a cluster handler and configuring attribute reporting in two batches."""
|
||||
|
||||
class TestZigbeeChannel(base_channels.ZigbeeChannel):
|
||||
class TestZigbeeClusterHandler(cluster_handlers.ClusterHandler):
|
||||
BIND = True
|
||||
REPORT_CONFIG = (
|
||||
# By name
|
||||
base_channels.AttrReportConfig(attr="current_x", config=(1, 60, 1)),
|
||||
base_channels.AttrReportConfig(attr="current_hue", config=(1, 60, 2)),
|
||||
base_channels.AttrReportConfig(attr="color_temperature", config=(1, 60, 3)),
|
||||
base_channels.AttrReportConfig(attr="current_y", config=(1, 60, 4)),
|
||||
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)
|
||||
@ -761,11 +758,8 @@ async def test_configure_reporting(hass: HomeAssistant) -> None:
|
||||
],
|
||||
)
|
||||
|
||||
ch_pool = mock.AsyncMock(spec_set=zha_channels.ChannelPool)
|
||||
ch_pool.skip_configuration = False
|
||||
|
||||
channel = TestZigbeeChannel(cluster, ch_pool)
|
||||
await channel.async_configure()
|
||||
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 == [
|
@ -46,9 +46,9 @@ def required_platforms_only():
|
||||
def zigpy_device(zigpy_device_mock):
|
||||
"""Device tracker zigpy device."""
|
||||
|
||||
def _dev(with_basic_channel: bool = True, **kwargs):
|
||||
def _dev(with_basic_cluster_handler: bool = True, **kwargs):
|
||||
in_clusters = [general.OnOff.cluster_id]
|
||||
if with_basic_channel:
|
||||
if with_basic_cluster_handler:
|
||||
in_clusters.append(general.Basic.cluster_id)
|
||||
|
||||
endpoints = {
|
||||
@ -67,9 +67,9 @@ def zigpy_device(zigpy_device_mock):
|
||||
def zigpy_device_mains(zigpy_device_mock):
|
||||
"""Device tracker zigpy device."""
|
||||
|
||||
def _dev(with_basic_channel: bool = True):
|
||||
def _dev(with_basic_cluster_handler: bool = True):
|
||||
in_clusters = [general.OnOff.cluster_id]
|
||||
if with_basic_channel:
|
||||
if with_basic_cluster_handler:
|
||||
in_clusters.append(general.Basic.cluster_id)
|
||||
|
||||
endpoints = {
|
||||
@ -87,15 +87,15 @@ def zigpy_device_mains(zigpy_device_mock):
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device_with_basic_channel(zigpy_device_mains):
|
||||
"""Return a ZHA device with a basic channel present."""
|
||||
return zigpy_device_mains(with_basic_channel=True)
|
||||
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_channel(zigpy_device):
|
||||
"""Return a ZHA device with a basic channel present."""
|
||||
return zigpy_device(with_basic_channel=False)
|
||||
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
|
||||
@ -125,32 +125,32 @@ def _send_time_changed(hass, seconds):
|
||||
|
||||
|
||||
@patch(
|
||||
"homeassistant.components.zha.core.channels.general.BasicChannel.async_initialize",
|
||||
"homeassistant.components.zha.core.cluster_handlers.general.BasicClusterHandler.async_initialize",
|
||||
new=mock.AsyncMock(),
|
||||
)
|
||||
async def test_check_available_success(
|
||||
hass: HomeAssistant, device_with_basic_channel, zha_device_restored
|
||||
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_channel)
|
||||
zha_device = await zha_device_restored(device_with_basic_cluster_handler)
|
||||
await async_enable_traffic(hass, [zha_device])
|
||||
basic_ch = device_with_basic_channel.endpoints[3].basic
|
||||
basic_ch = device_with_basic_cluster_handler.endpoints[3].basic
|
||||
|
||||
basic_ch.read_attributes.reset_mock()
|
||||
device_with_basic_channel.last_seen = None
|
||||
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_channel.last_seen = (
|
||||
device_with_basic_cluster_handler.last_seen = (
|
||||
time.time() - zha_device.consider_unavailable_time - 2
|
||||
)
|
||||
_seens = [time.time(), device_with_basic_channel.last_seen]
|
||||
_seens = [time.time(), device_with_basic_cluster_handler.last_seen]
|
||||
|
||||
def _update_last_seen(*args, **kwargs):
|
||||
device_with_basic_channel.last_seen = _seens.pop()
|
||||
device_with_basic_cluster_handler.last_seen = _seens.pop()
|
||||
|
||||
basic_ch.read_attributes.side_effect = _update_last_seen
|
||||
|
||||
@ -177,22 +177,22 @@ async def test_check_available_success(
|
||||
|
||||
|
||||
@patch(
|
||||
"homeassistant.components.zha.core.channels.general.BasicChannel.async_initialize",
|
||||
"homeassistant.components.zha.core.cluster_handlers.general.BasicClusterHandler.async_initialize",
|
||||
new=mock.AsyncMock(),
|
||||
)
|
||||
async def test_check_available_unsuccessful(
|
||||
hass: HomeAssistant, device_with_basic_channel, zha_device_restored
|
||||
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_channel)
|
||||
zha_device = await zha_device_restored(device_with_basic_cluster_handler)
|
||||
await async_enable_traffic(hass, [zha_device])
|
||||
basic_ch = device_with_basic_channel.endpoints[3].basic
|
||||
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_channel.last_seen = (
|
||||
device_with_basic_cluster_handler.last_seen = (
|
||||
time.time() - zha_device.consider_unavailable_time - 2
|
||||
)
|
||||
|
||||
@ -219,24 +219,24 @@ async def test_check_available_unsuccessful(
|
||||
|
||||
|
||||
@patch(
|
||||
"homeassistant.components.zha.core.channels.general.BasicChannel.async_initialize",
|
||||
"homeassistant.components.zha.core.cluster_handlers.general.BasicClusterHandler.async_initialize",
|
||||
new=mock.AsyncMock(),
|
||||
)
|
||||
async def test_check_available_no_basic_channel(
|
||||
async def test_check_available_no_basic_cluster_handler(
|
||||
hass: HomeAssistant,
|
||||
device_without_basic_channel,
|
||||
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_channel)
|
||||
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_channel.last_seen = (
|
||||
device_without_basic_cluster_handler.last_seen = (
|
||||
time.time() - zha_device.consider_unavailable_time - 2
|
||||
)
|
||||
|
||||
@ -248,9 +248,9 @@ async def test_check_available_no_basic_channel(
|
||||
|
||||
|
||||
async def test_ota_sw_version(hass: HomeAssistant, ota_zha_device) -> None:
|
||||
"""Test device entry gets sw_version updated via OTA channel."""
|
||||
"""Test device entry gets sw_version updated via OTA cluster handler."""
|
||||
|
||||
ota_ch = ota_zha_device.channels.pools[0].client_channels["1:0x0019"]
|
||||
ota_ch = ota_zha_device._endpoints[1].client_cluster_handlers["1:0x0019"]
|
||||
dev_registry = dr.async_get(hass)
|
||||
entry = dev_registry.async_get(ota_zha_device.device_id)
|
||||
assert entry.sw_version is None
|
||||
|
@ -302,8 +302,8 @@ async def test_action(hass: HomeAssistant, device_ias, device_inovelli) -> None:
|
||||
await hass.async_block_till_done()
|
||||
calls = async_mock_service(hass, DOMAIN, "warning_device_warn")
|
||||
|
||||
channel = zha_device.channels.pools[0].client_channels["1:0x0006"]
|
||||
channel.zha_send_event(COMMAND_SINGLE, [])
|
||||
cluster_handler = zha_device.endpoints[1].client_cluster_handlers["1:0x0006"]
|
||||
cluster_handler.zha_send_event(COMMAND_SINGLE, [])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls) == 1
|
||||
@ -350,8 +350,8 @@ async def test_action(hass: HomeAssistant, device_ias, device_inovelli) -> None:
|
||||
async def test_invalid_zha_event_type(hass: HomeAssistant, device_ias) -> None:
|
||||
"""Test that unexpected types are not passed to `zha_send_event`."""
|
||||
zigpy_device, zha_device = device_ias
|
||||
channel = zha_device.channels.pools[0].client_channels["1:0x0006"]
|
||||
cluster_handler = zha_device._endpoints[1].client_cluster_handlers["1:0x0006"]
|
||||
|
||||
# `zha_send_event` accepts only zigpy responses, lists, and dicts
|
||||
with pytest.raises(TypeError):
|
||||
channel.zha_send_event(COMMAND_SINGLE, 123)
|
||||
cluster_handler.zha_send_event(COMMAND_SINGLE, 123)
|
||||
|
@ -223,8 +223,8 @@ async def test_if_fires_on_event(hass: HomeAssistant, mock_devices, calls) -> No
|
||||
|
||||
await hass.async_block_till_done()
|
||||
|
||||
channel = zha_device.channels.pools[0].client_channels["1:0x0006"]
|
||||
channel.zha_send_event(COMMAND_SINGLE, [])
|
||||
cluster_handler = zha_device.endpoints[1].client_cluster_handlers["1:0x0006"]
|
||||
cluster_handler.zha_send_event(COMMAND_SINGLE, [])
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(calls) == 1
|
||||
|
@ -14,10 +14,10 @@ import zigpy.zcl.clusters.security
|
||||
import zigpy.zcl.foundation as zcl_f
|
||||
|
||||
import homeassistant.components.zha.binary_sensor
|
||||
import homeassistant.components.zha.core.channels as zha_channels
|
||||
import homeassistant.components.zha.core.channels.base as base_channels
|
||||
import homeassistant.components.zha.core.cluster_handlers as cluster_handlers
|
||||
import homeassistant.components.zha.core.const as zha_const
|
||||
import homeassistant.components.zha.core.discovery as disc
|
||||
from homeassistant.components.zha.core.endpoint import Endpoint
|
||||
import homeassistant.components.zha.core.registries as zha_regs
|
||||
import homeassistant.components.zha.cover
|
||||
import homeassistant.components.zha.device_tracker
|
||||
@ -33,11 +33,11 @@ import homeassistant.helpers.entity_registry as er
|
||||
from .common import get_zha_gateway
|
||||
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
|
||||
from .zha_devices_list import (
|
||||
DEV_SIG_CHANNELS,
|
||||
DEV_SIG_CLUSTER_HANDLERS,
|
||||
DEV_SIG_ENT_MAP,
|
||||
DEV_SIG_ENT_MAP_CLASS,
|
||||
DEV_SIG_ENT_MAP_ID,
|
||||
DEV_SIG_EVT_CHANNELS,
|
||||
DEV_SIG_EVT_CLUSTER_HANDLERS,
|
||||
DEVICES,
|
||||
)
|
||||
|
||||
@ -63,27 +63,6 @@ def contains_ignored_suffix(unique_id: str) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def channels_mock(zha_device_mock):
|
||||
"""Channels mock factory."""
|
||||
|
||||
def _mock(
|
||||
endpoints,
|
||||
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=False,
|
||||
):
|
||||
zha_dev = zha_device_mock(
|
||||
endpoints, ieee, manufacturer, model, node_desc, patch_cluster=patch_cluster
|
||||
)
|
||||
channels = zha_channels.Channels.new(zha_dev)
|
||||
return channels
|
||||
|
||||
return _mock
|
||||
|
||||
|
||||
@patch(
|
||||
"zigpy.zcl.clusters.general.Identify.request",
|
||||
new=AsyncMock(return_value=[mock.sentinel.data, zcl_f.Status.SUCCESS]),
|
||||
@ -119,14 +98,14 @@ async def test_devices(
|
||||
if cluster_identify:
|
||||
cluster_identify.request.reset_mock()
|
||||
|
||||
orig_new_entity = zha_channels.ChannelPool.async_new_entity
|
||||
orig_new_entity = Endpoint.async_new_entity
|
||||
_dispatch = mock.MagicMock(wraps=orig_new_entity)
|
||||
try:
|
||||
zha_channels.ChannelPool.async_new_entity = lambda *a, **kw: _dispatch(*a, **kw)
|
||||
Endpoint.async_new_entity = lambda *a, **kw: _dispatch(*a, **kw)
|
||||
zha_dev = await zha_device_joined_restored(zigpy_device)
|
||||
await hass_disable_services.async_block_till_done()
|
||||
finally:
|
||||
zha_channels.ChannelPool.async_new_entity = orig_new_entity
|
||||
Endpoint.async_new_entity = orig_new_entity
|
||||
|
||||
if cluster_identify:
|
||||
called = int(zha_device_joined_restored.name == "zha_device_joined")
|
||||
@ -147,34 +126,36 @@ async def test_devices(
|
||||
tsn=None,
|
||||
)
|
||||
|
||||
event_channels = {
|
||||
ch.id for pool in zha_dev.channels.pools for ch in pool.client_channels.values()
|
||||
event_cluster_handlers = {
|
||||
ch.id
|
||||
for endpoint in zha_dev._endpoints.values()
|
||||
for ch in endpoint.client_cluster_handlers.values()
|
||||
}
|
||||
assert event_channels == set(device[DEV_SIG_EVT_CHANNELS])
|
||||
assert event_cluster_handlers == set(device[DEV_SIG_EVT_CLUSTER_HANDLERS])
|
||||
# we need to probe the class create entity factory so we need to reset this to get accurate results
|
||||
zha_regs.ZHA_ENTITIES.clean_up()
|
||||
# build a dict of entity_class -> (component, unique_id, channels) tuple
|
||||
# build a dict of entity_class -> (platform, unique_id, cluster_handlers) tuple
|
||||
ha_ent_info = {}
|
||||
created_entity_count = 0
|
||||
for call in _dispatch.call_args_list:
|
||||
_, component, entity_cls, unique_id, channels = call[0]
|
||||
_, platform, entity_cls, unique_id, cluster_handlers = call[0]
|
||||
# the factory can return None. We filter these out to get an accurate created entity count
|
||||
response = entity_cls.create_entity(unique_id, zha_dev, channels)
|
||||
response = entity_cls.create_entity(unique_id, zha_dev, cluster_handlers)
|
||||
if response and not contains_ignored_suffix(response.name):
|
||||
created_entity_count += 1
|
||||
unique_id_head = UNIQUE_ID_HD.match(unique_id).group(
|
||||
0
|
||||
) # ieee + endpoint_id
|
||||
ha_ent_info[(unique_id_head, entity_cls.__name__)] = (
|
||||
component,
|
||||
platform,
|
||||
unique_id,
|
||||
channels,
|
||||
cluster_handlers,
|
||||
)
|
||||
|
||||
for comp_id, ent_info in device[DEV_SIG_ENT_MAP].items():
|
||||
component, unique_id = comp_id
|
||||
platform, unique_id = comp_id
|
||||
no_tail_id = NO_TAIL_ID.sub("", ent_info[DEV_SIG_ENT_MAP_ID])
|
||||
ha_entity_id = entity_registry.async_get_entity_id(component, "zha", unique_id)
|
||||
ha_entity_id = entity_registry.async_get_entity_id(platform, "zha", unique_id)
|
||||
assert ha_entity_id is not None
|
||||
assert ha_entity_id.startswith(no_tail_id)
|
||||
|
||||
@ -182,13 +163,15 @@ async def test_devices(
|
||||
test_unique_id_head = UNIQUE_ID_HD.match(unique_id).group(0)
|
||||
assert (test_unique_id_head, test_ent_class) in ha_ent_info
|
||||
|
||||
ha_comp, ha_unique_id, ha_channels = ha_ent_info[
|
||||
ha_comp, ha_unique_id, ha_cluster_handlers = ha_ent_info[
|
||||
(test_unique_id_head, test_ent_class)
|
||||
]
|
||||
assert component is ha_comp.value
|
||||
assert platform is ha_comp.value
|
||||
# unique_id used for discover is the same for "multi entities"
|
||||
assert unique_id.startswith(ha_unique_id)
|
||||
assert {ch.name for ch in ha_channels} == set(ent_info[DEV_SIG_CHANNELS])
|
||||
assert {ch.name for ch in ha_cluster_handlers} == set(
|
||||
ent_info[DEV_SIG_CLUSTER_HANDLERS]
|
||||
)
|
||||
|
||||
assert created_entity_count == len(device[DEV_SIG_ENT_MAP])
|
||||
|
||||
@ -219,16 +202,16 @@ def _get_first_identify_cluster(zigpy_device):
|
||||
)
|
||||
def test_discover_entities(m1, m2) -> None:
|
||||
"""Test discover endpoint class method."""
|
||||
ep_channels = mock.MagicMock()
|
||||
disc.PROBE.discover_entities(ep_channels)
|
||||
endpoint = mock.MagicMock()
|
||||
disc.PROBE.discover_entities(endpoint)
|
||||
assert m1.call_count == 1
|
||||
assert m1.call_args[0][0] is ep_channels
|
||||
assert m1.call_args[0][0] is endpoint
|
||||
assert m2.call_count == 1
|
||||
assert m2.call_args[0][0] is ep_channels
|
||||
assert m2.call_args[0][0] is endpoint
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("device_type", "component", "hit"),
|
||||
("device_type", "platform", "hit"),
|
||||
[
|
||||
(zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, Platform.LIGHT, True),
|
||||
(zigpy.profiles.zha.DeviceType.ON_OFF_BALLAST, Platform.SWITCH, True),
|
||||
@ -236,14 +219,14 @@ def test_discover_entities(m1, m2) -> None:
|
||||
(0xFFFF, None, False),
|
||||
],
|
||||
)
|
||||
def test_discover_by_device_type(device_type, component, hit) -> None:
|
||||
def test_discover_by_device_type(device_type, platform, hit) -> None:
|
||||
"""Test entity discovery by device type."""
|
||||
|
||||
ep_channels = mock.MagicMock(spec_set=zha_channels.ChannelPool)
|
||||
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(ep_channels).endpoint = ep_mock
|
||||
type(endpoint).zigpy_endpoint = ep_mock
|
||||
|
||||
get_entity_mock = mock.MagicMock(
|
||||
return_value=(mock.sentinel.entity_cls, mock.sentinel.claimed)
|
||||
@ -252,26 +235,26 @@ def test_discover_by_device_type(device_type, component, hit) -> None:
|
||||
"homeassistant.components.zha.core.registries.ZHA_ENTITIES.get_entity",
|
||||
get_entity_mock,
|
||||
):
|
||||
disc.PROBE.discover_by_device_type(ep_channels)
|
||||
disc.PROBE.discover_by_device_type(endpoint)
|
||||
if hit:
|
||||
assert get_entity_mock.call_count == 1
|
||||
assert ep_channels.claim_channels.call_count == 1
|
||||
assert ep_channels.claim_channels.call_args[0][0] is mock.sentinel.claimed
|
||||
assert ep_channels.async_new_entity.call_count == 1
|
||||
assert ep_channels.async_new_entity.call_args[0][0] == component
|
||||
assert ep_channels.async_new_entity.call_args[0][1] == mock.sentinel.entity_cls
|
||||
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."""
|
||||
|
||||
ep_channels = mock.MagicMock(spec_set=zha_channels.ChannelPool)
|
||||
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(ep_channels).endpoint = ep_mock
|
||||
type(endpoint).zigpy_endpoint = ep_mock
|
||||
|
||||
overrides = {ep_channels.unique_id: {"type": Platform.SWITCH}}
|
||||
overrides = {endpoint.unique_id: {"type": Platform.SWITCH}}
|
||||
get_entity_mock = mock.MagicMock(
|
||||
return_value=(mock.sentinel.entity_cls, mock.sentinel.claimed)
|
||||
)
|
||||
@ -279,99 +262,105 @@ def test_discover_by_device_type_override() -> None:
|
||||
"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(ep_channels)
|
||||
disc.PROBE.discover_by_device_type(endpoint)
|
||||
assert get_entity_mock.call_count == 1
|
||||
assert ep_channels.claim_channels.call_count == 1
|
||||
assert ep_channels.claim_channels.call_args[0][0] is mock.sentinel.claimed
|
||||
assert ep_channels.async_new_entity.call_count == 1
|
||||
assert ep_channels.async_new_entity.call_args[0][0] == Platform.SWITCH
|
||||
assert ep_channels.async_new_entity.call_args[0][1] == mock.sentinel.entity_cls
|
||||
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."""
|
||||
|
||||
ep_channels = mock.MagicMock(spec_set=zha_channels.ChannelPool)
|
||||
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(ep_channels).endpoint = ep_mock
|
||||
type(endpoint).zigpy_endpoint = ep_mock
|
||||
|
||||
get_entity_mock = mock.MagicMock(
|
||||
return_value=(mock.sentinel.entity_cls, mock.sentinel.claimed)
|
||||
)
|
||||
channel_mock = mock.MagicMock(spec_set=base_channels.ZigbeeChannel)
|
||||
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, channel_mock, ep_channels)
|
||||
disc.PROBE.probe_single_cluster(Platform.SWITCH, cluster_handler_mock, endpoint)
|
||||
|
||||
assert get_entity_mock.call_count == 1
|
||||
assert ep_channels.claim_channels.call_count == 1
|
||||
assert ep_channels.claim_channels.call_args[0][0] is mock.sentinel.claimed
|
||||
assert ep_channels.async_new_entity.call_count == 1
|
||||
assert ep_channels.async_new_entity.call_args[0][0] == Platform.SWITCH
|
||||
assert ep_channels.async_new_entity.call_args[0][1] == mock.sentinel.entity_cls
|
||||
assert ep_channels.async_new_entity.call_args[0][3] == mock.sentinel.claimed
|
||||
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, channels_mock, hass: HomeAssistant
|
||||
device_info, zha_device_mock, hass: HomeAssistant
|
||||
) -> None:
|
||||
"""Test device discovery."""
|
||||
|
||||
with mock.patch(
|
||||
"homeassistant.components.zha.core.channels.Channels.async_new_entity"
|
||||
"homeassistant.components.zha.core.endpoint.Endpoint.async_new_entity"
|
||||
) as new_ent:
|
||||
channels = channels_mock(
|
||||
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=False,
|
||||
patch_cluster=True,
|
||||
)
|
||||
|
||||
assert device_info[DEV_SIG_EVT_CHANNELS] == sorted(
|
||||
ch.id for pool in channels.pools for ch in pool.client_channels.values()
|
||||
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 -> (component, unique_id, channels) tuple
|
||||
# build a dict of entity_class -> (platform, unique_id, cluster_handlers) tuple
|
||||
ha_ent_info = {}
|
||||
for call in new_ent.call_args_list:
|
||||
component, entity_cls, unique_id, channels = call[0]
|
||||
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__)] = (
|
||||
component,
|
||||
platform,
|
||||
unique_id,
|
||||
channels,
|
||||
cluster_handlers,
|
||||
)
|
||||
|
||||
for comp_id, ent_info in device_info[DEV_SIG_ENT_MAP].items():
|
||||
component, unique_id = comp_id
|
||||
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
|
||||
|
||||
ha_comp, ha_unique_id, ha_channels = ha_ent_info[
|
||||
entity_platform, entity_unique_id, entity_cluster_handlers = ha_ent_info[
|
||||
(test_unique_id_head, test_ent_class)
|
||||
]
|
||||
assert component is ha_comp.value
|
||||
assert platform is entity_platform.value
|
||||
# unique_id used for discover is the same for "multi entities"
|
||||
assert unique_id.startswith(ha_unique_id)
|
||||
assert {ch.name for ch in ha_channels} == set(ent_info[DEV_SIG_CHANNELS])
|
||||
assert unique_id.startswith(entity_unique_id)
|
||||
assert {ch.name for ch in entity_cluster_handlers} == set(
|
||||
ent_info[DEV_SIG_CLUSTER_HANDLERS]
|
||||
)
|
||||
|
||||
|
||||
def _ch_mock(cluster):
|
||||
"""Return mock of a channel with a cluster."""
|
||||
channel = mock.MagicMock()
|
||||
type(channel).cluster = mock.PropertyMock(return_value=cluster(mock.MagicMock()))
|
||||
return channel
|
||||
"""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(
|
||||
@ -401,16 +390,16 @@ def _test_single_input_cluster_device_class(probe_mock):
|
||||
|
||||
analog_ch = _ch_mock(_Analog)
|
||||
|
||||
ch_pool = mock.MagicMock(spec_set=zha_channels.ChannelPool)
|
||||
ch_pool.unclaimed_channels.return_value = [
|
||||
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(ch_pool)
|
||||
assert probe_mock.call_count == len(ch_pool.unclaimed_channels())
|
||||
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),
|
||||
@ -419,8 +408,8 @@ def _test_single_input_cluster_device_class(probe_mock):
|
||||
(Platform.SENSOR, analog_ch),
|
||||
)
|
||||
for call, details in zip(probe_mock.call_args_list, probes):
|
||||
component, ch = details
|
||||
assert call[0][0] == component
|
||||
platform, ch = details
|
||||
assert call[0][0] == platform
|
||||
assert call[0][1] == ch
|
||||
|
||||
|
||||
@ -498,7 +487,7 @@ async def test_group_probe_cleanup_called(
|
||||
"homeassistant.components.zha.entity.ZhaEntity.entity_registry_enabled_default",
|
||||
new=Mock(return_value=True),
|
||||
)
|
||||
async def test_channel_with_empty_ep_attribute_cluster(
|
||||
async def test_cluster_handler_with_empty_ep_attribute_cluster(
|
||||
hass_disable_services,
|
||||
zigpy_device_mock,
|
||||
zha_device_joined_restored,
|
||||
|
@ -1036,18 +1036,18 @@ async def test_transitions(
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
group_on_off_channel = zha_group.endpoint[general.OnOff.cluster_id]
|
||||
group_level_channel = zha_group.endpoint[general.LevelControl.cluster_id]
|
||||
group_color_channel = zha_group.endpoint[lighting.Color.cluster_id]
|
||||
assert group_on_off_channel.request.call_count == 0
|
||||
assert group_on_off_channel.request.await_count == 0
|
||||
assert group_color_channel.request.call_count == 1
|
||||
assert group_color_channel.request.await_count == 1
|
||||
assert group_level_channel.request.call_count == 1
|
||||
assert group_level_channel.request.await_count == 1
|
||||
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_channel.request.call_args == call(
|
||||
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,
|
||||
@ -1058,7 +1058,7 @@ async def test_transitions(
|
||||
tries=1,
|
||||
tsn=None,
|
||||
)
|
||||
assert group_level_channel.request.call_args == call(
|
||||
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,
|
||||
@ -1076,9 +1076,9 @@ async def test_transitions(
|
||||
assert group_state.attributes["color_temp"] == 235
|
||||
assert group_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
|
||||
|
||||
group_on_off_channel.request.reset_mock()
|
||||
group_color_channel.request.reset_mock()
|
||||
group_level_channel.request.reset_mock()
|
||||
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(
|
||||
|
@ -25,36 +25,48 @@ def zha_device():
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def channels(channel):
|
||||
"""Return a mock of channels."""
|
||||
def cluster_handlers(cluster_handler):
|
||||
"""Return a mock of cluster_handlers."""
|
||||
|
||||
return [channel("level", 8), channel("on_off", 6)]
|
||||
return [cluster_handler("level", 8), cluster_handler("on_off", 6)]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("rule", "matched"),
|
||||
[
|
||||
(registries.MatchRule(), False),
|
||||
(registries.MatchRule(channel_names={"level"}), True),
|
||||
(registries.MatchRule(channel_names={"level", "no match"}), False),
|
||||
(registries.MatchRule(channel_names={"on_off"}), True),
|
||||
(registries.MatchRule(channel_names={"on_off", "no match"}), False),
|
||||
(registries.MatchRule(channel_names={"on_off", "level"}), True),
|
||||
(registries.MatchRule(channel_names={"on_off", "level", "no match"}), 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={"channel_0x0006"}), True),
|
||||
(registries.MatchRule(generic_ids={"channel_0x0008"}), True),
|
||||
(registries.MatchRule(generic_ids={"channel_0x0006", "channel_0x0008"}), True),
|
||||
(registries.MatchRule(generic_ids={"cluster_handler_0x0006"}), True),
|
||||
(registries.MatchRule(generic_ids={"cluster_handler_0x0008"}), True),
|
||||
(
|
||||
registries.MatchRule(
|
||||
generic_ids={"channel_0x0006", "channel_0x0008", "channel_0x0009"}
|
||||
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={"channel_0x0006", "channel_0x0008"},
|
||||
channel_names={"on_off", "level"},
|
||||
generic_ids={"cluster_handler_0x0006", "cluster_handler_0x0008"},
|
||||
cluster_handler_names={"on_off", "level"},
|
||||
),
|
||||
True,
|
||||
),
|
||||
@ -62,34 +74,50 @@ def channels(channel):
|
||||
(registries.MatchRule(manufacturers="no match"), False),
|
||||
(registries.MatchRule(manufacturers=MANUFACTURER), True),
|
||||
(
|
||||
registries.MatchRule(manufacturers="no match", aux_channels="aux_channel"),
|
||||
registries.MatchRule(
|
||||
manufacturers="no match", aux_cluster_handlers="aux_cluster_handler"
|
||||
),
|
||||
False,
|
||||
),
|
||||
(
|
||||
registries.MatchRule(
|
||||
manufacturers=MANUFACTURER, aux_channels="aux_channel"
|
||||
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_channels="aux_channel"), True),
|
||||
(registries.MatchRule(models="no match", aux_channels="aux_channel"), False),
|
||||
(registries.MatchRule(quirk_classes=QUIRK_CLASS), True),
|
||||
(registries.MatchRule(quirk_classes="no match"), False),
|
||||
(
|
||||
registries.MatchRule(quirk_classes=QUIRK_CLASS, aux_channels="aux_channel"),
|
||||
registries.MatchRule(
|
||||
models=MODEL, aux_cluster_handlers="aux_cluster_handler"
|
||||
),
|
||||
True,
|
||||
),
|
||||
(
|
||||
registries.MatchRule(quirk_classes="no match", aux_channels="aux_channel"),
|
||||
registries.MatchRule(
|
||||
models="no match", aux_cluster_handlers="aux_cluster_handler"
|
||||
),
|
||||
False,
|
||||
),
|
||||
(registries.MatchRule(quirk_classes=QUIRK_CLASS), True),
|
||||
(registries.MatchRule(quirk_classes="no match"), False),
|
||||
(
|
||||
registries.MatchRule(
|
||||
quirk_classes=QUIRK_CLASS, aux_cluster_handlers="aux_cluster_handler"
|
||||
),
|
||||
True,
|
||||
),
|
||||
(
|
||||
registries.MatchRule(
|
||||
quirk_classes="no match", aux_cluster_handlers="aux_cluster_handler"
|
||||
),
|
||||
False,
|
||||
),
|
||||
# match everything
|
||||
(
|
||||
registries.MatchRule(
|
||||
generic_ids={"channel_0x0006", "channel_0x0008"},
|
||||
channel_names={"on_off", "level"},
|
||||
generic_ids={"cluster_handler_0x0006", "cluster_handler_0x0008"},
|
||||
cluster_handler_names={"on_off", "level"},
|
||||
manufacturers=MANUFACTURER,
|
||||
models=MODEL,
|
||||
quirk_classes=QUIRK_CLASS,
|
||||
@ -98,96 +126,114 @@ def channels(channel):
|
||||
),
|
||||
(
|
||||
registries.MatchRule(
|
||||
channel_names="on_off", manufacturers={"random manuf", MANUFACTURER}
|
||||
cluster_handler_names="on_off",
|
||||
manufacturers={"random manuf", MANUFACTURER},
|
||||
),
|
||||
True,
|
||||
),
|
||||
(
|
||||
registries.MatchRule(
|
||||
channel_names="on_off", manufacturers={"random manuf", "Another manuf"}
|
||||
cluster_handler_names="on_off",
|
||||
manufacturers={"random manuf", "Another manuf"},
|
||||
),
|
||||
False,
|
||||
),
|
||||
(
|
||||
registries.MatchRule(
|
||||
channel_names="on_off", manufacturers=lambda x: x == MANUFACTURER
|
||||
cluster_handler_names="on_off",
|
||||
manufacturers=lambda x: x == MANUFACTURER,
|
||||
),
|
||||
True,
|
||||
),
|
||||
(
|
||||
registries.MatchRule(
|
||||
channel_names="on_off", manufacturers=lambda x: x != MANUFACTURER
|
||||
cluster_handler_names="on_off",
|
||||
manufacturers=lambda x: x != MANUFACTURER,
|
||||
),
|
||||
False,
|
||||
),
|
||||
(
|
||||
registries.MatchRule(
|
||||
channel_names="on_off", models={"random model", MODEL}
|
||||
cluster_handler_names="on_off", models={"random model", MODEL}
|
||||
),
|
||||
True,
|
||||
),
|
||||
(
|
||||
registries.MatchRule(
|
||||
channel_names="on_off", models={"random model", "Another model"}
|
||||
),
|
||||
False,
|
||||
),
|
||||
(
|
||||
registries.MatchRule(channel_names="on_off", models=lambda x: x == MODEL),
|
||||
True,
|
||||
),
|
||||
(
|
||||
registries.MatchRule(channel_names="on_off", models=lambda x: x != MODEL),
|
||||
False,
|
||||
),
|
||||
(
|
||||
registries.MatchRule(
|
||||
channel_names="on_off", quirk_classes={"random quirk", QUIRK_CLASS}
|
||||
),
|
||||
True,
|
||||
),
|
||||
(
|
||||
registries.MatchRule(
|
||||
channel_names="on_off", quirk_classes={"random quirk", "another quirk"}
|
||||
cluster_handler_names="on_off", models={"random model", "Another model"}
|
||||
),
|
||||
False,
|
||||
),
|
||||
(
|
||||
registries.MatchRule(
|
||||
channel_names="on_off", quirk_classes=lambda x: x == QUIRK_CLASS
|
||||
cluster_handler_names="on_off", models=lambda x: x == MODEL
|
||||
),
|
||||
True,
|
||||
),
|
||||
(
|
||||
registries.MatchRule(
|
||||
channel_names="on_off", quirk_classes=lambda x: x != QUIRK_CLASS
|
||||
cluster_handler_names="on_off", models=lambda x: x != MODEL
|
||||
),
|
||||
False,
|
||||
),
|
||||
(
|
||||
registries.MatchRule(
|
||||
cluster_handler_names="on_off",
|
||||
quirk_classes={"random quirk", QUIRK_CLASS},
|
||||
),
|
||||
True,
|
||||
),
|
||||
(
|
||||
registries.MatchRule(
|
||||
cluster_handler_names="on_off",
|
||||
quirk_classes={"random quirk", "another quirk"},
|
||||
),
|
||||
False,
|
||||
),
|
||||
(
|
||||
registries.MatchRule(
|
||||
cluster_handler_names="on_off", quirk_classes=lambda x: x == QUIRK_CLASS
|
||||
),
|
||||
True,
|
||||
),
|
||||
(
|
||||
registries.MatchRule(
|
||||
cluster_handler_names="on_off", quirk_classes=lambda x: x != QUIRK_CLASS
|
||||
),
|
||||
False,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_registry_matching(rule, matched, channels) -> None:
|
||||
def test_registry_matching(rule, matched, cluster_handlers) -> None:
|
||||
"""Test strict rule matching."""
|
||||
assert rule.strict_matched(MANUFACTURER, MODEL, channels, QUIRK_CLASS) is matched
|
||||
assert (
|
||||
rule.strict_matched(MANUFACTURER, MODEL, cluster_handlers, QUIRK_CLASS)
|
||||
is matched
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("rule", "matched"),
|
||||
[
|
||||
(registries.MatchRule(), False),
|
||||
(registries.MatchRule(channel_names={"level"}), True),
|
||||
(registries.MatchRule(channel_names={"level", "no match"}), False),
|
||||
(registries.MatchRule(channel_names={"on_off"}), True),
|
||||
(registries.MatchRule(channel_names={"on_off", "no match"}), False),
|
||||
(registries.MatchRule(channel_names={"on_off", "level"}), True),
|
||||
(registries.MatchRule(channel_names={"on_off", "level", "no match"}), 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(channel_names={"on_off", "level"}, models="no match"),
|
||||
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(
|
||||
channel_names={"on_off", "level"},
|
||||
cluster_handler_names={"on_off", "level"},
|
||||
models="no match",
|
||||
manufacturers="no match",
|
||||
),
|
||||
@ -195,40 +241,57 @@ def test_registry_matching(rule, matched, channels) -> None:
|
||||
),
|
||||
(
|
||||
registries.MatchRule(
|
||||
channel_names={"on_off", "level"},
|
||||
cluster_handler_names={"on_off", "level"},
|
||||
models="no match",
|
||||
manufacturers=MANUFACTURER,
|
||||
),
|
||||
True,
|
||||
),
|
||||
# test generic_id matching
|
||||
(registries.MatchRule(generic_ids={"channel_0x0006"}), True),
|
||||
(registries.MatchRule(generic_ids={"channel_0x0008"}), True),
|
||||
(registries.MatchRule(generic_ids={"channel_0x0006", "channel_0x0008"}), True),
|
||||
(registries.MatchRule(generic_ids={"cluster_handler_0x0006"}), True),
|
||||
(registries.MatchRule(generic_ids={"cluster_handler_0x0008"}), True),
|
||||
(
|
||||
registries.MatchRule(
|
||||
generic_ids={"channel_0x0006", "channel_0x0008", "channel_0x0009"}
|
||||
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={"channel_0x0006", "channel_0x0008", "channel_0x0009"},
|
||||
generic_ids={
|
||||
"cluster_handler_0x0006",
|
||||
"cluster_handler_0x0008",
|
||||
"cluster_handler_0x0009",
|
||||
},
|
||||
models="mo match",
|
||||
),
|
||||
False,
|
||||
),
|
||||
(
|
||||
registries.MatchRule(
|
||||
generic_ids={"channel_0x0006", "channel_0x0008", "channel_0x0009"},
|
||||
generic_ids={
|
||||
"cluster_handler_0x0006",
|
||||
"cluster_handler_0x0008",
|
||||
"cluster_handler_0x0009",
|
||||
},
|
||||
models=MODEL,
|
||||
),
|
||||
True,
|
||||
),
|
||||
(
|
||||
registries.MatchRule(
|
||||
generic_ids={"channel_0x0006", "channel_0x0008"},
|
||||
channel_names={"on_off", "level"},
|
||||
generic_ids={"cluster_handler_0x0006", "cluster_handler_0x0008"},
|
||||
cluster_handler_names={"on_off", "level"},
|
||||
),
|
||||
True,
|
||||
),
|
||||
@ -242,8 +305,8 @@ def test_registry_matching(rule, matched, channels) -> None:
|
||||
# match everything
|
||||
(
|
||||
registries.MatchRule(
|
||||
generic_ids={"channel_0x0006", "channel_0x0008"},
|
||||
channel_names={"on_off", "level"},
|
||||
generic_ids={"cluster_handler_0x0006", "cluster_handler_0x0008"},
|
||||
cluster_handler_names={"on_off", "level"},
|
||||
manufacturers=MANUFACTURER,
|
||||
models=MODEL,
|
||||
quirk_classes=QUIRK_CLASS,
|
||||
@ -252,51 +315,64 @@ def test_registry_matching(rule, matched, channels) -> None:
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_registry_loose_matching(rule, matched, channels) -> None:
|
||||
def test_registry_loose_matching(rule, matched, cluster_handlers) -> None:
|
||||
"""Test loose rule matching."""
|
||||
assert rule.loose_matched(MANUFACTURER, MODEL, channels, QUIRK_CLASS) is matched
|
||||
assert (
|
||||
rule.loose_matched(MANUFACTURER, MODEL, cluster_handlers, QUIRK_CLASS)
|
||||
is matched
|
||||
)
|
||||
|
||||
|
||||
def test_match_rule_claim_channels_color(channel) -> None:
|
||||
"""Test channel claiming."""
|
||||
ch_color = channel("color", 0x300)
|
||||
ch_level = channel("level", 8)
|
||||
ch_onoff = channel("on_off", 6)
|
||||
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(channel_names="on_off", aux_channels={"color", "level"})
|
||||
claimed = rule.claim_channels([ch_color, ch_level, ch_onoff])
|
||||
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(channel_names={"level"}), {"level"}),
|
||||
(registries.MatchRule(channel_names={"level", "no match"}), {"level"}),
|
||||
(registries.MatchRule(channel_names={"on_off"}), {"on_off"}),
|
||||
(registries.MatchRule(generic_ids="channel_0x0000"), {"basic"}),
|
||||
(
|
||||
registries.MatchRule(channel_names="level", generic_ids="channel_0x0000"),
|
||||
{"basic", "level"},
|
||||
),
|
||||
(registries.MatchRule(channel_names={"level", "power"}), {"level", "power"}),
|
||||
(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(
|
||||
channel_names={"level", "on_off"}, aux_channels={"basic", "power"}
|
||||
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(channel_names={"color"}), set()),
|
||||
(registries.MatchRule(cluster_handler_names={"color"}), set()),
|
||||
],
|
||||
)
|
||||
def test_match_rule_claim_channels(rule, match, channel, channels) -> None:
|
||||
"""Test channel claiming."""
|
||||
ch_basic = channel("basic", 0)
|
||||
channels.append(ch_basic)
|
||||
ch_power = channel("power", 1)
|
||||
channels.append(ch_power)
|
||||
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_channels(channels)
|
||||
claimed = rule.claim_cluster_handlers(cluster_handlers)
|
||||
assert match == {ch.name for ch in claimed}
|
||||
|
||||
|
||||
@ -318,7 +394,7 @@ def entity_registry():
|
||||
),
|
||||
)
|
||||
def test_weighted_match(
|
||||
channel,
|
||||
cluster_handler,
|
||||
entity_registry: er.EntityRegistry,
|
||||
manufacturer,
|
||||
model,
|
||||
@ -331,40 +407,45 @@ def test_weighted_match(
|
||||
|
||||
@entity_registry.strict_match(
|
||||
s.component,
|
||||
channel_names="on_off",
|
||||
cluster_handler_names="on_off",
|
||||
models={MODEL, "another model", "some model"},
|
||||
)
|
||||
class OnOffMultimodel:
|
||||
pass
|
||||
|
||||
@entity_registry.strict_match(s.component, channel_names="on_off")
|
||||
@entity_registry.strict_match(s.component, cluster_handler_names="on_off")
|
||||
class OnOff:
|
||||
pass
|
||||
|
||||
@entity_registry.strict_match(
|
||||
s.component, channel_names="on_off", manufacturers=MANUFACTURER
|
||||
s.component, cluster_handler_names="on_off", manufacturers=MANUFACTURER
|
||||
)
|
||||
class OnOffManufacturer:
|
||||
pass
|
||||
|
||||
@entity_registry.strict_match(s.component, channel_names="on_off", models=MODEL)
|
||||
@entity_registry.strict_match(
|
||||
s.component, cluster_handler_names="on_off", models=MODEL
|
||||
)
|
||||
class OnOffModel:
|
||||
pass
|
||||
|
||||
@entity_registry.strict_match(
|
||||
s.component, channel_names="on_off", models=MODEL, manufacturers=MANUFACTURER
|
||||
s.component,
|
||||
cluster_handler_names="on_off",
|
||||
models=MODEL,
|
||||
manufacturers=MANUFACTURER,
|
||||
)
|
||||
class OnOffModelManufacturer:
|
||||
pass
|
||||
|
||||
@entity_registry.strict_match(
|
||||
s.component, channel_names="on_off", quirk_classes=QUIRK_CLASS
|
||||
s.component, cluster_handler_names="on_off", quirk_classes=QUIRK_CLASS
|
||||
)
|
||||
class OnOffQuirk:
|
||||
pass
|
||||
|
||||
ch_on_off = channel("on_off", 6)
|
||||
ch_level = channel("level", 8)
|
||||
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_class
|
||||
@ -374,25 +455,27 @@ def test_weighted_match(
|
||||
assert claimed == [ch_on_off]
|
||||
|
||||
|
||||
def test_multi_sensor_match(channel, entity_registry: er.EntityRegistry) -> None:
|
||||
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,
|
||||
channel_names="smartenergy_metering",
|
||||
cluster_handler_names="smartenergy_metering",
|
||||
)
|
||||
class SmartEnergySensor2:
|
||||
pass
|
||||
|
||||
ch_se = channel("smartenergy_metering", 0x0702)
|
||||
ch_illuminati = channel("illuminance", 0x0401)
|
||||
ch_se = cluster_handler("smartenergy_metering", 0x0702)
|
||||
ch_illuminati = cluster_handler("illuminance", 0x0401)
|
||||
|
||||
match, claimed = entity_registry.get_multi_entity(
|
||||
"manufacturer",
|
||||
"model",
|
||||
channels=[ch_se, ch_illuminati],
|
||||
cluster_handlers=[ch_se, ch_illuminati],
|
||||
quirk_class="quirk_class",
|
||||
)
|
||||
|
||||
@ -404,15 +487,17 @@ def test_multi_sensor_match(channel, entity_registry: er.EntityRegistry) -> None
|
||||
}
|
||||
|
||||
@entity_registry.multipass_match(
|
||||
s.component, channel_names="smartenergy_metering", aux_channels="illuminance"
|
||||
s.component,
|
||||
cluster_handler_names="smartenergy_metering",
|
||||
aux_cluster_handlers="illuminance",
|
||||
)
|
||||
class SmartEnergySensor1:
|
||||
pass
|
||||
|
||||
@entity_registry.multipass_match(
|
||||
s.binary_sensor,
|
||||
channel_names="smartenergy_metering",
|
||||
aux_channels="illuminance",
|
||||
cluster_handler_names="smartenergy_metering",
|
||||
aux_cluster_handlers="illuminance",
|
||||
)
|
||||
class SmartEnergySensor3:
|
||||
pass
|
||||
@ -420,7 +505,7 @@ def test_multi_sensor_match(channel, entity_registry: er.EntityRegistry) -> None
|
||||
match, claimed = entity_registry.get_multi_entity(
|
||||
"manufacturer",
|
||||
"model",
|
||||
channels={ch_se, ch_illuminati},
|
||||
cluster_handlers={ch_se, ch_illuminati},
|
||||
quirk_class="quirk_class",
|
||||
)
|
||||
|
||||
|
@ -10,7 +10,7 @@ import zigpy.zcl.clusters.measurement as measurement
|
||||
import zigpy.zcl.clusters.smartenergy as smartenergy
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.components.zha.core.const import ZHA_CHANNEL_READS_PER_REQ
|
||||
from homeassistant.components.zha.core.const import ZHA_CLUSTER_HANDLER_READS_PER_REQ
|
||||
import homeassistant.config as config_util
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_CLASS,
|
||||
@ -616,30 +616,30 @@ async def test_electrical_measurement_init(
|
||||
await send_attributes_report(hass, cluster, {0: 1, 1291: 100, 10: 1000})
|
||||
assert int(hass.states.get(entity_id).state) == 100
|
||||
|
||||
channel = zha_device.channels.pools[0].all_channels["1:0x0b04"]
|
||||
assert channel.ac_power_divisor == 1
|
||||
assert channel.ac_power_multiplier == 1
|
||||
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 channel.ac_power_divisor == 5
|
||||
assert channel.ac_power_multiplier == 1
|
||||
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 channel.ac_power_divisor == 10
|
||||
assert channel.ac_power_multiplier == 1
|
||||
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 channel.ac_power_divisor == 10
|
||||
assert channel.ac_power_multiplier == 6
|
||||
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 channel.ac_power_divisor == 10
|
||||
assert channel.ac_power_multiplier == 20
|
||||
assert cluster_handler.ac_power_divisor == 10
|
||||
assert cluster_handler.ac_power_multiplier == 20
|
||||
assert hass.states.get(entity_id).state == "60.0"
|
||||
|
||||
|
||||
@ -972,7 +972,7 @@ async def test_elec_measurement_skip_unsupported_attribute(
|
||||
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_CHANNEL_READS_PER_REQ
|
||||
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]
|
||||
|
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user