diff --git a/.strict-typing b/.strict-typing index ce9d84204a5..7c2d9d8daf2 100644 --- a/.strict-typing +++ b/.strict-typing @@ -366,6 +366,7 @@ homeassistant.components.uptimerobot.* homeassistant.components.usb.* homeassistant.components.vacuum.* homeassistant.components.vallox.* +homeassistant.components.valve.* homeassistant.components.velbus.* homeassistant.components.vlc_telnet.* homeassistant.components.wake_on_lan.* diff --git a/CODEOWNERS b/CODEOWNERS index 4ad2a38fa04..c45293d03c8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1403,6 +1403,8 @@ build.json @home-assistant/supervisor /tests/components/vacuum/ @home-assistant/core /homeassistant/components/vallox/ @andre-richter @slovdahl @viiru- /tests/components/vallox/ @andre-richter @slovdahl @viiru- +/homeassistant/components/valve/ @home-assistant/core +/tests/components/valve/ @home-assistant/core /homeassistant/components/velbus/ @Cereal2nd @brefra /tests/components/velbus/ @Cereal2nd @brefra /homeassistant/components/velux/ @Julius2342 diff --git a/homeassistant/components/valve/__init__.py b/homeassistant/components/valve/__init__.py new file mode 100644 index 00000000000..9521d597303 --- /dev/null +++ b/homeassistant/components/valve/__init__.py @@ -0,0 +1,270 @@ +"""Support for Valve devices.""" +from __future__ import annotations + +from dataclasses import dataclass +from datetime import timedelta +from enum import IntFlag, StrEnum +import logging +from typing import Any, final + +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + SERVICE_CLOSE_VALVE, + SERVICE_OPEN_VALVE, + SERVICE_SET_VALVE_POSITION, + SERVICE_STOP_VALVE, + SERVICE_TOGGLE, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers.config_validation import ( # noqa: F401 + PLATFORM_SCHEMA, + PLATFORM_SCHEMA_BASE, +) +from homeassistant.helpers.entity import Entity, EntityDescription +from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.typing import ConfigType + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "valve" +SCAN_INTERVAL = timedelta(seconds=15) + +ENTITY_ID_FORMAT = DOMAIN + ".{}" + + +class ValveDeviceClass(StrEnum): + """Device class for valve.""" + + # Refer to the valve dev docs for device class descriptions + WATER = "water" + GAS = "gas" + + +DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(ValveDeviceClass)) + + +# mypy: disallow-any-generics +class ValveEntityFeature(IntFlag): + """Supported features of the valve entity.""" + + OPEN = 1 + CLOSE = 2 + SET_POSITION = 4 + STOP = 8 + + +ATTR_CURRENT_POSITION = "current_position" +ATTR_POSITION = "position" + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Track states and offer events for valves.""" + component = hass.data[DOMAIN] = EntityComponent[ValveEntity]( + _LOGGER, DOMAIN, hass, SCAN_INTERVAL + ) + + await component.async_setup(config) + + component.async_register_entity_service( + SERVICE_OPEN_VALVE, {}, "async_handle_open_valve", [ValveEntityFeature.OPEN] + ) + + component.async_register_entity_service( + SERVICE_CLOSE_VALVE, {}, "async_handle_close_valve", [ValveEntityFeature.CLOSE] + ) + + component.async_register_entity_service( + SERVICE_SET_VALVE_POSITION, + { + vol.Required(ATTR_POSITION): vol.All( + vol.Coerce(int), vol.Range(min=0, max=100) + ) + }, + "async_set_valve_position", + [ValveEntityFeature.SET_POSITION], + ) + + component.async_register_entity_service( + SERVICE_STOP_VALVE, {}, "async_stop_valve", [ValveEntityFeature.STOP] + ) + + component.async_register_entity_service( + SERVICE_TOGGLE, + {}, + "async_toggle", + [ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE], + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up a config entry.""" + component: EntityComponent[ValveEntity] = hass.data[DOMAIN] + return await component.async_setup_entry(entry) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + component: EntityComponent[ValveEntity] = hass.data[DOMAIN] + return await component.async_unload_entry(entry) + + +@dataclass(frozen=True, kw_only=True) +class ValveEntityDescription(EntityDescription): + """A class that describes valve entities.""" + + device_class: ValveDeviceClass | None = None + reports_position: bool = False + + +class ValveEntity(Entity): + """Base class for valve entities.""" + + entity_description: ValveEntityDescription + _attr_current_valve_position: int | None = None + _attr_device_class: ValveDeviceClass | None + _attr_is_closed: bool | None = None + _attr_is_closing: bool | None = None + _attr_is_opening: bool | None = None + _attr_reports_position: bool + _attr_supported_features: ValveEntityFeature = ValveEntityFeature(0) + + __is_last_toggle_direction_open = True + + @property + def reports_position(self) -> bool: + """Return True if entity reports position, False otherwise.""" + if hasattr(self, "_attr_reports_position"): + return self._attr_reports_position + if hasattr(self, "entity_description"): + return self.entity_description.reports_position + raise ValueError(f"'reports_position' not set for {self.entity_id}.") + + @property + def current_valve_position(self) -> int | None: + """Return current position of valve. + + None is unknown, 0 is closed, 100 is fully open. + """ + return self._attr_current_valve_position + + @property + def device_class(self) -> ValveDeviceClass | None: + """Return the class of this entity.""" + if hasattr(self, "_attr_device_class"): + return self._attr_device_class + if hasattr(self, "entity_description"): + return self.entity_description.device_class + return None + + @property + @final + def state(self) -> str | None: + """Return the state of the valve.""" + reports_position = self.reports_position + if self.is_opening: + self.__is_last_toggle_direction_open = True + return STATE_OPENING + if self.is_closing: + self.__is_last_toggle_direction_open = False + return STATE_CLOSING + if reports_position is True: + if (current_valve_position := self.current_valve_position) is None: + return None + position_zero = current_valve_position == 0 + return STATE_CLOSED if position_zero else STATE_OPEN + if (closed := self.is_closed) is None: + return None + return STATE_CLOSED if closed else STATE_OPEN + + @final + @property + def state_attributes(self) -> dict[str, Any]: + """Return the state attributes.""" + + return {ATTR_CURRENT_POSITION: self.current_valve_position} + + @property + def supported_features(self) -> ValveEntityFeature: + """Flag supported features.""" + return self._attr_supported_features + + @property + def is_opening(self) -> bool | None: + """Return if the valve is opening or not.""" + return self._attr_is_opening + + @property + def is_closing(self) -> bool | None: + """Return if the valve is closing or not.""" + return self._attr_is_closing + + @property + def is_closed(self) -> bool | None: + """Return if the valve is closed or not.""" + return self._attr_is_closed + + def open_valve(self) -> None: + """Open the valve.""" + raise NotImplementedError() + + async def async_open_valve(self) -> None: + """Open the valve.""" + await self.hass.async_add_executor_job(self.open_valve) + + @final + async def async_handle_open_valve(self) -> None: + """Open the valve.""" + if self.supported_features & ValveEntityFeature.SET_POSITION: + return await self.async_set_valve_position(100) + await self.async_open_valve() + + def close_valve(self) -> None: + """Close valve.""" + raise NotImplementedError() + + async def async_close_valve(self) -> None: + """Close valve.""" + await self.hass.async_add_executor_job(self.close_valve) + + @final + async def async_handle_close_valve(self) -> None: + """Close the valve.""" + if self.supported_features & ValveEntityFeature.SET_POSITION: + return await self.async_set_valve_position(0) + await self.async_close_valve() + + async def async_toggle(self) -> None: + """Toggle the entity.""" + if self.supported_features & ValveEntityFeature.STOP and ( + self.is_closing or self.is_opening + ): + return await self.async_stop_valve() + if self.is_closed: + return await self.async_handle_open_valve() + if self.__is_last_toggle_direction_open: + return await self.async_handle_close_valve() + return await self.async_handle_open_valve() + + def set_valve_position(self, position: int) -> None: + """Move the valve to a specific position.""" + raise NotImplementedError() + + async def async_set_valve_position(self, position: int) -> None: + """Move the valve to a specific position.""" + await self.hass.async_add_executor_job(self.set_valve_position, position) + + def stop_valve(self) -> None: + """Stop the valve.""" + raise NotImplementedError() + + async def async_stop_valve(self) -> None: + """Stop the valve.""" + await self.hass.async_add_executor_job(self.stop_valve) diff --git a/homeassistant/components/valve/manifest.json b/homeassistant/components/valve/manifest.json new file mode 100644 index 00000000000..28563f0976c --- /dev/null +++ b/homeassistant/components/valve/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "valve", + "name": "Valve", + "codeowners": ["@home-assistant/core"], + "documentation": "https://www.home-assistant.io/integrations/valve", + "integration_type": "entity", + "quality_scale": "internal" +} diff --git a/homeassistant/components/valve/services.yaml b/homeassistant/components/valve/services.yaml new file mode 100644 index 00000000000..936599818f1 --- /dev/null +++ b/homeassistant/components/valve/services.yaml @@ -0,0 +1,45 @@ +# Describes the format for available valve services + +open_valve: + target: + entity: + domain: valve + supported_features: + - valve.ValveEntityFeature.OPEN + +close_valve: + target: + entity: + domain: valve + supported_features: + - valve.ValveEntityFeature.CLOSE + +toggle: + target: + entity: + domain: valve + supported_features: + - - valve.ValveEntityFeature.CLOSE + - valve.ValveEntityFeature.OPEN + +set_valve_position: + target: + entity: + domain: valve + supported_features: + - valve.ValveEntityFeature.SET_POSITION + fields: + position: + required: true + selector: + number: + min: 0 + max: 100 + unit_of_measurement: "%" + +stop_valve: + target: + entity: + domain: valve + supported_features: + - valve.ValveEntityFeature.STOP diff --git a/homeassistant/components/valve/strings.json b/homeassistant/components/valve/strings.json new file mode 100644 index 00000000000..b86ec371b34 --- /dev/null +++ b/homeassistant/components/valve/strings.json @@ -0,0 +1,54 @@ +{ + "title": "Valve", + "entity_component": { + "_": { + "name": "[%key:component::valve::title%]", + "state": { + "open": "[%key:common::state::open%]", + "opening": "Opening", + "closed": "[%key:common::state::closed%]", + "closing": "Closing", + "stopped": "Stopped" + }, + "state_attributes": { + "current_position": { + "name": "Position" + } + } + }, + "water": { + "name": "Water" + }, + "gas": { + "name": "Gas" + } + }, + "services": { + "open_valve": { + "name": "[%key:common::action::open%]", + "description": "Opens a valve." + }, + "close_valve": { + "name": "[%key:common::action::close%]", + "description": "Closes a valve." + }, + "toggle": { + "name": "[%key:common::action::toggle%]", + "description": "Toggles a valve open/closed." + }, + "set_valve_position": { + "name": "Set position", + "description": "Moves a valve to a specific position.", + "fields": { + "position": { + "name": "Position", + "description": "Target position." + } + } + }, + "stop_valve": { + "name": "[%key:common::action::stop%]", + "description": "Stops the valve movement." + } + } +} diff --git a/homeassistant/const.py b/homeassistant/const.py index df68e3ab05a..40b66b6aed3 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -58,6 +58,7 @@ class Platform(StrEnum): TODO = "todo" TTS = "tts" VACUUM = "vacuum" + VALVE = "valve" UPDATE = "update" WAKE_WORD = "wake_word" WATER_HEATER = "water_heater" @@ -1105,6 +1106,11 @@ SERVICE_STOP_COVER: Final = "stop_cover" SERVICE_STOP_COVER_TILT: Final = "stop_cover_tilt" SERVICE_TOGGLE_COVER_TILT: Final = "toggle_cover_tilt" +SERVICE_CLOSE_VALVE: Final = "close_valve" +SERVICE_OPEN_VALVE: Final = "open_valve" +SERVICE_SET_VALVE_POSITION: Final = "set_valve_position" +SERVICE_STOP_VALVE: Final = "stop_valve" + SERVICE_SELECT_OPTION: Final = "select_option" # #### API / REMOTE #### diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index ab22877f906..9c4266583e8 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -102,6 +102,7 @@ def _entity_features() -> dict[str, type[IntFlag]]: from homeassistant.components.todo import TodoListEntityFeature from homeassistant.components.update import UpdateEntityFeature from homeassistant.components.vacuum import VacuumEntityFeature + from homeassistant.components.valve import ValveEntityFeature from homeassistant.components.water_heater import WaterHeaterEntityFeature from homeassistant.components.weather import WeatherEntityFeature @@ -122,6 +123,7 @@ def _entity_features() -> dict[str, type[IntFlag]]: "TodoListEntityFeature": TodoListEntityFeature, "UpdateEntityFeature": UpdateEntityFeature, "VacuumEntityFeature": VacuumEntityFeature, + "ValveEntityFeature": ValveEntityFeature, "WaterHeaterEntityFeature": WaterHeaterEntityFeature, "WeatherEntityFeature": WeatherEntityFeature, } diff --git a/mypy.ini b/mypy.ini index 7325c7fd357..45395463ce9 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3423,6 +3423,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.valve.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.velbus.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/tests/components/valve/__init__.py b/tests/components/valve/__init__.py new file mode 100644 index 00000000000..c39ec8220af --- /dev/null +++ b/tests/components/valve/__init__.py @@ -0,0 +1 @@ +"""Tests for the valve component.""" diff --git a/tests/components/valve/test_init.py b/tests/components/valve/test_init.py new file mode 100644 index 00000000000..08b0771da8e --- /dev/null +++ b/tests/components/valve/test_init.py @@ -0,0 +1,355 @@ +"""The tests for Valve.""" +from collections.abc import Generator + +import pytest + +from homeassistant.components.valve import ( + DOMAIN, + ValveDeviceClass, + ValveEntity, + ValveEntityDescription, + ValveEntityFeature, +) +from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigFlow +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_SET_VALVE_POSITION, + SERVICE_TOGGLE, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, + STATE_UNAVAILABLE, + Platform, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from tests.common import ( + MockConfigEntry, + MockModule, + MockPlatform, + mock_config_flow, + mock_integration, + mock_platform, +) + +TEST_DOMAIN = "test" + + +class MockFlow(ConfigFlow): + """Test flow.""" + + +class MockValveEntity(ValveEntity): + """Mock valve device to use in tests.""" + + _attr_should_poll = False + _target_valve_position: int + + def __init__( + self, + unique_id: str = "mock_valve", + name: str = "Valve", + features: ValveEntityFeature = ValveEntityFeature(0), + current_position: int = None, + device_class: ValveDeviceClass = None, + reports_position: bool = True, + ) -> None: + """Initialize the valve.""" + self._attr_name = name + self._attr_unique_id = unique_id + self._attr_supported_features = features + self._attr_current_valve_position = current_position + if reports_position is not None: + self._attr_reports_position = reports_position + if device_class is not None: + self._attr_device_class = device_class + + def set_valve_position(self, position: int) -> None: + """Set the valve to opening or closing towards a target percentage.""" + if position > self._attr_current_valve_position: + self._attr_is_closing = False + self._attr_is_opening = True + else: + self._attr_is_closing = True + self._attr_is_opening = False + self._target_valve_position = position + self.schedule_update_ha_state() + + def stop_valve(self) -> None: + """Stop the valve.""" + self._attr_is_closing = False + self._attr_is_opening = False + self._target_valve_position = None + self._attr_is_closed = self._attr_current_valve_position == 0 + self.schedule_update_ha_state() + + @callback + def finish_movement(self): + """Set the value to the saved target and removes intermediate states.""" + self._attr_current_valve_position = self._target_valve_position + self._attr_is_closing = False + self._attr_is_opening = False + self.async_write_ha_state() + + +class MockBinaryValveEntity(ValveEntity): + """Mock valve device to use in tests.""" + + def __init__( + self, + unique_id: str = "mock_valve_2", + name: str = "Valve", + features: ValveEntityFeature = ValveEntityFeature(0), + is_closed: bool = None, + ) -> None: + """Initialize the valve.""" + self._attr_name = name + self._attr_unique_id = unique_id + self._attr_supported_features = features + self._attr_is_closed = is_closed + self._attr_reports_position = False + + def open_valve(self) -> None: + """Open the valve.""" + self._attr_is_closed = False + + def close_valve(self) -> None: + """Mock implementantion for sync close function.""" + self._attr_is_closed = True + + +@pytest.fixture(autouse=True) +def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]: + """Mock config flow.""" + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + + with mock_config_flow(TEST_DOMAIN, MockFlow): + yield + + +@pytest.fixture +def mock_config_entry(hass) -> tuple[MockConfigEntry, list[ValveEntity]]: + """Mock a config entry which sets up a couple of valve entities.""" + entities = [ + MockBinaryValveEntity( + is_closed=False, + features=ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE, + ), + MockValveEntity( + current_position=50, + features=ValveEntityFeature.OPEN + | ValveEntityFeature.CLOSE + | ValveEntityFeature.STOP + | ValveEntityFeature.SET_POSITION, + ), + ] + + async def async_setup_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Set up test config entry.""" + await hass.config_entries.async_forward_entry_setup( + config_entry, Platform.VALVE + ) + return True + + async def async_unload_entry_init( + hass: HomeAssistant, config_entry: ConfigEntry + ) -> bool: + """Unload up test config entry.""" + await hass.config_entries.async_unload_platforms(config_entry, [Platform.VALVE]) + return True + + mock_platform(hass, f"{TEST_DOMAIN}.config_flow") + mock_integration( + hass, + MockModule( + TEST_DOMAIN, + async_setup_entry=async_setup_entry_init, + async_unload_entry=async_unload_entry_init, + ), + ) + + async def async_setup_entry_platform( + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, + ) -> None: + """Set up test platform via config entry.""" + async_add_entities(entities) + + mock_platform( + hass, + f"{TEST_DOMAIN}.{DOMAIN}", + MockPlatform(async_setup_entry=async_setup_entry_platform), + ) + + config_entry = MockConfigEntry(domain=TEST_DOMAIN) + config_entry.add_to_hass(hass) + + return (config_entry, entities) + + +async def test_valve_setup( + hass: HomeAssistant, mock_config_entry: tuple[MockConfigEntry, list[ValveEntity]] +) -> None: + """Test setup and tear down of valve platform and entity.""" + config_entry = mock_config_entry[0] + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + entity_id = mock_config_entry[1][0].entity_id + + assert config_entry.state == ConfigEntryState.LOADED + assert hass.states.get(entity_id) + + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ConfigEntryState.NOT_LOADED + entity_state = hass.states.get(entity_id) + + assert entity_state + assert entity_state.state == STATE_UNAVAILABLE + + +async def test_services( + hass: HomeAssistant, mock_config_entry: tuple[MockConfigEntry, list[ValveEntity]] +) -> None: + """Test the provided services.""" + config_entry = mock_config_entry[0] + ent1, ent2 = mock_config_entry[1] + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Test init all valves should be open + assert is_open(hass, ent1) + assert is_open(hass, ent2) + + # call basic toggle services + await call_service(hass, SERVICE_TOGGLE, ent1) + await call_service(hass, SERVICE_TOGGLE, ent2) + + # entities without stop should be closed and with stop should be closing + assert is_closed(hass, ent1) + assert is_closing(hass, ent2) + ent2.finish_movement() + assert is_closed(hass, ent2) + + # call basic toggle services and set different valve position states + await call_service(hass, SERVICE_TOGGLE, ent1) + await call_service(hass, SERVICE_TOGGLE, ent2) + await hass.async_block_till_done() + + # entities should be in correct state depending on the SUPPORT_STOP feature and valve position + assert is_open(hass, ent1) + assert is_opening(hass, ent2) + + # call basic toggle services + await call_service(hass, SERVICE_TOGGLE, ent1) + await call_service(hass, SERVICE_TOGGLE, ent2) + + # entities should be in correct state depending on the SUPPORT_STOP feature and valve position + assert is_closed(hass, ent1) + assert not is_opening(hass, ent2) + assert not is_closing(hass, ent2) + assert is_closed(hass, ent2) + + await call_service(hass, SERVICE_SET_VALVE_POSITION, ent2, 50) + assert is_opening(hass, ent2) + + +async def test_valve_device_class(hass: HomeAssistant) -> None: + """Test valve entity with defaults.""" + default_valve = MockValveEntity() + default_valve.hass = hass + + assert default_valve.device_class is None + + entity_description = ValveEntityDescription( + key="test", + device_class=ValveDeviceClass.GAS, + ) + default_valve.entity_description = entity_description + assert default_valve.device_class is ValveDeviceClass.GAS + + water_valve = MockValveEntity(device_class=ValveDeviceClass.WATER) + water_valve.hass = hass + + assert water_valve.device_class is ValveDeviceClass.WATER + + +async def test_valve_report_position(hass: HomeAssistant) -> None: + """Test valve entity with defaults.""" + default_valve = MockValveEntity(reports_position=None) + default_valve.hass = hass + + with pytest.raises(ValueError): + default_valve.reports_position + + second_valve = MockValveEntity(reports_position=True) + second_valve.hass = hass + + assert second_valve.reports_position is True + + entity_description = ValveEntityDescription(key="test", reports_position=True) + third_valve = MockValveEntity(reports_position=None) + third_valve.entity_description = entity_description + assert third_valve.reports_position is True + + +async def test_none_state(hass: HomeAssistant) -> None: + """Test different criteria for closeness.""" + binary_valve_with_none_is_closed_attr = MockBinaryValveEntity(is_closed=None) + binary_valve_with_none_is_closed_attr.hass = hass + + assert binary_valve_with_none_is_closed_attr.state is None + + pos_valve_with_none_is_closed_attr = MockValveEntity() + pos_valve_with_none_is_closed_attr.hass = hass + + assert pos_valve_with_none_is_closed_attr.state is None + + +async def test_supported_features(hass: HomeAssistant) -> None: + """Test valve entity with defaults.""" + valve = MockValveEntity(features=None) + valve.hass = hass + + assert valve.supported_features is None + + +def call_service(hass, service, ent, position=None): + """Call any service on entity.""" + params = {ATTR_ENTITY_ID: ent.entity_id} + if position is not None: + params["position"] = position + return hass.services.async_call(DOMAIN, service, params, blocking=True) + + +def set_valve_position(ent, position) -> None: + """Set a position value to a valve.""" + ent._values["current_valve_position"] = position + + +def is_open(hass, ent): + """Return if the valve is closed based on the statemachine.""" + return hass.states.is_state(ent.entity_id, STATE_OPEN) + + +def is_opening(hass, ent): + """Return if the valve is closed based on the statemachine.""" + return hass.states.is_state(ent.entity_id, STATE_OPENING) + + +def is_closed(hass, ent): + """Return if the valve is closed based on the statemachine.""" + return hass.states.is_state(ent.entity_id, STATE_CLOSED) + + +def is_closing(hass, ent): + """Return if the valve is closed based on the statemachine.""" + return hass.states.is_state(ent.entity_id, STATE_CLOSING)