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:
David F. Mulcahey 2023-04-19 10:47:07 -04:00 committed by GitHub
parent 090f59aaa2
commit 9c784ac622
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 3230 additions and 3009 deletions

View File

@ -1509,7 +1509,7 @@ omit =
homeassistant/components/zeversolar/entity.py homeassistant/components/zeversolar/entity.py
homeassistant/components/zeversolar/sensor.py homeassistant/components/zeversolar/sensor.py
homeassistant/components/zha/websocket_api.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/device.py
homeassistant/components/zha/core/gateway.py homeassistant/components/zha/core/gateway.py
homeassistant/components/zha/core/helpers.py homeassistant/components/zha/core/helpers.py

View File

@ -25,13 +25,13 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .core import discovery from .core import discovery
from .core.channels.security import ( from .core.cluster_handlers.security import (
SIGNAL_ALARM_TRIGGERED, SIGNAL_ALARM_TRIGGERED,
SIGNAL_ARMED_STATE_CHANGED, SIGNAL_ARMED_STATE_CHANGED,
IasAce as AceChannel, IasAce as AceClusterHandler,
) )
from .core.const import ( from .core.const import (
CHANNEL_IAS_ACE, CLUSTER_HANDLER_IAS_ACE,
CONF_ALARM_ARM_REQUIRES_CODE, CONF_ALARM_ARM_REQUIRES_CODE,
CONF_ALARM_FAILED_TRIES, CONF_ALARM_FAILED_TRIES,
CONF_ALARM_MASTER_CODE, CONF_ALARM_MASTER_CODE,
@ -77,7 +77,7 @@ async def async_setup_entry(
config_entry.async_on_unload(unsub) 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): class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity):
"""Entity for ZHA alarm control devices.""" """Entity for ZHA alarm control devices."""
@ -89,18 +89,20 @@ class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity):
| AlarmControlPanelEntityFeature.TRIGGER | 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.""" """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 cfg_entry = zha_device.gateway.config_entry
self._channel: AceChannel = channels[0] self._cluster_handler: AceClusterHandler = cluster_handlers[0]
self._channel.panel_code = async_get_zha_config_value( self._cluster_handler.panel_code = async_get_zha_config_value(
cfg_entry, ZHA_ALARM_OPTIONS, CONF_ALARM_MASTER_CODE, "1234" 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 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 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.""" """Run when about to be added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
self.async_accept_signal( 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.async_accept_signal(
self._channel, SIGNAL_ALARM_TRIGGERED, self.async_alarm_trigger self._cluster_handler, SIGNAL_ALARM_TRIGGERED, self.async_alarm_trigger
) )
@callback @callback
@ -122,26 +124,26 @@ class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity):
@property @property
def code_arm_required(self) -> bool: def code_arm_required(self) -> bool:
"""Whether the code is required for arm actions.""" """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: async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command.""" """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() self.async_write_ha_state()
async def async_alarm_arm_home(self, code: str | None = None) -> None: async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command.""" """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() self.async_write_ha_state()
async def async_alarm_arm_away(self, code: str | None = None) -> None: async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command.""" """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() self.async_write_ha_state()
async def async_alarm_arm_night(self, code: str | None = None) -> None: async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command.""" """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() self.async_write_ha_state()
async def async_alarm_trigger(self, code: str | None = None) -> None: async def async_alarm_trigger(self, code: str | None = None) -> None:
@ -151,4 +153,4 @@ class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity):
@property @property
def state(self) -> str | None: def state(self) -> str | None:
"""Return the state of the entity.""" """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)

View File

@ -20,11 +20,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .core import discovery from .core import discovery
from .core.const import ( from .core.const import (
CHANNEL_ACCELEROMETER, CLUSTER_HANDLER_ACCELEROMETER,
CHANNEL_BINARY_INPUT, CLUSTER_HANDLER_BINARY_INPUT,
CHANNEL_OCCUPANCY, CLUSTER_HANDLER_OCCUPANCY,
CHANNEL_ON_OFF, CLUSTER_HANDLER_ON_OFF,
CHANNEL_ZONE, CLUSTER_HANDLER_ZONE,
DATA_ZHA, DATA_ZHA,
SIGNAL_ADD_ENTITIES, SIGNAL_ADD_ENTITIES,
SIGNAL_ATTR_UPDATED, SIGNAL_ATTR_UPDATED,
@ -72,22 +72,22 @@ class BinarySensor(ZhaEntity, BinarySensorEntity):
SENSOR_ATTR: str | None = None 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.""" """Initialize the ZHA binary sensor."""
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
self._channel = channels[0] self._cluster_handler = cluster_handlers[0]
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Run when about to be added to hass.""" """Run when about to be added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
self.async_accept_signal( self.async_accept_signal(
self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state self._cluster_handler, SIGNAL_ATTR_UPDATED, self.async_set_state
) )
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return True if the switch is on based on the state machine.""" """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: if raw_state is None:
return False return False
return self.parse(raw_state) return self.parse(raw_state)
@ -103,7 +103,7 @@ class BinarySensor(ZhaEntity, BinarySensorEntity):
return bool(value) return bool(value)
@MULTI_MATCH(channel_names=CHANNEL_ACCELEROMETER) @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ACCELEROMETER)
class Accelerometer(BinarySensor): class Accelerometer(BinarySensor):
"""ZHA BinarySensor.""" """ZHA BinarySensor."""
@ -111,7 +111,7 @@ class Accelerometer(BinarySensor):
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.MOVING _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.MOVING
@MULTI_MATCH(channel_names=CHANNEL_OCCUPANCY) @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_OCCUPANCY)
class Occupancy(BinarySensor): class Occupancy(BinarySensor):
"""ZHA BinarySensor.""" """ZHA BinarySensor."""
@ -119,7 +119,7 @@ class Occupancy(BinarySensor):
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.OCCUPANCY _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): class Opening(BinarySensor):
"""ZHA OnOff BinarySensor.""" """ZHA OnOff BinarySensor."""
@ -131,13 +131,13 @@ class Opening(BinarySensor):
@callback @callback
def async_restore_last_state(self, last_state): def async_restore_last_state(self, last_state):
"""Restore previous state to zigpy cache.""" """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, OnOff.attributes_by_name[self.SENSOR_ATTR].id,
t.Bool.true if last_state.state == STATE_ON else t.Bool.false, 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): class BinaryInput(BinarySensor):
"""ZHA BinarySensor.""" """ZHA BinarySensor."""
@ -145,14 +145,14 @@ class BinaryInput(BinarySensor):
@STRICT_MATCH( @STRICT_MATCH(
channel_names=CHANNEL_ON_OFF, cluster_handler_names=CLUSTER_HANDLER_ON_OFF,
manufacturers="IKEA of Sweden", manufacturers="IKEA of Sweden",
models=lambda model: isinstance(model, str) models=lambda model: isinstance(model, str)
and model is not None and model is not None
and model.find("motion") != -1, and model.find("motion") != -1,
) )
@STRICT_MATCH( @STRICT_MATCH(
channel_names=CHANNEL_ON_OFF, cluster_handler_names=CLUSTER_HANDLER_ON_OFF,
manufacturers="Philips", manufacturers="Philips",
models={"SML001", "SML002"}, models={"SML001", "SML002"},
) )
@ -162,7 +162,7 @@ class Motion(Opening):
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.MOTION _attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.MOTION
@MULTI_MATCH(channel_names=CHANNEL_ZONE) @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ZONE)
class IASZone(BinarySensor): class IASZone(BinarySensor):
"""ZHA IAS BinarySensor.""" """ZHA IAS BinarySensor."""
@ -171,7 +171,7 @@ class IASZone(BinarySensor):
@property @property
def device_class(self) -> BinarySensorDeviceClass | None: def device_class(self) -> BinarySensorDeviceClass | None:
"""Return device class from component DEVICE_CLASSES.""" """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 @staticmethod
def parse(value: bool | int) -> bool: def parse(value: bool | int) -> bool:
@ -204,13 +204,13 @@ class IASZone(BinarySensor):
else: else:
migrated_state = IasZone.ZoneStatus(0) 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 IasZone.attributes_by_name[self.SENSOR_ATTR].id, migrated_state
) )
@MULTI_MATCH( @MULTI_MATCH(
channel_names="tuya_manufacturer", cluster_handler_names="tuya_manufacturer",
manufacturers={ manufacturers={
"_TZE200_htnnfasr", "_TZE200_htnnfasr",
}, },
@ -222,7 +222,7 @@ class FrostLock(BinarySensor, id_suffix="frost_lock"):
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.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"): class ReplaceFilter(BinarySensor, id_suffix="replace_filter"):
"""ZHA BinarySensor.""" """ZHA BinarySensor."""
@ -230,7 +230,7 @@ class ReplaceFilter(BinarySensor, id_suffix="replace_filter"):
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.PROBLEM _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"): class AqaraPetFeederErrorDetected(BinarySensor, id_suffix="error_detected"):
"""ZHA aqara pet feeder error detected binary sensor.""" """ZHA aqara pet feeder error detected binary sensor."""
@ -240,7 +240,8 @@ class AqaraPetFeederErrorDetected(BinarySensor, id_suffix="error_detected"):
@MULTI_MATCH( @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"): class XiaomiPlugConsumerConnected(BinarySensor, id_suffix="consumer_connected"):
"""ZHA Xiaomi plug consumer connected binary sensor.""" """ZHA Xiaomi plug consumer connected binary sensor."""
@ -250,7 +251,7 @@ class XiaomiPlugConsumerConnected(BinarySensor, id_suffix="consumer_connected"):
_attr_device_class: BinarySensorDeviceClass = BinarySensorDeviceClass.PLUG _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"): class AqaraThermostatWindowOpen(BinarySensor, id_suffix="window_open"):
"""ZHA Aqara thermostat window open binary sensor.""" """ZHA Aqara thermostat window open binary sensor."""
@ -259,7 +260,7 @@ class AqaraThermostatWindowOpen(BinarySensor, id_suffix="window_open"):
_attr_name: str = "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"): class AqaraThermostatValveAlarm(BinarySensor, id_suffix="valve_alarm"):
"""ZHA Aqara thermostat valve alarm binary sensor.""" """ZHA Aqara thermostat valve alarm binary sensor."""
@ -268,7 +269,9 @@ class AqaraThermostatValveAlarm(BinarySensor, id_suffix="valve_alarm"):
_attr_name: str = "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"): class AqaraThermostatCalibrated(BinarySensor, id_suffix="calibrated"):
"""ZHA Aqara thermostat calibrated binary sensor.""" """ZHA Aqara thermostat calibrated binary sensor."""
@ -277,7 +280,9 @@ class AqaraThermostatCalibrated(BinarySensor, id_suffix="calibrated"):
_attr_name: str = "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"): class AqaraThermostatExternalSensor(BinarySensor, id_suffix="sensor"):
"""ZHA Aqara thermostat external sensor binary sensor.""" """ZHA Aqara thermostat external sensor binary sensor."""
@ -286,7 +291,7 @@ class AqaraThermostatExternalSensor(BinarySensor, id_suffix="sensor"):
_attr_name: str = "External 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"): class AqaraLinkageAlarmState(BinarySensor, id_suffix="linkage_alarm_state"):
"""ZHA Aqara linkage alarm state binary sensor.""" """ZHA Aqara linkage alarm state binary sensor."""

View File

@ -18,12 +18,12 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .core import discovery 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 .core.registries import ZHA_ENTITIES
from .entity import ZhaEntity from .entity import ZhaEntity
if TYPE_CHECKING: if TYPE_CHECKING:
from .core.channels.base import ZigbeeChannel from .core.cluster_handlers import ClusterHandler
from .core.device import ZHADevice from .core.device import ZHADevice
@ -65,12 +65,12 @@ class ZHAButton(ZhaEntity, ButtonEntity):
self, self,
unique_id: str, unique_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
"""Init this button.""" """Init this button."""
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
self._channel: ZigbeeChannel = channels[0] self._cluster_handler: ClusterHandler = cluster_handlers[0]
@abc.abstractmethod @abc.abstractmethod
def get_args(self) -> list[Any]: def get_args(self) -> list[Any]:
@ -78,12 +78,12 @@ class ZHAButton(ZhaEntity, ButtonEntity):
async def async_press(self) -> None: async def async_press(self) -> None:
"""Send out a update command.""" """Send out a update command."""
command = getattr(self._channel, self._command_name) command = getattr(self._cluster_handler, self._command_name)
arguments = self.get_args() arguments = self.get_args()
await command(*arguments) await command(*arguments)
@MULTI_MATCH(channel_names=CHANNEL_IDENTIFY) @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_IDENTIFY)
class ZHAIdentifyButton(ZHAButton): class ZHAIdentifyButton(ZHAButton):
"""Defines a ZHA identify button.""" """Defines a ZHA identify button."""
@ -92,7 +92,7 @@ class ZHAIdentifyButton(ZHAButton):
cls, cls,
unique_id: str, unique_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
**kwargs: Any, **kwargs: Any,
) -> Self | None: ) -> Self | None:
"""Entity Factory. """Entity Factory.
@ -100,10 +100,10 @@ class ZHAIdentifyButton(ZHAButton):
Return entity if it is a supported configuration, otherwise return None Return entity if it is a supported configuration, otherwise return None
""" """
if ZHA_ENTITIES.prevent_entity_creation( if ZHA_ENTITIES.prevent_entity_creation(
Platform.BUTTON, zha_device.ieee, CHANNEL_IDENTIFY Platform.BUTTON, zha_device.ieee, CLUSTER_HANDLER_IDENTIFY
): ):
return None 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_device_class: ButtonDeviceClass = ButtonDeviceClass.UPDATE
_attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_category = EntityCategory.DIAGNOSTIC
@ -126,17 +126,17 @@ class ZHAAttributeButton(ZhaEntity, ButtonEntity):
self, self,
unique_id: str, unique_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
"""Init this button.""" """Init this button."""
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
self._channel: ZigbeeChannel = channels[0] self._cluster_handler: ClusterHandler = cluster_handlers[0]
async def async_press(self) -> None: async def async_press(self) -> None:
"""Write attribute with defined value.""" """Write attribute with defined value."""
try: try:
result = await self._channel.cluster.write_attributes( result = await self._cluster_handler.cluster.write_attributes(
{self._attribute_name: self._attribute_value} {self._attribute_name: self._attribute_value}
) )
except zigpy.exceptions.ZigbeeException as ex: except zigpy.exceptions.ZigbeeException as ex:
@ -149,7 +149,7 @@ class ZHAAttributeButton(ZhaEntity, ButtonEntity):
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names="tuya_manufacturer", cluster_handler_names="tuya_manufacturer",
manufacturers={ manufacturers={
"_TZE200_htnnfasr", "_TZE200_htnnfasr",
}, },
@ -164,7 +164,9 @@ class FrostLockResetButton(ZHAAttributeButton, id_suffix="reset_frost_lock"):
_attr_entity_category = EntityCategory.CONFIG _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( class NoPresenceStatusResetButton(
ZHAAttributeButton, id_suffix="reset_no_presence_status" ZHAAttributeButton, id_suffix="reset_no_presence_status"
): ):
@ -177,7 +179,7 @@ class NoPresenceStatusResetButton(
_attr_entity_category = EntityCategory.CONFIG _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"): class AqaraPetFeederFeedButton(ZHAAttributeButton, id_suffix="feeding"):
"""Defines a feed button for the aqara c1 pet feeder.""" """Defines a feed button for the aqara c1 pet feeder."""
@ -187,7 +189,7 @@ class AqaraPetFeederFeedButton(ZHAAttributeButton, id_suffix="feeding"):
@CONFIG_DIAGNOSTIC_MATCH( @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"): class AqaraSelfTestButton(ZHAAttributeButton, id_suffix="self_test"):
"""Defines a ZHA self-test button for Aqara smoke sensors.""" """Defines a ZHA self-test button for Aqara smoke sensors."""

View File

@ -43,8 +43,8 @@ import homeassistant.util.dt as dt_util
from .core import discovery from .core import discovery
from .core.const import ( from .core.const import (
CHANNEL_FAN, CLUSTER_HANDLER_FAN,
CHANNEL_THERMOSTAT, CLUSTER_HANDLER_THERMOSTAT,
DATA_ZHA, DATA_ZHA,
PRESET_COMPLEX, PRESET_COMPLEX,
PRESET_SCHEDULE, PRESET_SCHEDULE,
@ -127,9 +127,9 @@ async def async_setup_entry(
@MULTI_MATCH( @MULTI_MATCH(
channel_names=CHANNEL_THERMOSTAT, cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
aux_channels=CHANNEL_FAN, aux_cluster_handlers=CLUSTER_HANDLER_FAN,
stop_on_match_group=CHANNEL_THERMOSTAT, stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT,
) )
class Thermostat(ZhaEntity, ClimateEntity): class Thermostat(ZhaEntity, ClimateEntity):
"""Representation of a ZHA Thermostat device.""" """Representation of a ZHA Thermostat device."""
@ -140,14 +140,14 @@ class Thermostat(ZhaEntity, ClimateEntity):
_attr_precision = PRECISION_TENTHS _attr_precision = PRECISION_TENTHS
_attr_temperature_unit = UnitOfTemperature.CELSIUS _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.""" """Initialize ZHA Thermostat instance."""
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
self._thrm = self.cluster_channels.get(CHANNEL_THERMOSTAT) self._thrm = self.cluster_handlers.get(CLUSTER_HANDLER_THERMOSTAT)
self._preset = PRESET_NONE self._preset = PRESET_NONE
self._presets = [] self._presets = []
self._supported_flags = ClimateEntityFeature.TARGET_TEMPERATURE self._supported_flags = ClimateEntityFeature.TARGET_TEMPERATURE
self._fan = self.cluster_channels.get(CHANNEL_FAN) self._fan = self.cluster_handlers.get(CLUSTER_HANDLER_FAN)
@property @property
def current_temperature(self): def current_temperature(self):
@ -480,9 +480,9 @@ class Thermostat(ZhaEntity, ClimateEntity):
@MULTI_MATCH( @MULTI_MATCH(
channel_names={CHANNEL_THERMOSTAT, "sinope_manufacturer_specific"}, cluster_handler_names={CLUSTER_HANDLER_THERMOSTAT, "sinope_manufacturer_specific"},
manufacturers="Sinope Technologies", manufacturers="Sinope Technologies",
stop_on_match_group=CHANNEL_THERMOSTAT, stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT,
) )
class SinopeTechnologiesThermostat(Thermostat): class SinopeTechnologiesThermostat(Thermostat):
"""Sinope Technologies Thermostat.""" """Sinope Technologies Thermostat."""
@ -490,12 +490,12 @@ class SinopeTechnologiesThermostat(Thermostat):
manufacturer = 0x119C manufacturer = 0x119C
update_time_interval = timedelta(minutes=randint(45, 75)) 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.""" """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._presets = [PRESET_AWAY, PRESET_NONE]
self._supported_flags |= ClimateEntityFeature.PRESET_MODE 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 @property
def _rm_rs_action(self) -> HVACAction: def _rm_rs_action(self) -> HVACAction:
@ -553,28 +553,28 @@ class SinopeTechnologiesThermostat(Thermostat):
@MULTI_MATCH( @MULTI_MATCH(
channel_names=CHANNEL_THERMOSTAT, cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
aux_channels=CHANNEL_FAN, aux_cluster_handlers=CLUSTER_HANDLER_FAN,
manufacturers={"Zen Within", "LUX"}, manufacturers={"Zen Within", "LUX"},
stop_on_match_group=CHANNEL_THERMOSTAT, stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT,
) )
class ZenWithinThermostat(Thermostat): class ZenWithinThermostat(Thermostat):
"""Zen Within Thermostat implementation.""" """Zen Within Thermostat implementation."""
@MULTI_MATCH( @MULTI_MATCH(
channel_names=CHANNEL_THERMOSTAT, cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
aux_channels=CHANNEL_FAN, aux_cluster_handlers=CLUSTER_HANDLER_FAN,
manufacturers="Centralite", manufacturers="Centralite",
models={"3157100", "3157100-E"}, models={"3157100", "3157100-E"},
stop_on_match_group=CHANNEL_THERMOSTAT, stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT,
) )
class CentralitePearl(ZenWithinThermostat): class CentralitePearl(ZenWithinThermostat):
"""Centralite Pearl Thermostat implementation.""" """Centralite Pearl Thermostat implementation."""
@STRICT_MATCH( @STRICT_MATCH(
channel_names=CHANNEL_THERMOSTAT, cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
manufacturers={ manufacturers={
"_TZE200_ckud7u2l", "_TZE200_ckud7u2l",
"_TZE200_ywdxldoj", "_TZE200_ywdxldoj",
@ -594,9 +594,9 @@ class CentralitePearl(ZenWithinThermostat):
class MoesThermostat(Thermostat): class MoesThermostat(Thermostat):
"""Moes Thermostat implementation.""" """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.""" """Initialize ZHA Thermostat instance."""
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
self._presets = [ self._presets = [
PRESET_NONE, PRESET_NONE,
PRESET_AWAY, PRESET_AWAY,
@ -668,7 +668,7 @@ class MoesThermostat(Thermostat):
@STRICT_MATCH( @STRICT_MATCH(
channel_names=CHANNEL_THERMOSTAT, cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
manufacturers={ manufacturers={
"_TZE200_b6wax7g0", "_TZE200_b6wax7g0",
}, },
@ -676,9 +676,9 @@ class MoesThermostat(Thermostat):
class BecaThermostat(Thermostat): class BecaThermostat(Thermostat):
"""Beca Thermostat implementation.""" """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.""" """Initialize ZHA Thermostat instance."""
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
self._presets = [ self._presets = [
PRESET_NONE, PRESET_NONE,
PRESET_AWAY, PRESET_AWAY,
@ -743,10 +743,10 @@ class BecaThermostat(Thermostat):
@MULTI_MATCH( @MULTI_MATCH(
channel_names=CHANNEL_THERMOSTAT, cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
manufacturers="Stelpro", manufacturers="Stelpro",
models={"SORB"}, models={"SORB"},
stop_on_match_group=CHANNEL_THERMOSTAT, stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT,
) )
class StelproFanHeater(Thermostat): class StelproFanHeater(Thermostat):
"""Stelpro Fan Heater implementation.""" """Stelpro Fan Heater implementation."""
@ -758,7 +758,7 @@ class StelproFanHeater(Thermostat):
@STRICT_MATCH( @STRICT_MATCH(
channel_names=CHANNEL_THERMOSTAT, cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
manufacturers={ manufacturers={
"_TZE200_7yoranx2", "_TZE200_7yoranx2",
"_TZE200_e9ba97vf", # TV01-ZG "_TZE200_e9ba97vf", # TV01-ZG
@ -780,9 +780,9 @@ class ZONNSMARTThermostat(Thermostat):
PRESET_HOLIDAY = "holiday" PRESET_HOLIDAY = "holiday"
PRESET_FROST = "frost protect" 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.""" """Initialize ZHA Thermostat instance."""
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
self._presets = [ self._presets = [
PRESET_NONE, PRESET_NONE,
self.PRESET_HOLIDAY, self.PRESET_HOLIDAY,

View File

@ -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,
}
)

View File

@ -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",
)

View File

@ -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."""

View File

@ -1,4 +1,4 @@
"""Base classes for channels.""" """Cluster handlers module for Zigbee Home Automation."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
@ -29,19 +29,19 @@ from ..const import (
ATTR_TYPE, ATTR_TYPE,
ATTR_UNIQUE_ID, ATTR_UNIQUE_ID,
ATTR_VALUE, ATTR_VALUE,
CHANNEL_ZDO, CLUSTER_HANDLER_ZDO,
REPORT_CONFIG_ATTR_PER_REQ, REPORT_CONFIG_ATTR_PER_REQ,
SIGNAL_ATTR_UPDATED, SIGNAL_ATTR_UPDATED,
ZHA_CHANNEL_MSG, ZHA_CLUSTER_HANDLER_MSG,
ZHA_CHANNEL_MSG_BIND, ZHA_CLUSTER_HANDLER_MSG_BIND,
ZHA_CHANNEL_MSG_CFG_RPT, ZHA_CLUSTER_HANDLER_MSG_CFG_RPT,
ZHA_CHANNEL_MSG_DATA, ZHA_CLUSTER_HANDLER_MSG_DATA,
ZHA_CHANNEL_READS_PER_REQ, ZHA_CLUSTER_HANDLER_READS_PER_REQ,
) )
from ..helpers import LogMixin, retryable_req, safe_read from ..helpers import LogMixin, retryable_req, safe_read
if TYPE_CHECKING: if TYPE_CHECKING:
from . import ChannelPool from ..endpoint import Endpoint
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -56,31 +56,31 @@ class AttrReportConfig(TypedDict, total=True):
config: tuple[int, int, int | float] 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.""" """Parse and log a zigbee cluster command."""
try: try:
name = channel.cluster.server_commands[command_id].name name = cluster_handler.cluster.server_commands[command_id].name
except KeyError: except KeyError:
name = f"0x{command_id:02X}" name = f"0x{command_id:02X}"
channel.debug( cluster_handler.debug(
"received '%s' command with %s args on cluster_id '%s' tsn '%s'", "received '%s' command with %s args on cluster_id '%s' tsn '%s'",
name, name,
args, args,
channel.cluster.cluster_id, cluster_handler.cluster.cluster_id,
tsn, tsn,
) )
return name return name
def decorate_command(channel, command): def decorate_command(cluster_handler, command):
"""Wrap a cluster command to make it safe.""" """Wrap a cluster command to make it safe."""
@wraps(command) @wraps(command)
async def wrapper(*args, **kwds): async def wrapper(*args, **kwds):
try: try:
result = await command(*args, **kwds) result = await command(*args, **kwds)
channel.debug( cluster_handler.debug(
"executed '%s' command with args: '%s' kwargs: '%s' result: %s", "executed '%s' command with args: '%s' kwargs: '%s' result: %s",
command.__name__, command.__name__,
args, args,
@ -90,7 +90,7 @@ def decorate_command(channel, command):
return result return result
except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex: except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex:
channel.debug( cluster_handler.debug(
"command failed: '%s' args: '%s' kwargs '%s' exception: '%s'", "command failed: '%s' args: '%s' kwargs '%s' exception: '%s'",
command.__name__, command.__name__,
args, args,
@ -102,32 +102,32 @@ def decorate_command(channel, command):
return wrapper return wrapper
class ChannelStatus(Enum): class ClusterHandlerStatus(Enum):
"""Status of a channel.""" """Status of a cluster handler."""
CREATED = 1 CREATED = 1
CONFIGURED = 2 CONFIGURED = 2
INITIALIZED = 3 INITIALIZED = 3
class ZigbeeChannel(LogMixin): class ClusterHandler(LogMixin):
"""Base channel for a Zigbee cluster.""" """Base cluster handler for a Zigbee cluster."""
REPORT_CONFIG: tuple[AttrReportConfig, ...] = () REPORT_CONFIG: tuple[AttrReportConfig, ...] = ()
BIND: bool = True 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 # Dict keys -- attribute ID or names, with bool value indicating whether a cached
# attribute read is acceptable. # attribute read is acceptable.
ZCL_INIT_ATTRS: dict[int | str, bool] = {} ZCL_INIT_ATTRS: dict[int | str, bool] = {}
def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None:
"""Initialize ZigbeeChannel.""" """Initialize ClusterHandler."""
self._generic_id = f"channel_0x{cluster.cluster_id:04x}" self._generic_id = f"cluster_handler_0x{cluster.cluster_id:04x}"
self._ch_pool = ch_pool self._endpoint: Endpoint = endpoint
self._cluster = cluster self._cluster = cluster
self._id = f"{ch_pool.id}:0x{cluster.cluster_id:04x}" self._id = f"{endpoint.id}:0x{cluster.cluster_id:04x}"
unique_id = ch_pool.unique_id.replace("-", ":") unique_id = endpoint.unique_id.replace("-", ":")
self._unique_id = f"{unique_id}:0x{cluster.cluster_id:04x}" self._unique_id = f"{unique_id}:0x{cluster.cluster_id:04x}"
if not hasattr(self, "_value_attribute") and self.REPORT_CONFIG: if not hasattr(self, "_value_attribute") and self.REPORT_CONFIG:
attr_def: ZCLAttributeDef | None = self.cluster.attributes_by_name.get( attr_def: ZCLAttributeDef | None = self.cluster.attributes_by_name.get(
@ -137,28 +137,28 @@ class ZigbeeChannel(LogMixin):
self.value_attribute = attr_def.id self.value_attribute = attr_def.id
else: else:
self.value_attribute = None self.value_attribute = None
self._status = ChannelStatus.CREATED self._status = ClusterHandlerStatus.CREATED
self._cluster.add_listener(self) self._cluster.add_listener(self)
self.data_cache: dict[str, Enum] = {} self.data_cache: dict[str, Enum] = {}
@property @property
def id(self) -> str: def id(self) -> str:
"""Return channel id unique for this device only.""" """Return cluster handler id unique for this device only."""
return self._id return self._id
@property @property
def generic_id(self): def generic_id(self):
"""Return the generic id for this channel.""" """Return the generic id for this cluster handler."""
return self._generic_id return self._generic_id
@property @property
def unique_id(self): def unique_id(self):
"""Return the unique id for this channel.""" """Return the unique id for this cluster handler."""
return self._unique_id return self._unique_id
@property @property
def cluster(self): def cluster(self):
"""Return the zigpy cluster for this channel.""" """Return the zigpy cluster for this cluster handler."""
return self._cluster return self._cluster
@property @property
@ -168,7 +168,7 @@ class ZigbeeChannel(LogMixin):
@property @property
def status(self): def status(self):
"""Return the status of the channel.""" """Return the status of the cluster handler."""
return self._status return self._status
def __hash__(self) -> int: def __hash__(self) -> int:
@ -178,7 +178,7 @@ class ZigbeeChannel(LogMixin):
@callback @callback
def async_send_signal(self, signal: str, *args: Any) -> None: def async_send_signal(self, signal: str, *args: Any) -> None:
"""Send a signal through hass dispatcher.""" """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): async def bind(self):
"""Bind a zigbee cluster. """Bind a zigbee cluster.
@ -190,11 +190,11 @@ class ZigbeeChannel(LogMixin):
res = await self.cluster.bind() res = await self.cluster.bind()
self.debug("bound '%s' cluster: %s", self.cluster.ep_attribute, res[0]) self.debug("bound '%s' cluster: %s", self.cluster.ep_attribute, res[0])
async_dispatcher_send( async_dispatcher_send(
self._ch_pool.hass, self._endpoint.device.hass,
ZHA_CHANNEL_MSG, ZHA_CLUSTER_HANDLER_MSG,
{ {
ATTR_TYPE: ZHA_CHANNEL_MSG_BIND, ATTR_TYPE: ZHA_CLUSTER_HANDLER_MSG_BIND,
ZHA_CHANNEL_MSG_DATA: { ZHA_CLUSTER_HANDLER_MSG_DATA: {
"cluster_name": self.cluster.name, "cluster_name": self.cluster.name,
"cluster_id": self.cluster.cluster_id, "cluster_id": self.cluster.cluster_id,
"success": res[0] == 0, "success": res[0] == 0,
@ -206,11 +206,11 @@ class ZigbeeChannel(LogMixin):
"Failed to bind '%s' cluster: %s", self.cluster.ep_attribute, str(ex) "Failed to bind '%s' cluster: %s", self.cluster.ep_attribute, str(ex)
) )
async_dispatcher_send( async_dispatcher_send(
self._ch_pool.hass, self._endpoint.device.hass,
ZHA_CHANNEL_MSG, ZHA_CLUSTER_HANDLER_MSG,
{ {
ATTR_TYPE: ZHA_CHANNEL_MSG_BIND, ATTR_TYPE: ZHA_CLUSTER_HANDLER_MSG_BIND,
ZHA_CHANNEL_MSG_DATA: { ZHA_CLUSTER_HANDLER_MSG_DATA: {
"cluster_name": self.cluster.name, "cluster_name": self.cluster.name,
"cluster_id": self.cluster.cluster_id, "cluster_id": self.cluster.cluster_id,
"success": False, "success": False,
@ -226,8 +226,11 @@ class ZigbeeChannel(LogMixin):
""" """
event_data = {} event_data = {}
kwargs = {} kwargs = {}
if self.cluster.cluster_id >= 0xFC00 and self._ch_pool.manufacturer_code: if (
kwargs["manufacturer"] = self._ch_pool.manufacturer_code 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: for attr_report in self.REPORT_CONFIG:
attr, config = attr_report["attr"], attr_report["config"] attr, config = attr_report["attr"], attr_report["config"]
@ -272,11 +275,11 @@ class ZigbeeChannel(LogMixin):
) )
async_dispatcher_send( async_dispatcher_send(
self._ch_pool.hass, self._endpoint.device.hass,
ZHA_CHANNEL_MSG, ZHA_CLUSTER_HANDLER_MSG,
{ {
ATTR_TYPE: ZHA_CHANNEL_MSG_CFG_RPT, ATTR_TYPE: ZHA_CLUSTER_HANDLER_MSG_CFG_RPT,
ZHA_CHANNEL_MSG_DATA: { ZHA_CLUSTER_HANDLER_MSG_DATA: {
"cluster_name": self.cluster.name, "cluster_name": self.cluster.name,
"cluster_id": self.cluster.cluster_id, "cluster_id": self.cluster.cluster_id,
"attributes": event_data, "attributes": event_data,
@ -311,7 +314,6 @@ class ZigbeeChannel(LogMixin):
for record in res for record in res
if record.status != Status.SUCCESS if record.status != Status.SUCCESS
] ]
self.debug( self.debug(
"Successfully configured reporting for '%s' on '%s' cluster", "Successfully configured reporting for '%s' on '%s' cluster",
set(attrs) - set(failed), set(attrs) - set(failed),
@ -326,43 +328,45 @@ class ZigbeeChannel(LogMixin):
async def async_configure(self) -> None: async def async_configure(self) -> None:
"""Set cluster binding and attribute reporting.""" """Set cluster binding and attribute reporting."""
if not self._ch_pool.skip_configuration: if not self._endpoint.device.skip_configuration:
if self.BIND: if self.BIND:
self.debug("Performing cluster binding") self.debug("Performing cluster binding")
await self.bind() await self.bind()
if self.cluster.is_server: if self.cluster.is_server:
self.debug("Configuring cluster attribute reporting") self.debug("Configuring cluster attribute reporting")
await self.configure_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: if ch_specific_cfg:
self.debug("Performing channel specific configuration") self.debug("Performing cluster handler specific configuration")
await ch_specific_cfg() await ch_specific_cfg()
self.debug("finished channel configuration") self.debug("finished cluster handler configuration")
else: else:
self.debug("skipping channel configuration") self.debug("skipping cluster handler configuration")
self._status = ChannelStatus.CONFIGURED self._status = ClusterHandlerStatus.CONFIGURED
@retryable_req(delays=(1, 1, 3)) @retryable_req(delays=(1, 1, 3))
async def async_initialize(self, from_cache: bool) -> None: async def async_initialize(self, from_cache: bool) -> None:
"""Initialize channel.""" """Initialize cluster handler."""
if not from_cache and self._ch_pool.skip_configuration: if not from_cache and self._endpoint.device.skip_configuration:
self.debug("Skipping channel initialization") self.debug("Skipping cluster handler initialization")
self._status = ChannelStatus.INITIALIZED self._status = ClusterHandlerStatus.INITIALIZED
return 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] 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 = [a for a, cached in self.ZCL_INIT_ATTRS.items() if not cached]
uncached.extend([cfg["attr"] for cfg in self.REPORT_CONFIG]) uncached.extend([cfg["attr"] for cfg in self.REPORT_CONFIG])
if cached: if cached:
self.debug("initializing cached channel attributes: %s", cached) self.debug("initializing cached cluster handler attributes: %s", cached)
await self._get_attributes( await self._get_attributes(
True, cached, from_cache=True, only_cache=from_cache True, cached, from_cache=True, only_cache=from_cache
) )
if uncached: if uncached:
self.debug( self.debug(
"initializing uncached channel attributes: %s - from cache[%s]", "initializing uncached cluster handler attributes: %s - from cache[%s]",
uncached, uncached,
from_cache, from_cache,
) )
@ -370,13 +374,17 @@ class ZigbeeChannel(LogMixin):
True, uncached, from_cache=from_cache, only_cache=from_cache 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: 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) await ch_specific_init(from_cache=from_cache)
self.debug("finished channel initialization") self.debug("finished cluster handler initialization")
self._status = ChannelStatus.INITIALIZED self._status = ClusterHandlerStatus.INITIALIZED
@callback @callback
def cluster_command(self, tsn, command_id, args): def cluster_command(self, tsn, command_id, args):
@ -411,13 +419,13 @@ class ZigbeeChannel(LogMixin):
else: else:
raise TypeError(f"Unexpected zha_send_event {command!r} argument: {arg!r}") 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_UNIQUE_ID: self.unique_id,
ATTR_CLUSTER_ID: self.cluster.cluster_id, ATTR_CLUSTER_ID: self.cluster.cluster_id,
ATTR_COMMAND: command, ATTR_COMMAND: command,
# Maintain backwards compatibility with the old zigpy response format # Maintain backwards compatibility with the old zigpy response format
ATTR_ARGS: args, ATTR_ARGS: args, # type: ignore[dict-item]
ATTR_PARAMS: params, ATTR_PARAMS: params,
} }
) )
@ -434,7 +442,7 @@ class ZigbeeChannel(LogMixin):
async def get_attribute_value(self, attribute, from_cache=True): async def get_attribute_value(self, attribute, from_cache=True):
"""Get the value for an attribute.""" """Get the value for an attribute."""
manufacturer = None 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: if self.cluster.cluster_id >= 0xFC00 and manufacturer_code:
manufacturer = manufacturer_code manufacturer = manufacturer_code
result = await safe_read( result = await safe_read(
@ -455,11 +463,11 @@ class ZigbeeChannel(LogMixin):
) -> dict[int | str, Any]: ) -> dict[int | str, Any]:
"""Get the values for a list of attributes.""" """Get the values for a list of attributes."""
manufacturer = None 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: if self.cluster.cluster_id >= 0xFC00 and manufacturer_code:
manufacturer = manufacturer_code manufacturer = manufacturer_code
chunk = attributes[:ZHA_CHANNEL_READS_PER_REQ] chunk = attributes[:ZHA_CLUSTER_HANDLER_READS_PER_REQ]
rest = attributes[ZHA_CHANNEL_READS_PER_REQ:] rest = attributes[ZHA_CLUSTER_HANDLER_READS_PER_REQ:]
result = {} result = {}
while chunk: while chunk:
try: try:
@ -480,8 +488,8 @@ class ZigbeeChannel(LogMixin):
) )
if raise_exceptions: if raise_exceptions:
raise raise
chunk = rest[:ZHA_CHANNEL_READS_PER_REQ] chunk = rest[:ZHA_CLUSTER_HANDLER_READS_PER_REQ]
rest = rest[ZHA_CHANNEL_READS_PER_REQ:] rest = rest[ZHA_CLUSTER_HANDLER_READS_PER_REQ:]
return result return result
get_attributes = partialmethod(_get_attributes, False) get_attributes = partialmethod(_get_attributes, False)
@ -489,7 +497,7 @@ class ZigbeeChannel(LogMixin):
def log(self, level, msg, *args, **kwargs): def log(self, level, msg, *args, **kwargs):
"""Log a message.""" """Log a message."""
msg = f"[%s:%s]: {msg}" 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) _LOGGER.log(level, msg, *args, **kwargs)
def __getattr__(self, name): def __getattr__(self, name):
@ -501,31 +509,31 @@ class ZigbeeChannel(LogMixin):
return self.__getattribute__(name) return self.__getattribute__(name)
class ZDOChannel(LogMixin): class ZDOClusterHandler(LogMixin):
"""Channel for ZDO events.""" """Cluster handler for ZDO events."""
def __init__(self, cluster, device): def __init__(self, device):
"""Initialize ZDOChannel.""" """Initialize ZDOClusterHandler."""
self.name = CHANNEL_ZDO self.name = CLUSTER_HANDLER_ZDO
self._cluster = cluster self._cluster = device.device.endpoints[0]
self._zha_device = device self._zha_device = device
self._status = ChannelStatus.CREATED self._status = ClusterHandlerStatus.CREATED
self._unique_id = f"{str(device.ieee)}:{device.name}_ZDO" self._unique_id = f"{str(device.ieee)}:{device.name}_ZDO"
self._cluster.add_listener(self) self._cluster.add_listener(self)
@property @property
def unique_id(self): def unique_id(self):
"""Return the unique id for this channel.""" """Return the unique id for this cluster handler."""
return self._unique_id return self._unique_id
@property @property
def cluster(self): def cluster(self):
"""Return the aigpy cluster for this channel.""" """Return the aigpy cluster for this cluster handler."""
return self._cluster return self._cluster
@property @property
def status(self): def status(self):
"""Return the status of the channel.""" """Return the status of the cluster handler."""
return self._status return self._status
@callback @callback
@ -537,12 +545,12 @@ class ZDOChannel(LogMixin):
"""Permit handler.""" """Permit handler."""
async def async_initialize(self, from_cache): async def async_initialize(self, from_cache):
"""Initialize channel.""" """Initialize cluster handler."""
self._status = ChannelStatus.INITIALIZED self._status = ClusterHandlerStatus.INITIALIZED
async def async_configure(self): async def async_configure(self):
"""Configure channel.""" """Configure cluster handler."""
self._status = ChannelStatus.CONFIGURED self._status = ClusterHandlerStatus.CONFIGURED
def log(self, level, msg, *args, **kwargs): def log(self, level, msg, *args, **kwargs):
"""Log a message.""" """Log a message."""
@ -551,8 +559,8 @@ class ZDOChannel(LogMixin):
_LOGGER.log(level, msg, *args, **kwargs) _LOGGER.log(level, msg, *args, **kwargs)
class ClientChannel(ZigbeeChannel): class ClientClusterHandler(ClusterHandler):
"""Channel listener for Zigbee client (output) clusters.""" """ClusterHandler for Zigbee client (output) clusters."""
@callback @callback
def attribute_updated(self, attrid, value): def attribute_updated(self, attrid, value):

View File

@ -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 zigpy.zcl.clusters import closures
from homeassistant.core import callback from homeassistant.core import callback
from . import AttrReportConfig, ClientClusterHandler, ClusterHandler
from .. import registries from .. import registries
from ..const import REPORT_CONFIG_IMMEDIATE, SIGNAL_ATTR_UPDATED from ..const import REPORT_CONFIG_IMMEDIATE, SIGNAL_ATTR_UPDATED
from .base import AttrReportConfig, ClientChannel, ZigbeeChannel
@registries.ZIGBEE_CHANNEL_REGISTRY.register(closures.DoorLock.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(closures.DoorLock.cluster_id)
class DoorLockChannel(ZigbeeChannel): class DoorLockClusterHandler(ClusterHandler):
"""Door lock channel.""" """Door lock cluster handler."""
_value_attribute = 0 _value_attribute = 0
REPORT_CONFIG = ( REPORT_CONFIG = (
@ -107,19 +107,19 @@ class DoorLockChannel(ZigbeeChannel):
return result return result
@registries.ZIGBEE_CHANNEL_REGISTRY.register(closures.Shade.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(closures.Shade.cluster_id)
class Shade(ZigbeeChannel): class Shade(ClusterHandler):
"""Shade channel.""" """Shade cluster handler."""
@registries.CLIENT_CHANNELS_REGISTRY.register(closures.WindowCovering.cluster_id) @registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(closures.WindowCovering.cluster_id)
class WindowCoveringClient(ClientChannel): class WindowCoveringClient(ClientClusterHandler):
"""Window client channel.""" """Window client cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register(closures.WindowCovering.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(closures.WindowCovering.cluster_id)
class WindowCovering(ZigbeeChannel): class WindowCovering(ClusterHandler):
"""Window channel.""" """Window cluster handler."""
_value_attribute = 8 _value_attribute = 8
REPORT_CONFIG = ( REPORT_CONFIG = (

View File

@ -1,4 +1,4 @@
"""General channels module for Zigbee Home Automation.""" """General cluster handlers module for Zigbee Home Automation."""
from __future__ import annotations from __future__ import annotations
import asyncio import asyncio
@ -14,6 +14,12 @@ from zigpy.zcl.foundation import Status
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.event import async_call_later from homeassistant.helpers.event import async_call_later
from . import (
AttrReportConfig,
ClientClusterHandler,
ClusterHandler,
parse_and_log_command,
)
from .. import registries from .. import registries
from ..const import ( from ..const import (
REPORT_CONFIG_ASAP, REPORT_CONFIG_ASAP,
@ -27,21 +33,20 @@ from ..const import (
SIGNAL_SET_LEVEL, SIGNAL_SET_LEVEL,
SIGNAL_UPDATE_DEVICE, SIGNAL_UPDATE_DEVICE,
) )
from .base import AttrReportConfig, ClientChannel, ZigbeeChannel, parse_and_log_command
from .helpers import is_hue_motion_sensor from .helpers import is_hue_motion_sensor
if TYPE_CHECKING: if TYPE_CHECKING:
from . import ChannelPool from ..endpoint import Endpoint
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Alarms.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.Alarms.cluster_id)
class Alarms(ZigbeeChannel): class Alarms(ClusterHandler):
"""Alarms channel.""" """Alarms cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogInput.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.AnalogInput.cluster_id)
class AnalogInput(ZigbeeChannel): class AnalogInput(ClusterHandler):
"""Analog Input channel.""" """Analog Input cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT),
@ -49,9 +54,9 @@ class AnalogInput(ZigbeeChannel):
@registries.BINDABLE_CLUSTERS.register(general.AnalogOutput.cluster_id) @registries.BINDABLE_CLUSTERS.register(general.AnalogOutput.cluster_id)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogOutput.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.AnalogOutput.cluster_id)
class AnalogOutput(ZigbeeChannel): class AnalogOutput(ClusterHandler):
"""Analog Output channel.""" """Analog Output cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT),
@ -120,24 +125,26 @@ class AnalogOutput(ZigbeeChannel):
return False return False
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.AnalogValue.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.AnalogValue.cluster_id)
class AnalogValue(ZigbeeChannel): class AnalogValue(ClusterHandler):
"""Analog Value channel.""" """Analog Value cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT),
) )
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.ApplianceControl.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
class ApplianceContorl(ZigbeeChannel): general.ApplianceControl.cluster_id
"""Appliance Control channel.""" )
class ApplianceContorl(ClusterHandler):
"""Appliance Control cluster handler."""
@registries.CHANNEL_ONLY_CLUSTERS.register(general.Basic.cluster_id) @registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(general.Basic.cluster_id)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Basic.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.Basic.cluster_id)
class BasicChannel(ZigbeeChannel): class BasicClusterHandler(ClusterHandler):
"""Channel to interact with the basic cluster.""" """Cluster handler to interact with the basic cluster."""
UNKNOWN = 0 UNKNOWN = 0
BATTERY = 3 BATTERY = 3
@ -153,9 +160,9 @@ class BasicChannel(ZigbeeChannel):
6: "Emergency mains and transfer switch", 6: "Emergency mains and transfer switch",
} }
def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None:
"""Initialize Basic channel.""" """Initialize Basic cluster handler."""
super().__init__(cluster, ch_pool) super().__init__(cluster, endpoint)
if is_hue_motion_sensor(self) and self.cluster.endpoint.endpoint_id == 2: 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 = ( # pylint: disable=invalid-name
self.ZCL_INIT_ATTRS.copy() self.ZCL_INIT_ATTRS.copy()
@ -169,41 +176,43 @@ class BasicChannel(ZigbeeChannel):
self.ZCL_INIT_ATTRS["transmit_power"] = True self.ZCL_INIT_ATTRS["transmit_power"] = True
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryInput.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.BinaryInput.cluster_id)
class BinaryInput(ZigbeeChannel): class BinaryInput(ClusterHandler):
"""Binary Input channel.""" """Binary Input cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT),
) )
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryOutput.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.BinaryOutput.cluster_id)
class BinaryOutput(ZigbeeChannel): class BinaryOutput(ClusterHandler):
"""Binary Output channel.""" """Binary Output cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT),
) )
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.BinaryValue.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.BinaryValue.cluster_id)
class BinaryValue(ZigbeeChannel): class BinaryValue(ClusterHandler):
"""Binary Value channel.""" """Binary Value cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT),
) )
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Commissioning.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.Commissioning.cluster_id)
class Commissioning(ZigbeeChannel): class Commissioning(ClusterHandler):
"""Commissioning channel.""" """Commissioning cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.DeviceTemperature.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
class DeviceTemperature(ZigbeeChannel): general.DeviceTemperature.cluster_id
"""Device Temperature channel.""" )
class DeviceTemperature(ClusterHandler):
"""Device Temperature cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
{ {
@ -213,23 +222,23 @@ class DeviceTemperature(ZigbeeChannel):
) )
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.GreenPowerProxy.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.GreenPowerProxy.cluster_id)
class GreenPowerProxy(ZigbeeChannel): class GreenPowerProxy(ClusterHandler):
"""Green Power Proxy channel.""" """Green Power Proxy cluster handler."""
BIND: bool = False BIND: bool = False
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Groups.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.Groups.cluster_id)
class Groups(ZigbeeChannel): class Groups(ClusterHandler):
"""Groups channel.""" """Groups cluster handler."""
BIND: bool = False BIND: bool = False
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Identify.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.Identify.cluster_id)
class Identify(ZigbeeChannel): class Identify(ClusterHandler):
"""Identify channel.""" """Identify cluster handler."""
BIND: bool = False BIND: bool = False
@ -242,15 +251,15 @@ class Identify(ZigbeeChannel):
self.async_send_signal(f"{self.unique_id}_{cmd}", args[0]) self.async_send_signal(f"{self.unique_id}_{cmd}", args[0])
@registries.CLIENT_CHANNELS_REGISTRY.register(general.LevelControl.cluster_id) @registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(general.LevelControl.cluster_id)
class LevelControlClientChannel(ClientChannel): class LevelControlClientClusterHandler(ClientClusterHandler):
"""LevelControl client cluster.""" """LevelControl client cluster."""
@registries.BINDABLE_CLUSTERS.register(general.LevelControl.cluster_id) @registries.BINDABLE_CLUSTERS.register(general.LevelControl.cluster_id)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.LevelControl.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.LevelControl.cluster_id)
class LevelControlChannel(ZigbeeChannel): class LevelControlClusterHandler(ClusterHandler):
"""Channel for the LevelControl Zigbee cluster.""" """Cluster handler for the LevelControl Zigbee cluster."""
CURRENT_LEVEL = 0 CURRENT_LEVEL = 0
REPORT_CONFIG = (AttrReportConfig(attr="current_level", config=REPORT_CONFIG_ASAP),) 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) self.async_send_signal(f"{self.unique_id}_{command}", level)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.MultistateInput.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.MultistateInput.cluster_id)
class MultistateInput(ZigbeeChannel): class MultistateInput(ClusterHandler):
"""Multistate Input channel.""" """Multistate Input cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT),
) )
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.MultistateOutput.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
class MultistateOutput(ZigbeeChannel): general.MultistateOutput.cluster_id
"""Multistate Output channel.""" )
class MultistateOutput(ClusterHandler):
"""Multistate Output cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT),
) )
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.MultistateValue.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.MultistateValue.cluster_id)
class MultistateValue(ZigbeeChannel): class MultistateValue(ClusterHandler):
"""Multistate Value channel.""" """Multistate Value cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="present_value", config=REPORT_CONFIG_DEFAULT),
) )
@registries.CLIENT_CHANNELS_REGISTRY.register(general.OnOff.cluster_id) @registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(general.OnOff.cluster_id)
class OnOffClientChannel(ClientChannel): class OnOffClientClusterHandler(ClientClusterHandler):
"""OnOff client channel.""" """OnOff client cluster handler."""
@registries.BINDABLE_CLUSTERS.register(general.OnOff.cluster_id) @registries.BINDABLE_CLUSTERS.register(general.OnOff.cluster_id)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.OnOff.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.OnOff.cluster_id)
class OnOffChannel(ZigbeeChannel): class OnOffClusterHandler(ClusterHandler):
"""Channel for the OnOff Zigbee cluster.""" """Cluster handler for the OnOff Zigbee cluster."""
ON_OFF = 0 ON_OFF = 0
REPORT_CONFIG = (AttrReportConfig(attr="on_off", config=REPORT_CONFIG_IMMEDIATE),) REPORT_CONFIG = (AttrReportConfig(attr="on_off", config=REPORT_CONFIG_IMMEDIATE),)
@ -342,9 +353,9 @@ class OnOffChannel(ZigbeeChannel):
"start_up_on_off": True, "start_up_on_off": True,
} }
def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None:
"""Initialize OnOffChannel.""" """Initialize OnOffClusterHandler."""
super().__init__(cluster, ch_pool) super().__init__(cluster, endpoint)
self._off_listener = None self._off_listener = None
if self.cluster.endpoint.model in ( if self.cluster.endpoint.model in (
@ -404,7 +415,7 @@ class OnOffChannel(ZigbeeChannel):
self.cluster.update_attribute(self.ON_OFF, t.Bool.true) self.cluster.update_attribute(self.ON_OFF, t.Bool.true)
if on_time > 0: if on_time > 0:
self._off_listener = async_call_later( self._off_listener = async_call_later(
self._ch_pool.hass, self._endpoint.device.hass,
(on_time / 10), # value is in 10ths of a second (on_time / 10), # value is in 10ths of a second
self.set_to_off, self.set_to_off,
) )
@ -426,24 +437,26 @@ class OnOffChannel(ZigbeeChannel):
) )
async def async_update(self): async def async_update(self):
"""Initialize channel.""" """Initialize cluster handler."""
if self.cluster.is_client: if self.cluster.is_client:
return 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) 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 self.get_attribute_value(self.ON_OFF, from_cache=from_cache)
await super().async_update() await super().async_update()
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.OnOffConfiguration.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
class OnOffConfiguration(ZigbeeChannel): general.OnOffConfiguration.cluster_id
"""OnOff Configuration channel.""" )
class OnOffConfiguration(ClusterHandler):
"""OnOff Configuration cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Ota.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.Ota.cluster_id)
@registries.CLIENT_CHANNELS_REGISTRY.register(general.Ota.cluster_id) @registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(general.Ota.cluster_id)
class Ota(ClientChannel): class Ota(ClientClusterHandler):
"""OTA Channel.""" """OTA cluster handler."""
BIND: bool = False BIND: bool = False
@ -457,21 +470,21 @@ class Ota(ClientChannel):
else: else:
cmd_name = command_id 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": if cmd_name == "query_next_image":
assert args assert args
self.async_send_signal(SIGNAL_UPDATE_DEVICE.format(signal_id), args[3]) self.async_send_signal(SIGNAL_UPDATE_DEVICE.format(signal_id), args[3])
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Partition.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.Partition.cluster_id)
class Partition(ZigbeeChannel): class Partition(ClusterHandler):
"""Partition channel.""" """Partition cluster handler."""
@registries.CHANNEL_ONLY_CLUSTERS.register(general.PollControl.cluster_id) @registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(general.PollControl.cluster_id)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PollControl.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.PollControl.cluster_id)
class PollControl(ZigbeeChannel): class PollControl(ClusterHandler):
"""Poll Control channel.""" """Poll Control cluster handler."""
CHECKIN_INTERVAL = 55 * 60 * 4 # 55min CHECKIN_INTERVAL = 55 * 60 * 4 # 55min
CHECKIN_FAST_POLL_TIMEOUT = 2 * 4 # 2s CHECKIN_FAST_POLL_TIMEOUT = 2 * 4 # 2s
@ -480,8 +493,8 @@ class PollControl(ZigbeeChannel):
4476, 4476,
} # IKEA } # IKEA
async def async_configure_channel_specific(self) -> None: async def async_configure_cluster_handler_specific(self) -> None:
"""Configure channel: set check-in interval.""" """Configure cluster handler: set check-in interval."""
try: try:
res = await self.cluster.write_attributes( res = await self.cluster.write_attributes(
{"checkin_interval": self.CHECKIN_INTERVAL} {"checkin_interval": self.CHECKIN_INTERVAL}
@ -508,7 +521,7 @@ class PollControl(ZigbeeChannel):
async def check_in_response(self, tsn: int) -> None: async def check_in_response(self, tsn: int) -> None:
"""Respond to checkin command.""" """Respond to checkin command."""
await self.checkin_response(True, self.CHECKIN_FAST_POLL_TIMEOUT, tsn=tsn) 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.set_long_poll_interval(self.LONG_POLL)
await self.fast_poll_stop() await self.fast_poll_stop()
@ -518,9 +531,11 @@ class PollControl(ZigbeeChannel):
self._IGNORED_MANUFACTURER_ID.add(manufacturer_code) self._IGNORED_MANUFACTURER_ID.add(manufacturer_code)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PowerConfiguration.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
class PowerConfigurationChannel(ZigbeeChannel): general.PowerConfiguration.cluster_id
"""Channel for the zigbee power configuration cluster.""" )
class PowerConfigurationCLusterHandler(ClusterHandler):
"""Cluster handler for the zigbee power configuration cluster."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig(attr="battery_voltage", config=REPORT_CONFIG_BATTERY_SAVE), 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: def async_initialize_cluster_handler_specific(self, from_cache: bool) -> Coroutine:
"""Initialize channel specific attrs.""" """Initialize cluster handler specific attrs."""
attributes = [ attributes = [
"battery_size", "battery_size",
"battery_quantity", "battery_quantity",
@ -540,26 +555,26 @@ class PowerConfigurationChannel(ZigbeeChannel):
) )
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.PowerProfile.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.PowerProfile.cluster_id)
class PowerProfile(ZigbeeChannel): class PowerProfile(ClusterHandler):
"""Power Profile channel.""" """Power Profile cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.RSSILocation.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.RSSILocation.cluster_id)
class RSSILocation(ZigbeeChannel): class RSSILocation(ClusterHandler):
"""RSSI Location channel.""" """RSSI Location cluster handler."""
@registries.CLIENT_CHANNELS_REGISTRY.register(general.Scenes.cluster_id) @registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(general.Scenes.cluster_id)
class ScenesClientChannel(ClientChannel): class ScenesClientClusterHandler(ClientClusterHandler):
"""Scenes channel.""" """Scenes cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Scenes.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.Scenes.cluster_id)
class Scenes(ZigbeeChannel): class Scenes(ClusterHandler):
"""Scenes channel.""" """Scenes cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Time.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(general.Time.cluster_id)
class Time(ZigbeeChannel): class Time(ClusterHandler):
"""Time channel.""" """Time cluster handler."""

View File

@ -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",
)

View File

@ -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 from __future__ import annotations
import enum import enum
from zigpy.zcl.clusters import homeautomation from zigpy.zcl.clusters import homeautomation
from . import AttrReportConfig, ClusterHandler
from .. import registries from .. import registries
from ..const import ( from ..const import (
CHANNEL_ELECTRICAL_MEASUREMENT, CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT,
REPORT_CONFIG_DEFAULT, REPORT_CONFIG_DEFAULT,
REPORT_CONFIG_OP, REPORT_CONFIG_OP,
SIGNAL_ATTR_UPDATED, SIGNAL_ATTR_UPDATED,
) )
from .base import AttrReportConfig, ZigbeeChannel
@registries.ZIGBEE_CHANNEL_REGISTRY.register( @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
homeautomation.ApplianceEventAlerts.cluster_id homeautomation.ApplianceEventAlerts.cluster_id
) )
class ApplianceEventAlerts(ZigbeeChannel): class ApplianceEventAlerts(ClusterHandler):
"""Appliance Event Alerts channel.""" """Appliance Event Alerts cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register( @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
homeautomation.ApplianceIdentification.cluster_id homeautomation.ApplianceIdentification.cluster_id
) )
class ApplianceIdentification(ZigbeeChannel): class ApplianceIdentification(ClusterHandler):
"""Appliance Identification channel.""" """Appliance Identification cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register( @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
homeautomation.ApplianceStatistics.cluster_id homeautomation.ApplianceStatistics.cluster_id
) )
class ApplianceStatistics(ZigbeeChannel): class ApplianceStatistics(ClusterHandler):
"""Appliance Statistics channel.""" """Appliance Statistics cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register(homeautomation.Diagnostic.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
class Diagnostic(ZigbeeChannel): homeautomation.Diagnostic.cluster_id
"""Diagnostic channel.""" )
class Diagnostic(ClusterHandler):
"""Diagnostic cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register( @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
homeautomation.ElectricalMeasurement.cluster_id homeautomation.ElectricalMeasurement.cluster_id
) )
class ElectricalMeasurementChannel(ZigbeeChannel): class ElectricalMeasurementClusterHandler(ClusterHandler):
"""Channel that polls active power level.""" """Cluster handler that polls active power level."""
CHANNEL_NAME = CHANNEL_ELECTRICAL_MEASUREMENT CLUSTER_HANDLER_NAME = CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT
class MeasurementType(enum.IntFlag): class MeasurementType(enum.IntFlag):
"""Measurement types.""" """Measurement types."""
@ -91,7 +93,7 @@ class ElectricalMeasurementChannel(ZigbeeChannel):
"""Retrieve latest state.""" """Retrieve latest state."""
self.debug("async_update") self.debug("async_update")
# This is a polling channel. Don't allow cache. # This is a polling cluster handler. Don't allow cache.
attrs = [ attrs = [
a["attr"] a["attr"]
for a in self.REPORT_CONFIG 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 homeautomation.MeterIdentification.cluster_id
) )
class MeterIdentification(ZigbeeChannel): class MeterIdentification(ClusterHandler):
"""Metering Identification channel.""" """Metering Identification cluster handler."""

View File

@ -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 For more details about this component, please refer to the documentation at
https://home-assistant.io/integrations/zha/ https://home-assistant.io/integrations/zha/
@ -14,6 +14,7 @@ from zigpy.zcl.foundation import Status
from homeassistant.core import callback from homeassistant.core import callback
from . import AttrReportConfig, ClusterHandler
from .. import registries from .. import registries
from ..const import ( from ..const import (
REPORT_CONFIG_MAX_INT, REPORT_CONFIG_MAX_INT,
@ -21,7 +22,6 @@ from ..const import (
REPORT_CONFIG_OP, REPORT_CONFIG_OP,
SIGNAL_ATTR_UPDATED, SIGNAL_ATTR_UPDATED,
) )
from .base import AttrReportConfig, ZigbeeChannel
AttributeUpdateRecord = namedtuple("AttributeUpdateRecord", "attr_id, attr_name, value") AttributeUpdateRecord = namedtuple("AttributeUpdateRecord", "attr_id, attr_name, value")
REPORT_CONFIG_CLIMATE = (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 25) 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) REPORT_CONFIG_CLIMATE_DISCRETE = (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT, 1)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.Dehumidification.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(hvac.Dehumidification.cluster_id)
class Dehumidification(ZigbeeChannel): class Dehumidification(ClusterHandler):
"""Dehumidification channel.""" """Dehumidification cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.Fan.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(hvac.Fan.cluster_id)
class FanChannel(ZigbeeChannel): class FanClusterHandler(ClusterHandler):
"""Fan channel.""" """Fan cluster handler."""
_value_attribute = 0 _value_attribute = 0
@ -79,14 +79,14 @@ class FanChannel(ZigbeeChannel):
) )
@registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.Pump.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(hvac.Pump.cluster_id)
class Pump(ZigbeeChannel): class Pump(ClusterHandler):
"""Pump channel.""" """Pump cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.Thermostat.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(hvac.Thermostat.cluster_id)
class ThermostatChannel(ZigbeeChannel): class ThermostatClusterHandler(ClusterHandler):
"""Thermostat channel.""" """Thermostat cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig(attr="local_temperature", config=REPORT_CONFIG_CLIMATE), 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]) return all(record.status == Status.SUCCESS for record in res[0])
@registries.ZIGBEE_CHANNEL_REGISTRY.register(hvac.UserInterface.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(hvac.UserInterface.cluster_id)
class UserInterface(ZigbeeChannel): class UserInterface(ClusterHandler):
"""User interface (thermostat) channel.""" """User interface (thermostat) cluster handler."""

View File

@ -1,29 +1,29 @@
"""Lighting channels module for Zigbee Home Automation.""" """Lighting cluster handlers module for Zigbee Home Automation."""
from __future__ import annotations from __future__ import annotations
from functools import cached_property from functools import cached_property
from zigpy.zcl.clusters import lighting from zigpy.zcl.clusters import lighting
from . import AttrReportConfig, ClientClusterHandler, ClusterHandler
from .. import registries from .. import registries
from ..const import REPORT_CONFIG_DEFAULT from ..const import REPORT_CONFIG_DEFAULT
from .base import AttrReportConfig, ClientChannel, ZigbeeChannel
@registries.ZIGBEE_CHANNEL_REGISTRY.register(lighting.Ballast.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(lighting.Ballast.cluster_id)
class Ballast(ZigbeeChannel): class Ballast(ClusterHandler):
"""Ballast channel.""" """Ballast cluster handler."""
@registries.CLIENT_CHANNELS_REGISTRY.register(lighting.Color.cluster_id) @registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(lighting.Color.cluster_id)
class ColorClientChannel(ClientChannel): class ColorClientClusterHandler(ClientClusterHandler):
"""Color client channel.""" """Color client cluster handler."""
@registries.BINDABLE_CLUSTERS.register(lighting.Color.cluster_id) @registries.BINDABLE_CLUSTERS.register(lighting.Color.cluster_id)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(lighting.Color.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(lighting.Color.cluster_id)
class ColorChannel(ZigbeeChannel): class ColorClusterHandler(ClusterHandler):
"""Color channel.""" """Color cluster handler."""
CAPABILITIES_COLOR_XY = 0x08 CAPABILITIES_COLOR_XY = 0x08
CAPABILITIES_COLOR_TEMP = 0x10 CAPABILITIES_COLOR_TEMP = 0x10
@ -98,7 +98,7 @@ class ColorChannel(ZigbeeChannel):
@property @property
def min_mireds(self) -> int: 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) min_mireds = self.cluster.get("color_temp_physical_min", self.MIN_MIREDS)
if min_mireds == 0: if min_mireds == 0:
self.warning( self.warning(
@ -113,7 +113,7 @@ class ColorChannel(ZigbeeChannel):
@property @property
def max_mireds(self) -> int: 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) max_mireds = self.cluster.get("color_temp_physical_max", self.MAX_MIREDS)
if max_mireds == 0: if max_mireds == 0:
self.warning( self.warning(
@ -128,7 +128,7 @@ class ColorChannel(ZigbeeChannel):
@property @property
def hs_supported(self) -> bool: 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 ( return (
self.color_capabilities is not None self.color_capabilities is not None
and lighting.Color.ColorCapabilities.Hue_and_saturation and lighting.Color.ColorCapabilities.Hue_and_saturation
@ -137,7 +137,7 @@ class ColorChannel(ZigbeeChannel):
@property @property
def enhanced_hue_supported(self) -> bool: 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 ( return (
self.color_capabilities is not None self.color_capabilities is not None
and lighting.Color.ColorCapabilities.Enhanced_hue in self.color_capabilities and lighting.Color.ColorCapabilities.Enhanced_hue in self.color_capabilities
@ -145,7 +145,7 @@ class ColorChannel(ZigbeeChannel):
@property @property
def xy_supported(self) -> bool: def xy_supported(self) -> bool:
"""Return True if the channel supports xy.""" """Return True if the cluster handler supports xy."""
return ( return (
self.color_capabilities is not None self.color_capabilities is not None
and lighting.Color.ColorCapabilities.XY_attributes and lighting.Color.ColorCapabilities.XY_attributes
@ -154,7 +154,7 @@ class ColorChannel(ZigbeeChannel):
@property @property
def color_temp_supported(self) -> bool: def color_temp_supported(self) -> bool:
"""Return True if the channel supports color temperature.""" """Return True if the cluster handler supports color temperature."""
return ( return (
self.color_capabilities is not None self.color_capabilities is not None
and lighting.Color.ColorCapabilities.Color_temperature and lighting.Color.ColorCapabilities.Color_temperature
@ -163,7 +163,7 @@ class ColorChannel(ZigbeeChannel):
@property @property
def color_loop_supported(self) -> bool: def color_loop_supported(self) -> bool:
"""Return True if the channel supports color loop.""" """Return True if the cluster handler supports color loop."""
return ( return (
self.color_capabilities is not None self.color_capabilities is not None
and lighting.Color.ColorCapabilities.Color_loop in self.color_capabilities and lighting.Color.ColorCapabilities.Color_loop in self.color_capabilities
@ -171,10 +171,10 @@ class ColorChannel(ZigbeeChannel):
@property @property
def options(self) -> lighting.Color.Options: 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)) return lighting.Color.Options(self.cluster.get("options", 0))
@property @property
def execute_if_off_supported(self) -> bool: 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 return lighting.Color.Options.Execute_if_off in self.options

View File

@ -1,29 +1,29 @@
"""Lightlink channels module for Zigbee Home Automation.""" """Lightlink cluster handlers module for Zigbee Home Automation."""
import asyncio import asyncio
import zigpy.exceptions import zigpy.exceptions
from zigpy.zcl.clusters import lightlink from zigpy.zcl.clusters import lightlink
from zigpy.zcl.foundation import GENERAL_COMMANDS, GeneralCommand from zigpy.zcl.foundation import GENERAL_COMMANDS, GeneralCommand
from . import ClusterHandler, ClusterHandlerStatus
from .. import registries from .. import registries
from .base import ChannelStatus, ZigbeeChannel
@registries.CHANNEL_ONLY_CLUSTERS.register(lightlink.LightLink.cluster_id) @registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(lightlink.LightLink.cluster_id)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(lightlink.LightLink.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(lightlink.LightLink.cluster_id)
class LightLink(ZigbeeChannel): class LightLink(ClusterHandler):
"""Lightlink channel.""" """Lightlink cluster handler."""
BIND: bool = False BIND: bool = False
async def async_configure(self) -> None: async def async_configure(self) -> None:
"""Add Coordinator to LightLink group.""" """Add Coordinator to LightLink group."""
if self._ch_pool.skip_configuration: if self._endpoint.device.skip_configuration:
self._status = ChannelStatus.CONFIGURED self._status = ClusterHandlerStatus.CONFIGURED
return return
application = self._ch_pool.endpoint.device.application application = self._endpoint.zigpy_endpoint.device.application
try: try:
coordinator = application.get_device(application.state.node_info.ieee) coordinator = application.get_device(application.state.node_info.ieee)
except KeyError: except KeyError:

View File

@ -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 from __future__ import annotations
import logging import logging
@ -10,6 +10,7 @@ import zigpy.zcl
from homeassistant.core import callback from homeassistant.core import callback
from . import AttrReportConfig, ClientClusterHandler, ClusterHandler
from .. import registries from .. import registries
from ..const import ( from ..const import (
ATTR_ATTRIBUTE_ID, ATTR_ATTRIBUTE_ID,
@ -23,17 +24,18 @@ from ..const import (
SIGNAL_ATTR_UPDATED, SIGNAL_ATTR_UPDATED,
UNKNOWN, UNKNOWN,
) )
from .base import AttrReportConfig, ClientChannel, ZigbeeChannel
if TYPE_CHECKING: if TYPE_CHECKING:
from . import ChannelPool from ..endpoint import Endpoint
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(registries.SMARTTHINGS_HUMIDITY_CLUSTER) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
class SmartThingsHumidity(ZigbeeChannel): registries.SMARTTHINGS_HUMIDITY_CLUSTER
"""Smart Things Humidity channel.""" )
class SmartThingsHumidity(ClusterHandler):
"""Smart Things Humidity cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
{ {
@ -43,32 +45,34 @@ class SmartThingsHumidity(ZigbeeChannel):
) )
@registries.CHANNEL_ONLY_CLUSTERS.register(0xFD00) @registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(0xFD00)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(0xFD00) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(0xFD00)
class OsramButton(ZigbeeChannel): class OsramButton(ClusterHandler):
"""Osram button channel.""" """Osram button cluster handler."""
REPORT_CONFIG = () REPORT_CONFIG = ()
@registries.CHANNEL_ONLY_CLUSTERS.register(registries.PHILLIPS_REMOTE_CLUSTER) @registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(registries.PHILLIPS_REMOTE_CLUSTER)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(registries.PHILLIPS_REMOTE_CLUSTER) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(registries.PHILLIPS_REMOTE_CLUSTER)
class PhillipsRemote(ZigbeeChannel): class PhillipsRemote(ClusterHandler):
"""Phillips remote channel.""" """Phillips remote cluster handler."""
REPORT_CONFIG = () REPORT_CONFIG = ()
@registries.CHANNEL_ONLY_CLUSTERS.register(registries.TUYA_MANUFACTURER_CLUSTER) @registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(registries.TUYA_MANUFACTURER_CLUSTER)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(registries.TUYA_MANUFACTURER_CLUSTER) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
class TuyaChannel(ZigbeeChannel): registries.TUYA_MANUFACTURER_CLUSTER
"""Channel for the Tuya manufacturer Zigbee cluster.""" )
class TuyaClusterHandler(ClusterHandler):
"""Cluster handler for the Tuya manufacturer Zigbee cluster."""
REPORT_CONFIG = () REPORT_CONFIG = ()
def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None:
"""Initialize TuyaChannel.""" """Initialize TuyaClusterHandler."""
super().__init__(cluster, ch_pool) super().__init__(cluster, endpoint)
if self.cluster.endpoint.manufacturer in ( if self.cluster.endpoint.manufacturer in (
"_TZE200_7tdtqgwv", "_TZE200_7tdtqgwv",
@ -94,16 +98,16 @@ class TuyaChannel(ZigbeeChannel):
} }
@registries.CHANNEL_ONLY_CLUSTERS.register(0xFCC0) @registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(0xFCC0)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(0xFCC0) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(0xFCC0)
class OppleRemote(ZigbeeChannel): class OppleRemote(ClusterHandler):
"""Opple channel.""" """Opple cluster handler."""
REPORT_CONFIG = () REPORT_CONFIG = ()
def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None:
"""Initialize Opple channel.""" """Initialize Opple cluster handler."""
super().__init__(cluster, ch_pool) super().__init__(cluster, endpoint)
if self.cluster.endpoint.model == "lumi.motion.ac02": if self.cluster.endpoint.model == "lumi.motion.ac02":
self.ZCL_INIT_ATTRS = { # pylint: disable=invalid-name self.ZCL_INIT_ATTRS = { # pylint: disable=invalid-name
"detection_interval": True, "detection_interval": True,
@ -162,8 +166,8 @@ class OppleRemote(ZigbeeChannel):
"linkage_alarm": True, "linkage_alarm": True,
} }
async def async_initialize_channel_specific(self, from_cache: bool) -> None: async def async_initialize_cluster_handler_specific(self, from_cache: bool) -> None:
"""Initialize channel specific.""" """Initialize cluster handler specific."""
if self.cluster.endpoint.model in ("lumi.motion.ac02", "lumi.motion.agl04"): if self.cluster.endpoint.model in ("lumi.motion.ac02", "lumi.motion.agl04"):
interval = self.cluster.get("detection_interval", self.cluster.get(0x0102)) interval = self.cluster.get("detection_interval", self.cluster.get(0x0102))
if interval is not None: if interval is not None:
@ -171,11 +175,11 @@ class OppleRemote(ZigbeeChannel):
self.cluster.endpoint.ias_zone.reset_s = int(interval) self.cluster.endpoint.ias_zone.reset_s = int(interval)
@registries.ZIGBEE_CHANNEL_REGISTRY.register( @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
registries.SMARTTHINGS_ACCELERATION_CLUSTER registries.SMARTTHINGS_ACCELERATION_CLUSTER
) )
class SmartThingsAcceleration(ZigbeeChannel): class SmartThingsAcceleration(ClusterHandler):
"""Smart Things Acceleration channel.""" """Smart Things Acceleration cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig(attr="acceleration", config=REPORT_CONFIG_ASAP), AttrReportConfig(attr="acceleration", config=REPORT_CONFIG_ASAP),
@ -211,9 +215,9 @@ class SmartThingsAcceleration(ZigbeeChannel):
) )
@registries.CLIENT_CHANNELS_REGISTRY.register(0xFC31) @registries.CLIENT_CLUSTER_HANDLER_REGISTRY.register(0xFC31)
class InovelliNotificationChannel(ClientChannel): class InovelliNotificationClusterHandler(ClientClusterHandler):
"""Inovelli Notification channel.""" """Inovelli Notification cluster handler."""
@callback @callback
def attribute_updated(self, attrid, value): def attribute_updated(self, attrid, value):
@ -224,9 +228,9 @@ class InovelliNotificationChannel(ClientChannel):
"""Handle a cluster command received on this cluster.""" """Handle a cluster command received on this cluster."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register(0xFC31) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(0xFC31)
class InovelliConfigEntityChannel(ZigbeeChannel): class InovelliConfigEntityClusterHandler(ClusterHandler):
"""Inovelli Configuration Entity channel.""" """Inovelli Configuration Entity cluster handler."""
REPORT_CONFIG = () REPORT_CONFIG = ()
ZCL_INIT_ATTRS = { ZCL_INIT_ATTRS = {
@ -307,10 +311,12 @@ class InovelliConfigEntityChannel(ZigbeeChannel):
) )
@registries.CHANNEL_ONLY_CLUSTERS.register(registries.IKEA_AIR_PURIFIER_CLUSTER) @registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(registries.IKEA_AIR_PURIFIER_CLUSTER)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(registries.IKEA_AIR_PURIFIER_CLUSTER) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
class IkeaAirPurifierChannel(ZigbeeChannel): registries.IKEA_AIR_PURIFIER_CLUSTER
"""IKEA Air Purifier channel.""" )
class IkeaAirPurifierClusterHandler(ClusterHandler):
"""IKEA Air Purifier cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig(attr="filter_run_time", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="filter_run_time", config=REPORT_CONFIG_DEFAULT),
@ -360,9 +366,9 @@ class IkeaAirPurifierChannel(ZigbeeChannel):
) )
@registries.CHANNEL_ONLY_CLUSTERS.register(0xFC80) @registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(0xFC80)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(0xFC80) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(0xFC80)
class IkeaRemote(ZigbeeChannel): class IkeaRemote(ClusterHandler):
"""Ikea Matter remote channel.""" """Ikea Matter remote cluster handler."""
REPORT_CONFIG = () REPORT_CONFIG = ()

View File

@ -1,4 +1,4 @@
"""Measurement channels module for Zigbee Home Automation.""" """Measurement cluster handlers module for Zigbee Home Automation."""
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
@ -6,6 +6,7 @@ from typing import TYPE_CHECKING
import zigpy.zcl import zigpy.zcl
from zigpy.zcl.clusters import measurement from zigpy.zcl.clusters import measurement
from . import AttrReportConfig, ClusterHandler
from .. import registries from .. import registries
from ..const import ( from ..const import (
REPORT_CONFIG_DEFAULT, REPORT_CONFIG_DEFAULT,
@ -13,55 +14,58 @@ from ..const import (
REPORT_CONFIG_MAX_INT, REPORT_CONFIG_MAX_INT,
REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MIN_INT,
) )
from .base import AttrReportConfig, ZigbeeChannel
from .helpers import is_hue_motion_sensor from .helpers import is_hue_motion_sensor
if TYPE_CHECKING: if TYPE_CHECKING:
from . import ChannelPool from ..endpoint import Endpoint
@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.FlowMeasurement.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
class FlowMeasurement(ZigbeeChannel): measurement.FlowMeasurement.cluster_id
"""Flow Measurement channel.""" )
class FlowMeasurement(ClusterHandler):
"""Flow Measurement cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig(attr="measured_value", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="measured_value", config=REPORT_CONFIG_DEFAULT),
) )
@registries.ZIGBEE_CHANNEL_REGISTRY.register( @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
measurement.IlluminanceLevelSensing.cluster_id measurement.IlluminanceLevelSensing.cluster_id
) )
class IlluminanceLevelSensing(ZigbeeChannel): class IlluminanceLevelSensing(ClusterHandler):
"""Illuminance Level Sensing channel.""" """Illuminance Level Sensing cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig(attr="level_status", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="level_status", config=REPORT_CONFIG_DEFAULT),
) )
@registries.ZIGBEE_CHANNEL_REGISTRY.register( @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
measurement.IlluminanceMeasurement.cluster_id measurement.IlluminanceMeasurement.cluster_id
) )
class IlluminanceMeasurement(ZigbeeChannel): class IlluminanceMeasurement(ClusterHandler):
"""Illuminance Measurement channel.""" """Illuminance Measurement cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig(attr="measured_value", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="measured_value", config=REPORT_CONFIG_DEFAULT),
) )
@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.OccupancySensing.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
class OccupancySensing(ZigbeeChannel): measurement.OccupancySensing.cluster_id
"""Occupancy Sensing channel.""" )
class OccupancySensing(ClusterHandler):
"""Occupancy Sensing cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig(attr="occupancy", config=REPORT_CONFIG_IMMEDIATE), AttrReportConfig(attr="occupancy", config=REPORT_CONFIG_IMMEDIATE),
) )
def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None:
"""Initialize Occupancy channel.""" """Initialize Occupancy cluster handler."""
super().__init__(cluster, ch_pool) super().__init__(cluster, endpoint)
if is_hue_motion_sensor(self): if is_hue_motion_sensor(self):
self.ZCL_INIT_ATTRS = ( # pylint: disable=invalid-name self.ZCL_INIT_ATTRS = ( # pylint: disable=invalid-name
self.ZCL_INIT_ATTRS.copy() self.ZCL_INIT_ATTRS.copy()
@ -69,18 +73,22 @@ class OccupancySensing(ZigbeeChannel):
self.ZCL_INIT_ATTRS["sensitivity"] = True self.ZCL_INIT_ATTRS["sensitivity"] = True
@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.PressureMeasurement.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
class PressureMeasurement(ZigbeeChannel): measurement.PressureMeasurement.cluster_id
"""Pressure measurement channel.""" )
class PressureMeasurement(ClusterHandler):
"""Pressure measurement cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig(attr="measured_value", config=REPORT_CONFIG_DEFAULT), AttrReportConfig(attr="measured_value", config=REPORT_CONFIG_DEFAULT),
) )
@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.RelativeHumidity.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
class RelativeHumidity(ZigbeeChannel): measurement.RelativeHumidity.cluster_id
"""Relative Humidity measurement channel.""" )
class RelativeHumidity(ClusterHandler):
"""Relative Humidity measurement cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig( AttrReportConfig(
@ -90,9 +98,11 @@ class RelativeHumidity(ZigbeeChannel):
) )
@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.SoilMoisture.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
class SoilMoisture(ZigbeeChannel): measurement.SoilMoisture.cluster_id
"""Soil Moisture measurement channel.""" )
class SoilMoisture(ClusterHandler):
"""Soil Moisture measurement cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig( AttrReportConfig(
@ -102,9 +112,9 @@ class SoilMoisture(ZigbeeChannel):
) )
@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.LeafWetness.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(measurement.LeafWetness.cluster_id)
class LeafWetness(ZigbeeChannel): class LeafWetness(ClusterHandler):
"""Leaf Wetness measurement channel.""" """Leaf Wetness measurement cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig( AttrReportConfig(
@ -114,11 +124,11 @@ class LeafWetness(ZigbeeChannel):
) )
@registries.ZIGBEE_CHANNEL_REGISTRY.register( @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
measurement.TemperatureMeasurement.cluster_id measurement.TemperatureMeasurement.cluster_id
) )
class TemperatureMeasurement(ZigbeeChannel): class TemperatureMeasurement(ClusterHandler):
"""Temperature measurement channel.""" """Temperature measurement cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig( AttrReportConfig(
@ -128,11 +138,11 @@ class TemperatureMeasurement(ZigbeeChannel):
) )
@registries.ZIGBEE_CHANNEL_REGISTRY.register( @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
measurement.CarbonMonoxideConcentration.cluster_id measurement.CarbonMonoxideConcentration.cluster_id
) )
class CarbonMonoxideConcentration(ZigbeeChannel): class CarbonMonoxideConcentration(ClusterHandler):
"""Carbon Monoxide measurement channel.""" """Carbon Monoxide measurement cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig( AttrReportConfig(
@ -142,11 +152,11 @@ class CarbonMonoxideConcentration(ZigbeeChannel):
) )
@registries.ZIGBEE_CHANNEL_REGISTRY.register( @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
measurement.CarbonDioxideConcentration.cluster_id measurement.CarbonDioxideConcentration.cluster_id
) )
class CarbonDioxideConcentration(ZigbeeChannel): class CarbonDioxideConcentration(ClusterHandler):
"""Carbon Dioxide measurement channel.""" """Carbon Dioxide measurement cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig( AttrReportConfig(
@ -156,9 +166,9 @@ class CarbonDioxideConcentration(ZigbeeChannel):
) )
@registries.ZIGBEE_CHANNEL_REGISTRY.register(measurement.PM25.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(measurement.PM25.cluster_id)
class PM25(ZigbeeChannel): class PM25(ClusterHandler):
"""Particulate Matter 2.5 microns or less measurement channel.""" """Particulate Matter 2.5 microns or less measurement cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig( AttrReportConfig(
@ -168,11 +178,11 @@ class PM25(ZigbeeChannel):
) )
@registries.ZIGBEE_CHANNEL_REGISTRY.register( @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
measurement.FormaldehydeConcentration.cluster_id measurement.FormaldehydeConcentration.cluster_id
) )
class FormaldehydeConcentration(ZigbeeChannel): class FormaldehydeConcentration(ClusterHandler):
"""Formaldehyde measurement channel.""" """Formaldehyde measurement cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig( AttrReportConfig(

View 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."""

View File

@ -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 For more details about this component, please refer to the documentation at
https://home-assistant.io/integrations/zha/ 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 homeassistant.core import callback
from . import ClusterHandler, ClusterHandlerStatus
from .. import registries from .. import registries
from ..const import ( from ..const import (
SIGNAL_ATTR_UPDATED, SIGNAL_ATTR_UPDATED,
@ -24,10 +25,9 @@ from ..const import (
WARNING_DEVICE_STROBE_HIGH, WARNING_DEVICE_STROBE_HIGH,
WARNING_DEVICE_STROBE_YES, WARNING_DEVICE_STROBE_YES,
) )
from .base import ChannelStatus, ZigbeeChannel
if TYPE_CHECKING: 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_ARM = 0x0000 # ("arm", (t.enum8, t.CharacterString, t.uint8_t), False),
IAS_ACE_BYPASS = 0x0001 # ("bypass", (t.LVList(t.uint8_t), t.CharacterString), 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" SIGNAL_ALARM_TRIGGERED = "zha_armed_triggered"
@registries.ZIGBEE_CHANNEL_REGISTRY.register(AceCluster.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(AceCluster.cluster_id)
class IasAce(ZigbeeChannel): class IasAce(ClusterHandler):
"""IAS Ancillary Control Equipment channel.""" """IAS Ancillary Control Equipment cluster handler."""
def __init__(self, cluster: zigpy.zcl.Cluster, ch_pool: ChannelPool) -> None: def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None:
"""Initialize IAS Ancillary Control Equipment channel.""" """Initialize IAS Ancillary Control Equipment cluster handler."""
super().__init__(cluster, ch_pool) super().__init__(cluster, endpoint)
self.command_map: dict[int, Callable[..., Any]] = { self.command_map: dict[int, Callable[..., Any]] = {
IAS_ACE_ARM: self.arm, IAS_ACE_ARM: self.arm,
IAS_ACE_BYPASS: self._bypass, IAS_ACE_BYPASS: self._bypass,
@ -105,7 +105,7 @@ class IasAce(ZigbeeChannel):
) )
zigbee_reply = self.arm_map[mode](code) 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: if self.invalid_tries >= self.max_invalid_tries:
self.alarm_status = AceCluster.AlarmStatus.Emergency self.alarm_status = AceCluster.AlarmStatus.Emergency
@ -228,7 +228,7 @@ class IasAce(ZigbeeChannel):
AceCluster.AudibleNotification.Default_Sound, AceCluster.AudibleNotification.Default_Sound,
self.alarm_status, 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: def _send_panel_status_changed(self) -> None:
"""Handle the IAS ACE panel status changed command.""" """Handle the IAS ACE panel status changed command."""
@ -238,7 +238,7 @@ class IasAce(ZigbeeChannel):
AceCluster.AudibleNotification.Default_Sound, AceCluster.AudibleNotification.Default_Sound,
self.alarm_status, 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): def _get_bypassed_zone_list(self):
"""Handle the IAS ACE bypassed zone list command.""" """Handle the IAS ACE bypassed zone list command."""
@ -249,10 +249,10 @@ class IasAce(ZigbeeChannel):
"""Handle the IAS ACE zone status command.""" """Handle the IAS ACE zone status command."""
@registries.CHANNEL_ONLY_CLUSTERS.register(security.IasWd.cluster_id) @registries.CLUSTER_HANDLER_ONLY_CLUSTERS.register(security.IasWd.cluster_id)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(security.IasWd.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(security.IasWd.cluster_id)
class IasWd(ZigbeeChannel): class IasWd(ClusterHandler):
"""IAS Warning Device channel.""" """IAS Warning Device cluster handler."""
@staticmethod @staticmethod
def set_bit(destination_value, destination_bit, source_value, source_bit): 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) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(IasZone.cluster_id)
class IASZoneChannel(ZigbeeChannel): class IASZoneClusterHandler(ClusterHandler):
"""Channel for the IASZone Zigbee cluster.""" """Cluster handler for the IASZone Zigbee cluster."""
ZCL_INIT_ATTRS = {"zone_status": False, "zone_state": True, "zone_type": True} ZCL_INIT_ATTRS = {"zone_status": False, "zone_state": True, "zone_type": True}
@ -356,11 +356,11 @@ class IASZoneChannel(ZigbeeChannel):
async def async_configure(self): async def async_configure(self):
"""Configure IAS device.""" """Configure IAS device."""
await self.get_attribute_value("zone_type", from_cache=False) await self.get_attribute_value("zone_type", from_cache=False)
if self._ch_pool.skip_configuration: if self._endpoint.device.skip_configuration:
self.debug("skipping IASZoneChannel configuration") self.debug("skipping IASZoneClusterHandler configuration")
return return
self.debug("started IASZoneChannel configuration") self.debug("started IASZoneClusterHandler configuration")
await self.bind() await self.bind()
ieee = self.cluster.endpoint.device.application.state.node_info.ieee 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.debug("Sending pro-active IAS enroll response")
self._cluster.create_catching_task(self._cluster.enroll_response(0, 0)) self._cluster.create_catching_task(self._cluster.enroll_response(0, 0))
self._status = ChannelStatus.CONFIGURED self._status = ClusterHandlerStatus.CONFIGURED
self.debug("finished IASZoneChannel configuration") self.debug("finished IASZoneClusterHandler configuration")
@callback @callback
def attribute_updated(self, attrid, value): def attribute_updated(self, attrid, value):

View File

@ -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 from __future__ import annotations
import enum import enum
@ -8,6 +8,7 @@ from typing import TYPE_CHECKING
import zigpy.zcl import zigpy.zcl
from zigpy.zcl.clusters import smartenergy from zigpy.zcl.clusters import smartenergy
from . import AttrReportConfig, ClusterHandler
from .. import registries from .. import registries
from ..const import ( from ..const import (
REPORT_CONFIG_ASAP, REPORT_CONFIG_ASAP,
@ -15,55 +16,60 @@ from ..const import (
REPORT_CONFIG_OP, REPORT_CONFIG_OP,
SIGNAL_ATTR_UPDATED, SIGNAL_ATTR_UPDATED,
) )
from .base import AttrReportConfig, ZigbeeChannel
if TYPE_CHECKING: if TYPE_CHECKING:
from . import ChannelPool from ..endpoint import Endpoint
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Calendar.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(smartenergy.Calendar.cluster_id)
class Calendar(ZigbeeChannel): class Calendar(ClusterHandler):
"""Calendar channel.""" """Calendar cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.DeviceManagement.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
class DeviceManagement(ZigbeeChannel): smartenergy.DeviceManagement.cluster_id
"""Device Management channel.""" )
class DeviceManagement(ClusterHandler):
"""Device Management cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Drlc.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(smartenergy.Drlc.cluster_id)
class Drlc(ZigbeeChannel): class Drlc(ClusterHandler):
"""Demand Response and Load Control channel.""" """Demand Response and Load Control cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.EnergyManagement.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
class EnergyManagement(ZigbeeChannel): smartenergy.EnergyManagement.cluster_id
"""Energy Management channel.""" )
class EnergyManagement(ClusterHandler):
"""Energy Management cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Events.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(smartenergy.Events.cluster_id)
class Events(ZigbeeChannel): class Events(ClusterHandler):
"""Event channel.""" """Event cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.KeyEstablishment.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(
class KeyEstablishment(ZigbeeChannel): smartenergy.KeyEstablishment.cluster_id
"""Key Establishment channel.""" )
class KeyEstablishment(ClusterHandler):
"""Key Establishment cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.MduPairing.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(smartenergy.MduPairing.cluster_id)
class MduPairing(ZigbeeChannel): class MduPairing(ClusterHandler):
"""Pairing channel.""" """Pairing cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Messaging.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(smartenergy.Messaging.cluster_id)
class Messaging(ZigbeeChannel): class Messaging(ClusterHandler):
"""Messaging channel.""" """Messaging cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Metering.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(smartenergy.Metering.cluster_id)
class Metering(ZigbeeChannel): class Metering(ClusterHandler):
"""Metering channel.""" """Metering cluster handler."""
REPORT_CONFIG = ( REPORT_CONFIG = (
AttrReportConfig(attr="instantaneous_demand", config=REPORT_CONFIG_OP), AttrReportConfig(attr="instantaneous_demand", config=REPORT_CONFIG_OP),
@ -137,9 +143,9 @@ class Metering(ZigbeeChannel):
DEMAND = 0 DEMAND = 0
SUMMATION = 1 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.""" """Initialize Metering."""
super().__init__(cluster, ch_pool) super().__init__(cluster, endpoint)
self._format_spec: str | None = None self._format_spec: str | None = None
self._summa_format: str | None = None self._summa_format: str | None = None
@ -176,7 +182,7 @@ class Metering(ZigbeeChannel):
"""Return unit of measurement.""" """Return unit of measurement."""
return self.cluster.get("unit_of_measure") 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.""" """Fetch config from device and updates format specifier."""
fmting = self.cluster.get( fmting = self.cluster.get(
@ -249,16 +255,16 @@ class Metering(ZigbeeChannel):
summa_formatter = partialmethod(_formatter_function, FormatSelector.SUMMATION) summa_formatter = partialmethod(_formatter_function, FormatSelector.SUMMATION)
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Prepayment.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(smartenergy.Prepayment.cluster_id)
class Prepayment(ZigbeeChannel): class Prepayment(ClusterHandler):
"""Prepayment channel.""" """Prepayment cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Price.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(smartenergy.Price.cluster_id)
class Price(ZigbeeChannel): class Price(ClusterHandler):
"""Price channel.""" """Price cluster handler."""
@registries.ZIGBEE_CHANNEL_REGISTRY.register(smartenergy.Tunneling.cluster_id) @registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.register(smartenergy.Tunneling.cluster_id)
class Tunneling(ZigbeeChannel): class Tunneling(ClusterHandler):
"""Tunneling channel.""" """Tunneling cluster handler."""

View File

@ -64,39 +64,39 @@ ATTR_WARNING_DEVICE_STROBE_INTENSITY = "intensity"
BAUD_RATES = [2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200, 128000, 256000] BAUD_RATES = [2400, 4800, 9600, 14400, 19200, 38400, 57600, 115200, 128000, 256000]
BINDINGS = "bindings" BINDINGS = "bindings"
CHANNEL_ACCELEROMETER = "accelerometer" CLUSTER_HANDLER_ACCELEROMETER = "accelerometer"
CHANNEL_BINARY_INPUT = "binary_input" CLUSTER_HANDLER_BINARY_INPUT = "binary_input"
CHANNEL_ANALOG_INPUT = "analog_input" CLUSTER_HANDLER_ANALOG_INPUT = "analog_input"
CHANNEL_ANALOG_OUTPUT = "analog_output" CLUSTER_HANDLER_ANALOG_OUTPUT = "analog_output"
CHANNEL_ATTRIBUTE = "attribute" CLUSTER_HANDLER_ATTRIBUTE = "attribute"
CHANNEL_BASIC = "basic" CLUSTER_HANDLER_BASIC = "basic"
CHANNEL_COLOR = "light_color" CLUSTER_HANDLER_COLOR = "light_color"
CHANNEL_COVER = "window_covering" CLUSTER_HANDLER_COVER = "window_covering"
CHANNEL_DEVICE_TEMPERATURE = "device_temperature" CLUSTER_HANDLER_DEVICE_TEMPERATURE = "device_temperature"
CHANNEL_DOORLOCK = "door_lock" CLUSTER_HANDLER_DOORLOCK = "door_lock"
CHANNEL_ELECTRICAL_MEASUREMENT = "electrical_measurement" CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT = "electrical_measurement"
CHANNEL_EVENT_RELAY = "event_relay" CLUSTER_HANDLER_EVENT_RELAY = "event_relay"
CHANNEL_FAN = "fan" CLUSTER_HANDLER_FAN = "fan"
CHANNEL_HUMIDITY = "humidity" CLUSTER_HANDLER_HUMIDITY = "humidity"
CHANNEL_SOIL_MOISTURE = "soil_moisture" CLUSTER_HANDLER_SOIL_MOISTURE = "soil_moisture"
CHANNEL_LEAF_WETNESS = "leaf_wetness" CLUSTER_HANDLER_LEAF_WETNESS = "leaf_wetness"
CHANNEL_IAS_ACE = "ias_ace" CLUSTER_HANDLER_IAS_ACE = "ias_ace"
CHANNEL_IAS_WD = "ias_wd" CLUSTER_HANDLER_IAS_WD = "ias_wd"
CHANNEL_IDENTIFY = "identify" CLUSTER_HANDLER_IDENTIFY = "identify"
CHANNEL_ILLUMINANCE = "illuminance" CLUSTER_HANDLER_ILLUMINANCE = "illuminance"
CHANNEL_LEVEL = ATTR_LEVEL CLUSTER_HANDLER_LEVEL = ATTR_LEVEL
CHANNEL_MULTISTATE_INPUT = "multistate_input" CLUSTER_HANDLER_MULTISTATE_INPUT = "multistate_input"
CHANNEL_OCCUPANCY = "occupancy" CLUSTER_HANDLER_OCCUPANCY = "occupancy"
CHANNEL_ON_OFF = "on_off" CLUSTER_HANDLER_ON_OFF = "on_off"
CHANNEL_POWER_CONFIGURATION = "power" CLUSTER_HANDLER_POWER_CONFIGURATION = "power"
CHANNEL_PRESSURE = "pressure" CLUSTER_HANDLER_PRESSURE = "pressure"
CHANNEL_SHADE = "shade" CLUSTER_HANDLER_SHADE = "shade"
CHANNEL_SMARTENERGY_METERING = "smartenergy_metering" CLUSTER_HANDLER_SMARTENERGY_METERING = "smartenergy_metering"
CHANNEL_TEMPERATURE = "temperature" CLUSTER_HANDLER_TEMPERATURE = "temperature"
CHANNEL_THERMOSTAT = "thermostat" CLUSTER_HANDLER_THERMOSTAT = "thermostat"
CHANNEL_ZDO = "zdo" CLUSTER_HANDLER_ZDO = "zdo"
CHANNEL_ZONE = ZONE = "ias_zone" CLUSTER_HANDLER_ZONE = ZONE = "ias_zone"
CHANNEL_INOVELLI = "inovelli_vzm31sn_cluster" CLUSTER_HANDLER_INOVELLI = "inovelli_vzm31sn_cluster"
CLUSTER_COMMAND_SERVER = "server" CLUSTER_COMMAND_SERVER = "server"
CLUSTER_COMMANDS_CLIENT = "client_commands" CLUSTER_COMMANDS_CLIENT = "client_commands"
@ -330,15 +330,15 @@ REPORT_CONFIG_OP = (
SENSOR_ACCELERATION = "acceleration" SENSOR_ACCELERATION = "acceleration"
SENSOR_BATTERY = "battery" SENSOR_BATTERY = "battery"
SENSOR_ELECTRICAL_MEASUREMENT = CHANNEL_ELECTRICAL_MEASUREMENT SENSOR_ELECTRICAL_MEASUREMENT = CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT
SENSOR_GENERIC = "generic" SENSOR_GENERIC = "generic"
SENSOR_HUMIDITY = CHANNEL_HUMIDITY SENSOR_HUMIDITY = CLUSTER_HANDLER_HUMIDITY
SENSOR_ILLUMINANCE = CHANNEL_ILLUMINANCE SENSOR_ILLUMINANCE = CLUSTER_HANDLER_ILLUMINANCE
SENSOR_METERING = "metering" SENSOR_METERING = "metering"
SENSOR_OCCUPANCY = CHANNEL_OCCUPANCY SENSOR_OCCUPANCY = CLUSTER_HANDLER_OCCUPANCY
SENSOR_OPENING = "opening" SENSOR_OPENING = "opening"
SENSOR_PRESSURE = CHANNEL_PRESSURE SENSOR_PRESSURE = CLUSTER_HANDLER_PRESSURE
SENSOR_TEMPERATURE = CHANNEL_TEMPERATURE SENSOR_TEMPERATURE = CLUSTER_HANDLER_TEMPERATURE
SENSOR_TYPE = "sensor_type" SENSOR_TYPE = "sensor_type"
SIGNAL_ADD_ENTITIES = "zha_add_new_entities" SIGNAL_ADD_ENTITIES = "zha_add_new_entities"
@ -381,12 +381,12 @@ WARNING_DEVICE_SQUAWK_MODE_ARMED = 0
WARNING_DEVICE_SQUAWK_MODE_DISARMED = 1 WARNING_DEVICE_SQUAWK_MODE_DISARMED = 1
ZHA_DISCOVERY_NEW = "zha_discovery_new_{}" ZHA_DISCOVERY_NEW = "zha_discovery_new_{}"
ZHA_CHANNEL_MSG = "zha_channel_message" ZHA_CLUSTER_HANDLER_MSG = "zha_channel_message"
ZHA_CHANNEL_MSG_BIND = "zha_channel_bind" ZHA_CLUSTER_HANDLER_MSG_BIND = "zha_channel_bind"
ZHA_CHANNEL_MSG_CFG_RPT = "zha_channel_configure_reporting" ZHA_CLUSTER_HANDLER_MSG_CFG_RPT = "zha_channel_configure_reporting"
ZHA_CHANNEL_MSG_DATA = "zha_channel_msg_data" ZHA_CLUSTER_HANDLER_MSG_DATA = "zha_channel_msg_data"
ZHA_CHANNEL_CFG_DONE = "zha_channel_cfg_done" ZHA_CLUSTER_HANDLER_CFG_DONE = "zha_channel_cfg_done"
ZHA_CHANNEL_READS_PER_REQ = 5 ZHA_CLUSTER_HANDLER_READS_PER_REQ = 5
ZHA_EVENT = "zha_event" ZHA_EVENT = "zha_event"
ZHA_GW_MSG = "zha_gateway_message" ZHA_GW_MSG = "zha_gateway_message"
ZHA_GW_MSG_DEVICE_FULL_INIT = "device_fully_initialized" ZHA_GW_MSG_DEVICE_FULL_INIT = "device_fully_initialized"

View File

@ -13,10 +13,10 @@ class DictRegistry(dict[int | str, _TypeT]):
def register(self, name: int | str) -> Callable[[_TypeT], _TypeT]: def register(self, name: int | str) -> Callable[[_TypeT], _TypeT]:
"""Return decorator to register item with a specific name.""" """Return decorator to register item with a specific name."""
def decorator(channel: _TypeT) -> _TypeT: def decorator(cluster_handler: _TypeT) -> _TypeT:
"""Register decorated channel or item.""" """Register decorated cluster handler or item."""
self[name] = channel self[name] = cluster_handler
return channel return cluster_handler
return decorator return decorator
@ -27,9 +27,9 @@ class SetRegistry(set[int | str]):
def register(self, name: int | str) -> Callable[[_TypeT], _TypeT]: def register(self, name: int | str) -> Callable[[_TypeT], _TypeT]:
"""Return decorator to register item with a specific name.""" """Return decorator to register item with a specific name."""
def decorator(channel: _TypeT) -> _TypeT: def decorator(cluster_handler: _TypeT) -> _TypeT:
"""Register decorated channel or item.""" """Register decorated cluster handler or item."""
self.add(name) self.add(name)
return channel return cluster_handler
return decorator return decorator

View File

@ -23,7 +23,7 @@ from zigpy.zcl.clusters.general import Groups, Identify
from zigpy.zcl.foundation import Status as ZclStatus, ZCLCommandDef from zigpy.zcl.foundation import Status as ZclStatus, ZCLCommandDef
import zigpy.zdo.types as zdo_types 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.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
@ -32,7 +32,8 @@ from homeassistant.helpers.dispatcher import (
) )
from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.event import async_track_time_interval
from . import channels from . import const
from .cluster_handlers import ClusterHandler, ZDOClusterHandler
from .const import ( from .const import (
ATTR_ACTIVE_COORDINATOR, ATTR_ACTIVE_COORDINATOR,
ATTR_ARGS, ATTR_ARGS,
@ -81,6 +82,7 @@ from .const import (
UNKNOWN_MODEL, UNKNOWN_MODEL,
ZHA_OPTIONS, ZHA_OPTIONS,
) )
from .endpoint import Endpoint
from .helpers import LogMixin, async_get_zha_config_value, convert_to_zcl_values from .helpers import LogMixin, async_get_zha_config_value, convert_to_zcl_values
if TYPE_CHECKING: if TYPE_CHECKING:
@ -139,14 +141,26 @@ class ZHADevice(LogMixin):
CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY, CONF_DEFAULT_CONSIDER_UNAVAILABLE_BATTERY,
) )
self._zdo_handler: ZDOClusterHandler = ZDOClusterHandler(self)
self._power_config_ch: ClusterHandler | None = None
self._identify_ch: ClusterHandler | None = None
self._basic_ch: ClusterHandler | None = None
self.status: DeviceStatus = DeviceStatus.CREATED
self._endpoints: dict[int, Endpoint] = {}
for ep_id, endpoint in zigpy_device.endpoints.items():
if ep_id != 0:
self._endpoints[ep_id] = Endpoint.new(endpoint, self)
if not self.is_coordinator:
keep_alive_interval = random.randint(*_UPDATE_ALIVE_INTERVAL) keep_alive_interval = random.randint(*_UPDATE_ALIVE_INTERVAL)
self.unsubs.append( self.unsubs.append(
async_track_time_interval( async_track_time_interval(
self.hass, self._check_available, timedelta(seconds=keep_alive_interval) self.hass,
self._check_available,
timedelta(seconds=keep_alive_interval),
) )
) )
self.status: DeviceStatus = DeviceStatus.CREATED
self._channels = channels.Channels(self)
@property @property
def device_id(self) -> str: def device_id(self) -> str:
@ -162,17 +176,6 @@ class ZHADevice(LogMixin):
"""Return underlying Zigpy device.""" """Return underlying Zigpy device."""
return self._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 @property
def name(self) -> str: def name(self) -> str:
"""Return device name.""" """Return device name."""
@ -335,12 +338,62 @@ class ZHADevice(LogMixin):
"""Set device availability.""" """Set device availability."""
self._available = new_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 @property
def zigbee_signature(self) -> dict[str, Any]: def zigbee_signature(self) -> dict[str, Any]:
"""Get zigbee signature for this device.""" """Get zigbee signature for this device."""
return { return {
ATTR_NODE_DESCRIPTOR: str(self._zigpy_device.node_desc), 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 @classmethod
@ -353,11 +406,10 @@ class ZHADevice(LogMixin):
) -> Self: ) -> Self:
"""Create new device.""" """Create new device."""
zha_dev = cls(hass, zigpy_dev, gateway) zha_dev = cls(hass, zigpy_dev, gateway)
zha_dev.channels = channels.Channels.new(zha_dev)
zha_dev.unsubs.append( zha_dev.unsubs.append(
async_dispatcher_connect( async_dispatcher_connect(
hass, 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, zha_dev.async_update_sw_build_id,
) )
) )
@ -393,7 +445,7 @@ class ZHADevice(LogMixin):
if ( if (
self._checkins_missed_count >= _CHECKIN_GRACE_PERIODS self._checkins_missed_count >= _CHECKIN_GRACE_PERIODS
or self.manufacturer == "LUMI" or self.manufacturer == "LUMI"
or not self._channels.pools or not self._endpoints
): ):
self.debug( self.debug(
( (
@ -410,14 +462,13 @@ class ZHADevice(LogMixin):
"Attempting to checkin with device - missed checkins: %s", "Attempting to checkin with device - missed checkins: %s",
self._checkins_missed_count, self._checkins_missed_count,
) )
try: if not self.basic_ch:
pool = self._channels.pools[0]
basic_ch = pool.all_channels[f"{pool.id}:0x0000"]
except KeyError:
self.debug("does not have a mandatory basic cluster") self.debug("does not have a mandatory basic cluster")
self.update_available(False) self.update_available(False)
return 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: if res is not None:
self._checkins_missed_count = 0 self._checkins_missed_count = 0
@ -435,22 +486,35 @@ class ZHADevice(LogMixin):
availability_changed = self.available ^ available availability_changed = self.available ^ available
self.available = available self.available = available
if availability_changed and available: if availability_changed and available:
# reinit channels then signal entities # reinit cluster handlers then signal entities
self.debug( self.debug(
"Device availability changed and device became available," "Device availability changed and device became available,"
" reinitializing channels" " reinitializing cluster handlers"
) )
self.hass.async_create_task(self._async_became_available()) self.hass.async_create_task(self._async_became_available())
return return
if availability_changed and not available: if availability_changed and not available:
self.debug("Device availability changed and device became unavailable") self.debug("Device availability changed and device became unavailable")
self._channels.zha_send_event( self.zha_send_event(
{ {
"device_event_type": "device_offline", "device_event_type": "device_offline",
}, },
) )
async_dispatcher_send(self.hass, f"{self._available_signal}_entity") 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: async def _async_became_available(self) -> None:
"""Update device availability and signal entities.""" """Update device availability and signal entities."""
await self.async_initialize(False) await self.async_initialize(False)
@ -489,23 +553,41 @@ class ZHADevice(LogMixin):
True, True,
) )
self.debug("started configuration") 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") self.debug("completed configuration")
if ( if (
should_identify should_identify
and self._channels.identify_ch is not None and self.identify_ch is not None
and not self.skip_configuration and not self.skip_configuration
): ):
await self._channels.identify_ch.trigger_effect( await self.identify_ch.trigger_effect(
effect_id=Identify.EffectIdentifier.Okay, effect_id=Identify.EffectIdentifier.Okay,
effect_variant=Identify.EffectVariant.Default, effect_variant=Identify.EffectVariant.Default,
) )
async def async_initialize(self, from_cache: bool = False) -> None: async def async_initialize(self, from_cache: bool = False) -> None:
"""Initialize channels.""" """Initialize cluster handlers."""
self.debug("started initialization") 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.debug("power source: %s", self.power_source)
self.status = DeviceStatus.INITIALIZED self.status = DeviceStatus.INITIALIZED
self.debug("completed initialization") self.debug("completed initialization")

View File

@ -33,12 +33,27 @@ from .. import ( # noqa: F401 pylint: disable=unused-import,
siren, siren,
switch, 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: if TYPE_CHECKING:
from ..entity import ZhaEntity from ..entity import ZhaEntity
from .channels import ChannelPool
from .device import ZHADevice from .device import ZHADevice
from .endpoint import Endpoint
from .gateway import ZHAGateway from .gateway import ZHAGateway
from .group import ZHAGroup from .group import ZHAGroup
@ -51,7 +66,7 @@ async def async_add_entities(
entities: list[ entities: list[
tuple[ tuple[
type[ZhaEntity], type[ZhaEntity],
tuple[str, ZHADevice, list[base.ZigbeeChannel]], tuple[str, ZHADevice, list[ClusterHandler]],
] ]
], ],
) -> None: ) -> None:
@ -65,49 +80,56 @@ async def async_add_entities(
class ProbeEndpoint: class ProbeEndpoint:
"""All discovered channels and entities of an endpoint.""" """All discovered cluster handlers and entities of an endpoint."""
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize instance.""" """Initialize instance."""
self._device_configs: ConfigType = {} self._device_configs: ConfigType = {}
@callback @callback
def discover_entities(self, channel_pool: ChannelPool) -> None: def discover_entities(self, endpoint: Endpoint) -> None:
"""Process an endpoint on a zigpy device.""" """Process an endpoint on a zigpy device."""
self.discover_by_device_type(channel_pool) _LOGGER.debug(
self.discover_multi_entities(channel_pool) "Discovering entities for endpoint: %s-%s",
self.discover_by_cluster_id(channel_pool) str(endpoint.device.ieee),
self.discover_multi_entities(channel_pool, config_diagnostic_entities=True) 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() zha_regs.ZHA_ENTITIES.clean_up()
@callback @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.""" """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) platform: str | None = self._device_configs.get(unique_id, {}).get(CONF_TYPE)
if component is None: if platform is None:
ep_profile_id = channel_pool.endpoint.profile_id ep_profile_id = endpoint.zigpy_endpoint.profile_id
ep_device_type = channel_pool.endpoint.device_type ep_device_type = endpoint.zigpy_endpoint.device_type
component = zha_regs.DEVICE_CLASS[ep_profile_id].get(ep_device_type) platform = zha_regs.DEVICE_CLASS[ep_profile_id].get(ep_device_type)
if component and component in zha_const.PLATFORMS: if platform and platform in zha_const.PLATFORMS:
channels = channel_pool.unclaimed_channels() cluster_handlers = endpoint.unclaimed_cluster_handlers()
entity_class, claimed = zha_regs.ZHA_ENTITIES.get_entity( platform_entity_class, claimed = zha_regs.ZHA_ENTITIES.get_entity(
component, platform,
channel_pool.manufacturer, endpoint.device.manufacturer,
channel_pool.model, endpoint.device.model,
channels, cluster_handlers,
channel_pool.quirk_class, endpoint.device.quirk_class,
) )
if entity_class is None: if platform_entity_class is None:
return return
channel_pool.claim_channels(claimed) endpoint.claim_cluster_handlers(claimed)
channel_pool.async_new_entity(component, entity_class, unique_id, claimed) endpoint.async_new_entity(
platform, platform_entity_class, unique_id, claimed
)
@callback @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.""" """Process an endpoint on a zigpy device."""
items = zha_regs.SINGLE_INPUT_CLUSTER_DEVICE_CLASS.items() items = zha_regs.SINGLE_INPUT_CLUSTER_DEVICE_CLASS.items()
@ -116,124 +138,127 @@ class ProbeEndpoint:
for cluster_class, match in items for cluster_class, match in items
if not isinstance(cluster_class, int) if not isinstance(cluster_class, int)
} }
remaining_channels = channel_pool.unclaimed_channels() remaining_cluster_handlers = endpoint.unclaimed_cluster_handlers()
for channel in remaining_channels: for cluster_handler in remaining_cluster_handlers:
if channel.cluster.cluster_id in zha_regs.CHANNEL_ONLY_CLUSTERS: if (
channel_pool.claim_channels([channel]) cluster_handler.cluster.cluster_id
in zha_regs.CLUSTER_HANDLER_ONLY_CLUSTERS
):
endpoint.claim_cluster_handlers([cluster_handler])
continue continue
component = zha_regs.SINGLE_INPUT_CLUSTER_DEVICE_CLASS.get( platform = zha_regs.SINGLE_INPUT_CLUSTER_DEVICE_CLASS.get(
channel.cluster.cluster_id cluster_handler.cluster.cluster_id
) )
if component is None: if platform is None:
for cluster_class, match in single_input_clusters.items(): for cluster_class, match in single_input_clusters.items():
if isinstance(channel.cluster, cluster_class): if isinstance(cluster_handler.cluster, cluster_class):
component = match platform = match
break break
self.probe_single_cluster(component, channel, channel_pool) self.probe_single_cluster(platform, cluster_handler, endpoint)
# until we can get rid of registries # 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 @staticmethod
def probe_single_cluster( def probe_single_cluster(
component: Platform | None, platform: Platform | None,
channel: base.ZigbeeChannel, cluster_handler: ClusterHandler,
ep_channels: ChannelPool, endpoint: Endpoint,
) -> None: ) -> None:
"""Probe specified cluster for specific component.""" """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 return
channel_list = [channel] cluster_handler_list = [cluster_handler]
unique_id = f"{ep_channels.unique_id}-{channel.cluster.cluster_id}" unique_id = f"{endpoint.unique_id}-{cluster_handler.cluster.cluster_id}"
entity_class, claimed = zha_regs.ZHA_ENTITIES.get_entity( entity_class, claimed = zha_regs.ZHA_ENTITIES.get_entity(
component, platform,
ep_channels.manufacturer, endpoint.device.manufacturer,
ep_channels.model, endpoint.device.model,
channel_list, cluster_handler_list,
ep_channels.quirk_class, endpoint.device.quirk_class,
) )
if entity_class is None: if entity_class is None:
return return
ep_channels.claim_channels(claimed) endpoint.claim_cluster_handlers(claimed)
ep_channels.async_new_entity(component, entity_class, unique_id, 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.""" """Process output clusters of the endpoint."""
profile_id = ep_channels.endpoint.profile_id profile_id = endpoint.zigpy_endpoint.profile_id
device_type = ep_channels.endpoint.device_type device_type = endpoint.zigpy_endpoint.device_type
if device_type in zha_regs.REMOTE_DEVICE_TYPES.get(profile_id, []): if device_type in zha_regs.REMOTE_DEVICE_TYPES.get(profile_id, []):
return return
for cluster_id, cluster in ep_channels.endpoint.out_clusters.items(): for cluster_id, cluster in endpoint.zigpy_endpoint.out_clusters.items():
component = zha_regs.SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.get( platform = zha_regs.SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.get(
cluster.cluster_id cluster.cluster_id
) )
if component is None: if platform is None:
continue continue
channel_class = zha_regs.ZIGBEE_CHANNEL_REGISTRY.get( cluster_handler_class = zha_regs.ZIGBEE_CLUSTER_HANDLER_REGISTRY.get(
cluster_id, base.ZigbeeChannel cluster_id, ClusterHandler
) )
channel = channel_class(cluster, ep_channels) cluster_handler = cluster_handler_class(cluster, endpoint)
self.probe_single_cluster(component, channel, ep_channels) self.probe_single_cluster(platform, cluster_handler, endpoint)
@staticmethod @staticmethod
@callback @callback
def discover_multi_entities( def discover_multi_entities(
channel_pool: ChannelPool, endpoint: Endpoint,
config_diagnostic_entities: bool = False, config_diagnostic_entities: bool = False,
) -> None: ) -> None:
"""Process an endpoint on and discover multiple entities.""" """Process an endpoint on and discover multiple entities."""
ep_profile_id = channel_pool.endpoint.profile_id ep_profile_id = endpoint.zigpy_endpoint.profile_id
ep_device_type = channel_pool.endpoint.device_type ep_device_type = endpoint.zigpy_endpoint.device_type
cmpt_by_dev_type = zha_regs.DEVICE_CLASS[ep_profile_id].get(ep_device_type) cmpt_by_dev_type = zha_regs.DEVICE_CLASS[ep_profile_id].get(ep_device_type)
if config_diagnostic_entities: if config_diagnostic_entities:
matches, claimed = zha_regs.ZHA_ENTITIES.get_config_diagnostic_entity( matches, claimed = zha_regs.ZHA_ENTITIES.get_config_diagnostic_entity(
channel_pool.manufacturer, endpoint.device.manufacturer,
channel_pool.model, endpoint.device.model,
list(channel_pool.all_channels.values()), list(endpoint.all_cluster_handlers.values()),
channel_pool.quirk_class, endpoint.device.quirk_class,
) )
else: else:
matches, claimed = zha_regs.ZHA_ENTITIES.get_multi_entity( matches, claimed = zha_regs.ZHA_ENTITIES.get_multi_entity(
channel_pool.manufacturer, endpoint.device.manufacturer,
channel_pool.model, endpoint.device.model,
channel_pool.unclaimed_channels(), endpoint.unclaimed_cluster_handlers(),
channel_pool.quirk_class, endpoint.device.quirk_class,
) )
channel_pool.claim_channels(claimed) endpoint.claim_cluster_handlers(claimed)
for component, ent_n_chan_list in matches.items(): for platform, ent_n_handler_list in matches.items():
for entity_and_channel in ent_n_chan_list: for entity_and_handler in ent_n_handler_list:
_LOGGER.debug( _LOGGER.debug(
"'%s' component -> '%s' using %s", "'%s' component -> '%s' using %s",
component, platform,
entity_and_channel.entity_class.__name__, entity_and_handler.entity_class.__name__,
[ch.name for ch in entity_and_channel.claimed_channel], [ch.name for ch in entity_and_handler.claimed_cluster_handlers],
) )
for component, ent_n_chan_list in matches.items(): for platform, ent_n_handler_list in matches.items():
for entity_and_channel in ent_n_chan_list: for entity_and_handler in ent_n_handler_list:
if component == cmpt_by_dev_type: if platform == cmpt_by_dev_type:
# for well known device types, like thermostats we'll take only 1st class # for well known device types, like thermostats we'll take only 1st class
channel_pool.async_new_entity( endpoint.async_new_entity(
component, platform,
entity_and_channel.entity_class, entity_and_handler.entity_class,
channel_pool.unique_id, endpoint.unique_id,
entity_and_channel.claimed_channel, entity_and_handler.claimed_cluster_handlers,
) )
break break
first_ch = entity_and_channel.claimed_channel[0] first_ch = entity_and_handler.claimed_cluster_handlers[0]
channel_pool.async_new_entity( endpoint.async_new_entity(
component, platform,
entity_and_channel.entity_class, entity_and_handler.entity_class,
f"{channel_pool.unique_id}-{first_ch.cluster.cluster_id}", f"{endpoint.unique_id}-{first_ch.cluster.cluster_id}",
entity_and_channel.claimed_channel, entity_and_handler.claimed_cluster_handlers,
) )
def initialize(self, hass: HomeAssistant) -> None: def initialize(self, hass: HomeAssistant) -> None:

View 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)
]

View File

@ -93,7 +93,7 @@ if TYPE_CHECKING:
from logging import Filter, LogRecord from logging import Filter, LogRecord
from ..entity import ZhaEntity from ..entity import ZhaEntity
from .channels.base import ZigbeeChannel from .cluster_handlers import ClusterHandler
_LogFilterType = Filter | Callable[[LogRecord], bool] _LogFilterType = Filter | Callable[[LogRecord], bool]
@ -105,7 +105,7 @@ class EntityReference(NamedTuple):
reference_id: str reference_id: str
zha_device: ZHADevice zha_device: ZHADevice
cluster_channels: dict[str, ZigbeeChannel] cluster_handlers: dict[str, ClusterHandler]
device_info: DeviceInfo device_info: DeviceInfo
remove_future: asyncio.Future[Any] remove_future: asyncio.Future[Any]
@ -520,7 +520,7 @@ class ZHAGateway:
ieee: EUI64, ieee: EUI64,
reference_id: str, reference_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
cluster_channels: dict[str, ZigbeeChannel], cluster_handlers: dict[str, ClusterHandler],
device_info: DeviceInfo, device_info: DeviceInfo,
remove_future: asyncio.Future[Any], remove_future: asyncio.Future[Any],
): ):
@ -529,7 +529,7 @@ class ZHAGateway:
EntityReference( EntityReference(
reference_id=reference_id, reference_id=reference_id,
zha_device=zha_device, zha_device=zha_device,
cluster_channels=cluster_channels, cluster_handlers=cluster_handlers,
device_info=device_info, device_info=device_info,
remove_future=remove_future, remove_future=remove_future,
) )

View File

@ -89,7 +89,7 @@ class ZHAGroupMember(LogMixin):
entity_ref.reference_id, entity_ref.reference_id,
)._asdict() )._asdict()
for entity_ref in zha_device_registry.get(self.device.ieee) 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 0
].cluster.endpoint.endpoint_id ].cluster.endpoint.endpoint_id
== self.endpoint_id == self.endpoint_id

View File

@ -336,17 +336,17 @@ def retryable_req(
def decorator(func): def decorator(func):
@functools.wraps(func) @functools.wraps(func)
async def wrapper(channel, *args, **kwargs): async def wrapper(cluster_handler, *args, **kwargs):
exceptions = (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) exceptions = (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError)
try_count, errors = 1, [] try_count, errors = 1, []
for delay in itertools.chain(delays, [None]): for delay in itertools.chain(delays, [None]):
try: try:
return await func(channel, *args, **kwargs) return await func(cluster_handler, *args, **kwargs)
except exceptions as ex: except exceptions as ex:
errors.append(ex) errors.append(ex)
if delay: if delay:
delay = uniform(delay * 0.75, delay * 1.25) delay = uniform(delay * 0.75, delay * 1.25)
channel.debug( cluster_handler.debug(
"%s: retryable request #%d failed: %s. Retrying in %ss", "%s: retryable request #%d failed: %s. Retrying in %ss",
func.__name__, func.__name__,
try_count, try_count,
@ -356,7 +356,7 @@ def retryable_req(
try_count += 1 try_count += 1
await asyncio.sleep(delay) await asyncio.sleep(delay)
else: else:
channel.warning( cluster_handler.warning(
"%s: all attempts have failed: %s", func.__name__, errors "%s: all attempts have failed: %s", func.__name__, errors
) )
if raise_: if raise_:

View File

@ -14,13 +14,11 @@ from zigpy.types.named import EUI64
from homeassistant.const import Platform 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 from .decorators import DictRegistry, SetRegistry
if TYPE_CHECKING: if TYPE_CHECKING:
from ..entity import ZhaEntity, ZhaGroupEntity from ..entity import ZhaEntity, ZhaGroupEntity
from .channels.base import ClientChannel, ZigbeeChannel from .cluster_handlers import ClientClusterHandler, ClusterHandler
_ZhaEntityT = TypeVar("_ZhaEntityT", bound=type["ZhaEntity"]) _ZhaEntityT = TypeVar("_ZhaEntityT", bound=type["ZhaEntity"])
@ -75,7 +73,6 @@ SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {
} }
BINDABLE_CLUSTERS = SetRegistry() BINDABLE_CLUSTERS = SetRegistry()
CHANNEL_ONLY_CLUSTERS = SetRegistry()
DEVICE_CLASS = { DEVICE_CLASS = {
zigpy.profiles.zha.PROFILE_ID: { zigpy.profiles.zha.PROFILE_ID: {
@ -108,8 +105,11 @@ DEVICE_CLASS = {
} }
DEVICE_CLASS = collections.defaultdict(dict, DEVICE_CLASS) DEVICE_CLASS = collections.defaultdict(dict, DEVICE_CLASS)
CLIENT_CHANNELS_REGISTRY: DictRegistry[type[ClientChannel]] = DictRegistry() CLUSTER_HANDLER_ONLY_CLUSTERS = SetRegistry()
ZIGBEE_CHANNEL_REGISTRY: DictRegistry[type[ZigbeeChannel]] = DictRegistry() CLIENT_CLUSTER_HANDLER_REGISTRY: DictRegistry[
type[ClientClusterHandler]
] = DictRegistry()
ZIGBEE_CLUSTER_HANDLER_REGISTRY: DictRegistry[type[ClusterHandler]] = DictRegistry()
def set_or_callable(value) -> frozenset[str] | Callable: def set_or_callable(value) -> frozenset[str] | Callable:
@ -129,9 +129,9 @@ def _get_empty_frozenset() -> frozenset[str]:
@attr.s(frozen=True) @attr.s(frozen=True)
class MatchRule: 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 factory=frozenset, converter=set_or_callable
) )
generic_ids: 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( models: frozenset[str] | Callable = attr.ib(
factory=_get_empty_frozenset, converter=set_or_callable 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 factory=_get_empty_frozenset, converter=set_or_callable
) )
quirk_classes: frozenset[str] | Callable = attr.ib( 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 and have a priority over manufacturer matching rules and rules matching a
single model/manufacturer get a better priority over rules matching multiple single model/manufacturer get a better priority over rules matching multiple
models/manufacturers. And any model or manufacturers matching rules get better models/manufacturers. And any model or manufacturers matching rules get better
priority over rules matching only channels. priority over rules matching only cluster handlers.
But in case of a channel name/channel id matching, we give rules matching But in case of a cluster handler name/cluster handler id matching, we give rules matching
multiple channels a better priority over rules matching a single channel. multiple cluster handlers a better priority over rules matching a single cluster handler.
""" """
weight = 0 weight = 0
if self.quirk_classes: if self.quirk_classes:
@ -175,51 +175,57 @@ class MatchRule:
1 if callable(self.manufacturers) else len(self.manufacturers) 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) weight += 5 * len(self.generic_ids)
if isinstance(self.aux_channels, frozenset): if isinstance(self.aux_cluster_handlers, frozenset):
weight += 1 * len(self.aux_channels) weight += 1 * len(self.aux_cluster_handlers)
return weight return weight
def claim_channels(self, channel_pool: list[ZigbeeChannel]) -> list[ZigbeeChannel]: def claim_cluster_handlers(
"""Return a list of channels this rule matches + aux channels.""" self, cluster_handlers: list[ClusterHandler]
) -> list[ClusterHandler]:
"""Return a list of cluster handlers this rule matches + aux cluster handlers."""
claimed = [] claimed = []
if isinstance(self.channel_names, frozenset): if isinstance(self.cluster_handler_names, frozenset):
claimed.extend([ch for ch in channel_pool if ch.name in self.channel_names]) claimed.extend(
[ch for ch in cluster_handlers if ch.name in self.cluster_handler_names]
)
if isinstance(self.generic_ids, frozenset): if isinstance(self.generic_ids, frozenset):
claimed.extend( 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 return claimed
def strict_matched( 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: ) -> bool:
"""Return True if this device matches the criteria.""" """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( 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: ) -> bool:
"""Return True if this device matches the criteria.""" """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( def _matched(
self, manufacturer: str, model: str, channels: list, quirk_class: str self, manufacturer: str, model: str, cluster_handlers: list, quirk_class: str
) -> list: ) -> list:
"""Return a list of field matches.""" """Return a list of field matches."""
if not any(attr.asdict(self).values()): if not any(attr.asdict(self).values()):
return [False] return [False]
matches = [] matches = []
if self.channel_names: if self.cluster_handler_names:
channel_names = {ch.name for ch in channels} cluster_handler_names = {ch.name for ch in cluster_handlers}
matches.append(self.channel_names.issubset(channel_names)) matches.append(self.cluster_handler_names.issubset(cluster_handler_names))
if self.generic_ids: 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)) matches.append(self.generic_ids.issubset(all_generic_ids))
if self.manufacturers: if self.manufacturers:
@ -244,15 +250,15 @@ class MatchRule:
@dataclasses.dataclass @dataclasses.dataclass
class EntityClassAndChannels: class EntityClassAndClusterHandlers:
"""Container for entity class and corresponding channels.""" """Container for entity class and corresponding cluster handlers."""
entity_class: type[ZhaEntity] entity_class: type[ZhaEntity]
claimed_channel: list[ZigbeeChannel] claimed_cluster_handlers: list[ClusterHandler]
class ZHAEntityRegistry: class ZHAEntityRegistry:
"""Channel to ZHA Entity mapping.""" """Cluster handler to ZHA Entity mapping."""
def __init__(self) -> None: def __init__(self) -> None:
"""Initialize Registry instance.""" """Initialize Registry instance."""
@ -279,15 +285,15 @@ class ZHAEntityRegistry:
component: str, component: str,
manufacturer: str, manufacturer: str,
model: str, model: str,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
quirk_class: str, quirk_class: str,
default: type[ZhaEntity] | None = None, default: type[ZhaEntity] | None = None,
) -> tuple[type[ZhaEntity] | None, list[ZigbeeChannel]]: ) -> tuple[type[ZhaEntity] | None, list[ClusterHandler]]:
"""Match a ZHA Channels to a ZHA Entity class.""" """Match a ZHA ClusterHandler to a ZHA Entity class."""
matches = self._strict_registry[component] matches = self._strict_registry[component]
for match in sorted(matches, key=lambda x: x.weight, reverse=True): for match in sorted(matches, key=lambda x: x.weight, reverse=True):
if match.strict_matched(manufacturer, model, channels, quirk_class): if match.strict_matched(manufacturer, model, cluster_handlers, quirk_class):
claimed = match.claim_channels(channels) claimed = match.claim_cluster_handlers(cluster_handlers)
return self._strict_registry[component][match], claimed return self._strict_registry[component][match], claimed
return default, [] return default, []
@ -296,21 +302,27 @@ class ZHAEntityRegistry:
self, self,
manufacturer: str, manufacturer: str,
model: str, model: str,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
quirk_class: str, quirk_class: str,
) -> tuple[dict[str, list[EntityClassAndChannels]], list[ZigbeeChannel]]: ) -> tuple[dict[str, list[EntityClassAndClusterHandlers]], list[ClusterHandler]]:
"""Match ZHA Channels to potentially multiple ZHA Entity classes.""" """Match ZHA cluster handlers to potentially multiple ZHA Entity classes."""
result: dict[str, list[EntityClassAndChannels]] = collections.defaultdict(list) result: dict[
all_claimed: set[ZigbeeChannel] = set() str, list[EntityClassAndClusterHandlers]
] = collections.defaultdict(list)
all_claimed: set[ClusterHandler] = set()
for component, stop_match_groups in self._multi_entity_registry.items(): for component, stop_match_groups in self._multi_entity_registry.items():
for stop_match_grp, matches in stop_match_groups.items(): for stop_match_grp, matches in stop_match_groups.items():
sorted_matches = sorted(matches, key=lambda x: x.weight, reverse=True) sorted_matches = sorted(matches, key=lambda x: x.weight, reverse=True)
for match in sorted_matches: for match in sorted_matches:
if match.strict_matched(manufacturer, model, channels, quirk_class): if match.strict_matched(
claimed = match.claim_channels(channels) manufacturer, model, cluster_handlers, quirk_class
):
claimed = match.claim_cluster_handlers(cluster_handlers)
for ent_class in stop_match_groups[stop_match_grp][match]: for ent_class in stop_match_groups[stop_match_grp][match]:
ent_n_channels = EntityClassAndChannels(ent_class, claimed) ent_n_cluster_handlers = EntityClassAndClusterHandlers(
result[component].append(ent_n_channels) ent_class, claimed
)
result[component].append(ent_n_cluster_handlers)
all_claimed |= set(claimed) all_claimed |= set(claimed)
if stop_match_grp: if stop_match_grp:
break break
@ -321,12 +333,14 @@ class ZHAEntityRegistry:
self, self,
manufacturer: str, manufacturer: str,
model: str, model: str,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
quirk_class: str, quirk_class: str,
) -> tuple[dict[str, list[EntityClassAndChannels]], list[ZigbeeChannel]]: ) -> tuple[dict[str, list[EntityClassAndClusterHandlers]], list[ClusterHandler]]:
"""Match ZHA Channels to potentially multiple ZHA Entity classes.""" """Match ZHA cluster handlers to potentially multiple ZHA Entity classes."""
result: dict[str, list[EntityClassAndChannels]] = collections.defaultdict(list) result: dict[
all_claimed: set[ZigbeeChannel] = set() str, list[EntityClassAndClusterHandlers]
] = collections.defaultdict(list)
all_claimed: set[ClusterHandler] = set()
for ( for (
component, component,
stop_match_groups, stop_match_groups,
@ -334,11 +348,15 @@ class ZHAEntityRegistry:
for stop_match_grp, matches in stop_match_groups.items(): for stop_match_grp, matches in stop_match_groups.items():
sorted_matches = sorted(matches, key=lambda x: x.weight, reverse=True) sorted_matches = sorted(matches, key=lambda x: x.weight, reverse=True)
for match in sorted_matches: for match in sorted_matches:
if match.strict_matched(manufacturer, model, channels, quirk_class): if match.strict_matched(
claimed = match.claim_channels(channels) manufacturer, model, cluster_handlers, quirk_class
):
claimed = match.claim_cluster_handlers(cluster_handlers)
for ent_class in stop_match_groups[stop_match_grp][match]: for ent_class in stop_match_groups[stop_match_grp][match]:
ent_n_channels = EntityClassAndChannels(ent_class, claimed) ent_n_cluster_handlers = EntityClassAndClusterHandlers(
result[component].append(ent_n_channels) ent_class, claimed
)
result[component].append(ent_n_cluster_handlers)
all_claimed |= set(claimed) all_claimed |= set(claimed)
if stop_match_grp: if stop_match_grp:
break break
@ -352,21 +370,21 @@ class ZHAEntityRegistry:
def strict_match( def strict_match(
self, self,
component: str, component: str,
channel_names: set[str] | str | None = None, cluster_handler_names: set[str] | str | None = None,
generic_ids: set[str] | str | None = None, generic_ids: set[str] | str | None = None,
manufacturers: Callable | set[str] | str | None = None, manufacturers: Callable | set[str] | str | None = None,
models: 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, quirk_classes: set[str] | str | None = None,
) -> Callable[[_ZhaEntityT], _ZhaEntityT]: ) -> Callable[[_ZhaEntityT], _ZhaEntityT]:
"""Decorate a strict match rule.""" """Decorate a strict match rule."""
rule = MatchRule( rule = MatchRule(
channel_names, cluster_handler_names,
generic_ids, generic_ids,
manufacturers, manufacturers,
models, models,
aux_channels, aux_cluster_handlers,
quirk_classes, quirk_classes,
) )
@ -383,22 +401,22 @@ class ZHAEntityRegistry:
def multipass_match( def multipass_match(
self, self,
component: str, component: str,
channel_names: set[str] | str | None = None, cluster_handler_names: set[str] | str | None = None,
generic_ids: set[str] | str | None = None, generic_ids: set[str] | str | None = None,
manufacturers: Callable | set[str] | str | None = None, manufacturers: Callable | set[str] | str | None = None,
models: 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, stop_on_match_group: int | str | None = None,
quirk_classes: set[str] | str | None = None, quirk_classes: set[str] | str | None = None,
) -> Callable[[_ZhaEntityT], _ZhaEntityT]: ) -> Callable[[_ZhaEntityT], _ZhaEntityT]:
"""Decorate a loose match rule.""" """Decorate a loose match rule."""
rule = MatchRule( rule = MatchRule(
channel_names, cluster_handler_names,
generic_ids, generic_ids,
manufacturers, manufacturers,
models, models,
aux_channels, aux_cluster_handlers,
quirk_classes, quirk_classes,
) )
@ -407,7 +425,7 @@ class ZHAEntityRegistry:
All non empty fields of a match rule must match. 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( self._multi_entity_registry[component][stop_on_match_group][rule].append(
zha_entity zha_entity
) )
@ -418,22 +436,22 @@ class ZHAEntityRegistry:
def config_diagnostic_match( def config_diagnostic_match(
self, self,
component: str, component: str,
channel_names: set[str] | str | None = None, cluster_handler_names: set[str] | str | None = None,
generic_ids: set[str] | str | None = None, generic_ids: set[str] | str | None = None,
manufacturers: Callable | set[str] | str | None = None, manufacturers: Callable | set[str] | str | None = None,
models: 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, stop_on_match_group: int | str | None = None,
quirk_classes: set[str] | str | None = None, quirk_classes: set[str] | str | None = None,
) -> Callable[[_ZhaEntityT], _ZhaEntityT]: ) -> Callable[[_ZhaEntityT], _ZhaEntityT]:
"""Decorate a loose match rule.""" """Decorate a loose match rule."""
rule = MatchRule( rule = MatchRule(
channel_names, cluster_handler_names,
generic_ids, generic_ids,
manufacturers, manufacturers,
models, models,
aux_channels, aux_cluster_handlers,
quirk_classes, quirk_classes,
) )
@ -442,7 +460,7 @@ class ZHAEntityRegistry:
All non-empty fields of a match rule must match. 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][ self._config_diagnostic_entity_registry[component][stop_on_match_group][
rule rule
].append(zha_entity) ].append(zha_entity)

View File

@ -28,10 +28,10 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .core import discovery from .core import discovery
from .core.const import ( from .core.const import (
CHANNEL_COVER, CLUSTER_HANDLER_COVER,
CHANNEL_LEVEL, CLUSTER_HANDLER_LEVEL,
CHANNEL_ON_OFF, CLUSTER_HANDLER_ON_OFF,
CHANNEL_SHADE, CLUSTER_HANDLER_SHADE,
DATA_ZHA, DATA_ZHA,
SIGNAL_ADD_ENTITIES, SIGNAL_ADD_ENTITIES,
SIGNAL_ATTR_UPDATED, SIGNAL_ATTR_UPDATED,
@ -41,7 +41,7 @@ from .core.registries import ZHA_ENTITIES
from .entity import ZhaEntity from .entity import ZhaEntity
if TYPE_CHECKING: if TYPE_CHECKING:
from .core.channels.base import ZigbeeChannel from .core.cluster_handlers import ClusterHandler
from .core.device import ZHADevice from .core.device import ZHADevice
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -67,21 +67,21 @@ async def async_setup_entry(
config_entry.async_on_unload(unsub) config_entry.async_on_unload(unsub)
@MULTI_MATCH(channel_names=CHANNEL_COVER) @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_COVER)
class ZhaCover(ZhaEntity, CoverEntity): class ZhaCover(ZhaEntity, CoverEntity):
"""Representation of a ZHA cover.""" """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.""" """Init this sensor."""
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
self._cover_channel = self.cluster_channels.get(CHANNEL_COVER) self._cover_cluster_handler = self.cluster_handlers.get(CLUSTER_HANDLER_COVER)
self._current_position = None self._current_position = None
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Run when about to be added to hass.""" """Run when about to be added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
self.async_accept_signal( 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 @callback
@ -118,7 +118,7 @@ class ZhaCover(ZhaEntity, CoverEntity):
@callback @callback
def async_set_position(self, attr_id, attr_name, value): 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) _LOGGER.debug("setting position: %s", value)
self._current_position = 100 - value self._current_position = 100 - value
if self._current_position == 0: if self._current_position == 0:
@ -129,27 +129,27 @@ class ZhaCover(ZhaEntity, CoverEntity):
@callback @callback
def async_update_state(self, state): def async_update_state(self, state):
"""Handle state update from channel.""" """Handle state update from cluster handler."""
_LOGGER.debug("state=%s", state) _LOGGER.debug("state=%s", state)
self._state = state self._state = state
self.async_write_ha_state() self.async_write_ha_state()
async def async_open_cover(self, **kwargs: Any) -> None: async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the window cover.""" """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: if not isinstance(res, Exception) and res[1] is Status.SUCCESS:
self.async_update_state(STATE_OPENING) self.async_update_state(STATE_OPENING)
async def async_close_cover(self, **kwargs: Any) -> None: async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the window cover.""" """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: if not isinstance(res, Exception) and res[1] is Status.SUCCESS:
self.async_update_state(STATE_CLOSING) self.async_update_state(STATE_CLOSING)
async def async_set_cover_position(self, **kwargs: Any) -> None: async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the roller shutter to a specific position.""" """Move the roller shutter to a specific position."""
new_pos = kwargs[ATTR_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: if not isinstance(res, Exception) and res[1] is Status.SUCCESS:
self.async_update_state( self.async_update_state(
STATE_CLOSING if new_pos < self._current_position else STATE_OPENING 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: async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the window cover.""" """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: if not isinstance(res, Exception) and res[1] is Status.SUCCESS:
self._state = STATE_OPEN if self._current_position > 0 else STATE_CLOSED self._state = STATE_OPEN if self._current_position > 0 else STATE_CLOSED
self.async_write_ha_state() self.async_write_ha_state()
@ -170,8 +170,8 @@ class ZhaCover(ZhaEntity, CoverEntity):
async def async_get_state(self, from_cache=True): async def async_get_state(self, from_cache=True):
"""Fetch the current state.""" """Fetch the current state."""
_LOGGER.debug("polling current state") _LOGGER.debug("polling current state")
if self._cover_channel: if self._cover_cluster_handler:
pos = await self._cover_channel.get_attribute_value( pos = await self._cover_cluster_handler.get_attribute_value(
"current_position_lift_percentage", from_cache=from_cache "current_position_lift_percentage", from_cache=from_cache
) )
_LOGGER.debug("read pos=%s", pos) _LOGGER.debug("read pos=%s", pos)
@ -186,7 +186,13 @@ class ZhaCover(ZhaEntity, CoverEntity):
self._state = None 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): class Shade(ZhaEntity, CoverEntity):
"""ZHA Shade.""" """ZHA Shade."""
@ -196,13 +202,13 @@ class Shade(ZhaEntity, CoverEntity):
self, self,
unique_id: str, unique_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
**kwargs, **kwargs,
) -> None: ) -> None:
"""Initialize the ZHA light.""" """Initialize the ZHA light."""
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
self._on_off_channel = self.cluster_channels[CHANNEL_ON_OFF] self._on_off_cluster_handler = self.cluster_handlers[CLUSTER_HANDLER_ON_OFF]
self._level_channel = self.cluster_channels[CHANNEL_LEVEL] self._level_cluster_handler = self.cluster_handlers[CLUSTER_HANDLER_LEVEL]
self._position: int | None = None self._position: int | None = None
self._is_open: bool | None = None self._is_open: bool | None = None
@ -225,10 +231,12 @@ class Shade(ZhaEntity, CoverEntity):
"""Run when about to be added to hass.""" """Run when about to be added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
self.async_accept_signal( 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.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 @callback
@ -253,7 +261,7 @@ class Shade(ZhaEntity, CoverEntity):
async def async_open_cover(self, **kwargs: Any) -> None: async def async_open_cover(self, **kwargs: Any) -> None:
"""Open the window cover.""" """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: if isinstance(res, Exception) or res[1] != Status.SUCCESS:
self.debug("couldn't open cover: %s", res) self.debug("couldn't open cover: %s", res)
return return
@ -263,7 +271,7 @@ class Shade(ZhaEntity, CoverEntity):
async def async_close_cover(self, **kwargs: Any) -> None: async def async_close_cover(self, **kwargs: Any) -> None:
"""Close the window cover.""" """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: if isinstance(res, Exception) or res[1] != Status.SUCCESS:
self.debug("couldn't open cover: %s", res) self.debug("couldn't open cover: %s", res)
return return
@ -274,7 +282,7 @@ class Shade(ZhaEntity, CoverEntity):
async def async_set_cover_position(self, **kwargs: Any) -> None: async def async_set_cover_position(self, **kwargs: Any) -> None:
"""Move the roller shutter to a specific position.""" """Move the roller shutter to a specific position."""
new_pos = kwargs[ATTR_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 new_pos * 255 / 100, 1
) )
@ -287,14 +295,15 @@ class Shade(ZhaEntity, CoverEntity):
async def async_stop_cover(self, **kwargs: Any) -> None: async def async_stop_cover(self, **kwargs: Any) -> None:
"""Stop the cover.""" """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: if isinstance(res, Exception) or res[1] != Status.SUCCESS:
self.debug("couldn't stop cover: %s", res) self.debug("couldn't stop cover: %s", res)
return return
@MULTI_MATCH( @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): class KeenVent(Shade):
"""Keen vent cover.""" """Keen vent cover."""
@ -305,8 +314,10 @@ class KeenVent(Shade):
"""Open the cover.""" """Open the cover."""
position = self._position or 100 position = self._position or 100
tasks = [ tasks = [
self._level_channel.move_to_level_with_on_off(position * 255 / 100, 1), self._level_cluster_handler.move_to_level_with_on_off(
self._on_off_channel.on(), position * 255 / 100, 1
),
self._on_off_cluster_handler.on(),
] ]
results = await asyncio.gather(*tasks, return_exceptions=True) results = await asyncio.gather(*tasks, return_exceptions=True)
if any(isinstance(result, Exception) for result in results): if any(isinstance(result, Exception) for result in results):

View File

@ -12,8 +12,11 @@ from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.typing import ConfigType, TemplateVarsType from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from . import DOMAIN from . import DOMAIN
from .core.channels.manufacturerspecific import AllLEDEffectType, SingleLEDEffectType from .core.cluster_handlers.manufacturerspecific import (
from .core.const import CHANNEL_IAS_WD, CHANNEL_INOVELLI AllLEDEffectType,
SingleLEDEffectType,
)
from .core.const import CLUSTER_HANDLER_IAS_WD, CLUSTER_HANDLER_INOVELLI
from .core.helpers import async_get_zha_device from .core.helpers import async_get_zha_device
from .websocket_api import SERVICE_WARNING_DEVICE_SQUAWK, SERVICE_WARNING_DEVICE_WARN from .websocket_api import SERVICE_WARNING_DEVICE_SQUAWK, SERVICE_WARNING_DEVICE_WARN
@ -25,7 +28,7 @@ ATTR_DATA = "data"
ATTR_IEEE = "ieee" ATTR_IEEE = "ieee"
CONF_ZHA_ACTION_TYPE = "zha_action_type" CONF_ZHA_ACTION_TYPE = "zha_action_type"
ZHA_ACTION_TYPE_SERVICE_CALL = "service_call" 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_ALL_LED_EFFECT = "issue_all_led_effect"
INOVELLI_INDIVIDUAL_LED_EFFECT = "issue_individual_led_effect" INOVELLI_INDIVIDUAL_LED_EFFECT = "issue_individual_led_effect"
@ -67,11 +70,11 @@ ACTION_SCHEMA = vol.Any(
) )
DEVICE_ACTIONS = { DEVICE_ACTIONS = {
CHANNEL_IAS_WD: [ CLUSTER_HANDLER_IAS_WD: [
{CONF_TYPE: ACTION_SQUAWK, CONF_DOMAIN: DOMAIN}, {CONF_TYPE: ACTION_SQUAWK, CONF_DOMAIN: DOMAIN},
{CONF_TYPE: ACTION_WARN, 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_ALL_LED_EFFECT, CONF_DOMAIN: DOMAIN},
{CONF_TYPE: INOVELLI_INDIVIDUAL_LED_EFFECT, CONF_DOMAIN: DOMAIN}, {CONF_TYPE: INOVELLI_INDIVIDUAL_LED_EFFECT, CONF_DOMAIN: DOMAIN},
], ],
@ -80,8 +83,8 @@ DEVICE_ACTIONS = {
DEVICE_ACTION_TYPES = { DEVICE_ACTION_TYPES = {
ACTION_SQUAWK: ZHA_ACTION_TYPE_SERVICE_CALL, ACTION_SQUAWK: ZHA_ACTION_TYPE_SERVICE_CALL,
ACTION_WARN: ZHA_ACTION_TYPE_SERVICE_CALL, ACTION_WARN: ZHA_ACTION_TYPE_SERVICE_CALL,
INOVELLI_ALL_LED_EFFECT: ZHA_ACTION_TYPE_CHANNEL_COMMAND, INOVELLI_ALL_LED_EFFECT: ZHA_ACTION_TYPE_CLUSTER_HANDLER_COMMAND,
INOVELLI_INDIVIDUAL_LED_EFFECT: ZHA_ACTION_TYPE_CHANNEL_COMMAND, INOVELLI_INDIVIDUAL_LED_EFFECT: ZHA_ACTION_TYPE_CLUSTER_HANDLER_COMMAND,
} }
DEVICE_ACTION_SCHEMAS = { DEVICE_ACTION_SCHEMAS = {
@ -109,9 +112,9 @@ SERVICE_NAMES = {
ACTION_WARN: SERVICE_WARNING_DEVICE_WARN, ACTION_WARN: SERVICE_WARNING_DEVICE_WARN,
} }
CHANNEL_MAPPINGS = { CLUSTER_HANDLER_MAPPINGS = {
INOVELLI_ALL_LED_EFFECT: CHANNEL_INOVELLI, INOVELLI_ALL_LED_EFFECT: CLUSTER_HANDLER_INOVELLI,
INOVELLI_INDIVIDUAL_LED_EFFECT: CHANNEL_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) zha_device = async_get_zha_device(hass, device_id)
except (KeyError, AttributeError): except (KeyError, AttributeError):
return [] return []
cluster_channels = [ cluster_handlers = [
ch.name ch.name
for pool in zha_device.channels.pools for endpoint in zha_device.endpoints.values()
for ch in pool.claimed_channels.values() for ch in endpoint.claimed_cluster_handlers.values()
] ]
actions = [ actions = [
action action
for channel, channel_actions in DEVICE_ACTIONS.items() for cluster_handler, cluster_handler_actions in DEVICE_ACTIONS.items()
for action in channel_actions for action in cluster_handler_actions
if channel in cluster_channels if cluster_handler in cluster_handlers
] ]
for action in actions: for action in actions:
action[CONF_DEVICE_ID] = device_id 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, hass: HomeAssistant,
config: dict[str, Any], config: dict[str, Any],
variables: TemplateVarsType, variables: TemplateVarsType,
context: Context | None, context: Context | None,
) -> None: ) -> None:
action_type = config[CONF_TYPE] action_type = config[CONF_TYPE]
channel_name = CHANNEL_MAPPINGS[action_type] cluster_handler_name = CLUSTER_HANDLER_MAPPINGS[action_type]
try: try:
zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID]) zha_device = async_get_zha_device(hass, config[CONF_DEVICE_ID])
except (KeyError, AttributeError): except (KeyError, AttributeError):
return return
action_channel = None action_cluster_handler = None
for pool in zha_device.channels.pools: for endpoint in zha_device.endpoints.values():
for channel in pool.all_channels.values(): for cluster_handler in endpoint.all_cluster_handlers.values():
if channel.name == channel_name: if cluster_handler.name == cluster_handler_name:
action_channel = channel action_cluster_handler = cluster_handler
break break
if action_channel is None: if action_cluster_handler is None:
raise InvalidDeviceAutomationConfig( 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}" f" {action_type}"
) )
if not hasattr(action_channel, action_type): if not hasattr(action_cluster_handler, action_type):
raise InvalidDeviceAutomationConfig( 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}" f" {action_type}"
) )
await getattr(action_channel, action_type)(**config) await getattr(action_cluster_handler, action_type)(**config)
ZHA_ACTION_TYPES = { ZHA_ACTION_TYPES = {
ZHA_ACTION_TYPE_SERVICE_CALL: _execute_service_based_action, 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,
} }

View File

@ -14,7 +14,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .core import discovery from .core import discovery
from .core.const import ( from .core.const import (
CHANNEL_POWER_CONFIGURATION, CLUSTER_HANDLER_POWER_CONFIGURATION,
DATA_ZHA, DATA_ZHA,
SIGNAL_ADD_ENTITIES, SIGNAL_ADD_ENTITIES,
SIGNAL_ATTR_UPDATED, SIGNAL_ATTR_UPDATED,
@ -44,16 +44,18 @@ async def async_setup_entry(
config_entry.async_on_unload(unsub) 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): class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity):
"""Represent a tracked device.""" """Represent a tracked device."""
_attr_should_poll = True # BaseZhaEntity defaults to False _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.""" """Initialize the ZHA device tracker."""
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
self._battery_channel = self.cluster_channels.get(CHANNEL_POWER_CONFIGURATION) self._battery_cluster_handler = self.cluster_handlers.get(
CLUSTER_HANDLER_POWER_CONFIGURATION
)
self._connected = False self._connected = False
self._keepalive_interval = 60 self._keepalive_interval = 60
self._battery_level = None self._battery_level = None
@ -61,9 +63,9 @@ class ZHADeviceScannerEntity(ScannerEntity, ZhaEntity):
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Run when about to be added to hass.""" """Run when about to be added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
if self._battery_channel: if self._battery_cluster_handler:
self.async_accept_signal( self.async_accept_signal(
self._battery_channel, self._battery_cluster_handler,
SIGNAL_ATTR_UPDATED, SIGNAL_ATTR_UPDATED,
self.async_battery_percentage_remaining_updated, self.async_battery_percentage_remaining_updated,
) )

View File

@ -34,7 +34,7 @@ from .core.const import (
from .core.helpers import LogMixin from .core.helpers import LogMixin
if TYPE_CHECKING: if TYPE_CHECKING:
from .core.channels.base import ZigbeeChannel from .core.cluster_handlers import ClusterHandler
from .core.device import ZHADevice from .core.device import ZHADevice
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -122,19 +122,19 @@ class BaseZhaEntity(LogMixin, entity.Entity):
@callback @callback
def async_accept_signal( def async_accept_signal(
self, self,
channel: ZigbeeChannel | None, cluster_handler: ClusterHandler | None,
signal: str, signal: str,
func: Callable[..., Any], func: Callable[..., Any],
signal_override=False, signal_override=False,
): ):
"""Accept a signal from a channel.""" """Accept a signal from a cluster handler."""
unsub = None unsub = None
if signal_override: if signal_override:
unsub = async_dispatcher_connect(self.hass, signal, func) unsub = async_dispatcher_connect(self.hass, signal, func)
else: else:
assert channel assert cluster_handler
unsub = async_dispatcher_connect( 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) self._unsubs.append(unsub)
@ -152,7 +152,7 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity):
"""Initialize subclass. """Initialize subclass.
:param id_suffix: suffix to add to the unique_id of the entity. Used for multi :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) super().__init_subclass__(**kwargs)
if id_suffix: if id_suffix:
@ -162,7 +162,7 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity):
self, self,
unique_id: str, unique_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
"""Init ZHA entity.""" """Init ZHA entity."""
@ -174,23 +174,23 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity):
.replace("sensor", "") .replace("sensor", "")
.capitalize() .capitalize()
) )
self.cluster_channels: dict[str, ZigbeeChannel] = {} self.cluster_handlers: dict[str, ClusterHandler] = {}
for channel in channels: for cluster_handler in cluster_handlers:
self.cluster_channels[channel.name] = channel self.cluster_handlers[cluster_handler.name] = cluster_handler
@classmethod @classmethod
def create_entity( def create_entity(
cls, cls,
unique_id: str, unique_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
**kwargs: Any, **kwargs: Any,
) -> Self | None: ) -> Self | None:
"""Entity Factory. """Entity Factory.
Return entity if it is a supported configuration, otherwise return None 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 @property
def available(self) -> bool: def available(self) -> bool:
@ -220,7 +220,7 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity):
self._zha_device.ieee, self._zha_device.ieee,
self.entity_id, self.entity_id,
self._zha_device, self._zha_device,
self.cluster_channels, self.cluster_handlers,
self.device_info, self.device_info,
self.remove_future, self.remove_future,
) )
@ -238,9 +238,9 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity):
async def async_update(self) -> None: async def async_update(self) -> None:
"""Retrieve latest state.""" """Retrieve latest state."""
tasks = [ tasks = [
channel.async_update() cluster_handler.async_update()
for channel in self.cluster_channels.values() for cluster_handler in self.cluster_handlers.values()
if hasattr(channel, "async_update") if hasattr(cluster_handler, "async_update")
] ]
if tasks: if tasks:
await asyncio.gather(*tasks) await asyncio.gather(*tasks)

View File

@ -28,7 +28,12 @@ from homeassistant.util.percentage import (
) )
from .core import discovery 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 .core.registries import ZHA_ENTITIES
from .entity import ZhaEntity, ZhaGroupEntity from .entity import ZhaEntity, ZhaGroupEntity
@ -124,50 +129,52 @@ class BaseFan(FanEntity):
@callback @callback
def async_set_state(self, attr_id, attr_name, value): 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): class ZhaFan(BaseFan, ZhaEntity):
"""Representation of a ZHA fan.""" """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.""" """Init this sensor."""
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
self._fan_channel = self.cluster_channels.get(CHANNEL_FAN) self._fan_cluster_handler = self.cluster_handlers.get(CLUSTER_HANDLER_FAN)
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Run when about to be added to hass.""" """Run when about to be added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
self.async_accept_signal( 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 @property
def percentage(self) -> int | None: def percentage(self) -> int | None:
"""Return the current speed percentage.""" """Return the current speed percentage."""
if ( if (
self._fan_channel.fan_mode is None self._fan_cluster_handler.fan_mode is None
or self._fan_channel.fan_mode > SPEED_RANGE[1] or self._fan_cluster_handler.fan_mode > SPEED_RANGE[1]
): ):
return None return None
if self._fan_channel.fan_mode == 0: if self._fan_cluster_handler.fan_mode == 0:
return 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 @property
def preset_mode(self) -> str | None: def preset_mode(self) -> str | None:
"""Return the current preset mode.""" """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 @callback
def async_set_state(self, attr_id, attr_name, value): 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() self.async_write_ha_state()
async def _async_set_fan_mode(self, fan_mode: int) -> None: async def _async_set_fan_mode(self, fan_mode: int) -> None:
"""Set the fan mode for the fan.""" """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) 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) super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs)
self._available: bool = False self._available: bool = False
group = self.zha_device.gateway.get_group(self._group_id) 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._percentage = None
self._preset_mode = None self._preset_mode = None
@ -199,7 +206,7 @@ class FanGroup(BaseFan, ZhaGroupEntity):
async def _async_set_fan_mode(self, fan_mode: int) -> None: async def _async_set_fan_mode(self, fan_mode: int) -> None:
"""Set the fan mode for the group.""" """Set the fan mode for the group."""
try: 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: except ZigbeeException as ex:
self.error("Could not set fan mode: %s", ex) self.error("Could not set fan mode: %s", ex)
self.async_set_state(0, "fan_mode", fan_mode) self.async_set_state(0, "fan_mode", fan_mode)
@ -250,22 +257,22 @@ IKEA_PRESET_MODES = list(IKEA_NAME_TO_PRESET_MODE)
@MULTI_MATCH( @MULTI_MATCH(
channel_names="ikea_airpurifier", cluster_handler_names="ikea_airpurifier",
models={"STARKVIND Air purifier", "STARKVIND Air purifier table"}, models={"STARKVIND Air purifier", "STARKVIND Air purifier table"},
) )
class IkeaFan(BaseFan, ZhaEntity): class IkeaFan(BaseFan, ZhaEntity):
"""Representation of a ZHA fan.""" """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.""" """Init this sensor."""
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
self._fan_channel = self.cluster_channels.get("ikea_airpurifier") self._fan_cluster_handler = self.cluster_handlers.get("ikea_airpurifier")
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Run when about to be added to hass.""" """Run when about to be added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
self.async_accept_signal( 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 @property
@ -296,18 +303,20 @@ class IkeaFan(BaseFan, ZhaEntity):
def percentage(self) -> int | None: def percentage(self) -> int | None:
"""Return the current speed percentage.""" """Return the current speed percentage."""
if ( if (
self._fan_channel.fan_mode is None self._fan_cluster_handler.fan_mode is None
or self._fan_channel.fan_mode > IKEA_SPEED_RANGE[1] or self._fan_cluster_handler.fan_mode > IKEA_SPEED_RANGE[1]
): ):
return None return None
if self._fan_channel.fan_mode == 0: if self._fan_cluster_handler.fan_mode == 0:
return 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 @property
def preset_mode(self) -> str | None: def preset_mode(self) -> str | None:
"""Return the current preset mode.""" """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( async def async_turn_on(
self, self,
@ -328,10 +337,10 @@ class IkeaFan(BaseFan, ZhaEntity):
@callback @callback
def async_set_state(self, attr_id, attr_name, value): 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() self.async_write_ha_state()
async def _async_set_fan_mode(self, fan_mode: int) -> None: async def _async_set_fan_mode(self, fan_mode: int) -> None:
"""Set the fan mode for the fan.""" """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) self.async_set_state(0, "fan_mode", fan_mode)

View File

@ -39,9 +39,9 @@ from homeassistant.helpers.event import async_call_later, async_track_time_inter
from .core import discovery, helpers from .core import discovery, helpers
from .core.const import ( from .core.const import (
CHANNEL_COLOR, CLUSTER_HANDLER_COLOR,
CHANNEL_LEVEL, CLUSTER_HANDLER_LEVEL,
CHANNEL_ON_OFF, CLUSTER_HANDLER_ON_OFF,
CONF_ALWAYS_PREFER_XY_COLOR_MODE, CONF_ALWAYS_PREFER_XY_COLOR_MODE,
CONF_DEFAULT_LIGHT_TRANSITION, CONF_DEFAULT_LIGHT_TRANSITION,
CONF_ENABLE_ENHANCED_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_enhanced_light_transition: bool = False
self._zha_config_enable_light_transitioning_flag: bool = True self._zha_config_enable_light_transitioning_flag: bool = True
self._zha_config_always_prefer_xy_color_mode: bool = True self._zha_config_always_prefer_xy_color_mode: bool = True
self._on_off_channel = None self._on_off_cluster_handler = None
self._level_channel = None self._level_cluster_handler = None
self._color_channel = None self._color_cluster_handler = None
self._identify_channel = None self._identify_cluster_handler = None
self._transitioning_individual: bool = False self._transitioning_individual: bool = False
self._transitioning_group: bool = False self._transitioning_group: bool = False
self._transition_listener: Callable[[], None] | None = None self._transition_listener: Callable[[], None] | None = None
@ -193,7 +193,8 @@ class BaseLight(LogMixin, light.LightEntity):
execute_if_off_supported = ( execute_if_off_supported = (
self._GROUP_SUPPORTS_EXECUTE_IF_OFF self._GROUP_SUPPORTS_EXECUTE_IF_OFF
if isinstance(self, LightGroup) 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 = ( 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 # If the light is currently off, we first need to turn it on at a low
# brightness level with no transition. # brightness level with no transition.
# After that, we set it to the desired color/temperature 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, level=DEFAULT_MIN_BRIGHTNESS,
transition_time=self._DEFAULT_MIN_TRANSITION_TIME, transition_time=self._DEFAULT_MIN_TRANSITION_TIME,
) )
@ -329,7 +330,7 @@ class BaseLight(LogMixin, light.LightEntity):
and not new_color_provided_while_off and not new_color_provided_while_off
and brightness_supported(self._attr_supported_color_modes) 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, level=level,
transition_time=duration, 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, # 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 # we should call the on command on the on_off cluster
# if brightness is not 0. # 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 t_log["on_off"] = result
if isinstance(result, Exception) or result[1] is not Status.SUCCESS: if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
# 'On' call failed, but as brightness may still transition # 'On' call failed, but as brightness may still transition
@ -383,7 +384,7 @@ class BaseLight(LogMixin, light.LightEntity):
if new_color_provided_while_off: if new_color_provided_while_off:
# The light is has the correct color, so we can now transition # The light is has the correct color, so we can now transition
# it to the correct brightness level. # 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 level=level, transition_time=duration
) )
t_log["move_to_level_if_color"] = result t_log["move_to_level_if_color"] = result
@ -400,7 +401,7 @@ class BaseLight(LogMixin, light.LightEntity):
self.async_transition_start_timer(transition_time) self.async_transition_start_timer(transition_time)
if effect == light.EFFECT_COLORLOOP: if effect == light.EFFECT_COLORLOOP:
result = await self._color_channel.color_loop_set( result = await self._color_cluster_handler.color_loop_set(
update_flags=( update_flags=(
Color.ColorLoopUpdateFlags.Action Color.ColorLoopUpdateFlags.Action
| Color.ColorLoopUpdateFlags.Direction | Color.ColorLoopUpdateFlags.Direction
@ -417,7 +418,7 @@ class BaseLight(LogMixin, light.LightEntity):
self._attr_effect == light.EFFECT_COLORLOOP self._attr_effect == light.EFFECT_COLORLOOP
and 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, update_flags=Color.ColorLoopUpdateFlags.Action,
action=Color.ColorLoopAction.Deactivate, action=Color.ColorLoopAction.Deactivate,
direction=Color.ColorLoopDirection.Decrement, direction=Color.ColorLoopDirection.Decrement,
@ -428,7 +429,7 @@ class BaseLight(LogMixin, light.LightEntity):
self._attr_effect = None self._attr_effect = None
if flash is not 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_id=FLASH_EFFECTS[flash],
effect_variant=Identify.EffectVariant.Default, 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 # is not none looks odd here, but it will override built in bulb
# transition times if we pass 0 in here # transition times if we pass 0 in here
if transition is not None and supports_level: 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, level=0,
transition_time=(transition * 10 or self._DEFAULT_MIN_TRANSITION_TIME), transition_time=(transition * 10 or self._DEFAULT_MIN_TRANSITION_TIME),
) )
else: else:
result = await self._on_off_channel.off() result = await self._on_off_cluster_handler.off()
# Pause parsing attribute reports until transition is complete # Pause parsing attribute reports until transition is complete
if self._zha_config_enable_light_transitioning_flag: if self._zha_config_enable_light_transitioning_flag:
@ -503,7 +504,7 @@ class BaseLight(LogMixin, light.LightEntity):
) )
if temperature is not None: 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, color_temp_mireds=temperature,
transition_time=transition_time, transition_time=transition_time,
) )
@ -518,16 +519,16 @@ class BaseLight(LogMixin, light.LightEntity):
if hs_color is not None: if hs_color is not None:
if ( if (
not isinstance(self, LightGroup) 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), enhanced_hue=int(hs_color[0] * 65535 / 360),
saturation=int(hs_color[1] * 2.54), saturation=int(hs_color[1] * 2.54),
transition_time=transition_time, transition_time=transition_time,
) )
t_log["enhanced_move_to_hue_and_saturation"] = result t_log["enhanced_move_to_hue_and_saturation"] = result
else: 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), hue=int(hs_color[0] * 254 / 360),
saturation=int(hs_color[1] * 2.54), saturation=int(hs_color[1] * 2.54),
transition_time=transition_time, 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 xy_color = None # don't set xy_color if it is also present
if xy_color is not None: 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_x=int(xy_color[0] * 65535),
color_y=int(xy_color[1] * 65535), color_y=int(xy_color[1] * 65535),
transition_time=transition_time, 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): class Light(BaseLight, ZhaEntity):
"""Representation of a ZHA or ZLL light.""" """Representation of a ZHA or ZLL light."""
_attr_supported_color_modes: set[ColorMode] _attr_supported_color_modes: set[ColorMode]
_REFRESH_INTERVAL = (45, 75) _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.""" """Initialize the ZHA light."""
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
self._on_off_channel = self.cluster_channels[CHANNEL_ON_OFF] self._on_off_cluster_handler = self.cluster_handlers[CLUSTER_HANDLER_ON_OFF]
self._attr_state = bool(self._on_off_channel.on_off) self._attr_state = bool(self._on_off_cluster_handler.on_off)
self._level_channel = self.cluster_channels.get(CHANNEL_LEVEL) self._level_cluster_handler = self.cluster_handlers.get(CLUSTER_HANDLER_LEVEL)
self._color_channel = self.cluster_channels.get(CHANNEL_COLOR) self._color_cluster_handler = self.cluster_handlers.get(CLUSTER_HANDLER_COLOR)
self._identify_channel = self.zha_device.channels.identify_ch self._identify_cluster_handler = zha_device.identify_ch
if self._color_channel: if self._color_cluster_handler:
self._attr_min_mireds: int = self._color_channel.min_mireds self._attr_min_mireds: int = self._color_cluster_handler.min_mireds
self._attr_max_mireds: int = self._color_channel.max_mireds self._attr_max_mireds: int = self._color_cluster_handler.max_mireds
self._cancel_refresh_handle: CALLBACK_TYPE | None = None self._cancel_refresh_handle: CALLBACK_TYPE | None = None
effect_list = [] effect_list = []
@ -649,44 +655,48 @@ class Light(BaseLight, ZhaEntity):
) )
self._attr_supported_color_modes = {ColorMode.ONOFF} 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_color_modes.add(ColorMode.BRIGHTNESS)
self._attr_supported_features |= light.LightEntityFeature.TRANSITION 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_cluster_handler:
if self._color_channel.color_temp_supported: if self._color_cluster_handler.color_temp_supported:
self._attr_supported_color_modes.add(ColorMode.COLOR_TEMP) 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 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) self._attr_supported_color_modes.add(ColorMode.XY)
curr_x = self._color_channel.current_x curr_x = self._color_cluster_handler.current_x
curr_y = self._color_channel.current_y curr_y = self._color_cluster_handler.current_y
if curr_x is not None and curr_y is not None: if curr_x is not None and curr_y is not None:
self._attr_xy_color = (curr_x / 65535, curr_y / 65535) self._attr_xy_color = (curr_x / 65535, curr_y / 65535)
else: else:
self._attr_xy_color = (0, 0) self._attr_xy_color = (0, 0)
if ( if (
self._color_channel.hs_supported self._color_cluster_handler.hs_supported
and not self._zha_config_always_prefer_xy_color_mode and not self._zha_config_always_prefer_xy_color_mode
): ):
self._attr_supported_color_modes.add(ColorMode.HS) self._attr_supported_color_modes.add(ColorMode.HS)
if ( if (
self._color_channel.enhanced_hue_supported self._color_cluster_handler.enhanced_hue_supported
and self._color_channel.enhanced_current_hue is not None and self._color_cluster_handler.enhanced_current_hue is not None
): ):
curr_hue = self._color_channel.enhanced_current_hue * 65535 / 360 curr_hue = (
elif self._color_channel.current_hue is not None: self._color_cluster_handler.enhanced_current_hue * 65535 / 360
curr_hue = self._color_channel.current_hue * 254 / 360 )
elif self._color_cluster_handler.current_hue is not None:
curr_hue = self._color_cluster_handler.current_hue * 254 / 360
else: else:
curr_hue = 0 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 curr_saturation = 0
self._attr_hs_color = ( self._attr_hs_color = (
@ -694,10 +704,10 @@ class Light(BaseLight, ZhaEntity):
int(curr_saturation * 2.54), 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 self._attr_supported_features |= light.LightEntityFeature.EFFECT
effect_list.append(light.EFFECT_COLORLOOP) 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_effect = light.EFFECT_COLORLOOP
self._attr_supported_color_modes = filter_supported_color_modes( self._attr_supported_color_modes = filter_supported_color_modes(
self._attr_supported_color_modes self._attr_supported_color_modes
@ -705,13 +715,16 @@ class Light(BaseLight, ZhaEntity):
if len(self._attr_supported_color_modes) == 1: if len(self._attr_supported_color_modes) == 1:
self._attr_color_mode = next(iter(self._attr_supported_color_modes)) self._attr_color_mode = next(iter(self._attr_supported_color_modes))
else: # Light supports color_temp + hs, determine which mode the light is in else: # Light supports color_temp + hs, determine which mode the light is in
assert self._color_channel assert self._color_cluster_handler
if self._color_channel.color_mode == Color.ColorMode.Color_temperature: if (
self._color_cluster_handler.color_mode
== Color.ColorMode.Color_temperature
):
self._attr_color_mode = ColorMode.COLOR_TEMP self._attr_color_mode = ColorMode.COLOR_TEMP
else: else:
self._attr_color_mode = ColorMode.XY self._attr_color_mode = ColorMode.XY
if self._identify_channel: if self._identify_cluster_handler:
self._attr_supported_features |= light.LightEntityFeature.FLASH self._attr_supported_features |= light.LightEntityFeature.FLASH
if effect_list: if effect_list:
@ -755,11 +768,11 @@ class Light(BaseLight, ZhaEntity):
"""Run when about to be added to hass.""" """Run when about to be added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
self.async_accept_signal( 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.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)) refresh_interval = random.randint(*(x * 60 for x in self._REFRESH_INTERVAL))
self._cancel_refresh_handle = async_track_time_interval( self._cancel_refresh_handle = async_track_time_interval(
@ -844,8 +857,8 @@ class Light(BaseLight, ZhaEntity):
return return
self.debug("polling current state") self.debug("polling current state")
if self._on_off_channel: if self._on_off_cluster_handler:
state = await self._on_off_channel.get_attribute_value( state = await self._on_off_cluster_handler.get_attribute_value(
"on_off", from_cache=False "on_off", from_cache=False
) )
# check if transition started whilst waiting for polled state # check if transition started whilst waiting for polled state
@ -858,8 +871,8 @@ class Light(BaseLight, ZhaEntity):
self._off_with_transition = False self._off_with_transition = False
self._off_brightness = None self._off_brightness = None
if self._level_channel: if self._level_cluster_handler:
level = await self._level_channel.get_attribute_value( level = await self._level_cluster_handler.get_attribute_value(
"current_level", from_cache=False "current_level", from_cache=False
) )
# check if transition started whilst waiting for polled state # check if transition started whilst waiting for polled state
@ -868,7 +881,7 @@ class Light(BaseLight, ZhaEntity):
if level is not None: if level is not None:
self._attr_brightness = level self._attr_brightness = level
if self._color_channel: if self._color_cluster_handler:
attributes = [ attributes = [
"color_mode", "color_mode",
"current_x", "current_x",
@ -876,23 +889,23 @@ class Light(BaseLight, ZhaEntity):
] ]
if ( if (
not self._zha_config_always_prefer_xy_color_mode 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("enhanced_current_hue")
attributes.append("current_saturation") attributes.append("current_saturation")
if ( if (
self._color_channel.hs_supported self._color_cluster_handler.hs_supported
and not self._color_channel.enhanced_hue_supported and not self._color_cluster_handler.enhanced_hue_supported
and not self._zha_config_always_prefer_xy_color_mode and not self._zha_config_always_prefer_xy_color_mode
): ):
attributes.append("current_hue") attributes.append("current_hue")
attributes.append("current_saturation") attributes.append("current_saturation")
if self._color_channel.color_temp_supported: if self._color_cluster_handler.color_temp_supported:
attributes.append("color_temperature") attributes.append("color_temperature")
if self._color_channel.color_loop_supported: if self._color_cluster_handler.color_loop_supported:
attributes.append("color_loop_active") 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 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 and not self._zha_config_always_prefer_xy_color_mode
): ):
self._attr_color_mode = ColorMode.HS 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") current_hue = results.get("enhanced_current_hue")
else: else:
current_hue = results.get("current_hue") 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: if current_hue is not None and current_saturation is not None:
self._attr_hs_color = ( self._attr_hs_color = (
int(current_hue * 360 / 65535) 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), else int(current_hue * 360 / 254),
int(current_saturation / 2.54), int(current_saturation / 2.54),
) )
@ -1036,8 +1049,8 @@ class Light(BaseLight, ZhaEntity):
@STRICT_MATCH( @STRICT_MATCH(
channel_names=CHANNEL_ON_OFF, cluster_handler_names=CLUSTER_HANDLER_ON_OFF,
aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}, aux_cluster_handlers={CLUSTER_HANDLER_COLOR, CLUSTER_HANDLER_LEVEL},
manufacturers={"Philips", "Signify Netherlands B.V."}, manufacturers={"Philips", "Signify Netherlands B.V."},
) )
class HueLight(Light): class HueLight(Light):
@ -1047,8 +1060,8 @@ class HueLight(Light):
@STRICT_MATCH( @STRICT_MATCH(
channel_names=CHANNEL_ON_OFF, cluster_handler_names=CLUSTER_HANDLER_ON_OFF,
aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}, aux_cluster_handlers={CLUSTER_HANDLER_COLOR, CLUSTER_HANDLER_LEVEL},
manufacturers={"Jasco", "Quotra-Vision", "eWeLight", "eWeLink"}, manufacturers={"Jasco", "Quotra-Vision", "eWeLight", "eWeLink"},
) )
class ForceOnLight(Light): class ForceOnLight(Light):
@ -1058,8 +1071,8 @@ class ForceOnLight(Light):
@STRICT_MATCH( @STRICT_MATCH(
channel_names=CHANNEL_ON_OFF, cluster_handler_names=CLUSTER_HANDLER_ON_OFF,
aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}, aux_cluster_handlers={CLUSTER_HANDLER_COLOR, CLUSTER_HANDLER_LEVEL},
manufacturers=DEFAULT_MIN_TRANSITION_MANUFACTURERS, manufacturers=DEFAULT_MIN_TRANSITION_MANUFACTURERS,
) )
class MinTransitionLight(Light): 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, # If at least one member has a color cluster and doesn't support it,
# it's not used. # it's not used.
for member in group.members: for member in group.members:
for pool in member.device.channels.pools: for (
for channel in pool.all_channels.values(): endpoint
) in member.device._endpoints.values(): # pylint: disable=protected-access
for cluster_handler in endpoint.all_cluster_handlers.values():
if ( if (
channel.name == CHANNEL_COLOR cluster_handler.name == CLUSTER_HANDLER_COLOR
and not channel.execute_if_off_supported and not cluster_handler.execute_if_off_supported
): ):
self._GROUP_SUPPORTS_EXECUTE_IF_OFF = False self._GROUP_SUPPORTS_EXECUTE_IF_OFF = False
break break
@ -1102,10 +1117,10 @@ class LightGroup(BaseLight, ZhaGroupEntity):
member.device.manufacturer in DEFAULT_MIN_TRANSITION_MANUFACTURERS member.device.manufacturer in DEFAULT_MIN_TRANSITION_MANUFACTURERS
for member in group.members for member in group.members
) )
self._on_off_channel = group.endpoint[OnOff.cluster_id] self._on_off_cluster_handler = group.endpoint[OnOff.cluster_id]
self._level_channel = group.endpoint[LevelControl.cluster_id] self._level_cluster_handler = group.endpoint[LevelControl.cluster_id]
self._color_channel = group.endpoint[Color.cluster_id] self._color_cluster_handler = group.endpoint[Color.cluster_id]
self._identify_channel = group.endpoint[Identify.cluster_id] self._identify_cluster_handler = group.endpoint[Identify.cluster_id]
self._debounced_member_refresh: Debouncer | None = None self._debounced_member_refresh: Debouncer | None = None
self._zha_config_transition = async_get_zha_config_value( self._zha_config_transition = async_get_zha_config_value(
zha_device.gateway.config_entry, zha_device.gateway.config_entry,

View File

@ -19,7 +19,7 @@ from homeassistant.helpers.typing import StateType
from .core import discovery from .core import discovery
from .core.const import ( from .core.const import (
CHANNEL_DOORLOCK, CLUSTER_HANDLER_DOORLOCK,
DATA_ZHA, DATA_ZHA,
SIGNAL_ADD_ENTITIES, SIGNAL_ADD_ENTITIES,
SIGNAL_ATTR_UPDATED, 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): class ZhaDoorLock(ZhaEntity, LockEntity):
"""Representation of a ZHA lock.""" """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.""" """Init this sensor."""
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
self._doorlock_channel = self.cluster_channels.get(CHANNEL_DOORLOCK) self._doorlock_cluster_handler = self.cluster_handlers.get(
CLUSTER_HANDLER_DOORLOCK
)
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Run when about to be added to hass.""" """Run when about to be added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
self.async_accept_signal( 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 @callback
@ -127,7 +129,7 @@ class ZhaDoorLock(ZhaEntity, LockEntity):
async def async_lock(self, **kwargs: Any) -> None: async def async_lock(self, **kwargs: Any) -> None:
"""Lock the lock.""" """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: if isinstance(result, Exception) or result[0] is not Status.SUCCESS:
self.error("Error with lock_door: %s", result) self.error("Error with lock_door: %s", result)
return return
@ -135,7 +137,7 @@ class ZhaDoorLock(ZhaEntity, LockEntity):
async def async_unlock(self, **kwargs: Any) -> None: async def async_unlock(self, **kwargs: Any) -> None:
"""Unlock the lock.""" """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: if isinstance(result, Exception) or result[0] is not Status.SUCCESS:
self.error("Error with unlock_door: %s", result) self.error("Error with unlock_door: %s", result)
return return
@ -148,14 +150,14 @@ class ZhaDoorLock(ZhaEntity, LockEntity):
@callback @callback
def async_set_state(self, attr_id, attr_name, value): 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._state = VALUE_TO_STATE.get(value, self._state)
self.async_write_ha_state() self.async_write_ha_state()
async def async_get_state(self, from_cache=True): async def async_get_state(self, from_cache=True):
"""Attempt to retrieve state from the lock.""" """Attempt to retrieve state from the lock."""
if self._doorlock_channel: if self._doorlock_cluster_handler:
state = await self._doorlock_channel.get_attribute_value( state = await self._doorlock_cluster_handler.get_attribute_value(
"lock_state", from_cache=from_cache "lock_state", from_cache=from_cache
) )
if state is not None: 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: 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.""" """Set the user_code to index X on the lock."""
if self._doorlock_channel: if self._doorlock_cluster_handler:
await self._doorlock_channel.async_set_user_code(code_slot, user_code) await self._doorlock_cluster_handler.async_set_user_code(
code_slot, user_code
)
self.debug("User code at slot %s set", code_slot) self.debug("User code at slot %s set", code_slot)
async def async_enable_lock_user_code(self, code_slot: int) -> None: async def async_enable_lock_user_code(self, code_slot: int) -> None:
"""Enable user_code at index X on the lock.""" """Enable user_code at index X on the lock."""
if self._doorlock_channel: if self._doorlock_cluster_handler:
await self._doorlock_channel.async_enable_user_code(code_slot) await self._doorlock_cluster_handler.async_enable_user_code(code_slot)
self.debug("User code at slot %s enabled", code_slot) self.debug("User code at slot %s enabled", code_slot)
async def async_disable_lock_user_code(self, code_slot: int) -> None: async def async_disable_lock_user_code(self, code_slot: int) -> None:
"""Disable user_code at index X on the lock.""" """Disable user_code at index X on the lock."""
if self._doorlock_channel: if self._doorlock_cluster_handler:
await self._doorlock_channel.async_disable_user_code(code_slot) await self._doorlock_cluster_handler.async_disable_user_code(code_slot)
self.debug("User code at slot %s disabled", code_slot) self.debug("User code at slot %s disabled", code_slot)
async def async_clear_lock_user_code(self, code_slot: int) -> None: async def async_clear_lock_user_code(self, code_slot: int) -> None:
"""Clear the user_code at index X on the lock.""" """Clear the user_code at index X on the lock."""
if self._doorlock_channel: if self._doorlock_cluster_handler:
await self._doorlock_channel.async_clear_user_code(code_slot) await self._doorlock_cluster_handler.async_clear_user_code(code_slot)
self.debug("User code at slot %s cleared", code_slot) self.debug("User code at slot %s cleared", code_slot)

View File

@ -18,11 +18,11 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .core import discovery from .core import discovery
from .core.const import ( from .core.const import (
CHANNEL_ANALOG_OUTPUT, CLUSTER_HANDLER_ANALOG_OUTPUT,
CHANNEL_BASIC, CLUSTER_HANDLER_BASIC,
CHANNEL_COLOR, CLUSTER_HANDLER_COLOR,
CHANNEL_INOVELLI, CLUSTER_HANDLER_INOVELLI,
CHANNEL_LEVEL, CLUSTER_HANDLER_LEVEL,
DATA_ZHA, DATA_ZHA,
SIGNAL_ADD_ENTITIES, SIGNAL_ADD_ENTITIES,
SIGNAL_ATTR_UPDATED, SIGNAL_ATTR_UPDATED,
@ -31,7 +31,7 @@ from .core.registries import ZHA_ENTITIES
from .entity import ZhaEntity from .entity import ZhaEntity
if TYPE_CHECKING: if TYPE_CHECKING:
from .core.channels.base import ZigbeeChannel from .core.cluster_handlers import ClusterHandler
from .core.device import ZHADevice from .core.device import ZHADevice
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -275,7 +275,7 @@ async def async_setup_entry(
config_entry.async_on_unload(unsub) 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): class ZhaNumber(ZhaEntity, NumberEntity):
"""Representation of a ZHA Number entity.""" """Representation of a ZHA Number entity."""
@ -283,29 +283,33 @@ class ZhaNumber(ZhaEntity, NumberEntity):
self, self,
unique_id: str, unique_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
"""Init this entity.""" """Init this entity."""
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
self._analog_output_channel = self.cluster_channels[CHANNEL_ANALOG_OUTPUT] self._analog_output_cluster_handler = self.cluster_handlers[
CLUSTER_HANDLER_ANALOG_OUTPUT
]
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Run when about to be added to hass.""" """Run when about to be added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
self.async_accept_signal( 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 @property
def native_value(self) -> float | None: def native_value(self) -> float | None:
"""Return the current value.""" """Return the current value."""
return self._analog_output_channel.present_value return self._analog_output_cluster_handler.present_value
@property @property
def native_min_value(self) -> float: def native_min_value(self) -> float:
"""Return the minimum value.""" """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: if min_present_value is not None:
return min_present_value return min_present_value
return 0 return 0
@ -313,7 +317,7 @@ class ZhaNumber(ZhaEntity, NumberEntity):
@property @property
def native_max_value(self) -> float: def native_max_value(self) -> float:
"""Return the maximum value.""" """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: if max_present_value is not None:
return max_present_value return max_present_value
return 1023 return 1023
@ -321,7 +325,7 @@ class ZhaNumber(ZhaEntity, NumberEntity):
@property @property
def native_step(self) -> float | None: def native_step(self) -> float | None:
"""Return the value step.""" """Return the value step."""
resolution = self._analog_output_channel.resolution resolution = self._analog_output_cluster_handler.resolution
if resolution is not None: if resolution is not None:
return resolution return resolution
return super().native_step return super().native_step
@ -329,7 +333,7 @@ class ZhaNumber(ZhaEntity, NumberEntity):
@property @property
def name(self) -> str: def name(self) -> str:
"""Return the name of the number entity.""" """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: if description is not None and len(description) > 0:
return f"{super().name} {description}" return f"{super().name} {description}"
return super().name return super().name
@ -337,7 +341,7 @@ class ZhaNumber(ZhaEntity, NumberEntity):
@property @property
def icon(self) -> str | None: def icon(self) -> str | None:
"""Return the icon to be used for this entity.""" """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: if application_type is not None:
return ICONS.get(application_type >> 16, super().icon) return ICONS.get(application_type >> 16, super().icon)
return super().icon return super().icon
@ -345,26 +349,26 @@ class ZhaNumber(ZhaEntity, NumberEntity):
@property @property
def native_unit_of_measurement(self) -> str | None: def native_unit_of_measurement(self) -> str | None:
"""Return the unit the value is expressed in.""" """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) return UNITS.get(engineering_units)
@callback @callback
def async_set_state(self, attr_id, attr_name, value): 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() self.async_write_ha_state()
async def async_set_native_value(self, value: float) -> None: async def async_set_native_value(self, value: float) -> None:
"""Update the current value from HA.""" """Update the current value from HA."""
num_value = float(value) 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() self.async_write_ha_state()
async def async_update(self) -> None: async def async_update(self) -> None:
"""Attempt to retrieve the state of the entity.""" """Attempt to retrieve the state of the entity."""
await super().async_update() await super().async_update()
_LOGGER.debug("polling current state") _LOGGER.debug("polling current state")
if self._analog_output_channel: if self._analog_output_cluster_handler:
value = await self._analog_output_channel.get_attribute_value( value = await self._analog_output_cluster_handler.get_attribute_value(
"present_value", from_cache=False "present_value", from_cache=False
) )
_LOGGER.debug("read value=%s", value) _LOGGER.debug("read value=%s", value)
@ -383,17 +387,17 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity):
cls, cls,
unique_id: str, unique_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
**kwargs: Any, **kwargs: Any,
) -> Self | None: ) -> Self | None:
"""Entity Factory. """Entity Factory.
Return entity if it is a supported configuration, otherwise return None Return entity if it is a supported configuration, otherwise return None
""" """
channel = channels[0] cluster_handler = cluster_handlers[0]
if ( if (
cls._zcl_attribute in channel.cluster.unsupported_attributes cls._zcl_attribute in cluster_handler.cluster.unsupported_attributes
or channel.cluster.get(cls._zcl_attribute) is None or cluster_handler.cluster.get(cls._zcl_attribute) is None
): ):
_LOGGER.debug( _LOGGER.debug(
"%s is not supported - skipping %s entity creation", "%s is not supported - skipping %s entity creation",
@ -402,28 +406,31 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity):
) )
return None return None
return cls(unique_id, zha_device, channels, **kwargs) return cls(unique_id, zha_device, cluster_handlers, **kwargs)
def __init__( def __init__(
self, self,
unique_id: str, unique_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
"""Init this number configuration entity.""" """Init this number configuration entity."""
self._channel: ZigbeeChannel = channels[0] self._cluster_handler: ClusterHandler = cluster_handlers[0]
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
@property @property
def native_value(self) -> float: def native_value(self) -> float:
"""Return the current value.""" """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: async def async_set_native_value(self, value: float) -> None:
"""Update the current value from HA.""" """Update the current value from HA."""
try: try:
res = await self._channel.cluster.write_attributes( res = await self._cluster_handler.cluster.write_attributes(
{self._zcl_attribute: int(value / self._attr_multiplier)} {self._zcl_attribute: int(value / self._attr_multiplier)}
) )
except zigpy.exceptions.ZigbeeException as ex: except zigpy.exceptions.ZigbeeException as ex:
@ -438,15 +445,16 @@ class ZHANumberConfigurationEntity(ZhaEntity, NumberEntity):
"""Attempt to retrieve the state of the entity.""" """Attempt to retrieve the state of the entity."""
await super().async_update() await super().async_update()
_LOGGER.debug("polling current state") _LOGGER.debug("polling current state")
if self._channel: if self._cluster_handler:
value = await self._channel.get_attribute_value( value = await self._cluster_handler.get_attribute_value(
self._zcl_attribute, from_cache=False self._zcl_attribute, from_cache=False
) )
_LOGGER.debug("read value=%s", value) _LOGGER.debug("read value=%s", value)
@CONFIG_DIAGNOSTIC_MATCH( @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( class AqaraMotionDetectionInterval(
ZHANumberConfigurationEntity, id_suffix="detection_interval" ZHANumberConfigurationEntity, id_suffix="detection_interval"
@ -459,7 +467,7 @@ class AqaraMotionDetectionInterval(
_attr_name = "Detection interval" _attr_name = "Detection interval"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL) @CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_LEVEL)
class OnOffTransitionTimeConfigurationEntity( class OnOffTransitionTimeConfigurationEntity(
ZHANumberConfigurationEntity, id_suffix="on_off_transition_time" ZHANumberConfigurationEntity, id_suffix="on_off_transition_time"
): ):
@ -471,7 +479,7 @@ class OnOffTransitionTimeConfigurationEntity(
_attr_name = "On/Off transition time" _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"): class OnLevelConfigurationEntity(ZHANumberConfigurationEntity, id_suffix="on_level"):
"""Representation of a ZHA on level configuration entity.""" """Representation of a ZHA on level configuration entity."""
@ -481,7 +489,7 @@ class OnLevelConfigurationEntity(ZHANumberConfigurationEntity, id_suffix="on_lev
_attr_name = "On level" _attr_name = "On level"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL) @CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_LEVEL)
class OnTransitionTimeConfigurationEntity( class OnTransitionTimeConfigurationEntity(
ZHANumberConfigurationEntity, id_suffix="on_transition_time" ZHANumberConfigurationEntity, id_suffix="on_transition_time"
): ):
@ -493,7 +501,7 @@ class OnTransitionTimeConfigurationEntity(
_attr_name = "On transition time" _attr_name = "On transition time"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL) @CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_LEVEL)
class OffTransitionTimeConfigurationEntity( class OffTransitionTimeConfigurationEntity(
ZHANumberConfigurationEntity, id_suffix="off_transition_time" ZHANumberConfigurationEntity, id_suffix="off_transition_time"
): ):
@ -505,7 +513,7 @@ class OffTransitionTimeConfigurationEntity(
_attr_name = "Off transition time" _attr_name = "Off transition time"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL) @CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_LEVEL)
class DefaultMoveRateConfigurationEntity( class DefaultMoveRateConfigurationEntity(
ZHANumberConfigurationEntity, id_suffix="default_move_rate" ZHANumberConfigurationEntity, id_suffix="default_move_rate"
): ):
@ -517,7 +525,7 @@ class DefaultMoveRateConfigurationEntity(
_attr_name = "Default move rate" _attr_name = "Default move rate"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_LEVEL) @CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_LEVEL)
class StartUpCurrentLevelConfigurationEntity( class StartUpCurrentLevelConfigurationEntity(
ZHANumberConfigurationEntity, id_suffix="start_up_current_level" ZHANumberConfigurationEntity, id_suffix="start_up_current_level"
): ):
@ -529,7 +537,7 @@ class StartUpCurrentLevelConfigurationEntity(
_attr_name = "Start-up current level" _attr_name = "Start-up current level"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_COLOR) @CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_COLOR)
class StartUpColorTemperatureConfigurationEntity( class StartUpColorTemperatureConfigurationEntity(
ZHANumberConfigurationEntity, id_suffix="start_up_color_temperature" ZHANumberConfigurationEntity, id_suffix="start_up_color_temperature"
): ):
@ -544,18 +552,18 @@ class StartUpColorTemperatureConfigurationEntity(
self, self,
unique_id: str, unique_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
"""Init this ZHA startup color temperature entity.""" """Init this ZHA startup color temperature entity."""
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
if self._channel: if self._cluster_handler:
self._attr_native_min_value: float = self._channel.min_mireds self._attr_native_min_value: float = self._cluster_handler.min_mireds
self._attr_native_max_value: float = self._channel.max_mireds self._attr_native_max_value: float = self._cluster_handler.max_mireds
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names="tuya_manufacturer", cluster_handler_names="tuya_manufacturer",
manufacturers={ manufacturers={
"_TZE200_htnnfasr", "_TZE200_htnnfasr",
}, },
@ -572,7 +580,7 @@ class TimerDurationMinutes(ZHANumberConfigurationEntity, id_suffix="timer_durati
_attr_name = "Timer duration" _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"): class FilterLifeTime(ZHANumberConfigurationEntity, id_suffix="filter_life_time"):
"""Representation of a ZHA filter lifetime configuration entity.""" """Representation of a ZHA filter lifetime configuration entity."""
@ -586,7 +594,7 @@ class FilterLifeTime(ZHANumberConfigurationEntity, id_suffix="filter_life_time")
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_BASIC, cluster_handler_names=CLUSTER_HANDLER_BASIC,
manufacturers={"TexasInstruments"}, manufacturers={"TexasInstruments"},
models={"ti.router"}, models={"ti.router"},
) )
@ -599,7 +607,7 @@ class TiRouterTransmitPower(ZHANumberConfigurationEntity, id_suffix="transmit_po
_attr_name = "Transmit power" _attr_name = "Transmit power"
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_INOVELLI) @CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_INOVELLI)
class InovelliRemoteDimmingUpSpeed( class InovelliRemoteDimmingUpSpeed(
ZHANumberConfigurationEntity, id_suffix="dimming_speed_up_remote" ZHANumberConfigurationEntity, id_suffix="dimming_speed_up_remote"
): ):
@ -613,7 +621,7 @@ class InovelliRemoteDimmingUpSpeed(
_attr_name: str = "Remote dimming up speed" _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"): class InovelliButtonDelay(ZHANumberConfigurationEntity, id_suffix="button_delay"):
"""Inovelli button delay configuration entity.""" """Inovelli button delay configuration entity."""
@ -625,7 +633,7 @@ class InovelliButtonDelay(ZHANumberConfigurationEntity, id_suffix="button_delay"
_attr_name: str = "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( class InovelliLocalDimmingUpSpeed(
ZHANumberConfigurationEntity, id_suffix="dimming_speed_up_local" ZHANumberConfigurationEntity, id_suffix="dimming_speed_up_local"
): ):
@ -639,7 +647,7 @@ class InovelliLocalDimmingUpSpeed(
_attr_name: str = "Local dimming up speed" _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( class InovelliLocalRampRateOffToOn(
ZHANumberConfigurationEntity, id_suffix="ramp_rate_off_to_on_local" ZHANumberConfigurationEntity, id_suffix="ramp_rate_off_to_on_local"
): ):
@ -653,7 +661,7 @@ class InovelliLocalRampRateOffToOn(
_attr_name: str = "Local ramp rate off to on" _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( class InovelliRemoteDimmingSpeedOffToOn(
ZHANumberConfigurationEntity, id_suffix="ramp_rate_off_to_on_remote" ZHANumberConfigurationEntity, id_suffix="ramp_rate_off_to_on_remote"
): ):
@ -667,7 +675,7 @@ class InovelliRemoteDimmingSpeedOffToOn(
_attr_name: str = "Remote ramp rate off to on" _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( class InovelliRemoteDimmingDownSpeed(
ZHANumberConfigurationEntity, id_suffix="dimming_speed_down_remote" ZHANumberConfigurationEntity, id_suffix="dimming_speed_down_remote"
): ):
@ -681,7 +689,7 @@ class InovelliRemoteDimmingDownSpeed(
_attr_name: str = "Remote dimming down speed" _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( class InovelliLocalDimmingDownSpeed(
ZHANumberConfigurationEntity, id_suffix="dimming_speed_down_local" ZHANumberConfigurationEntity, id_suffix="dimming_speed_down_local"
): ):
@ -695,7 +703,7 @@ class InovelliLocalDimmingDownSpeed(
_attr_name: str = "Local dimming down speed" _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( class InovelliLocalRampRateOnToOff(
ZHANumberConfigurationEntity, id_suffix="ramp_rate_on_to_off_local" ZHANumberConfigurationEntity, id_suffix="ramp_rate_on_to_off_local"
): ):
@ -709,7 +717,7 @@ class InovelliLocalRampRateOnToOff(
_attr_name: str = "Local ramp rate on to off" _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( class InovelliRemoteDimmingSpeedOnToOff(
ZHANumberConfigurationEntity, id_suffix="ramp_rate_on_to_off_remote" ZHANumberConfigurationEntity, id_suffix="ramp_rate_on_to_off_remote"
): ):
@ -723,7 +731,7 @@ class InovelliRemoteDimmingSpeedOnToOff(
_attr_name: str = "Remote ramp rate on to off" _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( class InovelliMinimumLoadDimmingLevel(
ZHANumberConfigurationEntity, id_suffix="minimum_level" ZHANumberConfigurationEntity, id_suffix="minimum_level"
): ):
@ -737,7 +745,7 @@ class InovelliMinimumLoadDimmingLevel(
_attr_name: str = "Minimum load dimming level" _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( class InovelliMaximumLoadDimmingLevel(
ZHANumberConfigurationEntity, id_suffix="maximum_level" ZHANumberConfigurationEntity, id_suffix="maximum_level"
): ):
@ -751,7 +759,7 @@ class InovelliMaximumLoadDimmingLevel(
_attr_name: str = "Maximum load dimming level" _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( class InovelliAutoShutoffTimer(
ZHANumberConfigurationEntity, id_suffix="auto_off_timer" ZHANumberConfigurationEntity, id_suffix="auto_off_timer"
): ):
@ -765,7 +773,7 @@ class InovelliAutoShutoffTimer(
_attr_name: str = "Automatic switch shutoff timer" _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( class InovelliLoadLevelIndicatorTimeout(
ZHANumberConfigurationEntity, id_suffix="load_level_indicator_timeout" ZHANumberConfigurationEntity, id_suffix="load_level_indicator_timeout"
): ):
@ -779,7 +787,7 @@ class InovelliLoadLevelIndicatorTimeout(
_attr_name: str = "Load level indicator timeout" _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( class InovelliDefaultAllLEDOnColor(
ZHANumberConfigurationEntity, id_suffix="led_color_when_on" ZHANumberConfigurationEntity, id_suffix="led_color_when_on"
): ):
@ -793,7 +801,7 @@ class InovelliDefaultAllLEDOnColor(
_attr_name: str = "Default all LED on color" _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( class InovelliDefaultAllLEDOffColor(
ZHANumberConfigurationEntity, id_suffix="led_color_when_off" ZHANumberConfigurationEntity, id_suffix="led_color_when_off"
): ):
@ -807,7 +815,7 @@ class InovelliDefaultAllLEDOffColor(
_attr_name: str = "Default all LED off color" _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( class InovelliDefaultAllLEDOnIntensity(
ZHANumberConfigurationEntity, id_suffix="led_intensity_when_on" ZHANumberConfigurationEntity, id_suffix="led_intensity_when_on"
): ):
@ -821,7 +829,7 @@ class InovelliDefaultAllLEDOnIntensity(
_attr_name: str = "Default all LED on intensity" _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( class InovelliDefaultAllLEDOffIntensity(
ZHANumberConfigurationEntity, id_suffix="led_intensity_when_off" ZHANumberConfigurationEntity, id_suffix="led_intensity_when_off"
): ):
@ -835,7 +843,7 @@ class InovelliDefaultAllLEDOffIntensity(
_attr_name: str = "Default all LED off intensity" _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( class InovelliDoubleTapUpLevel(
ZHANumberConfigurationEntity, id_suffix="double_tap_up_level" ZHANumberConfigurationEntity, id_suffix="double_tap_up_level"
): ):
@ -849,7 +857,7 @@ class InovelliDoubleTapUpLevel(
_attr_name: str = "Double tap up level" _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( class InovelliDoubleTapDownLevel(
ZHANumberConfigurationEntity, id_suffix="double_tap_down_level" ZHANumberConfigurationEntity, id_suffix="double_tap_down_level"
): ):
@ -863,7 +871,9 @@ class InovelliDoubleTapDownLevel(
_attr_name: str = "Double tap down level" _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"): class AqaraPetFeederServingSize(ZHANumberConfigurationEntity, id_suffix="serving_size"):
"""Aqara pet feeder serving size configuration entity.""" """Aqara pet feeder serving size configuration entity."""
@ -876,7 +886,9 @@ class AqaraPetFeederServingSize(ZHANumberConfigurationEntity, id_suffix="serving
_attr_icon: str = "mdi:counter" _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( class AqaraPetFeederPortionWeight(
ZHANumberConfigurationEntity, id_suffix="portion_weight" ZHANumberConfigurationEntity, id_suffix="portion_weight"
): ):
@ -892,7 +904,9 @@ class AqaraPetFeederPortionWeight(
_attr_icon: str = "mdi:weight-gram" _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( class AqaraThermostatAwayTemp(
ZHANumberConfigurationEntity, id_suffix="away_preset_temperature" ZHANumberConfigurationEntity, id_suffix="away_preset_temperature"
): ):

View File

@ -20,10 +20,10 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .core import discovery from .core import discovery
from .core.const import ( from .core.const import (
CHANNEL_IAS_WD, CLUSTER_HANDLER_IAS_WD,
CHANNEL_INOVELLI, CLUSTER_HANDLER_INOVELLI,
CHANNEL_OCCUPANCY, CLUSTER_HANDLER_OCCUPANCY,
CHANNEL_ON_OFF, CLUSTER_HANDLER_ON_OFF,
DATA_ZHA, DATA_ZHA,
SIGNAL_ADD_ENTITIES, SIGNAL_ADD_ENTITIES,
SIGNAL_ATTR_UPDATED, SIGNAL_ATTR_UPDATED,
@ -33,7 +33,7 @@ from .core.registries import ZHA_ENTITIES
from .entity import ZhaEntity from .entity import ZhaEntity
if TYPE_CHECKING: if TYPE_CHECKING:
from .core.channels.base import ZigbeeChannel from .core.cluster_handlers import ClusterHandler
from .core.device import ZHADevice from .core.device import ZHADevice
@ -74,33 +74,35 @@ class ZHAEnumSelectEntity(ZhaEntity, SelectEntity):
self, self,
unique_id: str, unique_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
"""Init this select entity.""" """Init this select entity."""
self._attribute = self._enum.__name__ self._attribute = self._enum.__name__
self._attr_options = [entry.name.replace("_", " ") for entry in self._enum] self._attr_options = [entry.name.replace("_", " ") for entry in self._enum]
self._channel: ZigbeeChannel = channels[0] self._cluster_handler: ClusterHandler = cluster_handlers[0]
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
@property @property
def current_option(self) -> str | None: def current_option(self) -> str | None:
"""Return the selected entity option to represent the entity state.""" """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: if option is None:
return None return None
return option.name.replace("_", " ") return option.name.replace("_", " ")
async def async_select_option(self, option: str) -> None: async def async_select_option(self, option: str) -> None:
"""Change the selected option.""" """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() self.async_write_ha_state()
@callback @callback
def async_restore_last_state(self, last_state) -> None: def async_restore_last_state(self, last_state) -> None:
"""Restore previous state.""" """Restore previous state."""
if last_state.state and last_state.state != STATE_UNKNOWN: 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(" ", "_") last_state.state.replace(" ", "_")
] ]
@ -114,7 +116,7 @@ class ZHANonZCLSelectEntity(ZHAEnumSelectEntity):
return True return True
@CONFIG_DIAGNOSTIC_MATCH(channel_names=CHANNEL_IAS_WD) @CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_IAS_WD)
class ZHADefaultToneSelectEntity( class ZHADefaultToneSelectEntity(
ZHANonZCLSelectEntity, id_suffix=IasWd.Warning.WarningMode.__name__ ZHANonZCLSelectEntity, id_suffix=IasWd.Warning.WarningMode.__name__
): ):
@ -124,7 +126,7 @@ class ZHADefaultToneSelectEntity(
_attr_name = "Default siren tone" _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( class ZHADefaultSirenLevelSelectEntity(
ZHANonZCLSelectEntity, id_suffix=IasWd.Warning.SirenLevel.__name__ ZHANonZCLSelectEntity, id_suffix=IasWd.Warning.SirenLevel.__name__
): ):
@ -134,7 +136,7 @@ class ZHADefaultSirenLevelSelectEntity(
_attr_name = "Default siren level" _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( class ZHADefaultStrobeLevelSelectEntity(
ZHANonZCLSelectEntity, id_suffix=IasWd.StrobeLevel.__name__ ZHANonZCLSelectEntity, id_suffix=IasWd.StrobeLevel.__name__
): ):
@ -144,7 +146,7 @@ class ZHADefaultStrobeLevelSelectEntity(
_attr_name = "Default strobe level" _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__): class ZHADefaultStrobeSelectEntity(ZHANonZCLSelectEntity, id_suffix=Strobe.__name__):
"""Representation of a ZHA default siren strobe select entity.""" """Representation of a ZHA default siren strobe select entity."""
@ -164,17 +166,17 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity):
cls, cls,
unique_id: str, unique_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
**kwargs: Any, **kwargs: Any,
) -> Self | None: ) -> Self | None:
"""Entity Factory. """Entity Factory.
Return entity if it is a supported configuration, otherwise return None Return entity if it is a supported configuration, otherwise return None
""" """
channel = channels[0] cluster_handler = cluster_handlers[0]
if ( if (
cls._select_attr in channel.cluster.unsupported_attributes cls._select_attr in cluster_handler.cluster.unsupported_attributes
or channel.cluster.get(cls._select_attr) is None or cluster_handler.cluster.get(cls._select_attr) is None
): ):
_LOGGER.debug( _LOGGER.debug(
"%s is not supported - skipping %s entity creation", "%s is not supported - skipping %s entity creation",
@ -183,24 +185,24 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity):
) )
return None return None
return cls(unique_id, zha_device, channels, **kwargs) return cls(unique_id, zha_device, cluster_handlers, **kwargs)
def __init__( def __init__(
self, self,
unique_id: str, unique_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
"""Init this select entity.""" """Init this select entity."""
self._attr_options = [entry.name.replace("_", " ") for entry in self._enum] self._attr_options = [entry.name.replace("_", " ") for entry in self._enum]
self._channel: ZigbeeChannel = channels[0] self._cluster_handler: ClusterHandler = cluster_handlers[0]
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
@property @property
def current_option(self) -> str | None: def current_option(self) -> str | None:
"""Return the selected entity option to represent the entity state.""" """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: if option is None:
return None return None
option = self._enum(option) option = self._enum(option)
@ -208,7 +210,7 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity):
async def async_select_option(self, option: str) -> None: async def async_select_option(self, option: str) -> None:
"""Change the selected option.""" """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._select_attr: self._enum[option.replace(" ", "_")]}
) )
self.async_write_ha_state() self.async_write_ha_state()
@ -217,16 +219,16 @@ class ZCLEnumSelectEntity(ZhaEntity, SelectEntity):
"""Run when about to be added to hass.""" """Run when about to be added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
self.async_accept_signal( self.async_accept_signal(
self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state self._cluster_handler, SIGNAL_ATTR_UPDATED, self.async_set_state
) )
@callback @callback
def async_set_state(self, attr_id: int, attr_name: str, value: Any): 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() 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( class ZHAStartupOnOffSelectEntity(
ZCLEnumSelectEntity, id_suffix=OnOff.StartUpOnOff.__name__ ZCLEnumSelectEntity, id_suffix=OnOff.StartUpOnOff.__name__
): ):
@ -246,11 +248,11 @@ class TuyaPowerOnState(types.enum8):
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_ON_OFF, cluster_handler_names=CLUSTER_HANDLER_ON_OFF,
models={"TS011F", "TS0121", "TS0001", "TS0002", "TS0003", "TS0004"}, models={"TS011F", "TS0121", "TS0001", "TS0002", "TS0003", "TS0004"},
) )
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names="tuya_manufacturer", cluster_handler_names="tuya_manufacturer",
manufacturers={ manufacturers={
"_TZE200_7tdtqgwv", "_TZE200_7tdtqgwv",
"_TZE200_amp6tsvy", "_TZE200_amp6tsvy",
@ -287,7 +289,7 @@ class TuyaBacklightMode(types.enum8):
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_ON_OFF, cluster_handler_names=CLUSTER_HANDLER_ON_OFF,
models={"TS011F", "TS0121", "TS0001", "TS0002", "TS0003", "TS0004"}, models={"TS011F", "TS0121", "TS0001", "TS0002", "TS0003", "TS0004"},
) )
class TuyaBacklightModeSelectEntity(ZCLEnumSelectEntity, id_suffix="backlight_mode"): class TuyaBacklightModeSelectEntity(ZCLEnumSelectEntity, id_suffix="backlight_mode"):
@ -308,7 +310,7 @@ class MoesBacklightMode(types.enum8):
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names="tuya_manufacturer", cluster_handler_names="tuya_manufacturer",
manufacturers={ manufacturers={
"_TZE200_7tdtqgwv", "_TZE200_7tdtqgwv",
"_TZE200_amp6tsvy", "_TZE200_amp6tsvy",
@ -345,7 +347,7 @@ class AqaraMotionSensitivities(types.enum8):
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names="opple_cluster", cluster_handler_names="opple_cluster",
models={"lumi.motion.ac01", "lumi.motion.ac02", "lumi.motion.agl04"}, models={"lumi.motion.ac01", "lumi.motion.ac02", "lumi.motion.agl04"},
) )
class AqaraMotionSensitivity(ZCLEnumSelectEntity, id_suffix="motion_sensitivity"): class AqaraMotionSensitivity(ZCLEnumSelectEntity, id_suffix="motion_sensitivity"):
@ -365,7 +367,7 @@ class HueV1MotionSensitivities(types.enum8):
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_OCCUPANCY, cluster_handler_names=CLUSTER_HANDLER_OCCUPANCY,
manufacturers={"Philips", "Signify Netherlands B.V."}, manufacturers={"Philips", "Signify Netherlands B.V."},
models={"SML001"}, models={"SML001"},
) )
@ -388,7 +390,7 @@ class HueV2MotionSensitivities(types.enum8):
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_OCCUPANCY, cluster_handler_names=CLUSTER_HANDLER_OCCUPANCY,
manufacturers={"Philips", "Signify Netherlands B.V."}, manufacturers={"Philips", "Signify Netherlands B.V."},
models={"SML002", "SML003", "SML004"}, models={"SML002", "SML003", "SML004"},
) )
@ -407,7 +409,9 @@ class AqaraMonitoringModess(types.enum8):
Left_Right = 0x01 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"): class AqaraMonitoringMode(ZCLEnumSelectEntity, id_suffix="monitoring_mode"):
"""Representation of a ZHA monitoring mode configuration entity.""" """Representation of a ZHA monitoring mode configuration entity."""
@ -424,7 +428,9 @@ class AqaraApproachDistances(types.enum8):
Near = 0x02 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"): class AqaraApproachDistance(ZCLEnumSelectEntity, id_suffix="approach_distance"):
"""Representation of a ZHA approach distance configuration entity.""" """Representation of a ZHA approach distance configuration entity."""
@ -441,7 +447,7 @@ class AqaraE1ReverseDirection(types.enum8):
@CONFIG_DIAGNOSTIC_MATCH( @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"): class AqaraCurtainMode(ZCLEnumSelectEntity, id_suffix="window_covering_mode"):
"""Representation of a ZHA curtain mode configuration entity.""" """Representation of a ZHA curtain mode configuration entity."""
@ -459,7 +465,7 @@ class InovelliOutputMode(types.enum1):
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI, cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
) )
class InovelliOutputModeEntity(ZCLEnumSelectEntity, id_suffix="output_mode"): class InovelliOutputModeEntity(ZCLEnumSelectEntity, id_suffix="output_mode"):
"""Inovelli output mode control.""" """Inovelli output mode control."""
@ -479,7 +485,7 @@ class InovelliSwitchType(types.enum8):
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI, cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
) )
class InovelliSwitchTypeEntity(ZCLEnumSelectEntity, id_suffix="switch_type"): class InovelliSwitchTypeEntity(ZCLEnumSelectEntity, id_suffix="switch_type"):
"""Inovelli switch type control.""" """Inovelli switch type control."""
@ -497,7 +503,7 @@ class InovelliLedScalingMode(types.enum1):
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI, cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
) )
class InovelliLedScalingModeEntity(ZCLEnumSelectEntity, id_suffix="led_scaling_mode"): class InovelliLedScalingModeEntity(ZCLEnumSelectEntity, id_suffix="led_scaling_mode"):
"""Inovelli led mode control.""" """Inovelli led mode control."""
@ -515,7 +521,7 @@ class InovelliNonNeutralOutput(types.enum1):
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI, cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
) )
class InovelliNonNeutralOutputEntity( class InovelliNonNeutralOutputEntity(
ZCLEnumSelectEntity, id_suffix="increased_non_neutral_output" ZCLEnumSelectEntity, id_suffix="increased_non_neutral_output"
@ -534,7 +540,9 @@ class AqaraFeedingMode(types.enum8):
Schedule = 0x01 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"): class AqaraPetFeederMode(ZCLEnumSelectEntity, id_suffix="feeding_mode"):
"""Representation of an Aqara pet feeder mode configuration entity.""" """Representation of an Aqara pet feeder mode configuration entity."""
@ -552,7 +560,9 @@ class AqaraThermostatPresetMode(types.enum8):
Away = 0x02 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"): class AqaraThermostatPreset(ZCLEnumSelectEntity, id_suffix="preset"):
"""Representation of an Aqara thermostat preset configuration entity.""" """Representation of an Aqara thermostat preset configuration entity."""

View File

@ -46,19 +46,19 @@ from homeassistant.helpers.typing import StateType
from .core import discovery from .core import discovery
from .core.const import ( from .core.const import (
CHANNEL_ANALOG_INPUT, CLUSTER_HANDLER_ANALOG_INPUT,
CHANNEL_BASIC, CLUSTER_HANDLER_BASIC,
CHANNEL_DEVICE_TEMPERATURE, CLUSTER_HANDLER_DEVICE_TEMPERATURE,
CHANNEL_ELECTRICAL_MEASUREMENT, CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT,
CHANNEL_HUMIDITY, CLUSTER_HANDLER_HUMIDITY,
CHANNEL_ILLUMINANCE, CLUSTER_HANDLER_ILLUMINANCE,
CHANNEL_LEAF_WETNESS, CLUSTER_HANDLER_LEAF_WETNESS,
CHANNEL_POWER_CONFIGURATION, CLUSTER_HANDLER_POWER_CONFIGURATION,
CHANNEL_PRESSURE, CLUSTER_HANDLER_PRESSURE,
CHANNEL_SMARTENERGY_METERING, CLUSTER_HANDLER_SMARTENERGY_METERING,
CHANNEL_SOIL_MOISTURE, CLUSTER_HANDLER_SOIL_MOISTURE,
CHANNEL_TEMPERATURE, CLUSTER_HANDLER_TEMPERATURE,
CHANNEL_THERMOSTAT, CLUSTER_HANDLER_THERMOSTAT,
DATA_ZHA, DATA_ZHA,
SIGNAL_ADD_ENTITIES, SIGNAL_ADD_ENTITIES,
SIGNAL_ATTR_UPDATED, SIGNAL_ATTR_UPDATED,
@ -67,7 +67,7 @@ from .core.registries import SMARTTHINGS_HUMIDITY_CLUSTER, ZHA_ENTITIES
from .entity import ZhaEntity from .entity import ZhaEntity
if TYPE_CHECKING: if TYPE_CHECKING:
from .core.channels.base import ZigbeeChannel from .core.cluster_handlers import ClusterHandler
from .core.device import ZHADevice from .core.device import ZHADevice
PARALLEL_UPDATES = 5 PARALLEL_UPDATES = 5
@ -88,7 +88,9 @@ BATTERY_SIZES = {
255: "Unknown", 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) STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.SENSOR)
MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.SENSOR) MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.SENSOR)
@ -125,50 +127,50 @@ class Sensor(ZhaEntity, SensorEntity):
self, self,
unique_id: str, unique_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
"""Init this sensor.""" """Init this sensor."""
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
self._channel: ZigbeeChannel = channels[0] self._cluster_handler: ClusterHandler = cluster_handlers[0]
@classmethod @classmethod
def create_entity( def create_entity(
cls, cls,
unique_id: str, unique_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
**kwargs: Any, **kwargs: Any,
) -> Self | None: ) -> Self | None:
"""Entity Factory. """Entity Factory.
Return entity if it is a supported configuration, otherwise return None Return entity if it is a supported configuration, otherwise return None
""" """
channel = channels[0] cluster_handler = cluster_handlers[0]
if cls.SENSOR_ATTR in channel.cluster.unsupported_attributes: if cls.SENSOR_ATTR in cluster_handler.cluster.unsupported_attributes:
return None 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: async def async_added_to_hass(self) -> None:
"""Run when about to be added to hass.""" """Run when about to be added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
self.async_accept_signal( self.async_accept_signal(
self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state self._cluster_handler, SIGNAL_ATTR_UPDATED, self.async_set_state
) )
@property @property
def native_value(self) -> StateType: def native_value(self) -> StateType:
"""Return the state of the entity.""" """Return the state of the entity."""
assert self.SENSOR_ATTR is not None 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: if raw_state is None:
return None return None
return self.formatter(raw_state) return self.formatter(raw_state)
@callback @callback
def async_set_state(self, attr_id: int, attr_name: str, value: Any) -> None: 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() self.async_write_ha_state()
def formatter(self, value: int | enum.IntEnum) -> int | float | str | None: def formatter(self, value: int | enum.IntEnum) -> int | float | str | None:
@ -181,9 +183,9 @@ class Sensor(ZhaEntity, SensorEntity):
@MULTI_MATCH( @MULTI_MATCH(
channel_names=CHANNEL_ANALOG_INPUT, cluster_handler_names=CLUSTER_HANDLER_ANALOG_INPUT,
manufacturers="Digi", manufacturers="Digi",
stop_on_match_group=CHANNEL_ANALOG_INPUT, stop_on_match_group=CLUSTER_HANDLER_ANALOG_INPUT,
) )
class AnalogInput(Sensor): class AnalogInput(Sensor):
"""Sensor that displays analog input values.""" """Sensor that displays analog input values."""
@ -191,7 +193,7 @@ class AnalogInput(Sensor):
SENSOR_ATTR = "present_value" SENSOR_ATTR = "present_value"
@MULTI_MATCH(channel_names=CHANNEL_POWER_CONFIGURATION) @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_POWER_CONFIGURATION)
class Battery(Sensor): class Battery(Sensor):
"""Battery sensor of power configuration cluster.""" """Battery sensor of power configuration cluster."""
@ -207,7 +209,7 @@ class Battery(Sensor):
cls, cls,
unique_id: str, unique_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
**kwargs: Any, **kwargs: Any,
) -> Self | None: ) -> Self | None:
"""Entity Factory. """Entity Factory.
@ -216,7 +218,9 @@ class Battery(Sensor):
battery_percent_remaining attribute, but zha-device-handlers takes care of it battery_percent_remaining attribute, but zha-device-handlers takes care of it
so create the entity regardless 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 @staticmethod
def formatter(value: int) -> int | None: # pylint: disable=arguments-differ 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]: def extra_state_attributes(self) -> dict[str, Any]:
"""Return device state attrs for battery sensors.""" """Return device state attrs for battery sensors."""
state_attrs = {} 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: if battery_size is not None:
state_attrs["battery_size"] = BATTERY_SIZES.get(battery_size, "Unknown") 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: if battery_quantity is not None:
state_attrs["battery_quantity"] = battery_quantity 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: if battery_voltage is not None:
state_attrs["battery_voltage"] = round(battery_voltage / 10, 2) state_attrs["battery_voltage"] = round(battery_voltage / 10, 2)
return state_attrs return state_attrs
@MULTI_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT) @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
class ElectricalMeasurement(Sensor): class ElectricalMeasurement(Sensor):
"""Active power measurement.""" """Active power measurement."""
@ -259,19 +263,21 @@ class ElectricalMeasurement(Sensor):
def extra_state_attributes(self) -> dict[str, Any]: def extra_state_attributes(self) -> dict[str, Any]:
"""Return device state attrs for sensor.""" """Return device state attrs for sensor."""
attrs = {} attrs = {}
if self._channel.measurement_type is not None: if self._cluster_handler.measurement_type is not None:
attrs["measurement_type"] = self._channel.measurement_type attrs["measurement_type"] = self._cluster_handler.measurement_type
max_attr_name = f"{self.SENSOR_ATTR}_max" 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)) attrs[max_attr_name] = str(self.formatter(max_v))
return attrs return attrs
def formatter(self, value: int) -> int | float: def formatter(self, value: int) -> int | float:
"""Return 'normalized' value.""" """Return 'normalized' value."""
multiplier = getattr(self._channel, f"{self._div_mul_prefix}_multiplier") multiplier = getattr(
divisor = getattr(self._channel, f"{self._div_mul_prefix}_divisor") 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 value = float(value * multiplier) / divisor
if value < 100 and divisor > 1: if value < 100 and divisor > 1:
return round(value, self._decimals) return round(value, self._decimals)
@ -284,7 +290,7 @@ class ElectricalMeasurement(Sensor):
await super().async_update() await super().async_update()
@MULTI_MATCH(channel_names=CHANNEL_ELECTRICAL_MEASUREMENT) @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
class ElectricalMeasurementApparentPower( class ElectricalMeasurementApparentPower(
ElectricalMeasurement, id_suffix="apparent_power" ElectricalMeasurement, id_suffix="apparent_power"
): ):
@ -298,7 +304,7 @@ class ElectricalMeasurementApparentPower(
_div_mul_prefix = "ac_power" _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"): class ElectricalMeasurementRMSCurrent(ElectricalMeasurement, id_suffix="rms_current"):
"""RMS current measurement.""" """RMS current measurement."""
@ -310,7 +316,7 @@ class ElectricalMeasurementRMSCurrent(ElectricalMeasurement, id_suffix="rms_curr
_div_mul_prefix = "ac_current" _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"): class ElectricalMeasurementRMSVoltage(ElectricalMeasurement, id_suffix="rms_voltage"):
"""RMS Voltage measurement.""" """RMS Voltage measurement."""
@ -322,7 +328,7 @@ class ElectricalMeasurementRMSVoltage(ElectricalMeasurement, id_suffix="rms_volt
_div_mul_prefix = "ac_voltage" _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"): class ElectricalMeasurementFrequency(ElectricalMeasurement, id_suffix="ac_frequency"):
"""Frequency measurement.""" """Frequency measurement."""
@ -334,7 +340,7 @@ class ElectricalMeasurementFrequency(ElectricalMeasurement, id_suffix="ac_freque
_div_mul_prefix = "ac_frequency" _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"): class ElectricalMeasurementPowerFactor(ElectricalMeasurement, id_suffix="power_factor"):
"""Frequency measurement.""" """Frequency measurement."""
@ -346,9 +352,13 @@ class ElectricalMeasurementPowerFactor(ElectricalMeasurement, id_suffix="power_f
@MULTI_MATCH( @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): class Humidity(Sensor):
"""Humidity sensor.""" """Humidity sensor."""
@ -360,7 +370,7 @@ class Humidity(Sensor):
_attr_native_unit_of_measurement = PERCENTAGE _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): class SoilMoisture(Sensor):
"""Soil Moisture sensor.""" """Soil Moisture sensor."""
@ -372,7 +382,7 @@ class SoilMoisture(Sensor):
_attr_native_unit_of_measurement = PERCENTAGE _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): class LeafWetness(Sensor):
"""Leaf Wetness sensor.""" """Leaf Wetness sensor."""
@ -384,7 +394,7 @@ class LeafWetness(Sensor):
_attr_native_unit_of_measurement = PERCENTAGE _attr_native_unit_of_measurement = PERCENTAGE
@MULTI_MATCH(channel_names=CHANNEL_ILLUMINANCE) @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ILLUMINANCE)
class Illuminance(Sensor): class Illuminance(Sensor):
"""Illuminance Sensor.""" """Illuminance Sensor."""
@ -400,8 +410,8 @@ class Illuminance(Sensor):
@MULTI_MATCH( @MULTI_MATCH(
channel_names=CHANNEL_SMARTENERGY_METERING, cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
stop_on_match_group=CHANNEL_SMARTENERGY_METERING, stop_on_match_group=CLUSTER_HANDLER_SMARTENERGY_METERING,
) )
class SmartEnergyMetering(Sensor): class SmartEnergyMetering(Sensor):
"""Metering sensor.""" """Metering sensor."""
@ -428,21 +438,21 @@ class SmartEnergyMetering(Sensor):
} }
def formatter(self, value: int) -> int | float: def formatter(self, value: int) -> int | float:
"""Pass through channel formatter.""" """Pass through cluster handler formatter."""
return self._channel.demand_formatter(value) return self._cluster_handler.demand_formatter(value)
@property @property
def native_unit_of_measurement(self) -> str | None: def native_unit_of_measurement(self) -> str | None:
"""Return Unit of measurement.""" """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 @property
def extra_state_attributes(self) -> dict[str, Any]: def extra_state_attributes(self) -> dict[str, Any]:
"""Return device state attrs for battery sensors.""" """Return device state attrs for battery sensors."""
attrs = {} attrs = {}
if self._channel.device_type is not None: if self._cluster_handler.device_type is not None:
attrs["device_type"] = self._channel.device_type attrs["device_type"] = self._cluster_handler.device_type
if (status := self._channel.status) is not None: if (status := self._cluster_handler.status) is not None:
if isinstance(status, enum.IntFlag) and sys.version_info >= (3, 11): if isinstance(status, enum.IntFlag) and sys.version_info >= (3, 11):
attrs["status"] = str( attrs["status"] = str(
status.name if status.name is not None else status.value status.name if status.name is not None else status.value
@ -453,8 +463,8 @@ class SmartEnergyMetering(Sensor):
@MULTI_MATCH( @MULTI_MATCH(
channel_names=CHANNEL_SMARTENERGY_METERING, cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
stop_on_match_group=CHANNEL_SMARTENERGY_METERING, stop_on_match_group=CLUSTER_HANDLER_SMARTENERGY_METERING,
) )
class SmartEnergySummation(SmartEnergyMetering, id_suffix="summation_delivered"): class SmartEnergySummation(SmartEnergyMetering, id_suffix="summation_delivered"):
"""Smart Energy Metering summation sensor.""" """Smart Energy Metering summation sensor."""
@ -482,17 +492,20 @@ class SmartEnergySummation(SmartEnergyMetering, id_suffix="summation_delivered")
def formatter(self, value: int) -> int | float: def formatter(self, value: int) -> int | float:
"""Numeric pass-through formatter.""" """Numeric pass-through formatter."""
if self._channel.unit_of_measurement != 0: if self._cluster_handler.unit_of_measurement != 0:
return self._channel.summa_formatter(value) 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) return round(cooked, 3)
@MULTI_MATCH( @MULTI_MATCH(
channel_names=CHANNEL_SMARTENERGY_METERING, cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
models={"TS011F", "ZLinky_TIC"}, models={"TS011F", "ZLinky_TIC"},
stop_on_match_group=CHANNEL_SMARTENERGY_METERING, stop_on_match_group=CLUSTER_HANDLER_SMARTENERGY_METERING,
) )
class PolledSmartEnergySummation(SmartEnergySummation): class PolledSmartEnergySummation(SmartEnergySummation):
"""Polled Smart Energy Metering summation sensor.""" """Polled Smart Energy Metering summation sensor."""
@ -503,11 +516,11 @@ class PolledSmartEnergySummation(SmartEnergySummation):
"""Retrieve latest state.""" """Retrieve latest state."""
if not self.available: if not self.available:
return return
await self._channel.async_force_update() await self._cluster_handler.async_force_update()
@MULTI_MATCH( @MULTI_MATCH(
channel_names=CHANNEL_SMARTENERGY_METERING, cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
models={"ZLinky_TIC"}, models={"ZLinky_TIC"},
) )
class Tier1SmartEnergySummation( class Tier1SmartEnergySummation(
@ -520,7 +533,7 @@ class Tier1SmartEnergySummation(
@MULTI_MATCH( @MULTI_MATCH(
channel_names=CHANNEL_SMARTENERGY_METERING, cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
models={"ZLinky_TIC"}, models={"ZLinky_TIC"},
) )
class Tier2SmartEnergySummation( class Tier2SmartEnergySummation(
@ -533,7 +546,7 @@ class Tier2SmartEnergySummation(
@MULTI_MATCH( @MULTI_MATCH(
channel_names=CHANNEL_SMARTENERGY_METERING, cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
models={"ZLinky_TIC"}, models={"ZLinky_TIC"},
) )
class Tier3SmartEnergySummation( class Tier3SmartEnergySummation(
@ -546,7 +559,7 @@ class Tier3SmartEnergySummation(
@MULTI_MATCH( @MULTI_MATCH(
channel_names=CHANNEL_SMARTENERGY_METERING, cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
models={"ZLinky_TIC"}, models={"ZLinky_TIC"},
) )
class Tier4SmartEnergySummation( class Tier4SmartEnergySummation(
@ -559,7 +572,7 @@ class Tier4SmartEnergySummation(
@MULTI_MATCH( @MULTI_MATCH(
channel_names=CHANNEL_SMARTENERGY_METERING, cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
models={"ZLinky_TIC"}, models={"ZLinky_TIC"},
) )
class Tier5SmartEnergySummation( class Tier5SmartEnergySummation(
@ -572,7 +585,7 @@ class Tier5SmartEnergySummation(
@MULTI_MATCH( @MULTI_MATCH(
channel_names=CHANNEL_SMARTENERGY_METERING, cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
models={"ZLinky_TIC"}, models={"ZLinky_TIC"},
) )
class Tier6SmartEnergySummation( class Tier6SmartEnergySummation(
@ -584,7 +597,7 @@ class Tier6SmartEnergySummation(
_attr_name: str = "Tier 6 summation delivered" _attr_name: str = "Tier 6 summation delivered"
@MULTI_MATCH(channel_names=CHANNEL_PRESSURE) @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_PRESSURE)
class Pressure(Sensor): class Pressure(Sensor):
"""Pressure sensor.""" """Pressure sensor."""
@ -596,7 +609,7 @@ class Pressure(Sensor):
_attr_native_unit_of_measurement = UnitOfPressure.HPA _attr_native_unit_of_measurement = UnitOfPressure.HPA
@MULTI_MATCH(channel_names=CHANNEL_TEMPERATURE) @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_TEMPERATURE)
class Temperature(Sensor): class Temperature(Sensor):
"""Temperature Sensor.""" """Temperature Sensor."""
@ -608,7 +621,7 @@ class Temperature(Sensor):
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS _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): class DeviceTemperature(Sensor):
"""Device Temperature Sensor.""" """Device Temperature Sensor."""
@ -621,7 +634,7 @@ class DeviceTemperature(Sensor):
_attr_entity_category = EntityCategory.DIAGNOSTIC _attr_entity_category = EntityCategory.DIAGNOSTIC
@MULTI_MATCH(channel_names="carbon_dioxide_concentration") @MULTI_MATCH(cluster_handler_names="carbon_dioxide_concentration")
class CarbonDioxideConcentration(Sensor): class CarbonDioxideConcentration(Sensor):
"""Carbon Dioxide Concentration sensor.""" """Carbon Dioxide Concentration sensor."""
@ -634,7 +647,7 @@ class CarbonDioxideConcentration(Sensor):
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION _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): class CarbonMonoxideConcentration(Sensor):
"""Carbon Monoxide Concentration sensor.""" """Carbon Monoxide Concentration sensor."""
@ -647,8 +660,8 @@ class CarbonMonoxideConcentration(Sensor):
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION _attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
@MULTI_MATCH(generic_ids="channel_0x042e", stop_on_match_group="voc_level") @MULTI_MATCH(generic_ids="cluster_handler_0x042e", stop_on_match_group="voc_level")
@MULTI_MATCH(channel_names="voc_level", stop_on_match_group="voc_level") @MULTI_MATCH(cluster_handler_names="voc_level", stop_on_match_group="voc_level")
class VOCLevel(Sensor): class VOCLevel(Sensor):
"""VOC Level sensor.""" """VOC Level sensor."""
@ -662,7 +675,7 @@ class VOCLevel(Sensor):
@MULTI_MATCH( @MULTI_MATCH(
channel_names="voc_level", cluster_handler_names="voc_level",
models="lumi.airmonitor.acn01", models="lumi.airmonitor.acn01",
stop_on_match_group="voc_level", stop_on_match_group="voc_level",
) )
@ -678,7 +691,7 @@ class PPBVOCLevel(Sensor):
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_BILLION _attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_BILLION
@MULTI_MATCH(channel_names="pm25") @MULTI_MATCH(cluster_handler_names="pm25")
class PM25(Sensor): class PM25(Sensor):
"""Particulate Matter 2.5 microns or less 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 _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): class FormaldehydeConcentration(Sensor):
"""Formaldehyde Concentration sensor.""" """Formaldehyde Concentration sensor."""
@ -702,7 +715,10 @@ class FormaldehydeConcentration(Sensor):
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION _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"): class ThermostatHVACAction(Sensor, id_suffix="hvac_action"):
"""Thermostat HVAC action sensor.""" """Thermostat HVAC action sensor."""
@ -713,7 +729,7 @@ class ThermostatHVACAction(Sensor, id_suffix="hvac_action"):
cls, cls,
unique_id: str, unique_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
**kwargs: Any, **kwargs: Any,
) -> Self | None: ) -> Self | None:
"""Entity Factory. """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 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 @property
def native_value(self) -> str | None: def native_value(self) -> str | None:
"""Return the current HVAC action.""" """Return the current HVAC action."""
if ( if (
self._channel.pi_heating_demand is None self._cluster_handler.pi_heating_demand is None
and self._channel.pi_cooling_demand is None and self._cluster_handler.pi_cooling_demand is None
): ):
return self._rm_rs_action return self._rm_rs_action
return self._pi_demand_action return self._pi_demand_action
@ -737,36 +753,36 @@ class ThermostatHVACAction(Sensor, id_suffix="hvac_action"):
def _rm_rs_action(self) -> HVACAction | None: def _rm_rs_action(self) -> HVACAction | None:
"""Return the current HVAC action based on running mode and running state.""" """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 return None
rs_heat = ( rs_heat = (
self._channel.RunningState.Heat_State_On self._cluster_handler.RunningState.Heat_State_On
| self._channel.RunningState.Heat_2nd_Stage_On | self._cluster_handler.RunningState.Heat_2nd_Stage_On
) )
if running_state & rs_heat: if running_state & rs_heat:
return HVACAction.HEATING return HVACAction.HEATING
rs_cool = ( rs_cool = (
self._channel.RunningState.Cool_State_On self._cluster_handler.RunningState.Cool_State_On
| self._channel.RunningState.Cool_2nd_Stage_On | self._cluster_handler.RunningState.Cool_2nd_Stage_On
) )
if running_state & rs_cool: if running_state & rs_cool:
return HVACAction.COOLING return HVACAction.COOLING
running_state = self._channel.running_state running_state = self._cluster_handler.running_state
if running_state and running_state & ( if running_state and running_state & (
self._channel.RunningState.Fan_State_On self._cluster_handler.RunningState.Fan_State_On
| self._channel.RunningState.Fan_2nd_Stage_On | self._cluster_handler.RunningState.Fan_2nd_Stage_On
| self._channel.RunningState.Fan_3rd_Stage_On | self._cluster_handler.RunningState.Fan_3rd_Stage_On
): ):
return HVACAction.FAN return HVACAction.FAN
running_state = self._channel.running_state running_state = self._cluster_handler.running_state
if running_state and running_state & self._channel.RunningState.Idle: if running_state and running_state & self._cluster_handler.RunningState.Idle:
return HVACAction.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.IDLE
return HVACAction.OFF return HVACAction.OFF
@ -774,27 +790,27 @@ class ThermostatHVACAction(Sensor, id_suffix="hvac_action"):
def _pi_demand_action(self) -> HVACAction: def _pi_demand_action(self) -> HVACAction:
"""Return the current HVAC action based on pi_demands.""" """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: if heating_demand is not None and heating_demand > 0:
return HVACAction.HEATING 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: if cooling_demand is not None and cooling_demand > 0:
return HVACAction.COOLING 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.IDLE
return HVACAction.OFF return HVACAction.OFF
@callback @callback
def async_set_state(self, *args, **kwargs) -> None: def async_set_state(self, *args, **kwargs) -> None:
"""Handle state update from channel.""" """Handle state update from cluster handler."""
self.async_write_ha_state() self.async_write_ha_state()
@MULTI_MATCH( @MULTI_MATCH(
channel_names={CHANNEL_THERMOSTAT}, cluster_handler_names={CLUSTER_HANDLER_THERMOSTAT},
manufacturers="Sinope Technologies", manufacturers="Sinope Technologies",
stop_on_match_group=CHANNEL_THERMOSTAT, stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT,
) )
class SinopeHVACAction(ThermostatHVACAction): class SinopeHVACAction(ThermostatHVACAction):
"""Sinope Thermostat HVAC action sensor.""" """Sinope Thermostat HVAC action sensor."""
@ -803,28 +819,28 @@ class SinopeHVACAction(ThermostatHVACAction):
def _rm_rs_action(self) -> HVACAction: def _rm_rs_action(self) -> HVACAction:
"""Return the current HVAC action based on running mode and running state.""" """Return the current HVAC action based on running mode and running state."""
running_mode = self._channel.running_mode running_mode = self._cluster_handler.running_mode
if running_mode == self._channel.RunningMode.Heat: if running_mode == self._cluster_handler.RunningMode.Heat:
return HVACAction.HEATING return HVACAction.HEATING
if running_mode == self._channel.RunningMode.Cool: if running_mode == self._cluster_handler.RunningMode.Cool:
return HVACAction.COOLING return HVACAction.COOLING
running_state = self._channel.running_state running_state = self._cluster_handler.running_state
if running_state and running_state & ( if running_state and running_state & (
self._channel.RunningState.Fan_State_On self._cluster_handler.RunningState.Fan_State_On
| self._channel.RunningState.Fan_2nd_Stage_On | self._cluster_handler.RunningState.Fan_2nd_Stage_On
| self._channel.RunningState.Fan_3rd_Stage_On | self._cluster_handler.RunningState.Fan_3rd_Stage_On
): ):
return HVACAction.FAN return HVACAction.FAN
if ( if (
self._channel.system_mode != self._channel.SystemMode.Off self._cluster_handler.system_mode != self._cluster_handler.SystemMode.Off
and running_mode == self._channel.SystemMode.Off and running_mode == self._cluster_handler.SystemMode.Off
): ):
return HVACAction.IDLE return HVACAction.IDLE
return HVACAction.OFF return HVACAction.OFF
@MULTI_MATCH(channel_names=CHANNEL_BASIC) @MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_BASIC)
class RSSISensor(Sensor, id_suffix="rssi"): class RSSISensor(Sensor, id_suffix="rssi"):
"""RSSI sensor for a device.""" """RSSI sensor for a device."""
@ -842,17 +858,17 @@ class RSSISensor(Sensor, id_suffix="rssi"):
cls, cls,
unique_id: str, unique_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
**kwargs: Any, **kwargs: Any,
) -> Self | None: ) -> Self | None:
"""Entity Factory. """Entity Factory.
Return entity if it is a supported configuration, otherwise return None 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): if ZHA_ENTITIES.prevent_entity_creation(Platform.SENSOR, zha_device.ieee, key):
return None return None
return cls(unique_id, zha_device, channels, **kwargs) return cls(unique_id, zha_device, cluster_handlers, **kwargs)
@property @property
def native_value(self) -> StateType: 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) 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"): class LQISensor(RSSISensor, id_suffix="lqi"):
"""LQI sensor for a device.""" """LQI sensor for a device."""
@ -870,7 +886,7 @@ class LQISensor(RSSISensor, id_suffix="lqi"):
@MULTI_MATCH( @MULTI_MATCH(
channel_names="tuya_manufacturer", cluster_handler_names="tuya_manufacturer",
manufacturers={ manufacturers={
"_TZE200_htnnfasr", "_TZE200_htnnfasr",
}, },
@ -885,7 +901,7 @@ class TimeLeft(Sensor, id_suffix="time_left"):
_attr_native_unit_of_measurement = UnitOfTime.MINUTES _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"): class IkeaDeviceRunTime(Sensor, id_suffix="device_run_time"):
"""Sensor that displays device run time (in minutes).""" """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 _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"): class IkeaFilterRunTime(Sensor, id_suffix="filter_run_time"):
"""Sensor that displays run time of the current filter (in minutes).""" """Sensor that displays run time of the current filter (in minutes)."""
@ -914,7 +930,7 @@ class AqaraFeedingSource(types.enum8):
HomeAssistant = 0x02 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"): class AqaraPetFeederLastFeedingSource(Sensor, id_suffix="last_feeding_source"):
"""Sensor that displays the last feeding source of pet feeder.""" """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 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"): class AqaraPetFeederLastFeedingSize(Sensor, id_suffix="last_feeding_size"):
"""Sensor that displays the last feeding size of the pet feeder.""" """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" _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"): class AqaraPetFeederPortionsDispensed(Sensor, id_suffix="portions_dispensed"):
"""Sensor that displays the number of portions dispensed by the pet feeder.""" """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" _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"): class AqaraPetFeederWeightDispensed(Sensor, id_suffix="weight_dispensed"):
"""Sensor that displays the weight dispensed by the pet feeder.""" """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" _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"): class AqaraSmokeDensityDbm(Sensor, id_suffix="smoke_density_dbm"):
"""Sensor that displays the smoke density of an Aqara smoke sensor in dB/m.""" """Sensor that displays the smoke density of an Aqara smoke sensor in dB/m."""

View File

@ -22,9 +22,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_call_later from homeassistant.helpers.event import async_call_later
from .core import discovery from .core import discovery
from .core.channels.security import IasWd from .core.cluster_handlers.security import IasWd
from .core.const import ( from .core.const import (
CHANNEL_IAS_WD, CLUSTER_HANDLER_IAS_WD,
DATA_ZHA, DATA_ZHA,
SIGNAL_ADD_ENTITIES, SIGNAL_ADD_ENTITIES,
WARNING_DEVICE_MODE_BURGLAR, WARNING_DEVICE_MODE_BURGLAR,
@ -43,7 +43,7 @@ from .core.registries import ZHA_ENTITIES
from .entity import ZhaEntity from .entity import ZhaEntity
if TYPE_CHECKING: if TYPE_CHECKING:
from .core.channels.base import ZigbeeChannel from .core.cluster_handlers import ClusterHandler
from .core.device import ZHADevice from .core.device import ZHADevice
MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.SIREN) 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) 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): class ZHASiren(ZhaEntity, SirenEntity):
"""Representation of a ZHA siren.""" """Representation of a ZHA siren."""
@ -78,7 +78,7 @@ class ZHASiren(ZhaEntity, SirenEntity):
self, self,
unique_id: str, unique_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
**kwargs, **kwargs,
) -> None: ) -> None:
"""Init this siren.""" """Init this siren."""
@ -97,8 +97,8 @@ class ZHASiren(ZhaEntity, SirenEntity):
WARNING_DEVICE_MODE_FIRE_PANIC: "Fire Panic", WARNING_DEVICE_MODE_FIRE_PANIC: "Fire Panic",
WARNING_DEVICE_MODE_EMERGENCY_PANIC: "Emergency Panic", WARNING_DEVICE_MODE_EMERGENCY_PANIC: "Emergency Panic",
} }
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
self._channel: IasWd = cast(IasWd, channels[0]) self._cluster_handler: IasWd = cast(IasWd, cluster_handlers[0])
self._attr_is_on: bool = False self._attr_is_on: bool = False
self._off_listener: Callable[[], None] | None = None self._off_listener: Callable[[], None] | None = None
@ -107,22 +107,28 @@ class ZHASiren(ZhaEntity, SirenEntity):
if self._off_listener: if self._off_listener:
self._off_listener() self._off_listener()
self._off_listener = None 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 = ( siren_tone = (
tone_cache.value tone_cache.value
if tone_cache is not None if tone_cache is not None
else WARNING_DEVICE_MODE_EMERGENCY else WARNING_DEVICE_MODE_EMERGENCY
) )
siren_duration = DEFAULT_DURATION 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 = ( siren_level = (
level_cache.value if level_cache is not None else WARNING_DEVICE_SOUND_HIGH 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 = ( should_strobe = (
strobe_cache.value if strobe_cache is not None else Strobe.No_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 = (
strobe_level_cache.value strobe_level_cache.value
if strobe_level_cache is not None if strobe_level_cache is not None
@ -134,7 +140,7 @@ class ZHASiren(ZhaEntity, SirenEntity):
siren_tone = tone siren_tone = tone
if (level := kwargs.get(ATTR_VOLUME_LEVEL)) is not None: if (level := kwargs.get(ATTR_VOLUME_LEVEL)) is not None:
siren_level = int(level) siren_level = int(level)
await self._channel.issue_start_warning( await self._cluster_handler.issue_start_warning(
mode=siren_tone, mode=siren_tone,
warning_duration=siren_duration, warning_duration=siren_duration,
siren_level=siren_level, siren_level=siren_level,
@ -150,7 +156,7 @@ class ZHASiren(ZhaEntity, SirenEntity):
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn off siren.""" """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 mode=WARNING_DEVICE_MODE_STOP, strobe=WARNING_DEVICE_STROBE_NO
) )
self._attr_is_on = False self._attr_is_on = False

View File

@ -19,9 +19,9 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .core import discovery from .core import discovery
from .core.const import ( from .core.const import (
CHANNEL_BASIC, CLUSTER_HANDLER_BASIC,
CHANNEL_INOVELLI, CLUSTER_HANDLER_INOVELLI,
CHANNEL_ON_OFF, CLUSTER_HANDLER_ON_OFF,
DATA_ZHA, DATA_ZHA,
SIGNAL_ADD_ENTITIES, SIGNAL_ADD_ENTITIES,
SIGNAL_ATTR_UPDATED, SIGNAL_ATTR_UPDATED,
@ -30,7 +30,7 @@ from .core.registries import ZHA_ENTITIES
from .entity import ZhaEntity, ZhaGroupEntity from .entity import ZhaEntity, ZhaGroupEntity
if TYPE_CHECKING: if TYPE_CHECKING:
from .core.channels.base import ZigbeeChannel from .core.cluster_handlers import ClusterHandler
from .core.device import ZHADevice from .core.device import ZHADevice
STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.SWITCH) 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) 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): class Switch(ZhaEntity, SwitchEntity):
"""ZHA switch.""" """ZHA switch."""
@ -68,51 +68,53 @@ class Switch(ZhaEntity, SwitchEntity):
self, self,
unique_id: str, unique_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
"""Initialize the ZHA switch.""" """Initialize the ZHA switch."""
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
self._on_off_channel = self.cluster_channels[CHANNEL_ON_OFF] self._on_off_cluster_handler = self.cluster_handlers[CLUSTER_HANDLER_ON_OFF]
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return if the switch is on based on the statemachine.""" """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 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: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on.""" """Turn the entity on."""
result = await self._on_off_channel.turn_on() result = await self._on_off_cluster_handler.turn_on()
if not result: if not result:
return return
self.async_write_ha_state() self.async_write_ha_state()
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off.""" """Turn the entity off."""
result = await self._on_off_channel.turn_off() result = await self._on_off_cluster_handler.turn_off()
if not result: if not result:
return return
self.async_write_ha_state() self.async_write_ha_state()
@callback @callback
def async_set_state(self, attr_id: int, attr_name: str, value: Any): 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() self.async_write_ha_state()
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Run when about to be added to hass.""" """Run when about to be added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
self.async_accept_signal( 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: async def async_update(self) -> None:
"""Attempt to retrieve on off state from the switch.""" """Attempt to retrieve on off state from the switch."""
await super().async_update() await super().async_update()
if self._on_off_channel: if self._on_off_cluster_handler:
await self._on_off_channel.get_attribute_value("on_off", from_cache=False) await self._on_off_cluster_handler.get_attribute_value(
"on_off", from_cache=False
)
@GROUP_MATCH() @GROUP_MATCH()
@ -132,7 +134,7 @@ class SwitchGroup(ZhaGroupEntity, SwitchEntity):
self._available: bool self._available: bool
self._state: bool self._state: bool
group = self.zha_device.gateway.get_group(self._group_id) 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 @property
def is_on(self) -> bool: def is_on(self) -> bool:
@ -141,7 +143,7 @@ class SwitchGroup(ZhaGroupEntity, SwitchEntity):
async def async_turn_on(self, **kwargs: Any) -> None: async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on.""" """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: if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
return return
self._state = True self._state = True
@ -149,7 +151,7 @@ class SwitchGroup(ZhaGroupEntity, SwitchEntity):
async def async_turn_off(self, **kwargs: Any) -> None: async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the entity off.""" """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: if isinstance(result, Exception) or result[1] is not Status.SUCCESS:
return return
self._state = False self._state = False
@ -178,17 +180,17 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity):
cls, cls,
unique_id: str, unique_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
**kwargs: Any, **kwargs: Any,
) -> Self | None: ) -> Self | None:
"""Entity Factory. """Entity Factory.
Return entity if it is a supported configuration, otherwise return None Return entity if it is a supported configuration, otherwise return None
""" """
channel = channels[0] cluster_handler = cluster_handlers[0]
if ( if (
cls._zcl_attribute in channel.cluster.unsupported_attributes cls._zcl_attribute in cluster_handler.cluster.unsupported_attributes
or channel.cluster.get(cls._zcl_attribute) is None or cluster_handler.cluster.get(cls._zcl_attribute) is None
): ):
_LOGGER.debug( _LOGGER.debug(
"%s is not supported - skipping %s entity creation", "%s is not supported - skipping %s entity creation",
@ -197,48 +199,48 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity):
) )
return None return None
return cls(unique_id, zha_device, channels, **kwargs) return cls(unique_id, zha_device, cluster_handlers, **kwargs)
def __init__( def __init__(
self, self,
unique_id: str, unique_id: str,
zha_device: ZHADevice, zha_device: ZHADevice,
channels: list[ZigbeeChannel], cluster_handlers: list[ClusterHandler],
**kwargs: Any, **kwargs: Any,
) -> None: ) -> None:
"""Init this number configuration entity.""" """Init this number configuration entity."""
self._channel: ZigbeeChannel = channels[0] self._cluster_handler: ClusterHandler = cluster_handlers[0]
super().__init__(unique_id, zha_device, channels, **kwargs) super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
async def async_added_to_hass(self) -> None: async def async_added_to_hass(self) -> None:
"""Run when about to be added to hass.""" """Run when about to be added to hass."""
await super().async_added_to_hass() await super().async_added_to_hass()
self.async_accept_signal( self.async_accept_signal(
self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state self._cluster_handler, SIGNAL_ATTR_UPDATED, self.async_set_state
) )
@callback @callback
def async_set_state(self, attr_id: int, attr_name: str, value: Any): 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() self.async_write_ha_state()
@property @property
def inverted(self) -> bool: def inverted(self) -> bool:
"""Return True if the switch is inverted.""" """Return True if the switch is inverted."""
if self._zcl_inverter_attribute: 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 return self._force_inverted
@property @property
def is_on(self) -> bool: def is_on(self) -> bool:
"""Return if the switch is on based on the statemachine.""" """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 return (not val) if self.inverted else val
async def async_turn_on_off(self, state: bool) -> None: async def async_turn_on_off(self, state: bool) -> None:
"""Turn the entity on or off.""" """Turn the entity on or off."""
try: 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} {self._zcl_attribute: not state if self.inverted else state}
) )
except zigpy.exceptions.ZigbeeException as ex: except zigpy.exceptions.ZigbeeException as ex:
@ -261,18 +263,18 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity):
"""Attempt to retrieve the state of the entity.""" """Attempt to retrieve the state of the entity."""
await super().async_update() await super().async_update()
self.error("Polling current state") self.error("Polling current state")
if self._channel: if self._cluster_handler:
value = await self._channel.get_attribute_value( value = await self._cluster_handler.get_attribute_value(
self._zcl_attribute, from_cache=False 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._zcl_inverter_attribute, from_cache=False
) )
self.debug("read value=%s, inverted=%s", value, self.inverted) self.debug("read value=%s, inverted=%s", value, self.inverted)
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names="tuya_manufacturer", cluster_handler_names="tuya_manufacturer",
manufacturers={ manufacturers={
"_TZE200_b6wax7g0", "_TZE200_b6wax7g0",
}, },
@ -286,7 +288,9 @@ class OnOffWindowDetectionFunctionConfigurationEntity(
_zcl_inverter_attribute: str = "window_detection_function_inverter" _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( class P1MotionTriggerIndicatorSwitch(
ZHASwitchConfigurationEntity, id_suffix="trigger_indicator" ZHASwitchConfigurationEntity, id_suffix="trigger_indicator"
): ):
@ -297,7 +301,8 @@ class P1MotionTriggerIndicatorSwitch(
@CONFIG_DIAGNOSTIC_MATCH( @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( class XiaomiPlugPowerOutageMemorySwitch(
ZHASwitchConfigurationEntity, id_suffix="power_outage_memory" ZHASwitchConfigurationEntity, id_suffix="power_outage_memory"
@ -309,7 +314,7 @@ class XiaomiPlugPowerOutageMemorySwitch(
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_BASIC, cluster_handler_names=CLUSTER_HANDLER_BASIC,
manufacturers={"Philips", "Signify Netherlands B.V."}, manufacturers={"Philips", "Signify Netherlands B.V."},
models={"SML001", "SML002", "SML003", "SML004"}, models={"SML001", "SML002", "SML003", "SML004"},
) )
@ -323,7 +328,7 @@ class HueMotionTriggerIndicatorSwitch(
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names="ikea_airpurifier", cluster_handler_names="ikea_airpurifier",
models={"STARKVIND Air purifier", "STARKVIND Air purifier table"}, models={"STARKVIND Air purifier", "STARKVIND Air purifier table"},
) )
class ChildLock(ZHASwitchConfigurationEntity, id_suffix="child_lock"): class ChildLock(ZHASwitchConfigurationEntity, id_suffix="child_lock"):
@ -334,7 +339,7 @@ class ChildLock(ZHASwitchConfigurationEntity, id_suffix="child_lock"):
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names="ikea_airpurifier", cluster_handler_names="ikea_airpurifier",
models={"STARKVIND Air purifier", "STARKVIND Air purifier table"}, models={"STARKVIND Air purifier", "STARKVIND Air purifier table"},
) )
class DisableLed(ZHASwitchConfigurationEntity, id_suffix="disable_led"): class DisableLed(ZHASwitchConfigurationEntity, id_suffix="disable_led"):
@ -345,7 +350,7 @@ class DisableLed(ZHASwitchConfigurationEntity, id_suffix="disable_led"):
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI, cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
) )
class InovelliInvertSwitch(ZHASwitchConfigurationEntity, id_suffix="invert_switch"): class InovelliInvertSwitch(ZHASwitchConfigurationEntity, id_suffix="invert_switch"):
"""Inovelli invert switch control.""" """Inovelli invert switch control."""
@ -355,7 +360,7 @@ class InovelliInvertSwitch(ZHASwitchConfigurationEntity, id_suffix="invert_switc
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI, cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
) )
class InovelliSmartBulbMode(ZHASwitchConfigurationEntity, id_suffix="smart_bulb_mode"): class InovelliSmartBulbMode(ZHASwitchConfigurationEntity, id_suffix="smart_bulb_mode"):
"""Inovelli smart bulb mode control.""" """Inovelli smart bulb mode control."""
@ -365,7 +370,7 @@ class InovelliSmartBulbMode(ZHASwitchConfigurationEntity, id_suffix="smart_bulb_
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI, cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
) )
class InovelliDoubleTapUpEnabled( class InovelliDoubleTapUpEnabled(
ZHASwitchConfigurationEntity, id_suffix="double_tap_up_enabled" ZHASwitchConfigurationEntity, id_suffix="double_tap_up_enabled"
@ -377,7 +382,7 @@ class InovelliDoubleTapUpEnabled(
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI, cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
) )
class InovelliDoubleTapDownEnabled( class InovelliDoubleTapDownEnabled(
ZHASwitchConfigurationEntity, id_suffix="double_tap_down_enabled" ZHASwitchConfigurationEntity, id_suffix="double_tap_down_enabled"
@ -389,7 +394,7 @@ class InovelliDoubleTapDownEnabled(
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI, cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
) )
class InovelliAuxSwitchScenes( class InovelliAuxSwitchScenes(
ZHASwitchConfigurationEntity, id_suffix="aux_switch_scenes" ZHASwitchConfigurationEntity, id_suffix="aux_switch_scenes"
@ -401,7 +406,7 @@ class InovelliAuxSwitchScenes(
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI, cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
) )
class InovelliBindingOffToOnSyncLevel( class InovelliBindingOffToOnSyncLevel(
ZHASwitchConfigurationEntity, id_suffix="binding_off_to_on_sync_level" ZHASwitchConfigurationEntity, id_suffix="binding_off_to_on_sync_level"
@ -413,7 +418,7 @@ class InovelliBindingOffToOnSyncLevel(
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI, cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
) )
class InovelliLocalProtection( class InovelliLocalProtection(
ZHASwitchConfigurationEntity, id_suffix="local_protection" ZHASwitchConfigurationEntity, id_suffix="local_protection"
@ -425,7 +430,7 @@ class InovelliLocalProtection(
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI, cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
) )
class InovelliOnOffLEDMode(ZHASwitchConfigurationEntity, id_suffix="on_off_led_mode"): class InovelliOnOffLEDMode(ZHASwitchConfigurationEntity, id_suffix="on_off_led_mode"):
"""Inovelli only 1 LED mode control.""" """Inovelli only 1 LED mode control."""
@ -435,7 +440,7 @@ class InovelliOnOffLEDMode(ZHASwitchConfigurationEntity, id_suffix="on_off_led_m
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI, cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
) )
class InovelliFirmwareProgressLED( class InovelliFirmwareProgressLED(
ZHASwitchConfigurationEntity, id_suffix="firmware_progress_led" ZHASwitchConfigurationEntity, id_suffix="firmware_progress_led"
@ -447,7 +452,7 @@ class InovelliFirmwareProgressLED(
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI, cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
) )
class InovelliRelayClickInOnOffMode( class InovelliRelayClickInOnOffMode(
ZHASwitchConfigurationEntity, id_suffix="relay_click_in_on_off_mode" ZHASwitchConfigurationEntity, id_suffix="relay_click_in_on_off_mode"
@ -459,7 +464,7 @@ class InovelliRelayClickInOnOffMode(
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_INOVELLI, cluster_handler_names=CLUSTER_HANDLER_INOVELLI,
) )
class InovelliDisableDoubleTapClearNotificationsMode( class InovelliDisableDoubleTapClearNotificationsMode(
ZHASwitchConfigurationEntity, id_suffix="disable_clear_notifications_double_tap" ZHASwitchConfigurationEntity, id_suffix="disable_clear_notifications_double_tap"
@ -470,7 +475,9 @@ class InovelliDisableDoubleTapClearNotificationsMode(
_attr_name: str = "Disable config 2x tap to clear notifications" _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( class AqaraPetFeederLEDIndicator(
ZHASwitchConfigurationEntity, id_suffix="disable_led_indicator" ZHASwitchConfigurationEntity, id_suffix="disable_led_indicator"
): ):
@ -482,7 +489,9 @@ class AqaraPetFeederLEDIndicator(
_attr_icon: str = "mdi:led-on" _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"): class AqaraPetFeederChildLock(ZHASwitchConfigurationEntity, id_suffix="child_lock"):
"""Representation of a child lock configuration entity.""" """Representation of a child lock configuration entity."""
@ -492,7 +501,7 @@ class AqaraPetFeederChildLock(ZHASwitchConfigurationEntity, id_suffix="child_loc
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names=CHANNEL_ON_OFF, cluster_handler_names=CLUSTER_HANDLER_ON_OFF,
models={"TS011F"}, models={"TS011F"},
) )
class TuyaChildLockSwitch(ZHASwitchConfigurationEntity, id_suffix="child_lock"): class TuyaChildLockSwitch(ZHASwitchConfigurationEntity, id_suffix="child_lock"):
@ -503,7 +512,9 @@ class TuyaChildLockSwitch(ZHASwitchConfigurationEntity, id_suffix="child_lock"):
_attr_icon: str = "mdi:account-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( class AqaraThermostatWindowDetection(
ZHASwitchConfigurationEntity, id_suffix="window_detection" ZHASwitchConfigurationEntity, id_suffix="window_detection"
): ):
@ -513,7 +524,9 @@ class AqaraThermostatWindowDetection(
_attr_name = "Window detection" _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( class AqaraThermostatValveDetection(
ZHASwitchConfigurationEntity, id_suffix="valve_detection" ZHASwitchConfigurationEntity, id_suffix="valve_detection"
): ):
@ -523,7 +536,9 @@ class AqaraThermostatValveDetection(
_attr_name = "Valve detection" _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"): class AqaraThermostatChildLock(ZHASwitchConfigurationEntity, id_suffix="child_lock"):
"""Representation of an Aqara thermostat child lock configuration entity.""" """Representation of an Aqara thermostat child lock configuration entity."""
@ -533,7 +548,7 @@ class AqaraThermostatChildLock(ZHASwitchConfigurationEntity, id_suffix="child_lo
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names="opple_cluster", models={"lumi.sensor_smoke.acn03"} cluster_handler_names="opple_cluster", models={"lumi.sensor_smoke.acn03"}
) )
class AqaraHeartbeatIndicator( class AqaraHeartbeatIndicator(
ZHASwitchConfigurationEntity, id_suffix="heartbeat_indicator" ZHASwitchConfigurationEntity, id_suffix="heartbeat_indicator"
@ -546,7 +561,7 @@ class AqaraHeartbeatIndicator(
@CONFIG_DIAGNOSTIC_MATCH( @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"): class AqaraLinkageAlarm(ZHASwitchConfigurationEntity, id_suffix="linkage_alarm"):
"""Representation of a linkage alarm configuration entity for Aqara smoke sensors.""" """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( @CONFIG_DIAGNOSTIC_MATCH(
channel_names="opple_cluster", models={"lumi.sensor_smoke.acn03"} cluster_handler_names="opple_cluster", models={"lumi.sensor_smoke.acn03"}
) )
class AqaraBuzzerManualMute( class AqaraBuzzerManualMute(
ZHASwitchConfigurationEntity, id_suffix="buzzer_manual_mute" ZHASwitchConfigurationEntity, id_suffix="buzzer_manual_mute"
@ -570,7 +585,7 @@ class AqaraBuzzerManualMute(
@CONFIG_DIAGNOSTIC_MATCH( @CONFIG_DIAGNOSTIC_MATCH(
channel_names="opple_cluster", models={"lumi.sensor_smoke.acn03"} cluster_handler_names="opple_cluster", models={"lumi.sensor_smoke.acn03"}
) )
class AqaraBuzzerManualAlarm( class AqaraBuzzerManualAlarm(
ZHASwitchConfigurationEntity, id_suffix="buzzer_manual_alarm" ZHASwitchConfigurationEntity, id_suffix="buzzer_manual_alarm"

View File

@ -40,10 +40,10 @@ from .core.const import (
ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE, ATTR_WARNING_DEVICE_STROBE_DUTY_CYCLE,
ATTR_WARNING_DEVICE_STROBE_INTENSITY, ATTR_WARNING_DEVICE_STROBE_INTENSITY,
BINDINGS, BINDINGS,
CHANNEL_IAS_WD,
CLUSTER_COMMAND_SERVER, CLUSTER_COMMAND_SERVER,
CLUSTER_COMMANDS_CLIENT, CLUSTER_COMMANDS_CLIENT,
CLUSTER_COMMANDS_SERVER, CLUSTER_COMMANDS_SERVER,
CLUSTER_HANDLER_IAS_WD,
CLUSTER_TYPE_IN, CLUSTER_TYPE_IN,
CLUSTER_TYPE_OUT, CLUSTER_TYPE_OUT,
CUSTOM_CONFIGURATION, CUSTOM_CONFIGURATION,
@ -61,7 +61,7 @@ from .core.const import (
WARNING_DEVICE_STROBE_HIGH, WARNING_DEVICE_STROBE_HIGH,
WARNING_DEVICE_STROBE_YES, WARNING_DEVICE_STROBE_YES,
ZHA_ALARM_OPTIONS, ZHA_ALARM_OPTIONS,
ZHA_CHANNEL_MSG, ZHA_CLUSTER_HANDLER_MSG,
ZHA_CONFIG_SCHEMAS, ZHA_CONFIG_SCHEMAS,
) )
from .core.gateway import EntityReference from .core.gateway import EntityReference
@ -389,7 +389,7 @@ async def websocket_get_groupable_devices(
), ),
} }
for entity_ref in entity_refs for entity_ref in entity_refs
if list(entity_ref.cluster_channels.values())[ if list(entity_ref.cluster_handlers.values())[
0 0
].cluster.endpoint.endpoint_id ].cluster.endpoint.endpoint_id
== ep_id == ep_id
@ -597,7 +597,7 @@ async def websocket_reconfigure_node(
connection.send_message(websocket_api.event_message(msg["id"], data)) connection.send_message(websocket_api.event_message(msg["id"], data))
remove_dispatcher_function = async_dispatcher_connect( remove_dispatcher_function = async_dispatcher_connect(
hass, ZHA_CHANNEL_MSG, forward_messages hass, ZHA_CLUSTER_HANDLER_MSG, forward_messages
) )
@callback @callback
@ -1406,14 +1406,14 @@ def async_load_api(hass: HomeAssistant) -> None:
schema=SERVICE_SCHEMAS[SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND], schema=SERVICE_SCHEMAS[SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND],
) )
def _get_ias_wd_channel(zha_device): def _get_ias_wd_cluster_handler(zha_device):
"""Get the IASWD channel for a device.""" """Get the IASWD cluster handler for a device."""
cluster_channels = { cluster_handlers = {
ch.name: ch ch.name: ch
for pool in zha_device.channels.pools for endpoint in zha_device.endpoints.values()
for ch in pool.claimed_channels.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: async def warning_device_squawk(service: ServiceCall) -> None:
"""Issue the squawk command for an IAS warning device.""" """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] level: int = service.data[ATTR_LEVEL]
if (zha_device := zha_gateway.get_device(ieee)) is not None: if (zha_device := zha_gateway.get_device(ieee)) is not None:
if channel := _get_ias_wd_channel(zha_device): if cluster_handler := _get_ias_wd_cluster_handler(zha_device):
await channel.issue_squawk(mode, strobe, level) await cluster_handler.issue_squawk(mode, strobe, level)
else: else:
_LOGGER.error( _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, ATTR_IEEE,
str(ieee), str(ieee),
) )
@ -1466,13 +1466,13 @@ def async_load_api(hass: HomeAssistant) -> None:
intensity: int = service.data[ATTR_WARNING_DEVICE_STROBE_INTENSITY] intensity: int = service.data[ATTR_WARNING_DEVICE_STROBE_INTENSITY]
if (zha_device := zha_gateway.get_device(ieee)) is not None: if (zha_device := zha_gateway.get_device(ieee)) is not None:
if channel := _get_ias_wd_channel(zha_device): if cluster_handler := _get_ias_wd_cluster_handler(zha_device):
await channel.issue_start_warning( await cluster_handler.issue_start_warning(
mode, strobe, level, duration, duty_mode, intensity mode, strobe, level, duration, duty_mode, intensity
) )
else: else:
_LOGGER.error( _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, ATTR_IEEE,
str(ieee), str(ieee),
) )

View File

@ -121,19 +121,19 @@ def setup_zha(hass, config_entry, zigpy_app_controller):
@pytest.fixture @pytest.fixture
def channel(): def cluster_handler():
"""Channel mock factory fixture.""" """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 = MagicMock()
ch.name = name 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.id = f"{endpoint_id}:0x{cluster_id:04x}"
ch.async_configure = AsyncMock() ch.async_configure = AsyncMock()
ch.async_initialize = AsyncMock() ch.async_initialize = AsyncMock()
return ch return ch
return channel return cluster_handler
@pytest.fixture @pytest.fixture
@ -162,7 +162,7 @@ def zigpy_device_mock(zigpy_app_controller):
for epid, ep in endpoints.items(): for epid, ep in endpoints.items():
endpoint = device.add_endpoint(epid) endpoint = device.add_endpoint(epid)
endpoint.device_type = ep[SIG_EP_TYPE] 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]) endpoint.request = AsyncMock(return_value=[0])
for cluster_id in ep.get(SIG_EP_INPUT, []): 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, []): for cluster_id in ep.get(SIG_EP_OUTPUT, []):
endpoint.add_output_cluster(cluster_id) endpoint.add_output_cluster(cluster_id)
device.status = zigpy.device.Status.ENDPOINTS_INIT
if quirk: if quirk:
device = quirk(zigpy_app_controller, device.ieee, device.nwk, device) device = quirk(zigpy_app_controller, device.ieee, device.nwk, device)

View File

@ -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 from tests.components.zha.test_cluster_handlers import ( # noqa: F401
channel_pool, endpoint,
poll_control_ch, poll_control_ch,
zigpy_coordinator_device, zigpy_coordinator_device,
) )

View File

@ -1,4 +1,4 @@
"""Test ZHA Core channels.""" """Test ZHA Core cluster handlers."""
import asyncio import asyncio
import math import math
from unittest import mock from unittest import mock
@ -6,15 +6,17 @@ from unittest.mock import AsyncMock, patch
import pytest import pytest
import zigpy.endpoint import zigpy.endpoint
from zigpy.endpoint import Endpoint as ZigpyEndpoint
import zigpy.profiles.zha import zigpy.profiles.zha
import zigpy.types as t import zigpy.types as t
from zigpy.zcl import foundation from zigpy.zcl import foundation
import zigpy.zcl.clusters import zigpy.zcl.clusters
import zigpy.zdo.types as zdo_t import zigpy.zdo.types as zdo_t
import homeassistant.components.zha.core.channels as zha_channels import homeassistant.components.zha.core.cluster_handlers as cluster_handlers
import homeassistant.components.zha.core.channels.base as base_channels
import homeassistant.components.zha.core.const as zha_const 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 import homeassistant.components.zha.core.registries as registries
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@ -65,20 +67,22 @@ def zigpy_coordinator_device(zigpy_device_mock):
@pytest.fixture @pytest.fixture
def channel_pool(zigpy_coordinator_device): def endpoint(zigpy_coordinator_device):
"""Endpoint Channels fixture.""" """Endpoint fixture."""
ch_pool_mock = mock.MagicMock(spec_set=zha_channels.ChannelPool) endpoint_mock = mock.MagicMock(spec_set=Endpoint)
ch_pool_mock.endpoint.device.application.get_device.return_value = ( endpoint_mock.zigpy_endpoint.device.application.get_device.return_value = (
zigpy_coordinator_device zigpy_coordinator_device
) )
type(ch_pool_mock).skip_configuration = mock.PropertyMock(return_value=False) type(endpoint_mock.device).skip_configuration = mock.PropertyMock(
ch_pool_mock.id = 1 return_value=False
return ch_pool_mock )
endpoint_mock.id = 1
return endpoint_mock
@pytest.fixture @pytest.fixture
def poll_control_ch(channel_pool, zigpy_device_mock): def poll_control_ch(endpoint, zigpy_device_mock):
"""Poll control channel fixture.""" """Poll control cluster handler fixture."""
cluster_id = zigpy.zcl.clusters.general.PollControl.cluster_id cluster_id = zigpy.zcl.clusters.general.PollControl.cluster_id
zigpy_dev = zigpy_device_mock( zigpy_dev = zigpy_device_mock(
{1: {SIG_EP_INPUT: [cluster_id], SIG_EP_OUTPUT: [], SIG_EP_TYPE: 0x1234}}, {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] cluster = zigpy_dev.endpoints[1].in_clusters[cluster_id]
channel_class = registries.ZIGBEE_CHANNEL_REGISTRY.get(cluster_id) cluster_handler_class = registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.get(cluster_id)
return channel_class(cluster, channel_pool) return cluster_handler_class(cluster, endpoint)
@pytest.fixture @pytest.fixture
@ -236,10 +240,10 @@ async def poll_control_device(zha_device_restored, zigpy_device_mock):
), ),
], ],
) )
async def test_in_channel_config( async def test_in_cluster_handler_config(
cluster_id, bind_count, attrs, channel_pool, zigpy_device_mock, zha_gateway cluster_id, bind_count, attrs, endpoint, zigpy_device_mock, zha_gateway
) -> None: ) -> None:
"""Test ZHA core channel configuration for input clusters.""" """Test ZHA core cluster handler configuration for input clusters."""
zigpy_dev = zigpy_device_mock( zigpy_dev = zigpy_device_mock(
{1: {SIG_EP_INPUT: [cluster_id], SIG_EP_OUTPUT: [], SIG_EP_TYPE: 0x1234}}, {1: {SIG_EP_INPUT: [cluster_id], SIG_EP_OUTPUT: [], SIG_EP_TYPE: 0x1234}},
"00:11:22:33:44:55:66:77", "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] cluster = zigpy_dev.endpoints[1].in_clusters[cluster_id]
channel_class = registries.ZIGBEE_CHANNEL_REGISTRY.get( cluster_handler_class = registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.get(
cluster_id, base_channels.ZigbeeChannel 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.bind.call_count == bind_count
assert cluster.configure_reporting.call_count == 0 assert cluster.configure_reporting.call_count == 0
@ -299,10 +303,10 @@ async def test_in_channel_config(
(0x0B04, 1), (0x0B04, 1),
], ],
) )
async def test_out_channel_config( async def test_out_cluster_handler_config(
cluster_id, bind_count, channel_pool, zigpy_device_mock, zha_gateway cluster_id, bind_count, endpoint, zigpy_device_mock, zha_gateway
) -> None: ) -> None:
"""Test ZHA core channel configuration for output clusters.""" """Test ZHA core cluster handler configuration for output clusters."""
zigpy_dev = zigpy_device_mock( zigpy_dev = zigpy_device_mock(
{1: {SIG_EP_OUTPUT: [cluster_id], SIG_EP_INPUT: [], SIG_EP_TYPE: 0x1234}}, {1: {SIG_EP_OUTPUT: [cluster_id], SIG_EP_INPUT: [], SIG_EP_TYPE: 0x1234}},
"00:11:22:33:44:55:66:77", "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 = zigpy_dev.endpoints[1].out_clusters[cluster_id]
cluster.bind_only = True cluster.bind_only = True
channel_class = registries.ZIGBEE_CHANNEL_REGISTRY.get( cluster_handler_class = registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.get(
cluster_id, base_channels.ZigbeeChannel 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.bind.call_count == bind_count
assert cluster.configure_reporting.call_count == 0 assert cluster.configure_reporting.call_count == 0
def test_channel_registry() -> None: def test_cluster_handler_registry() -> None:
"""Test ZIGBEE Channel Registry.""" """Test ZIGBEE cluster handler Registry."""
for cluster_id, channel in registries.ZIGBEE_CHANNEL_REGISTRY.items(): for (
cluster_id,
cluster_handler,
) in registries.ZIGBEE_CLUSTER_HANDLER_REGISTRY.items():
assert isinstance(cluster_id, int) assert isinstance(cluster_id, int)
assert 0 <= cluster_id <= 0xFFFF 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: def test_epch_unclaimed_cluster_handlers(cluster_handler) -> None:
"""Test unclaimed channels.""" """Test unclaimed cluster handlers."""
ch_1 = channel(zha_const.CHANNEL_ON_OFF, 6) ch_1 = cluster_handler(zha_const.CLUSTER_HANDLER_ON_OFF, 6)
ch_2 = channel(zha_const.CHANNEL_LEVEL, 8) ch_2 = cluster_handler(zha_const.CLUSTER_HANDLER_LEVEL, 8)
ch_3 = channel(zha_const.CHANNEL_COLOR, 768) ch_3 = cluster_handler(zha_const.CLUSTER_HANDLER_COLOR, 768)
ep_channels = zha_channels.ChannelPool( ep_cluster_handlers = Endpoint(
mock.MagicMock(spec_set=zha_channels.Channels), mock.sentinel.ep 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} all_cluster_handlers = {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): with mock.patch.dict(
available = ep_channels.unclaimed_channels() 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_1 in available
assert ch_2 in available assert ch_2 in available
assert ch_3 in available assert ch_3 in available
ep_channels.claimed_channels[ch_2.id] = ch_2 ep_cluster_handlers.claimed_cluster_handlers[ch_2.id] = ch_2
available = ep_channels.unclaimed_channels() available = ep_cluster_handlers.unclaimed_cluster_handlers()
assert ch_1 in available assert ch_1 in available
assert ch_2 not in available assert ch_2 not in available
assert ch_3 in available assert ch_3 in available
ep_channels.claimed_channels[ch_1.id] = ch_1 ep_cluster_handlers.claimed_cluster_handlers[ch_1.id] = ch_1
available = ep_channels.unclaimed_channels() available = ep_cluster_handlers.unclaimed_cluster_handlers()
assert ch_1 not in available assert ch_1 not in available
assert ch_2 not in available assert ch_2 not in available
assert ch_3 in available assert ch_3 in available
ep_channels.claimed_channels[ch_3.id] = ch_3 ep_cluster_handlers.claimed_cluster_handlers[ch_3.id] = ch_3
available = ep_channels.unclaimed_channels() available = ep_cluster_handlers.unclaimed_cluster_handlers()
assert ch_1 not in available assert ch_1 not in available
assert ch_2 not in available assert ch_2 not in available
assert ch_3 not in available assert ch_3 not in available
def test_epch_claim_channels(channel) -> None: def test_epch_claim_cluster_handlers(cluster_handler) -> None:
"""Test channel claiming.""" """Test cluster handler claiming."""
ch_1 = channel(zha_const.CHANNEL_ON_OFF, 6) ch_1 = cluster_handler(zha_const.CLUSTER_HANDLER_ON_OFF, 6)
ch_2 = channel(zha_const.CHANNEL_LEVEL, 8) ch_2 = cluster_handler(zha_const.CLUSTER_HANDLER_LEVEL, 8)
ch_3 = channel(zha_const.CHANNEL_COLOR, 768) ch_3 = cluster_handler(zha_const.CLUSTER_HANDLER_COLOR, 768)
ep_channels = zha_channels.ChannelPool( ep_cluster_handlers = Endpoint(
mock.MagicMock(spec_set=zha_channels.Channels), mock.sentinel.ep 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} all_cluster_handlers = {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): with mock.patch.dict(
assert ch_1.id not in ep_channels.claimed_channels ep_cluster_handlers.all_cluster_handlers, all_cluster_handlers, clear=True
assert ch_2.id not in ep_channels.claimed_channels ):
assert ch_3.id not in ep_channels.claimed_channels 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]) ep_cluster_handlers.claim_cluster_handlers([ch_2])
assert ch_1.id not in ep_channels.claimed_channels assert ch_1.id not in ep_cluster_handlers.claimed_cluster_handlers
assert ch_2.id in ep_channels.claimed_channels assert ch_2.id in ep_cluster_handlers.claimed_cluster_handlers
assert ep_channels.claimed_channels[ch_2.id] is ch_2 assert ep_cluster_handlers.claimed_cluster_handlers[ch_2.id] is ch_2
assert ch_3.id not in ep_channels.claimed_channels assert ch_3.id not in ep_cluster_handlers.claimed_cluster_handlers
ep_channels.claim_channels([ch_3, ch_1]) ep_cluster_handlers.claim_cluster_handlers([ch_3, ch_1])
assert ch_1.id in ep_channels.claimed_channels assert ch_1.id in ep_cluster_handlers.claimed_cluster_handlers
assert ep_channels.claimed_channels[ch_1.id] is ch_1 assert ep_cluster_handlers.claimed_cluster_handlers[ch_1.id] is ch_1
assert ch_2.id in ep_channels.claimed_channels assert ch_2.id in ep_cluster_handlers.claimed_cluster_handlers
assert ep_channels.claimed_channels[ch_2.id] is ch_2 assert ep_cluster_handlers.claimed_cluster_handlers[ch_2.id] is ch_2
assert ch_3.id in ep_channels.claimed_channels assert ch_3.id in ep_cluster_handlers.claimed_cluster_handlers
assert ep_channels.claimed_channels[ch_3.id] is ch_3 assert ep_cluster_handlers.claimed_cluster_handlers[ch_3.id] is ch_3
assert "1:0x0300" in ep_channels.claimed_channels assert "1:0x0300" in ep_cluster_handlers.claimed_cluster_handlers
@mock.patch( @mock.patch(
"homeassistant.components.zha.core.channels.ChannelPool.add_client_channels" "homeassistant.components.zha.core.endpoint.Endpoint.add_client_cluster_handlers"
) )
@mock.patch( @mock.patch(
"homeassistant.components.zha.core.discovery.PROBE.discover_entities", "homeassistant.components.zha.core.discovery.PROBE.discover_entities",
mock.MagicMock(), mock.MagicMock(),
) )
def test_ep_channels_all_channels(m1, zha_device_mock) -> None: def test_ep_all_cluster_handlers(m1, zha_device_mock) -> None:
"""Test EndpointChannels adding all channels.""" """Test Endpoint adding all cluster handlers."""
zha_device = zha_device_mock( zha_device = zha_device_mock(
{ {
1: { 1: {
@ -422,43 +433,37 @@ def test_ep_channels_all_channels(m1, zha_device_mock) -> None:
}, },
} }
) )
channels = zha_channels.Channels(zha_device) assert "1:0x0000" in zha_device._endpoints[1].all_cluster_handlers
assert "1:0x0001" in zha_device._endpoints[1].all_cluster_handlers
ep_channels = zha_channels.ChannelPool.new(channels, 1) assert "1:0x0006" in zha_device._endpoints[1].all_cluster_handlers
assert "1:0x0000" in ep_channels.all_channels assert "1:0x0008" in zha_device._endpoints[1].all_cluster_handlers
assert "1:0x0001" in ep_channels.all_channels assert "1:0x0300" not in zha_device._endpoints[1].all_cluster_handlers
assert "1:0x0006" in ep_channels.all_channels assert "2:0x0000" not in zha_device._endpoints[1].all_cluster_handlers
assert "1:0x0008" in ep_channels.all_channels assert "2:0x0001" not in zha_device._endpoints[1].all_cluster_handlers
assert "1:0x0300" not in ep_channels.all_channels assert "2:0x0006" not in zha_device._endpoints[1].all_cluster_handlers
assert "2:0x0000" not in ep_channels.all_channels assert "2:0x0008" not in zha_device._endpoints[1].all_cluster_handlers
assert "2:0x0001" not in ep_channels.all_channels assert "2:0x0300" not in zha_device._endpoints[1].all_cluster_handlers
assert "2:0x0006" not in ep_channels.all_channels assert "1:0x0000" not in zha_device._endpoints[2].all_cluster_handlers
assert "2:0x0008" not in ep_channels.all_channels assert "1:0x0001" not in zha_device._endpoints[2].all_cluster_handlers
assert "2:0x0300" not in ep_channels.all_channels assert "1:0x0006" not in zha_device._endpoints[2].all_cluster_handlers
assert "1:0x0008" not in zha_device._endpoints[2].all_cluster_handlers
channels = zha_channels.Channels(zha_device) assert "1:0x0300" not in zha_device._endpoints[2].all_cluster_handlers
ep_channels = zha_channels.ChannelPool.new(channels, 2) assert "2:0x0000" in zha_device._endpoints[2].all_cluster_handlers
assert "1:0x0000" not in ep_channels.all_channels assert "2:0x0001" in zha_device._endpoints[2].all_cluster_handlers
assert "1:0x0001" not in ep_channels.all_channels assert "2:0x0006" in zha_device._endpoints[2].all_cluster_handlers
assert "1:0x0006" not in ep_channels.all_channels assert "2:0x0008" in zha_device._endpoints[2].all_cluster_handlers
assert "1:0x0008" not in ep_channels.all_channels assert "2:0x0300" in zha_device._endpoints[2].all_cluster_handlers
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
@mock.patch( @mock.patch(
"homeassistant.components.zha.core.channels.ChannelPool.add_client_channels" "homeassistant.components.zha.core.endpoint.Endpoint.add_client_cluster_handlers"
) )
@mock.patch( @mock.patch(
"homeassistant.components.zha.core.discovery.PROBE.discover_entities", "homeassistant.components.zha.core.discovery.PROBE.discover_entities",
mock.MagicMock(), mock.MagicMock(),
) )
def test_channel_power_config(m1, zha_device_mock) -> None: def test_cluster_handler_power_config(m1, zha_device_mock) -> None:
"""Test that channels only get a single power channel.""" """Test that cluster handlers only get a single power cluster handler."""
in_clusters = [0, 1, 6, 8] in_clusters = [0, 1, 6, 8]
zha_device = zha_device_mock( 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) assert "1:0x0000" in zha_device._endpoints[1].all_cluster_handlers
pools = {pool.id: pool for pool in channels.pools} assert "1:0x0001" in zha_device._endpoints[1].all_cluster_handlers
assert "1:0x0000" in pools[1].all_channels assert "1:0x0006" in zha_device._endpoints[1].all_cluster_handlers
assert "1:0x0001" in pools[1].all_channels assert "1:0x0008" in zha_device._endpoints[1].all_cluster_handlers
assert "1:0x0006" in pools[1].all_channels assert "1:0x0300" not in zha_device._endpoints[1].all_cluster_handlers
assert "1:0x0008" in pools[1].all_channels assert "2:0x0000" in zha_device._endpoints[2].all_cluster_handlers
assert "1:0x0300" not in pools[1].all_channels assert "2:0x0001" in zha_device._endpoints[2].all_cluster_handlers
assert "2:0x0000" in pools[2].all_channels assert "2:0x0006" in zha_device._endpoints[2].all_cluster_handlers
assert "2:0x0001" not in pools[2].all_channels assert "2:0x0008" in zha_device._endpoints[2].all_cluster_handlers
assert "2:0x0006" in pools[2].all_channels assert "2:0x0300" in zha_device._endpoints[2].all_cluster_handlers
assert "2:0x0008" in pools[2].all_channels
assert "2:0x0300" in pools[2].all_channels
zha_device = zha_device_mock( 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}, 2: {SIG_EP_INPUT: in_clusters, SIG_EP_OUTPUT: [], SIG_EP_TYPE: 0x0000},
} }
) )
channels = zha_channels.Channels.new(zha_device) assert "1:0x0001" not in zha_device._endpoints[1].all_cluster_handlers
pools = {pool.id: pool for pool in channels.pools} assert "2:0x0001" in zha_device._endpoints[2].all_cluster_handlers
assert "1:0x0001" not in pools[1].all_channels
assert "2:0x0001" in pools[2].all_channels
zha_device = zha_device_mock( zha_device = zha_device_mock(
{2: {SIG_EP_INPUT: in_clusters, SIG_EP_OUTPUT: [], SIG_EP_TYPE: 0x0000}} {2: {SIG_EP_INPUT: in_clusters, SIG_EP_OUTPUT: [], SIG_EP_TYPE: 0x0000}}
) )
channels = zha_channels.Channels.new(zha_device) assert "2:0x0001" in zha_device._endpoints[2].all_cluster_handlers
pools = {pool.id: pool for pool in channels.pools}
assert "2:0x0001" in pools[2].all_channels
async def test_ep_channels_configure(channel) -> None: async def test_ep_cluster_handlers_configure(cluster_handler) -> None:
"""Test unclaimed channels.""" """Test unclaimed cluster handlers."""
ch_1 = channel(zha_const.CHANNEL_ON_OFF, 6) ch_1 = cluster_handler(zha_const.CLUSTER_HANDLER_ON_OFF, 6)
ch_2 = channel(zha_const.CHANNEL_LEVEL, 8) ch_2 = cluster_handler(zha_const.CLUSTER_HANDLER_LEVEL, 8)
ch_3 = channel(zha_const.CHANNEL_COLOR, 768) ch_3 = cluster_handler(zha_const.CLUSTER_HANDLER_COLOR, 768)
ch_3.async_configure = AsyncMock(side_effect=asyncio.TimeoutError) ch_3.async_configure = AsyncMock(side_effect=asyncio.TimeoutError)
ch_3.async_initialize = AsyncMock(side_effect=asyncio.TimeoutError) ch_3.async_initialize = AsyncMock(side_effect=asyncio.TimeoutError)
ch_4 = channel(zha_const.CHANNEL_ON_OFF, 6) ch_4 = cluster_handler(zha_const.CLUSTER_HANDLER_ON_OFF, 6)
ch_5 = channel(zha_const.CHANNEL_LEVEL, 8) ch_5 = cluster_handler(zha_const.CLUSTER_HANDLER_LEVEL, 8)
ch_5.async_configure = AsyncMock(side_effect=asyncio.TimeoutError) ch_5.async_configure = AsyncMock(side_effect=asyncio.TimeoutError)
ch_5.async_initialize = AsyncMock(side_effect=asyncio.TimeoutError) ch_5.async_initialize = AsyncMock(side_effect=asyncio.TimeoutError)
channels = mock.MagicMock(spec_set=zha_channels.Channels) endpoint_mock = mock.MagicMock(spec_set=ZigpyEndpoint)
type(channels).semaphore = mock.PropertyMock(return_value=asyncio.Semaphore(3)) type(endpoint_mock).in_clusters = mock.PropertyMock(return_value={})
ep_channels = zha_channels.ChannelPool(channels, mock.sentinel.ep) 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} 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( with mock.patch.dict(
ep_channels.claimed_channels, claimed, clear=True endpoint.claimed_cluster_handlers, claimed, clear=True
), mock.patch.dict(ep_channels.client_channels, client_chans, clear=True): ), mock.patch.dict(endpoint.client_cluster_handlers, client_handlers, clear=True):
await ep_channels.async_configure() await endpoint.async_configure()
await ep_channels.async_initialize(mock.sentinel.from_cache) 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.call_count == 1
assert ch.async_initialize.await_count == 1 assert ch.async_initialize.await_count == 1
assert ch.async_initialize.call_args[0][0] is mock.sentinel.from_cache assert ch.async_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: 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() await poll_control_ch.async_configure()
assert poll_control_ch.cluster.write_attributes.call_count == 1 assert poll_control_ch.cluster.write_attributes.call_count == 1
assert poll_control_ch.cluster.write_attributes.call_args[0][0] == { 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: 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() rsp_mock = AsyncMock()
set_interval_mock = AsyncMock() set_interval_mock = AsyncMock()
fast_poll_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( async def test_poll_control_cluster_command(
hass: HomeAssistant, poll_control_device hass: HomeAssistant, poll_control_device
) -> None: ) -> None:
"""Test poll control channel response to cluster command.""" """Test poll control cluster handler response to cluster command."""
checkin_mock = AsyncMock() 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 cluster = poll_control_ch.cluster
events = async_capture_events(hass, zha_const.ZHA_EVENT) 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( async def test_poll_control_ignore_list(
hass: HomeAssistant, poll_control_device hass: HomeAssistant, poll_control_device
) -> None: ) -> None:
"""Test poll control channel ignore list.""" """Test poll control cluster handler ignore list."""
set_long_poll_mock = AsyncMock() 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 cluster = poll_control_ch.cluster
with mock.patch.object(cluster, "set_long_poll_interval", set_long_poll_mock): 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: 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() 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 cluster = poll_control_ch.cluster
poll_control_device.device.node_desc.manufacturer_code = 4476 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( async def test_zll_device_groups(
zigpy_zll_device, channel_pool, zigpy_coordinator_device zigpy_zll_device, endpoint, zigpy_coordinator_device
) -> None: ) -> None:
"""Test adding coordinator to ZLL groups.""" """Test adding coordinator to ZLL groups."""
cluster = zigpy_zll_device.endpoints[1].lightlink 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 = zigpy.zcl.clusters.lightlink.LightLink.commands_by_name[
"get_group_identifiers_rsp" "get_group_identifiers_rsp"
@ -671,7 +671,7 @@ async def test_zll_device_groups(
) )
), ),
) as cmd_mock: ) as cmd_mock:
await channel.async_configure() await cluster_handler.async_configure()
assert cmd_mock.await_count == 1 assert cmd_mock.await_count == 1
assert ( assert (
cluster.server_commands[cmd_mock.await_args[0][0]].name cluster.server_commands[cmd_mock.await_args[0][0]].name
@ -693,7 +693,7 @@ async def test_zll_device_groups(
) )
), ),
) as cmd_mock: ) as cmd_mock:
await channel.async_configure() await cluster_handler.async_configure()
assert cmd_mock.await_count == 1 assert cmd_mock.await_count == 1
assert ( assert (
cluster.server_commands[cmd_mock.await_args[0][0]].name 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( @mock.patch(
"homeassistant.components.zha.core.discovery.PROBE.discover_entities", "homeassistant.components.zha.core.discovery.PROBE.discover_entities",
mock.MagicMock(), mock.MagicMock(),
) )
async def test_cluster_no_ep_attribute(m1, zha_device_mock) -> None: async def test_cluster_no_ep_attribute(zha_device_mock) -> None:
"""Test channels for clusters without ep_attribute.""" """Test cluster handlers for clusters without ep_attribute."""
zha_device = zha_device_mock( zha_device = zha_device_mock(
{1: {SIG_EP_INPUT: [0x042E], SIG_EP_OUTPUT: [], SIG_EP_TYPE: 0x1234}}, {1: {SIG_EP_INPUT: [0x042E], SIG_EP_OUTPUT: [], SIG_EP_TYPE: 0x1234}},
) )
channels = zha_channels.Channels.new(zha_device) assert "1:0x042e" in zha_device._endpoints[1].all_cluster_handlers
pools = {pool.id: pool for pool in channels.pools} assert zha_device._endpoints[1].all_cluster_handlers["1:0x042e"].name
assert "1:0x042e" in pools[1].all_channels
assert pools[1].all_channels["1:0x042e"].name
async def test_configure_reporting(hass: HomeAssistant) -> None: async def test_configure_reporting(hass: HomeAssistant, endpoint) -> None:
"""Test setting up a channel and configuring attribute reporting in two batches.""" """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 BIND = True
REPORT_CONFIG = ( REPORT_CONFIG = (
# By name # By name
base_channels.AttrReportConfig(attr="current_x", config=(1, 60, 1)), cluster_handlers.AttrReportConfig(attr="current_x", config=(1, 60, 1)),
base_channels.AttrReportConfig(attr="current_hue", config=(1, 60, 2)), cluster_handlers.AttrReportConfig(attr="current_hue", config=(1, 60, 2)),
base_channels.AttrReportConfig(attr="color_temperature", config=(1, 60, 3)), cluster_handlers.AttrReportConfig(
base_channels.AttrReportConfig(attr="current_y", config=(1, 60, 4)), attr="color_temperature", config=(1, 60, 3)
),
cluster_handlers.AttrReportConfig(attr="current_y", config=(1, 60, 4)),
) )
mock_ep = mock.AsyncMock(spec_set=zigpy.endpoint.Endpoint) mock_ep = 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) cluster_handler = TestZigbeeClusterHandler(cluster, endpoint)
ch_pool.skip_configuration = False await cluster_handler.async_configure()
channel = TestZigbeeChannel(cluster, ch_pool)
await channel.async_configure()
# Since we request reporting for five attributes, we need to make two calls (3 + 1) # Since we request reporting for five attributes, we need to make two calls (3 + 1)
assert cluster.configure_reporting_multiple.mock_calls == [ assert cluster.configure_reporting_multiple.mock_calls == [

View File

@ -46,9 +46,9 @@ def required_platforms_only():
def zigpy_device(zigpy_device_mock): def zigpy_device(zigpy_device_mock):
"""Device tracker zigpy device.""" """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] in_clusters = [general.OnOff.cluster_id]
if with_basic_channel: if with_basic_cluster_handler:
in_clusters.append(general.Basic.cluster_id) in_clusters.append(general.Basic.cluster_id)
endpoints = { endpoints = {
@ -67,9 +67,9 @@ def zigpy_device(zigpy_device_mock):
def zigpy_device_mains(zigpy_device_mock): def zigpy_device_mains(zigpy_device_mock):
"""Device tracker zigpy device.""" """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] in_clusters = [general.OnOff.cluster_id]
if with_basic_channel: if with_basic_cluster_handler:
in_clusters.append(general.Basic.cluster_id) in_clusters.append(general.Basic.cluster_id)
endpoints = { endpoints = {
@ -87,15 +87,15 @@ def zigpy_device_mains(zigpy_device_mock):
@pytest.fixture @pytest.fixture
def device_with_basic_channel(zigpy_device_mains): def device_with_basic_cluster_handler(zigpy_device_mains):
"""Return a ZHA device with a basic channel present.""" """Return a ZHA device with a basic cluster handler present."""
return zigpy_device_mains(with_basic_channel=True) return zigpy_device_mains(with_basic_cluster_handler=True)
@pytest.fixture @pytest.fixture
def device_without_basic_channel(zigpy_device): def device_without_basic_cluster_handler(zigpy_device):
"""Return a ZHA device with a basic channel present.""" """Return a ZHA device without a basic cluster handler present."""
return zigpy_device(with_basic_channel=False) return zigpy_device(with_basic_cluster_handler=False)
@pytest.fixture @pytest.fixture
@ -125,32 +125,32 @@ def _send_time_changed(hass, seconds):
@patch( @patch(
"homeassistant.components.zha.core.channels.general.BasicChannel.async_initialize", "homeassistant.components.zha.core.cluster_handlers.general.BasicClusterHandler.async_initialize",
new=mock.AsyncMock(), new=mock.AsyncMock(),
) )
async def test_check_available_success( 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: ) -> None:
"""Check device availability success on 1st try.""" """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]) 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() 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 assert zha_device.available is True
_send_time_changed(hass, zha_device.consider_unavailable_time + 2) _send_time_changed(hass, zha_device.consider_unavailable_time + 2)
await hass.async_block_till_done() await hass.async_block_till_done()
assert zha_device.available is False assert zha_device.available is False
assert basic_ch.read_attributes.await_count == 0 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 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): 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 basic_ch.read_attributes.side_effect = _update_last_seen
@ -177,22 +177,22 @@ async def test_check_available_success(
@patch( @patch(
"homeassistant.components.zha.core.channels.general.BasicChannel.async_initialize", "homeassistant.components.zha.core.cluster_handlers.general.BasicClusterHandler.async_initialize",
new=mock.AsyncMock(), new=mock.AsyncMock(),
) )
async def test_check_available_unsuccessful( 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: ) -> None:
"""Check device availability all tries fail.""" """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]) 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 zha_device.available is True
assert basic_ch.read_attributes.await_count == 0 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 time.time() - zha_device.consider_unavailable_time - 2
) )
@ -219,24 +219,24 @@ async def test_check_available_unsuccessful(
@patch( @patch(
"homeassistant.components.zha.core.channels.general.BasicChannel.async_initialize", "homeassistant.components.zha.core.cluster_handlers.general.BasicClusterHandler.async_initialize",
new=mock.AsyncMock(), new=mock.AsyncMock(),
) )
async def test_check_available_no_basic_channel( async def test_check_available_no_basic_cluster_handler(
hass: HomeAssistant, hass: HomeAssistant,
device_without_basic_channel, device_without_basic_cluster_handler,
zha_device_restored, zha_device_restored,
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Check device availability for a device without basic cluster.""" """Check device availability for a device without basic cluster."""
caplog.set_level(logging.DEBUG, logger="homeassistant.components.zha") 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]) await async_enable_traffic(hass, [zha_device])
assert zha_device.available is True 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 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: 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) dev_registry = dr.async_get(hass)
entry = dev_registry.async_get(ota_zha_device.device_id) entry = dev_registry.async_get(ota_zha_device.device_id)
assert entry.sw_version is None assert entry.sw_version is None

View File

@ -302,8 +302,8 @@ async def test_action(hass: HomeAssistant, device_ias, device_inovelli) -> None:
await hass.async_block_till_done() await hass.async_block_till_done()
calls = async_mock_service(hass, DOMAIN, "warning_device_warn") calls = async_mock_service(hass, DOMAIN, "warning_device_warn")
channel = zha_device.channels.pools[0].client_channels["1:0x0006"] cluster_handler = zha_device.endpoints[1].client_cluster_handlers["1:0x0006"]
channel.zha_send_event(COMMAND_SINGLE, []) cluster_handler.zha_send_event(COMMAND_SINGLE, [])
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(calls) == 1 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: async def test_invalid_zha_event_type(hass: HomeAssistant, device_ias) -> None:
"""Test that unexpected types are not passed to `zha_send_event`.""" """Test that unexpected types are not passed to `zha_send_event`."""
zigpy_device, zha_device = device_ias 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 # `zha_send_event` accepts only zigpy responses, lists, and dicts
with pytest.raises(TypeError): with pytest.raises(TypeError):
channel.zha_send_event(COMMAND_SINGLE, 123) cluster_handler.zha_send_event(COMMAND_SINGLE, 123)

View File

@ -223,8 +223,8 @@ async def test_if_fires_on_event(hass: HomeAssistant, mock_devices, calls) -> No
await hass.async_block_till_done() await hass.async_block_till_done()
channel = zha_device.channels.pools[0].client_channels["1:0x0006"] cluster_handler = zha_device.endpoints[1].client_cluster_handlers["1:0x0006"]
channel.zha_send_event(COMMAND_SINGLE, []) cluster_handler.zha_send_event(COMMAND_SINGLE, [])
await hass.async_block_till_done() await hass.async_block_till_done()
assert len(calls) == 1 assert len(calls) == 1

View File

@ -14,10 +14,10 @@ import zigpy.zcl.clusters.security
import zigpy.zcl.foundation as zcl_f import zigpy.zcl.foundation as zcl_f
import homeassistant.components.zha.binary_sensor import homeassistant.components.zha.binary_sensor
import homeassistant.components.zha.core.channels as zha_channels import homeassistant.components.zha.core.cluster_handlers as cluster_handlers
import homeassistant.components.zha.core.channels.base as base_channels
import homeassistant.components.zha.core.const as zha_const import homeassistant.components.zha.core.const as zha_const
import homeassistant.components.zha.core.discovery as disc 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.core.registries as zha_regs
import homeassistant.components.zha.cover import homeassistant.components.zha.cover
import homeassistant.components.zha.device_tracker import homeassistant.components.zha.device_tracker
@ -33,11 +33,11 @@ import homeassistant.helpers.entity_registry as er
from .common import get_zha_gateway from .common import get_zha_gateway
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
from .zha_devices_list import ( from .zha_devices_list import (
DEV_SIG_CHANNELS, DEV_SIG_CLUSTER_HANDLERS,
DEV_SIG_ENT_MAP, DEV_SIG_ENT_MAP,
DEV_SIG_ENT_MAP_CLASS, DEV_SIG_ENT_MAP_CLASS,
DEV_SIG_ENT_MAP_ID, DEV_SIG_ENT_MAP_ID,
DEV_SIG_EVT_CHANNELS, DEV_SIG_EVT_CLUSTER_HANDLERS,
DEVICES, DEVICES,
) )
@ -63,27 +63,6 @@ def contains_ignored_suffix(unique_id: str) -> bool:
return False 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( @patch(
"zigpy.zcl.clusters.general.Identify.request", "zigpy.zcl.clusters.general.Identify.request",
new=AsyncMock(return_value=[mock.sentinel.data, zcl_f.Status.SUCCESS]), new=AsyncMock(return_value=[mock.sentinel.data, zcl_f.Status.SUCCESS]),
@ -119,14 +98,14 @@ async def test_devices(
if cluster_identify: if cluster_identify:
cluster_identify.request.reset_mock() 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) _dispatch = mock.MagicMock(wraps=orig_new_entity)
try: 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) zha_dev = await zha_device_joined_restored(zigpy_device)
await hass_disable_services.async_block_till_done() await hass_disable_services.async_block_till_done()
finally: finally:
zha_channels.ChannelPool.async_new_entity = orig_new_entity Endpoint.async_new_entity = orig_new_entity
if cluster_identify: if cluster_identify:
called = int(zha_device_joined_restored.name == "zha_device_joined") called = int(zha_device_joined_restored.name == "zha_device_joined")
@ -147,34 +126,36 @@ async def test_devices(
tsn=None, tsn=None,
) )
event_channels = { event_cluster_handlers = {
ch.id for pool in zha_dev.channels.pools for ch in pool.client_channels.values() 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 # 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() 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 = {} ha_ent_info = {}
created_entity_count = 0 created_entity_count = 0
for call in _dispatch.call_args_list: 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 # 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): if response and not contains_ignored_suffix(response.name):
created_entity_count += 1 created_entity_count += 1
unique_id_head = UNIQUE_ID_HD.match(unique_id).group( unique_id_head = UNIQUE_ID_HD.match(unique_id).group(
0 0
) # ieee + endpoint_id ) # ieee + endpoint_id
ha_ent_info[(unique_id_head, entity_cls.__name__)] = ( ha_ent_info[(unique_id_head, entity_cls.__name__)] = (
component, platform,
unique_id, unique_id,
channels, cluster_handlers,
) )
for comp_id, ent_info in device[DEV_SIG_ENT_MAP].items(): 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]) 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 is not None
assert ha_entity_id.startswith(no_tail_id) 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) test_unique_id_head = UNIQUE_ID_HD.match(unique_id).group(0)
assert (test_unique_id_head, test_ent_class) in ha_ent_info 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) (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" # unique_id used for discover is the same for "multi entities"
assert unique_id.startswith(ha_unique_id) 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]) 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: def test_discover_entities(m1, m2) -> None:
"""Test discover endpoint class method.""" """Test discover endpoint class method."""
ep_channels = mock.MagicMock() endpoint = mock.MagicMock()
disc.PROBE.discover_entities(ep_channels) disc.PROBE.discover_entities(endpoint)
assert m1.call_count == 1 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_count == 1
assert m2.call_args[0][0] is ep_channels assert m2.call_args[0][0] is endpoint
@pytest.mark.parametrize( @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_LIGHT, Platform.LIGHT, True),
(zigpy.profiles.zha.DeviceType.ON_OFF_BALLAST, Platform.SWITCH, 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), (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.""" """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 = mock.PropertyMock()
ep_mock.return_value.profile_id = 0x0104 ep_mock.return_value.profile_id = 0x0104
ep_mock.return_value.device_type = device_type 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( get_entity_mock = mock.MagicMock(
return_value=(mock.sentinel.entity_cls, mock.sentinel.claimed) 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", "homeassistant.components.zha.core.registries.ZHA_ENTITIES.get_entity",
get_entity_mock, get_entity_mock,
): ):
disc.PROBE.discover_by_device_type(ep_channels) disc.PROBE.discover_by_device_type(endpoint)
if hit: if hit:
assert get_entity_mock.call_count == 1 assert get_entity_mock.call_count == 1
assert ep_channels.claim_channels.call_count == 1 assert endpoint.claim_cluster_handlers.call_count == 1
assert ep_channels.claim_channels.call_args[0][0] is mock.sentinel.claimed assert endpoint.claim_cluster_handlers.call_args[0][0] is mock.sentinel.claimed
assert ep_channels.async_new_entity.call_count == 1 assert endpoint.async_new_entity.call_count == 1
assert ep_channels.async_new_entity.call_args[0][0] == component assert endpoint.async_new_entity.call_args[0][0] == platform
assert ep_channels.async_new_entity.call_args[0][1] == mock.sentinel.entity_cls assert endpoint.async_new_entity.call_args[0][1] == mock.sentinel.entity_cls
def test_discover_by_device_type_override() -> None: def test_discover_by_device_type_override() -> None:
"""Test entity discovery by device type overriding.""" """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 = mock.PropertyMock()
ep_mock.return_value.profile_id = 0x0104 ep_mock.return_value.profile_id = 0x0104
ep_mock.return_value.device_type = 0x0100 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( get_entity_mock = mock.MagicMock(
return_value=(mock.sentinel.entity_cls, mock.sentinel.claimed) 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", "homeassistant.components.zha.core.registries.ZHA_ENTITIES.get_entity",
get_entity_mock, get_entity_mock,
), mock.patch.dict(disc.PROBE._device_configs, overrides, clear=True): ), 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 get_entity_mock.call_count == 1
assert ep_channels.claim_channels.call_count == 1 assert endpoint.claim_cluster_handlers.call_count == 1
assert ep_channels.claim_channels.call_args[0][0] is mock.sentinel.claimed assert endpoint.claim_cluster_handlers.call_args[0][0] is mock.sentinel.claimed
assert ep_channels.async_new_entity.call_count == 1 assert endpoint.async_new_entity.call_count == 1
assert ep_channels.async_new_entity.call_args[0][0] == Platform.SWITCH assert endpoint.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.async_new_entity.call_args[0][1] == mock.sentinel.entity_cls
def test_discover_probe_single_cluster() -> None: def test_discover_probe_single_cluster() -> None:
"""Test entity discovery by single cluster.""" """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 = mock.PropertyMock()
ep_mock.return_value.profile_id = 0x0104 ep_mock.return_value.profile_id = 0x0104
ep_mock.return_value.device_type = 0x0100 ep_mock.return_value.device_type = 0x0100
type(ep_channels).endpoint = ep_mock type(endpoint).zigpy_endpoint = ep_mock
get_entity_mock = mock.MagicMock( get_entity_mock = mock.MagicMock(
return_value=(mock.sentinel.entity_cls, mock.sentinel.claimed) 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( with mock.patch(
"homeassistant.components.zha.core.registries.ZHA_ENTITIES.get_entity", "homeassistant.components.zha.core.registries.ZHA_ENTITIES.get_entity",
get_entity_mock, 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 get_entity_mock.call_count == 1
assert ep_channels.claim_channels.call_count == 1 assert endpoint.claim_cluster_handlers.call_count == 1
assert ep_channels.claim_channels.call_args[0][0] is mock.sentinel.claimed assert endpoint.claim_cluster_handlers.call_args[0][0] is mock.sentinel.claimed
assert ep_channels.async_new_entity.call_count == 1 assert endpoint.async_new_entity.call_count == 1
assert ep_channels.async_new_entity.call_args[0][0] == Platform.SWITCH assert endpoint.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.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.async_new_entity.call_args[0][3] == mock.sentinel.claimed
@pytest.mark.parametrize("device_info", DEVICES) @pytest.mark.parametrize("device_info", DEVICES)
async def test_discover_endpoint( async def test_discover_endpoint(
device_info, channels_mock, hass: HomeAssistant device_info, zha_device_mock, hass: HomeAssistant
) -> None: ) -> None:
"""Test device discovery.""" """Test device discovery."""
with mock.patch( with mock.patch(
"homeassistant.components.zha.core.channels.Channels.async_new_entity" "homeassistant.components.zha.core.endpoint.Endpoint.async_new_entity"
) as new_ent: ) as new_ent:
channels = channels_mock( device = zha_device_mock(
device_info[SIG_ENDPOINTS], device_info[SIG_ENDPOINTS],
manufacturer=device_info[SIG_MANUFACTURER], manufacturer=device_info[SIG_MANUFACTURER],
model=device_info[SIG_MODEL], model=device_info[SIG_MODEL],
node_desc=device_info[SIG_NODE_DESC], node_desc=device_info[SIG_NODE_DESC],
patch_cluster=False, patch_cluster=True,
) )
assert device_info[DEV_SIG_EVT_CHANNELS] == sorted( assert device_info[DEV_SIG_EVT_CLUSTER_HANDLERS] == sorted(
ch.id for pool in channels.pools for ch in pool.client_channels.values() 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 = {} ha_ent_info = {}
for call in new_ent.call_args_list: 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): if not contains_ignored_suffix(unique_id):
unique_id_head = UNIQUE_ID_HD.match(unique_id).group( unique_id_head = UNIQUE_ID_HD.match(unique_id).group(
0 0
) # ieee + endpoint_id ) # ieee + endpoint_id
ha_ent_info[(unique_id_head, entity_cls.__name__)] = ( ha_ent_info[(unique_id_head, entity_cls.__name__)] = (
component, platform,
unique_id, unique_id,
channels, cluster_handlers,
) )
for comp_id, ent_info in device_info[DEV_SIG_ENT_MAP].items(): for platform_id, ent_info in device_info[DEV_SIG_ENT_MAP].items():
component, unique_id = comp_id platform, unique_id = platform_id
test_ent_class = ent_info[DEV_SIG_ENT_MAP_CLASS] test_ent_class = ent_info[DEV_SIG_ENT_MAP_CLASS]
test_unique_id_head = UNIQUE_ID_HD.match(unique_id).group(0) test_unique_id_head = UNIQUE_ID_HD.match(unique_id).group(0)
assert (test_unique_id_head, test_ent_class) in ha_ent_info 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) (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" # unique_id used for discover is the same for "multi entities"
assert unique_id.startswith(ha_unique_id) assert unique_id.startswith(entity_unique_id)
assert {ch.name for ch in ha_channels} == set(ent_info[DEV_SIG_CHANNELS]) assert {ch.name for ch in entity_cluster_handlers} == set(
ent_info[DEV_SIG_CLUSTER_HANDLERS]
)
def _ch_mock(cluster): def _ch_mock(cluster):
"""Return mock of a channel with a cluster.""" """Return mock of a cluster_handler with a cluster."""
channel = mock.MagicMock() cluster_handler = mock.MagicMock()
type(channel).cluster = mock.PropertyMock(return_value=cluster(mock.MagicMock())) type(cluster_handler).cluster = mock.PropertyMock(
return channel return_value=cluster(mock.MagicMock())
)
return cluster_handler
@mock.patch( @mock.patch(
@ -401,16 +390,16 @@ def _test_single_input_cluster_device_class(probe_mock):
analog_ch = _ch_mock(_Analog) analog_ch = _ch_mock(_Analog)
ch_pool = mock.MagicMock(spec_set=zha_channels.ChannelPool) endpoint = mock.MagicMock(spec_set=Endpoint)
ch_pool.unclaimed_channels.return_value = [ endpoint.unclaimed_cluster_handlers.return_value = [
door_ch, door_ch,
cover_ch, cover_ch,
multistate_ch, multistate_ch,
ias_ch, ias_ch,
] ]
disc.ProbeEndpoint().discover_by_cluster_id(ch_pool) disc.ProbeEndpoint().discover_by_cluster_id(endpoint)
assert probe_mock.call_count == len(ch_pool.unclaimed_channels()) assert probe_mock.call_count == len(endpoint.unclaimed_cluster_handlers())
probes = ( probes = (
(Platform.LOCK, door_ch), (Platform.LOCK, door_ch),
(Platform.COVER, cover_ch), (Platform.COVER, cover_ch),
@ -419,8 +408,8 @@ def _test_single_input_cluster_device_class(probe_mock):
(Platform.SENSOR, analog_ch), (Platform.SENSOR, analog_ch),
) )
for call, details in zip(probe_mock.call_args_list, probes): for call, details in zip(probe_mock.call_args_list, probes):
component, ch = details platform, ch = details
assert call[0][0] == component assert call[0][0] == platform
assert call[0][1] == ch 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", "homeassistant.components.zha.entity.ZhaEntity.entity_registry_enabled_default",
new=Mock(return_value=True), 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, hass_disable_services,
zigpy_device_mock, zigpy_device_mock,
zha_device_joined_restored, zha_device_joined_restored,

View File

@ -1036,18 +1036,18 @@ async def test_transitions(
blocking=True, blocking=True,
) )
group_on_off_channel = zha_group.endpoint[general.OnOff.cluster_id] group_on_off_cluster_handler = zha_group.endpoint[general.OnOff.cluster_id]
group_level_channel = zha_group.endpoint[general.LevelControl.cluster_id] group_level_cluster_handler = zha_group.endpoint[general.LevelControl.cluster_id]
group_color_channel = zha_group.endpoint[lighting.Color.cluster_id] group_color_cluster_handler = zha_group.endpoint[lighting.Color.cluster_id]
assert group_on_off_channel.request.call_count == 0 assert group_on_off_cluster_handler.request.call_count == 0
assert group_on_off_channel.request.await_count == 0 assert group_on_off_cluster_handler.request.await_count == 0
assert group_color_channel.request.call_count == 1 assert group_color_cluster_handler.request.call_count == 1
assert group_color_channel.request.await_count == 1 assert group_color_cluster_handler.request.await_count == 1
assert group_level_channel.request.call_count == 1 assert group_level_cluster_handler.request.call_count == 1
assert group_level_channel.request.await_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 # 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, False,
dev2_cluster_color.commands_by_name["move_to_color_temp"].id, dev2_cluster_color.commands_by_name["move_to_color_temp"].id,
dev2_cluster_color.commands_by_name["move_to_color_temp"].schema, dev2_cluster_color.commands_by_name["move_to_color_temp"].schema,
@ -1058,7 +1058,7 @@ async def test_transitions(
tries=1, tries=1,
tsn=None, tsn=None,
) )
assert group_level_channel.request.call_args == call( assert group_level_cluster_handler.request.call_args == call(
False, 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"].id,
dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema, 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_temp"] == 235
assert group_state.attributes["color_mode"] == ColorMode.COLOR_TEMP assert group_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
group_on_off_channel.request.reset_mock() group_on_off_cluster_handler.request.reset_mock()
group_color_channel.request.reset_mock() group_color_cluster_handler.request.reset_mock()
group_level_channel.request.reset_mock() group_level_cluster_handler.request.reset_mock()
# turn the sengled light back on # turn the sengled light back on
await hass.services.async_call( await hass.services.async_call(

View File

@ -25,36 +25,48 @@ def zha_device():
@pytest.fixture @pytest.fixture
def channels(channel): def cluster_handlers(cluster_handler):
"""Return a mock of channels.""" """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( @pytest.mark.parametrize(
("rule", "matched"), ("rule", "matched"),
[ [
(registries.MatchRule(), False), (registries.MatchRule(), False),
(registries.MatchRule(channel_names={"level"}), True), (registries.MatchRule(cluster_handler_names={"level"}), True),
(registries.MatchRule(channel_names={"level", "no match"}), False), (registries.MatchRule(cluster_handler_names={"level", "no match"}), False),
(registries.MatchRule(channel_names={"on_off"}), True), (registries.MatchRule(cluster_handler_names={"on_off"}), True),
(registries.MatchRule(channel_names={"on_off", "no match"}), False), (registries.MatchRule(cluster_handler_names={"on_off", "no match"}), False),
(registries.MatchRule(channel_names={"on_off", "level"}), True), (registries.MatchRule(cluster_handler_names={"on_off", "level"}), True),
(registries.MatchRule(channel_names={"on_off", "level", "no match"}), False), (
registries.MatchRule(cluster_handler_names={"on_off", "level", "no match"}),
False,
),
# test generic_id matching # test generic_id matching
(registries.MatchRule(generic_ids={"channel_0x0006"}), True), (registries.MatchRule(generic_ids={"cluster_handler_0x0006"}), True),
(registries.MatchRule(generic_ids={"channel_0x0008"}), True), (registries.MatchRule(generic_ids={"cluster_handler_0x0008"}), True),
(registries.MatchRule(generic_ids={"channel_0x0006", "channel_0x0008"}), True),
( (
registries.MatchRule( 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, False,
), ),
( (
registries.MatchRule( registries.MatchRule(
generic_ids={"channel_0x0006", "channel_0x0008"}, generic_ids={"cluster_handler_0x0006", "cluster_handler_0x0008"},
channel_names={"on_off", "level"}, cluster_handler_names={"on_off", "level"},
), ),
True, True,
), ),
@ -62,34 +74,50 @@ def channels(channel):
(registries.MatchRule(manufacturers="no match"), False), (registries.MatchRule(manufacturers="no match"), False),
(registries.MatchRule(manufacturers=MANUFACTURER), True), (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, False,
), ),
( (
registries.MatchRule( registries.MatchRule(
manufacturers=MANUFACTURER, aux_channels="aux_channel" manufacturers=MANUFACTURER, aux_cluster_handlers="aux_cluster_handler"
), ),
True, True,
), ),
(registries.MatchRule(models=MODEL), True), (registries.MatchRule(models=MODEL), True),
(registries.MatchRule(models="no match"), False), (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, 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, False,
), ),
# match everything # match everything
( (
registries.MatchRule( registries.MatchRule(
generic_ids={"channel_0x0006", "channel_0x0008"}, generic_ids={"cluster_handler_0x0006", "cluster_handler_0x0008"},
channel_names={"on_off", "level"}, cluster_handler_names={"on_off", "level"},
manufacturers=MANUFACTURER, manufacturers=MANUFACTURER,
models=MODEL, models=MODEL,
quirk_classes=QUIRK_CLASS, quirk_classes=QUIRK_CLASS,
@ -98,96 +126,114 @@ def channels(channel):
), ),
( (
registries.MatchRule( registries.MatchRule(
channel_names="on_off", manufacturers={"random manuf", MANUFACTURER} cluster_handler_names="on_off",
manufacturers={"random manuf", MANUFACTURER},
), ),
True, True,
), ),
( (
registries.MatchRule( registries.MatchRule(
channel_names="on_off", manufacturers={"random manuf", "Another manuf"} cluster_handler_names="on_off",
manufacturers={"random manuf", "Another manuf"},
), ),
False, False,
), ),
( (
registries.MatchRule( registries.MatchRule(
channel_names="on_off", manufacturers=lambda x: x == MANUFACTURER cluster_handler_names="on_off",
manufacturers=lambda x: x == MANUFACTURER,
), ),
True, True,
), ),
( (
registries.MatchRule( registries.MatchRule(
channel_names="on_off", manufacturers=lambda x: x != MANUFACTURER cluster_handler_names="on_off",
manufacturers=lambda x: x != MANUFACTURER,
), ),
False, False,
), ),
( (
registries.MatchRule( registries.MatchRule(
channel_names="on_off", models={"random model", MODEL} cluster_handler_names="on_off", models={"random model", MODEL}
), ),
True, True,
), ),
( (
registries.MatchRule( registries.MatchRule(
channel_names="on_off", models={"random model", "Another model"} cluster_handler_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"}
), ),
False, False,
), ),
( (
registries.MatchRule( registries.MatchRule(
channel_names="on_off", quirk_classes=lambda x: x == QUIRK_CLASS cluster_handler_names="on_off", models=lambda x: x == MODEL
), ),
True, True,
), ),
( (
registries.MatchRule( 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, False,
), ),
], ],
) )
def test_registry_matching(rule, matched, channels) -> None: def test_registry_matching(rule, matched, cluster_handlers) -> None:
"""Test strict rule matching.""" """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( @pytest.mark.parametrize(
("rule", "matched"), ("rule", "matched"),
[ [
(registries.MatchRule(), False), (registries.MatchRule(), False),
(registries.MatchRule(channel_names={"level"}), True), (registries.MatchRule(cluster_handler_names={"level"}), True),
(registries.MatchRule(channel_names={"level", "no match"}), False), (registries.MatchRule(cluster_handler_names={"level", "no match"}), False),
(registries.MatchRule(channel_names={"on_off"}), True), (registries.MatchRule(cluster_handler_names={"on_off"}), True),
(registries.MatchRule(channel_names={"on_off", "no match"}), False), (registries.MatchRule(cluster_handler_names={"on_off", "no match"}), False),
(registries.MatchRule(channel_names={"on_off", "level"}), True), (registries.MatchRule(cluster_handler_names={"on_off", "level"}), True),
(registries.MatchRule(channel_names={"on_off", "level", "no match"}), False),
( (
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, True,
), ),
( (
registries.MatchRule( registries.MatchRule(
channel_names={"on_off", "level"}, cluster_handler_names={"on_off", "level"},
models="no match", models="no match",
manufacturers="no match", manufacturers="no match",
), ),
@ -195,40 +241,57 @@ def test_registry_matching(rule, matched, channels) -> None:
), ),
( (
registries.MatchRule( registries.MatchRule(
channel_names={"on_off", "level"}, cluster_handler_names={"on_off", "level"},
models="no match", models="no match",
manufacturers=MANUFACTURER, manufacturers=MANUFACTURER,
), ),
True, True,
), ),
# test generic_id matching # test generic_id matching
(registries.MatchRule(generic_ids={"channel_0x0006"}), True), (registries.MatchRule(generic_ids={"cluster_handler_0x0006"}), True),
(registries.MatchRule(generic_ids={"channel_0x0008"}), True), (registries.MatchRule(generic_ids={"cluster_handler_0x0008"}), True),
(registries.MatchRule(generic_ids={"channel_0x0006", "channel_0x0008"}), True),
( (
registries.MatchRule( 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, False,
), ),
( (
registries.MatchRule( registries.MatchRule(
generic_ids={"channel_0x0006", "channel_0x0008", "channel_0x0009"}, generic_ids={
"cluster_handler_0x0006",
"cluster_handler_0x0008",
"cluster_handler_0x0009",
},
models="mo match", models="mo match",
), ),
False, False,
), ),
( (
registries.MatchRule( registries.MatchRule(
generic_ids={"channel_0x0006", "channel_0x0008", "channel_0x0009"}, generic_ids={
"cluster_handler_0x0006",
"cluster_handler_0x0008",
"cluster_handler_0x0009",
},
models=MODEL, models=MODEL,
), ),
True, True,
), ),
( (
registries.MatchRule( registries.MatchRule(
generic_ids={"channel_0x0006", "channel_0x0008"}, generic_ids={"cluster_handler_0x0006", "cluster_handler_0x0008"},
channel_names={"on_off", "level"}, cluster_handler_names={"on_off", "level"},
), ),
True, True,
), ),
@ -242,8 +305,8 @@ def test_registry_matching(rule, matched, channels) -> None:
# match everything # match everything
( (
registries.MatchRule( registries.MatchRule(
generic_ids={"channel_0x0006", "channel_0x0008"}, generic_ids={"cluster_handler_0x0006", "cluster_handler_0x0008"},
channel_names={"on_off", "level"}, cluster_handler_names={"on_off", "level"},
manufacturers=MANUFACTURER, manufacturers=MANUFACTURER,
models=MODEL, models=MODEL,
quirk_classes=QUIRK_CLASS, 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.""" """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: def test_match_rule_claim_cluster_handlers_color(cluster_handler) -> None:
"""Test channel claiming.""" """Test cluster handler claiming."""
ch_color = channel("color", 0x300) ch_color = cluster_handler("color", 0x300)
ch_level = channel("level", 8) ch_level = cluster_handler("level", 8)
ch_onoff = channel("on_off", 6) ch_onoff = cluster_handler("on_off", 6)
rule = registries.MatchRule(channel_names="on_off", aux_channels={"color", "level"}) rule = registries.MatchRule(
claimed = rule.claim_channels([ch_color, ch_level, ch_onoff]) 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} assert {"color", "level", "on_off"} == {ch.name for ch in claimed}
@pytest.mark.parametrize( @pytest.mark.parametrize(
("rule", "match"), ("rule", "match"),
[ [
(registries.MatchRule(channel_names={"level"}), {"level"}), (registries.MatchRule(cluster_handler_names={"level"}), {"level"}),
(registries.MatchRule(channel_names={"level", "no match"}), {"level"}), (registries.MatchRule(cluster_handler_names={"level", "no match"}), {"level"}),
(registries.MatchRule(channel_names={"on_off"}), {"on_off"}), (registries.MatchRule(cluster_handler_names={"on_off"}), {"on_off"}),
(registries.MatchRule(generic_ids="channel_0x0000"), {"basic"}), (registries.MatchRule(generic_ids="cluster_handler_0x0000"), {"basic"}),
(
registries.MatchRule(channel_names="level", generic_ids="channel_0x0000"),
{"basic", "level"},
),
(registries.MatchRule(channel_names={"level", "power"}), {"level", "power"}),
( (
registries.MatchRule( 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"}, {"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: def test_match_rule_claim_cluster_handlers(
"""Test channel claiming.""" rule, match, cluster_handler, cluster_handlers
ch_basic = channel("basic", 0) ) -> None:
channels.append(ch_basic) """Test cluster handler claiming."""
ch_power = channel("power", 1) ch_basic = cluster_handler("basic", 0)
channels.append(ch_power) 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} assert match == {ch.name for ch in claimed}
@ -318,7 +394,7 @@ def entity_registry():
), ),
) )
def test_weighted_match( def test_weighted_match(
channel, cluster_handler,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
manufacturer, manufacturer,
model, model,
@ -331,40 +407,45 @@ def test_weighted_match(
@entity_registry.strict_match( @entity_registry.strict_match(
s.component, s.component,
channel_names="on_off", cluster_handler_names="on_off",
models={MODEL, "another model", "some model"}, models={MODEL, "another model", "some model"},
) )
class OnOffMultimodel: class OnOffMultimodel:
pass pass
@entity_registry.strict_match(s.component, channel_names="on_off") @entity_registry.strict_match(s.component, cluster_handler_names="on_off")
class OnOff: class OnOff:
pass pass
@entity_registry.strict_match( @entity_registry.strict_match(
s.component, channel_names="on_off", manufacturers=MANUFACTURER s.component, cluster_handler_names="on_off", manufacturers=MANUFACTURER
) )
class OnOffManufacturer: class OnOffManufacturer:
pass 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: class OnOffModel:
pass pass
@entity_registry.strict_match( @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: class OnOffModelManufacturer:
pass pass
@entity_registry.strict_match( @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: class OnOffQuirk:
pass pass
ch_on_off = channel("on_off", 6) ch_on_off = cluster_handler("on_off", 6)
ch_level = channel("level", 8) ch_level = cluster_handler("level", 8)
match, claimed = entity_registry.get_entity( match, claimed = entity_registry.get_entity(
s.component, manufacturer, model, [ch_on_off, ch_level], quirk_class s.component, manufacturer, model, [ch_on_off, ch_level], quirk_class
@ -374,25 +455,27 @@ def test_weighted_match(
assert claimed == [ch_on_off] 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.""" """Test multi-entity match."""
s = mock.sentinel s = mock.sentinel
@entity_registry.multipass_match( @entity_registry.multipass_match(
s.binary_sensor, s.binary_sensor,
channel_names="smartenergy_metering", cluster_handler_names="smartenergy_metering",
) )
class SmartEnergySensor2: class SmartEnergySensor2:
pass pass
ch_se = channel("smartenergy_metering", 0x0702) ch_se = cluster_handler("smartenergy_metering", 0x0702)
ch_illuminati = channel("illuminance", 0x0401) ch_illuminati = cluster_handler("illuminance", 0x0401)
match, claimed = entity_registry.get_multi_entity( match, claimed = entity_registry.get_multi_entity(
"manufacturer", "manufacturer",
"model", "model",
channels=[ch_se, ch_illuminati], cluster_handlers=[ch_se, ch_illuminati],
quirk_class="quirk_class", quirk_class="quirk_class",
) )
@ -404,15 +487,17 @@ def test_multi_sensor_match(channel, entity_registry: er.EntityRegistry) -> None
} }
@entity_registry.multipass_match( @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: class SmartEnergySensor1:
pass pass
@entity_registry.multipass_match( @entity_registry.multipass_match(
s.binary_sensor, s.binary_sensor,
channel_names="smartenergy_metering", cluster_handler_names="smartenergy_metering",
aux_channels="illuminance", aux_cluster_handlers="illuminance",
) )
class SmartEnergySensor3: class SmartEnergySensor3:
pass pass
@ -420,7 +505,7 @@ def test_multi_sensor_match(channel, entity_registry: er.EntityRegistry) -> None
match, claimed = entity_registry.get_multi_entity( match, claimed = entity_registry.get_multi_entity(
"manufacturer", "manufacturer",
"model", "model",
channels={ch_se, ch_illuminati}, cluster_handlers={ch_se, ch_illuminati},
quirk_class="quirk_class", quirk_class="quirk_class",
) )

View File

@ -10,7 +10,7 @@ import zigpy.zcl.clusters.measurement as measurement
import zigpy.zcl.clusters.smartenergy as smartenergy import zigpy.zcl.clusters.smartenergy as smartenergy
from homeassistant.components.sensor import SensorDeviceClass 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 import homeassistant.config as config_util
from homeassistant.const import ( from homeassistant.const import (
ATTR_DEVICE_CLASS, 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}) await send_attributes_report(hass, cluster, {0: 1, 1291: 100, 10: 1000})
assert int(hass.states.get(entity_id).state) == 100 assert int(hass.states.get(entity_id).state) == 100
channel = zha_device.channels.pools[0].all_channels["1:0x0b04"] cluster_handler = zha_device._endpoints[1].all_cluster_handlers["1:0x0b04"]
assert channel.ac_power_divisor == 1 assert cluster_handler.ac_power_divisor == 1
assert channel.ac_power_multiplier == 1 assert cluster_handler.ac_power_multiplier == 1
# update power divisor # update power divisor
await send_attributes_report(hass, cluster, {0: 1, 1291: 20, 0x0403: 5, 10: 1000}) await send_attributes_report(hass, cluster, {0: 1, 1291: 20, 0x0403: 5, 10: 1000})
assert channel.ac_power_divisor == 5 assert cluster_handler.ac_power_divisor == 5
assert channel.ac_power_multiplier == 1 assert cluster_handler.ac_power_multiplier == 1
assert hass.states.get(entity_id).state == "4.0" assert hass.states.get(entity_id).state == "4.0"
await send_attributes_report(hass, cluster, {0: 1, 1291: 30, 0x0605: 10, 10: 1000}) await send_attributes_report(hass, cluster, {0: 1, 1291: 30, 0x0605: 10, 10: 1000})
assert channel.ac_power_divisor == 10 assert cluster_handler.ac_power_divisor == 10
assert channel.ac_power_multiplier == 1 assert cluster_handler.ac_power_multiplier == 1
assert hass.states.get(entity_id).state == "3.0" assert hass.states.get(entity_id).state == "3.0"
# update power multiplier # update power multiplier
await send_attributes_report(hass, cluster, {0: 1, 1291: 20, 0x0402: 6, 10: 1000}) await send_attributes_report(hass, cluster, {0: 1, 1291: 20, 0x0402: 6, 10: 1000})
assert channel.ac_power_divisor == 10 assert cluster_handler.ac_power_divisor == 10
assert channel.ac_power_multiplier == 6 assert cluster_handler.ac_power_multiplier == 6
assert hass.states.get(entity_id).state == "12.0" assert hass.states.get(entity_id).state == "12.0"
await send_attributes_report(hass, cluster, {0: 1, 1291: 30, 0x0604: 20, 10: 1000}) await send_attributes_report(hass, cluster, {0: 1, 1291: 30, 0x0604: 20, 10: 1000})
assert channel.ac_power_divisor == 10 assert cluster_handler.ac_power_divisor == 10
assert channel.ac_power_multiplier == 20 assert cluster_handler.ac_power_multiplier == 20
assert hass.states.get(entity_id).state == "60.0" 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 async_update_entity(hass, entity_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert cluster.read_attributes.call_count == math.ceil( 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 = { read_attrs = {
a for call in cluster.read_attributes.call_args_list for a in call[0][0] 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