From c80f34a75454610bc337f6b22790e5bf52542308 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Wed, 31 Mar 2021 09:36:06 +0200 Subject: [PATCH] Add support for ZHADoorLock locks to deCONZ integration(#48516) --- homeassistant/components/deconz/const.py | 2 +- homeassistant/components/deconz/lock.py | 30 ++++++- homeassistant/components/deconz/manifest.json | 2 +- homeassistant/components/deconz/sensor.py | 6 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_gateway.py | 4 +- tests/components/deconz/test_lock.py | 85 ++++++++++++++++++- 8 files changed, 120 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 64667a9e616..5ed1def66c2 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -60,7 +60,7 @@ COVER_TYPES = DAMPERS + WINDOW_COVERS FANS = ["Fan"] # Locks -LOCKS = ["Door Lock"] +LOCKS = ["Door Lock", "ZHADoorLock"] LOCK_TYPES = LOCKS # Switches diff --git a/homeassistant/components/deconz/lock.py b/homeassistant/components/deconz/lock.py index 4d428af3673..4b6da1e0b97 100644 --- a/homeassistant/components/deconz/lock.py +++ b/homeassistant/components/deconz/lock.py @@ -3,7 +3,7 @@ from homeassistant.components.lock import DOMAIN, LockEntity from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import LOCKS, NEW_LIGHT +from .const import LOCKS, NEW_LIGHT, NEW_SENSOR from .deconz_device import DeconzDevice from .gateway import get_gateway_from_config_entry @@ -14,7 +14,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway.entities[DOMAIN] = set() @callback - def async_add_lock(lights=gateway.api.lights.values()): + def async_add_lock_from_light(lights=gateway.api.lights.values()): """Add lock from deCONZ.""" entities = [] @@ -28,11 +28,33 @@ async def async_setup_entry(hass, config_entry, async_add_entities): gateway.listeners.append( async_dispatcher_connect( - hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_lock + hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_lock_from_light ) ) - async_add_lock() + @callback + def async_add_lock_from_sensor(sensors=gateway.api.sensors.values()): + """Add lock from deCONZ.""" + entities = [] + + for sensor in sensors: + + if sensor.type in LOCKS and sensor.uniqueid not in gateway.entities[DOMAIN]: + entities.append(DeconzLock(sensor, gateway)) + + if entities: + async_add_entities(entities) + + gateway.listeners.append( + async_dispatcher_connect( + hass, + gateway.async_signal_new_device(NEW_SENSOR), + async_add_lock_from_sensor, + ) + ) + + async_add_lock_from_light() + async_add_lock_from_sensor() class DeconzLock(DeconzDevice, LockEntity): diff --git a/homeassistant/components/deconz/manifest.json b/homeassistant/components/deconz/manifest.json index 22711b84b9d..5cce8858910 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==77"], + "requirements": ["pydeconz==78"], "ssdp": [ { "manufacturer": "Royal Philips Electronics" diff --git a/homeassistant/components/deconz/sensor.py b/homeassistant/components/deconz/sensor.py index 0646ad09a31..a38b7cb20aa 100644 --- a/homeassistant/components/deconz/sensor.py +++ b/homeassistant/components/deconz/sensor.py @@ -3,6 +3,7 @@ from pydeconz.sensor import ( Battery, Consumption, Daylight, + DoorLock, Humidity, LightLevel, Power, @@ -103,7 +104,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if ( not sensor.BINARY and sensor.type - not in Battery.ZHATYPE + Switch.ZHATYPE + Thermostat.ZHATYPE + not in Battery.ZHATYPE + + DoorLock.ZHATYPE + + Switch.ZHATYPE + + Thermostat.ZHATYPE and sensor.uniqueid not in gateway.entities[DOMAIN] ): entities.append(DeconzSensor(sensor, gateway)) diff --git a/requirements_all.txt b/requirements_all.txt index 787e667e89a..999d9f618c9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1337,7 +1337,7 @@ pydaikin==2.4.1 pydanfossair==0.1.0 # homeassistant.components.deconz -pydeconz==77 +pydeconz==78 # homeassistant.components.delijn pydelijn==0.6.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8684d40efbb..50496876386 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -705,7 +705,7 @@ pycoolmasternet-async==0.1.2 pydaikin==2.4.1 # homeassistant.components.deconz -pydeconz==77 +pydeconz==78 # homeassistant.components.dexcom pydexcom==0.2.0 diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 6743ae0696a..603644f47e3 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -4,7 +4,7 @@ from copy import deepcopy from unittest.mock import Mock, patch import pydeconz -from pydeconz.websocket import STATE_RUNNING, STATE_STARTING +from pydeconz.websocket import STATE_RETRYING, STATE_RUNNING import pytest from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN @@ -199,7 +199,7 @@ async def test_connection_status_signalling( assert hass.states.get("binary_sensor.presence").state == STATE_OFF - await mock_deconz_websocket(state=STATE_STARTING) + await mock_deconz_websocket(state=STATE_RETRYING) await hass.async_block_till_done() assert hass.states.get("binary_sensor.presence").state == STATE_UNAVAILABLE diff --git a/tests/components/deconz/test_lock.py b/tests/components/deconz/test_lock.py index 1b4bf3c160c..5aff698149d 100644 --- a/tests/components/deconz/test_lock.py +++ b/tests/components/deconz/test_lock.py @@ -27,8 +27,8 @@ async def test_no_locks(hass, aioclient_mock): assert len(hass.states.async_all()) == 0 -async def test_locks(hass, aioclient_mock, mock_deconz_websocket): - """Test that all supported lock entities are created.""" +async def test_lock_from_light(hass, aioclient_mock, mock_deconz_websocket): + """Test that all supported lock entities based on lights are created.""" data = { "lights": { "1": { @@ -98,3 +98,84 @@ async def test_locks(hass, aioclient_mock, mock_deconz_websocket): await hass.config_entries.async_remove(config_entry.entry_id) await hass.async_block_till_done() assert len(hass.states.async_all()) == 0 + + +async def test_lock_from_sensor(hass, aioclient_mock, mock_deconz_websocket): + """Test that all supported lock entities based on sensors are created.""" + data = { + "sensors": { + "1": { + "config": { + "battery": 100, + "lock": False, + "on": True, + "reachable": True, + }, + "ep": 11, + "etag": "a43862f76b7fa48b0fbb9107df123b0e", + "lastseen": "2021-03-06T22:25Z", + "manufacturername": "Onesti Products AS", + "modelid": "easyCodeTouch_v1", + "name": "Door lock", + "state": { + "lastupdated": "2021-03-06T21:25:45.624", + "lockstate": "unlocked", + }, + "swversion": "20201211", + "type": "ZHADoorLock", + "uniqueid": "00:00:00:00:00:00:00:00-00", + } + } + } + with patch.dict(DECONZ_WEB_REQUEST, data): + config_entry = await setup_deconz_integration(hass, aioclient_mock) + + assert len(hass.states.async_all()) == 2 + assert hass.states.get("lock.door_lock").state == STATE_UNLOCKED + + event_changed_light = { + "t": "event", + "e": "changed", + "r": "sensors", + "id": "1", + "state": {"lockstate": "locked"}, + } + await mock_deconz_websocket(data=event_changed_light) + await hass.async_block_till_done() + + assert hass.states.get("lock.door_lock").state == STATE_LOCKED + + # Verify service calls + + mock_deconz_put_request(aioclient_mock, config_entry.data, "/sensors/1/config") + + # Service lock door + + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_LOCK, + {ATTR_ENTITY_ID: "lock.door_lock"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[1][2] == {"lock": True} + + # Service unlock door + + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_UNLOCK, + {ATTR_ENTITY_ID: "lock.door_lock"}, + blocking=True, + ) + assert aioclient_mock.mock_calls[2][2] == {"lock": False} + + await hass.config_entries.async_unload(config_entry.entry_id) + + states = hass.states.async_all() + assert len(states) == 2 + for state in states: + assert state.state == STATE_UNAVAILABLE + + await hass.config_entries.async_remove(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0