mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 01:38:02 +00:00
Refactor ZHA (#18629)
* refactor ZHA * lint * review request * Exclude more zha modules from coverage
This commit is contained in:
parent
cccc41c23e
commit
67aa76d295
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
)
|
||||
|
@ -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',
|
||||
|
10
homeassistant/components/zha/entities/__init__.py
Normal file
10
homeassistant/components/zha/entities/__init__.py
Normal 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
|
81
homeassistant/components/zha/entities/device_entity.py
Normal file
81
homeassistant/components/zha/entities/device_entity.py
Normal 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'
|
89
homeassistant/components/zha/entities/entity.py
Normal file
89
homeassistant/components/zha/entities/entity.py
Normal 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
|
84
homeassistant/components/zha/helpers.py
Normal file
84
homeassistant/components/zha/helpers.py
Normal 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)
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user