From 557e585e561f1c907e84ab876db0c8d287f1cf73 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 1 Nov 2019 22:31:22 +0100 Subject: [PATCH] deCONZ - Support creating battery sensor when reported (#27538) --- .../components/deconz/binary_sensor.py | 4 +- homeassistant/components/deconz/climate.py | 4 +- homeassistant/components/deconz/sensor.py | 67 +++++++++++++++++-- tests/components/deconz/test_sensor.py | 23 +++++++ 4 files changed, 90 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index b81ecdc5164..1a4d9680c1e 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -26,13 +26,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entity_handler = DeconzEntityHandler(gateway) @callback - def async_add_sensor(sensors): + def async_add_sensor(sensors, new=True): """Add binary sensor from deCONZ.""" entities = [] for sensor in sensors: - if sensor.BINARY: + if new and sensor.BINARY: new_sensor = DeconzBinarySensor(sensor, gateway) entity_handler.add_entity(new_sensor) entities.append(new_sensor) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index b7a1ebce22a..ba1f1ce846a 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -31,13 +31,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway = get_gateway_from_config_entry(hass, config_entry) @callback - def async_add_climate(sensors): + def async_add_climate(sensors, new=True): """Add climate devices from deCONZ.""" entities = [] for sensor in sensors: - if sensor.type in Thermostat.ZHATYPE: + if new and sensor.type in Thermostat.ZHATYPE: entities.append(DeconzThermostat(sensor, gateway)) async_add_entities(entities, True) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index cc3f3de3170..3a3dbceb46b 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -3,7 +3,10 @@ from pydeconz.sensor import Consumption, Daylight, LightLevel, Power, Switch, Th from homeassistant.const import ATTR_TEMPERATURE, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR from .deconz_device import DeconzDevice @@ -25,21 +28,23 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway = get_gateway_from_config_entry(hass, config_entry) batteries = set() + battery_handler = DeconzBatteryHandler(gateway) entity_handler = DeconzEntityHandler(gateway) @callback - def async_add_sensor(sensors): + def async_add_sensor(sensors, new=True): """Add sensors from deCONZ. Create DeconzEvent if part of ZHAType list. Create DeconzSensor if not a ZHAType and not a binary sensor. Create DeconzBattery if sensor has a battery attribute. + If new is false it means an existing sensor has got a battery state reported. """ entities = [] for sensor in sensors: - if sensor.type in Switch.ZHATYPE: + if new and sensor.type in Switch.ZHATYPE: if gateway.option_allow_clip_sensor or not sensor.type.startswith( "CLIP" @@ -48,7 +53,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass.async_create_task(new_event.async_update_device_registry()) gateway.events.append(new_event) - elif not sensor.BINARY and sensor.type not in Thermostat.ZHATYPE: + elif new and not sensor.BINARY and sensor.type not in Thermostat.ZHATYPE: new_sensor = DeconzSensor(sensor, gateway) entity_handler.add_entity(new_sensor) @@ -59,6 +64,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if new_battery.unique_id not in batteries: batteries.add(new_battery.unique_id) entities.append(new_battery) + battery_handler.remove_tracker(sensor) + else: + battery_handler.create_tracker(sensor) async_add_entities(entities, True) @@ -176,3 +184,54 @@ class DeconzBattery(DeconzDevice): attr[ATTR_EVENT_ID] = event.event_id return attr + + +class DeconzSensorStateTracker: + """Track sensors without a battery state and signal when battery state exist.""" + + def __init__(self, sensor, gateway): + """Set up tracker.""" + self.sensor = sensor + self.gateway = gateway + sensor.register_async_callback(self.async_update_callback) + + @callback + def close(self): + """Clean up tracker.""" + self.sensor.remove_callback(self.async_update_callback) + self.gateway = None + self.sensor = None + + @callback + def async_update_callback(self): + """Sensor state updated.""" + if "battery" in self.sensor.changed_keys: + async_dispatcher_send( + self.gateway.hass, + self.gateway.async_signal_new_device(NEW_SENSOR), + [self.sensor], + False, + ) + + +class DeconzBatteryHandler: + """Creates and stores trackers for sensors without a battery state.""" + + def __init__(self, gateway): + """Set up battery handler.""" + self.gateway = gateway + self._trackers = set() + + @callback + def create_tracker(self, sensor): + """Create new tracker for battery state.""" + self._trackers.add(DeconzSensorStateTracker(sensor, self.gateway)) + + @callback + def remove_tracker(self, sensor): + """Remove tracker of battery state.""" + for tracker in self._trackers: + if sensor == tracker.sensor: + tracker.close() + self._trackers.remove(tracker) + break diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 928e527dd07..7b6ae41086b 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -233,3 +233,26 @@ async def test_add_new_sensor(hass): light_level_sensor = hass.states.get("sensor.light_level_sensor") assert light_level_sensor.state == "999.8" + + +async def test_add_battery_later(hass): + """Test that a sensor without an initial battery state creates a battery sensor once state exist.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = {"1": deepcopy(SENSORS["3"])} + gateway = await setup_deconz_integration( + hass, ENTRY_CONFIG, options={}, get_state_response=data + ) + remote = gateway.api.sensors["1"] + assert len(gateway.deconz_ids) == 0 + assert len(gateway.events) == 1 + assert len(remote._async_callbacks) == 2 + + remote.async_update({"config": {"battery": 50}}) + await hass.async_block_till_done() + + assert len(gateway.deconz_ids) == 1 + assert len(gateway.events) == 1 + assert len(remote._async_callbacks) == 2 + + battery_sensor = hass.states.get("sensor.switch_1_battery_level") + assert battery_sensor is not None