diff --git a/homeassistant/components/switch_as_x/__init__.py b/homeassistant/components/switch_as_x/__init__.py index e2ad91e990ecae..3fe2ff7bc7d128 100644 --- a/homeassistant/components/switch_as_x/__init__.py +++ b/homeassistant/components/switch_as_x/__init__.py @@ -13,7 +13,7 @@ from homeassistant.helpers.event import async_track_entity_registry_updated_event from homeassistant.helpers.typing import EventType -from .const import CONF_TARGET_DOMAIN +from .const import CONF_INVERT, CONF_TARGET_DOMAIN from .light import LightSwitch __all__ = ["LightSwitch"] @@ -91,6 +91,7 @@ async def async_registry_updated( hass, entity_id, async_registry_updated ) ) + entry.async_on_unload(entry.add_update_listener(config_entry_update_listener)) device_id = async_add_to_device(hass, entry, entity_id) @@ -100,6 +101,36 @@ async def async_registry_updated( return True +async def async_migrate_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: + """Migrate old entry.""" + _LOGGER.debug( + "Migrating from version %s.%s", config_entry.version, config_entry.minor_version + ) + + if config_entry.version > 1: + # This means the user has downgraded from a future version + return False + if config_entry.version == 1: + options = {**config_entry.options} + if config_entry.minor_version < 2: + options.setdefault(CONF_INVERT, False) + config_entry.minor_version = 2 + hass.config_entries.async_update_entry(config_entry, options=options) + + _LOGGER.debug( + "Migration to version %s.%s successful", + config_entry.version, + config_entry.minor_version, + ) + + return True + + +async def config_entry_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Update listener, called when the config entry options are changed.""" + await hass.config_entries.async_reload(entry.entry_id) + + async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms( diff --git a/homeassistant/components/switch_as_x/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py index 90f6b98589389c..e40e247f1051bf 100644 --- a/homeassistant/components/switch_as_x/config_flow.py +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -14,7 +14,7 @@ wrapped_entity_config_entry_title, ) -from .const import CONF_TARGET_DOMAIN, DOMAIN +from .const import CONF_INVERT, CONF_TARGET_DOMAIN, DOMAIN TARGET_DOMAIN_OPTIONS = [ selector.SelectOptionDict(value=Platform.COVER, label="Cover"), @@ -32,6 +32,7 @@ vol.Required(CONF_ENTITY_ID): selector.EntitySelector( selector.EntitySelectorConfig(domain=Platform.SWITCH), ), + vol.Optional(CONF_INVERT, default=False): selector.BooleanSelector(), vol.Required(CONF_TARGET_DOMAIN): selector.SelectSelector( selector.SelectSelectorConfig(options=TARGET_DOMAIN_OPTIONS), ), @@ -40,11 +41,21 @@ ) } +OPTIONS_FLOW = { + "init": SchemaFlowFormStep( + vol.Schema({vol.Required(CONF_INVERT): selector.BooleanSelector()}) + ), +} + class SwitchAsXConfigFlowHandler(SchemaConfigFlowHandler, domain=DOMAIN): """Handle a config flow for Switch as X.""" config_flow = CONFIG_FLOW + options_flow = OPTIONS_FLOW + + VERSION = 1 + MINOR_VERSION = 2 def async_config_entry_title(self, options: Mapping[str, Any]) -> str: """Return config entry title and hide the wrapped entity if registered.""" diff --git a/homeassistant/components/switch_as_x/const.py b/homeassistant/components/switch_as_x/const.py index 4963d6fa60b976..58ace36487aed3 100644 --- a/homeassistant/components/switch_as_x/const.py +++ b/homeassistant/components/switch_as_x/const.py @@ -4,4 +4,5 @@ DOMAIN: Final = "switch_as_x" +CONF_INVERT: Final = "invert" CONF_TARGET_DOMAIN: Final = "target_domain" diff --git a/homeassistant/components/switch_as_x/cover.py b/homeassistant/components/switch_as_x/cover.py index b7fe0fbf36439e..37071ac67714aa 100644 --- a/homeassistant/components/switch_as_x/cover.py +++ b/homeassistant/components/switch_as_x/cover.py @@ -23,7 +23,8 @@ from homeassistant.helpers.event import EventStateChangedData from homeassistant.helpers.typing import EventType -from .entity import BaseEntity +from .const import CONF_INVERT +from .entity import BaseInvertableEntity async def async_setup_entry( @@ -43,6 +44,7 @@ async def async_setup_entry( hass, config_entry.title, COVER_DOMAIN, + config_entry.options[CONF_INVERT], entity_id, config_entry.entry_id, ) @@ -50,7 +52,7 @@ async def async_setup_entry( ) -class CoverSwitch(BaseEntity, CoverEntity): +class CoverSwitch(BaseInvertableEntity, CoverEntity): """Represents a Switch as a Cover.""" _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE @@ -59,7 +61,7 @@ async def async_open_cover(self, **kwargs: Any) -> None: """Open the cover.""" await self.hass.services.async_call( SWITCH_DOMAIN, - SERVICE_TURN_ON, + SERVICE_TURN_OFF if self._invert_state else SERVICE_TURN_ON, {ATTR_ENTITY_ID: self._switch_entity_id}, blocking=True, context=self._context, @@ -69,7 +71,7 @@ async def async_close_cover(self, **kwargs: Any) -> None: """Close cover.""" await self.hass.services.async_call( SWITCH_DOMAIN, - SERVICE_TURN_OFF, + SERVICE_TURN_ON if self._invert_state else SERVICE_TURN_OFF, {ATTR_ENTITY_ID: self._switch_entity_id}, blocking=True, context=self._context, @@ -87,4 +89,7 @@ def async_state_changed_listener( ): return - self._attr_is_closed = state.state != STATE_ON + if self._invert_state: + self._attr_is_closed = state.state == STATE_ON + else: + self._attr_is_closed = state.state != STATE_ON diff --git a/homeassistant/components/switch_as_x/entity.py b/homeassistant/components/switch_as_x/entity.py index 52d58157e34242..39c2a8cab60f22 100644 --- a/homeassistant/components/switch_as_x/entity.py +++ b/homeassistant/components/switch_as_x/entity.py @@ -106,7 +106,7 @@ def _async_state_changed_listener( registry.async_update_entity_options( self.entity_id, SWITCH_AS_X_DOMAIN, - {"entity_id": self._switch_entity_id}, + self.async_generate_entity_options(), ) if not self._is_new_entity or not ( @@ -141,6 +141,11 @@ def copy_expose_settings() -> None: copy_custom_name(wrapped_switch) copy_expose_settings() + @callback + def async_generate_entity_options(self) -> dict[str, Any]: + """Generate entity options.""" + return {"entity_id": self._switch_entity_id, "invert": False} + class BaseToggleEntity(BaseEntity, ToggleEntity): """Represents a Switch as a ToggleEntity.""" @@ -178,3 +183,25 @@ def async_state_changed_listener( return self._attr_is_on = state.state == STATE_ON + + +class BaseInvertableEntity(BaseEntity): + """Represents a Switch as an X.""" + + def __init__( + self, + hass: HomeAssistant, + config_entry_title: str, + domain: str, + invert: bool, + switch_entity_id: str, + unique_id: str, + ) -> None: + """Initialize Switch as an X.""" + super().__init__(hass, config_entry_title, domain, switch_entity_id, unique_id) + self._invert_state = invert + + @callback + def async_generate_entity_options(self) -> dict[str, Any]: + """Generate entity options.""" + return super().async_generate_entity_options() | {"invert": self._invert_state} diff --git a/homeassistant/components/switch_as_x/lock.py b/homeassistant/components/switch_as_x/lock.py index 9e7606865a11ce..528825c0300796 100644 --- a/homeassistant/components/switch_as_x/lock.py +++ b/homeassistant/components/switch_as_x/lock.py @@ -19,7 +19,8 @@ from homeassistant.helpers.event import EventStateChangedData from homeassistant.helpers.typing import EventType -from .entity import BaseEntity +from .const import CONF_INVERT +from .entity import BaseInvertableEntity async def async_setup_entry( @@ -39,6 +40,7 @@ async def async_setup_entry( hass, config_entry.title, LOCK_DOMAIN, + config_entry.options[CONF_INVERT], entity_id, config_entry.entry_id, ) @@ -46,14 +48,14 @@ async def async_setup_entry( ) -class LockSwitch(BaseEntity, LockEntity): +class LockSwitch(BaseInvertableEntity, 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, + SERVICE_TURN_ON if self._invert_state else SERVICE_TURN_OFF, {ATTR_ENTITY_ID: self._switch_entity_id}, blocking=True, context=self._context, @@ -63,7 +65,7 @@ async def async_unlock(self, **kwargs: Any) -> None: """Unlock the lock.""" await self.hass.services.async_call( SWITCH_DOMAIN, - SERVICE_TURN_ON, + SERVICE_TURN_OFF if self._invert_state else SERVICE_TURN_ON, {ATTR_ENTITY_ID: self._switch_entity_id}, blocking=True, context=self._context, @@ -83,4 +85,7 @@ def async_state_changed_listener( # 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 + if self._invert_state: + self._attr_is_locked = state.state == STATE_ON + else: + self._attr_is_locked = state.state != STATE_ON diff --git a/homeassistant/components/switch_as_x/strings.json b/homeassistant/components/switch_as_x/strings.json index 10adfd7686e176..81567ef9e40fa9 100644 --- a/homeassistant/components/switch_as_x/strings.json +++ b/homeassistant/components/switch_as_x/strings.json @@ -6,7 +6,23 @@ "description": "Pick a switch that you want to show up in Home Assistant as a light, cover or anything else. The original switch will be hidden.", "data": { "entity_id": "Switch", + "invert": "Invert state", "target_domain": "New Type" + }, + "data_description": { + "invert": "Invert state, only supported for cover, lock and valve." + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "invert": "[%key:component::switch_as_x::config::step::user::data::invert%]" + }, + "data_description": { + "invert": "[%key:component::switch_as_x::config::step::user::data_description::invert%]" } } } diff --git a/homeassistant/components/switch_as_x/valve.py b/homeassistant/components/switch_as_x/valve.py index 3a9fbc16247adf..971338764a5fbc 100644 --- a/homeassistant/components/switch_as_x/valve.py +++ b/homeassistant/components/switch_as_x/valve.py @@ -23,7 +23,8 @@ from homeassistant.helpers.event import EventStateChangedData from homeassistant.helpers.typing import EventType -from .entity import BaseEntity +from .const import CONF_INVERT +from .entity import BaseInvertableEntity async def async_setup_entry( @@ -43,6 +44,7 @@ async def async_setup_entry( hass, config_entry.title, VALVE_DOMAIN, + config_entry.options[CONF_INVERT], entity_id, config_entry.entry_id, ) @@ -50,7 +52,7 @@ async def async_setup_entry( ) -class ValveSwitch(BaseEntity, ValveEntity): +class ValveSwitch(BaseInvertableEntity, ValveEntity): """Represents a Switch as a Valve.""" _attr_supported_features = ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE @@ -60,7 +62,7 @@ async def async_open_valve(self, **kwargs: Any) -> None: """Open the valve.""" await self.hass.services.async_call( SWITCH_DOMAIN, - SERVICE_TURN_ON, + SERVICE_TURN_OFF if self._invert_state else SERVICE_TURN_ON, {ATTR_ENTITY_ID: self._switch_entity_id}, blocking=True, context=self._context, @@ -70,7 +72,7 @@ async def async_close_valve(self, **kwargs: Any) -> None: """Close valve.""" await self.hass.services.async_call( SWITCH_DOMAIN, - SERVICE_TURN_OFF, + SERVICE_TURN_ON if self._invert_state else SERVICE_TURN_OFF, {ATTR_ENTITY_ID: self._switch_entity_id}, blocking=True, context=self._context, @@ -88,4 +90,7 @@ def async_state_changed_listener( ): return - self._attr_is_closed = state.state != STATE_ON + if self._invert_state: + self._attr_is_closed = state.state == STATE_ON + else: + self._attr_is_closed = state.state != STATE_ON diff --git a/tests/components/switch_as_x/__init__.py b/tests/components/switch_as_x/__init__.py index d7cf944e624157..de6f1bac7906f5 100644 --- a/tests/components/switch_as_x/__init__.py +++ b/tests/components/switch_as_x/__init__.py @@ -1 +1,39 @@ """The tests for Switch as X platforms.""" + +from homeassistant.const import ( + STATE_CLOSED, + STATE_LOCKED, + STATE_OFF, + STATE_ON, + STATE_OPEN, + STATE_UNLOCKED, + Platform, +) + +PLATFORMS_TO_TEST = ( + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.LOCK, + Platform.SIREN, + Platform.VALVE, +) + +STATE_MAP = { + False: { + Platform.COVER: {STATE_ON: STATE_OPEN, STATE_OFF: STATE_CLOSED}, + Platform.FAN: {STATE_ON: STATE_ON, STATE_OFF: STATE_OFF}, + Platform.LIGHT: {STATE_ON: STATE_ON, STATE_OFF: STATE_OFF}, + Platform.LOCK: {STATE_ON: STATE_UNLOCKED, STATE_OFF: STATE_LOCKED}, + Platform.SIREN: {STATE_ON: STATE_ON, STATE_OFF: STATE_OFF}, + Platform.VALVE: {STATE_ON: STATE_OPEN, STATE_OFF: STATE_CLOSED}, + }, + True: { + Platform.COVER: {STATE_ON: STATE_CLOSED, STATE_OFF: STATE_OPEN}, + Platform.FAN: {STATE_ON: STATE_ON, STATE_OFF: STATE_OFF}, + Platform.LIGHT: {STATE_ON: STATE_ON, STATE_OFF: STATE_OFF}, + Platform.LOCK: {STATE_ON: STATE_LOCKED, STATE_OFF: STATE_UNLOCKED}, + Platform.SIREN: {STATE_ON: STATE_ON, STATE_OFF: STATE_OFF}, + Platform.VALVE: {STATE_ON: STATE_CLOSED, STATE_OFF: STATE_OPEN}, + }, +} diff --git a/tests/components/switch_as_x/test_config_flow.py b/tests/components/switch_as_x/test_config_flow.py index 51efbf99892a17..09661b0619c880 100644 --- a/tests/components/switch_as_x/test_config_flow.py +++ b/tests/components/switch_as_x/test_config_flow.py @@ -5,23 +5,21 @@ import pytest -from homeassistant import config_entries, data_entry_flow -from homeassistant.components.switch_as_x.const import CONF_TARGET_DOMAIN, DOMAIN -from homeassistant.const import CONF_ENTITY_ID, Platform +from homeassistant import config_entries +from homeassistant.components.switch_as_x.config_flow import SwitchAsXConfigFlowHandler +from homeassistant.components.switch_as_x.const import ( + CONF_INVERT, + CONF_TARGET_DOMAIN, + DOMAIN, +) +from homeassistant.const import CONF_ENTITY_ID, STATE_ON, Platform from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from homeassistant.helpers import entity_registry as er -from tests.common import MockConfigEntry +from . import PLATFORMS_TO_TEST, STATE_MAP -PLATFORMS_TO_TEST = ( - Platform.COVER, - Platform.FAN, - Platform.LIGHT, - Platform.LOCK, - Platform.SIREN, - Platform.VALVE, -) +from tests.common import MockConfigEntry @pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) @@ -41,6 +39,7 @@ async def test_config_flow( result["flow_id"], { CONF_ENTITY_ID: "switch.ceiling", + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, }, ) @@ -51,6 +50,7 @@ async def test_config_flow( assert result["data"] == {} assert result["options"] == { CONF_ENTITY_ID: "switch.ceiling", + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, } assert len(mock_setup_entry.mock_calls) == 1 @@ -59,6 +59,7 @@ async def test_config_flow( assert config_entry.data == {} assert config_entry.options == { CONF_ENTITY_ID: "switch.ceiling", + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, } @@ -96,6 +97,7 @@ async def test_config_flow_registered_entity( result["flow_id"], { CONF_ENTITY_ID: "switch.ceiling", + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, }, ) @@ -106,6 +108,7 @@ async def test_config_flow_registered_entity( assert result["data"] == {} assert result["options"] == { CONF_ENTITY_ID: "switch.ceiling", + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, } assert len(mock_setup_entry.mock_calls) == 1 @@ -114,6 +117,7 @@ async def test_config_flow_registered_entity( assert config_entry.data == {} assert config_entry.options == { CONF_ENTITY_ID: "switch.ceiling", + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, } @@ -125,26 +129,66 @@ async def test_config_flow_registered_entity( async def test_options( hass: HomeAssistant, target_domain: Platform, - mock_setup_entry: AsyncMock, ) -> None: """Test reconfiguring.""" + switch_state = STATE_ON + hass.states.async_set("switch.ceiling", switch_state) switch_as_x_config_entry = MockConfigEntry( data={}, domain=DOMAIN, options={ CONF_ENTITY_ID: "switch.ceiling", + CONF_INVERT: True, CONF_TARGET_DOMAIN: target_domain, }, title="ABC", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) 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() + state = hass.states.get(f"{target_domain}.abc") + assert state.state == STATE_MAP[True][target_domain][switch_state] + config_entry = hass.config_entries.async_entries(DOMAIN)[0] assert config_entry - # Switch light has no options flow - with pytest.raises(data_entry_flow.UnknownHandler): - await hass.config_entries.options.async_init(config_entry.entry_id) + result = await hass.config_entries.options.async_init(config_entry.entry_id) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + schema = result["data_schema"].schema + schema_key = next(k for k in schema if k == CONF_INVERT) + assert schema_key.description["suggested_value"] is True + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_INVERT: False, + }, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_ENTITY_ID: "switch.ceiling", + CONF_INVERT: False, + CONF_TARGET_DOMAIN: target_domain, + } + assert config_entry.data == {} + assert config_entry.options == { + CONF_ENTITY_ID: "switch.ceiling", + CONF_INVERT: False, + CONF_TARGET_DOMAIN: target_domain, + } + assert config_entry.title == "ABC" + + # Check config entry is reloaded with new options + await hass.async_block_till_done() + + # Check the entity was updated, no new entity was created + assert len(hass.states.async_all()) == 2 + + # Check the state of the entity has changed as expected + state = hass.states.get(f"{target_domain}.abc") + assert state.state == STATE_MAP[False][target_domain][switch_state] diff --git a/tests/components/switch_as_x/test_cover.py b/tests/components/switch_as_x/test_cover.py index d0aef0b9490616..78a76c20bebf06 100644 --- a/tests/components/switch_as_x/test_cover.py +++ b/tests/components/switch_as_x/test_cover.py @@ -1,7 +1,13 @@ """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.components.switch_as_x.config_flow import SwitchAsXConfigFlowHandler +from homeassistant.components.switch_as_x.const import ( + CONF_INVERT, + CONF_TARGET_DOMAIN, + DOMAIN, +) from homeassistant.const import ( CONF_ENTITY_ID, SERVICE_CLOSE_COVER, @@ -28,9 +34,12 @@ async def test_default_state(hass: HomeAssistant) -> None: domain=DOMAIN, options={ CONF_ENTITY_ID: "switch.test", + CONF_INVERT: False, CONF_TARGET_DOMAIN: Platform.COVER, }, title="Garage Door", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -51,9 +60,12 @@ async def test_service_calls(hass: HomeAssistant) -> None: domain=DOMAIN, options={ CONF_ENTITY_ID: "switch.decorative_lights", + CONF_INVERT: False, CONF_TARGET_DOMAIN: Platform.COVER, }, title="Title is ignored", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -120,3 +132,86 @@ async def test_service_calls(hass: HomeAssistant) -> None: assert hass.states.get("switch.decorative_lights").state == STATE_ON assert hass.states.get("cover.decorative_lights").state == STATE_OPEN + + +async def test_service_calls_inverted(hass: HomeAssistant) -> None: + """Test service calls to cover.""" + await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) + await hass.async_block_till_done() + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.decorative_lights", + CONF_INVERT: True, + CONF_TARGET_DOMAIN: Platform.COVER, + }, + title="Title is ignored", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, + ) + 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.decorative_lights").state == STATE_CLOSED + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_TOGGLE, + {CONF_ENTITY_ID: "cover.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("cover.decorative_lights").state == STATE_OPEN + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {CONF_ENTITY_ID: "cover.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("cover.decorative_lights").state == STATE_OPEN + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {CONF_ENTITY_ID: "cover.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("cover.decorative_lights").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.decorative_lights").state == STATE_CLOSED + + 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.decorative_lights").state == STATE_OPEN + + 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.decorative_lights").state == STATE_CLOSED diff --git a/tests/components/switch_as_x/test_fan.py b/tests/components/switch_as_x/test_fan.py index cf6789d439cd8f..c459831b3ad405 100644 --- a/tests/components/switch_as_x/test_fan.py +++ b/tests/components/switch_as_x/test_fan.py @@ -1,7 +1,12 @@ """Tests for the Switch as X Fan platform.""" from homeassistant.components.fan import DOMAIN as FAN_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.components.switch_as_x.const import CONF_TARGET_DOMAIN, DOMAIN +from homeassistant.components.switch_as_x.config_flow import SwitchAsXConfigFlowHandler +from homeassistant.components.switch_as_x.const import ( + CONF_INVERT, + CONF_TARGET_DOMAIN, + DOMAIN, +) from homeassistant.const import ( CONF_ENTITY_ID, SERVICE_TOGGLE, @@ -24,9 +29,12 @@ async def test_default_state(hass: HomeAssistant) -> None: domain=DOMAIN, options={ CONF_ENTITY_ID: "switch.test", + CONF_INVERT: False, CONF_TARGET_DOMAIN: Platform.FAN, }, title="Wind Machine", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -47,9 +55,95 @@ async def test_service_calls(hass: HomeAssistant) -> None: domain=DOMAIN, options={ CONF_ENTITY_ID: "switch.decorative_lights", + CONF_INVERT: False, + CONF_TARGET_DOMAIN: Platform.FAN, + }, + title="Title is ignored", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, + ) + 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("fan.decorative_lights").state == STATE_ON + + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TOGGLE, + {CONF_ENTITY_ID: "fan.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("fan.decorative_lights").state == STATE_OFF + + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + {CONF_ENTITY_ID: "fan.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("fan.decorative_lights").state == STATE_ON + + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_OFF, + {CONF_ENTITY_ID: "fan.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("fan.decorative_lights").state == STATE_OFF + + 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("fan.decorative_lights").state == STATE_ON + + 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("fan.decorative_lights").state == STATE_OFF + + 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("fan.decorative_lights").state == STATE_ON + + +async def test_service_calls_inverted(hass: HomeAssistant) -> None: + """Test service calls affecting the switch as fan entity.""" + await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) + await hass.async_block_till_done() + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.decorative_lights", + CONF_INVERT: True, CONF_TARGET_DOMAIN: Platform.FAN, }, title="Title is ignored", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index 738127faf43909..2b0a67f3984834 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -6,7 +6,13 @@ import pytest from homeassistant.components.homeassistant import exposed_entities -from homeassistant.components.switch_as_x.const import CONF_TARGET_DOMAIN, DOMAIN +from homeassistant.components.switch_as_x.config_flow import SwitchAsXConfigFlowHandler +from homeassistant.components.switch_as_x.const import ( + CONF_INVERT, + CONF_TARGET_DOMAIN, + DOMAIN, +) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_ENTITY_ID, STATE_CLOSED, @@ -22,6 +28,8 @@ from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component +from . import PLATFORMS_TO_TEST + from tests.common import MockConfigEntry EXPOSE_SETTINGS = { @@ -30,15 +38,6 @@ "conversation": True, } -PLATFORMS_TO_TEST = ( - Platform.COVER, - Platform.FAN, - Platform.LIGHT, - Platform.LOCK, - Platform.SIREN, - Platform.VALVE, -) - @pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) async def test_config_entry_unregistered_uuid( @@ -52,9 +51,12 @@ async def test_config_entry_unregistered_uuid( domain=DOMAIN, options={ CONF_ENTITY_ID: fake_uuid, + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, }, title="ABC", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) config_entry.add_to_hass(hass) @@ -92,9 +94,12 @@ async def test_entity_registry_events( domain=DOMAIN, options={ CONF_ENTITY_ID: registry_entry.id, + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, }, title="ABC", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) config_entry.add_to_hass(hass) @@ -169,9 +174,12 @@ async def test_device_registry_config_entry_1( domain=DOMAIN, options={ CONF_ENTITY_ID: switch_entity_entry.id, + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, }, title="ABC", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) switch_as_x_config_entry.add_to_hass(hass) @@ -224,9 +232,12 @@ async def test_device_registry_config_entry_2( domain=DOMAIN, options={ CONF_ENTITY_ID: switch_entity_entry.id, + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, }, title="ABC", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) switch_as_x_config_entry.add_to_hass(hass) @@ -258,9 +269,12 @@ async def test_config_entry_entity_id( domain=DOMAIN, options={ CONF_ENTITY_ID: "switch.abc", + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, }, title="ABC", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) config_entry.add_to_hass(hass) @@ -296,9 +310,12 @@ async def test_config_entry_uuid(hass: HomeAssistant, target_domain: Platform) - domain=DOMAIN, options={ CONF_ENTITY_ID: registry_entry.id, + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, }, title="ABC", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) config_entry.add_to_hass(hass) @@ -331,9 +348,12 @@ async def test_device(hass: HomeAssistant, target_domain: Platform) -> None: domain=DOMAIN, options={ CONF_ENTITY_ID: switch_entity_entry.id, + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, }, title="ABC", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) switch_as_x_config_entry.add_to_hass(hass) @@ -360,9 +380,12 @@ async def test_setup_and_remove_config_entry( domain=DOMAIN, options={ CONF_ENTITY_ID: "switch.test", + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, }, title="ABC", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) switch_as_x_config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(switch_as_x_config_entry.entry_id) @@ -409,9 +432,12 @@ async def test_reset_hidden_by( domain=DOMAIN, options={ CONF_ENTITY_ID: switch_entity_entry.id, + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, }, title="ABC", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) switch_as_x_config_entry.add_to_hass(hass) @@ -445,9 +471,12 @@ async def test_entity_category_inheritance( domain=DOMAIN, options={ CONF_ENTITY_ID: switch_entity_entry.id, + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, }, title="ABC", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) switch_as_x_config_entry.add_to_hass(hass) @@ -481,9 +510,12 @@ async def test_entity_options( domain=DOMAIN, options={ CONF_ENTITY_ID: switch_entity_entry.id, + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, }, title="ABC", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) switch_as_x_config_entry.add_to_hass(hass) @@ -494,7 +526,7 @@ async def test_entity_options( assert entity_entry assert entity_entry.device_id == switch_entity_entry.device_id assert entity_entry.options == { - DOMAIN: {"entity_id": switch_entity_entry.entity_id} + DOMAIN: {"entity_id": switch_entity_entry.entity_id, "invert": False}, } @@ -534,9 +566,12 @@ async def test_entity_name( domain=DOMAIN, options={ CONF_ENTITY_ID: switch_entity_entry.id, + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, }, title="ABC", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) switch_as_x_config_entry.add_to_hass(hass) @@ -550,7 +585,7 @@ async def test_entity_name( assert entity_entry.name is None assert entity_entry.original_name is None assert entity_entry.options == { - DOMAIN: {"entity_id": switch_entity_entry.entity_id} + DOMAIN: {"entity_id": switch_entity_entry.entity_id, "invert": False} } @@ -592,9 +627,12 @@ async def test_custom_name_1( domain=DOMAIN, options={ CONF_ENTITY_ID: switch_entity_entry.id, + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, }, title="ABC", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) switch_as_x_config_entry.add_to_hass(hass) @@ -610,7 +648,7 @@ async def test_custom_name_1( assert entity_entry.name == "Custom entity name" assert entity_entry.original_name == "Original entity name" assert entity_entry.options == { - DOMAIN: {"entity_id": switch_entity_entry.entity_id} + DOMAIN: {"entity_id": switch_entity_entry.entity_id, "invert": False} } @@ -656,9 +694,12 @@ async def test_custom_name_2( domain=DOMAIN, options={ CONF_ENTITY_ID: switch_entity_entry.id, + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, }, title="ABC", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) switch_as_x_config_entry.add_to_hass(hass) @@ -689,7 +730,7 @@ async def test_custom_name_2( assert entity_entry.name == "Old custom entity name" assert entity_entry.original_name == "Original entity name" assert entity_entry.options == { - DOMAIN: {"entity_id": switch_entity_entry.entity_id} + DOMAIN: {"entity_id": switch_entity_entry.entity_id, "invert": False} } @@ -719,9 +760,12 @@ async def test_import_expose_settings_1( domain=DOMAIN, options={ CONF_ENTITY_ID: switch_entity_entry.id, + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, }, title="ABC", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) switch_as_x_config_entry.add_to_hass(hass) @@ -777,9 +821,12 @@ async def test_import_expose_settings_2( domain=DOMAIN, options={ CONF_ENTITY_ID: switch_entity_entry.id, + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, }, title="ABC", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) switch_as_x_config_entry.add_to_hass(hass) @@ -842,9 +889,12 @@ async def test_restore_expose_settings( domain=DOMAIN, options={ CONF_ENTITY_ID: switch_entity_entry.id, + CONF_INVERT: False, CONF_TARGET_DOMAIN: target_domain, }, title="ABC", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) switch_as_x_config_entry.add_to_hass(hass) @@ -871,3 +921,80 @@ async def test_restore_expose_settings( ) for assistant in EXPOSE_SETTINGS: assert expose_settings[assistant]["should_expose"] == EXPOSE_SETTINGS[assistant] + + +@pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) +async def test_migrate( + hass: HomeAssistant, + target_domain: Platform, +) -> None: + """Test migration.""" + registry = er.async_get(hass) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.test", + CONF_TARGET_DOMAIN: target_domain, + }, + title="ABC", + version=1, + minor_version=1, + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Check migration was successful and added invert option + assert config_entry.state == ConfigEntryState.LOADED + assert config_entry.options == { + CONF_ENTITY_ID: "switch.test", + CONF_INVERT: False, + CONF_TARGET_DOMAIN: target_domain, + } + assert config_entry.version == SwitchAsXConfigFlowHandler.VERSION + assert config_entry.minor_version == SwitchAsXConfigFlowHandler.MINOR_VERSION + + # Check the state and entity registry entry are present + assert hass.states.get(f"{target_domain}.abc") is not None + assert registry.async_get(f"{target_domain}.abc") is not None + + +@pytest.mark.parametrize("target_domain", PLATFORMS_TO_TEST) +async def test_migrate_from_future( + hass: HomeAssistant, + target_domain: Platform, +) -> None: + """Test migration.""" + registry = er.async_get(hass) + + # Setup the config entry + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.test", + CONF_TARGET_DOMAIN: target_domain, + }, + title="ABC", + version=2, + minor_version=1, + ) + config_entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Check migration was not successful and did not add invert option + assert config_entry.state == ConfigEntryState.MIGRATION_ERROR + assert config_entry.options == { + CONF_ENTITY_ID: "switch.test", + CONF_TARGET_DOMAIN: target_domain, + } + assert config_entry.version == 2 + assert config_entry.minor_version == 1 + + # Check the state and entity registry entry are not present + assert hass.states.get(f"{target_domain}.abc") is None + assert registry.async_get(f"{target_domain}.abc") is None diff --git a/tests/components/switch_as_x/test_light.py b/tests/components/switch_as_x/test_light.py index 9a33bab20a89c1..5bdec990fd4ad0 100644 --- a/tests/components/switch_as_x/test_light.py +++ b/tests/components/switch_as_x/test_light.py @@ -11,7 +11,12 @@ ColorMode, ) from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.components.switch_as_x.const import CONF_TARGET_DOMAIN, DOMAIN +from homeassistant.components.switch_as_x.config_flow import SwitchAsXConfigFlowHandler +from homeassistant.components.switch_as_x.const import ( + CONF_INVERT, + CONF_TARGET_DOMAIN, + DOMAIN, +) from homeassistant.const import ( CONF_ENTITY_ID, SERVICE_TOGGLE, @@ -34,9 +39,12 @@ async def test_default_state(hass: HomeAssistant) -> None: domain=DOMAIN, options={ CONF_ENTITY_ID: "switch.test", + CONF_INVERT: False, CONF_TARGET_DOMAIN: Platform.LIGHT, }, title="Christmas Tree Lights", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -64,9 +72,12 @@ async def test_light_service_calls(hass: HomeAssistant) -> None: domain=DOMAIN, options={ CONF_ENTITY_ID: "switch.decorative_lights", + CONF_INVERT: False, CONF_TARGET_DOMAIN: Platform.LIGHT, }, title="decorative_lights", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -118,9 +129,112 @@ async def test_switch_service_calls(hass: HomeAssistant) -> None: domain=DOMAIN, options={ CONF_ENTITY_ID: "switch.decorative_lights", + CONF_INVERT: False, + CONF_TARGET_DOMAIN: Platform.LIGHT, + }, + title="Title is ignored", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, + ) + 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 == STATE_ON + + 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("light.decorative_lights").state == STATE_OFF + + 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("light.decorative_lights").state == STATE_ON + + +async def test_light_service_calls_inverted(hass: HomeAssistant) -> None: + """Test service calls to light.""" + await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) + await hass.async_block_till_done() + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.decorative_lights", + CONF_INVERT: True, + CONF_TARGET_DOMAIN: Platform.LIGHT, + }, + title="decorative_lights", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, + ) + 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 == STATE_ON + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TOGGLE, + {CONF_ENTITY_ID: "light.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("light.decorative_lights").state == STATE_OFF + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {CONF_ENTITY_ID: "light.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("light.decorative_lights").state == STATE_ON + assert ( + hass.states.get("light.decorative_lights").attributes.get(ATTR_COLOR_MODE) + == ColorMode.ONOFF + ) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {CONF_ENTITY_ID: "light.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("light.decorative_lights").state == STATE_OFF + + +async def test_switch_service_calls_inverted(hass: HomeAssistant) -> None: + """Test service calls to switch.""" + await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) + await hass.async_block_till_done() + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.decorative_lights", + CONF_INVERT: True, CONF_TARGET_DOMAIN: Platform.LIGHT, }, title="Title is ignored", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/switch_as_x/test_lock.py b/tests/components/switch_as_x/test_lock.py index 6d30ac4646bd0c..bdf1b754c5ac4e 100644 --- a/tests/components/switch_as_x/test_lock.py +++ b/tests/components/switch_as_x/test_lock.py @@ -1,7 +1,12 @@ """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.components.switch_as_x.config_flow import SwitchAsXConfigFlowHandler +from homeassistant.components.switch_as_x.const import ( + CONF_INVERT, + CONF_TARGET_DOMAIN, + DOMAIN, +) from homeassistant.const import ( CONF_ENTITY_ID, SERVICE_LOCK, @@ -28,9 +33,12 @@ async def test_default_state(hass: HomeAssistant) -> None: domain=DOMAIN, options={ CONF_ENTITY_ID: "switch.test", + CONF_INVERT: False, CONF_TARGET_DOMAIN: Platform.LOCK, }, title="candy_jar", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -50,9 +58,12 @@ async def test_service_calls(hass: HomeAssistant) -> None: domain=DOMAIN, options={ CONF_ENTITY_ID: "switch.decorative_lights", + CONF_INVERT: False, CONF_TARGET_DOMAIN: Platform.LOCK, }, title="Title is ignored", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -109,3 +120,76 @@ async def test_service_calls(hass: HomeAssistant) -> None: assert hass.states.get("switch.decorative_lights").state == STATE_OFF assert hass.states.get("lock.decorative_lights").state == STATE_LOCKED + + +async def test_service_calls_inverted(hass: HomeAssistant) -> None: + """Test service calls affecting the switch as lock entity.""" + await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) + await hass.async_block_till_done() + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.decorative_lights", + CONF_INVERT: True, + CONF_TARGET_DOMAIN: Platform.LOCK, + }, + title="Title is ignored", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, + ) + 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.decorative_lights").state == STATE_LOCKED + + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_LOCK, + {CONF_ENTITY_ID: "lock.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("lock.decorative_lights").state == STATE_LOCKED + + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_UNLOCK, + {CONF_ENTITY_ID: "lock.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("lock.decorative_lights").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.decorative_lights").state == STATE_UNLOCKED + + 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.decorative_lights").state == STATE_LOCKED + + 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.decorative_lights").state == STATE_UNLOCKED diff --git a/tests/components/switch_as_x/test_siren.py b/tests/components/switch_as_x/test_siren.py index f776ab2ae014e2..581aa74daff0b1 100644 --- a/tests/components/switch_as_x/test_siren.py +++ b/tests/components/switch_as_x/test_siren.py @@ -1,7 +1,12 @@ """Tests for the Switch as X Siren platform.""" from homeassistant.components.siren import DOMAIN as SIREN_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.components.switch_as_x.const import CONF_TARGET_DOMAIN, DOMAIN +from homeassistant.components.switch_as_x.config_flow import SwitchAsXConfigFlowHandler +from homeassistant.components.switch_as_x.const import ( + CONF_INVERT, + CONF_TARGET_DOMAIN, + DOMAIN, +) from homeassistant.const import ( CONF_ENTITY_ID, SERVICE_TOGGLE, @@ -24,9 +29,12 @@ async def test_default_state(hass: HomeAssistant) -> None: domain=DOMAIN, options={ CONF_ENTITY_ID: "switch.test", + CONF_INVERT: False, CONF_TARGET_DOMAIN: Platform.SIREN, }, title="Noise Maker", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -47,9 +55,95 @@ async def test_service_calls(hass: HomeAssistant) -> None: domain=DOMAIN, options={ CONF_ENTITY_ID: "switch.decorative_lights", + CONF_INVERT: False, + CONF_TARGET_DOMAIN: Platform.SIREN, + }, + title="Title is ignored", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, + ) + 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("siren.decorative_lights").state == STATE_ON + + await hass.services.async_call( + SIREN_DOMAIN, + SERVICE_TOGGLE, + {CONF_ENTITY_ID: "siren.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("siren.decorative_lights").state == STATE_OFF + + await hass.services.async_call( + SIREN_DOMAIN, + SERVICE_TURN_ON, + {CONF_ENTITY_ID: "siren.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("siren.decorative_lights").state == STATE_ON + + await hass.services.async_call( + SIREN_DOMAIN, + SERVICE_TURN_OFF, + {CONF_ENTITY_ID: "siren.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("siren.decorative_lights").state == STATE_OFF + + 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("siren.decorative_lights").state == STATE_ON + + 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("siren.decorative_lights").state == STATE_OFF + + 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("siren.decorative_lights").state == STATE_ON + + +async def test_service_calls_inverted(hass: HomeAssistant) -> None: + """Test service calls affecting the switch as siren entity.""" + await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) + await hass.async_block_till_done() + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.decorative_lights", + CONF_INVERT: True, CONF_TARGET_DOMAIN: Platform.SIREN, }, title="Title is ignored", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) diff --git a/tests/components/switch_as_x/test_valve.py b/tests/components/switch_as_x/test_valve.py index da20c544f644d9..b76da012bdeb87 100644 --- a/tests/components/switch_as_x/test_valve.py +++ b/tests/components/switch_as_x/test_valve.py @@ -1,6 +1,11 @@ """Tests for the Switch as X Valve platform.""" from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.components.switch_as_x.const import CONF_TARGET_DOMAIN, DOMAIN +from homeassistant.components.switch_as_x.config_flow import SwitchAsXConfigFlowHandler +from homeassistant.components.switch_as_x.const import ( + CONF_INVERT, + CONF_TARGET_DOMAIN, + DOMAIN, +) from homeassistant.components.valve import DOMAIN as VALVE_DOMAIN from homeassistant.const import ( CONF_ENTITY_ID, @@ -28,9 +33,12 @@ async def test_default_state(hass: HomeAssistant) -> None: domain=DOMAIN, options={ CONF_ENTITY_ID: "switch.test", + CONF_INVERT: False, CONF_TARGET_DOMAIN: Platform.VALVE, }, title="Garage Door", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -51,9 +59,12 @@ async def test_service_calls(hass: HomeAssistant) -> None: domain=DOMAIN, options={ CONF_ENTITY_ID: "switch.decorative_lights", + CONF_INVERT: False, CONF_TARGET_DOMAIN: Platform.VALVE, }, title="Title is ignored", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, ) config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -120,3 +131,86 @@ async def test_service_calls(hass: HomeAssistant) -> None: assert hass.states.get("switch.decorative_lights").state == STATE_ON assert hass.states.get("valve.decorative_lights").state == STATE_OPEN + + +async def test_service_calls_inverted(hass: HomeAssistant) -> None: + """Test service calls to valve.""" + await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) + await hass.async_block_till_done() + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.decorative_lights", + CONF_INVERT: True, + CONF_TARGET_DOMAIN: Platform.VALVE, + }, + title="Title is ignored", + version=SwitchAsXConfigFlowHandler.VERSION, + minor_version=SwitchAsXConfigFlowHandler.MINOR_VERSION, + ) + 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("valve.decorative_lights").state == STATE_CLOSED + + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_TOGGLE, + {CONF_ENTITY_ID: "valve.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("valve.decorative_lights").state == STATE_OPEN + + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_OPEN_VALVE, + {CONF_ENTITY_ID: "valve.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("valve.decorative_lights").state == STATE_OPEN + + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_CLOSE_VALVE, + {CONF_ENTITY_ID: "valve.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("valve.decorative_lights").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("valve.decorative_lights").state == STATE_CLOSED + + 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("valve.decorative_lights").state == STATE_OPEN + + 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("valve.decorative_lights").state == STATE_CLOSED