From b5df2ba8538e2632e7861c0b353dcfc1a18a5538 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Tue, 18 Feb 2020 22:24:25 +0100 Subject: [PATCH] deCONZ - Directly reflect changes to config entry options (#31661) * Directly reflect changes to config entry options * Remove print * Add tests * Add title to config options --- .../components/deconz/.translations/en.json | 3 +- .../components/deconz/binary_sensor.py | 1 + homeassistant/components/deconz/climate.py | 1 + .../components/deconz/deconz_device.py | 14 +++++ homeassistant/components/deconz/gateway.py | 62 +++++++++++++++++-- homeassistant/components/deconz/light.py | 2 +- homeassistant/components/deconz/sensor.py | 1 + homeassistant/components/deconz/strings.json | 20 ++---- tests/components/deconz/test_binary_sensor.py | 22 +++++++ tests/components/deconz/test_climate.py | 24 +++++++ tests/components/deconz/test_light.py | 26 ++++++++ tests/components/deconz/test_sensor.py | 34 ++++++++++ 12 files changed, 188 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index b3d9e00bfe6..756636ad90a 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -108,7 +108,8 @@ "allow_clip_sensor": "Allow deCONZ CLIP sensors", "allow_deconz_groups": "Allow deCONZ light groups" }, - "description": "Configure visibility of deCONZ device types" + "description": "Configure visibility of deCONZ device types", + "title": "deCONZ options" } } } diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 6a528a66ba6..2514a49f23c 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -37,6 +37,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway.option_allow_clip_sensor or not sensor.type.startswith("CLIP") ) + and sensor.deconz_id not in gateway.deconz_ids.values() ): entities.append(DeconzBinarySensor(sensor, gateway)) diff --git a/homeassistant/components/deconz/climate.py b/homeassistant/components/deconz/climate.py index 7b0f44807ec..34cc0e0b832 100644 --- a/homeassistant/components/deconz/climate.py +++ b/homeassistant/components/deconz/climate.py @@ -44,6 +44,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway.option_allow_clip_sensor or not sensor.type.startswith("CLIP") ) + and sensor.deconz_id not in gateway.deconz_ids.values() ): entities.append(DeconzThermostat(sensor, gateway)) diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index 615fd3db473..b3dedf6cf00 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -77,6 +77,11 @@ class DeconzDevice(DeconzBase, Entity): self.hass, self.gateway.signal_reachable, self.async_update_callback ) ) + self.listeners.append( + async_dispatcher_connect( + self.hass, self.gateway.signal_remove_entity, self.async_remove_self + ) + ) async def async_will_remove_from_hass(self) -> None: """Disconnect device object when removed.""" @@ -85,6 +90,15 @@ class DeconzDevice(DeconzBase, Entity): for unsub_dispatcher in self.listeners: unsub_dispatcher() + async def async_remove_self(self, deconz_ids: list) -> None: + """Schedule removal of this entity. + + Called by signal_remove_entity scheduled by async_added_to_hass. + """ + if self._device.deconz_id not in deconz_ids: + return + await self.async_remove() + @callback def async_update_callback(self, force_update=False, ignore_update=False): """Update the device's state.""" diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 48fecc1ec4f..0b69b82463c 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -20,6 +20,8 @@ from .const import ( DOMAIN, LOGGER, NEW_DEVICE, + NEW_GROUP, + NEW_SENSOR, SUPPORTED_PLATFORMS, ) from .errors import AuthenticationRequired, CannotConnect @@ -45,6 +47,9 @@ class DeconzGateway: self.events = [] self.listeners = [] + self._current_option_allow_clip_sensor = self.option_allow_clip_sensor + self._current_option_allow_deconz_groups = self.option_allow_deconz_groups + @property def bridgeid(self) -> str: """Return the unique identifier of the gateway.""" @@ -108,22 +113,64 @@ class DeconzGateway: self.api.start() - self.config_entry.add_update_listener(self.async_new_address) + self.config_entry.add_update_listener(self.async_config_entry_updated) return True @staticmethod - async def async_new_address(hass, entry) -> None: - """Handle signals of gateway getting new address. + async def async_config_entry_updated(hass, entry) -> None: + """Handle signals of config entry being updated. - This is a static method because a class method (bound method), - can not be used with weak references. + This is a static method because a class method (bound method), can not be used with weak references. + Causes for this is either discovery updating host address or config entry options changing. """ gateway = get_gateway_from_config_entry(hass, entry) if gateway.api.host != entry.data[CONF_HOST]: gateway.api.close() gateway.api.host = entry.data[CONF_HOST] gateway.api.start() + return + + await gateway.options_updated() + + async def options_updated(self): + """Manage entities affected by config entry options.""" + deconz_ids = [] + + if self._current_option_allow_clip_sensor != self.option_allow_clip_sensor: + self._current_option_allow_clip_sensor = self.option_allow_clip_sensor + + sensors = [ + sensor + for sensor in self.api.sensors.values() + if sensor.type.startswith("CLIP") + ] + + if self.option_allow_clip_sensor: + self.async_add_device_callback(NEW_SENSOR, sensors) + else: + deconz_ids += [sensor.deconz_id for sensor in sensors] + + if self._current_option_allow_deconz_groups != self.option_allow_deconz_groups: + self._current_option_allow_deconz_groups = self.option_allow_deconz_groups + + groups = list(self.api.groups.values()) + + if self.option_allow_deconz_groups: + self.async_add_device_callback(NEW_GROUP, groups) + else: + deconz_ids += [group.deconz_id for group in groups] + + if deconz_ids: + async_dispatcher_send(self.hass, self.signal_remove_entity, deconz_ids) + + entity_registry = await self.hass.helpers.entity_registry.async_get_registry() + + for entity_id, deconz_id in self.deconz_ids.items(): + if deconz_id in deconz_ids and entity_registry.async_is_registered( + entity_id + ): + entity_registry.async_remove(entity_id) @property def signal_reachable(self) -> str: @@ -141,6 +188,11 @@ class DeconzGateway: """Gateway specific event to signal new device.""" return NEW_DEVICE[device_type].format(self.bridgeid) + @property + def signal_remove_entity(self) -> str: + """Gateway specific event to signal removal of entity.""" + return f"deconz-remove-{self.bridgeid}" + @callback def async_add_device_callback(self, device_type, device) -> None: """Handle event of new device creation in deCONZ.""" diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index e836f1e4490..f62f9315c49 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -67,7 +67,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities = [] for group in groups: - if group.lights: + if group.lights and group.deconz_id not in gateway.deconz_ids.values(): entities.append(DeconzGroup(group, gateway)) async_add_entities(entities, True) diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index c32b26f299d..6b88c414243 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -68,6 +68,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway.option_allow_clip_sensor or not sensor.type.startswith("CLIP") ) + and sensor.deconz_id not in gateway.deconz_ids.values() ): entities.append(DeconzSensor(sensor, gateway)) diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index b61ea6236da..52cd90e54a1 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -14,20 +14,9 @@ "title": "Link with deCONZ", "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button" }, - "options": { - "title": "Extra configuration options for deCONZ", - "data": { - "allow_clip_sensor": "Allow importing virtual sensors", - "allow_deconz_groups": "Allow importing deCONZ groups" - } - }, "hassio_confirm": { "title": "deCONZ Zigbee gateway via Hass.io add-on", - "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the Hass.io add-on {addon}?", - "data": { - "allow_clip_sensor": "Allow importing virtual sensors", - "allow_deconz_groups": "Allow importing deCONZ groups" - } + "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the Hass.io add-on {addon}?" } }, "error": { @@ -45,11 +34,12 @@ "options": { "step": { "deconz_devices": { - "description": "Configure visibility of deCONZ device types", "data": { "allow_clip_sensor": "Allow deCONZ CLIP sensors", "allow_deconz_groups": "Allow deCONZ light groups" - } + }, + "description": "Configure visibility of deCONZ device types", + "title": "deCONZ options" } } }, @@ -105,4 +95,4 @@ "side_6": "Side 6" } } -} +} \ No newline at end of file diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index c190cd82649..864ba91fbc1 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -134,6 +134,28 @@ async def test_allow_clip_sensor(hass): vibration_sensor = hass.states.get("binary_sensor.vibration_sensor") assert vibration_sensor.state == "on" + hass.config_entries.async_update_entry( + gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: False} + ) + await hass.async_block_till_done() + + assert "binary_sensor.presence_sensor" in gateway.deconz_ids + assert "binary_sensor.temperature_sensor" not in gateway.deconz_ids + assert "binary_sensor.clip_presence_sensor" not in gateway.deconz_ids + assert "binary_sensor.vibration_sensor" in gateway.deconz_ids + assert len(hass.states.async_all()) == 3 + + hass.config_entries.async_update_entry( + gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True} + ) + await hass.async_block_till_done() + + assert "binary_sensor.presence_sensor" in gateway.deconz_ids + assert "binary_sensor.temperature_sensor" not in gateway.deconz_ids + assert "binary_sensor.clip_presence_sensor" in gateway.deconz_ids + assert "binary_sensor.vibration_sensor" in gateway.deconz_ids + assert len(hass.states.async_all()) == 4 + async def test_add_new_binary_sensor(hass): """Test that adding a new binary sensor works.""" diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 802571cdf60..c03dc72019e 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -214,6 +214,30 @@ async def test_clip_climate_device(hass): clip_thermostat = hass.states.get("climate.clip_thermostat") assert clip_thermostat.state == "heat" + hass.config_entries.async_update_entry( + gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: False} + ) + await hass.async_block_till_done() + + assert "climate.thermostat" in gateway.deconz_ids + assert "sensor.thermostat" not in gateway.deconz_ids + assert "sensor.thermostat_battery_level" in gateway.deconz_ids + assert "climate.presence_sensor" not in gateway.deconz_ids + assert "climate.clip_thermostat" not in gateway.deconz_ids + assert len(hass.states.async_all()) == 3 + + hass.config_entries.async_update_entry( + gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True} + ) + await hass.async_block_till_done() + + assert "climate.thermostat" in gateway.deconz_ids + assert "sensor.thermostat" not in gateway.deconz_ids + assert "sensor.thermostat_battery_level" in gateway.deconz_ids + assert "climate.presence_sensor" not in gateway.deconz_ids + assert "climate.clip_thermostat" in gateway.deconz_ids + assert len(hass.states.async_all()) == 4 + async def test_verify_state_update(hass): """Test that state update properly.""" diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 8990e1ff236..e39722fdacb 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -245,3 +245,29 @@ async def test_disable_light_groups(hass): empty_group = hass.states.get("light.empty_group") assert empty_group is None + + hass.config_entries.async_update_entry( + gateway.config_entry, options={deconz.gateway.CONF_ALLOW_DECONZ_GROUPS: True} + ) + await hass.async_block_till_done() + + assert "light.rgb_light" in gateway.deconz_ids + assert "light.tunable_white_light" in gateway.deconz_ids + assert "light.light_group" in gateway.deconz_ids + assert "light.empty_group" not in gateway.deconz_ids + assert "light.on_off_switch" not in gateway.deconz_ids + # 3 entities + assert len(hass.states.async_all()) == 5 + + hass.config_entries.async_update_entry( + gateway.config_entry, options={deconz.gateway.CONF_ALLOW_DECONZ_GROUPS: False} + ) + await hass.async_block_till_done() + + assert "light.rgb_light" in gateway.deconz_ids + assert "light.tunable_white_light" in gateway.deconz_ids + assert "light.light_group" not in gateway.deconz_ids + assert "light.empty_group" not in gateway.deconz_ids + assert "light.on_off_switch" not in gateway.deconz_ids + # 3 entities + assert len(hass.states.async_all()) == 4 diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 3e151b55890..cda3138557d 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -218,6 +218,40 @@ async def test_allow_clip_sensors(hass): clip_light_level_sensor = hass.states.get("sensor.clip_light_level_sensor") assert clip_light_level_sensor.state == "999.8" + hass.config_entries.async_update_entry( + gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: False} + ) + await hass.async_block_till_done() + + assert "sensor.light_level_sensor" in gateway.deconz_ids + assert "sensor.presence_sensor" not in gateway.deconz_ids + assert "sensor.switch_1" not in gateway.deconz_ids + assert "sensor.switch_1_battery_level" not in gateway.deconz_ids + assert "sensor.switch_2" not in gateway.deconz_ids + assert "sensor.switch_2_battery_level" in gateway.deconz_ids + assert "sensor.daylight_sensor" not in gateway.deconz_ids + assert "sensor.power_sensor" in gateway.deconz_ids + assert "sensor.consumption_sensor" in gateway.deconz_ids + assert "sensor.clip_light_level_sensor" not in gateway.deconz_ids + assert len(hass.states.async_all()) == 5 + + hass.config_entries.async_update_entry( + gateway.config_entry, options={deconz.gateway.CONF_ALLOW_CLIP_SENSOR: True} + ) + await hass.async_block_till_done() + + assert "sensor.light_level_sensor" in gateway.deconz_ids + assert "sensor.presence_sensor" not in gateway.deconz_ids + assert "sensor.switch_1" not in gateway.deconz_ids + assert "sensor.switch_1_battery_level" not in gateway.deconz_ids + assert "sensor.switch_2" not in gateway.deconz_ids + assert "sensor.switch_2_battery_level" in gateway.deconz_ids + assert "sensor.daylight_sensor" not in gateway.deconz_ids + assert "sensor.power_sensor" in gateway.deconz_ids + assert "sensor.consumption_sensor" in gateway.deconz_ids + assert "sensor.clip_light_level_sensor" in gateway.deconz_ids + assert len(hass.states.async_all()) == 6 + async def test_add_new_sensor(hass): """Test that adding a new sensor works."""