ZHA entity ZCL reporting configuration (#19177)
* Implement async_configure() method for ZHA entities. Allow attribute reporting configuration to be stored as dict of zha entity. * Update ZHA platform to use new attribute reporting configuration. * Use const declaration instead of magic numbers. * Add support for manufacturer_id in ZCL attribute reporting configuration. * Refactor async_configure() method. Rename attribute reporting dict to zcl_reporting_config.
This commit is contained in:
parent
23a579421d
commit
4692605974
8 changed files with 214 additions and 42 deletions
|
@ -9,7 +9,7 @@ import logging
|
|||
from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice
|
||||
from homeassistant.components.zha import helpers
|
||||
from homeassistant.components.zha.const import (
|
||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW)
|
||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW)
|
||||
from homeassistant.components.zha.entities import ZhaEntity
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
|
@ -89,21 +89,7 @@ async def _async_setup_remote(discovery_info):
|
|||
remote = Remote(**discovery_info)
|
||||
|
||||
if discovery_info['new_join']:
|
||||
from zigpy.zcl.clusters.general import OnOff, LevelControl
|
||||
out_clusters = discovery_info['out_clusters']
|
||||
if OnOff.cluster_id in out_clusters:
|
||||
cluster = out_clusters[OnOff.cluster_id]
|
||||
await helpers.configure_reporting(
|
||||
remote.entity_id, cluster, 0, min_report=0, max_report=600,
|
||||
reportable_change=1
|
||||
)
|
||||
if LevelControl.cluster_id in out_clusters:
|
||||
cluster = out_clusters[LevelControl.cluster_id]
|
||||
await helpers.configure_reporting(
|
||||
remote.entity_id, cluster, 0, min_report=1, max_report=600,
|
||||
reportable_change=1
|
||||
)
|
||||
|
||||
await remote.async_configure()
|
||||
return remote
|
||||
|
||||
|
||||
|
@ -238,6 +224,14 @@ class Remote(ZhaEntity, BinarySensorDevice):
|
|||
general.OnOff.cluster_id: self.OnOffListener(self),
|
||||
general.LevelControl.cluster_id: self.LevelListener(self),
|
||||
}
|
||||
out_clusters = kwargs.get('out_clusters')
|
||||
self._zcl_reporting = {}
|
||||
for cluster_id in [general.OnOff.cluster_id,
|
||||
general.LevelControl.cluster_id]:
|
||||
if cluster_id not in out_clusters:
|
||||
continue
|
||||
cluster = out_clusters[cluster_id]
|
||||
self._zcl_reporting[cluster] = {0: REPORT_CONFIG_IMMEDIATE}
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
|
@ -257,6 +251,11 @@ class Remote(ZhaEntity, BinarySensorDevice):
|
|||
})
|
||||
return self._device_state_attributes
|
||||
|
||||
@property
|
||||
def zcl_reporting_config(self):
|
||||
"""Return ZCL attribute reporting configuration."""
|
||||
return self._zcl_reporting
|
||||
|
||||
def move_level(self, change):
|
||||
"""Increment the level, setting state if appropriate."""
|
||||
if not self._state and change > 0:
|
||||
|
|
|
@ -11,7 +11,7 @@ from homeassistant.components.fan import (
|
|||
FanEntity)
|
||||
from homeassistant.components.zha import helpers
|
||||
from homeassistant.components.zha.const import (
|
||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW)
|
||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_OP, ZHA_DISCOVERY_NEW)
|
||||
from homeassistant.components.zha.entities import ZhaEntity
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
|
@ -70,7 +70,10 @@ async def _async_setup_entities(hass, config_entry, async_add_entities,
|
|||
"""Set up the ZHA fans."""
|
||||
entities = []
|
||||
for discovery_info in discovery_infos:
|
||||
entities.append(ZhaFan(**discovery_info))
|
||||
fan = ZhaFan(**discovery_info)
|
||||
if discovery_info['new_join']:
|
||||
await fan.async_configure()
|
||||
entities.append(fan)
|
||||
|
||||
async_add_entities(entities, update_before_add=True)
|
||||
|
||||
|
@ -79,6 +82,19 @@ class ZhaFan(ZhaEntity, FanEntity):
|
|||
"""Representation of a ZHA fan."""
|
||||
|
||||
_domain = DOMAIN
|
||||
value_attribute = 0 # fan_mode
|
||||
|
||||
@property
|
||||
def zcl_reporting_config(self) -> dict:
|
||||
"""Return a dict of attribute reporting configuration."""
|
||||
return {
|
||||
self.cluster: {self.value_attribute: REPORT_CONFIG_OP}
|
||||
}
|
||||
|
||||
@property
|
||||
def cluster(self):
|
||||
"""Fan ZCL Cluster."""
|
||||
return self._endpoint.fan
|
||||
|
||||
@property
|
||||
def supported_features(self) -> int:
|
||||
|
@ -129,7 +145,7 @@ class ZhaFan(ZhaEntity, FanEntity):
|
|||
|
||||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
result = await helpers.safe_read(self._endpoint.fan, ['fan_mode'],
|
||||
result = await helpers.safe_read(self.cluster, ['fan_mode'],
|
||||
allow_cache=False,
|
||||
only_cache=(not self._initialized))
|
||||
new_value = result.get('fan_mode', None)
|
||||
|
@ -142,3 +158,12 @@ class ZhaFan(ZhaEntity, FanEntity):
|
|||
False if entity pushes its state to HA.
|
||||
"""
|
||||
return False
|
||||
|
||||
def attribute_updated(self, attribute, value):
|
||||
"""Handle attribute update from device."""
|
||||
attr_name = self.cluster.attributes.get(attribute, [attribute])[0]
|
||||
_LOGGER.debug("%s: Attribute report '%s'[%s] = %s",
|
||||
self.entity_id, self.cluster.name, attr_name, value)
|
||||
if attribute == self.value_attribute:
|
||||
self._state = VALUE_TO_SPEED.get(value, self._state)
|
||||
self.async_schedule_update_ha_state()
|
||||
|
|
|
@ -9,7 +9,8 @@ import logging
|
|||
from homeassistant.components import light
|
||||
from homeassistant.components.zha import helpers
|
||||
from homeassistant.components.zha.const import (
|
||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW)
|
||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT,
|
||||
REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW)
|
||||
from homeassistant.components.zha.entities import ZhaEntity
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
import homeassistant.util.color as color_util
|
||||
|
@ -73,7 +74,10 @@ async def _async_setup_entities(hass, config_entry, async_add_entities,
|
|||
UNSUPPORTED_ATTRIBUTE):
|
||||
discovery_info['color_capabilities'] |= \
|
||||
CAPABILITIES_COLOR_TEMP
|
||||
entities.append(Light(**discovery_info))
|
||||
zha_light = Light(**discovery_info)
|
||||
if discovery_info['new_join']:
|
||||
await zha_light.async_configure()
|
||||
entities.append(zha_light)
|
||||
|
||||
async_add_entities(entities, update_before_add=True)
|
||||
|
||||
|
@ -105,6 +109,19 @@ class Light(ZhaEntity, light.Light):
|
|||
self._supported_features |= light.SUPPORT_COLOR
|
||||
self._hs_color = (0, 0)
|
||||
|
||||
@property
|
||||
def zcl_reporting_config(self) -> dict:
|
||||
"""Return attribute reporting configuration."""
|
||||
return {
|
||||
'on_off': {'on_off': REPORT_CONFIG_IMMEDIATE},
|
||||
'level': {'current_level': REPORT_CONFIG_ASAP},
|
||||
'light_color': {
|
||||
'current_x': REPORT_CONFIG_DEFAULT,
|
||||
'current_y': REPORT_CONFIG_DEFAULT,
|
||||
'color_temperature': REPORT_CONFIG_DEFAULT,
|
||||
}
|
||||
}
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return true if entity is on."""
|
||||
|
|
|
@ -9,7 +9,8 @@ import logging
|
|||
from homeassistant.components.sensor import DOMAIN
|
||||
from homeassistant.components.zha import helpers
|
||||
from homeassistant.components.zha.const import (
|
||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW)
|
||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_MAX_INT,
|
||||
REPORT_CONFIG_MIN_INT, REPORT_CONFIG_RPT_CHANGE, ZHA_DISCOVERY_NEW)
|
||||
from homeassistant.components.zha.entities import ZhaEntity
|
||||
from homeassistant.const import TEMP_CELSIUS
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
@ -81,11 +82,7 @@ async def make_sensor(discovery_info):
|
|||
sensor = Sensor(**discovery_info)
|
||||
|
||||
if discovery_info['new_join']:
|
||||
cluster = list(in_clusters.values())[0]
|
||||
await helpers.configure_reporting(
|
||||
sensor.entity_id, cluster, sensor.value_attribute,
|
||||
reportable_change=sensor.min_reportable_change
|
||||
)
|
||||
await sensor.async_configure()
|
||||
|
||||
return sensor
|
||||
|
||||
|
@ -95,7 +92,28 @@ class Sensor(ZhaEntity):
|
|||
|
||||
_domain = DOMAIN
|
||||
value_attribute = 0
|
||||
min_reportable_change = 1
|
||||
min_report_interval = REPORT_CONFIG_MIN_INT
|
||||
max_report_interval = REPORT_CONFIG_MAX_INT
|
||||
min_reportable_change = REPORT_CONFIG_RPT_CHANGE
|
||||
report_config = (min_report_interval, max_report_interval,
|
||||
min_reportable_change)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""Init ZHA Sensor instance."""
|
||||
super().__init__(**kwargs)
|
||||
self._cluster = list(kwargs['in_clusters'].values())[0]
|
||||
|
||||
@property
|
||||
def zcl_reporting_config(self) -> dict:
|
||||
"""Return a dict of attribute reporting configuration."""
|
||||
return {
|
||||
self.cluster: {self.value_attribute: self.report_config}
|
||||
}
|
||||
|
||||
@property
|
||||
def cluster(self):
|
||||
"""Return Sensor's cluster."""
|
||||
return self._cluster
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
|
@ -119,7 +137,7 @@ class Sensor(ZhaEntity):
|
|||
async def async_update(self):
|
||||
"""Retrieve latest state."""
|
||||
result = await helpers.safe_read(
|
||||
list(self._in_clusters.values())[0],
|
||||
self.cluster,
|
||||
[self.value_attribute],
|
||||
allow_cache=False,
|
||||
only_cache=(not self._initialized)
|
||||
|
@ -251,6 +269,6 @@ class ElectricalMeasurementSensor(Sensor):
|
|||
_LOGGER.debug("%s async_update", self.entity_id)
|
||||
|
||||
result = await helpers.safe_read(
|
||||
self._endpoint.electrical_measurement, ['active_power'],
|
||||
self.cluster, ['active_power'],
|
||||
allow_cache=False, only_cache=(not self._initialized))
|
||||
self._state = result.get('active_power', self._state)
|
||||
|
|
|
@ -9,7 +9,7 @@ import logging
|
|||
from homeassistant.components.switch import DOMAIN, SwitchDevice
|
||||
from homeassistant.components.zha import helpers
|
||||
from homeassistant.components.zha.const import (
|
||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW)
|
||||
DATA_ZHA, DATA_ZHA_DISPATCHERS, REPORT_CONFIG_IMMEDIATE, ZHA_DISCOVERY_NEW)
|
||||
from homeassistant.components.zha.entities import ZhaEntity
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
|
@ -44,17 +44,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
|
|||
async def _async_setup_entities(hass, config_entry, async_add_entities,
|
||||
discovery_infos):
|
||||
"""Set up the ZHA switches."""
|
||||
from zigpy.zcl.clusters.general import OnOff
|
||||
entities = []
|
||||
for discovery_info in discovery_infos:
|
||||
switch = Switch(**discovery_info)
|
||||
if discovery_info['new_join']:
|
||||
in_clusters = discovery_info['in_clusters']
|
||||
cluster = in_clusters[OnOff.cluster_id]
|
||||
await helpers.configure_reporting(
|
||||
switch.entity_id, cluster, switch.value_attribute,
|
||||
min_report=0, max_report=600, reportable_change=1
|
||||
)
|
||||
await switch.async_configure()
|
||||
entities.append(switch)
|
||||
|
||||
async_add_entities(entities, update_before_add=True)
|
||||
|
@ -76,6 +70,18 @@ class Switch(ZhaEntity, SwitchDevice):
|
|||
self._state = value
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@property
|
||||
def zcl_reporting_config(self) -> dict:
|
||||
"""Retrun a dict of attribute reporting configuration."""
|
||||
return {
|
||||
self.cluster: {'on_off': REPORT_CONFIG_IMMEDIATE}
|
||||
}
|
||||
|
||||
@property
|
||||
def cluster(self):
|
||||
"""Entity's cluster."""
|
||||
return self._endpoint.on_off
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Let zha handle polling."""
|
||||
|
|
|
@ -57,6 +57,27 @@ CUSTOM_CLUSTER_MAPPINGS = {}
|
|||
COMPONENT_CLUSTERS = {}
|
||||
EVENTABLE_CLUSTERS = []
|
||||
|
||||
REPORT_CONFIG_MAX_INT = 900
|
||||
REPORT_CONFIG_MAX_INT_BATTERY_SAVE = 10800
|
||||
REPORT_CONFIG_MIN_INT = 30
|
||||
REPORT_CONFIG_MIN_INT_ASAP = 1
|
||||
REPORT_CONFIG_MIN_INT_IMMEDIATE = 0
|
||||
REPORT_CONFIG_MIN_INT_OP = 5
|
||||
REPORT_CONFIG_MIN_INT_BATTERY_SAVE = 3600
|
||||
REPORT_CONFIG_RPT_CHANGE = 1
|
||||
REPORT_CONFIG_DEFAULT = (REPORT_CONFIG_MIN_INT, REPORT_CONFIG_MAX_INT,
|
||||
REPORT_CONFIG_RPT_CHANGE)
|
||||
REPORT_CONFIG_ASAP = (REPORT_CONFIG_MIN_INT_ASAP, REPORT_CONFIG_MAX_INT,
|
||||
REPORT_CONFIG_RPT_CHANGE)
|
||||
REPORT_CONFIG_BATTERY_SAVE = (REPORT_CONFIG_MIN_INT_BATTERY_SAVE,
|
||||
REPORT_CONFIG_MAX_INT,
|
||||
REPORT_CONFIG_RPT_CHANGE)
|
||||
REPORT_CONFIG_IMMEDIATE = (REPORT_CONFIG_MIN_INT_IMMEDIATE,
|
||||
REPORT_CONFIG_MAX_INT,
|
||||
REPORT_CONFIG_RPT_CHANGE)
|
||||
REPORT_CONFIG_OP = (REPORT_CONFIG_MIN_INT_OP, REPORT_CONFIG_MAX_INT,
|
||||
REPORT_CONFIG_RPT_CHANGE)
|
||||
|
||||
|
||||
def populate_data():
|
||||
"""Populate data using constants from bellows.
|
||||
|
|
|
@ -4,13 +4,20 @@ Entity for Zigbee Home Automation.
|
|||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/zha/
|
||||
"""
|
||||
from asyncio import sleep
|
||||
import logging
|
||||
from random import uniform
|
||||
|
||||
from homeassistant.components.zha.const import (
|
||||
DATA_ZHA, DATA_ZHA_BRIDGE_ID, DOMAIN)
|
||||
from homeassistant.components.zha.helpers import configure_reporting
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import entity
|
||||
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
|
||||
from homeassistant.util import slugify
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ZhaEntity(entity.Entity):
|
||||
"""A base class for ZHA entities."""
|
||||
|
@ -57,6 +64,7 @@ class ZhaEntity(entity.Entity):
|
|||
self._out_listeners = {}
|
||||
|
||||
self._initialized = False
|
||||
self.manufacturer_code = None
|
||||
application_listener.register_entity(ieee, self)
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
|
@ -71,6 +79,79 @@ class ZhaEntity(entity.Entity):
|
|||
|
||||
self._initialized = True
|
||||
|
||||
async def async_configure(self):
|
||||
"""Set cluster binding and attribute reporting."""
|
||||
for cluster_key, attrs in self.zcl_reporting_config.items():
|
||||
cluster = self._get_cluster_from_report_config(cluster_key)
|
||||
if cluster is None:
|
||||
continue
|
||||
|
||||
manufacturer = None
|
||||
if cluster.cluster_id >= 0xfc00 and self.manufacturer_code:
|
||||
manufacturer = self.manufacturer_code
|
||||
|
||||
skip_bind = False # bind cluster only for the 1st configured attr
|
||||
for attr, details in attrs.items():
|
||||
min_report_interval, max_report_interval, change = details
|
||||
await configure_reporting(
|
||||
self.entity_id, cluster, attr,
|
||||
min_report=min_report_interval,
|
||||
max_report=max_report_interval,
|
||||
reportable_change=change,
|
||||
skip_bind=skip_bind,
|
||||
manufacturer=manufacturer
|
||||
)
|
||||
skip_bind = True
|
||||
await sleep(uniform(0.1, 0.5))
|
||||
_LOGGER.debug("%s: finished configuration", self.entity_id)
|
||||
|
||||
def _get_cluster_from_report_config(self, cluster_key):
|
||||
"""Parse an entry from zcl_reporting_config dict."""
|
||||
from zigpy.zcl import Cluster as Zcl_Cluster
|
||||
|
||||
cluster = None
|
||||
if isinstance(cluster_key, Zcl_Cluster):
|
||||
cluster = cluster_key
|
||||
elif isinstance(cluster_key, str):
|
||||
cluster = getattr(self._endpoint, cluster_key, None)
|
||||
elif isinstance(cluster_key, int):
|
||||
if cluster_key in self._in_clusters:
|
||||
cluster = self._in_clusters[cluster_key]
|
||||
elif cluster_key in self._out_clusters:
|
||||
cluster = self._out_clusters[cluster_key]
|
||||
elif issubclass(cluster_key, Zcl_Cluster):
|
||||
cluster_id = cluster_key.cluster_id
|
||||
if cluster_id in self._in_clusters:
|
||||
cluster = self._in_clusters[cluster_id]
|
||||
elif cluster_id in self._out_clusters:
|
||||
cluster = self._out_clusters[cluster_id]
|
||||
return cluster
|
||||
|
||||
@property
|
||||
def zcl_reporting_config(self):
|
||||
"""Return a dict of ZCL attribute reporting configuration.
|
||||
|
||||
{
|
||||
Cluster_Class: {
|
||||
attr_id: (min_report_interval, max_report_interval, change),
|
||||
attr_name: (min_rep_interval, max_rep_interval, change)
|
||||
}
|
||||
Cluster_Instance: {
|
||||
attr_id: (min_report_interval, max_report_interval, change),
|
||||
attr_name: (min_rep_interval, max_rep_interval, change)
|
||||
}
|
||||
cluster_id: {
|
||||
attr_id: (min_report_interval, max_report_interval, change),
|
||||
attr_name: (min_rep_interval, max_rep_interval, change)
|
||||
}
|
||||
'cluster_name': {
|
||||
attr_id: (min_report_interval, max_report_interval, change),
|
||||
attr_name: (min_rep_interval, max_rep_interval, change)
|
||||
}
|
||||
}
|
||||
"""
|
||||
return dict()
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique ID."""
|
||||
|
|
|
@ -7,7 +7,9 @@ https://home-assistant.io/components/zha/
|
|||
import asyncio
|
||||
import logging
|
||||
|
||||
from .const import DEFAULT_BAUDRATE, RadioType
|
||||
from .const import (
|
||||
DEFAULT_BAUDRATE, REPORT_CONFIG_MAX_INT, REPORT_CONFIG_MIN_INT,
|
||||
REPORT_CONFIG_RPT_CHANGE, RadioType)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -31,8 +33,10 @@ async def safe_read(cluster, attributes, allow_cache=True, only_cache=False):
|
|||
|
||||
|
||||
async def configure_reporting(entity_id, cluster, attr, skip_bind=False,
|
||||
min_report=300, max_report=900,
|
||||
reportable_change=1):
|
||||
min_report=REPORT_CONFIG_MIN_INT,
|
||||
max_report=REPORT_CONFIG_MAX_INT,
|
||||
reportable_change=REPORT_CONFIG_RPT_CHANGE,
|
||||
manufacturer=None):
|
||||
"""Configure attribute reporting for a cluster.
|
||||
|
||||
while swallowing the DeliverError exceptions in case of unreachable
|
||||
|
@ -56,7 +60,8 @@ async def configure_reporting(entity_id, cluster, attr, skip_bind=False,
|
|||
|
||||
try:
|
||||
res = await cluster.configure_reporting(attr, min_report,
|
||||
max_report, reportable_change)
|
||||
max_report, reportable_change,
|
||||
manufacturer=manufacturer)
|
||||
_LOGGER.debug(
|
||||
"%s: reporting '%s' attr on '%s' cluster: %d/%d/%d: Result: '%s'",
|
||||
entity_id, attr_name, cluster_name, min_report, max_report,
|
||||
|
|
Loading…
Add table
Reference in a new issue