Refactor ZHA (#18629)

* refactor ZHA

* lint

* review request

* Exclude more zha modules from coverage
This commit is contained in:
David F. Mulcahey 2018-11-22 13:00:46 -05:00 committed by Martin Hjelmare
parent cccc41c23e
commit 67aa76d295
12 changed files with 343 additions and 284 deletions

View File

@ -400,6 +400,8 @@ omit =
homeassistant/components/zha/__init__.py
homeassistant/components/zha/const.py
homeassistant/components/zha/entities/*
homeassistant/components/zha/helpers.py
homeassistant/components/*/zha.py
homeassistant/components/zigbee.py

View File

@ -7,7 +7,8 @@ at https://home-assistant.io/components/binary_sensor.zha/
import logging
from homeassistant.components.binary_sensor import DOMAIN, BinarySensorDevice
from homeassistant.components import zha
from homeassistant.components.zha.entities import ZhaEntity
from homeassistant.components.zha import helpers
_LOGGER = logging.getLogger(__name__)
@ -27,7 +28,7 @@ CLASS_MAPPING = {
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Zigbee Home Automation binary sensors."""
discovery_info = zha.get_discovery_info(hass, discovery_info)
discovery_info = helpers.get_discovery_info(hass, discovery_info)
if discovery_info is None:
return
@ -72,13 +73,13 @@ async def _async_setup_remote(hass, config, async_add_entities,
out_clusters = discovery_info['out_clusters']
if OnOff.cluster_id in out_clusters:
cluster = out_clusters[OnOff.cluster_id]
await zha.configure_reporting(
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 zha.configure_reporting(
await helpers.configure_reporting(
remote.entity_id, cluster, 0, min_report=1, max_report=600,
reportable_change=1
)
@ -86,7 +87,7 @@ async def _async_setup_remote(hass, config, async_add_entities,
async_add_entities([remote], update_before_add=True)
class BinarySensor(zha.Entity, BinarySensorDevice):
class BinarySensor(ZhaEntity, BinarySensorDevice):
"""The ZHA Binary Sensor."""
_domain = DOMAIN
@ -130,16 +131,16 @@ class BinarySensor(zha.Entity, BinarySensorDevice):
"""Retrieve latest state."""
from zigpy.types.basic import uint16_t
result = await zha.safe_read(self._endpoint.ias_zone,
['zone_status'],
allow_cache=False,
only_cache=(not self._initialized))
result = await helpers.safe_read(self._endpoint.ias_zone,
['zone_status'],
allow_cache=False,
only_cache=(not self._initialized))
state = result.get('zone_status', self._state)
if isinstance(state, (int, uint16_t)):
self._state = result.get('zone_status', self._state) & 3
class Remote(zha.Entity, BinarySensorDevice):
class Remote(ZhaEntity, BinarySensorDevice):
"""ZHA switch/remote controller/button."""
_domain = DOMAIN
@ -252,7 +253,7 @@ class Remote(zha.Entity, BinarySensorDevice):
async def async_update(self):
"""Retrieve latest state."""
from zigpy.zcl.clusters.general import OnOff
result = await zha.safe_read(
result = await helpers.safe_read(
self._endpoint.out_clusters[OnOff.cluster_id],
['on_off'],
allow_cache=False,

View File

@ -5,7 +5,8 @@ For more details on this platform, please refer to the documentation
at https://home-assistant.io/components/fan.zha/
"""
import logging
from homeassistant.components import zha
from homeassistant.components.zha.entities import ZhaEntity
from homeassistant.components.zha import helpers
from homeassistant.components.fan import (
DOMAIN, FanEntity, SPEED_OFF, SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH,
SUPPORT_SET_SPEED)
@ -40,14 +41,14 @@ SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)}
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Zigbee Home Automation fans."""
discovery_info = zha.get_discovery_info(hass, discovery_info)
discovery_info = helpers.get_discovery_info(hass, discovery_info)
if discovery_info is None:
return
async_add_entities([ZhaFan(**discovery_info)], update_before_add=True)
class ZhaFan(zha.Entity, FanEntity):
class ZhaFan(ZhaEntity, FanEntity):
"""Representation of a ZHA fan."""
_domain = DOMAIN
@ -101,9 +102,9 @@ class ZhaFan(zha.Entity, FanEntity):
async def async_update(self):
"""Retrieve latest state."""
result = await zha.safe_read(self._endpoint.fan, ['fan_mode'],
allow_cache=False,
only_cache=(not self._initialized))
result = await helpers.safe_read(self._endpoint.fan, ['fan_mode'],
allow_cache=False,
only_cache=(not self._initialized))
new_value = result.get('fan_mode', None)
self._state = VALUE_TO_SPEED.get(new_value, None)

View File

@ -5,7 +5,9 @@ For more details on this platform, please refer to the documentation
at https://home-assistant.io/components/light.zha/
"""
import logging
from homeassistant.components import light, zha
from homeassistant.components import light
from homeassistant.components.zha.entities import ZhaEntity
from homeassistant.components.zha import helpers
import homeassistant.util.color as color_util
_LOGGER = logging.getLogger(__name__)
@ -23,13 +25,13 @@ UNSUPPORTED_ATTRIBUTE = 0x86
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up the Zigbee Home Automation lights."""
discovery_info = zha.get_discovery_info(hass, discovery_info)
discovery_info = helpers.get_discovery_info(hass, discovery_info)
if discovery_info is None:
return
endpoint = discovery_info['endpoint']
if hasattr(endpoint, 'light_color'):
caps = await zha.safe_read(
caps = await helpers.safe_read(
endpoint.light_color, ['color_capabilities'])
discovery_info['color_capabilities'] = caps.get('color_capabilities')
if discovery_info['color_capabilities'] is None:
@ -37,7 +39,7 @@ async def async_setup_platform(hass, config, async_add_entities,
# attribute. In this version XY support is mandatory, but we need
# to probe to determine if the device supports color temperature.
discovery_info['color_capabilities'] = CAPABILITIES_COLOR_XY
result = await zha.safe_read(
result = await helpers.safe_read(
endpoint.light_color, ['color_temperature'])
if result.get('color_temperature') is not UNSUPPORTED_ATTRIBUTE:
discovery_info['color_capabilities'] |= CAPABILITIES_COLOR_TEMP
@ -45,7 +47,7 @@ async def async_setup_platform(hass, config, async_add_entities,
async_add_entities([Light(**discovery_info)], update_before_add=True)
class Light(zha.Entity, light.Light):
class Light(ZhaEntity, light.Light):
"""Representation of a ZHA or ZLL light."""
_domain = light.DOMAIN
@ -181,31 +183,37 @@ class Light(zha.Entity, light.Light):
async def async_update(self):
"""Retrieve latest state."""
result = await zha.safe_read(self._endpoint.on_off, ['on_off'],
allow_cache=False,
only_cache=(not self._initialized))
result = await helpers.safe_read(self._endpoint.on_off, ['on_off'],
allow_cache=False,
only_cache=(not self._initialized))
self._state = result.get('on_off', self._state)
if self._supported_features & light.SUPPORT_BRIGHTNESS:
result = await zha.safe_read(self._endpoint.level,
['current_level'],
allow_cache=False,
only_cache=(not self._initialized))
result = await helpers.safe_read(self._endpoint.level,
['current_level'],
allow_cache=False,
only_cache=(
not self._initialized
))
self._brightness = result.get('current_level', self._brightness)
if self._supported_features & light.SUPPORT_COLOR_TEMP:
result = await zha.safe_read(self._endpoint.light_color,
['color_temperature'],
allow_cache=False,
only_cache=(not self._initialized))
result = await helpers.safe_read(self._endpoint.light_color,
['color_temperature'],
allow_cache=False,
only_cache=(
not self._initialized
))
self._color_temp = result.get('color_temperature',
self._color_temp)
if self._supported_features & light.SUPPORT_COLOR:
result = await zha.safe_read(self._endpoint.light_color,
['current_x', 'current_y'],
allow_cache=False,
only_cache=(not self._initialized))
result = await helpers.safe_read(self._endpoint.light_color,
['current_x', 'current_y'],
allow_cache=False,
only_cache=(
not self._initialized
))
if 'current_x' in result and 'current_y' in result:
xy_color = (round(result['current_x']/65535, 3),
round(result['current_y']/65535, 3))

View File

@ -7,7 +7,8 @@ at https://home-assistant.io/components/sensor.zha/
import logging
from homeassistant.components.sensor import DOMAIN
from homeassistant.components import zha
from homeassistant.components.zha.entities import ZhaEntity
from homeassistant.components.zha import helpers
from homeassistant.const import TEMP_CELSIUS
from homeassistant.util.temperature import convert as convert_temperature
@ -19,7 +20,7 @@ DEPENDENCIES = ['zha']
async def async_setup_platform(hass, config, async_add_entities,
discovery_info=None):
"""Set up Zigbee Home Automation sensors."""
discovery_info = zha.get_discovery_info(hass, discovery_info)
discovery_info = helpers.get_discovery_info(hass, discovery_info)
if discovery_info is None:
return
@ -56,7 +57,7 @@ async def make_sensor(discovery_info):
if discovery_info['new_join']:
cluster = list(in_clusters.values())[0]
await zha.configure_reporting(
await helpers.configure_reporting(
sensor.entity_id, cluster, sensor.value_attribute,
reportable_change=sensor.min_reportable_change
)
@ -64,7 +65,7 @@ async def make_sensor(discovery_info):
return sensor
class Sensor(zha.Entity):
class Sensor(ZhaEntity):
"""Base ZHA sensor."""
_domain = DOMAIN
@ -92,7 +93,7 @@ class Sensor(zha.Entity):
async def async_update(self):
"""Retrieve latest state."""
result = await zha.safe_read(
result = await helpers.safe_read(
list(self._in_clusters.values())[0],
[self.value_attribute],
allow_cache=False,
@ -224,7 +225,7 @@ class ElectricalMeasurementSensor(Sensor):
"""Retrieve latest state."""
_LOGGER.debug("%s async_update", self.entity_id)
result = await zha.safe_read(
result = await helpers.safe_read(
self._endpoint.electrical_measurement, ['active_power'],
allow_cache=False, only_cache=(not self._initialized))
self._state = result.get('active_power', self._state)

View File

@ -7,7 +7,8 @@ at https://home-assistant.io/components/switch.zha/
import logging
from homeassistant.components.switch import DOMAIN, SwitchDevice
from homeassistant.components import zha
from homeassistant.components.zha.entities import ZhaEntity
from homeassistant.components.zha import helpers
_LOGGER = logging.getLogger(__name__)
@ -19,7 +20,7 @@ async def async_setup_platform(hass, config, async_add_entities,
"""Set up the Zigbee Home Automation switches."""
from zigpy.zcl.clusters.general import OnOff
discovery_info = zha.get_discovery_info(hass, discovery_info)
discovery_info = helpers.get_discovery_info(hass, discovery_info)
if discovery_info is None:
return
@ -28,7 +29,7 @@ async def async_setup_platform(hass, config, async_add_entities,
if discovery_info['new_join']:
in_clusters = discovery_info['in_clusters']
cluster = in_clusters[OnOff.cluster_id]
await zha.configure_reporting(
await helpers.configure_reporting(
switch.entity_id, cluster, switch.value_attribute,
min_report=0, max_report=600, reportable_change=1
)
@ -36,7 +37,7 @@ async def async_setup_platform(hass, config, async_add_entities,
async_add_entities([switch], update_before_add=True)
class Switch(zha.Entity, SwitchDevice):
class Switch(ZhaEntity, SwitchDevice):
"""ZHA switch."""
_domain = DOMAIN
@ -94,8 +95,8 @@ class Switch(zha.Entity, SwitchDevice):
async def async_update(self):
"""Retrieve latest state."""
result = await zha.safe_read(self._endpoint.on_off,
['on_off'],
allow_cache=False,
only_cache=(not self._initialized))
result = await helpers.safe_read(self._endpoint.on_off,
['on_off'],
allow_cache=False,
only_cache=(not self._initialized))
self._state = result.get('on_off', self._state)

View File

@ -7,15 +7,15 @@ https://home-assistant.io/components/zha/
import collections
import enum
import logging
import time
import voluptuous as vol
import homeassistant.helpers.config_validation as cv
from homeassistant import const as ha_const
from homeassistant.helpers import discovery, entity
from homeassistant.util import slugify
from homeassistant.helpers import discovery
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.components.zha.entities import ZhaDeviceEntity
from . import const as zha_const
REQUIREMENTS = [
'bellows==0.7.0',
@ -145,6 +145,7 @@ class ApplicationListener:
self._component = EntityComponent(_LOGGER, DOMAIN, hass)
self._device_registry = collections.defaultdict(list)
hass.data[DISCOVERY_KEY] = hass.data.get(DISCOVERY_KEY, {})
zha_const.populate_data()
def device_joined(self, device):
"""Handle device joined.
@ -177,8 +178,6 @@ class ApplicationListener:
async def async_device_initialized(self, device, join):
"""Handle device joined and basic information discovered (async)."""
import zigpy.profiles
import homeassistant.components.zha.const as zha_const
zha_const.populate_data()
device_manufacturer = device_model = None
@ -276,7 +275,6 @@ class ApplicationListener:
device_classes, discovery_attr,
is_new_join):
"""Try to set up an entity from a "bare" cluster."""
import homeassistant.components.zha.const as zha_const
if cluster.cluster_id in profile_clusters:
return
@ -320,226 +318,3 @@ class ApplicationListener:
{'discovery_key': cluster_key},
self._config,
)
class Entity(entity.Entity):
"""A base class for ZHA entities."""
_domain = None # Must be overridden by subclasses
def __init__(self, endpoint, in_clusters, out_clusters, manufacturer,
model, application_listener, unique_id, **kwargs):
"""Init ZHA entity."""
self._device_state_attributes = {}
ieee = endpoint.device.ieee
ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
if manufacturer and model is not None:
self.entity_id = "{}.{}_{}_{}_{}{}".format(
self._domain,
slugify(manufacturer),
slugify(model),
ieeetail,
endpoint.endpoint_id,
kwargs.get('entity_suffix', ''),
)
self._device_state_attributes['friendly_name'] = "{} {}".format(
manufacturer,
model,
)
else:
self.entity_id = "{}.zha_{}_{}{}".format(
self._domain,
ieeetail,
endpoint.endpoint_id,
kwargs.get('entity_suffix', ''),
)
self._endpoint = endpoint
self._in_clusters = in_clusters
self._out_clusters = out_clusters
self._state = None
self._unique_id = unique_id
# Normally the entity itself is the listener. Sub-classes may set this
# to a dict of cluster ID -> listener to receive messages for specific
# clusters separately
self._in_listeners = {}
self._out_listeners = {}
self._initialized = False
application_listener.register_entity(ieee, self)
async def async_added_to_hass(self):
"""Handle entity addition to hass.
It is now safe to update the entity state
"""
for cluster_id, cluster in self._in_clusters.items():
cluster.add_listener(self._in_listeners.get(cluster_id, self))
for cluster_id, cluster in self._out_clusters.items():
cluster.add_listener(self._out_listeners.get(cluster_id, self))
self._initialized = True
@property
def unique_id(self) -> str:
"""Return a unique ID."""
return self._unique_id
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
return self._device_state_attributes
def attribute_updated(self, attribute, value):
"""Handle an attribute updated on this cluster."""
pass
def zdo_command(self, tsn, command_id, args):
"""Handle a ZDO command received on this cluster."""
pass
class ZhaDeviceEntity(entity.Entity):
"""A base class for ZHA devices."""
def __init__(self, device, manufacturer, model, application_listener,
keepalive_interval=7200, **kwargs):
"""Init ZHA endpoint entity."""
self._device_state_attributes = {
'nwk': '0x{0:04x}'.format(device.nwk),
'ieee': str(device.ieee),
'lqi': device.lqi,
'rssi': device.rssi,
}
ieee = device.ieee
ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
if manufacturer is not None and model is not None:
self._unique_id = "{}_{}_{}".format(
slugify(manufacturer),
slugify(model),
ieeetail,
)
self._device_state_attributes['friendly_name'] = "{} {}".format(
manufacturer,
model,
)
else:
self._unique_id = str(ieeetail)
self._device = device
self._state = 'offline'
self._keepalive_interval = keepalive_interval
application_listener.register_entity(ieee, self)
@property
def unique_id(self) -> str:
"""Return a unique ID."""
return self._unique_id
@property
def state(self) -> str:
"""Return the state of the entity."""
return self._state
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
update_time = None
if self._device.last_seen is not None and self._state == 'offline':
time_struct = time.localtime(self._device.last_seen)
update_time = time.strftime("%Y-%m-%dT%H:%M:%S", time_struct)
self._device_state_attributes['last_seen'] = update_time
if ('last_seen' in self._device_state_attributes and
self._state != 'offline'):
del self._device_state_attributes['last_seen']
self._device_state_attributes['lqi'] = self._device.lqi
self._device_state_attributes['rssi'] = self._device.rssi
return self._device_state_attributes
async def async_update(self):
"""Handle polling."""
if self._device.last_seen is None:
self._state = 'offline'
else:
difference = time.time() - self._device.last_seen
if difference > self._keepalive_interval:
self._state = 'offline'
else:
self._state = 'online'
def get_discovery_info(hass, discovery_info):
"""Get the full discovery info for a device.
Some of the info that needs to be passed to platforms is not JSON
serializable, so it cannot be put in the discovery_info dictionary. This
component places that info we need to pass to the platform in hass.data,
and this function is a helper for platforms to retrieve the complete
discovery info.
"""
if discovery_info is None:
return
discovery_key = discovery_info.get('discovery_key', None)
all_discovery_info = hass.data.get(DISCOVERY_KEY, {})
return all_discovery_info.get(discovery_key, None)
async def safe_read(cluster, attributes, allow_cache=True, only_cache=False):
"""Swallow all exceptions from network read.
If we throw during initialization, setup fails. Rather have an entity that
exists, but is in a maybe wrong state, than no entity. This method should
probably only be used during initialization.
"""
try:
result, _ = await cluster.read_attributes(
attributes,
allow_cache=allow_cache,
only_cache=only_cache
)
return result
except Exception: # pylint: disable=broad-except
return {}
async def configure_reporting(entity_id, cluster, attr, skip_bind=False,
min_report=300, max_report=900,
reportable_change=1):
"""Configure attribute reporting for a cluster.
while swallowing the DeliverError exceptions in case of unreachable
devices.
"""
from zigpy.exceptions import DeliveryError
attr_name = cluster.attributes.get(attr, [attr])[0]
cluster_name = cluster.ep_attribute
if not skip_bind:
try:
res = await cluster.bind()
_LOGGER.debug(
"%s: bound '%s' cluster: %s", entity_id, cluster_name, res[0]
)
except DeliveryError as ex:
_LOGGER.debug(
"%s: Failed to bind '%s' cluster: %s",
entity_id, cluster_name, str(ex)
)
try:
res = await cluster.configure_reporting(attr, min_report,
max_report, reportable_change)
_LOGGER.debug(
"%s: reporting '%s' attr on '%s' cluster: %d/%d/%d: Result: '%s'",
entity_id, attr_name, cluster_name, min_report, max_report,
reportable_change, res
)
except DeliveryError as ex:
_LOGGER.debug(
"%s: failed to set reporting for '%s' attr on '%s' cluster: %s",
entity_id, attr_name, cluster_name, str(ex)
)

View File

@ -1,5 +1,6 @@
"""All constants related to the ZHA component."""
DISCOVERY_KEY = 'zha_discovery_info'
DEVICE_CLASS = {}
SINGLE_INPUT_CLUSTER_DEVICE_CLASS = {}
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS = {}
@ -17,7 +18,12 @@ def populate_data():
from zigpy.profiles import PROFILES, zha, zll
from homeassistant.components.sensor import zha as sensor_zha
DEVICE_CLASS[zha.PROFILE_ID] = {
if zha.PROFILE_ID not in DEVICE_CLASS:
DEVICE_CLASS[zha.PROFILE_ID] = {}
if zll.PROFILE_ID not in DEVICE_CLASS:
DEVICE_CLASS[zll.PROFILE_ID] = {}
DEVICE_CLASS[zha.PROFILE_ID].update({
zha.DeviceType.ON_OFF_SWITCH: 'binary_sensor',
zha.DeviceType.LEVEL_CONTROL_SWITCH: 'binary_sensor',
zha.DeviceType.REMOTE_CONTROL: 'binary_sensor',
@ -29,8 +35,8 @@ def populate_data():
zha.DeviceType.ON_OFF_LIGHT_SWITCH: 'binary_sensor',
zha.DeviceType.DIMMER_SWITCH: 'binary_sensor',
zha.DeviceType.COLOR_DIMMER_SWITCH: 'binary_sensor',
}
DEVICE_CLASS[zll.PROFILE_ID] = {
})
DEVICE_CLASS[zll.PROFILE_ID].update({
zll.DeviceType.ON_OFF_LIGHT: 'light',
zll.DeviceType.ON_OFF_PLUGIN_UNIT: 'switch',
zll.DeviceType.DIMMABLE_LIGHT: 'light',
@ -43,7 +49,7 @@ def populate_data():
zll.DeviceType.CONTROLLER: 'binary_sensor',
zll.DeviceType.SCENE_CONTROLLER: 'binary_sensor',
zll.DeviceType.ON_OFF_SENSOR: 'binary_sensor',
}
})
SINGLE_INPUT_CLUSTER_DEVICE_CLASS.update({
zcl.clusters.general.OnOff: 'switch',

View File

@ -0,0 +1,10 @@
"""
Entities for Zigbee Home Automation.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/
"""
# flake8: noqa
from .entity import ZhaEntity
from .device_entity import ZhaDeviceEntity

View File

@ -0,0 +1,81 @@
"""
Device entity for Zigbee Home Automation.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/
"""
import time
from homeassistant.helpers import entity
from homeassistant.util import slugify
class ZhaDeviceEntity(entity.Entity):
"""A base class for ZHA devices."""
def __init__(self, device, manufacturer, model, application_listener,
keepalive_interval=7200, **kwargs):
"""Init ZHA endpoint entity."""
self._device_state_attributes = {
'nwk': '0x{0:04x}'.format(device.nwk),
'ieee': str(device.ieee),
'lqi': device.lqi,
'rssi': device.rssi,
}
ieee = device.ieee
ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
if manufacturer is not None and model is not None:
self._unique_id = "{}_{}_{}".format(
slugify(manufacturer),
slugify(model),
ieeetail,
)
self._device_state_attributes['friendly_name'] = "{} {}".format(
manufacturer,
model,
)
else:
self._unique_id = str(ieeetail)
self._device = device
self._state = 'offline'
self._keepalive_interval = keepalive_interval
application_listener.register_entity(ieee, self)
@property
def unique_id(self) -> str:
"""Return a unique ID."""
return self._unique_id
@property
def state(self) -> str:
"""Return the state of the entity."""
return self._state
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
update_time = None
if self._device.last_seen is not None and self._state == 'offline':
time_struct = time.localtime(self._device.last_seen)
update_time = time.strftime("%Y-%m-%dT%H:%M:%S", time_struct)
self._device_state_attributes['last_seen'] = update_time
if ('last_seen' in self._device_state_attributes and
self._state != 'offline'):
del self._device_state_attributes['last_seen']
self._device_state_attributes['lqi'] = self._device.lqi
self._device_state_attributes['rssi'] = self._device.rssi
return self._device_state_attributes
async def async_update(self):
"""Handle polling."""
if self._device.last_seen is None:
self._state = 'offline'
else:
difference = time.time() - self._device.last_seen
if difference > self._keepalive_interval:
self._state = 'offline'
else:
self._state = 'online'

View File

@ -0,0 +1,89 @@
"""
Entity for Zigbee Home Automation.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/
"""
from homeassistant.helpers import entity
from homeassistant.util import slugify
from homeassistant.core import callback
class ZhaEntity(entity.Entity):
"""A base class for ZHA entities."""
_domain = None # Must be overridden by subclasses
def __init__(self, endpoint, in_clusters, out_clusters, manufacturer,
model, application_listener, unique_id, **kwargs):
"""Init ZHA entity."""
self._device_state_attributes = {}
ieee = endpoint.device.ieee
ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
if manufacturer and model is not None:
self.entity_id = "{}.{}_{}_{}_{}{}".format(
self._domain,
slugify(manufacturer),
slugify(model),
ieeetail,
endpoint.endpoint_id,
kwargs.get('entity_suffix', ''),
)
self._device_state_attributes['friendly_name'] = "{} {}".format(
manufacturer,
model,
)
else:
self.entity_id = "{}.zha_{}_{}{}".format(
self._domain,
ieeetail,
endpoint.endpoint_id,
kwargs.get('entity_suffix', ''),
)
self._endpoint = endpoint
self._in_clusters = in_clusters
self._out_clusters = out_clusters
self._state = None
self._unique_id = unique_id
# Normally the entity itself is the listener. Sub-classes may set this
# to a dict of cluster ID -> listener to receive messages for specific
# clusters separately
self._in_listeners = {}
self._out_listeners = {}
self._initialized = False
application_listener.register_entity(ieee, self)
async def async_added_to_hass(self):
"""Handle entity addition to hass.
It is now safe to update the entity state
"""
for cluster_id, cluster in self._in_clusters.items():
cluster.add_listener(self._in_listeners.get(cluster_id, self))
for cluster_id, cluster in self._out_clusters.items():
cluster.add_listener(self._out_listeners.get(cluster_id, self))
self._initialized = True
@property
def unique_id(self) -> str:
"""Return a unique ID."""
return self._unique_id
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
return self._device_state_attributes
@callback
def attribute_updated(self, attribute, value):
"""Handle an attribute updated on this cluster."""
pass
@callback
def zdo_command(self, tsn, command_id, args):
"""Handle a ZDO command received on this cluster."""
pass

View File

@ -0,0 +1,84 @@
"""
Helpers for Zigbee Home Automation.
For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/
"""
import logging
_LOGGER = logging.getLogger(__name__)
def get_discovery_info(hass, discovery_info):
"""Get the full discovery info for a device.
Some of the info that needs to be passed to platforms is not JSON
serializable, so it cannot be put in the discovery_info dictionary. This
component places that info we need to pass to the platform in hass.data,
and this function is a helper for platforms to retrieve the complete
discovery info.
"""
if discovery_info is None:
return
import homeassistant.components.zha.const as zha_const
discovery_key = discovery_info.get('discovery_key', None)
all_discovery_info = hass.data.get(zha_const.DISCOVERY_KEY, {})
return all_discovery_info.get(discovery_key, None)
async def safe_read(cluster, attributes, allow_cache=True, only_cache=False):
"""Swallow all exceptions from network read.
If we throw during initialization, setup fails. Rather have an entity that
exists, but is in a maybe wrong state, than no entity. This method should
probably only be used during initialization.
"""
try:
result, _ = await cluster.read_attributes(
attributes,
allow_cache=allow_cache,
only_cache=only_cache
)
return result
except Exception: # pylint: disable=broad-except
return {}
async def configure_reporting(entity_id, cluster, attr, skip_bind=False,
min_report=300, max_report=900,
reportable_change=1):
"""Configure attribute reporting for a cluster.
while swallowing the DeliverError exceptions in case of unreachable
devices.
"""
from zigpy.exceptions import DeliveryError
attr_name = cluster.attributes.get(attr, [attr])[0]
cluster_name = cluster.ep_attribute
if not skip_bind:
try:
res = await cluster.bind()
_LOGGER.debug(
"%s: bound '%s' cluster: %s", entity_id, cluster_name, res[0]
)
except DeliveryError as ex:
_LOGGER.debug(
"%s: Failed to bind '%s' cluster: %s",
entity_id, cluster_name, str(ex)
)
try:
res = await cluster.configure_reporting(attr, min_report,
max_report, reportable_change)
_LOGGER.debug(
"%s: reporting '%s' attr on '%s' cluster: %d/%d/%d: Result: '%s'",
entity_id, attr_name, cluster_name, min_report, max_report,
reportable_change, res
)
except DeliveryError as ex:
_LOGGER.debug(
"%s: failed to set reporting for '%s' attr on '%s' cluster: %s",
entity_id, attr_name, cluster_name, str(ex)
)