diff --git a/homeassistant/components/switch_as_x/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py index ff5c60274ef..6247d55b1fa 100644 --- a/homeassistant/components/switch_as_x/config_flow.py +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -23,6 +23,7 @@ CONFIG_FLOW = { "select": { "options": [ {"value": Platform.COVER, "label": "Cover"}, + {"value": Platform.FAN, "label": "Fan"}, {"value": Platform.LIGHT, "label": "Light"}, {"value": Platform.SIREN, "label": "Siren"}, ] diff --git a/homeassistant/components/switch_as_x/fan.py b/homeassistant/components/switch_as_x/fan.py new file mode 100644 index 00000000000..546e22b3fc1 --- /dev/null +++ b/homeassistant/components/switch_as_x/fan.py @@ -0,0 +1,64 @@ +"""Fan support for switch entities.""" +from __future__ import annotations + +from homeassistant.components.fan import FanEntity +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 homeassistant.helpers.entity_platform import AddEntitiesCallback + +from .entity import BaseToggleEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Fan 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( + [ + FanSwitch( + config_entry.title, + entity_id, + config_entry.entry_id, + device_id, + ) + ] + ) + + +class FanSwitch(BaseToggleEntity, FanEntity): + """Represents a Switch as a Fan.""" + + @property + def is_on(self) -> bool | None: + """Return true if the entity is on. + + Fan logic uses speed percentage or preset mode to determine + its it on or off, however, when using a wrapped switch, we + just use the wrapped switch's state. + """ + return self._attr_is_on + + # pylint: disable=arguments-differ + async def async_turn_on( + self, + percentage: int | None = None, + preset_mode: str | None = None, + **kwargs, + ) -> None: + """Turn on the fan. + + Arguments of the turn_on methods fan entity differ, + thus we need to override them here. + """ + await super().async_turn_on() diff --git a/tests/components/switch_as_x/test_fan.py b/tests/components/switch_as_x/test_fan.py new file mode 100644 index 00000000000..b7b746344b3 --- /dev/null +++ b/tests/components/switch_as_x/test_fan.py @@ -0,0 +1,117 @@ +"""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.const import ( + CONF_ENTITY_ID, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, + 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 fan switch default state.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.test", + CONF_TARGET_DOMAIN: Platform.FAN, + }, + title="Wind Machine", + ) + 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("fan.wind_machine") + assert state is not None + assert state.state == "unavailable" + assert state.attributes["supported_features"] == 0 + + +async def test_service_calls(hass: HomeAssistant) -> None: + """Test service calls affecting the switch as fan entity.""" + await async_setup_component(hass, "switch", {"switch": [{"platform": "demo"}]}) + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.decorative_lights", + CONF_TARGET_DOMAIN: Platform.FAN, + }, + title="wind_machine", + ) + 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.wind_machine").state == STATE_ON + + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TOGGLE, + {CONF_ENTITY_ID: "fan.wind_machine"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("fan.wind_machine").state == STATE_OFF + + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + {CONF_ENTITY_ID: "fan.wind_machine"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("fan.wind_machine").state == STATE_ON + + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_OFF, + {CONF_ENTITY_ID: "fan.wind_machine"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("fan.wind_machine").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.wind_machine").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.wind_machine").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.wind_machine").state == STATE_ON diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index a8145fe201a..7a7002de094 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -12,7 +12,13 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize( - "target_domain", (Platform.LIGHT, Platform.COVER, Platform.SIREN) + "target_domain", + ( + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.SIREN, + ), ) async def test_config_entry_unregistered_uuid( hass: HomeAssistant, target_domain: str @@ -38,7 +44,14 @@ async def test_config_entry_unregistered_uuid( assert len(hass.states.async_all()) == 0 -@pytest.mark.parametrize("target_domain", (Platform.LIGHT, Platform.SIREN)) +@pytest.mark.parametrize( + "target_domain", + ( + Platform.FAN, + Platform.LIGHT, + Platform.SIREN, + ), +) async def test_entity_registry_events(hass: HomeAssistant, target_domain: str) -> None: """Test entity registry events are tracked.""" registry = er.async_get(hass) @@ -96,7 +109,13 @@ async def test_entity_registry_events(hass: HomeAssistant, target_domain: str) - @pytest.mark.parametrize( - "target_domain", (Platform.LIGHT, Platform.COVER, Platform.SIREN) + "target_domain", + ( + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.SIREN, + ), ) async def test_device_registry_config_entry_1( hass: HomeAssistant, target_domain: str @@ -156,7 +175,13 @@ async def test_device_registry_config_entry_1( @pytest.mark.parametrize( - "target_domain", (Platform.LIGHT, Platform.COVER, Platform.SIREN) + "target_domain", + ( + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.SIREN, + ), ) async def test_device_registry_config_entry_2( hass: HomeAssistant, target_domain: str @@ -209,7 +234,13 @@ async def test_device_registry_config_entry_2( @pytest.mark.parametrize( - "target_domain", (Platform.LIGHT, Platform.COVER, Platform.SIREN) + "target_domain", + ( + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.SIREN, + ), ) async def test_config_entry_entity_id( hass: HomeAssistant, target_domain: Platform @@ -246,7 +277,13 @@ async def test_config_entry_entity_id( @pytest.mark.parametrize( - "target_domain", (Platform.LIGHT, Platform.COVER, Platform.SIREN) + "target_domain", + ( + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.SIREN, + ), ) async def test_config_entry_uuid(hass: HomeAssistant, target_domain: Platform) -> None: """Test light switch setup from config entry with entity registry id.""" @@ -272,7 +309,13 @@ async def test_config_entry_uuid(hass: HomeAssistant, target_domain: Platform) - @pytest.mark.parametrize( - "target_domain", (Platform.LIGHT, Platform.COVER, Platform.SIREN) + "target_domain", + ( + Platform.COVER, + Platform.FAN, + Platform.LIGHT, + Platform.SIREN, + ), ) async def test_device(hass: HomeAssistant, target_domain: Platform) -> None: """Test the entity is added to the wrapped entity's device."""