From cb7e492e815715d13422c239b0ef23f3ed4c6fef Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 10 Mar 2022 12:23:01 +0100 Subject: [PATCH] Add switch_as_x integration (#67878) * Add switch_as_x integration * Address review comments * Add translation strings * Rename entity_type option to target_domain * Move LightSwitch class definition to switch_as_x/light.py * Update manifest * Move tests --- CODEOWNERS | 2 + homeassistant/components/switch/__init__.py | 20 -- homeassistant/components/switch/light.py | 110 +---------- homeassistant/components/switch/manifest.json | 4 +- homeassistant/components/switch/strings.json | 10 - .../components/switch_as_x/__init__.py | 43 +++++ .../{switch => switch_as_x}/config_flow.py | 11 +- homeassistant/components/switch_as_x/light.py | 102 ++++++++++ .../components/switch_as_x/manifest.json | 11 ++ .../components/switch_as_x/strings.json | 14 ++ homeassistant/generated/config_flows.py | 2 +- tests/components/switch/test_light.py | 152 ++------------- tests/components/switch_as_x/__init__.py | 1 + .../test_config_flow.py | 63 +++---- tests/components/switch_as_x/test_light.py | 174 ++++++++++++++++++ 15 files changed, 402 insertions(+), 317 deletions(-) create mode 100644 homeassistant/components/switch_as_x/__init__.py rename homeassistant/components/{switch => switch_as_x}/config_flow.py (80%) create mode 100644 homeassistant/components/switch_as_x/light.py create mode 100644 homeassistant/components/switch_as_x/manifest.json create mode 100644 homeassistant/components/switch_as_x/strings.json create mode 100644 tests/components/switch_as_x/__init__.py rename tests/components/{switch => switch_as_x}/test_config_flow.py (69%) create mode 100644 tests/components/switch_as_x/test_light.py diff --git a/CODEOWNERS b/CODEOWNERS index 3ad23837c43..c2be1901344 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -980,6 +980,8 @@ homeassistant/components/swiss_hydrological_data/* @fabaff homeassistant/components/swiss_public_transport/* @fabaff homeassistant/components/switch/* @home-assistant/core tests/components/switch/* @home-assistant/core +homeassistant/components/switch_as_x/* @home-assistant/core +tests/components/switch_as_x/* @home-assistant/core homeassistant/components/switchbot/* @danielhiversen @RenierM26 tests/components/switchbot/* @danielhiversen @RenierM26 homeassistant/components/switcher_kis/* @tomerfi @thecode diff --git a/homeassistant/components/switch/__init__.py b/homeassistant/components/switch/__init__.py index b7e0ffac59c..2abb9ff81ca 100644 --- a/homeassistant/components/switch/__init__.py +++ b/homeassistant/components/switch/__init__.py @@ -11,15 +11,12 @@ import voluptuous as vol from homeassistant.backports.enum import StrEnum from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, - Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, @@ -63,8 +60,6 @@ DEVICE_CLASSES = [cls.value for cls in SwitchDeviceClass] DEVICE_CLASS_OUTLET = SwitchDeviceClass.OUTLET.value DEVICE_CLASS_SWITCH = SwitchDeviceClass.SWITCH.value -PLATFORMS: list[Platform] = [Platform.LIGHT] - @bind_hass def is_on(hass: HomeAssistant, entity_id: str) -> bool: @@ -91,21 +86,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up a config entry.""" - if entry.domain == DOMAIN: - registry = er.async_get(hass) - try: - er.async_validate_entity_id(registry, entry.options[CONF_ENTITY_ID]) - except vol.Invalid: - # The entity is identified by an unknown entity registry ID - _LOGGER.error( - "Failed to setup light switch for unknown entity %s", - entry.options[CONF_ENTITY_ID], - ) - return False - - hass.config_entries.async_setup_platforms(entry, PLATFORMS) - return True - component: EntityComponent = hass.data[DOMAIN] return await component.async_setup_entry(entry) diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 7c732d7750d..ad75d229d51 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -1,39 +1,25 @@ """Light support for switch entities.""" from __future__ import annotations -from typing import Any - import voluptuous as vol -from homeassistant.components import switch -from homeassistant.components.light import ( - COLOR_MODE_ONOFF, - PLATFORM_SCHEMA, - LightEntity, -) -from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_ENTITY_ID, - CONF_NAME, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, - STATE_ON, - STATE_UNAVAILABLE, -) -from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.components.light import PLATFORM_SCHEMA +from homeassistant.components.switch_as_x import LightSwitch +from homeassistant.const import CONF_ENTITY_ID, CONF_NAME +from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.event import async_track_state_change_event from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .const import DOMAIN as SWITCH_DOMAIN + DEFAULT_NAME = "Light Switch" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_ENTITY_ID): cv.entity_domain(switch.DOMAIN), + vol.Required(CONF_ENTITY_ID): cv.entity_domain(SWITCH_DOMAIN), } ) @@ -58,85 +44,3 @@ async def async_setup_platform( ) ] ) - - -async def async_setup_entry( - hass: HomeAssistant, - config_entry: ConfigEntry, - async_add_entities: AddEntitiesCallback, -) -> None: - """Initialize Light Switch config entry.""" - - registry = er.async_get(hass) - entity_id = er.async_validate_entity_id( - registry, config_entry.options[CONF_ENTITY_ID] - ) - - async_add_entities( - [ - LightSwitch( - config_entry.title, - entity_id, - config_entry.entry_id, - ) - ] - ) - - -class LightSwitch(LightEntity): - """Represents a Switch as a Light.""" - - _attr_color_mode = COLOR_MODE_ONOFF - _attr_should_poll = False - _attr_supported_color_modes = {COLOR_MODE_ONOFF} - - def __init__(self, name: str, switch_entity_id: str, unique_id: str | None) -> None: - """Initialize Light Switch.""" - self._attr_name = name - self._attr_unique_id = unique_id - self._switch_entity_id = switch_entity_id - - async def async_turn_on(self, **kwargs: Any) -> None: - """Forward the turn_on command to the switch in this light switch.""" - 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_turn_off(self, **kwargs: Any) -> None: - """Forward the turn_off command to the switch in this light switch.""" - 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_added_to_hass(self) -> None: - """Register callbacks.""" - - @callback - def async_state_changed_listener(event: Event | None = None) -> None: - """Handle child updates.""" - if ( - state := self.hass.states.get(self._switch_entity_id) - ) is None or state.state == STATE_UNAVAILABLE: - self._attr_available = False - return - - self._attr_available = True - self._attr_is_on = state.state == STATE_ON - self.async_write_ha_state() - - self.async_on_remove( - async_track_state_change_event( - self.hass, [self._switch_entity_id], async_state_changed_listener - ) - ) - - # Call once on adding - async_state_changed_listener() diff --git a/homeassistant/components/switch/manifest.json b/homeassistant/components/switch/manifest.json index f087ace1bce..ea11bc9591a 100644 --- a/homeassistant/components/switch/manifest.json +++ b/homeassistant/components/switch/manifest.json @@ -2,9 +2,9 @@ "domain": "switch", "name": "Switch", "documentation": "https://www.home-assistant.io/integrations/switch", + "after_dependencies": ["switch_as_x"], "codeowners": [ "@home-assistant/core" ], - "quality_scale": "internal", - "config_flow": true + "quality_scale": "internal" } diff --git a/homeassistant/components/switch/strings.json b/homeassistant/components/switch/strings.json index 5cdd0c35936..7ea84e649ef 100644 --- a/homeassistant/components/switch/strings.json +++ b/homeassistant/components/switch/strings.json @@ -1,15 +1,5 @@ { "title": "Switch", - "config": { - "step": { - "init": { - "description": "Select the switch for the light switch.", - "data": { - "entity_id": "Switch entity" - } - } - } - }, "device_automation": { "action_type": { "toggle": "Toggle {entity_name}", diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py new file mode 100644 index 00000000000..6cfea21a349 --- /dev/null +++ b/homeassistant/components/switch_as_x/__init__.py @@ -0,0 +1,43 @@ +"""Component to wrap switch entities in entities of other domains.""" +from __future__ import annotations + +import logging + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ENTITY_ID +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .light import LightSwitch + +__all__ = ["LightSwitch"] + +DOMAIN = "switch_as_x" + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + registry = er.async_get(hass) + try: + er.async_validate_entity_id(registry, entry.options[CONF_ENTITY_ID]) + except vol.Invalid: + # The entity is identified by an unknown entity registry ID + _LOGGER.error( + "Failed to setup switch_as_x for unknown entity %s", + entry.options[CONF_ENTITY_ID], + ) + return False + + hass.config_entries.async_setup_platforms(entry, (entry.options["target_domain"],)) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + return await hass.config_entries.async_unload_platforms( + entry, (entry.options["target_domain"],) + ) diff --git a/homeassistant/components/switch/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py similarity index 80% rename from homeassistant/components/switch/config_flow.py rename to homeassistant/components/switch_as_x/config_flow.py index efb3baf363f..90fb2e4bc0c 100644 --- a/homeassistant/components/switch/config_flow.py +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -1,4 +1,4 @@ -"""Config flow for Switch integration.""" +"""Config flow for Switch as X integration.""" from __future__ import annotations from collections.abc import Mapping @@ -13,7 +13,7 @@ from homeassistant.helpers import ( selector, ) -from .const import DOMAIN +from . import DOMAIN CONFIG_FLOW = { "user": helper_config_entry_flow.HelperFlowStep( @@ -22,16 +22,19 @@ CONFIG_FLOW = { vol.Required("entity_id"): selector.selector( {"entity": {"domain": "switch"}} ), + vol.Required("target_domain"): selector.selector( + {"select": {"options": ["light"]}} + ), } ) ) } -class SwitchLightConfigFlowHandler( +class SwitchAsXConfigFlowHandler( helper_config_entry_flow.HelperConfigFlowHandler, domain=DOMAIN ): - """Handle a config or options flow for Switch Light.""" + """Handle a config flow for Switch as X.""" config_flow = CONFIG_FLOW diff --git a/homeassistant/components/switch_as_x/light.py b/homeassistant/components/switch_as_x/light.py new file mode 100644 index 00000000000..8dd863029da --- /dev/null +++ b/homeassistant/components/switch_as_x/light.py @@ -0,0 +1,102 @@ +"""Light support for switch entities.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.light import COLOR_MODE_ONOFF, LightEntity +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, + STATE_UNAVAILABLE, +) +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import async_track_state_change_event + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Light Switch config entry.""" + + registry = er.async_get(hass) + entity_id = er.async_validate_entity_id( + registry, config_entry.options[CONF_ENTITY_ID] + ) + + async_add_entities( + [ + LightSwitch( + config_entry.title, + entity_id, + config_entry.entry_id, + ) + ] + ) + + +class LightSwitch(LightEntity): + """Represents a Switch as a Light.""" + + _attr_color_mode = COLOR_MODE_ONOFF + _attr_should_poll = False + _attr_supported_color_modes = {COLOR_MODE_ONOFF} + + def __init__(self, name: str, switch_entity_id: str, unique_id: str | None) -> None: + """Initialize Light Switch.""" + self._attr_name = name + self._attr_unique_id = unique_id + self._switch_entity_id = switch_entity_id + + async def async_turn_on(self, **kwargs: Any) -> None: + """Forward the turn_on command to the switch in this light switch.""" + 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_turn_off(self, **kwargs: Any) -> None: + """Forward the turn_off command to the switch in this light switch.""" + 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_added_to_hass(self) -> None: + """Register callbacks.""" + + @callback + def async_state_changed_listener(event: Event | None = None) -> None: + """Handle child updates.""" + if ( + state := self.hass.states.get(self._switch_entity_id) + ) is None or state.state == STATE_UNAVAILABLE: + self._attr_available = False + return + + self._attr_available = True + self._attr_is_on = state.state == STATE_ON + self.async_write_ha_state() + + self.async_on_remove( + async_track_state_change_event( + self.hass, [self._switch_entity_id], async_state_changed_listener + ) + ) + + # Call once on adding + async_state_changed_listener() diff --git a/homeassistant/components/switch_as_x/manifest.json b/homeassistant/components/switch_as_x/manifest.json new file mode 100644 index 00000000000..358b50bfd1b --- /dev/null +++ b/homeassistant/components/switch_as_x/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "switch_as_x", + "name": "Switch as X", + "documentation": "https://www.home-assistant.io/integrations/switch_as_x", + "codeowners": [ + "@home-assistant/core" + ], + "quality_scale": "internal", + "iot_class": "calculated", + "config_flow": true +} diff --git a/homeassistant/components/switch_as_x/strings.json b/homeassistant/components/switch_as_x/strings.json new file mode 100644 index 00000000000..cc50d1922cf --- /dev/null +++ b/homeassistant/components/switch_as_x/strings.json @@ -0,0 +1,14 @@ +{ + "title": "Switch as X", + "config": { + "step": { + "init": { + "title": "Make a switch a ...", + "data": { + "entity_id": "Switch entity", + "target_domain": "Type" + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 7e94ae5a165..93592a2e1a9 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -323,7 +323,7 @@ FLOWS = [ "stookalert", "subaru", "surepetcare", - "switch", + "switch_as_x", "switchbot", "switcher_kis", "syncthing", diff --git a/tests/components/switch/test_light.py b/tests/components/switch/test_light.py index 518fd8db20b..e75e0e6d313 100644 --- a/tests/components/switch/test_light.py +++ b/tests/components/switch/test_light.py @@ -1,22 +1,10 @@ """The tests for the Light Switch platform.""" -from homeassistant.components.light import ( - ATTR_COLOR_MODE, - ATTR_SUPPORTED_COLOR_MODES, - COLOR_MODE_ONOFF, -) -from homeassistant.components.switch.const import DOMAIN as SWITCH_DOMAIN -from homeassistant.core import HomeAssistant -from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry -from tests.components.light import common -from tests.components.switch import common as switch_common - async def test_default_state(hass): - """Test light switch default state.""" + """Test light switch yaml config.""" await async_setup_component( hass, "light", @@ -30,139 +18,21 @@ async def test_default_state(hass): ) await hass.async_block_till_done() - state = hass.states.get("light.christmas_tree_lights") - assert state is not None - assert state.state == "unavailable" - assert state.attributes["supported_features"] == 0 - assert state.attributes.get("brightness") is None - assert state.attributes.get("hs_color") is None - assert state.attributes.get("color_temp") is None - assert state.attributes.get("white_value") is None - assert state.attributes.get("effect_list") is None - assert state.attributes.get("effect") is None - assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [COLOR_MODE_ONOFF] - assert state.attributes.get(ATTR_COLOR_MODE) is None + assert hass.states.get("light.christmas_tree_lights") -async def test_light_service_calls(hass): - """Test service calls to light.""" - await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) +async def test_default_state_no_name(hass): + """Test light switch default name.""" await async_setup_component( hass, "light", - {"light": [{"platform": "switch", "entity_id": "switch.decorative_lights"}]}, + { + "light": { + "platform": "switch", + "entity_id": "switch.test", + } + }, ) await hass.async_block_till_done() - assert hass.states.get("light.light_switch").state == "on" - - await common.async_toggle(hass, "light.light_switch") - - assert hass.states.get("switch.decorative_lights").state == "off" - assert hass.states.get("light.light_switch").state == "off" - - await common.async_turn_on(hass, "light.light_switch") - - assert hass.states.get("switch.decorative_lights").state == "on" - assert hass.states.get("light.light_switch").state == "on" - assert ( - hass.states.get("light.light_switch").attributes.get(ATTR_COLOR_MODE) - == COLOR_MODE_ONOFF - ) - - await common.async_turn_off(hass, "light.light_switch") - await hass.async_block_till_done() - - assert hass.states.get("switch.decorative_lights").state == "off" - assert hass.states.get("light.light_switch").state == "off" - - -async def test_switch_service_calls(hass): - """Test service calls to switch.""" - await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) - await async_setup_component( - hass, - "light", - {"light": [{"platform": "switch", "entity_id": "switch.decorative_lights"}]}, - ) - await hass.async_block_till_done() - - assert hass.states.get("light.light_switch").state == "on" - - await switch_common.async_turn_off(hass, "switch.decorative_lights") - await hass.async_block_till_done() - - assert hass.states.get("switch.decorative_lights").state == "off" - assert hass.states.get("light.light_switch").state == "off" - - await switch_common.async_turn_on(hass, "switch.decorative_lights") - await hass.async_block_till_done() - - assert hass.states.get("switch.decorative_lights").state == "on" - assert hass.states.get("light.light_switch").state == "on" - - -async def test_config_entry(hass: HomeAssistant): - """Test light switch setup from config entry.""" - config_entry = MockConfigEntry( - data={}, - domain=SWITCH_DOMAIN, - options={"entity_id": "switch.abc"}, - 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 SWITCH_DOMAIN in hass.config.components - - state = hass.states.get("light.abc") - 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("light.abc") - assert entity_entry.unique_id == config_entry.entry_id - - -async def test_config_entry_uuid(hass: HomeAssistant): - """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=SWITCH_DOMAIN, - options={"entity_id": registry_entry.id}, - 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("light.abc") - - -async def test_config_entry_unregistered_uuid(hass: HomeAssistant): - """Test light switch setup from config entry with unknown entity registry id.""" - fake_uuid = "a266a680b608c32770e6c45bfe6b8411" - - config_entry = MockConfigEntry( - data={}, - domain=SWITCH_DOMAIN, - options={"entity_id": fake_uuid}, - title="ABC", - ) - - config_entry.add_to_hass(hass) - - assert not await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - - assert len(hass.states.async_all()) == 0 + assert hass.states.get("light.light_switch") diff --git a/tests/components/switch_as_x/__init__.py b/tests/components/switch_as_x/__init__.py new file mode 100644 index 00000000000..d7cf944e624 --- /dev/null +++ b/tests/components/switch_as_x/__init__.py @@ -0,0 +1 @@ +"""The tests for Switch as X platforms.""" diff --git a/tests/components/switch/test_config_flow.py b/tests/components/switch_as_x/test_config_flow.py similarity index 69% rename from tests/components/switch/test_config_flow.py rename to tests/components/switch_as_x/test_config_flow.py index ca838b7b972..223547dfb75 100644 --- a/tests/components/switch/test_config_flow.py +++ b/tests/components/switch_as_x/test_config_flow.py @@ -1,17 +1,17 @@ -"""Test the switch light config flow.""" +"""Test the Switch as X config flow.""" from unittest.mock import patch import pytest from homeassistant import config_entries, data_entry_flow -from homeassistant.components.switch import async_setup_entry -from homeassistant.components.switch.const import DOMAIN +from homeassistant.components.switch_as_x import DOMAIN, async_setup_entry from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM from homeassistant.helpers import entity_registry as er -async def test_config_flow(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("target_domain", ("light",)) +async def test_config_flow(hass: HomeAssistant, target_domain) -> None: """Test the config flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -20,13 +20,14 @@ async def test_config_flow(hass: HomeAssistant) -> None: assert result["errors"] is None with patch( - "homeassistant.components.switch.async_setup_entry", + "homeassistant.components.switch_as_x.async_setup_entry", wraps=async_setup_entry, ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], { "entity_id": "switch.ceiling", + "target_domain": target_domain, }, ) await hass.async_block_till_done() @@ -34,17 +35,24 @@ async def test_config_flow(hass: HomeAssistant) -> None: assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == "ceiling" assert result["data"] == {} - assert result["options"] == {"entity_id": "switch.ceiling"} + assert result["options"] == { + "entity_id": "switch.ceiling", + "target_domain": target_domain, + } assert len(mock_setup_entry.mock_calls) == 1 config_entry = hass.config_entries.async_entries(DOMAIN)[0] assert config_entry.data == {} - assert config_entry.options == {"entity_id": "switch.ceiling"} + assert config_entry.options == { + "entity_id": "switch.ceiling", + "target_domain": target_domain, + } - assert hass.states.get("light.ceiling") + assert hass.states.get(f"{target_domain}.ceiling") -async def test_name(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("target_domain", ("light",)) +async def test_name(hass: HomeAssistant, target_domain) -> None: """Test the config flow name is copied from registry entry, with fallback to state.""" registry = er.async_get(hass) @@ -55,7 +63,7 @@ async def test_name(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], - {"entity_id": "switch.ceiling"}, + {"entity_id": "switch.ceiling", "target_domain": target_domain}, ) assert result["title"] == "ceiling" @@ -68,7 +76,7 @@ async def test_name(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], - {"entity_id": "switch.ceiling"}, + {"entity_id": "switch.ceiling", "target_domain": target_domain}, ) assert result["title"] == "State Name" @@ -90,7 +98,7 @@ async def test_name(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], - {"entity_id": "switch.ceiling"}, + {"entity_id": "switch.ceiling", "target_domain": target_domain}, ) assert result["title"] == "Original Name" @@ -103,51 +111,34 @@ async def test_name(hass: HomeAssistant) -> None: result = await hass.config_entries.flow.async_configure( result["flow_id"], - {"entity_id": "switch.ceiling"}, + {"entity_id": "switch.ceiling", "target_domain": target_domain}, ) assert result["title"] == "Custom Name" -def get_suggested(schema, key): - """Get suggested value for key in voluptuous schema.""" - for k in schema.keys(): - if k == key: - if k.description is None or "suggested_value" not in k.description: - return None - return k.description["suggested_value"] - - -async def test_options(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("target_domain", ("light",)) +async def test_options(hass: HomeAssistant, target_domain) -> None: """Test reconfiguring.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] == RESULT_TYPE_FORM assert result["errors"] is None - assert get_suggested(result["data_schema"].schema, "entity_id") is None - assert get_suggested(result["data_schema"].schema, "name") is None with patch( - "homeassistant.components.switch.async_setup_entry", + "homeassistant.components.switch_as_x.async_setup_entry", return_value=True, - ) as mock_setup_entry: + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], - { - "entity_id": "switch.ceiling", - }, + {"entity_id": "switch.ceiling", "target_domain": target_domain}, ) await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "ceiling" - assert result["data"] == {} - assert result["options"] == {"entity_id": "switch.ceiling"} - assert len(mock_setup_entry.mock_calls) == 1 config_entry = hass.config_entries.async_entries(DOMAIN)[0] - assert config_entry.data == {} - assert config_entry.options == {"entity_id": "switch.ceiling"} + assert config_entry # Switch light has no options flow with pytest.raises(data_entry_flow.UnknownHandler): diff --git a/tests/components/switch_as_x/test_light.py b/tests/components/switch_as_x/test_light.py new file mode 100644 index 00000000000..302311b3845 --- /dev/null +++ b/tests/components/switch_as_x/test_light.py @@ -0,0 +1,174 @@ +"""The tests for the Light Switch platform.""" +import pytest + +from homeassistant.components.light import ( + ATTR_COLOR_MODE, + ATTR_SUPPORTED_COLOR_MODES, + COLOR_MODE_ONOFF, +) +from homeassistant.components.switch_as_x import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry +from tests.components.light import common +from tests.components.switch import common as switch_common + + +async def test_default_state(hass): + """Test light switch default state.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={"entity_id": "switch.test", "target_domain": "light"}, + title="Christmas Tree Lights", + ) + 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("light.christmas_tree_lights") + assert state is not None + assert state.state == "unavailable" + assert state.attributes["supported_features"] == 0 + assert state.attributes.get("brightness") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("white_value") is None + assert state.attributes.get("effect_list") is None + assert state.attributes.get("effect") is None + assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [COLOR_MODE_ONOFF] + assert state.attributes.get(ATTR_COLOR_MODE) is None + + +async def test_light_service_calls(hass): + """Test service calls to light.""" + await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={"entity_id": "switch.decorative_lights", "target_domain": "light"}, + title="decorative_lights", + ) + 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("light.decorative_lights").state == "on" + + await common.async_toggle(hass, "light.decorative_lights") + + assert hass.states.get("switch.decorative_lights").state == "off" + assert hass.states.get("light.decorative_lights").state == "off" + + await common.async_turn_on(hass, "light.decorative_lights") + + assert hass.states.get("switch.decorative_lights").state == "on" + assert hass.states.get("light.decorative_lights").state == "on" + assert ( + hass.states.get("light.decorative_lights").attributes.get(ATTR_COLOR_MODE) + == COLOR_MODE_ONOFF + ) + + await common.async_turn_off(hass, "light.decorative_lights") + await hass.async_block_till_done() + + assert hass.states.get("switch.decorative_lights").state == "off" + assert hass.states.get("light.decorative_lights").state == "off" + + +async def test_switch_service_calls(hass): + """Test service calls to switch.""" + await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={"entity_id": "switch.decorative_lights", "target_domain": "light"}, + title="decorative_lights", + ) + 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("light.decorative_lights").state == "on" + + await switch_common.async_turn_off(hass, "switch.decorative_lights") + await hass.async_block_till_done() + + assert hass.states.get("switch.decorative_lights").state == "off" + assert hass.states.get("light.decorative_lights").state == "off" + + await switch_common.async_turn_on(hass, "switch.decorative_lights") + await hass.async_block_till_done() + + assert hass.states.get("switch.decorative_lights").state == "on" + assert hass.states.get("light.decorative_lights").state == "on" + + +@pytest.mark.parametrize("target_domain", ("light",)) +async def test_config_entry(hass: HomeAssistant, target_domain): + """Test light switch setup from config entry.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={"entity_id": "switch.abc", "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.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.unique_id == config_entry.entry_id + + +@pytest.mark.parametrize("target_domain", ("light",)) +async def test_config_entry_uuid(hass: HomeAssistant, target_domain): + """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={"entity_id": registry_entry.id, "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") + + +async def test_config_entry_unregistered_uuid(hass: HomeAssistant): + """Test light switch setup from config entry with unknown entity registry id.""" + fake_uuid = "a266a680b608c32770e6c45bfe6b8411" + + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={"entity_id": fake_uuid}, + title="ABC", + ) + + config_entry.add_to_hass(hass) + + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 0