From ed6466f706f0b59a5e3406ba7b98ad0f00a3331a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 14 Mar 2022 18:45:45 +0100 Subject: [PATCH] Add Lock platform to Switch as X (#68123) Co-authored-by: Erik Montnemery --- .../components/switch_as_x/config_flow.py | 1 + homeassistant/components/switch_as_x/fan.py | 2 +- homeassistant/components/switch_as_x/lock.py | 83 +++++++++++++ tests/components/switch_as_x/test_init.py | 6 + tests/components/switch_as_x/test_lock.py | 110 ++++++++++++++++++ 5 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/switch_as_x/lock.py create mode 100644 tests/components/switch_as_x/test_lock.py diff --git a/homeassistant/components/switch_as_x/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py index 6247d55b1fa..5c3c68e9353 100644 --- a/homeassistant/components/switch_as_x/config_flow.py +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -25,6 +25,7 @@ CONFIG_FLOW = { {"value": Platform.COVER, "label": "Cover"}, {"value": Platform.FAN, "label": "Fan"}, {"value": Platform.LIGHT, "label": "Light"}, + {"value": Platform.LOCK, "label": "Lock"}, {"value": Platform.SIREN, "label": "Siren"}, ] } diff --git a/homeassistant/components/switch_as_x/fan.py b/homeassistant/components/switch_as_x/fan.py index 546e22b3fc1..e87f49b1b7b 100644 --- a/homeassistant/components/switch_as_x/fan.py +++ b/homeassistant/components/switch_as_x/fan.py @@ -44,7 +44,7 @@ class FanSwitch(BaseToggleEntity, FanEntity): """Return true if the entity is on. Fan logic uses speed percentage or preset mode to determine - its it on or off, however, when using a wrapped switch, we + if it's on or off, however, when using a wrapped switch, we just use the wrapped switch's state. """ return self._attr_is_on diff --git a/homeassistant/components/switch_as_x/lock.py b/homeassistant/components/switch_as_x/lock.py new file mode 100644 index 00000000000..0eaabb03770 --- /dev/null +++ b/homeassistant/components/switch_as_x/lock.py @@ -0,0 +1,83 @@ +"""Lock support for switch entities.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.lock import LockEntity +from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .entity import BaseEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Lock Switch config entry.""" + registry = er.async_get(hass) + entity_id = er.async_validate_entity_id( + registry, config_entry.options[CONF_ENTITY_ID] + ) + wrapped_switch = registry.async_get(entity_id) + device_id = wrapped_switch.device_id if wrapped_switch else None + + async_add_entities( + [ + LockSwitch( + config_entry.title, + entity_id, + config_entry.entry_id, + device_id, + ) + ] + ) + + +class LockSwitch(BaseEntity, LockEntity): + """Represents a Switch as a Lock.""" + + async def async_lock(self, **kwargs: Any) -> None: + """Lock the lock.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: self._switch_entity_id}, + blocking=True, + context=self._context, + ) + + async def async_unlock(self, **kwargs: Any) -> None: + """Unlock the lock.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: self._switch_entity_id}, + blocking=True, + context=self._context, + ) + + @callback + def async_state_changed_listener(self, event: Event | None = None) -> None: + """Handle child updates.""" + super().async_state_changed_listener(event) + if ( + not self.available + or (state := self.hass.states.get(self._switch_entity_id)) is None + ): + return + + # Logic is the same as the lock device class for binary sensors + # on means open (unlocked), off means closed (locked) + self._attr_is_locked = state.state != STATE_ON diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index 7a7002de094..b36ce64e593 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -17,6 +17,7 @@ from tests.common import MockConfigEntry Platform.COVER, Platform.FAN, Platform.LIGHT, + Platform.LOCK, Platform.SIREN, ), ) @@ -114,6 +115,7 @@ async def test_entity_registry_events(hass: HomeAssistant, target_domain: str) - Platform.COVER, Platform.FAN, Platform.LIGHT, + Platform.LOCK, Platform.SIREN, ), ) @@ -180,6 +182,7 @@ async def test_device_registry_config_entry_1( Platform.COVER, Platform.FAN, Platform.LIGHT, + Platform.LOCK, Platform.SIREN, ), ) @@ -239,6 +242,7 @@ async def test_device_registry_config_entry_2( Platform.COVER, Platform.FAN, Platform.LIGHT, + Platform.LOCK, Platform.SIREN, ), ) @@ -282,6 +286,7 @@ async def test_config_entry_entity_id( Platform.COVER, Platform.FAN, Platform.LIGHT, + Platform.LOCK, Platform.SIREN, ), ) @@ -314,6 +319,7 @@ async def test_config_entry_uuid(hass: HomeAssistant, target_domain: Platform) - Platform.COVER, Platform.FAN, Platform.LIGHT, + Platform.LOCK, Platform.SIREN, ), ) diff --git a/tests/components/switch_as_x/test_lock.py b/tests/components/switch_as_x/test_lock.py new file mode 100644 index 00000000000..de4c729e492 --- /dev/null +++ b/tests/components/switch_as_x/test_lock.py @@ -0,0 +1,110 @@ +"""Tests for the Switch as X Lock platform.""" +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.switch_as_x.const import CONF_TARGET_DOMAIN, DOMAIN +from homeassistant.const import ( + CONF_ENTITY_ID, + SERVICE_LOCK, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_UNLOCK, + STATE_LOCKED, + STATE_OFF, + STATE_ON, + STATE_UNLOCKED, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_default_state(hass: HomeAssistant) -> None: + """Test lock switch default state.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.test", + CONF_TARGET_DOMAIN: Platform.LOCK, + }, + title="candy_jar", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("lock.candy_jar") + assert state is not None + assert state.state == "unavailable" + + +async def test_service_calls(hass: HomeAssistant) -> None: + """Test service calls affecting the switch as lock entity.""" + await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.decorative_lights", + CONF_TARGET_DOMAIN: Platform.LOCK, + }, + title="candy_jar", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("lock.candy_jar").state == STATE_UNLOCKED + + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_LOCK, + {CONF_ENTITY_ID: "lock.candy_jar"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("lock.candy_jar").state == STATE_LOCKED + + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_UNLOCK, + {CONF_ENTITY_ID: "lock.candy_jar"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("lock.candy_jar").state == STATE_UNLOCKED + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {CONF_ENTITY_ID: "switch.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("lock.candy_jar").state == STATE_LOCKED + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {CONF_ENTITY_ID: "switch.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("lock.candy_jar").state == STATE_UNLOCKED + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TOGGLE, + {CONF_ENTITY_ID: "switch.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("lock.candy_jar").state == STATE_LOCKED