From 60387a417fb82b47700899a6b7e80b30dcc9766f Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 27 May 2022 09:43:39 -0400 Subject: [PATCH] Add support for polled Smart Energy Metering sensors to ZHA (#71527) * Add framework for polled se metering sensors * add model * find attr * type info --- .../zha/core/channels/homeautomation.py | 2 +- .../zha/core/channels/smartenergy.py | 26 +++++++++++- homeassistant/components/zha/sensor.py | 25 ++++++++++- homeassistant/components/zha/switch.py | 42 ++++++++++++------- 4 files changed, 77 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/zha/core/channels/homeautomation.py b/homeassistant/components/zha/core/channels/homeautomation.py index e1019ed31bf..60c33c93003 100644 --- a/homeassistant/components/zha/core/channels/homeautomation.py +++ b/homeassistant/components/zha/core/channels/homeautomation.py @@ -102,7 +102,7 @@ class ElectricalMeasurementChannel(ZigbeeChannel): for attr, value in result.items(): self.async_send_signal( f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", - self.cluster.attridx.get(attr, attr), + self.cluster.find_attribute(attr).id, attr, value, ) diff --git a/homeassistant/components/zha/core/channels/smartenergy.py b/homeassistant/components/zha/core/channels/smartenergy.py index b153372a322..927ceb248c5 100644 --- a/homeassistant/components/zha/core/channels/smartenergy.py +++ b/homeassistant/components/zha/core/channels/smartenergy.py @@ -7,7 +7,12 @@ from functools import partialmethod from zigpy.zcl.clusters import smartenergy from .. import registries, typing as zha_typing -from ..const import REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, REPORT_CONFIG_OP +from ..const import ( + REPORT_CONFIG_ASAP, + REPORT_CONFIG_DEFAULT, + REPORT_CONFIG_OP, + SIGNAL_ATTR_UPDATED, +) from .base import ZigbeeChannel @@ -163,6 +168,25 @@ class Metering(ZigbeeChannel): ) # 1 digit to the right, 15 digits to the left self._summa_format = self.get_formatting(fmting) + async def async_force_update(self) -> None: + """Retrieve latest state.""" + self.debug("async_force_update") + + attrs = [ + a["attr"] + for a in self.REPORT_CONFIG + if a["attr"] not in self.cluster.unsupported_attributes + ] + result = await self.get_attributes(attrs, from_cache=False, only_cache=False) + if result: + for attr, value in result.items(): + self.async_send_signal( + f"{self.unique_id}_{SIGNAL_ATTR_UPDATED}", + self.cluster.find_attribute(attr).id, + attr, + value, + ) + @staticmethod def get_formatting(formatting: int) -> str: """Return a formatting string, given the formatting value. diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 3e3017f6fa9..36ca873188f 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -420,7 +420,10 @@ class Illuminance(Sensor): return round(pow(10, ((value - 1) / 10000)), 1) -@MULTI_MATCH(channel_names=CHANNEL_SMARTENERGY_METERING) +@MULTI_MATCH( + channel_names=CHANNEL_SMARTENERGY_METERING, + stop_on_match_group=CHANNEL_SMARTENERGY_METERING, +) class SmartEnergyMetering(Sensor): """Metering sensor.""" @@ -464,6 +467,26 @@ class SmartEnergyMetering(Sensor): return attrs +@MULTI_MATCH( + channel_names=CHANNEL_SMARTENERGY_METERING, + models={"TS011F"}, + stop_on_match_group=CHANNEL_SMARTENERGY_METERING, +) +class PolledSmartEnergyMetering(SmartEnergyMetering): + """Polled metering sensor.""" + + @property + def should_poll(self) -> bool: + """Poll the entity for current state.""" + return True + + async def async_update(self) -> None: + """Retrieve latest state.""" + if not self.available: + return + await self._channel.async_force_update() + + @MULTI_MATCH(channel_names=CHANNEL_SMARTENERGY_METERING) class SmartEnergySummation(SmartEnergyMetering, id_suffix="summation_delivered"): """Smart Energy Metering summation sensor.""" diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index d9199ed77c8..1926b08fc60 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -29,6 +29,7 @@ from .entity import ZhaEntity, ZhaGroupEntity if TYPE_CHECKING: from .core.channels.base import ZigbeeChannel + from .core.channels.general import OnOffChannel from .core.device import ZHADevice STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.SWITCH) @@ -62,10 +63,16 @@ async def async_setup_entry( class Switch(ZhaEntity, SwitchEntity): """ZHA switch.""" - def __init__(self, unique_id, zha_device, channels, **kwargs): + def __init__( + self, + unique_id: str, + zha_device: ZHADevice, + channels: list[ZigbeeChannel], + **kwargs: Any, + ) -> None: """Initialize the ZHA switch.""" super().__init__(unique_id, zha_device, channels, **kwargs) - self._on_off_channel = self.cluster_channels.get(CHANNEL_ON_OFF) + self._on_off_channel: OnOffChannel = self.cluster_channels.get(CHANNEL_ON_OFF) @property def is_on(self) -> bool: @@ -74,14 +81,14 @@ class Switch(ZhaEntity, SwitchEntity): return False return self._on_off_channel.on_off - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" result = await self._on_off_channel.turn_on() if not result: return self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" result = await self._on_off_channel.turn_off() if not result: @@ -112,7 +119,12 @@ class SwitchGroup(ZhaGroupEntity, SwitchEntity): """Representation of a switch group.""" def __init__( - self, entity_ids: list[str], unique_id: str, group_id: int, zha_device, **kwargs + self, + entity_ids: list[str], + unique_id: str, + group_id: int, + zha_device: ZHADevice, + **kwargs: Any, ) -> None: """Initialize a switch group.""" super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs) @@ -126,7 +138,7 @@ class SwitchGroup(ZhaGroupEntity, SwitchEntity): """Return if the switch is on based on the statemachine.""" return bool(self._state) - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" result = await self._on_off_channel.on() if isinstance(result, Exception) or result[1] is not Status.SUCCESS: @@ -134,7 +146,7 @@ class SwitchGroup(ZhaGroupEntity, SwitchEntity): self._state = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" result = await self._on_off_channel.off() if isinstance(result, Exception) or result[1] is not Status.SUCCESS: @@ -165,7 +177,7 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, + **kwargs: Any, ) -> ZhaEntity | None: """Entity Factory. @@ -190,7 +202,7 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): unique_id: str, zha_device: ZHADevice, channels: list[ZigbeeChannel], - **kwargs, + **kwargs: Any, ) -> None: """Init this number configuration entity.""" self._channel: ZigbeeChannel = channels[0] @@ -215,7 +227,7 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): invert = bool(self._channel.cluster.get(self._zcl_inverter_attribute)) return (not val) if invert else val - async def async_turn_on_off(self, state) -> None: + async def async_turn_on_off(self, state: bool) -> None: """Turn the entity on or off.""" try: invert = bool(self._channel.cluster.get(self._zcl_inverter_attribute)) @@ -230,11 +242,11 @@ class ZHASwitchConfigurationEntity(ZhaEntity, SwitchEntity): ): self.async_write_ha_state() - async def async_turn_on(self, **kwargs) -> None: + async def async_turn_on(self, **kwargs: Any) -> None: """Turn the entity on.""" await self.async_turn_on_off(True) - async def async_turn_off(self, **kwargs) -> None: + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the entity off.""" await self.async_turn_on_off(False) @@ -263,8 +275,8 @@ class OnOffWindowDetectionFunctionConfigurationEntity( ): """Representation of a ZHA window detection configuration entity.""" - _zcl_attribute = "window_detection_function" - _zcl_inverter_attribute = "window_detection_function_inverter" + _zcl_attribute: str = "window_detection_function" + _zcl_inverter_attribute: str = "window_detection_function_inverter" @CONFIG_DIAGNOSTIC_MATCH(channel_names="opple_cluster", models={"lumi.motion.ac02"}) @@ -273,4 +285,4 @@ class P1MotionTriggerIndicatorSwitch( ): """Representation of a ZHA motion triggering configuration entity.""" - _zcl_attribute = "trigger_indicator" + _zcl_attribute: str = "trigger_indicator"