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:
Alexei Chetroi 2018-12-19 08:52:20 -05:00 committed by Paulus Schoutsen
parent 23a579421d
commit 4692605974
8 changed files with 214 additions and 42 deletions

View file

@ -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:

View file

@ -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()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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