Library refactorization of deCONZ (#23725)

* Improved sensors

* Lib update signalling

* Replace reason with changed

* Move imports to top of file

* Add support for secondary temperature reported by some Xiaomi devices

* Bump dependency to v59
This commit is contained in:
Robert Svensson 2019-05-27 06:56:00 +02:00 committed by GitHub
parent 0ba54ee9b7
commit 31b2f331db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 104 additions and 83 deletions

View file

@ -164,6 +164,7 @@ async def async_unload_entry(hass, config_entry):
if not hass.data[DOMAIN]:
hass.services.async_remove(DOMAIN, SERVICE_DECONZ)
hass.services.async_remove(DOMAIN, SERVICE_DEVICE_REFRESH)
elif gateway.master:
await async_populate_options(hass, config_entry)
new_master_gateway = next(iter(hass.data[DOMAIN].values()))

View file

@ -1,6 +1,8 @@
"""Support for deCONZ binary sensors."""
from pydeconz.sensor import Presence, Vibration
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
@ -15,7 +17,7 @@ ATTR_VIBRATIONSTRENGTH = 'vibrationstrength'
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ binary sensors."""
"""Old way of setting up deCONZ platforms."""
pass
@ -26,12 +28,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
@callback
def async_add_sensor(sensors):
"""Add binary sensor from deCONZ."""
from pydeconz.sensor import DECONZ_BINARY_SENSOR
entities = []
for sensor in sensors:
if sensor.type in DECONZ_BINARY_SENSOR and \
if sensor.BINARY and \
not (not gateway.allow_clip_sensor and
sensor.type.startswith('CLIP')):
@ -49,16 +50,11 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice):
"""Representation of a deCONZ binary sensor."""
@callback
def async_update_callback(self, reason):
"""Update the sensor's state.
If reason is that state is updated,
or reachable has changed or battery has changed.
"""
if reason['state'] or \
'reachable' in reason['attr'] or \
'battery' in reason['attr'] or \
'on' in reason['attr']:
def async_update_callback(self, force_update=False):
"""Update the sensor's state."""
changed = set(self._device.changed_keys)
keys = {'battery', 'on', 'reachable', 'state'}
if force_update or any(key in changed for key in keys):
self.async_schedule_update_ha_state()
@property
@ -69,26 +65,33 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorDevice):
@property
def device_class(self):
"""Return the class of the sensor."""
return self._device.sensor_class
return self._device.SENSOR_CLASS
@property
def icon(self):
"""Return the icon to use in the frontend."""
return self._device.sensor_icon
return self._device.SENSOR_ICON
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
from pydeconz.sensor import PRESENCE, VIBRATION
attr = {}
if self._device.battery:
attr[ATTR_BATTERY_LEVEL] = self._device.battery
if self._device.on is not None:
attr[ATTR_ON] = self._device.on
if self._device.type in PRESENCE and self._device.dark is not None:
if self._device.secondary_temperature is not None:
attr[ATTR_TEMPERATURE] = self._device.secondary_temperature
if self._device.type in Presence.ZHATYPE and \
self._device.dark is not None:
attr[ATTR_DARK] = self._device.dark
elif self._device.type in VIBRATION:
elif self._device.type in Vibration.ZHATYPE:
attr[ATTR_ORIENTATION] = self._device.orientation
attr[ATTR_TILTANGLE] = self._device.tiltangle
attr[ATTR_VIBRATIONSTRENGTH] = self._device.vibrationstrength
return attr

View file

@ -1,4 +1,6 @@
"""Support for deCONZ climate devices."""
from pydeconz.sensor import Thermostat
from homeassistant.components.climate import ClimateDevice
from homeassistant.components.climate.const import (
SUPPORT_ON_OFF, SUPPORT_TARGET_TEMPERATURE)
@ -12,6 +14,12 @@ from .deconz_device import DeconzDevice
from .gateway import get_gateway_from_config_entry
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ platforms."""
pass
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the deCONZ climate devices.
@ -22,12 +30,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
@callback
def async_add_climate(sensors):
"""Add climate devices from deCONZ."""
from pydeconz.sensor import THERMOSTAT
entities = []
for sensor in sensors:
if sensor.type in THERMOSTAT and \
if sensor.type in Thermostat.ZHATYPE and \
not (not gateway.allow_clip_sensor and
sensor.type.startswith('CLIP')):
@ -59,7 +66,7 @@ class DeconzThermostat(DeconzDevice, ClimateDevice):
@property
def is_on(self):
"""Return true if on."""
return self._device.on
return self._device.state_on
async def async_turn_on(self):
"""Turn on switch."""

View file

@ -4,6 +4,10 @@ import asyncio
import async_timeout
import voluptuous as vol
from pydeconz.errors import ResponseError, RequestError
from pydeconz.utils import (
async_discovery, async_get_api_key, async_get_bridgeid)
from homeassistant import config_entries
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT
from homeassistant.core import callback
@ -54,8 +58,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
If more than one bridge is found let user choose bridge to link.
If no bridge is found allow user to manually input configuration.
"""
from pydeconz.utils import async_discovery
if user_input is not None:
for bridge in self.bridges:
if bridge[CONF_HOST] == user_input[CONF_HOST]:
@ -101,8 +103,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
async def async_step_link(self, user_input=None):
"""Attempt to link with the deCONZ bridge."""
from pydeconz.errors import ResponseError, RequestError
from pydeconz.utils import async_get_api_key
errors = {}
if user_input is not None:
@ -127,8 +127,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow):
async def _create_entry(self):
"""Create entry for gateway."""
from pydeconz.utils import async_get_bridgeid
if CONF_BRIDGEID not in self.deconz_config:
session = aiohttp_client.async_get_clientsession(self.hass)

View file

@ -14,7 +14,7 @@ ZIGBEE_SPEC = ['lumi.curtain']
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Unsupported way of setting up deCONZ covers."""
"""Old way of setting up deCONZ platforms."""
pass

View file

@ -31,7 +31,7 @@ class DeconzDevice(Entity):
self.unsub_dispatcher()
@callback
def async_update_callback(self, reason):
def async_update_callback(self, force_update=False):
"""Update the device's state."""
self.async_schedule_update_ha_state()

View file

@ -2,6 +2,9 @@
import asyncio
import async_timeout
from pydeconz import DeconzSession, errors
from pydeconz.sensor import Switch
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.const import CONF_EVENT, CONF_HOST, CONF_ID
from homeassistant.core import EventOrigin, callback
@ -126,8 +129,7 @@ class DeconzGateway:
def async_connection_status_callback(self, available):
"""Handle signals of gateway connection status."""
self.available = available
async_dispatcher_send(self.hass, self.event_reachable,
{'state': True, 'attr': 'reachable'})
async_dispatcher_send(self.hass, self.event_reachable, True)
@callback
def async_event_new_device(self, device_type):
@ -145,9 +147,8 @@ class DeconzGateway:
@callback
def async_add_remote(self, sensors):
"""Set up remote from deCONZ."""
from pydeconz.sensor import SWITCH as DECONZ_REMOTE
for sensor in sensors:
if sensor.type in DECONZ_REMOTE and \
if sensor.type in Switch.ZHATYPE and \
not (not self.allow_clip_sensor and
sensor.type.startswith('CLIP')):
self.events.append(DeconzEvent(self.hass, sensor))
@ -187,8 +188,6 @@ class DeconzGateway:
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, errors
session = aiohttp_client.async_get_clientsession(hass)
deconz = DeconzSession(hass.loop, session, **config,
@ -232,8 +231,8 @@ class DeconzEvent:
self._device = None
@callback
def async_update_callback(self, reason):
def async_update_callback(self, force_update=False):
"""Fire the event if reason is that state is updated."""
if reason['state']:
if 'state' in self._device.changed_keys:
data = {CONF_ID: self._id, CONF_EVENT: self._device.state}
self._hass.bus.async_fire(self._event, data, EventOrigin.remote)

View file

@ -15,7 +15,7 @@ from .gateway import get_gateway_from_config_entry
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ lights and group."""
"""Old way of setting up deCONZ platforms."""
pass

View file

@ -4,7 +4,7 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/components/deconz",
"requirements": [
"pydeconz==58"
"pydeconz==59"
],
"dependencies": [],
"codeowners": [

View file

@ -9,7 +9,7 @@ from .gateway import get_gateway_from_config_entry
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ scenes."""
"""Old way of setting up deCONZ platforms."""
pass

View file

@ -1,6 +1,8 @@
"""Support for deCONZ sensors."""
from pydeconz.sensor import LightLevel, Switch
from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY)
ATTR_BATTERY_LEVEL, ATTR_TEMPERATURE, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY)
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util import slugify
@ -16,7 +18,7 @@ ATTR_EVENT_ID = 'event_id'
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ sensors."""
"""Old way of setting up deCONZ platforms."""
pass
@ -27,17 +29,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
@callback
def async_add_sensor(sensors):
"""Add sensors from deCONZ."""
from pydeconz.sensor import (
DECONZ_SENSOR, SWITCH as DECONZ_REMOTE)
entities = []
for sensor in sensors:
if sensor.type in DECONZ_SENSOR and \
if not sensor.BINARY and \
not (not gateway.allow_clip_sensor and
sensor.type.startswith('CLIP')):
if sensor.type in DECONZ_REMOTE:
if sensor.type in Switch.ZHATYPE:
if sensor.battery:
entities.append(DeconzBattery(sensor, gateway))
@ -56,16 +56,11 @@ class DeconzSensor(DeconzDevice):
"""Representation of a deCONZ sensor."""
@callback
def async_update_callback(self, reason):
"""Update the sensor's state.
If reason is that state is updated,
or reachable has changed or battery has changed.
"""
if reason['state'] or \
'reachable' in reason['attr'] or \
'battery' in reason['attr'] or \
'on' in reason['attr']:
def async_update_callback(self, force_update=False):
"""Update the sensor's state."""
changed = set(self._device.changed_keys)
keys = {'battery', 'on', 'reachable', 'state'}
if force_update or any(key in changed for key in keys):
self.async_schedule_update_ha_state()
@property
@ -76,34 +71,42 @@ class DeconzSensor(DeconzDevice):
@property
def device_class(self):
"""Return the class of the sensor."""
return self._device.sensor_class
return self._device.SENSOR_CLASS
@property
def icon(self):
"""Return the icon to use in the frontend."""
return self._device.sensor_icon
return self._device.SENSOR_ICON
@property
def unit_of_measurement(self):
"""Return the unit of measurement of this sensor."""
return self._device.sensor_unit
return self._device.SENSOR_UNIT
@property
def device_state_attributes(self):
"""Return the state attributes of the sensor."""
from pydeconz.sensor import LIGHTLEVEL
attr = {}
if self._device.battery:
attr[ATTR_BATTERY_LEVEL] = self._device.battery
if self._device.on is not None:
attr[ATTR_ON] = self._device.on
if self._device.type in LIGHTLEVEL and self._device.dark is not None:
if self._device.secondary_temperature is not None:
attr[ATTR_TEMPERATURE] = self._device.secondary_temperature
if self._device.type in LightLevel.ZHATYPE and \
self._device.dark is not None:
attr[ATTR_DARK] = self._device.dark
if self.unit_of_measurement == 'Watts':
attr[ATTR_CURRENT] = self._device.current
attr[ATTR_VOLTAGE] = self._device.voltage
if self._device.sensor_class == 'daylight':
if self._device.SENSOR_CLASS == 'daylight':
attr[ATTR_DAYLIGHT] = self._device.daylight
return attr
@ -118,9 +121,11 @@ class DeconzBattery(DeconzDevice):
self._unit_of_measurement = "%"
@callback
def async_update_callback(self, reason):
def async_update_callback(self, force_update=False):
"""Update the battery's state, if needed."""
if 'reachable' in reason['attr'] or 'battery' in reason['attr']:
changed = set(self._device.changed_keys)
keys = {'battery', 'reachable'}
if force_update or any(key in changed for key in keys):
self.async_schedule_update_ha_state()
@property

View file

@ -10,7 +10,7 @@ from .gateway import get_gateway_from_config_entry
async def async_setup_platform(
hass, config, async_add_entities, discovery_info=None):
"""Old way of setting up deCONZ switches."""
"""Old way of setting up deCONZ platforms."""
pass

View file

@ -1048,7 +1048,7 @@ pydaikin==1.4.6
pydanfossair==0.1.0
# homeassistant.components.deconz
pydeconz==58
pydeconz==59
# homeassistant.components.zwave
pydispatcher==2.0.5

View file

@ -230,7 +230,7 @@ pyHS100==0.3.5
pyblackbird==0.5
# homeassistant.components.deconz
pydeconz==58
pydeconz==59
# homeassistant.components.zwave
pydispatcher==2.0.5

View file

@ -1,6 +1,8 @@
"""deCONZ binary sensor platform tests."""
from unittest.mock import Mock, patch
from tests.common import mock_coro
from homeassistant import config_entries
from homeassistant.components import deconz
from homeassistant.helpers.dispatcher import async_dispatcher_send
@ -8,8 +10,6 @@ from homeassistant.setup import async_setup_component
import homeassistant.components.binary_sensor as binary_sensor
from tests.common import mock_coro
SENSOR = {
"1": {
@ -104,6 +104,7 @@ async def test_add_new_sensor(hass):
sensor = Mock()
sensor.name = 'name'
sensor.type = 'ZHAPresence'
sensor.BINARY = True
sensor.register_async_callback = Mock()
async_dispatcher_send(
hass, gateway.async_event_new_device('sensor'), [sensor])

View file

@ -1,4 +1,5 @@
"""deCONZ climate platform tests."""
from copy import deepcopy
from unittest.mock import Mock, patch
import asynctest
@ -18,9 +19,9 @@ SENSOR = {
"id": "Climate 1 id",
"name": "Climate 1 name",
"type": "ZHAThermostat",
"state": {"on": True, "temperature": 2260},
"state": {"on": True, "temperature": 2260, "valve": 30},
"config": {"battery": 100, "heatsetpoint": 2200, "mode": "auto",
"offset": 10, "reachable": True, "valve": 30},
"offset": 10, "reachable": True},
"uniqueid": "00:00:00:00:00:00:00:00-00"
},
"2": {
@ -97,7 +98,7 @@ async def test_no_sensors(hass):
async def test_climate_devices(hass):
"""Test successful creation of sensor entities."""
gateway = await setup_gateway(hass, {"sensors": SENSOR})
gateway = await setup_gateway(hass, {"sensors": deepcopy(SENSOR)})
assert "climate.climate_1_name" in gateway.deconz_ids
assert "sensor.sensor_2_name" not in gateway.deconz_ids
assert len(hass.states.async_all()) == 1
@ -138,7 +139,7 @@ async def test_climate_devices(hass):
async def test_verify_state_update(hass):
"""Test that state update properly."""
gateway = await setup_gateway(hass, {"sensors": SENSOR})
gateway = await setup_gateway(hass, {"sensors": deepcopy(SENSOR)})
assert "climate.climate_1_name" in gateway.deconz_ids
thermostat = hass.states.get('climate.climate_1_name')
@ -149,7 +150,7 @@ async def test_verify_state_update(hass):
"e": "changed",
"r": "sensors",
"id": "1",
"config": {"on": False}
"state": {"on": False}
}
gateway.api.async_event_handler(state_update)
@ -158,6 +159,8 @@ async def test_verify_state_update(hass):
thermostat = hass.states.get('climate.climate_1_name')
assert thermostat.state == 'off'
assert gateway.api.sensors['1'].changed_keys == \
{'state', 'r', 't', 'on', 'e', 'id'}
async def test_add_new_climate_device(hass):

View file

@ -43,7 +43,7 @@ async def test_flow_works(hass, aioclient_mock):
async def test_user_step_bridge_discovery_fails(hass, aioclient_mock):
"""Test config flow works when discovery fails."""
with patch('pydeconz.utils.async_discovery',
with patch('homeassistant.components.deconz.config_flow.async_discovery',
side_effect=asyncio.TimeoutError):
result = await hass.config_entries.flow.async_init(
config_flow.DOMAIN,
@ -158,8 +158,9 @@ async def test_link_no_api_key(hass):
config_flow.CONF_PORT: 80
}
with patch('pydeconz.utils.async_get_api_key',
side_effect=pydeconz.errors.ResponseError):
with patch(
'homeassistant.components.deconz.config_flow.async_get_api_key',
side_effect=pydeconz.errors.ResponseError):
result = await flow.async_step_link(user_input={})
assert result['type'] == 'form'
@ -275,8 +276,9 @@ async def test_create_entry_timeout(hass, aioclient_mock):
config_flow.CONF_API_KEY: '1234567890ABCDEF'
}
with patch('pydeconz.utils.async_get_bridgeid',
side_effect=asyncio.TimeoutError):
with patch(
'homeassistant.components.deconz.config_flow.async_get_bridgeid',
side_effect=asyncio.TimeoutError):
result = await flow._create_entry()
assert result['type'] == 'abort'

View file

@ -223,7 +223,8 @@ async def test_update_event():
remote.name = 'Name'
event = gateway.DeconzEvent(hass, remote)
event.async_update_callback({'state': True})
remote.changed_keys = {'state': True}
event.async_update_callback()
assert len(hass.bus.async_fire.mock_calls) == 1

View file

@ -1,6 +1,8 @@
"""deCONZ sensor platform tests."""
from unittest.mock import Mock, patch
from tests.common import mock_coro
from homeassistant import config_entries
from homeassistant.components import deconz
from homeassistant.helpers.dispatcher import async_dispatcher_send
@ -8,8 +10,6 @@ from homeassistant.setup import async_setup_component
import homeassistant.components.sensor as sensor
from tests.common import mock_coro
SENSOR = {
"1": {
@ -142,6 +142,7 @@ async def test_add_new_sensor(hass):
sensor = Mock()
sensor.name = 'name'
sensor.type = 'ZHATemperature'
sensor.BINARY = False
sensor.register_async_callback = Mock()
async_dispatcher_send(
hass, gateway.async_event_new_device('sensor'), [sensor])