diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index 31ae9eb018c..f60c4c35646 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -23,6 +23,7 @@ SUPPORTED_PLATFORMS = [ "climate", "cover", "light", + "lock", "scene", "sensor", "switch", @@ -38,10 +39,16 @@ ATTR_OFFSET = "offset" ATTR_ON = "on" ATTR_VALVE = "valve" +# Covers DAMPERS = ["Level controllable output"] WINDOW_COVERS = ["Window covering device", "Window covering controller"] COVER_TYPES = DAMPERS + WINDOW_COVERS +# Locks +LOCKS = ["Door Lock"] +LOCK_TYPES = LOCKS + +# Switches POWER_PLUGS = ["On/Off light", "On/Off plug-in unit", "Smart plug"] SIRENS = ["Warning device"] SWITCH_TYPES = POWER_PLUGS + SIRENS diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 3f11cef31da..544699970f2 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -26,6 +26,7 @@ from .const import ( CONF_GROUP_ID_BASE, COVER_TYPES, DOMAIN as DECONZ_DOMAIN, + LOCK_TYPES, NEW_GROUP, NEW_LIGHT, SWITCH_TYPES, @@ -50,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 + SWITCH_TYPES + light.type not in COVER_TYPES + LOCK_TYPES + SWITCH_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 new file mode 100644 index 00000000000..1f4fbe57069 --- /dev/null +++ b/homeassistant/components/deconz/lock.py @@ -0,0 +1,59 @@ +"""Support for deCONZ locks.""" +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 .deconz_device import DeconzDevice +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. + """ + gateway = get_gateway_from_config_entry(hass, config_entry) + gateway.entities[DOMAIN] = set() + + @callback + def async_add_lock(lights): + """Add lock from deCONZ.""" + entities = [] + + for light in lights: + + if light.type in LOCKS and light.uniqueid not in gateway.entities[DOMAIN]: + entities.append(DeconzLock(light, gateway)) + + if entities: + async_add_entities(entities, True) + + gateway.listeners.append( + async_dispatcher_connect( + hass, gateway.async_signal_new_device(NEW_LIGHT), async_add_lock + ) + ) + + async_add_lock(gateway.api.lights.values()) + + +class DeconzLock(DeconzDevice, LockEntity): + """Representation of a deCONZ lock.""" + + TYPE = DOMAIN + + @property + def is_locked(self): + """Return true if lock is on.""" + return self._device.state + + async def async_lock(self, **kwargs): + """Lock the lock.""" + data = {"on": True} + await self._device.async_set_state(data) + + async def async_unlock(self, **kwargs): + """Unlock the lock.""" + data = {"on": False} + await self._device.async_set_state(data) diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 64818401bcb..e4dc0424f83 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -91,9 +91,10 @@ async def test_gateway_setup(hass): assert forward_entry_setup.mock_calls[1][1] == (entry, "climate") assert forward_entry_setup.mock_calls[2][1] == (entry, "cover") assert forward_entry_setup.mock_calls[3][1] == (entry, "light") - assert forward_entry_setup.mock_calls[4][1] == (entry, "scene") - assert forward_entry_setup.mock_calls[5][1] == (entry, "sensor") - assert forward_entry_setup.mock_calls[6][1] == (entry, "switch") + assert forward_entry_setup.mock_calls[4][1] == (entry, "lock") + assert forward_entry_setup.mock_calls[5][1] == (entry, "scene") + assert forward_entry_setup.mock_calls[6][1] == (entry, "sensor") + assert forward_entry_setup.mock_calls[7][1] == (entry, "switch") async def test_gateway_retry(hass): diff --git a/tests/components/deconz/test_lock.py b/tests/components/deconz/test_lock.py new file mode 100644 index 00000000000..554f825d42a --- /dev/null +++ b/tests/components/deconz/test_lock.py @@ -0,0 +1,96 @@ +"""deCONZ lock platform tests.""" +from copy import deepcopy + +from homeassistant.components import deconz +import homeassistant.components.lock as lock +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED +from homeassistant.setup import async_setup_component + +from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration + +from tests.async_mock import patch + +LOCKS = { + "1": { + "etag": "5c2ec06cde4bd654aef3a555fcd8ad12", + "hascolor": False, + "lastannounced": None, + "lastseen": "2020-08-22T15:29:03Z", + "manufacturername": "Danalock", + "modelid": "V3-BTZB", + "name": "Door lock", + "state": {"alert": "none", "on": False, "reachable": True}, + "swversion": "19042019", + "type": "Door Lock", + "uniqueid": "00:00:00:00:00:00:00:00-00", + } +} + + +async def test_platform_manually_configured(hass): + """Test that we do not discover anything or try to set up a gateway.""" + assert ( + await async_setup_component( + hass, lock.DOMAIN, {"lock": {"platform": deconz.DOMAIN}} + ) + is True + ) + assert deconz.DOMAIN not in hass.data + + +async def test_no_locks(hass): + """Test that no lock entities are created.""" + gateway = await setup_deconz_integration(hass) + assert len(gateway.deconz_ids) == 0 + assert len(hass.states.async_all()) == 0 + + +async def test_locks(hass): + """Test that all supported lock entities are created.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["lights"] = deepcopy(LOCKS) + gateway = await setup_deconz_integration(hass, get_state_response=data) + assert "lock.door_lock" in gateway.deconz_ids + assert len(hass.states.async_all()) == 1 + + door_lock = hass.states.get("lock.door_lock") + assert door_lock.state == STATE_UNLOCKED + + state_changed_event = { + "t": "event", + "e": "changed", + "r": "lights", + "id": "1", + "state": {"on": True}, + } + gateway.api.event_handler(state_changed_event) + await hass.async_block_till_done() + + door_lock = hass.states.get("lock.door_lock") + assert door_lock.state == STATE_LOCKED + + door_lock_device = gateway.api.lights["1"] + + with patch.object(door_lock_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + lock.DOMAIN, + lock.SERVICE_LOCK, + {"entity_id": "lock.door_lock"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("put", "/lights/1/state", json={"on": True}) + + with patch.object(door_lock_device, "_request", return_value=True) as set_callback: + await hass.services.async_call( + lock.DOMAIN, + lock.SERVICE_UNLOCK, + {"entity_id": "lock.door_lock"}, + blocking=True, + ) + await hass.async_block_till_done() + set_callback.assert_called_with("put", "/lights/1/state", json={"on": False}) + + await gateway.async_reset() + + assert len(hass.states.async_all()) == 0