Remove ZHA device entity (#24909)

* move availability handling to device

* update last_seen format

* add battery sensor

* fix interval

* fix battery reporting now that it is a sensor

* remove zha entities and add battery sensor
pull/24928/head
David F. Mulcahey 2019-07-03 13:36:36 -04:00 committed by GitHub
parent eec67d8b1a
commit a9459c6d92
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 118 additions and 205 deletions

View File

@ -15,8 +15,8 @@ from .core.channels.registry import populate_channel_registry
from .core.const import (
COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, CONF_DEVICE_CONFIG,
CONF_RADIO_TYPE, CONF_USB_PATH, DATA_ZHA, DATA_ZHA_CONFIG,
DATA_ZHA_CORE_COMPONENT, DATA_ZHA_DISPATCHERS, DATA_ZHA_GATEWAY,
DEFAULT_BAUDRATE, DEFAULT_RADIO_TYPE, DOMAIN, ENABLE_QUIRKS, RadioType)
DATA_ZHA_DISPATCHERS, DATA_ZHA_GATEWAY, DEFAULT_BAUDRATE,
DEFAULT_RADIO_TYPE, DOMAIN, ENABLE_QUIRKS, RadioType)
from .core.registries import establish_device_mappings
DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({
@ -147,11 +147,5 @@ async def async_unload_entry(hass, config_entry):
await hass.config_entries.async_forward_entry_unload(
config_entry, component)
# clean up device entities
component = hass.data[DATA_ZHA][DATA_ZHA_CORE_COMPONENT]
entity_ids = [entity.entity_id for entity in component.entities]
for entity_id in entity_ids:
await component.async_remove_entity(entity_id)
del hass.data[DATA_ZHA]
return True

View File

@ -22,7 +22,6 @@ from ..const import (
)
from ..registries import CLUSTER_REPORT_CONFIGS
ZIGBEE_CHANNEL_REGISTRY = {}
_LOGGER = logging.getLogger(__name__)

View File

@ -11,8 +11,7 @@ from homeassistant.helpers.event import async_call_later
from . import ZigbeeChannel, parse_and_log_command
from ..helpers import get_attr_id_by_name
from ..const import (
SIGNAL_ATTR_UPDATED, SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL,
SIGNAL_STATE_ATTR
SIGNAL_ATTR_UPDATED, SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL
)
_LOGGER = logging.getLogger(__name__)
@ -202,8 +201,7 @@ class PowerConfigurationChannel(ZigbeeChannel):
if attrid == attr_id:
async_dispatcher_send(
self._zha_device.hass,
"{}_{}".format(self.unique_id, SIGNAL_STATE_ATTR),
'battery_level',
"{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED),
value
)

View File

@ -19,7 +19,6 @@ DATA_ZHA = 'zha'
DATA_ZHA_CONFIG = 'config'
DATA_ZHA_BRIDGE_ID = 'zha_bridge_id'
DATA_ZHA_DISPATCHERS = 'zha_dispatchers'
DATA_ZHA_CORE_COMPONENT = 'zha_core_component'
DATA_ZHA_CORE_EVENTS = 'zha_core_events'
DATA_ZHA_GATEWAY = 'zha_gateway'
ZHA_DISCOVERY_NEW = 'zha_discovery_new_{}'
@ -67,6 +66,9 @@ SERVER = 'server'
IEEE = 'ieee'
MODEL = 'model'
NAME = 'name'
LQI = 'lqi'
RSSI = 'rssi'
LAST_SEEN = 'last_seen'
SENSOR_TYPE = 'sensor_type'
HUMIDITY = 'humidity'
@ -76,6 +78,7 @@ PRESSURE = 'pressure'
METERING = 'metering'
ELECTRICAL_MEASUREMENT = 'electrical_measurement'
GENERIC = 'generic'
BATTERY = 'battery'
UNKNOWN = 'unknown'
UNKNOWN_MANUFACTURER = 'unk_manufacturer'
UNKNOWN_MODEL = 'unk_model'

View File

@ -5,12 +5,15 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/zha/
"""
import asyncio
from datetime import timedelta
from enum import Enum
import logging
import time
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send)
from homeassistant.helpers.event import async_track_time_interval
from .channels import EventRelayChannel
from .const import (
@ -19,9 +22,12 @@ from .const import (
BATTERY_OR_UNKNOWN, CLIENT_COMMANDS, IEEE, IN, MAINS_POWERED,
MANUFACTURER_CODE, MODEL, NAME, NWK, OUT, POWER_CONFIGURATION_CHANNEL,
POWER_SOURCE, QUIRK_APPLIED, QUIRK_CLASS, SERVER, SERVER_COMMANDS,
SIGNAL_AVAILABLE, UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, ZDO_CHANNEL)
SIGNAL_AVAILABLE, UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, ZDO_CHANNEL,
LQI, RSSI, LAST_SEEN)
_LOGGER = logging.getLogger(__name__)
_KEEP_ALIVE_INTERVAL = 7200
_UPDATE_ALIVE_INTERVAL = timedelta(seconds=60)
class DeviceStatus(Enum):
@ -56,6 +62,11 @@ class ZHADevice:
self._zigpy_device.__class__.__module__,
self._zigpy_device.__class__.__name__
)
self._available_check = async_track_time_interval(
self.hass,
self._check_available,
_UPDATE_ALIVE_INTERVAL
)
self.status = DeviceStatus.CREATED
@property
@ -158,6 +169,16 @@ class ZHADevice:
"""Set availability from restore and prevent signals."""
self._available = available
def _check_available(self, *_):
if self.last_seen is None:
self.update_available(False)
else:
difference = time.time() - self.last_seen
if difference > _KEEP_ALIVE_INTERVAL:
self.update_available(False)
else:
self.update_available(True)
def update_available(self, available):
"""Set sensor availability."""
if self._available != available and available:
@ -178,6 +199,8 @@ class ZHADevice:
def device_info(self):
"""Return a device description for device."""
ieee = str(self.ieee)
time_struct = time.localtime(self.last_seen)
update_time = time.strftime("%Y-%m-%dT%H:%M:%S", time_struct)
return {
IEEE: ieee,
NWK: self.nwk,
@ -187,7 +210,10 @@ class ZHADevice:
QUIRK_APPLIED: self.quirk_applied,
QUIRK_CLASS: self.quirk_class,
MANUFACTURER_CODE: self.manufacturer_code,
POWER_SOURCE: self.power_source
POWER_SOURCE: self.power_source,
LQI: self.lqi,
RSSI: self.rssi,
LAST_SEEN: update_time
}
def add_cluster_channel(self, cluster_channel):

View File

@ -18,7 +18,7 @@ from .channels import (
from .channels.registry import ZIGBEE_CHANNEL_REGISTRY
from .const import (
CONF_DEVICE_CONFIG, COMPONENTS, ZHA_DISCOVERY_NEW, DATA_ZHA,
SENSOR_TYPE, UNKNOWN, GENERIC, POWER_CONFIGURATION_CHANNEL
SENSOR_TYPE, UNKNOWN, GENERIC
)
from .registries import (
BINARY_SENSOR_TYPES, CHANNEL_ONLY_CLUSTERS, EVENT_RELAY_CLUSTERS,
@ -26,7 +26,6 @@ from .registries import (
SINGLE_INPUT_CLUSTER_DEVICE_CLASS, SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS,
OUTPUT_CHANNEL_ONLY_CLUSTERS, REMOTE_DEVICE_TYPES
)
from ..device_entity import ZhaDeviceEntity
_LOGGER = logging.getLogger(__name__)
@ -168,9 +167,10 @@ def _async_handle_single_cluster_matches(hass, endpoint, zha_device,
profile_clusters, device_key,
is_new_join):
"""Dispatch single cluster matches to HA components."""
from zigpy.zcl.clusters.general import OnOff
from zigpy.zcl.clusters.general import OnOff, PowerConfiguration
cluster_matches = []
cluster_match_results = []
matched_power_configuration = False
for cluster in endpoint.in_clusters.values():
if cluster.cluster_id in CHANNEL_ONLY_CLUSTERS:
cluster_match_results.append(
@ -182,6 +182,14 @@ def _async_handle_single_cluster_matches(hass, endpoint, zha_device,
continue
if cluster.cluster_id not in profile_clusters:
# Only create one battery sensor per device
if cluster.cluster_id == PowerConfiguration.cluster_id and \
(zha_device.is_mains_powered or
matched_power_configuration):
continue
elif cluster.cluster_id == PowerConfiguration.cluster_id and not \
zha_device.is_mains_powered:
matched_power_configuration = True
cluster_match_results.append(_async_handle_single_cluster_match(
hass,
zha_device,
@ -279,13 +287,3 @@ def _async_handle_single_cluster_match(hass, zha_device, cluster, device_key,
})
return discovery_info
@callback
def async_create_device_entity(zha_device):
"""Create ZHADeviceEntity."""
device_entity_channels = []
if POWER_CONFIGURATION_CHANNEL in zha_device.cluster_channels:
channel = zha_device.cluster_channels.get(POWER_CONFIGURATION_CHANNEL)
device_entity_channels.append(channel)
return ZhaDeviceEntity(zha_device, device_entity_channels)

View File

@ -17,22 +17,21 @@ from homeassistant.core import callback
from homeassistant.helpers.device_registry import (
async_get_registry as get_dev_reg)
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity_component import EntityComponent
from ..api import async_get_device_info
from .const import (
ADD_DEVICE_RELAY_LOGGERS, ATTR_MANUFACTURER, BELLOWS, CONF_BAUDRATE,
CONF_DATABASE, CONF_RADIO_TYPE, CONF_USB_PATH, CONTROLLER, CURRENT,
DATA_ZHA, DATA_ZHA_BRIDGE_ID, DATA_ZHA_CORE_COMPONENT, DATA_ZHA_GATEWAY,
DEBUG_LEVELS, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME, DEVICE_FULL_INIT,
DATA_ZHA, DATA_ZHA_BRIDGE_ID, DATA_ZHA_GATEWAY, DEBUG_LEVELS,
DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME, DEVICE_FULL_INIT,
DEVICE_INFO, DEVICE_JOINED, DEVICE_REMOVED, DOMAIN, IEEE, LOG_ENTRY,
LOG_OUTPUT, MODEL, NWK, ORIGINAL, RADIO, RADIO_DESCRIPTION, RAW_INIT,
SIGNAL_REMOVE, SIGNATURE, TYPE, UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, ZHA,
ZHA_GW_MSG, ZIGPY, ZIGPY_DECONZ, ZIGPY_XBEE)
from .device import DeviceStatus, ZHADevice
from .discovery import (
async_create_device_entity, async_dispatch_discovery_info,
async_process_endpoint)
async_dispatch_discovery_info, async_process_endpoint
)
from .patches import apply_application_controller_patch
from .registries import INPUT_BIND_ONLY_CLUSTERS, RADIO_TYPES
from .store import async_get_registry
@ -51,13 +50,11 @@ class ZHAGateway:
"""Initialize the gateway."""
self._hass = hass
self._config = config
self._component = EntityComponent(_LOGGER, DOMAIN, hass)
self._devices = {}
self._device_registry = collections.defaultdict(list)
self.zha_storage = None
self.application_controller = None
self.radio_description = None
hass.data[DATA_ZHA][DATA_ZHA_CORE_COMPONENT] = self._component
hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self
self._log_levels = {
ORIGINAL: async_capture_log_levels(),
@ -324,9 +321,6 @@ class ZHAGateway:
discovery_info
)
device_entity = async_create_device_entity(zha_device)
await self._component.async_add_entities([device_entity])
if is_new_join:
device_info = async_get_device_info(self._hass, zha_device)
async_dispatcher_send(

View File

@ -18,7 +18,7 @@ from .const import (
OCCUPANCY, REPORT_CONFIG_IMMEDIATE, OPENING, ZONE, RADIO_DESCRIPTION,
REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, REPORT_CONFIG_MIN_INT,
REPORT_CONFIG_MAX_INT, REPORT_CONFIG_OP, ACCELERATION, RadioType, RADIO,
CONTROLLER
CONTROLLER, BATTERY
)
SMARTTHINGS_HUMIDITY_CLUSTER = 64581
@ -110,8 +110,6 @@ def establish_device_mappings():
EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id)
CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.general.Basic.cluster_id)
CHANNEL_ONLY_CLUSTERS.append(
zcl.clusters.general.PowerConfiguration.cluster_id)
CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.lightlink.LightLink.cluster_id)
OUTPUT_CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.general.Scenes.cluster_id)
@ -166,7 +164,8 @@ def establish_device_mappings():
SMARTTHINGS_ACCELERATION_CLUSTER: BINARY_SENSOR,
zcl.clusters.general.MultistateInput.cluster_id: SENSOR,
zcl.clusters.general.AnalogInput.cluster_id: SENSOR,
zcl.clusters.closures.DoorLock: LOCK
zcl.clusters.closures.DoorLock: LOCK,
zcl.clusters.general.PowerConfiguration: SENSOR
})
SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({
@ -184,6 +183,7 @@ def establish_device_mappings():
zcl.clusters.smartenergy.Metering.cluster_id: METERING,
zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id:
ELECTRICAL_MEASUREMENT,
zcl.clusters.general.PowerConfiguration.cluster_id: BATTERY
})
BINARY_SENSOR_TYPES.update({

View File

@ -1,158 +0,0 @@
"""Device entity for Zigbee Home Automation."""
import logging
import numbers
import time
from homeassistant.core import callback
from homeassistant.util import slugify
from .core.const import POWER_CONFIGURATION_CHANNEL, SIGNAL_STATE_ATTR
from .entity import ZhaEntity
_LOGGER = logging.getLogger(__name__)
BATTERY_SIZES = {
0: 'No battery',
1: 'Built in',
2: 'Other',
3: 'AA',
4: 'AAA',
5: 'C',
6: 'D',
7: 'CR2',
8: 'CR123A',
9: 'CR2450',
10: 'CR2032',
11: 'CR1632',
255: 'Unknown'
}
STATE_ONLINE = 'online'
STATE_OFFLINE = 'offline'
class ZhaDeviceEntity(ZhaEntity):
"""A base class for ZHA devices."""
def __init__(self, zha_device, channels, keepalive_interval=7200,
**kwargs):
"""Init ZHA endpoint entity."""
ieee = zha_device.ieee
ieeetail = ''.join(['%02x' % (o, ) for o in ieee[-4:]])
unique_id = "{}_{}_{}".format(
slugify(zha_device.manufacturer),
slugify(zha_device.model),
ieeetail,
)
kwargs['component'] = 'zha'
super().__init__(unique_id, zha_device, channels, skip_entity_id=True,
**kwargs)
self._keepalive_interval = keepalive_interval
self._device_state_attributes.update({
'nwk': '0x{0:04x}'.format(zha_device.nwk),
'ieee': str(zha_device.ieee),
'lqi': zha_device.lqi,
'rssi': zha_device.rssi,
})
self._should_poll = True
self._battery_channel = self.cluster_channels.get(
POWER_CONFIGURATION_CHANNEL)
@property
def state(self) -> str:
"""Return the state of the entity."""
return self._state
@property
def available(self):
"""Return True if device is available."""
return self._zha_device.available
@property
def device_state_attributes(self):
"""Return device specific state attributes."""
update_time = None
device = self._zha_device
if device.last_seen is not None and not self.available:
time_struct = time.localtime(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.available):
del self._device_state_attributes['last_seen']
self._device_state_attributes['lqi'] = device.lqi
self._device_state_attributes['rssi'] = device.rssi
return self._device_state_attributes
async def async_added_to_hass(self):
"""Run when about to be added to hass."""
await super().async_added_to_hass()
await self.async_check_recently_seen()
if self._battery_channel:
await self.async_accept_signal(
self._battery_channel, SIGNAL_STATE_ATTR,
self.async_update_state_attribute)
# only do this on add to HA because it is static
await self._async_init_battery_values()
def async_update_state_attribute(self, key, value):
"""Update a single device state attribute."""
if key == 'battery_level':
if not isinstance(value, numbers.Number) or value == -1:
return
value = value / 2
value = int(round(value))
self._device_state_attributes.update({
key: value
})
self.async_schedule_update_ha_state()
async def async_update(self):
"""Handle polling."""
if self._zha_device.last_seen is None:
self._zha_device.update_available(False)
else:
difference = time.time() - self._zha_device.last_seen
if difference > self._keepalive_interval:
self._zha_device.update_available(False)
else:
self._zha_device.update_available(True)
if self._battery_channel:
await self.async_get_latest_battery_reading()
@callback
def async_set_available(self, available):
"""Set entity availability."""
if available:
self._state = STATE_ONLINE
else:
self._state = STATE_OFFLINE
super().async_set_available(available)
async def _async_init_battery_values(self):
"""Get initial battery level and battery info from channel cache."""
battery_size = await self._battery_channel.get_attribute_value(
'battery_size')
if battery_size is not None:
self._device_state_attributes['battery_size'] = BATTERY_SIZES.get(
battery_size, 'Unknown')
battery_quantity = await self._battery_channel.get_attribute_value(
'battery_quantity')
if battery_quantity is not None:
self._device_state_attributes['battery_quantity'] = \
battery_quantity
await self.async_get_latest_battery_reading()
async def async_get_latest_battery_reading(self):
"""Get the latest battery reading from channels cache."""
battery = await self._battery_channel.get_attribute_value(
'battery_percentage_remaining')
# per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯
if battery is not None and battery != -1:
battery = battery / 2
battery = int(round(battery))
self._device_state_attributes['battery_level'] = battery

View File

@ -1,10 +1,12 @@
"""Sensors on Zigbee Home Automation networks."""
import logging
import numbers
from homeassistant.core import callback
from homeassistant.components.sensor import (
DOMAIN, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_POWER
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_POWER,
DEVICE_CLASS_BATTERY
)
from homeassistant.const import (
TEMP_CELSIUS, POWER_WATT, ATTR_UNIT_OF_MEASUREMENT
@ -14,12 +16,29 @@ from .core.const import (
DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, HUMIDITY, TEMPERATURE,
ILLUMINANCE, PRESSURE, METERING, ELECTRICAL_MEASUREMENT,
GENERIC, SENSOR_TYPE, ATTRIBUTE_CHANNEL, ELECTRICAL_MEASUREMENT_CHANNEL,
SIGNAL_ATTR_UPDATED, SIGNAL_STATE_ATTR, UNKNOWN)
SIGNAL_ATTR_UPDATED, SIGNAL_STATE_ATTR, UNKNOWN, BATTERY,
POWER_CONFIGURATION_CHANNEL)
from .entity import ZhaEntity
PARALLEL_UPDATES = 5
_LOGGER = logging.getLogger(__name__)
BATTERY_SIZES = {
0: 'No battery',
1: 'Built in',
2: 'Other',
3: 'AA',
4: 'AAA',
5: 'C',
6: 'D',
7: 'CR2',
8: 'CR123A',
9: 'CR2450',
10: 'CR2032',
11: 'CR1632',
255: 'Unknown'
}
# Formatter functions
def pass_through_formatter(value):
@ -63,6 +82,29 @@ def pressure_formatter(value):
return round(float(value))
def battery_percentage_remaining_formatter(value):
"""Return the state of the entity."""
# per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯
if not isinstance(value, numbers.Number) or value == -1:
return value
value = value / 2
value = int(round(value))
return value
async def async_battery_device_state_attr_provider(channel):
"""Return device statr attrs for battery sensors."""
state_attrs = {}
battery_size = await channel.get_attribute_value('battery_size')
if battery_size is not None:
state_attrs['battery_size'] = BATTERY_SIZES.get(
battery_size, 'Unknown')
battery_quantity = await channel.get_attribute_value('battery_quantity')
if battery_quantity is not None:
state_attrs['battery_quantity'] = battery_quantity
return state_attrs
FORMATTER_FUNC_REGISTRY = {
HUMIDITY: humidity_formatter,
TEMPERATURE: temperature_formatter,
@ -70,6 +112,7 @@ FORMATTER_FUNC_REGISTRY = {
ELECTRICAL_MEASUREMENT: active_power_formatter,
ILLUMINANCE: illuminance_formatter,
GENERIC: pass_through_formatter,
BATTERY: battery_percentage_remaining_formatter
}
UNIT_REGISTRY = {
@ -79,11 +122,13 @@ UNIT_REGISTRY = {
ILLUMINANCE: 'lx',
METERING: POWER_WATT,
ELECTRICAL_MEASUREMENT: POWER_WATT,
GENERIC: None
GENERIC: None,
BATTERY: '%'
}
CHANNEL_REGISTRY = {
ELECTRICAL_MEASUREMENT: ELECTRICAL_MEASUREMENT_CHANNEL,
BATTERY: POWER_CONFIGURATION_CHANNEL
}
POLLING_REGISTRY = {
@ -101,7 +146,13 @@ DEVICE_CLASS_REGISTRY = {
PRESSURE: DEVICE_CLASS_PRESSURE,
ILLUMINANCE: DEVICE_CLASS_ILLUMINANCE,
METERING: DEVICE_CLASS_POWER,
ELECTRICAL_MEASUREMENT: DEVICE_CLASS_POWER
ELECTRICAL_MEASUREMENT: DEVICE_CLASS_POWER,
BATTERY: DEVICE_CLASS_BATTERY
}
DEVICE_STATE_ATTR_PROVIDER_REGISTRY = {
BATTERY: async_battery_device_state_attr_provider
}
@ -172,10 +223,18 @@ class Sensor(ZhaEntity):
self._sensor_type,
None
)
self.state_attr_provider = DEVICE_STATE_ATTR_PROVIDER_REGISTRY.get(
self._sensor_type,
None
)
async def async_added_to_hass(self):
"""Run when about to be added to hass."""
await super().async_added_to_hass()
if self.state_attr_provider is not None:
self._device_state_attributes = await self.state_attr_provider(
self._channel
)
await self.async_accept_signal(
self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state)
await self.async_accept_signal(