deCONZ - use entity registry disabled_by to control available entities (#26219)

* First draft

* Support enabling disabled entities

* Clean up

* Move import

* Local entity enabled replaced during rebase

* Add option flow test

* Mark options properties with option
pull/26467/head
Robert Svensson 2019-09-06 01:38:00 +02:00 committed by Paulus Schoutsen
parent b1c2a5fa08
commit 518d2c31bb
19 changed files with 191 additions and 82 deletions

View File

@ -12,15 +12,7 @@ from homeassistant.helpers import config_validation as cv
# Loading the config flow file will register the flow
from .config_flow import get_master_gateway
from .const import (
CONF_ALLOW_CLIP_SENSOR,
CONF_ALLOW_DECONZ_GROUPS,
CONF_BRIDGEID,
CONF_MASTER_GATEWAY,
DEFAULT_PORT,
DOMAIN,
_LOGGER,
)
from .const import CONF_BRIDGEID, CONF_MASTER_GATEWAY, DEFAULT_PORT, DOMAIN, _LOGGER
from .gateway import DeconzGateway
CONFIG_SCHEMA = vol.Schema(
@ -86,7 +78,7 @@ async def async_setup_entry(hass, config_entry):
hass.data[DOMAIN] = {}
if not config_entry.options:
await async_populate_options(hass, config_entry)
await async_update_master_gateway(hass, config_entry)
gateway = DeconzGateway(hass, config_entry)
@ -203,25 +195,25 @@ async def async_unload_entry(hass, config_entry):
hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH)
elif gateway.master:
await async_populate_options(hass, config_entry)
await async_update_master_gateway(hass, config_entry)
new_master_gateway = next(iter(hass.data[DOMAIN].values()))
await async_populate_options(hass, new_master_gateway.config_entry)
await async_update_master_gateway(hass, new_master_gateway.config_entry)
return await gateway.async_reset()
async def async_populate_options(hass, config_entry):
"""Populate default options for gateway.
async def async_update_master_gateway(hass, config_entry):
"""Update master gateway boolean.
Called by setup_entry and unload_entry.
Makes sure there is always one master available.
"""
master = not get_master_gateway(hass)
options = {
CONF_MASTER_GATEWAY: master,
CONF_ALLOW_CLIP_SENSOR: config_entry.data.get(CONF_ALLOW_CLIP_SENSOR, False),
CONF_ALLOW_DECONZ_GROUPS: config_entry.data.get(CONF_ALLOW_DECONZ_GROUPS, True),
}
old_options = dict(config_entry.options)
new_options = {CONF_MASTER_GATEWAY: master}
options = {**old_options, **new_options}
hass.config_entries.async_update_entry(config_entry, options=options)

View File

@ -8,7 +8,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR
from .deconz_device import DeconzDevice
from .gateway import get_gateway_from_config_entry
from .gateway import get_gateway_from_config_entry, DeconzEntityHandler
ATTR_ORIENTATION = "orientation"
ATTR_TILTANGLE = "tiltangle"
@ -24,6 +24,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ binary sensor."""
gateway = get_gateway_from_config_entry(hass, config_entry)
entity_handler = DeconzEntityHandler(gateway)
@callback
def async_add_sensor(sensors):
"""Add binary sensor from deCONZ."""
@ -31,17 +33,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
for sensor in sensors:
if sensor.BINARY and not (
not gateway.allow_clip_sensor and sensor.type.startswith("CLIP")
):
entities.append(DeconzBinarySensor(sensor, gateway))
if sensor.BINARY:
new_sensor = DeconzBinarySensor(sensor, gateway)
entity_handler.add_entity(new_sensor)
entities.append(new_sensor)
async_add_entities(entities, True)
gateway.listeners.append(
async_dispatcher_connect(
hass, gateway.async_event_new_device(NEW_SENSOR), async_add_sensor
hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_sensor
)
)

View File

@ -38,17 +38,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
for sensor in sensors:
if sensor.type in Thermostat.ZHATYPE and not (
not gateway.allow_clip_sensor and sensor.type.startswith("CLIP")
):
if sensor.type in Thermostat.ZHATYPE:
entities.append(DeconzThermostat(sensor, gateway))
async_add_entities(entities, True)
gateway.listeners.append(
async_dispatcher_connect(
hass, gateway.async_event_new_device(NEW_SENSOR), async_add_climate
hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_climate
)
)

View File

@ -1,6 +1,5 @@
"""Config flow to configure deCONZ component."""
import asyncio
from copy import copy
import async_timeout
import voluptuous as vol
@ -17,6 +16,8 @@ from .const import (
CONF_ALLOW_CLIP_SENSOR,
CONF_ALLOW_DECONZ_GROUPS,
CONF_BRIDGEID,
DEFAULT_ALLOW_CLIP_SENSOR,
DEFAULT_ALLOW_DECONZ_GROUPS,
DEFAULT_PORT,
DOMAIN,
)
@ -256,7 +257,7 @@ class DeconzOptionsFlowHandler(config_entries.OptionsFlow):
def __init__(self, config_entry):
"""Initialize deCONZ options flow."""
self.config_entry = config_entry
self.options = copy(config_entry.options)
self.options = dict(config_entry.options)
async def async_step_init(self, user_input=None):
"""Manage the deCONZ options."""
@ -277,11 +278,15 @@ class DeconzOptionsFlowHandler(config_entries.OptionsFlow):
{
vol.Optional(
CONF_ALLOW_CLIP_SENSOR,
default=self.config_entry.options[CONF_ALLOW_CLIP_SENSOR],
default=self.config_entry.options.get(
CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR
),
): bool,
vol.Optional(
CONF_ALLOW_DECONZ_GROUPS,
default=self.config_entry.options[CONF_ALLOW_DECONZ_GROUPS],
default=self.config_entry.options.get(
CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS
),
): bool,
}
),

View File

@ -7,7 +7,7 @@ DOMAIN = "deconz"
DEFAULT_PORT = 80
DEFAULT_ALLOW_CLIP_SENSOR = False
DEFAULT_ALLOW_DECONZ_GROUPS = False
DEFAULT_ALLOW_DECONZ_GROUPS = True
CONF_ALLOW_CLIP_SENSOR = "allow_clip_sensor"
CONF_ALLOW_DECONZ_GROUPS = "allow_deconz_groups"

View File

@ -40,7 +40,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
gateway.listeners.append(
async_dispatcher_connect(
hass, gateway.async_event_new_device(NEW_LIGHT), async_add_cover
hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_cover
)
)

View File

@ -14,21 +14,40 @@ class DeconzDevice(Entity):
"""Set up device and add update callback to get data from websocket."""
self._device = device
self.gateway = gateway
self.unsub_dispatcher = None
self.listeners = []
@property
def entity_registry_enabled_default(self):
"""Return if the entity should be enabled when first added to the entity registry."""
if not self.gateway.option_allow_clip_sensor and self._device.type.startswith(
"CLIP"
):
return False
if (
not self.gateway.option_allow_deconz_groups
and self._device.type == "LightGroup"
):
return False
return True
async def async_added_to_hass(self):
"""Subscribe to device events."""
self._device.register_async_callback(self.async_update_callback)
self.gateway.deconz_ids[self.entity_id] = self._device.deconz_id
self.unsub_dispatcher = async_dispatcher_connect(
self.hass, self.gateway.event_reachable, self.async_update_callback
self.listeners.append(
async_dispatcher_connect(
self.hass, self.gateway.signal_reachable, self.async_update_callback
)
)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect device object when removed."""
self._device.remove_callback(self.async_update_callback)
del self.gateway.deconz_ids[self.entity_id]
self.unsub_dispatcher()
for unsub_dispatcher in self.listeners:
unsub_dispatcher()
@callback
def async_update_callback(self, force_update=False):

View File

@ -14,6 +14,10 @@ from homeassistant.helpers.dispatcher import (
async_dispatcher_connect,
async_dispatcher_send,
)
from homeassistant.helpers.entity_registry import (
async_get_registry,
DISABLED_CONFIG_ENTRY,
)
from homeassistant.util import slugify
from .const import (
@ -22,6 +26,8 @@ from .const import (
CONF_ALLOW_DECONZ_GROUPS,
CONF_BRIDGEID,
CONF_MASTER_GATEWAY,
DEFAULT_ALLOW_CLIP_SENSOR,
DEFAULT_ALLOW_DECONZ_GROUPS,
DOMAIN,
NEW_DEVICE,
NEW_SENSOR,
@ -61,14 +67,18 @@ class DeconzGateway:
return self.config_entry.options[CONF_MASTER_GATEWAY]
@property
def allow_clip_sensor(self) -> bool:
def option_allow_clip_sensor(self) -> bool:
"""Allow loading clip sensor from gateway."""
return self.config_entry.options.get(CONF_ALLOW_CLIP_SENSOR, True)
return self.config_entry.options.get(
CONF_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_CLIP_SENSOR
)
@property
def allow_deconz_groups(self) -> bool:
def option_allow_deconz_groups(self) -> bool:
"""Allow loading deCONZ groups from gateway."""
return self.config_entry.options.get(CONF_ALLOW_DECONZ_GROUPS, True)
return self.config_entry.options.get(
CONF_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_DECONZ_GROUPS
)
async def async_update_device_registry(self):
"""Update device registry."""
@ -111,7 +121,7 @@ class DeconzGateway:
self.listeners.append(
async_dispatcher_connect(
hass, self.async_event_new_device(NEW_SENSOR), self.async_add_remote
hass, self.async_signal_new_device(NEW_SENSOR), self.async_add_remote
)
)
@ -119,35 +129,50 @@ class DeconzGateway:
self.api.start()
self.config_entry.add_update_listener(self.async_new_address_callback)
self.config_entry.add_update_listener(self.async_new_address)
self.config_entry.add_update_listener(self.async_options_updated)
return True
@staticmethod
async def async_new_address_callback(hass, entry):
async def async_new_address(hass, entry):
"""Handle signals of gateway getting new address.
This is a static method because a class method (bound method),
can not be used with weak references.
"""
gateway = hass.data[DOMAIN][entry.data[CONF_BRIDGEID]]
gateway.api.close()
gateway.api.host = entry.data[CONF_HOST]
gateway.api.start()
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()
@property
def event_reachable(self):
def signal_reachable(self):
"""Gateway specific event to signal a change in connection status."""
return f"deconz_reachable_{self.bridgeid}"
return f"deconz-reachable-{self.bridgeid}"
@callback
def async_connection_status_callback(self, available):
"""Handle signals of gateway connection status."""
self.available = available
async_dispatcher_send(self.hass, self.event_reachable, True)
async_dispatcher_send(self.hass, self.signal_reachable, True)
@property
def signal_options_update(self):
"""Event specific per deCONZ entry to signal new options."""
return f"deconz-options-{self.bridgeid}"
@staticmethod
async def async_options_updated(hass, entry):
"""Triggered by config entry options updates."""
gateway = get_gateway_from_config_entry(hass, entry)
registry = await async_get_registry(hass)
async_dispatcher_send(hass, gateway.signal_options_update, registry)
@callback
def async_event_new_device(self, device_type):
def async_signal_new_device(self, device_type):
"""Gateway specific event to signal new device."""
return NEW_DEVICE[device_type].format(self.bridgeid)
@ -157,7 +182,7 @@ class DeconzGateway:
if not isinstance(device, list):
device = [device]
async_dispatcher_send(
self.hass, self.async_event_new_device(device_type), device
self.hass, self.async_signal_new_device(device_type), device
)
@callback
@ -165,7 +190,7 @@ class DeconzGateway:
"""Set up remote from deCONZ."""
for sensor in sensors:
if sensor.type in Switch.ZHATYPE and not (
not self.allow_clip_sensor and sensor.type.startswith("CLIP")
not self.option_allow_clip_sensor and sensor.type.startswith("CLIP")
):
self.events.append(DeconzEvent(self.hass, sensor))
@ -183,6 +208,7 @@ class DeconzGateway:
Will cancel any scheduled setup retry and will unload
the config entry.
"""
self.api.async_connection_status_callback = None
self.api.close()
for component in SUPPORTED_PLATFORMS:
@ -229,6 +255,41 @@ async def get_gateway(
raise CannotConnect
class DeconzEntityHandler:
"""Platform entity handler to help with updating disabled by."""
def __init__(self, gateway):
"""Create an entity handler."""
self.gateway = gateway
self._entities = []
gateway.listeners.append(
async_dispatcher_connect(
gateway.hass, gateway.signal_options_update, self.update_entity_registry
)
)
@callback
def add_entity(self, entity):
"""Add a new entity to handler."""
self._entities.append(entity)
@callback
def update_entity_registry(self, entity_registry):
"""Update entity registry disabled by status."""
for entity in self._entities:
if entity.entity_registry_enabled_default != entity.enabled:
disabled_by = None
if entity.enabled:
disabled_by = DISABLED_CONFIG_ENTRY
entity_registry.async_update_entity(
entity.registry_entry.entity_id, disabled_by=disabled_by
)
class DeconzEvent:
"""When you want signals instead of entities.

View File

@ -29,7 +29,7 @@ from .const import (
SWITCH_TYPES,
)
from .deconz_device import DeconzDevice
from .gateway import get_gateway_from_config_entry
from .gateway import get_gateway_from_config_entry, DeconzEntityHandler
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
@ -41,6 +41,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ lights and groups from a config entry."""
gateway = get_gateway_from_config_entry(hass, config_entry)
entity_handler = DeconzEntityHandler(gateway)
@callback
def async_add_light(lights):
"""Add light from deCONZ."""
@ -54,7 +56,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
gateway.listeners.append(
async_dispatcher_connect(
hass, gateway.async_event_new_device(NEW_LIGHT), async_add_light
hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_light
)
)
@ -64,14 +66,16 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
entities = []
for group in groups:
if group.lights and gateway.allow_deconz_groups:
entities.append(DeconzGroup(group, gateway))
if group.lights:
new_group = DeconzGroup(group, gateway)
entity_handler.add_entity(new_group)
entities.append(new_group)
async_add_entities(entities, True)
gateway.listeners.append(
async_dispatcher_connect(
hass, gateway.async_event_new_device(NEW_GROUP), async_add_group
hass, gateway.async_signal_new_device(NEW_GROUP), async_add_group
)
)

View File

@ -28,7 +28,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
gateway.listeners.append(
async_dispatcher_connect(
hass, gateway.async_event_new_device(NEW_SCENE), async_add_scene
hass, gateway.async_signal_new_device(NEW_SCENE), async_add_scene
)
)
@ -49,6 +49,7 @@ class DeconzScene(Scene):
async def async_will_remove_from_hass(self) -> None:
"""Disconnect scene object when removed."""
del self.gateway.deconz_ids[self.entity_id]
self._scene = None
async def async_activate(self):

View File

@ -13,7 +13,7 @@ from homeassistant.util import slugify
from .const import ATTR_DARK, ATTR_ON, NEW_SENSOR
from .deconz_device import DeconzDevice
from .gateway import get_gateway_from_config_entry
from .gateway import get_gateway_from_config_entry, DeconzEntityHandler
ATTR_CURRENT = "current"
ATTR_POWER = "power"
@ -30,6 +30,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ sensors."""
gateway = get_gateway_from_config_entry(hass, config_entry)
entity_handler = DeconzEntityHandler(gateway)
@callback
def async_add_sensor(sensors):
"""Add sensors from deCONZ."""
@ -37,22 +39,22 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
for sensor in sensors:
if not sensor.BINARY and not (
not gateway.allow_clip_sensor and sensor.type.startswith("CLIP")
):
if not sensor.BINARY:
if sensor.type in Switch.ZHATYPE:
if sensor.battery:
entities.append(DeconzBattery(sensor, gateway))
else:
entities.append(DeconzSensor(sensor, gateway))
new_sensor = DeconzSensor(sensor, gateway)
entity_handler.add_entity(new_sensor)
entities.append(new_sensor)
async_add_entities(entities, True)
gateway.listeners.append(
async_dispatcher_connect(
hass, gateway.async_event_new_device(NEW_SENSOR), async_add_sensor
hass, gateway.async_signal_new_device(NEW_SENSOR), async_add_sensor
)
)

View File

@ -37,7 +37,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
gateway.listeners.append(
async_dispatcher_connect(
hass, gateway.async_event_new_device(NEW_LIGHT), async_add_switch
hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_switch
)
)

View File

@ -118,7 +118,7 @@ async def test_add_new_sensor(hass):
sensor.BINARY = True
sensor.uniqueid = "1"
sensor.register_async_callback = Mock()
async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor])
async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor])
await hass.async_block_till_done()
assert "binary_sensor.name" in gateway.deconz_ids
@ -131,7 +131,7 @@ async def test_do_not_allow_clip_sensor(hass):
sensor.name = "name"
sensor.type = "CLIPPresence"
sensor.register_async_callback = Mock()
async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor])
async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor])
await hass.async_block_till_done()
assert len(gateway.deconz_ids) == 0

View File

@ -191,7 +191,7 @@ async def test_add_new_climate_device(hass):
sensor.type = "ZHAThermostat"
sensor.uniqueid = "1"
sensor.register_async_callback = Mock()
async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor])
async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor])
await hass.async_block_till_done()
assert "climate.name" in gateway.deconz_ids
@ -203,7 +203,7 @@ async def test_do_not_allow_clipsensor(hass):
sensor.name = "name"
sensor.type = "CLIPThermostat"
sensor.register_async_callback = Mock()
async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor])
async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor])
await hass.async_block_till_done()
assert len(gateway.deconz_ids) == 0

View File

@ -382,3 +382,29 @@ async def test_hassio_confirm(hass):
config_flow.CONF_BRIDGEID: "id",
config_flow.CONF_API_KEY: "1234567890ABCDEF",
}
async def test_option_flow(hass):
"""Test config flow selection of one of two bridges."""
entry = MockConfigEntry(domain=config_flow.DOMAIN, data={}, options=None)
hass.config_entries._entries.append(entry)
flow = await hass.config_entries.options._async_create_flow(
entry.entry_id, context={"source": "test"}, data=None
)
result = await flow.async_step_init()
assert result["type"] == "form"
assert result["step_id"] == "deconz_devices"
result = await flow.async_step_deconz_devices(
user_input={
config_flow.CONF_ALLOW_CLIP_SENSOR: False,
config_flow.CONF_ALLOW_DECONZ_GROUPS: False,
}
)
assert result["type"] == "create_entry"
assert result["data"] == {
config_flow.CONF_ALLOW_CLIP_SENSOR: False,
config_flow.CONF_ALLOW_DECONZ_GROUPS: False,
}

View File

@ -135,7 +135,7 @@ async def test_add_new_cover(hass):
cover.type = "Level controllable output"
cover.uniqueid = "1"
cover.register_async_callback = Mock()
async_dispatcher_send(hass, gateway.async_event_new_device("light"), [cover])
async_dispatcher_send(hass, gateway.async_signal_new_device("light"), [cover])
await hass.async_block_till_done()
assert "cover.name" in gateway.deconz_ids

View File

@ -193,7 +193,7 @@ async def test_add_new_light(hass):
light.name = "name"
light.uniqueid = "1"
light.register_async_callback = Mock()
async_dispatcher_send(hass, gateway.async_event_new_device("light"), [light])
async_dispatcher_send(hass, gateway.async_signal_new_device("light"), [light])
await hass.async_block_till_done()
assert "light.name" in gateway.deconz_ids
@ -204,7 +204,7 @@ async def test_add_new_group(hass):
group = Mock()
group.name = "name"
group.register_async_callback = Mock()
async_dispatcher_send(hass, gateway.async_event_new_device("group"), [group])
async_dispatcher_send(hass, gateway.async_signal_new_device("group"), [group])
await hass.async_block_till_done()
assert "light.name" in gateway.deconz_ids
@ -214,8 +214,9 @@ async def test_do_not_add_deconz_groups(hass):
gateway = await setup_gateway(hass, {}, allow_deconz_groups=False)
group = Mock()
group.name = "name"
group.type = "LightGroup"
group.register_async_callback = Mock()
async_dispatcher_send(hass, gateway.async_event_new_device("group"), [group])
async_dispatcher_send(hass, gateway.async_signal_new_device("group"), [group])
await hass.async_block_till_done()
assert len(gateway.deconz_ids) == 0

View File

@ -162,7 +162,7 @@ async def test_add_new_sensor(hass):
sensor.uniqueid = "1"
sensor.BINARY = False
sensor.register_async_callback = Mock()
async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor])
async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor])
await hass.async_block_till_done()
assert "sensor.name" in gateway.deconz_ids
@ -174,7 +174,7 @@ async def test_do_not_allow_clipsensor(hass):
sensor.name = "name"
sensor.type = "CLIPTemperature"
sensor.register_async_callback = Mock()
async_dispatcher_send(hass, gateway.async_event_new_device("sensor"), [sensor])
async_dispatcher_send(hass, gateway.async_signal_new_device("sensor"), [sensor])
await hass.async_block_till_done()
assert len(gateway.deconz_ids) == 0

View File

@ -143,7 +143,7 @@ async def test_add_new_switch(hass):
switch.type = "Smart plug"
switch.uniqueid = "1"
switch.register_async_callback = Mock()
async_dispatcher_send(hass, gateway.async_event_new_device("light"), [switch])
async_dispatcher_send(hass, gateway.async_signal_new_device("light"), [switch])
await hass.async_block_till_done()
assert "switch.name" in gateway.deconz_ids