mirror of https://github.com/home-assistant/core
277 lines
8.5 KiB
Python
277 lines
8.5 KiB
Python
"""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 ( # noqa: F401
|
|
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 import config_validation as cv
|
|
from homeassistant.helpers.entity import Entity, EntityDescription
|
|
from homeassistant.helpers.entity_component import EntityComponent
|
|
from homeassistant.helpers.typing import ConfigType
|
|
from homeassistant.util.hass_dict import HassKey
|
|
|
|
from .const import DOMAIN, ValveState
|
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
DATA_COMPONENT: HassKey[EntityComponent[ValveEntity]] = HassKey(DOMAIN)
|
|
ENTITY_ID_FORMAT = DOMAIN + ".{}"
|
|
PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
|
|
PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
|
|
SCAN_INTERVAL = timedelta(seconds=15)
|
|
|
|
|
|
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[DATA_COMPONENT] = EntityComponent[ValveEntity](
|
|
_LOGGER, DOMAIN, hass, SCAN_INTERVAL
|
|
)
|
|
|
|
await component.async_setup(config)
|
|
|
|
component.async_register_entity_service(
|
|
SERVICE_OPEN_VALVE, None, "async_handle_open_valve", [ValveEntityFeature.OPEN]
|
|
)
|
|
|
|
component.async_register_entity_service(
|
|
SERVICE_CLOSE_VALVE,
|
|
None,
|
|
"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, None, "async_stop_valve", [ValveEntityFeature.STOP]
|
|
)
|
|
|
|
component.async_register_entity_service(
|
|
SERVICE_TOGGLE,
|
|
None,
|
|
"async_toggle",
|
|
[ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE],
|
|
)
|
|
|
|
return True
|
|
|
|
|
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
"""Set up a config entry."""
|
|
return await hass.data[DATA_COMPONENT].async_setup_entry(entry)
|
|
|
|
|
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
"""Unload a config entry."""
|
|
return await hass.data[DATA_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 ValveState.OPENING
|
|
if self.is_closing:
|
|
self.__is_last_toggle_direction_open = False
|
|
return ValveState.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 ValveState.CLOSED if position_zero else ValveState.OPEN
|
|
if (closed := self.is_closed) is None:
|
|
return None
|
|
return ValveState.CLOSED if closed else ValveState.OPEN
|
|
|
|
@final
|
|
@property
|
|
def state_attributes(self) -> dict[str, Any] | None:
|
|
"""Return the state attributes."""
|
|
if not self.reports_position:
|
|
return None
|
|
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:
|
|
await self.async_set_valve_position(100)
|
|
return
|
|
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:
|
|
await self.async_set_valve_position(0)
|
|
return
|
|
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)
|