From 1ae6f1e9e2495d1df2d5071d3ca8be9365644495 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 19 Dec 2023 20:16:18 +0100 Subject: [PATCH] Add valve support to switch_as_x (#105988) --- .../components/switch_as_x/config_flow.py | 1 + homeassistant/components/switch_as_x/valve.py | 91 +++++++++++++ .../switch_as_x/test_config_flow.py | 1 + tests/components/switch_as_x/test_init.py | 2 + tests/components/switch_as_x/test_valve.py | 122 ++++++++++++++++++ 5 files changed, 217 insertions(+) create mode 100644 homeassistant/components/switch_as_x/valve.py create mode 100644 tests/components/switch_as_x/test_valve.py diff --git a/homeassistant/components/switch_as_x/config_flow.py b/homeassistant/components/switch_as_x/config_flow.py index 8b6527eb49ed..90f6b9858938 100644 --- a/homeassistant/components/switch_as_x/config_flow.py +++ b/homeassistant/components/switch_as_x/config_flow.py @@ -22,6 +22,7 @@ TARGET_DOMAIN_OPTIONS = [ selector.SelectOptionDict(value=Platform.LIGHT, label="Light"), selector.SelectOptionDict(value=Platform.LOCK, label="Lock"), selector.SelectOptionDict(value=Platform.SIREN, label="Siren"), + selector.SelectOptionDict(value=Platform.VALVE, label="Valve"), ] CONFIG_FLOW = { diff --git a/homeassistant/components/switch_as_x/valve.py b/homeassistant/components/switch_as_x/valve.py new file mode 100644 index 000000000000..3a9fbc16247a --- /dev/null +++ b/homeassistant/components/switch_as_x/valve.py @@ -0,0 +1,91 @@ +"""Valve support for switch entities.""" +from __future__ import annotations + +from typing import Any + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.components.valve import ( + DOMAIN as VALVE_DOMAIN, + ValveEntity, + ValveEntityFeature, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry as er +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.event import EventStateChangedData +from homeassistant.helpers.typing import EventType + +from .entity import BaseEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Initialize Valve 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( + [ + ValveSwitch( + hass, + config_entry.title, + VALVE_DOMAIN, + entity_id, + config_entry.entry_id, + ) + ] + ) + + +class ValveSwitch(BaseEntity, ValveEntity): + """Represents a Switch as a Valve.""" + + _attr_supported_features = ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE + _attr_reports_position = False + + async def async_open_valve(self, **kwargs: Any) -> None: + """Open the valve.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: self._switch_entity_id}, + blocking=True, + context=self._context, + ) + + async def async_close_valve(self, **kwargs: Any) -> None: + """Close valve.""" + await self.hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: self._switch_entity_id}, + blocking=True, + context=self._context, + ) + + @callback + def async_state_changed_listener( + self, event: EventType[EventStateChangedData] | None = None + ) -> None: + """Handle child updates.""" + super().async_state_changed_listener(event) + if ( + not self.available + or (state := self.hass.states.get(self._switch_entity_id)) is None + ): + return + + self._attr_is_closed = state.state != STATE_ON diff --git a/tests/components/switch_as_x/test_config_flow.py b/tests/components/switch_as_x/test_config_flow.py index 412cbc4333b2..51efbf99892a 100644 --- a/tests/components/switch_as_x/test_config_flow.py +++ b/tests/components/switch_as_x/test_config_flow.py @@ -20,6 +20,7 @@ PLATFORMS_TO_TEST = ( Platform.LIGHT, Platform.LOCK, Platform.SIREN, + Platform.VALVE, ) diff --git a/tests/components/switch_as_x/test_init.py b/tests/components/switch_as_x/test_init.py index a0c0bfca8254..738127faf439 100644 --- a/tests/components/switch_as_x/test_init.py +++ b/tests/components/switch_as_x/test_init.py @@ -36,6 +36,7 @@ PLATFORMS_TO_TEST = ( Platform.LIGHT, Platform.LOCK, Platform.SIREN, + Platform.VALVE, ) @@ -72,6 +73,7 @@ async def test_config_entry_unregistered_uuid( (Platform.LIGHT, STATE_ON, STATE_OFF), (Platform.LOCK, STATE_UNLOCKED, STATE_LOCKED), (Platform.SIREN, STATE_ON, STATE_OFF), + (Platform.VALVE, STATE_OPEN, STATE_CLOSED), ), ) async def test_entity_registry_events( diff --git a/tests/components/switch_as_x/test_valve.py b/tests/components/switch_as_x/test_valve.py new file mode 100644 index 000000000000..da20c544f644 --- /dev/null +++ b/tests/components/switch_as_x/test_valve.py @@ -0,0 +1,122 @@ +"""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.valve import DOMAIN as VALVE_DOMAIN +from homeassistant.const import ( + CONF_ENTITY_ID, + SERVICE_CLOSE_VALVE, + SERVICE_OPEN_VALVE, + SERVICE_TOGGLE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_CLOSED, + STATE_OFF, + STATE_ON, + STATE_OPEN, + Platform, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def test_default_state(hass: HomeAssistant) -> None: + """Test valve switch default state.""" + config_entry = MockConfigEntry( + data={}, + domain=DOMAIN, + options={ + CONF_ENTITY_ID: "switch.test", + CONF_TARGET_DOMAIN: Platform.VALVE, + }, + title="Garage Door", + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("valve.garage_door") + assert state is not None + assert state.state == "unavailable" + assert state.attributes["supported_features"] == 3 + + +async def test_service_calls(hass: HomeAssistant) -> None: + """Test service calls to 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_TARGET_DOMAIN: Platform.VALVE, + }, + title="Title is ignored", + ) + 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_OPEN + + 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_CLOSED + + 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_ON + 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_OFF + 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_OPEN + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {CONF_ENTITY_ID: "switch.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_OFF + assert hass.states.get("valve.decorative_lights").state == STATE_CLOSED + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TOGGLE, + {CONF_ENTITY_ID: "switch.decorative_lights"}, + blocking=True, + ) + + assert hass.states.get("switch.decorative_lights").state == STATE_ON + assert hass.states.get("valve.decorative_lights").state == STATE_OPEN