From 55cbd5aa0d12bfb2a4c8ea9640f2cdd5e7986db6 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 23 Nov 2020 11:37:11 +0100 Subject: [PATCH] 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 --- .../components/deconz/binary_sensor.py | 2 +- homeassistant/components/deconz/cover.py | 27 ++---- homeassistant/components/deconz/fan.py | 9 +- homeassistant/components/deconz/gateway.py | 2 +- homeassistant/components/deconz/light.py | 6 +- homeassistant/components/deconz/lock.py | 13 +-- homeassistant/components/deconz/manifest.json | 2 +- homeassistant/components/deconz/switch.py | 8 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_binary_sensor.py | 2 +- tests/components/deconz/test_cover.py | 82 ++++++++++++++++++- tests/components/deconz/test_light.py | 23 ++++++ tests/components/deconz/test_sensor.py | 78 ++++++++++++++++++ 14 files changed, 206 insertions(+), 52 deletions(-) diff --git a/homeassistant/components/deconz/binary_sensor.py b/homeassistant/components/deconz/binary_sensor.py index 5b536aeb74c..184bce8defc 100644 --- a/homeassistant/components/deconz/binary_sensor.py +++ b/homeassistant/components/deconz/binary_sensor.py @@ -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): diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 5982aead14f..ab5f6e0be9e 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -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() diff --git a/homeassistant/components/deconz/fan.py b/homeassistant/components/deconz/fan.py index 7936a8fead5..69e77befb4f 100644 --- a/homeassistant/components/deconz/fan.py +++ b/homeassistant/components/deconz/fan.py @@ -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.""" diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 475b3c48525..8a9daded289 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -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: diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 3bdf2c67caa..01e36e2ccf7 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -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)) diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index 175d422ea1b..a5b53e86af5 100644 --- a/homeassistant/components/deconz/lock.py +++ b/homeassistant/components/deconz/lock.py @@ -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() diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 6a47864375e..7b8abe82472 100644 --- a/homeassistant/components/deconz/manifest.json +++ b/homeassistant/components/deconz/manifest.json @@ -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" diff --git a/homeassistant/components/deconz/switch.py b/homeassistant/components/deconz/switch.py index 509282e45f2..84bb0f84da1 100644 --- a/homeassistant/components/deconz/switch.py +++ b/homeassistant/components/deconz/switch.py @@ -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() diff --git a/requirements_all.txt b/requirements_all.txt index 918a0bd1122..993806e2a40 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d3ef6b7a0bc..0ec6690d1f5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 5872aee1bf1..5038c5bf3f2 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -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", }, diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 485ae4239be..3e31006438a 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -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 diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 460f81e830c..b971de28d43 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -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 diff --git a/tests/components/deconz/test_sensor.py b/tests/components/deconz/test_sensor.py index 8b2f1e4da76..def2a1412e5 100644 --- a/tests/components/deconz/test_sensor.py +++ b/tests/components/deconz/test_sensor.py @@ -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"