Track deCONZ lib changes to light based devices (#43366)

* Improve control of covers

* Log backtrace if available

* Do not create entity for controller tool
Binary sensor should use state rather than is_tripped
Add some more tests to lights and sensors

* Bump dependency to v74

* Fix Balloobs comments
pull/43560/head
Robert Svensson 2020-11-23 11:37:11 +01:00 committed by GitHub
parent 3b105c415b
commit 55cbd5aa0d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 206 additions and 52 deletions

View File

@ -84,7 +84,7 @@ class DeconzBinarySensor(DeconzDevice, BinarySensorEntity):
@property
def is_on(self):
"""Return true if sensor is on."""
return self._device.is_tripped
return self._device.state
@property
def device_class(self):

View File

@ -18,10 +18,7 @@ from .gateway import get_gateway_from_config_entry
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up covers for deCONZ component.
Covers are based on the same device class as lights in deCONZ.
"""
"""Set up covers for deCONZ component."""
gateway = get_gateway_from_config_entry(hass, config_entry)
gateway.entities[DOMAIN] = set()
@ -66,12 +63,12 @@ class DeconzCover(DeconzDevice, CoverEntity):
@property
def current_cover_position(self):
"""Return the current position of the cover."""
return 100 - int(self._device.brightness / 254 * 100)
return 100 - self._device.position
@property
def is_closed(self):
"""Return if the cover is closed."""
return self._device.state
return not self._device.is_open
@property
def device_class(self):
@ -88,26 +85,16 @@ class DeconzCover(DeconzDevice, CoverEntity):
async def async_set_cover_position(self, **kwargs):
"""Move the cover to a specific position."""
position = kwargs[ATTR_POSITION]
data = {"on": False}
if position < 100:
data["on"] = True
data["bri"] = 254 - int(position / 100 * 254)
await self._device.async_set_state(data)
await self._device.set_position(kwargs[ATTR_POSITION])
async def async_open_cover(self, **kwargs):
"""Open cover."""
data = {ATTR_POSITION: 100}
await self.async_set_cover_position(**data)
await self._device.open()
async def async_close_cover(self, **kwargs):
"""Close cover."""
data = {ATTR_POSITION: 0}
await self.async_set_cover_position(**data)
await self._device.close()
async def async_stop_cover(self, **kwargs):
"""Stop cover."""
data = {"bri_inc": 0}
await self._device.async_set_state(data)
await self._device.stop()

View File

@ -32,10 +32,7 @@ def convert_speed(speed: int) -> str:
async def async_setup_entry(hass, config_entry, async_add_entities) -> None:
"""Set up fans for deCONZ component.
Fans are based on the same device class as lights in deCONZ.
"""
"""Set up fans for deCONZ component."""
gateway = get_gateway_from_config_entry(hass, config_entry)
gateway.entities[DOMAIN] = set()
@ -108,9 +105,7 @@ class DeconzFan(DeconzDevice, FanEntity):
if speed not in SPEEDS:
raise ValueError(f"Unsupported speed {speed}")
data = {"speed": SPEEDS[speed]}
await self._device.async_set_state(data)
await self._device.set_speed(SPEEDS[speed])
async def async_turn_on(self, speed: str = None, **kwargs) -> None:
"""Turn on fan."""

View File

@ -171,7 +171,7 @@ class DeconzGateway:
raise ConfigEntryNotReady from err
except Exception as err: # pylint: disable=broad-except
LOGGER.error("Error connecting with deCONZ gateway: %s", err)
LOGGER.error("Error connecting with deCONZ gateway: %s", err, exc_info=True)
return False
for component in SUPPORTED_PLATFORMS:

View File

@ -34,12 +34,16 @@ from .const import (
from .deconz_device import DeconzDevice
from .gateway import get_gateway_from_config_entry
CONTROLLER = ["Configuration tool"]
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)
gateway.entities[DOMAIN] = set()
other_light_resource_types = CONTROLLER + COVER_TYPES + LOCK_TYPES + SWITCH_TYPES
@callback
def async_add_light(lights):
"""Add light from deCONZ."""
@ -47,7 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities):
for light in lights:
if (
light.type not in COVER_TYPES + LOCK_TYPES + SWITCH_TYPES
light.type not in other_light_resource_types
and light.uniqueid not in gateway.entities[DOMAIN]
):
entities.append(DeconzLight(light, gateway))

View File

@ -9,10 +9,7 @@ from .gateway import get_gateway_from_config_entry
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up locks for deCONZ component.
Locks are based on the same device class as lights in deCONZ.
"""
"""Set up locks for deCONZ component."""
gateway = get_gateway_from_config_entry(hass, config_entry)
gateway.entities[DOMAIN] = set()
@ -46,14 +43,12 @@ class DeconzLock(DeconzDevice, LockEntity):
@property
def is_locked(self):
"""Return true if lock is on."""
return self._device.state
return self._device.is_locked
async def async_lock(self, **kwargs):
"""Lock the lock."""
data = {"on": True}
await self._device.async_set_state(data)
await self._device.lock()
async def async_unlock(self, **kwargs):
"""Unlock the lock."""
data = {"on": False}
await self._device.async_set_state(data)
await self._device.unlock()

View File

@ -3,7 +3,7 @@
"name": "deCONZ",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/deconz",
"requirements": ["pydeconz==73"],
"requirements": ["pydeconz==74"],
"ssdp": [
{
"manufacturer": "Royal Philips Electronics"

View File

@ -75,14 +75,12 @@ class DeconzSiren(DeconzDevice, SwitchEntity):
@property
def is_on(self):
"""Return true if switch is on."""
return self._device.alert == "lselect"
return self._device.is_on
async def async_turn_on(self, **kwargs):
"""Turn on switch."""
data = {"alert": "lselect"}
await self._device.async_set_state(data)
await self._device.turn_on()
async def async_turn_off(self, **kwargs):
"""Turn off switch."""
data = {"alert": "none"}
await self._device.async_set_state(data)
await self._device.turn_off()

View File

@ -1334,7 +1334,7 @@ pydaikin==2.3.1
pydanfossair==0.1.0
# homeassistant.components.deconz
pydeconz==73
pydeconz==74
# homeassistant.components.delijn
pydelijn==0.6.1

View File

@ -667,7 +667,7 @@ pycountry==19.8.18
pydaikin==2.3.1
# homeassistant.components.deconz
pydeconz==73
pydeconz==74
# homeassistant.components.dexcom
pydexcom==0.2.0

View File

@ -40,7 +40,7 @@ SENSORS = {
"id": "CLIP presence sensor id",
"name": "CLIP presence sensor",
"type": "CLIPPresence",
"state": {},
"state": {"presence": False},
"config": {},
"uniqueid": "00:00:00:00:00:00:00:02-00",
},

View File

@ -3,9 +3,11 @@
from copy import deepcopy
from homeassistant.components.cover import (
ATTR_POSITION,
DOMAIN as COVER_DOMAIN,
SERVICE_CLOSE_COVER,
SERVICE_OPEN_COVER,
SERVICE_SET_COVER_POSITION,
SERVICE_STOP_COVER,
)
from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN
@ -30,7 +32,7 @@ COVERS = {
"id": "Window covering device id",
"name": "Window covering device",
"type": "Window covering device",
"state": {"bri": 254, "on": True, "reachable": True},
"state": {"lift": 100, "open": False, "reachable": True},
"modelid": "lumi.curtain",
"uniqueid": "00:00:00:00:00:00:00:01-00",
},
@ -105,7 +107,67 @@ async def test_cover(hass):
assert hass.states.get("cover.level_controllable_cover").state == STATE_CLOSED
# Verify service calls
# Verify service calls for cover
windows_covering_device = gateway.api.lights["2"]
# Service open cover
with patch.object(
windows_covering_device, "_request", return_value=True
) as set_callback:
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_OPEN_COVER,
{ATTR_ENTITY_ID: "cover.window_covering_device"},
blocking=True,
)
await hass.async_block_till_done()
set_callback.assert_called_with("put", "/lights/2/state", json={"open": True})
# Service close cover
with patch.object(
windows_covering_device, "_request", return_value=True
) as set_callback:
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_CLOSE_COVER,
{ATTR_ENTITY_ID: "cover.window_covering_device"},
blocking=True,
)
await hass.async_block_till_done()
set_callback.assert_called_with("put", "/lights/2/state", json={"open": False})
# Service set cover position
with patch.object(
windows_covering_device, "_request", return_value=True
) as set_callback:
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_SET_COVER_POSITION,
{ATTR_ENTITY_ID: "cover.window_covering_device", ATTR_POSITION: 50},
blocking=True,
)
await hass.async_block_till_done()
set_callback.assert_called_with("put", "/lights/2/state", json={"lift": 50})
# Service stop cover movement
with patch.object(
windows_covering_device, "_request", return_value=True
) as set_callback:
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_STOP_COVER,
{ATTR_ENTITY_ID: "cover.window_covering_device"},
blocking=True,
)
await hass.async_block_till_done()
set_callback.assert_called_with("put", "/lights/2/state", json={"bri_inc": 0})
# Verify service calls for legacy cover
level_controllable_cover_device = gateway.api.lights["1"]
@ -135,9 +197,21 @@ async def test_cover(hass):
blocking=True,
)
await hass.async_block_till_done()
set_callback.assert_called_with(
"put", "/lights/1/state", json={"on": True, "bri": 254}
set_callback.assert_called_with("put", "/lights/1/state", json={"on": True})
# Service set cover position
with patch.object(
level_controllable_cover_device, "_request", return_value=True
) as set_callback:
await hass.services.async_call(
COVER_DOMAIN,
SERVICE_SET_COVER_POSITION,
{ATTR_ENTITY_ID: "cover.level_controllable_cover", ATTR_POSITION: 50},
blocking=True,
)
await hass.async_block_till_done()
set_callback.assert_called_with("put", "/lights/1/state", json={"bri": 127})
# Service stop cover movement

View File

@ -330,3 +330,26 @@ async def test_disable_light_groups(hass):
assert len(hass.states.async_all()) == 5
assert hass.states.get("light.light_group") is None
async def test_configuration_tool(hass):
"""Test that lights or groups entities are created."""
data = deepcopy(DECONZ_WEB_REQUEST)
data["lights"] = {
"0": {
"etag": "26839cb118f5bf7ba1f2108256644010",
"hascolor": False,
"lastannounced": None,
"lastseen": "2020-11-22T11:27Z",
"manufacturername": "dresden elektronik",
"modelid": "ConBee II",
"name": "Configuration tool 1",
"state": {"reachable": True},
"swversion": "0x264a0700",
"type": "Configuration tool",
"uniqueid": "00:21:2e:ff:ff:05:a7:a3-01",
}
}
await setup_deconz_integration(hass, get_state_response=data)
assert len(hass.states.async_all()) == 0

View File

@ -242,3 +242,81 @@ async def test_add_battery_later(hass):
assert len(remote._callbacks) == 2 # Event and battery entity
assert hass.states.get("sensor.switch_1_battery_level")
async def test_air_quality_sensor(hass):
"""Test successful creation of air quality sensor entities."""
data = deepcopy(DECONZ_WEB_REQUEST)
data["sensors"] = {
"0": {
"config": {"on": True, "reachable": True},
"ep": 2,
"etag": "c2d2e42396f7c78e11e46c66e2ec0200",
"lastseen": "2020-11-20T22:48Z",
"manufacturername": "BOSCH",
"modelid": "AIR",
"name": "Air quality",
"state": {
"airquality": "poor",
"airqualityppb": 809,
"lastupdated": "2020-11-20T22:48:00.209",
},
"swversion": "20200402",
"type": "ZHAAirQuality",
"uniqueid": "00:12:4b:00:14:4d:00:07-02-fdef",
}
}
await setup_deconz_integration(hass, get_state_response=data)
assert len(hass.states.async_all()) == 1
air_quality = hass.states.get("sensor.air_quality")
assert air_quality.state == "poor"
async def test_time_sensor(hass):
"""Test successful creation of time sensor entities."""
data = deepcopy(DECONZ_WEB_REQUEST)
data["sensors"] = {
"0": {
"config": {"battery": 40, "on": True, "reachable": True},
"ep": 1,
"etag": "28e796678d9a24712feef59294343bb6",
"lastseen": "2020-11-22T11:26Z",
"manufacturername": "Danfoss",
"modelid": "eTRV0100",
"name": "Time",
"state": {
"lastset": "2020-11-19T08:07:08Z",
"lastupdated": "2020-11-22T10:51:03.444",
"localtime": "2020-11-22T10:51:01",
"utc": "2020-11-22T10:51:01Z",
},
"swversion": "20200429",
"type": "ZHATime",
"uniqueid": "cc:cc:cc:ff:fe:38:4d:b3-01-000a",
}
}
await setup_deconz_integration(hass, get_state_response=data)
assert len(hass.states.async_all()) == 2
time = hass.states.get("sensor.time")
assert time.state == "2020-11-19T08:07:08Z"
time_battery = hass.states.get("sensor.time_battery_level")
assert time_battery.state == "40"
async def test_unsupported_sensor(hass):
"""Test that unsupported sensors doesn't break anything."""
data = deepcopy(DECONZ_WEB_REQUEST)
data["sensors"] = {
"0": {"type": "not supported", "name": "name", "state": {}, "config": {}}
}
await setup_deconz_integration(hass, get_state_response=data)
assert len(hass.states.async_all()) == 1
unsupported_sensor = hass.states.get("sensor.name")
assert unsupported_sensor.state == "unknown"