From c70c699af0397b677f84abfb8d0de97cb087aa66 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 14 Mar 2022 11:59:55 +0100 Subject: [PATCH] Add cover platform to Switch as X (#68107) --- .../components/switch_as_x/config_flow.py | 1 + homeassistant/components/switch_as_x/cover.py | 83 ++++++++++++ tests/components/switch_as_x/test_cover.py | 121 ++++++++++++++++++ tests/components/switch_as_x/test_init.py | 95 ++++++++++++++ tests/components/switch_as_x/test_light.py | 98 -------------- 5 files changed, 300 insertions(+), 98 deletions(-) create mode 100644 homeassistant/components/switch_as_x/cover.py create mode 100644 tests/components/switch_as_x/test_cover.py diff --git a/homeassistant/components/switch_as_x/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py index c81a449eea7..ed11ae25489 100644 --- a/homeassistant/components/switch_as_x/config_flow.py +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -22,6 +22,7 @@ CONFIG_FLOW = { { "select": { "options": [ + {"value": Platform.COVER, "label": "Cover"}, {"value": Platform.LIGHT, "label": "Light"}, ] } diff --git a/homeassistant/components/switch_as_x/cover.py b/homeassistant/components/switch_as_x/cover.py new file mode 100644 index 00000000000..3825953ed63 --- /dev/null +++ b/homeassistant/components/switch_as_x/cover.py @@ -0,0 +1,83 @@ +"""Cover support for switch entities.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN, CoverEntity +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 Cover 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( + [ + CoverSwitch( + config_entry.title, + entity_id, + config_entry.entry_id, + device_id, + ) + ] + ) + + +class CoverSwitch(BaseEntity, CoverEntity): + """Represents a Switch as a Cover.""" + + _attr_supported_features = SUPPORT_OPEN | SUPPORT_CLOSE + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: self._switch_entity_id}, + blocking=True, + context=self._context, + ) + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close cover.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {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 + + self._attr_is_closed = state.state != STATE_ON diff --git a/tests/components/switch_as_x/test_cover.py b/tests/components/switch_as_x/test_cover.py new file mode 100644 index 00000000000..d8317a51b8c --- /dev/null +++ b/tests/components/switch_as_x/test_cover.py @@ -0,0 +1,121 @@ +"""Tests for the Switch as X Cover platform.""" +from homeassistant.components.cover import DOMAIN as COVER_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_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_CLOSED, + STATE_OFF, + STATE_ON, + STATE_OPEN, + 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 cover switch default state.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.test", + CONF_TARGET_DOMAIN: Platform.COVER, + }, + title="Garage Door", + ) + 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("cover.garage_door") + assert state is not None + assert state.state == "unavailable" + assert state.attributes["supported_features"] == 3 + + +async def test_service_calls(hass: HomeAssistant) -> None: + """Test service calls to cover.""" + 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.COVER, + }, + title="garage_door", + ) + 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("cover.garage_door").state == STATE_OPEN + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_TOGGLE, + {CONF_ENTITY_ID: "cover.garage_door"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("cover.garage_door").state == STATE_CLOSED + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {CONF_ENTITY_ID: "cover.garage_door"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("cover.garage_door").state == STATE_OPEN + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {CONF_ENTITY_ID: "cover.garage_door"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("cover.garage_door").state == STATE_CLOSED + + 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("cover.garage_door").state == STATE_OPEN + + 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("cover.garage_door").state == STATE_CLOSED + + 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_ON + assert hass.states.get("cover.garage_door").state == STATE_OPEN diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index dc4d418d060..ca87673dfba 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -200,3 +200,98 @@ async def test_device_registry_config_entry_2( # Check that the switch_as_x config entry is removed from the device device_entry = device_registry.async_get(device_entry.id) assert switch_as_x_config_entry.entry_id not in device_entry.config_entries + + +@pytest.mark.parametrize("target_domain", (Platform.LIGHT, Platform.COVER)) +async def test_config_entry_entity_id( + hass: HomeAssistant, target_domain: Platform +) -> None: + """Test light switch setup from config entry with entity id.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.abc", + CONF_TARGET_DOMAIN: target_domain, + }, + title="ABC", + ) + + config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert DOMAIN in hass.config.components + + state = hass.states.get(f"{target_domain}.abc") + assert state + assert state.state == "unavailable" + # Name copied from config entry title + assert state.name == "ABC" + + # Check the light is added to the entity registry + registry = er.async_get(hass) + entity_entry = registry.async_get(f"{target_domain}.abc") + assert entity_entry + assert entity_entry.unique_id == config_entry.entry_id + + +@pytest.mark.parametrize("target_domain", (Platform.LIGHT, Platform.COVER)) +async def test_config_entry_uuid(hass: HomeAssistant, target_domain: Platform) -> None: + """Test light switch setup from config entry with entity registry id.""" + registry = er.async_get(hass) + registry_entry = registry.async_get_or_create("switch", "test", "unique") + + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: registry_entry.id, + CONF_TARGET_DOMAIN: target_domain, + }, + title="ABC", + ) + + 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(f"{target_domain}.abc") + + +@pytest.mark.parametrize("target_domain", (Platform.LIGHT, Platform.COVER)) +async def test_device(hass: HomeAssistant, target_domain: Platform) -> None: + """Test the entity is added to the wrapped entity's device.""" + device_registry = dr.async_get(hass) + entity_registry = er.async_get(hass) + + test_config_entry = MockConfigEntry() + + device_entry = device_registry.async_get_or_create( + config_entry_id=test_config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + switch_entity_entry = entity_registry.async_get_or_create( + "switch", "test", "unique", device_id=device_entry.id + ) + + switch_as_x_config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: switch_entity_entry.id, + CONF_TARGET_DOMAIN: target_domain, + }, + title="ABC", + ) + + switch_as_x_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id) + await hass.async_block_till_done() + + entity_entry = entity_registry.async_get(f"{target_domain}.abc") + assert entity_entry + assert entity_entry.device_id == switch_entity_entry.device_id diff --git a/tests/components/switch_as_x/test_light.py b/tests/components/switch_as_x/test_light.py index 09fd7808459..75e62008e7b 100644 --- a/tests/components/switch_as_x/test_light.py +++ b/tests/components/switch_as_x/test_light.py @@ -1,6 +1,4 @@ """Tests for the Switch as X Light platform.""" -import pytest - from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_MODE, @@ -25,7 +23,6 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -150,98 +147,3 @@ async def test_switch_service_calls(hass: HomeAssistant) -> None: assert hass.states.get("switch.decorative_lights").state == STATE_ON assert hass.states.get("light.decorative_lights").state == STATE_ON - - -@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) -async def test_config_entry_entity_id( - hass: HomeAssistant, target_domain: Platform -) -> None: - """Test light switch setup from config entry with entity id.""" - config_entry = MockConfigEntry( - data={}, - domain=DOMAIN, - options={ - CONF_ENTITY_ID: "switch.abc", - CONF_TARGET_DOMAIN: target_domain, - }, - title="ABC", - ) - - config_entry.add_to_hass(hass) - - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert DOMAIN in hass.config.components - - state = hass.states.get(f"{target_domain}.abc") - assert state - assert state.state == "unavailable" - # Name copied from config entry title - assert state.name == "ABC" - - # Check the light is added to the entity registry - registry = er.async_get(hass) - entity_entry = registry.async_get(f"{target_domain}.abc") - assert entity_entry - assert entity_entry.unique_id == config_entry.entry_id - - -@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) -async def test_config_entry_uuid(hass: HomeAssistant, target_domain: Platform) -> None: - """Test light switch setup from config entry with entity registry id.""" - registry = er.async_get(hass) - registry_entry = registry.async_get_or_create("switch", "test", "unique") - - config_entry = MockConfigEntry( - data={}, - domain=DOMAIN, - options={ - CONF_ENTITY_ID: registry_entry.id, - CONF_TARGET_DOMAIN: target_domain, - }, - title="ABC", - ) - - 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(f"{target_domain}.abc") - - -@pytest.mark.parametrize("target_domain", (Platform.LIGHT,)) -async def test_device(hass: HomeAssistant, target_domain: Platform) -> None: - """Test the entity is added to the wrapped entity's device.""" - device_registry = dr.async_get(hass) - entity_registry = er.async_get(hass) - - test_config_entry = MockConfigEntry() - - device_entry = device_registry.async_get_or_create( - config_entry_id=test_config_entry.entry_id, - connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, - ) - switch_entity_entry = entity_registry.async_get_or_create( - "switch", "test", "unique", device_id=device_entry.id - ) - - switch_as_x_config_entry = MockConfigEntry( - data={}, - domain=DOMAIN, - options={ - CONF_ENTITY_ID: switch_entity_entry.id, - CONF_TARGET_DOMAIN: target_domain, - }, - title="ABC", - ) - - switch_as_x_config_entry.add_to_hass(hass) - - assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id) - await hass.async_block_till_done() - - entity_entry = entity_registry.async_get(f"{target_domain}.abc") - assert entity_entry - assert entity_entry.device_id == switch_entity_entry.device_id