deCONZ - reflect hub status on entities (#18106)

* Support for controlling entity available attribute based on gateways availability

* Fix string not being in imperative mood
This commit is contained in:
Robert Svensson 2018-11-05 16:21:44 +01:00 committed by Paulus Schoutsen
parent 3d4ff74761
commit dcdae325ea
8 changed files with 131 additions and 71 deletions

View File

@ -6,8 +6,8 @@ https://home-assistant.io/components/binary_sensor.deconz/
"""
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.deconz.const import (
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ,
DECONZ_DOMAIN)
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DECONZ_REACHABLE,
DOMAIN as DECONZ_DOMAIN)
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
@ -24,6 +24,8 @@ async def async_setup_platform(hass, config, async_add_entities,
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ binary sensor."""
gateway = hass.data[DECONZ_DOMAIN]
@callback
def async_add_sensor(sensors):
"""Add binary sensor from deCONZ."""
@ -33,30 +35,35 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
for sensor in sensors:
if sensor.type in DECONZ_BINARY_SENSOR and \
not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
entities.append(DeconzBinarySensor(sensor))
entities.append(DeconzBinarySensor(sensor, gateway))
async_add_entities(entities, True)
hass.data[DATA_DECONZ].listeners.append(
gateway.listeners.append(
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
async_add_sensor(hass.data[DATA_DECONZ].api.sensors.values())
async_add_sensor(gateway.api.sensors.values())
class DeconzBinarySensor(BinarySensorDevice):
"""Representation of a binary sensor."""
def __init__(self, sensor):
def __init__(self, sensor, gateway):
"""Set up sensor and add update callback to get data from websocket."""
self._sensor = sensor
self.gateway = gateway
self.unsub_dispatcher = None
async def async_added_to_hass(self):
"""Subscribe sensors events."""
self._sensor.register_async_callback(self.async_update_callback)
self.hass.data[DATA_DECONZ].deconz_ids[self.entity_id] = \
self._sensor.deconz_id
self.gateway.deconz_ids[self.entity_id] = self._sensor.deconz_id
self.unsub_dispatcher = async_dispatcher_connect(
self.hass, DECONZ_REACHABLE, self.async_update_callback)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect sensor object when removed."""
if self.unsub_dispatcher is not None:
self.unsub_dispatcher()
self._sensor.remove_callback(self.async_update_callback)
self._sensor = None
@ -101,7 +108,7 @@ class DeconzBinarySensor(BinarySensorDevice):
@property
def available(self):
"""Return True if sensor is available."""
return self._sensor.reachable
return self.gateway.available and self._sensor.reachable
@property
def should_poll(self):
@ -128,7 +135,7 @@ class DeconzBinarySensor(BinarySensorDevice):
self._sensor.uniqueid.count(':') != 7):
return None
serial = self._sensor.uniqueid.split('-', 1)[0]
bridgeid = self.hass.data[DATA_DECONZ].api.config.bridgeid
bridgeid = self.gateway.api.config.bridgeid
return {
'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)},

View File

@ -5,7 +5,8 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/cover.deconz/
"""
from homeassistant.components.deconz.const import (
COVER_TYPES, DAMPERS, DOMAIN as DATA_DECONZ, DECONZ_DOMAIN, WINDOW_COVERS)
COVER_TYPES, DAMPERS, DECONZ_REACHABLE, DOMAIN as DECONZ_DOMAIN,
WINDOW_COVERS)
from homeassistant.components.cover import (
ATTR_POSITION, CoverDevice, SUPPORT_CLOSE, SUPPORT_OPEN, SUPPORT_STOP,
SUPPORT_SET_POSITION)
@ -29,6 +30,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
Covers are based on same device class as lights in deCONZ.
"""
gateway = hass.data[DECONZ_DOMAIN]
@callback
def async_add_cover(lights):
"""Add cover from deCONZ."""
@ -36,23 +39,26 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
for light in lights:
if light.type in COVER_TYPES:
if light.modelid in ZIGBEE_SPEC:
entities.append(DeconzCoverZigbeeSpec(light))
entities.append(DeconzCoverZigbeeSpec(light, gateway))
else:
entities.append(DeconzCover(light))
entities.append(DeconzCover(light, gateway))
async_add_entities(entities, True)
hass.data[DATA_DECONZ].listeners.append(
gateway.listeners.append(
async_dispatcher_connect(hass, 'deconz_new_light', async_add_cover))
async_add_cover(hass.data[DATA_DECONZ].api.lights.values())
async_add_cover(gateway.api.lights.values())
class DeconzCover(CoverDevice):
"""Representation of a deCONZ cover."""
def __init__(self, cover):
def __init__(self, cover, gateway):
"""Set up cover and add update callback to get data from websocket."""
self._cover = cover
self.gateway = gateway
self.unsub_dispatcher = None
self._features = SUPPORT_OPEN
self._features |= SUPPORT_CLOSE
self._features |= SUPPORT_STOP
@ -61,11 +67,14 @@ class DeconzCover(CoverDevice):
async def async_added_to_hass(self):
"""Subscribe to covers events."""
self._cover.register_async_callback(self.async_update_callback)
self.hass.data[DATA_DECONZ].deconz_ids[self.entity_id] = \
self._cover.deconz_id
self.gateway.deconz_ids[self.entity_id] = self._cover.deconz_id
self.unsub_dispatcher = async_dispatcher_connect(
self.hass, DECONZ_REACHABLE, self.async_update_callback)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect cover object when removed."""
if self.unsub_dispatcher is not None:
self.unsub_dispatcher()
self._cover.remove_callback(self.async_update_callback)
self._cover = None
@ -112,7 +121,7 @@ class DeconzCover(CoverDevice):
@property
def available(self):
"""Return True if light is available."""
return self._cover.reachable
return self.gateway.available and self._cover.reachable
@property
def should_poll(self):
@ -150,7 +159,7 @@ class DeconzCover(CoverDevice):
self._cover.uniqueid.count(':') != 7):
return None
serial = self._cover.uniqueid.split('-', 1)[0]
bridgeid = self.hass.data[DATA_DECONZ].api.config.bridgeid
bridgeid = self.gateway.api.config.bridgeid
return {
'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)},

View File

@ -5,9 +5,6 @@ _LOGGER = logging.getLogger('homeassistant.components.deconz')
DOMAIN = 'deconz'
CONFIG_FILE = 'deconz.conf'
DATA_DECONZ_EVENT = 'deconz_events'
DATA_DECONZ_ID = 'deconz_entities'
DATA_DECONZ_UNSUB = 'deconz_dispatchers'
DECONZ_DOMAIN = 'deconz'
CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor'
@ -16,6 +13,8 @@ CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups'
SUPPORTED_PLATFORMS = ['binary_sensor', 'cover',
'light', 'scene', 'sensor', 'switch']
DECONZ_REACHABLE = 'deconz_reachable'
ATTR_DARK = 'dark'
ATTR_ON = 'on'

View File

@ -8,7 +8,7 @@ from homeassistant.helpers.dispatcher import (
from homeassistant.util import slugify
from .const import (
_LOGGER, CONF_ALLOW_CLIP_SENSOR, SUPPORTED_PLATFORMS)
_LOGGER, DECONZ_REACHABLE, CONF_ALLOW_CLIP_SENSOR, SUPPORTED_PLATFORMS)
class DeconzGateway:
@ -18,6 +18,7 @@ class DeconzGateway:
"""Initialize the system."""
self.hass = hass
self.config_entry = config_entry
self.available = True
self.api = None
self._cancel_retry_setup = None
@ -30,7 +31,8 @@ class DeconzGateway:
hass = self.hass
self.api = await get_gateway(
hass, self.config_entry.data, self.async_add_device_callback
hass, self.config_entry.data, self.async_add_device_callback,
self.async_connection_status_callback
)
if self.api is False:
@ -65,6 +67,13 @@ class DeconzGateway:
return True
@callback
def async_connection_status_callback(self, available):
"""Handle signals of gateway connection status."""
self.available = available
async_dispatcher_send(
self.hass, DECONZ_REACHABLE, {'state': True, 'attr': 'reachable'})
@callback
def async_add_device_callback(self, device_type, device):
"""Handle event of new device creation in deCONZ."""
@ -122,13 +131,15 @@ class DeconzGateway:
return True
async def get_gateway(hass, config, async_add_device_callback):
async def get_gateway(hass, config, async_add_device_callback,
async_connection_status_callback):
"""Create a gateway object and verify configuration."""
from pydeconz import DeconzSession
session = aiohttp_client.async_get_clientsession(hass)
deconz = DeconzSession(hass.loop, session, **config,
async_add_device=async_add_device_callback)
async_add_device=async_add_device_callback,
connection_status=async_connection_status_callback)
result = await deconz.async_load_parameters()
if result:

View File

@ -5,7 +5,7 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/light.deconz/
"""
from homeassistant.components.deconz.const import (
CONF_ALLOW_DECONZ_GROUPS, DOMAIN as DATA_DECONZ, DECONZ_DOMAIN,
CONF_ALLOW_DECONZ_GROUPS, DECONZ_REACHABLE, DOMAIN as DECONZ_DOMAIN,
COVER_TYPES, SWITCH_TYPES)
from homeassistant.components.light import (
ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR,
@ -28,16 +28,18 @@ async def async_setup_platform(hass, config, async_add_entities,
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ lights and groups from a config entry."""
gateway = hass.data[DECONZ_DOMAIN]
@callback
def async_add_light(lights):
"""Add light from deCONZ."""
entities = []
for light in lights:
if light.type not in COVER_TYPES + SWITCH_TYPES:
entities.append(DeconzLight(light))
entities.append(DeconzLight(light, gateway))
async_add_entities(entities, True)
hass.data[DATA_DECONZ].listeners.append(
gateway.listeners.append(
async_dispatcher_connect(hass, 'deconz_new_light', async_add_light))
@callback
@ -47,22 +49,24 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
allow_group = config_entry.data.get(CONF_ALLOW_DECONZ_GROUPS, True)
for group in groups:
if group.lights and allow_group:
entities.append(DeconzLight(group))
entities.append(DeconzLight(group, gateway))
async_add_entities(entities, True)
hass.data[DATA_DECONZ].listeners.append(
gateway.listeners.append(
async_dispatcher_connect(hass, 'deconz_new_group', async_add_group))
async_add_light(hass.data[DATA_DECONZ].api.lights.values())
async_add_group(hass.data[DATA_DECONZ].api.groups.values())
async_add_light(gateway.api.lights.values())
async_add_group(gateway.api.groups.values())
class DeconzLight(Light):
"""Representation of a deCONZ light."""
def __init__(self, light):
def __init__(self, light, gateway):
"""Set up light and add update callback to get data from websocket."""
self._light = light
self.gateway = gateway
self.unsub_dispatcher = None
self._features = SUPPORT_BRIGHTNESS
self._features |= SUPPORT_FLASH
@ -80,11 +84,14 @@ class DeconzLight(Light):
async def async_added_to_hass(self):
"""Subscribe to lights events."""
self._light.register_async_callback(self.async_update_callback)
self.hass.data[DATA_DECONZ].deconz_ids[self.entity_id] = \
self._light.deconz_id
self.gateway.deconz_ids[self.entity_id] = self._light.deconz_id
self.unsub_dispatcher = async_dispatcher_connect(
self.hass, DECONZ_REACHABLE, self.async_update_callback)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect light object when removed."""
if self.unsub_dispatcher is not None:
self.unsub_dispatcher()
self._light.remove_callback(self.async_update_callback)
self._light = None
@ -141,7 +148,7 @@ class DeconzLight(Light):
@property
def available(self):
"""Return True if light is available."""
return self._light.reachable
return self.gateway.available and self._light.reachable
@property
def should_poll(self):
@ -214,7 +221,7 @@ class DeconzLight(Light):
self._light.uniqueid.count(':') != 7):
return None
serial = self._light.uniqueid.split('-', 1)[0]
bridgeid = self.hass.data[DATA_DECONZ].api.config.bridgeid
bridgeid = self.gateway.api.config.bridgeid
return {
'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)},

View File

@ -4,7 +4,7 @@ Support for deCONZ scenes.
For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/scene.deconz/
"""
from homeassistant.components.deconz import DOMAIN as DATA_DECONZ
from homeassistant.components.deconz import DOMAIN as DECONZ_DOMAIN
from homeassistant.components.scene import Scene
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -20,30 +20,32 @@ async def async_setup_platform(hass, config, async_add_entities,
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up scenes for deCONZ component."""
gateway = hass.data[DECONZ_DOMAIN]
@callback
def async_add_scene(scenes):
"""Add scene from deCONZ."""
entities = []
for scene in scenes:
entities.append(DeconzScene(scene))
entities.append(DeconzScene(scene, gateway))
async_add_entities(entities)
hass.data[DATA_DECONZ].listeners.append(
gateway.listeners.append(
async_dispatcher_connect(hass, 'deconz_new_scene', async_add_scene))
async_add_scene(hass.data[DATA_DECONZ].api.scenes.values())
async_add_scene(gateway.api.scenes.values())
class DeconzScene(Scene):
"""Representation of a deCONZ scene."""
def __init__(self, scene):
def __init__(self, scene, gateway):
"""Set up a scene."""
self._scene = scene
self.gateway = gateway
async def async_added_to_hass(self):
"""Subscribe to sensors events."""
self.hass.data[DATA_DECONZ].deconz_ids[self.entity_id] = \
self._scene.deconz_id
self.gateway.deconz_ids[self.entity_id] = self._scene.deconz_id
async def async_will_remove_from_hass(self) -> None:
"""Disconnect scene object when removed."""

View File

@ -5,8 +5,8 @@ For more details about this component, please refer to the documentation at
https://home-assistant.io/components/sensor.deconz/
"""
from homeassistant.components.deconz.const import (
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ,
DECONZ_DOMAIN)
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DECONZ_REACHABLE,
DOMAIN as DECONZ_DOMAIN)
from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY)
from homeassistant.core import callback
@ -30,6 +30,8 @@ async def async_setup_platform(hass, config, async_add_entities,
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ sensors."""
gateway = hass.data[DECONZ_DOMAIN]
@callback
def async_add_sensor(sensors):
"""Add sensors from deCONZ."""
@ -41,32 +43,37 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
not (not allow_clip_sensor and sensor.type.startswith('CLIP')):
if sensor.type in DECONZ_REMOTE:
if sensor.battery:
entities.append(DeconzBattery(sensor))
entities.append(DeconzBattery(sensor, gateway))
else:
entities.append(DeconzSensor(sensor))
entities.append(DeconzSensor(sensor, gateway))
async_add_entities(entities, True)
hass.data[DATA_DECONZ].listeners.append(
gateway.listeners.append(
async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor))
async_add_sensor(hass.data[DATA_DECONZ].api.sensors.values())
async_add_sensor(gateway.api.sensors.values())
class DeconzSensor(Entity):
"""Representation of a sensor."""
def __init__(self, sensor):
def __init__(self, sensor, gateway):
"""Set up sensor and add update callback to get data from websocket."""
self._sensor = sensor
self.gateway = gateway
self.unsub_dispatcher = None
async def async_added_to_hass(self):
"""Subscribe to sensors events."""
self._sensor.register_async_callback(self.async_update_callback)
self.hass.data[DATA_DECONZ].deconz_ids[self.entity_id] = \
self._sensor.deconz_id
self.gateway.deconz_ids[self.entity_id] = self._sensor.deconz_id
self.unsub_dispatcher = async_dispatcher_connect(
self.hass, DECONZ_REACHABLE, self.async_update_callback)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect sensor object when removed."""
if self.unsub_dispatcher is not None:
self.unsub_dispatcher()
self._sensor.remove_callback(self.async_update_callback)
self._sensor = None
@ -116,7 +123,7 @@ class DeconzSensor(Entity):
@property
def available(self):
"""Return true if sensor is available."""
return self._sensor.reachable
return self.gateway.available and self._sensor.reachable
@property
def should_poll(self):
@ -148,7 +155,7 @@ class DeconzSensor(Entity):
self._sensor.uniqueid.count(':') != 7):
return None
serial = self._sensor.uniqueid.split('-', 1)[0]
bridgeid = self.hass.data[DATA_DECONZ].api.config.bridgeid
bridgeid = self.gateway.api.config.bridgeid
return {
'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)},
@ -163,20 +170,26 @@ class DeconzSensor(Entity):
class DeconzBattery(Entity):
"""Battery class for when a device is only represented as an event."""
def __init__(self, sensor):
def __init__(self, sensor, gateway):
"""Register dispatcher callback for update of battery state."""
self._sensor = sensor
self.gateway = gateway
self.unsub_dispatcher = None
self._name = '{} {}'.format(self._sensor.name, 'Battery Level')
self._unit_of_measurement = "%"
async def async_added_to_hass(self):
"""Subscribe to sensors events."""
self._sensor.register_async_callback(self.async_update_callback)
self.hass.data[DATA_DECONZ].deconz_ids[self.entity_id] = \
self._sensor.deconz_id
self.gateway.deconz_ids[self.entity_id] = self._sensor.deconz_id
self.unsub_dispatcher = async_dispatcher_connect(
self.hass, DECONZ_REACHABLE, self.async_update_callback)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect sensor object when removed."""
if self.unsub_dispatcher is not None:
self.unsub_dispatcher()
self._sensor.remove_callback(self.async_update_callback)
self._sensor = None
@ -211,6 +224,11 @@ class DeconzBattery(Entity):
"""Return the unit of measurement of this entity."""
return self._unit_of_measurement
@property
def available(self):
"""Return true if sensor is available."""
return self.gateway.available and self._sensor.reachable
@property
def should_poll(self):
"""No polling needed."""
@ -231,7 +249,7 @@ class DeconzBattery(Entity):
self._sensor.uniqueid.count(':') != 7):
return None
serial = self._sensor.uniqueid.split('-', 1)[0]
bridgeid = self.hass.data[DATA_DECONZ].api.config.bridgeid
bridgeid = self.gateway.api.config.bridgeid
return {
'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)},

View File

@ -5,7 +5,7 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/switch.deconz/
"""
from homeassistant.components.deconz.const import (
DOMAIN as DATA_DECONZ, DECONZ_DOMAIN, POWER_PLUGS, SIRENS)
DECONZ_REACHABLE, DOMAIN as DECONZ_DOMAIN, POWER_PLUGS, SIRENS)
from homeassistant.components.switch import SwitchDevice
from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
@ -25,38 +25,45 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
Switches are based same device class as lights in deCONZ.
"""
gateway = hass.data[DECONZ_DOMAIN]
@callback
def async_add_switch(lights):
"""Add switch from deCONZ."""
entities = []
for light in lights:
if light.type in POWER_PLUGS:
entities.append(DeconzPowerPlug(light))
entities.append(DeconzPowerPlug(light, gateway))
elif light.type in SIRENS:
entities.append(DeconzSiren(light))
entities.append(DeconzSiren(light, gateway))
async_add_entities(entities, True)
hass.data[DATA_DECONZ].listeners.append(
gateway.listeners.append(
async_dispatcher_connect(hass, 'deconz_new_light', async_add_switch))
async_add_switch(hass.data[DATA_DECONZ].api.lights.values())
async_add_switch(gateway.api.lights.values())
class DeconzSwitch(SwitchDevice):
"""Representation of a deCONZ switch."""
def __init__(self, switch):
def __init__(self, switch, gateway):
"""Set up switch and add update callback to get data from websocket."""
self._switch = switch
self.gateway = gateway
self.unsub_dispatcher = None
async def async_added_to_hass(self):
"""Subscribe to switches events."""
self._switch.register_async_callback(self.async_update_callback)
self.hass.data[DATA_DECONZ].deconz_ids[self.entity_id] = \
self._switch.deconz_id
self.gateway.deconz_ids[self.entity_id] = self._switch.deconz_id
self.unsub_dispatcher = async_dispatcher_connect(
self.hass, DECONZ_REACHABLE, self.async_update_callback)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect switch object when removed."""
if self.unsub_dispatcher is not None:
self.unsub_dispatcher()
self._switch.remove_callback(self.async_update_callback)
self._switch = None
@ -78,7 +85,7 @@ class DeconzSwitch(SwitchDevice):
@property
def available(self):
"""Return True if light is available."""
return self._switch.reachable
return self.gateway.available and self._switch.reachable
@property
def should_poll(self):
@ -92,7 +99,7 @@ class DeconzSwitch(SwitchDevice):
self._switch.uniqueid.count(':') != 7):
return None
serial = self._switch.uniqueid.split('-', 1)[0]
bridgeid = self.hass.data[DATA_DECONZ].api.config.bridgeid
bridgeid = self.gateway.api.config.bridgeid
return {
'connections': {(CONNECTION_ZIGBEE, serial)},
'identifiers': {(DECONZ_DOMAIN, serial)},