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 commentspull/43560/head
parent
3b105c415b
commit
55cbd5aa0d
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue