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:
parent
93a9a9d1e2
commit
5175737b60
11 changed files with 754 additions and 0 deletions
|
@ -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.*
|
||||
|
|
|
@ -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
|
||||
|
|
270
homeassistant/components/valve/__init__.py
Normal file
270
homeassistant/components/valve/__init__.py
Normal 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)
|
8
homeassistant/components/valve/manifest.json
Normal file
8
homeassistant/components/valve/manifest.json
Normal 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"
|
||||
}
|
45
homeassistant/components/valve/services.yaml
Normal file
45
homeassistant/components/valve/services.yaml
Normal 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
|
54
homeassistant/components/valve/strings.json
Normal file
54
homeassistant/components/valve/strings.json
Normal 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."
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 ####
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
10
mypy.ini
10
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
|
||||
|
|
1
tests/components/valve/__init__.py
Normal file
1
tests/components/valve/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Tests for the valve component."""
|
355
tests/components/valve/test_init.py
Normal file
355
tests/components/valve/test_init.py
Normal 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)
|
Loading…
Add table
Add a link
Reference in a new issue