From 2a2af8030901d72628c897c9fec5ad84ccd27824 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 22 Dec 2018 14:53:15 -0500 Subject: [PATCH] Add ZHA occupancy sensor (#19365) * occupancy sensor * lint * map occupancy cluster to binary_sensor * update to use reporting configuration and async_configure * refactor * fix typo - review comment * handle restore entity functionality --- homeassistant/components/binary_sensor/zha.py | 78 ++++++++++++++++++- homeassistant/components/zha/const.py | 1 + 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/binary_sensor/zha.py b/homeassistant/components/binary_sensor/zha.py index e065b8cc34e..fbe4d33c44e 100644 --- a/homeassistant/components/binary_sensor/zha.py +++ b/homeassistant/components/binary_sensor/zha.py @@ -28,6 +28,7 @@ CLASS_MAPPING = { 0x002b: 'gas', 0x002d: 'vibration', } +DEVICE_CLASS_OCCUPANCY = 'occupancy' async def async_setup_platform(hass, config, async_add_entities, @@ -59,9 +60,15 @@ async def _async_setup_entities(hass, config_entry, async_add_entities, entities = [] for discovery_info in discovery_infos: from zigpy.zcl.clusters.general import OnOff + from zigpy.zcl.clusters.measurement import OccupancySensing from zigpy.zcl.clusters.security import IasZone if IasZone.cluster_id in discovery_info['in_clusters']: entities.append(await _async_setup_iaszone(discovery_info)) + elif OccupancySensing.cluster_id in discovery_info['in_clusters']: + entities.append(await _async_setup_occupancy( + DEVICE_CLASS_OCCUPANCY, + discovery_info + )) elif OnOff.cluster_id in discovery_info['out_clusters']: entities.append(await _async_setup_remote(discovery_info)) @@ -84,7 +91,7 @@ async def _async_setup_iaszone(discovery_info): # If we fail to read from the device, use a non-specific class pass - return BinarySensor(device_class, **discovery_info) + return IasZoneSensor(device_class, **discovery_info) async def _async_setup_remote(discovery_info): @@ -95,8 +102,15 @@ async def _async_setup_remote(discovery_info): return remote -class BinarySensor(RestoreEntity, ZhaEntity, BinarySensorDevice): - """The ZHA Binary Sensor.""" +async def _async_setup_occupancy(device_class, discovery_info): + sensor = BinarySensor(device_class, **discovery_info) + if discovery_info['new_join']: + await sensor.async_configure() + return sensor + + +class IasZoneSensor(RestoreEntity, ZhaEntity, BinarySensorDevice): + """The IasZoneSensor Binary Sensor.""" _domain = DOMAIN @@ -313,3 +327,61 @@ class Remote(RestoreEntity, ZhaEntity, BinarySensorDevice): only_cache=(not self._initialized) ) self._state = result.get('on_off', self._state) + + +class BinarySensor(RestoreEntity, ZhaEntity, BinarySensorDevice): + """ZHA switch.""" + + _domain = DOMAIN + _device_class = None + value_attribute = 0 + + def __init__(self, device_class, **kwargs): + """Initialize the ZHA binary sensor.""" + super().__init__(**kwargs) + self._device_class = device_class + self._cluster = list(kwargs['in_clusters'].values())[0] + + def attribute_updated(self, attribute, value): + """Handle attribute update from device.""" + _LOGGER.debug("Attribute updated: %s %s %s", self, attribute, value) + if attribute == self.value_attribute: + self._state = bool(value) + self.async_schedule_update_ha_state() + + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + await super().async_added_to_hass() + old_state = await self.async_get_last_state() + if self._state is not None or old_state is None: + return + + _LOGGER.debug("%s restoring old state: %s", self.entity_id, old_state) + self._state = old_state.state == STATE_ON + + @property + def should_poll(self) -> bool: + """Let zha handle polling.""" + return False + + @property + def cluster(self): + """Zigbee cluster for this entity.""" + return self._cluster + + @property + def zcl_reporting_config(self): + """ZHA reporting configuration.""" + return {self.cluster: {self.value_attribute: REPORT_CONFIG_IMMEDIATE}} + + @property + def is_on(self) -> bool: + """Return if the switch is on based on the statemachine.""" + if self._state is None: + return False + return self._state + + @property + def device_class(self) -> str: + """Return device class from component DEVICE_CLASSES.""" + return self._device_class diff --git a/homeassistant/components/zha/const.py b/homeassistant/components/zha/const.py index 250d699aecb..3e7f9f89f91 100644 --- a/homeassistant/components/zha/const.py +++ b/homeassistant/components/zha/const.py @@ -138,6 +138,7 @@ def populate_data(): zcl.clusters.homeautomation.ElectricalMeasurement: 'sensor', zcl.clusters.general.PowerConfiguration: 'sensor', zcl.clusters.security.IasZone: 'binary_sensor', + zcl.clusters.measurement.OccupancySensing: 'binary_sensor', zcl.clusters.hvac.Fan: 'fan', }) SINGLE_OUTPUT_CLUSTER_DEVICE_CLASS.update({