From f7927f9da1fd13b996475ac17d7909e6240de334 Mon Sep 17 00:00:00 2001 From: Tatham Oddie Date: Sun, 2 Mar 2025 07:54:48 +1000 Subject: [PATCH] Introduce demo valve (#138187) --- homeassistant/components/demo/__init__.py | 1 + homeassistant/components/demo/valve.py | 89 +++++++++++++++++++++++ tests/components/demo/test_valve.py | 83 +++++++++++++++++++++ 3 files changed, 173 insertions(+) create mode 100644 homeassistant/components/demo/valve.py create mode 100644 tests/components/demo/test_valve.py diff --git a/homeassistant/components/demo/__init__.py b/homeassistant/components/demo/__init__.py index 9314fc211de..dbc65119bfa 100644 --- a/homeassistant/components/demo/__init__.py +++ b/homeassistant/components/demo/__init__.py @@ -48,6 +48,7 @@ COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM = [ Platform.TIME, Platform.UPDATE, Platform.VACUUM, + Platform.VALVE, Platform.WATER_HEATER, Platform.WEATHER, ] diff --git a/homeassistant/components/demo/valve.py b/homeassistant/components/demo/valve.py new file mode 100644 index 00000000000..9c6acd45a8a --- /dev/null +++ b/homeassistant/components/demo/valve.py @@ -0,0 +1,89 @@ +"""Demo valve platform that implements valves.""" + +from __future__ import annotations + +import asyncio +from typing import Any + +from homeassistant.components.valve import ValveEntity, ValveEntityFeature, ValveState +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +OPEN_CLOSE_DELAY = 2 # Used to give a realistic open/close experience in frontend + + +async def async_setup_entry( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up the Demo config entry.""" + async_add_entities( + [ + DemoValve("Front Garden", ValveState.OPEN), + DemoValve("Orchard", ValveState.CLOSED), + ] + ) + + +class DemoValve(ValveEntity): + """Representation of a Demo valve.""" + + _attr_should_poll = False + + def __init__( + self, + name: str, + state: str, + moveable: bool = True, + ) -> None: + """Initialize the valve.""" + self._attr_name = name + if moveable: + self._attr_supported_features = ( + ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE + ) + self._state = state + self._moveable = moveable + + @property + def is_open(self) -> bool: + """Return true if valve is open.""" + return self._state == ValveState.OPEN + + @property + def is_opening(self) -> bool: + """Return true if valve is opening.""" + return self._state == ValveState.OPENING + + @property + def is_closing(self) -> bool: + """Return true if valve is closing.""" + return self._state == ValveState.CLOSING + + @property + def is_closed(self) -> bool: + """Return true if valve is closed.""" + return self._state == ValveState.CLOSED + + @property + def reports_position(self) -> bool: + """Return True if entity reports position, False otherwise.""" + return False + + async def async_open_valve(self, **kwargs: Any) -> None: + """Open the valve.""" + self._state = ValveState.OPENING + self.async_write_ha_state() + await asyncio.sleep(OPEN_CLOSE_DELAY) + self._state = ValveState.OPEN + self.async_write_ha_state() + + async def async_close_valve(self, **kwargs: Any) -> None: + """Close the valve.""" + self._state = ValveState.CLOSING + self.async_write_ha_state() + await asyncio.sleep(OPEN_CLOSE_DELAY) + self._state = ValveState.CLOSED + self.async_write_ha_state() diff --git a/tests/components/demo/test_valve.py b/tests/components/demo/test_valve.py new file mode 100644 index 00000000000..1057065ce70 --- /dev/null +++ b/tests/components/demo/test_valve.py @@ -0,0 +1,83 @@ +"""The tests for the Demo valve platform.""" + +from unittest.mock import patch + +import pytest + +from homeassistant.components.demo import DOMAIN, valve as demo_valve +from homeassistant.components.valve import ( + DOMAIN as VALVE_DOMAIN, + SERVICE_CLOSE_VALVE, + SERVICE_OPEN_VALVE, + ValveState, +) +from homeassistant.const import ATTR_ENTITY_ID, EVENT_STATE_CHANGED, Platform +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import async_capture_events + +FRONT_GARDEN = "valve.front_garden" +ORCHARD = "valve.orchard" + + +@pytest.fixture +async def valve_only() -> None: + """Enable only the valve platform.""" + with patch( + "homeassistant.components.demo.COMPONENTS_WITH_CONFIG_ENTRY_DEMO_PLATFORM", + [Platform.VALVE], + ): + yield + + +@pytest.fixture(autouse=True) +async def setup_comp(hass: HomeAssistant, valve_only: None): + """Set up demo component.""" + assert await async_setup_component( + hass, VALVE_DOMAIN, {VALVE_DOMAIN: {"platform": DOMAIN}} + ) + await hass.async_block_till_done() + + +@patch.object(demo_valve, "OPEN_CLOSE_DELAY", 0) +async def test_closing(hass: HomeAssistant) -> None: + """Test the closing of a valve.""" + state = hass.states.get(FRONT_GARDEN) + assert state.state == ValveState.OPEN + await hass.async_block_till_done() + + state_changes = async_capture_events(hass, EVENT_STATE_CHANGED) + await hass.services.async_call( + VALVE_DOMAIN, + SERVICE_CLOSE_VALVE, + {ATTR_ENTITY_ID: FRONT_GARDEN}, + blocking=False, + ) + await hass.async_block_till_done() + + assert state_changes[0].data["entity_id"] == FRONT_GARDEN + assert state_changes[0].data["new_state"].state == ValveState.CLOSING + + assert state_changes[1].data["entity_id"] == FRONT_GARDEN + assert state_changes[1].data["new_state"].state == ValveState.CLOSED + + +@patch.object(demo_valve, "OPEN_CLOSE_DELAY", 0) +async def test_opening(hass: HomeAssistant) -> None: + """Test the opening of a valve.""" + state = hass.states.get(ORCHARD) + assert state.state == ValveState.CLOSED + await hass.async_block_till_done() + + state_changes = async_capture_events(hass, EVENT_STATE_CHANGED) + await hass.services.async_call( + VALVE_DOMAIN, SERVICE_OPEN_VALVE, {ATTR_ENTITY_ID: ORCHARD}, blocking=False + ) + await hass.async_block_till_done() + + assert state_changes[0].data["entity_id"] == ORCHARD + assert state_changes[0].data["new_state"].state == ValveState.OPENING + + assert state_changes[1].data["entity_id"] == ORCHARD + assert state_changes[1].data["new_state"].state == ValveState.OPEN