diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 87c405873ee..5c8d9381a2e 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -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 diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index 162ef5a59e4..a3db90d75bf 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -22,7 +22,6 @@ from ..const import ( ) from ..registries import CLUSTER_REPORT_CONFIGS -ZIGBEE_CHANNEL_REGISTRY = {} _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 3f08a738a13..0bad5f17456 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -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 ) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 23b2bb99050..b40f1cf5ff4 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -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' diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 401d2fac5be..1ba890da411 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -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): diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 8901726ff88..e4bc58eeecf 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -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) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 307d85a8d9e..4a38bc647e6 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -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( diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 8a6832caed6..e710b0cc856 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -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({ diff --git a/homeassistant/components/zha/device_entity.py b/homeassistant/components/zha/device_entity.py deleted file mode 100644 index 8f761e9a8be..00000000000 --- a/homeassistant/components/zha/device_entity.py +++ /dev/null @@ -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 diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 15ef922bd98..fefd60f45b5 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -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(