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/sensor.py
homeassistant/components/zha/websocket_api.py
homeassistant/components/zha/core/channels/*
homeassistant/components/zha/core/cluster_handlers/*
homeassistant/components/zha/core/device.py
homeassistant/components/zha/core/gateway.py
homeassistant/components/zha/core/helpers.py

View File

@ -25,13 +25,13 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .core import discovery
from .core.channels.security import (
from .core.cluster_handlers.security import (
SIGNAL_ALARM_TRIGGERED,
SIGNAL_ARMED_STATE_CHANGED,
IasAce as AceChannel,
IasAce as AceClusterHandler,
)
from .core.const import (
CHANNEL_IAS_ACE,
CLUSTER_HANDLER_IAS_ACE,
CONF_ALARM_ARM_REQUIRES_CODE,
CONF_ALARM_FAILED_TRIES,
CONF_ALARM_MASTER_CODE,
@ -77,7 +77,7 @@ async def async_setup_entry(
config_entry.async_on_unload(unsub)
@STRICT_MATCH(channel_names=CHANNEL_IAS_ACE)
@STRICT_MATCH(cluster_handler_names=CLUSTER_HANDLER_IAS_ACE)
class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity):
"""Entity for ZHA alarm control devices."""
@ -89,18 +89,20 @@ class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity):
| AlarmControlPanelEntityFeature.TRIGGER
)
def __init__(self, unique_id, zha_device: ZHADevice, channels, **kwargs) -> None:
def __init__(
self, unique_id, zha_device: ZHADevice, cluster_handlers, **kwargs
) -> None:
"""Initialize the ZHA alarm control device."""
super().__init__(unique_id, zha_device, channels, **kwargs)
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
cfg_entry = zha_device.gateway.config_entry
self._channel: AceChannel = channels[0]
self._channel.panel_code = async_get_zha_config_value(
self._cluster_handler: AceClusterHandler = cluster_handlers[0]
self._cluster_handler.panel_code = async_get_zha_config_value(
cfg_entry, ZHA_ALARM_OPTIONS, CONF_ALARM_MASTER_CODE, "1234"
)
self._channel.code_required_arm_actions = async_get_zha_config_value(
self._cluster_handler.code_required_arm_actions = async_get_zha_config_value(
cfg_entry, ZHA_ALARM_OPTIONS, CONF_ALARM_ARM_REQUIRES_CODE, False
)
self._channel.max_invalid_tries = async_get_zha_config_value(
self._cluster_handler.max_invalid_tries = async_get_zha_config_value(
cfg_entry, ZHA_ALARM_OPTIONS, CONF_ALARM_FAILED_TRIES, 3
)
@ -108,10 +110,10 @@ class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity):
"""Run when about to be added to hass."""
await super().async_added_to_hass()
self.async_accept_signal(
self._channel, SIGNAL_ARMED_STATE_CHANGED, self.async_set_armed_mode
self._cluster_handler, SIGNAL_ARMED_STATE_CHANGED, self.async_set_armed_mode
)
self.async_accept_signal(
self._channel, SIGNAL_ALARM_TRIGGERED, self.async_alarm_trigger
self._cluster_handler, SIGNAL_ALARM_TRIGGERED, self.async_alarm_trigger
)
@callback
@ -122,26 +124,26 @@ class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity):
@property
def code_arm_required(self) -> bool:
"""Whether the code is required for arm actions."""
return self._channel.code_required_arm_actions
return self._cluster_handler.code_required_arm_actions
async def async_alarm_disarm(self, code: str | None = None) -> None:
"""Send disarm command."""
self._channel.arm(IasAce.ArmMode.Disarm, code, 0)
self._cluster_handler.arm(IasAce.ArmMode.Disarm, code, 0)
self.async_write_ha_state()
async def async_alarm_arm_home(self, code: str | None = None) -> None:
"""Send arm home command."""
self._channel.arm(IasAce.ArmMode.Arm_Day_Home_Only, code, 0)
self._cluster_handler.arm(IasAce.ArmMode.Arm_Day_Home_Only, code, 0)
self.async_write_ha_state()
async def async_alarm_arm_away(self, code: str | None = None) -> None:
"""Send arm away command."""
self._channel.arm(IasAce.ArmMode.Arm_All_Zones, code, 0)
self._cluster_handler.arm(IasAce.ArmMode.Arm_All_Zones, code, 0)
self.async_write_ha_state()
async def async_alarm_arm_night(self, code: str | None = None) -> None:
"""Send arm night command."""
self._channel.arm(IasAce.ArmMode.Arm_Night_Sleep_Only, code, 0)
self._cluster_handler.arm(IasAce.ArmMode.Arm_Night_Sleep_Only, code, 0)
self.async_write_ha_state()
async def async_alarm_trigger(self, code: str | None = None) -> None:
@ -151,4 +153,4 @@ class ZHAAlarmControlPanel(ZhaEntity, AlarmControlPanelEntity):
@property
def state(self) -> str | None:
"""Return the state of the entity."""
return IAS_ACE_STATE_MAP.get(self._channel.armed_state)
return IAS_ACE_STATE_MAP.get(self._cluster_handler.armed_state)

View File

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

View File

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

View File

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

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

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

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

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

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

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

@ -89,7 +89,7 @@ class ZHAGroupMember(LogMixin):
entity_ref.reference_id,
)._asdict()
for entity_ref in zha_device_registry.get(self.device.ieee)
if list(entity_ref.cluster_channels.values())[
if list(entity_ref.cluster_handlers.values())[
0
].cluster.endpoint.endpoint_id
== self.endpoint_id

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -121,19 +121,19 @@ def setup_zha(hass, config_entry, zigpy_app_controller):
@pytest.fixture
def channel():
"""Channel mock factory fixture."""
def cluster_handler():
"""ClusterHandler mock factory fixture."""
def channel(name: str, cluster_id: int, endpoint_id: int = 1):
def cluster_handler(name: str, cluster_id: int, endpoint_id: int = 1):
ch = MagicMock()
ch.name = name
ch.generic_id = f"channel_0x{cluster_id:04x}"
ch.generic_id = f"cluster_handler_0x{cluster_id:04x}"
ch.id = f"{endpoint_id}:0x{cluster_id:04x}"
ch.async_configure = AsyncMock()
ch.async_initialize = AsyncMock()
return ch
return channel
return cluster_handler
@pytest.fixture
@ -162,7 +162,7 @@ def zigpy_device_mock(zigpy_app_controller):
for epid, ep in endpoints.items():
endpoint = device.add_endpoint(epid)
endpoint.device_type = ep[SIG_EP_TYPE]
endpoint.profile_id = ep.get(SIG_EP_PROFILE)
endpoint.profile_id = ep.get(SIG_EP_PROFILE, 0x0104)
endpoint.request = AsyncMock(return_value=[0])
for cluster_id in ep.get(SIG_EP_INPUT, []):
@ -171,6 +171,8 @@ def zigpy_device_mock(zigpy_app_controller):
for cluster_id in ep.get(SIG_EP_OUTPUT, []):
endpoint.add_output_cluster(cluster_id)
device.status = zigpy.device.Status.ENDPOINTS_INIT
if quirk:
device = quirk(zigpy_app_controller, device.ieee, device.nwk, device)

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

View File

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

View File

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

View File

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

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()
channel = zha_device.channels.pools[0].client_channels["1:0x0006"]
channel.zha_send_event(COMMAND_SINGLE, [])
cluster_handler = zha_device.endpoints[1].client_cluster_handlers["1:0x0006"]
cluster_handler.zha_send_event(COMMAND_SINGLE, [])
await hass.async_block_till_done()
assert len(calls) == 1

View File

@ -14,10 +14,10 @@ import zigpy.zcl.clusters.security
import zigpy.zcl.foundation as zcl_f
import homeassistant.components.zha.binary_sensor
import homeassistant.components.zha.core.channels as zha_channels
import homeassistant.components.zha.core.channels.base as base_channels
import homeassistant.components.zha.core.cluster_handlers as cluster_handlers
import homeassistant.components.zha.core.const as zha_const
import homeassistant.components.zha.core.discovery as disc
from homeassistant.components.zha.core.endpoint import Endpoint
import homeassistant.components.zha.core.registries as zha_regs
import homeassistant.components.zha.cover
import homeassistant.components.zha.device_tracker
@ -33,11 +33,11 @@ import homeassistant.helpers.entity_registry as er
from .common import get_zha_gateway
from .conftest import SIG_EP_INPUT, SIG_EP_OUTPUT, SIG_EP_PROFILE, SIG_EP_TYPE
from .zha_devices_list import (
DEV_SIG_CHANNELS,
DEV_SIG_CLUSTER_HANDLERS,
DEV_SIG_ENT_MAP,
DEV_SIG_ENT_MAP_CLASS,
DEV_SIG_ENT_MAP_ID,
DEV_SIG_EVT_CHANNELS,
DEV_SIG_EVT_CLUSTER_HANDLERS,
DEVICES,
)
@ -63,27 +63,6 @@ def contains_ignored_suffix(unique_id: str) -> bool:
return False
@pytest.fixture
def channels_mock(zha_device_mock):
"""Channels mock factory."""
def _mock(
endpoints,
ieee="00:11:22:33:44:55:66:77",
manufacturer="mock manufacturer",
model="mock model",
node_desc=b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00",
patch_cluster=False,
):
zha_dev = zha_device_mock(
endpoints, ieee, manufacturer, model, node_desc, patch_cluster=patch_cluster
)
channels = zha_channels.Channels.new(zha_dev)
return channels
return _mock
@patch(
"zigpy.zcl.clusters.general.Identify.request",
new=AsyncMock(return_value=[mock.sentinel.data, zcl_f.Status.SUCCESS]),
@ -119,14 +98,14 @@ async def test_devices(
if cluster_identify:
cluster_identify.request.reset_mock()
orig_new_entity = zha_channels.ChannelPool.async_new_entity
orig_new_entity = Endpoint.async_new_entity
_dispatch = mock.MagicMock(wraps=orig_new_entity)
try:
zha_channels.ChannelPool.async_new_entity = lambda *a, **kw: _dispatch(*a, **kw)
Endpoint.async_new_entity = lambda *a, **kw: _dispatch(*a, **kw)
zha_dev = await zha_device_joined_restored(zigpy_device)
await hass_disable_services.async_block_till_done()
finally:
zha_channels.ChannelPool.async_new_entity = orig_new_entity
Endpoint.async_new_entity = orig_new_entity
if cluster_identify:
called = int(zha_device_joined_restored.name == "zha_device_joined")
@ -147,34 +126,36 @@ async def test_devices(
tsn=None,
)
event_channels = {
ch.id for pool in zha_dev.channels.pools for ch in pool.client_channels.values()
event_cluster_handlers = {
ch.id
for endpoint in zha_dev._endpoints.values()
for ch in endpoint.client_cluster_handlers.values()
}
assert event_channels == set(device[DEV_SIG_EVT_CHANNELS])
assert event_cluster_handlers == set(device[DEV_SIG_EVT_CLUSTER_HANDLERS])
# we need to probe the class create entity factory so we need to reset this to get accurate results
zha_regs.ZHA_ENTITIES.clean_up()
# build a dict of entity_class -> (component, unique_id, channels) tuple
# build a dict of entity_class -> (platform, unique_id, cluster_handlers) tuple
ha_ent_info = {}
created_entity_count = 0
for call in _dispatch.call_args_list:
_, component, entity_cls, unique_id, channels = call[0]
_, platform, entity_cls, unique_id, cluster_handlers = call[0]
# the factory can return None. We filter these out to get an accurate created entity count
response = entity_cls.create_entity(unique_id, zha_dev, channels)
response = entity_cls.create_entity(unique_id, zha_dev, cluster_handlers)
if response and not contains_ignored_suffix(response.name):
created_entity_count += 1
unique_id_head = UNIQUE_ID_HD.match(unique_id).group(
0
) # ieee + endpoint_id
ha_ent_info[(unique_id_head, entity_cls.__name__)] = (
component,
platform,
unique_id,
channels,
cluster_handlers,
)
for comp_id, ent_info in device[DEV_SIG_ENT_MAP].items():
component, unique_id = comp_id
platform, unique_id = comp_id
no_tail_id = NO_TAIL_ID.sub("", ent_info[DEV_SIG_ENT_MAP_ID])
ha_entity_id = entity_registry.async_get_entity_id(component, "zha", unique_id)
ha_entity_id = entity_registry.async_get_entity_id(platform, "zha", unique_id)
assert ha_entity_id is not None
assert ha_entity_id.startswith(no_tail_id)
@ -182,13 +163,15 @@ async def test_devices(
test_unique_id_head = UNIQUE_ID_HD.match(unique_id).group(0)
assert (test_unique_id_head, test_ent_class) in ha_ent_info
ha_comp, ha_unique_id, ha_channels = ha_ent_info[
ha_comp, ha_unique_id, ha_cluster_handlers = ha_ent_info[
(test_unique_id_head, test_ent_class)
]
assert component is ha_comp.value
assert platform is ha_comp.value
# unique_id used for discover is the same for "multi entities"
assert unique_id.startswith(ha_unique_id)
assert {ch.name for ch in ha_channels} == set(ent_info[DEV_SIG_CHANNELS])
assert {ch.name for ch in ha_cluster_handlers} == set(
ent_info[DEV_SIG_CLUSTER_HANDLERS]
)
assert created_entity_count == len(device[DEV_SIG_ENT_MAP])
@ -219,16 +202,16 @@ def _get_first_identify_cluster(zigpy_device):
)
def test_discover_entities(m1, m2) -> None:
"""Test discover endpoint class method."""
ep_channels = mock.MagicMock()
disc.PROBE.discover_entities(ep_channels)
endpoint = mock.MagicMock()
disc.PROBE.discover_entities(endpoint)
assert m1.call_count == 1
assert m1.call_args[0][0] is ep_channels
assert m1.call_args[0][0] is endpoint
assert m2.call_count == 1
assert m2.call_args[0][0] is ep_channels
assert m2.call_args[0][0] is endpoint
@pytest.mark.parametrize(
("device_type", "component", "hit"),
("device_type", "platform", "hit"),
[
(zigpy.profiles.zha.DeviceType.ON_OFF_LIGHT, Platform.LIGHT, True),
(zigpy.profiles.zha.DeviceType.ON_OFF_BALLAST, Platform.SWITCH, True),
@ -236,14 +219,14 @@ def test_discover_entities(m1, m2) -> None:
(0xFFFF, None, False),
],
)
def test_discover_by_device_type(device_type, component, hit) -> None:
def test_discover_by_device_type(device_type, platform, hit) -> None:
"""Test entity discovery by device type."""
ep_channels = mock.MagicMock(spec_set=zha_channels.ChannelPool)
endpoint = mock.MagicMock(spec_set=Endpoint)
ep_mock = mock.PropertyMock()
ep_mock.return_value.profile_id = 0x0104
ep_mock.return_value.device_type = device_type
type(ep_channels).endpoint = ep_mock
type(endpoint).zigpy_endpoint = ep_mock
get_entity_mock = mock.MagicMock(
return_value=(mock.sentinel.entity_cls, mock.sentinel.claimed)
@ -252,26 +235,26 @@ def test_discover_by_device_type(device_type, component, hit) -> None:
"homeassistant.components.zha.core.registries.ZHA_ENTITIES.get_entity",
get_entity_mock,
):
disc.PROBE.discover_by_device_type(ep_channels)
disc.PROBE.discover_by_device_type(endpoint)
if hit:
assert get_entity_mock.call_count == 1
assert ep_channels.claim_channels.call_count == 1
assert ep_channels.claim_channels.call_args[0][0] is mock.sentinel.claimed
assert ep_channels.async_new_entity.call_count == 1
assert ep_channels.async_new_entity.call_args[0][0] == component
assert ep_channels.async_new_entity.call_args[0][1] == mock.sentinel.entity_cls
assert endpoint.claim_cluster_handlers.call_count == 1
assert endpoint.claim_cluster_handlers.call_args[0][0] is mock.sentinel.claimed
assert endpoint.async_new_entity.call_count == 1
assert endpoint.async_new_entity.call_args[0][0] == platform
assert endpoint.async_new_entity.call_args[0][1] == mock.sentinel.entity_cls
def test_discover_by_device_type_override() -> None:
"""Test entity discovery by device type overriding."""
ep_channels = mock.MagicMock(spec_set=zha_channels.ChannelPool)
endpoint = mock.MagicMock(spec_set=Endpoint)
ep_mock = mock.PropertyMock()
ep_mock.return_value.profile_id = 0x0104
ep_mock.return_value.device_type = 0x0100
type(ep_channels).endpoint = ep_mock
type(endpoint).zigpy_endpoint = ep_mock
overrides = {ep_channels.unique_id: {"type": Platform.SWITCH}}
overrides = {endpoint.unique_id: {"type": Platform.SWITCH}}
get_entity_mock = mock.MagicMock(
return_value=(mock.sentinel.entity_cls, mock.sentinel.claimed)
)
@ -279,99 +262,105 @@ def test_discover_by_device_type_override() -> None:
"homeassistant.components.zha.core.registries.ZHA_ENTITIES.get_entity",
get_entity_mock,
), mock.patch.dict(disc.PROBE._device_configs, overrides, clear=True):
disc.PROBE.discover_by_device_type(ep_channels)
disc.PROBE.discover_by_device_type(endpoint)
assert get_entity_mock.call_count == 1
assert ep_channels.claim_channels.call_count == 1
assert ep_channels.claim_channels.call_args[0][0] is mock.sentinel.claimed
assert ep_channels.async_new_entity.call_count == 1
assert ep_channels.async_new_entity.call_args[0][0] == Platform.SWITCH
assert ep_channels.async_new_entity.call_args[0][1] == mock.sentinel.entity_cls
assert endpoint.claim_cluster_handlers.call_count == 1
assert endpoint.claim_cluster_handlers.call_args[0][0] is mock.sentinel.claimed
assert endpoint.async_new_entity.call_count == 1
assert endpoint.async_new_entity.call_args[0][0] == Platform.SWITCH
assert endpoint.async_new_entity.call_args[0][1] == mock.sentinel.entity_cls
def test_discover_probe_single_cluster() -> None:
"""Test entity discovery by single cluster."""
ep_channels = mock.MagicMock(spec_set=zha_channels.ChannelPool)
endpoint = mock.MagicMock(spec_set=Endpoint)
ep_mock = mock.PropertyMock()
ep_mock.return_value.profile_id = 0x0104
ep_mock.return_value.device_type = 0x0100
type(ep_channels).endpoint = ep_mock
type(endpoint).zigpy_endpoint = ep_mock
get_entity_mock = mock.MagicMock(
return_value=(mock.sentinel.entity_cls, mock.sentinel.claimed)
)
channel_mock = mock.MagicMock(spec_set=base_channels.ZigbeeChannel)
cluster_handler_mock = mock.MagicMock(spec_set=cluster_handlers.ClusterHandler)
with mock.patch(
"homeassistant.components.zha.core.registries.ZHA_ENTITIES.get_entity",
get_entity_mock,
):
disc.PROBE.probe_single_cluster(Platform.SWITCH, channel_mock, ep_channels)
disc.PROBE.probe_single_cluster(Platform.SWITCH, cluster_handler_mock, endpoint)
assert get_entity_mock.call_count == 1
assert ep_channels.claim_channels.call_count == 1
assert ep_channels.claim_channels.call_args[0][0] is mock.sentinel.claimed
assert ep_channels.async_new_entity.call_count == 1
assert ep_channels.async_new_entity.call_args[0][0] == Platform.SWITCH
assert ep_channels.async_new_entity.call_args[0][1] == mock.sentinel.entity_cls
assert ep_channels.async_new_entity.call_args[0][3] == mock.sentinel.claimed
assert endpoint.claim_cluster_handlers.call_count == 1
assert endpoint.claim_cluster_handlers.call_args[0][0] is mock.sentinel.claimed
assert endpoint.async_new_entity.call_count == 1
assert endpoint.async_new_entity.call_args[0][0] == Platform.SWITCH
assert endpoint.async_new_entity.call_args[0][1] == mock.sentinel.entity_cls
assert endpoint.async_new_entity.call_args[0][3] == mock.sentinel.claimed
@pytest.mark.parametrize("device_info", DEVICES)
async def test_discover_endpoint(
device_info, channels_mock, hass: HomeAssistant
device_info, zha_device_mock, hass: HomeAssistant
) -> None:
"""Test device discovery."""
with mock.patch(
"homeassistant.components.zha.core.channels.Channels.async_new_entity"
"homeassistant.components.zha.core.endpoint.Endpoint.async_new_entity"
) as new_ent:
channels = channels_mock(
device = zha_device_mock(
device_info[SIG_ENDPOINTS],
manufacturer=device_info[SIG_MANUFACTURER],
model=device_info[SIG_MODEL],
node_desc=device_info[SIG_NODE_DESC],
patch_cluster=False,
patch_cluster=True,
)
assert device_info[DEV_SIG_EVT_CHANNELS] == sorted(
ch.id for pool in channels.pools for ch in pool.client_channels.values()
assert device_info[DEV_SIG_EVT_CLUSTER_HANDLERS] == sorted(
ch.id
for endpoint in device._endpoints.values()
for ch in endpoint.client_cluster_handlers.values()
)
# build a dict of entity_class -> (component, unique_id, channels) tuple
# build a dict of entity_class -> (platform, unique_id, cluster_handlers) tuple
ha_ent_info = {}
for call in new_ent.call_args_list:
component, entity_cls, unique_id, channels = call[0]
platform, entity_cls, unique_id, cluster_handlers = call[0]
if not contains_ignored_suffix(unique_id):
unique_id_head = UNIQUE_ID_HD.match(unique_id).group(
0
) # ieee + endpoint_id
ha_ent_info[(unique_id_head, entity_cls.__name__)] = (
component,
platform,
unique_id,
channels,
cluster_handlers,
)
for comp_id, ent_info in device_info[DEV_SIG_ENT_MAP].items():
component, unique_id = comp_id
for platform_id, ent_info in device_info[DEV_SIG_ENT_MAP].items():
platform, unique_id = platform_id
test_ent_class = ent_info[DEV_SIG_ENT_MAP_CLASS]
test_unique_id_head = UNIQUE_ID_HD.match(unique_id).group(0)
assert (test_unique_id_head, test_ent_class) in ha_ent_info
ha_comp, ha_unique_id, ha_channels = ha_ent_info[
entity_platform, entity_unique_id, entity_cluster_handlers = ha_ent_info[
(test_unique_id_head, test_ent_class)
]
assert component is ha_comp.value
assert platform is entity_platform.value
# unique_id used for discover is the same for "multi entities"
assert unique_id.startswith(ha_unique_id)
assert {ch.name for ch in ha_channels} == set(ent_info[DEV_SIG_CHANNELS])
assert unique_id.startswith(entity_unique_id)
assert {ch.name for ch in entity_cluster_handlers} == set(
ent_info[DEV_SIG_CLUSTER_HANDLERS]
)
def _ch_mock(cluster):
"""Return mock of a channel with a cluster."""
channel = mock.MagicMock()
type(channel).cluster = mock.PropertyMock(return_value=cluster(mock.MagicMock()))
return channel
"""Return mock of a cluster_handler with a cluster."""
cluster_handler = mock.MagicMock()
type(cluster_handler).cluster = mock.PropertyMock(
return_value=cluster(mock.MagicMock())
)
return cluster_handler
@mock.patch(
@ -401,16 +390,16 @@ def _test_single_input_cluster_device_class(probe_mock):
analog_ch = _ch_mock(_Analog)
ch_pool = mock.MagicMock(spec_set=zha_channels.ChannelPool)
ch_pool.unclaimed_channels.return_value = [
endpoint = mock.MagicMock(spec_set=Endpoint)
endpoint.unclaimed_cluster_handlers.return_value = [
door_ch,
cover_ch,
multistate_ch,
ias_ch,
]
disc.ProbeEndpoint().discover_by_cluster_id(ch_pool)
assert probe_mock.call_count == len(ch_pool.unclaimed_channels())
disc.ProbeEndpoint().discover_by_cluster_id(endpoint)
assert probe_mock.call_count == len(endpoint.unclaimed_cluster_handlers())
probes = (
(Platform.LOCK, door_ch),
(Platform.COVER, cover_ch),
@ -419,8 +408,8 @@ def _test_single_input_cluster_device_class(probe_mock):
(Platform.SENSOR, analog_ch),
)
for call, details in zip(probe_mock.call_args_list, probes):
component, ch = details
assert call[0][0] == component
platform, ch = details
assert call[0][0] == platform
assert call[0][1] == ch
@ -498,7 +487,7 @@ async def test_group_probe_cleanup_called(
"homeassistant.components.zha.entity.ZhaEntity.entity_registry_enabled_default",
new=Mock(return_value=True),
)
async def test_channel_with_empty_ep_attribute_cluster(
async def test_cluster_handler_with_empty_ep_attribute_cluster(
hass_disable_services,
zigpy_device_mock,
zha_device_joined_restored,

View File

@ -1036,18 +1036,18 @@ async def test_transitions(
blocking=True,
)
group_on_off_channel = zha_group.endpoint[general.OnOff.cluster_id]
group_level_channel = zha_group.endpoint[general.LevelControl.cluster_id]
group_color_channel = zha_group.endpoint[lighting.Color.cluster_id]
assert group_on_off_channel.request.call_count == 0
assert group_on_off_channel.request.await_count == 0
assert group_color_channel.request.call_count == 1
assert group_color_channel.request.await_count == 1
assert group_level_channel.request.call_count == 1
assert group_level_channel.request.await_count == 1
group_on_off_cluster_handler = zha_group.endpoint[general.OnOff.cluster_id]
group_level_cluster_handler = zha_group.endpoint[general.LevelControl.cluster_id]
group_color_cluster_handler = zha_group.endpoint[lighting.Color.cluster_id]
assert group_on_off_cluster_handler.request.call_count == 0
assert group_on_off_cluster_handler.request.await_count == 0
assert group_color_cluster_handler.request.call_count == 1
assert group_color_cluster_handler.request.await_count == 1
assert group_level_cluster_handler.request.call_count == 1
assert group_level_cluster_handler.request.await_count == 1
# groups are omitted from the 3 call dance for new_color_provided_while_off
assert group_color_channel.request.call_args == call(
assert group_color_cluster_handler.request.call_args == call(
False,
dev2_cluster_color.commands_by_name["move_to_color_temp"].id,
dev2_cluster_color.commands_by_name["move_to_color_temp"].schema,
@ -1058,7 +1058,7 @@ async def test_transitions(
tries=1,
tsn=None,
)
assert group_level_channel.request.call_args == call(
assert group_level_cluster_handler.request.call_args == call(
False,
dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].id,
dev2_cluster_level.commands_by_name["move_to_level_with_on_off"].schema,
@ -1076,9 +1076,9 @@ async def test_transitions(
assert group_state.attributes["color_temp"] == 235
assert group_state.attributes["color_mode"] == ColorMode.COLOR_TEMP
group_on_off_channel.request.reset_mock()
group_color_channel.request.reset_mock()
group_level_channel.request.reset_mock()
group_on_off_cluster_handler.request.reset_mock()
group_color_cluster_handler.request.reset_mock()
group_level_cluster_handler.request.reset_mock()
# turn the sengled light back on
await hass.services.async_call(

View File

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

View File

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

File diff suppressed because it is too large Load Diff