Add Valve integration (#102184)

* Add Valve integration.

This adds the valve integration discussed in https://github.com/home-assistant/architecture/discussions/975
Most of the code is taken from the cover integration but simplified since valves
can't tilt.

There are a couple outstanding errors I'm not sure how to solve and prevents
me from even making this commit without `--no-verify`.

* Apply PR feedback

* Apply more feedback: Intruduce the bare minimum

* Remove file commited by mistake

* Hopefully this fixes tests

* Match cover's typing and mypy settings

* Change some configuration files

* Fix test

* Increase code coverage a little

* Code coverate inproved to 91%

* 95% code coverage

* Coverate up to 97%

* Coverage 98%

* Apply PR feedback

* Even more feedback

* Add line I shouldn't have removed

* Derive closed/open state from current position

* Hopefully last feedback

* Update homeassistant/components/valve/__init__.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/valve/__init__.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Remove unnecesary translation

* Remove unused method arguments

* Complete code coverage

* Update homeassistant/components/valve/__init__.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Simplify tests

* Update homeassistant/components/valve/__init__.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Apply last feedback

* Update tests/components/valve/test_init.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update tests/components/valve/test_init.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update tests/testing_config/custom_components/test/valve.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* More feedback

* Apply suggestion

* And more feedback

* Apply feedback

* Remove commented code

* Reverse logic to unindent

* Update homeassistant/components/valve/__init__.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update homeassistant/components/valve/__init__.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Implement stop valve for Mock valve

* Fix tests now that I've implemented stop_valve

* Assert it's neither opening nor closing

* Use current position instead

* Avoid scheduling executor when opening or closing

* Fix incorrect bitwise operation

* Simplify toggle

* Remove uneeded partial functions

* Make is_last_toggle_direction_open private

* Remove valve from test custom integration

* Improve test coverage

* Address review comments

* Address review comments

* Address review comments

* Update homeassistant/components/valve/__init__.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update tests

---------

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: Erik <erik@montnemery.com>
This commit is contained in:
Miguel Camba 2023-12-18 19:48:00 +01:00 committed by GitHub
parent 93a9a9d1e2
commit 5175737b60
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 754 additions and 0 deletions

View File

@ -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.*

View File

@ -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

View File

@ -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)

View File

@ -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"
}

View File

@ -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

View File

@ -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."
}
}
}

View File

@ -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 ####

View File

@ -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,
}

View File

@ -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

View File

@ -0,0 +1 @@
"""Tests for the valve component."""

View File

@ -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)