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
This commit is contained in:
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 ( from .core.const import (
COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, CONF_DEVICE_CONFIG, COMPONENTS, CONF_BAUDRATE, CONF_DATABASE, CONF_DEVICE_CONFIG,
CONF_RADIO_TYPE, CONF_USB_PATH, DATA_ZHA, DATA_ZHA_CONFIG, CONF_RADIO_TYPE, CONF_USB_PATH, DATA_ZHA, DATA_ZHA_CONFIG,
DATA_ZHA_CORE_COMPONENT, DATA_ZHA_DISPATCHERS, DATA_ZHA_GATEWAY, DATA_ZHA_DISPATCHERS, DATA_ZHA_GATEWAY, DEFAULT_BAUDRATE,
DEFAULT_BAUDRATE, DEFAULT_RADIO_TYPE, DOMAIN, ENABLE_QUIRKS, RadioType) DEFAULT_RADIO_TYPE, DOMAIN, ENABLE_QUIRKS, RadioType)
from .core.registries import establish_device_mappings from .core.registries import establish_device_mappings
DEVICE_CONFIG_SCHEMA_ENTRY = vol.Schema({ 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( await hass.config_entries.async_forward_entry_unload(
config_entry, component) 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] del hass.data[DATA_ZHA]
return True return True

View file

@ -22,7 +22,6 @@ from ..const import (
) )
from ..registries import CLUSTER_REPORT_CONFIGS from ..registries import CLUSTER_REPORT_CONFIGS
ZIGBEE_CHANNEL_REGISTRY = {}
_LOGGER = logging.getLogger(__name__) _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 . import ZigbeeChannel, parse_and_log_command
from ..helpers import get_attr_id_by_name from ..helpers import get_attr_id_by_name
from ..const import ( from ..const import (
SIGNAL_ATTR_UPDATED, SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL, SIGNAL_ATTR_UPDATED, SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL
SIGNAL_STATE_ATTR
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -202,8 +201,7 @@ class PowerConfigurationChannel(ZigbeeChannel):
if attrid == attr_id: if attrid == attr_id:
async_dispatcher_send( async_dispatcher_send(
self._zha_device.hass, self._zha_device.hass,
"{}_{}".format(self.unique_id, SIGNAL_STATE_ATTR), "{}_{}".format(self.unique_id, SIGNAL_ATTR_UPDATED),
'battery_level',
value value
) )

View file

@ -19,7 +19,6 @@ DATA_ZHA = 'zha'
DATA_ZHA_CONFIG = 'config' DATA_ZHA_CONFIG = 'config'
DATA_ZHA_BRIDGE_ID = 'zha_bridge_id' DATA_ZHA_BRIDGE_ID = 'zha_bridge_id'
DATA_ZHA_DISPATCHERS = 'zha_dispatchers' DATA_ZHA_DISPATCHERS = 'zha_dispatchers'
DATA_ZHA_CORE_COMPONENT = 'zha_core_component'
DATA_ZHA_CORE_EVENTS = 'zha_core_events' DATA_ZHA_CORE_EVENTS = 'zha_core_events'
DATA_ZHA_GATEWAY = 'zha_gateway' DATA_ZHA_GATEWAY = 'zha_gateway'
ZHA_DISCOVERY_NEW = 'zha_discovery_new_{}' ZHA_DISCOVERY_NEW = 'zha_discovery_new_{}'
@ -67,6 +66,9 @@ SERVER = 'server'
IEEE = 'ieee' IEEE = 'ieee'
MODEL = 'model' MODEL = 'model'
NAME = 'name' NAME = 'name'
LQI = 'lqi'
RSSI = 'rssi'
LAST_SEEN = 'last_seen'
SENSOR_TYPE = 'sensor_type' SENSOR_TYPE = 'sensor_type'
HUMIDITY = 'humidity' HUMIDITY = 'humidity'
@ -76,6 +78,7 @@ PRESSURE = 'pressure'
METERING = 'metering' METERING = 'metering'
ELECTRICAL_MEASUREMENT = 'electrical_measurement' ELECTRICAL_MEASUREMENT = 'electrical_measurement'
GENERIC = 'generic' GENERIC = 'generic'
BATTERY = 'battery'
UNKNOWN = 'unknown' UNKNOWN = 'unknown'
UNKNOWN_MANUFACTURER = 'unk_manufacturer' UNKNOWN_MANUFACTURER = 'unk_manufacturer'
UNKNOWN_MODEL = 'unk_model' 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/ https://home-assistant.io/components/zha/
""" """
import asyncio import asyncio
from datetime import timedelta
from enum import Enum from enum import Enum
import logging import logging
import time
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.dispatcher import ( from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send) async_dispatcher_connect, async_dispatcher_send)
from homeassistant.helpers.event import async_track_time_interval
from .channels import EventRelayChannel from .channels import EventRelayChannel
from .const import ( from .const import (
@ -19,9 +22,12 @@ from .const import (
BATTERY_OR_UNKNOWN, CLIENT_COMMANDS, IEEE, IN, MAINS_POWERED, BATTERY_OR_UNKNOWN, CLIENT_COMMANDS, IEEE, IN, MAINS_POWERED,
MANUFACTURER_CODE, MODEL, NAME, NWK, OUT, POWER_CONFIGURATION_CHANNEL, MANUFACTURER_CODE, MODEL, NAME, NWK, OUT, POWER_CONFIGURATION_CHANNEL,
POWER_SOURCE, QUIRK_APPLIED, QUIRK_CLASS, SERVER, SERVER_COMMANDS, 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__) _LOGGER = logging.getLogger(__name__)
_KEEP_ALIVE_INTERVAL = 7200
_UPDATE_ALIVE_INTERVAL = timedelta(seconds=60)
class DeviceStatus(Enum): class DeviceStatus(Enum):
@ -56,6 +62,11 @@ class ZHADevice:
self._zigpy_device.__class__.__module__, self._zigpy_device.__class__.__module__,
self._zigpy_device.__class__.__name__ self._zigpy_device.__class__.__name__
) )
self._available_check = async_track_time_interval(
self.hass,
self._check_available,
_UPDATE_ALIVE_INTERVAL
)
self.status = DeviceStatus.CREATED self.status = DeviceStatus.CREATED
@property @property
@ -158,6 +169,16 @@ class ZHADevice:
"""Set availability from restore and prevent signals.""" """Set availability from restore and prevent signals."""
self._available = available 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): def update_available(self, available):
"""Set sensor availability.""" """Set sensor availability."""
if self._available != available and available: if self._available != available and available:
@ -178,6 +199,8 @@ class ZHADevice:
def device_info(self): def device_info(self):
"""Return a device description for device.""" """Return a device description for device."""
ieee = str(self.ieee) ieee = str(self.ieee)
time_struct = time.localtime(self.last_seen)
update_time = time.strftime("%Y-%m-%dT%H:%M:%S", time_struct)
return { return {
IEEE: ieee, IEEE: ieee,
NWK: self.nwk, NWK: self.nwk,
@ -187,7 +210,10 @@ class ZHADevice:
QUIRK_APPLIED: self.quirk_applied, QUIRK_APPLIED: self.quirk_applied,
QUIRK_CLASS: self.quirk_class, QUIRK_CLASS: self.quirk_class,
MANUFACTURER_CODE: self.manufacturer_code, 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): def add_cluster_channel(self, cluster_channel):

View file

@ -18,7 +18,7 @@ from .channels import (
from .channels.registry import ZIGBEE_CHANNEL_REGISTRY from .channels.registry import ZIGBEE_CHANNEL_REGISTRY
from .const import ( from .const import (
CONF_DEVICE_CONFIG, COMPONENTS, ZHA_DISCOVERY_NEW, DATA_ZHA, CONF_DEVICE_CONFIG, COMPONENTS, ZHA_DISCOVERY_NEW, DATA_ZHA,
SENSOR_TYPE, UNKNOWN, GENERIC, POWER_CONFIGURATION_CHANNEL SENSOR_TYPE, UNKNOWN, GENERIC
) )
from .registries import ( from .registries import (
BINARY_SENSOR_TYPES, CHANNEL_ONLY_CLUSTERS, EVENT_RELAY_CLUSTERS, 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, SINGLE_INPUT_CLUSTER_DEVICE_CLASS, SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS,
OUTPUT_CHANNEL_ONLY_CLUSTERS, REMOTE_DEVICE_TYPES OUTPUT_CHANNEL_ONLY_CLUSTERS, REMOTE_DEVICE_TYPES
) )
from ..device_entity import ZhaDeviceEntity
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -168,9 +167,10 @@ def _async_handle_single_cluster_matches(hass, endpoint, zha_device,
profile_clusters, device_key, profile_clusters, device_key,
is_new_join): is_new_join):
"""Dispatch single cluster matches to HA components.""" """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_matches = []
cluster_match_results = [] cluster_match_results = []
matched_power_configuration = False
for cluster in endpoint.in_clusters.values(): for cluster in endpoint.in_clusters.values():
if cluster.cluster_id in CHANNEL_ONLY_CLUSTERS: if cluster.cluster_id in CHANNEL_ONLY_CLUSTERS:
cluster_match_results.append( cluster_match_results.append(
@ -182,6 +182,14 @@ def _async_handle_single_cluster_matches(hass, endpoint, zha_device,
continue continue
if cluster.cluster_id not in profile_clusters: 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( cluster_match_results.append(_async_handle_single_cluster_match(
hass, hass,
zha_device, zha_device,
@ -279,13 +287,3 @@ def _async_handle_single_cluster_match(hass, zha_device, cluster, device_key,
}) })
return discovery_info 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 ( from homeassistant.helpers.device_registry import (
async_get_registry as get_dev_reg) async_get_registry as get_dev_reg)
from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity_component import EntityComponent
from ..api import async_get_device_info from ..api import async_get_device_info
from .const import ( from .const import (
ADD_DEVICE_RELAY_LOGGERS, ATTR_MANUFACTURER, BELLOWS, CONF_BAUDRATE, ADD_DEVICE_RELAY_LOGGERS, ATTR_MANUFACTURER, BELLOWS, CONF_BAUDRATE,
CONF_DATABASE, CONF_RADIO_TYPE, CONF_USB_PATH, CONTROLLER, CURRENT, CONF_DATABASE, CONF_RADIO_TYPE, CONF_USB_PATH, CONTROLLER, CURRENT,
DATA_ZHA, DATA_ZHA_BRIDGE_ID, DATA_ZHA_CORE_COMPONENT, DATA_ZHA_GATEWAY, DATA_ZHA, DATA_ZHA_BRIDGE_ID, DATA_ZHA_GATEWAY, DEBUG_LEVELS,
DEBUG_LEVELS, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME, DEVICE_FULL_INIT, DEFAULT_BAUDRATE, DEFAULT_DATABASE_NAME, DEVICE_FULL_INIT,
DEVICE_INFO, DEVICE_JOINED, DEVICE_REMOVED, DOMAIN, IEEE, LOG_ENTRY, DEVICE_INFO, DEVICE_JOINED, DEVICE_REMOVED, DOMAIN, IEEE, LOG_ENTRY,
LOG_OUTPUT, MODEL, NWK, ORIGINAL, RADIO, RADIO_DESCRIPTION, RAW_INIT, LOG_OUTPUT, MODEL, NWK, ORIGINAL, RADIO, RADIO_DESCRIPTION, RAW_INIT,
SIGNAL_REMOVE, SIGNATURE, TYPE, UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, ZHA, SIGNAL_REMOVE, SIGNATURE, TYPE, UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, ZHA,
ZHA_GW_MSG, ZIGPY, ZIGPY_DECONZ, ZIGPY_XBEE) ZHA_GW_MSG, ZIGPY, ZIGPY_DECONZ, ZIGPY_XBEE)
from .device import DeviceStatus, ZHADevice from .device import DeviceStatus, ZHADevice
from .discovery import ( from .discovery import (
async_create_device_entity, async_dispatch_discovery_info, async_dispatch_discovery_info, async_process_endpoint
async_process_endpoint) )
from .patches import apply_application_controller_patch from .patches import apply_application_controller_patch
from .registries import INPUT_BIND_ONLY_CLUSTERS, RADIO_TYPES from .registries import INPUT_BIND_ONLY_CLUSTERS, RADIO_TYPES
from .store import async_get_registry from .store import async_get_registry
@ -51,13 +50,11 @@ class ZHAGateway:
"""Initialize the gateway.""" """Initialize the gateway."""
self._hass = hass self._hass = hass
self._config = config self._config = config
self._component = EntityComponent(_LOGGER, DOMAIN, hass)
self._devices = {} self._devices = {}
self._device_registry = collections.defaultdict(list) self._device_registry = collections.defaultdict(list)
self.zha_storage = None self.zha_storage = None
self.application_controller = None self.application_controller = None
self.radio_description = None self.radio_description = None
hass.data[DATA_ZHA][DATA_ZHA_CORE_COMPONENT] = self._component
hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self
self._log_levels = { self._log_levels = {
ORIGINAL: async_capture_log_levels(), ORIGINAL: async_capture_log_levels(),
@ -324,9 +321,6 @@ class ZHAGateway:
discovery_info discovery_info
) )
device_entity = async_create_device_entity(zha_device)
await self._component.async_add_entities([device_entity])
if is_new_join: if is_new_join:
device_info = async_get_device_info(self._hass, zha_device) device_info = async_get_device_info(self._hass, zha_device)
async_dispatcher_send( async_dispatcher_send(

View file

@ -18,7 +18,7 @@ from .const import (
OCCUPANCY, REPORT_CONFIG_IMMEDIATE, OPENING, ZONE, RADIO_DESCRIPTION, OCCUPANCY, REPORT_CONFIG_IMMEDIATE, OPENING, ZONE, RADIO_DESCRIPTION,
REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, REPORT_CONFIG_MIN_INT, REPORT_CONFIG_ASAP, REPORT_CONFIG_DEFAULT, REPORT_CONFIG_MIN_INT,
REPORT_CONFIG_MAX_INT, REPORT_CONFIG_OP, ACCELERATION, RadioType, RADIO, REPORT_CONFIG_MAX_INT, REPORT_CONFIG_OP, ACCELERATION, RadioType, RADIO,
CONTROLLER CONTROLLER, BATTERY
) )
SMARTTHINGS_HUMIDITY_CLUSTER = 64581 SMARTTHINGS_HUMIDITY_CLUSTER = 64581
@ -110,8 +110,6 @@ def establish_device_mappings():
EVENT_RELAY_CLUSTERS.append(zcl.clusters.general.OnOff.cluster_id) 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.Basic.cluster_id)
CHANNEL_ONLY_CLUSTERS.append(
zcl.clusters.general.PowerConfiguration.cluster_id)
CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.lightlink.LightLink.cluster_id) CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.lightlink.LightLink.cluster_id)
OUTPUT_CHANNEL_ONLY_CLUSTERS.append(zcl.clusters.general.Scenes.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, SMARTTHINGS_ACCELERATION_CLUSTER: BINARY_SENSOR,
zcl.clusters.general.MultistateInput.cluster_id: SENSOR, zcl.clusters.general.MultistateInput.cluster_id: SENSOR,
zcl.clusters.general.AnalogInput.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({ SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({
@ -184,6 +183,7 @@ def establish_device_mappings():
zcl.clusters.smartenergy.Metering.cluster_id: METERING, zcl.clusters.smartenergy.Metering.cluster_id: METERING,
zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id: zcl.clusters.homeautomation.ElectricalMeasurement.cluster_id:
ELECTRICAL_MEASUREMENT, ELECTRICAL_MEASUREMENT,
zcl.clusters.general.PowerConfiguration.cluster_id: BATTERY
}) })
BINARY_SENSOR_TYPES.update({ 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.""" """Sensors on Zigbee Home Automation networks."""
import logging import logging
import numbers
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.components.sensor import ( from homeassistant.components.sensor import (
DOMAIN, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, 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 ( from homeassistant.const import (
TEMP_CELSIUS, POWER_WATT, ATTR_UNIT_OF_MEASUREMENT 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, DATA_ZHA, DATA_ZHA_DISPATCHERS, ZHA_DISCOVERY_NEW, HUMIDITY, TEMPERATURE,
ILLUMINANCE, PRESSURE, METERING, ELECTRICAL_MEASUREMENT, ILLUMINANCE, PRESSURE, METERING, ELECTRICAL_MEASUREMENT,
GENERIC, SENSOR_TYPE, ATTRIBUTE_CHANNEL, ELECTRICAL_MEASUREMENT_CHANNEL, 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 from .entity import ZhaEntity
PARALLEL_UPDATES = 5 PARALLEL_UPDATES = 5
_LOGGER = logging.getLogger(__name__) _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 # Formatter functions
def pass_through_formatter(value): def pass_through_formatter(value):
@ -63,6 +82,29 @@ def pressure_formatter(value):
return round(float(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 = { FORMATTER_FUNC_REGISTRY = {
HUMIDITY: humidity_formatter, HUMIDITY: humidity_formatter,
TEMPERATURE: temperature_formatter, TEMPERATURE: temperature_formatter,
@ -70,6 +112,7 @@ FORMATTER_FUNC_REGISTRY = {
ELECTRICAL_MEASUREMENT: active_power_formatter, ELECTRICAL_MEASUREMENT: active_power_formatter,
ILLUMINANCE: illuminance_formatter, ILLUMINANCE: illuminance_formatter,
GENERIC: pass_through_formatter, GENERIC: pass_through_formatter,
BATTERY: battery_percentage_remaining_formatter
} }
UNIT_REGISTRY = { UNIT_REGISTRY = {
@ -79,11 +122,13 @@ UNIT_REGISTRY = {
ILLUMINANCE: 'lx', ILLUMINANCE: 'lx',
METERING: POWER_WATT, METERING: POWER_WATT,
ELECTRICAL_MEASUREMENT: POWER_WATT, ELECTRICAL_MEASUREMENT: POWER_WATT,
GENERIC: None GENERIC: None,
BATTERY: '%'
} }
CHANNEL_REGISTRY = { CHANNEL_REGISTRY = {
ELECTRICAL_MEASUREMENT: ELECTRICAL_MEASUREMENT_CHANNEL, ELECTRICAL_MEASUREMENT: ELECTRICAL_MEASUREMENT_CHANNEL,
BATTERY: POWER_CONFIGURATION_CHANNEL
} }
POLLING_REGISTRY = { POLLING_REGISTRY = {
@ -101,7 +146,13 @@ DEVICE_CLASS_REGISTRY = {
PRESSURE: DEVICE_CLASS_PRESSURE, PRESSURE: DEVICE_CLASS_PRESSURE,
ILLUMINANCE: DEVICE_CLASS_ILLUMINANCE, ILLUMINANCE: DEVICE_CLASS_ILLUMINANCE,
METERING: DEVICE_CLASS_POWER, 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, self._sensor_type,
None None
) )
self.state_attr_provider = DEVICE_STATE_ATTR_PROVIDER_REGISTRY.get(
self._sensor_type,
None
)
async def async_added_to_hass(self): async def async_added_to_hass(self):
"""Run when about to be added to hass.""" """Run when about to be added to hass."""
await super().async_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( await self.async_accept_signal(
self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state) self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state)
await self.async_accept_signal( await self.async_accept_signal(