Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for Valve devices."""
2 
3 from __future__ import annotations
4 
5 from dataclasses import dataclass
6 from datetime import timedelta
7 from enum import IntFlag, StrEnum
8 import logging
9 from typing import Any, final
10 
11 import voluptuous as vol
12 
13 from homeassistant.config_entries import ConfigEntry
14 from homeassistant.const import ( # noqa: F401
15  SERVICE_CLOSE_VALVE,
16  SERVICE_OPEN_VALVE,
17  SERVICE_SET_VALVE_POSITION,
18  SERVICE_STOP_VALVE,
19  SERVICE_TOGGLE,
20  STATE_CLOSED,
21  STATE_CLOSING,
22  STATE_OPEN,
23  STATE_OPENING,
24 )
25 from homeassistant.core import HomeAssistant
26 from homeassistant.helpers import config_validation as cv
27 from homeassistant.helpers.entity import Entity, EntityDescription
28 from homeassistant.helpers.entity_component import EntityComponent
29 from homeassistant.helpers.typing import ConfigType
30 from homeassistant.util.hass_dict import HassKey
31 
32 from .const import DOMAIN, ValveState
33 
34 _LOGGER = logging.getLogger(__name__)
35 
36 DATA_COMPONENT: HassKey[EntityComponent[ValveEntity]] = HassKey(DOMAIN)
37 ENTITY_ID_FORMAT = DOMAIN + ".{}"
38 PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA
39 PLATFORM_SCHEMA_BASE = cv.PLATFORM_SCHEMA_BASE
40 SCAN_INTERVAL = timedelta(seconds=15)
41 
42 
43 class ValveDeviceClass(StrEnum):
44  """Device class for valve."""
45 
46  # Refer to the valve dev docs for device class descriptions
47  WATER = "water"
48  GAS = "gas"
49 
50 
51 DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.Coerce(ValveDeviceClass))
52 
53 
54 # mypy: disallow-any-generics
55 class ValveEntityFeature(IntFlag):
56  """Supported features of the valve entity."""
57 
58  OPEN = 1
59  CLOSE = 2
60  SET_POSITION = 4
61  STOP = 8
62 
63 
64 ATTR_CURRENT_POSITION = "current_position"
65 ATTR_POSITION = "position"
66 
67 
68 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
69  """Track states and offer events for valves."""
70  component = hass.data[DATA_COMPONENT] = EntityComponent[ValveEntity](
71  _LOGGER, DOMAIN, hass, SCAN_INTERVAL
72  )
73 
74  await component.async_setup(config)
75 
76  component.async_register_entity_service(
77  SERVICE_OPEN_VALVE, None, "async_handle_open_valve", [ValveEntityFeature.OPEN]
78  )
79 
80  component.async_register_entity_service(
81  SERVICE_CLOSE_VALVE,
82  None,
83  "async_handle_close_valve",
84  [ValveEntityFeature.CLOSE],
85  )
86 
87  component.async_register_entity_service(
88  SERVICE_SET_VALVE_POSITION,
89  {
90  vol.Required(ATTR_POSITION): vol.All(
91  vol.Coerce(int), vol.Range(min=0, max=100)
92  )
93  },
94  "async_set_valve_position",
95  [ValveEntityFeature.SET_POSITION],
96  )
97 
98  component.async_register_entity_service(
99  SERVICE_STOP_VALVE, None, "async_stop_valve", [ValveEntityFeature.STOP]
100  )
101 
102  component.async_register_entity_service(
103  SERVICE_TOGGLE,
104  None,
105  "async_toggle",
106  [ValveEntityFeature.OPEN | ValveEntityFeature.CLOSE],
107  )
108 
109  return True
110 
111 
112 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
113  """Set up a config entry."""
114  return await hass.data[DATA_COMPONENT].async_setup_entry(entry)
115 
116 
117 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
118  """Unload a config entry."""
119  return await hass.data[DATA_COMPONENT].async_unload_entry(entry)
120 
121 
122 @dataclass(frozen=True, kw_only=True)
124  """A class that describes valve entities."""
125 
126  device_class: ValveDeviceClass | None = None
127  reports_position: bool = False
128 
129 
131  """Base class for valve entities."""
132 
133  entity_description: ValveEntityDescription
134  _attr_current_valve_position: int | None = None
135  _attr_device_class: ValveDeviceClass | None
136  _attr_is_closed: bool | None = None
137  _attr_is_closing: bool | None = None
138  _attr_is_opening: bool | None = None
139  _attr_reports_position: bool
140  _attr_supported_features: ValveEntityFeature = ValveEntityFeature(0)
141 
142  __is_last_toggle_direction_open = True
143 
144  @property
145  def reports_position(self) -> bool:
146  """Return True if entity reports position, False otherwise."""
147  if hasattr(self, "_attr_reports_position"):
148  return self._attr_reports_position
149  if hasattr(self, "entity_description"):
150  return self.entity_description.reports_position
151  raise ValueError(f"'reports_position' not set for {self.entity_id}.")
152 
153  @property
154  def current_valve_position(self) -> int | None:
155  """Return current position of valve.
156 
157  None is unknown, 0 is closed, 100 is fully open.
158  """
159  return self._attr_current_valve_position
160 
161  @property
162  def device_class(self) -> ValveDeviceClass | None:
163  """Return the class of this entity."""
164  if hasattr(self, "_attr_device_class"):
165  return self._attr_device_class
166  if hasattr(self, "entity_description"):
167  return self.entity_description.device_class
168  return None
169 
170  @property
171  @final
172  def state(self) -> str | None:
173  """Return the state of the valve."""
174  reports_position = self.reports_positionreports_position
175  if self.is_openingis_opening:
176  self.__is_last_toggle_direction_open__is_last_toggle_direction_open = True
177  return ValveState.OPENING
178  if self.is_closingis_closing:
179  self.__is_last_toggle_direction_open__is_last_toggle_direction_open = False
180  return ValveState.CLOSING
181  if reports_position is True:
182  if (current_valve_position := self.current_valve_positioncurrent_valve_position) is None:
183  return None
184  position_zero = current_valve_position == 0
185  return ValveState.CLOSED if position_zero else ValveState.OPEN
186  if (closed := self.is_closedis_closed) is None:
187  return None
188  return ValveState.CLOSED if closed else ValveState.OPEN
189 
190  @final
191  @property
192  def state_attributes(self) -> dict[str, Any] | None:
193  """Return the state attributes."""
194  if not self.reports_positionreports_position:
195  return None
196  return {ATTR_CURRENT_POSITION: self.current_valve_positioncurrent_valve_position}
197 
198  @property
199  def supported_features(self) -> ValveEntityFeature:
200  """Flag supported features."""
201  return self._attr_supported_features
202 
203  @property
204  def is_opening(self) -> bool | None:
205  """Return if the valve is opening or not."""
206  return self._attr_is_opening
207 
208  @property
209  def is_closing(self) -> bool | None:
210  """Return if the valve is closing or not."""
211  return self._attr_is_closing
212 
213  @property
214  def is_closed(self) -> bool | None:
215  """Return if the valve is closed or not."""
216  return self._attr_is_closed
217 
218  def open_valve(self) -> None:
219  """Open the valve."""
220  raise NotImplementedError
221 
222  async def async_open_valve(self) -> None:
223  """Open the valve."""
224  await self.hasshass.async_add_executor_job(self.open_valveopen_valve)
225 
226  @final
227  async def async_handle_open_valve(self) -> None:
228  """Open the valve."""
229  if self.supported_featuressupported_featuressupported_features & ValveEntityFeature.SET_POSITION:
230  await self.async_set_valve_positionasync_set_valve_position(100)
231  return
232  await self.async_open_valveasync_open_valve()
233 
234  def close_valve(self) -> None:
235  """Close valve."""
236  raise NotImplementedError
237 
238  async def async_close_valve(self) -> None:
239  """Close valve."""
240  await self.hasshass.async_add_executor_job(self.close_valveclose_valve)
241 
242  @final
243  async def async_handle_close_valve(self) -> None:
244  """Close the valve."""
245  if self.supported_featuressupported_featuressupported_features & ValveEntityFeature.SET_POSITION:
246  await self.async_set_valve_positionasync_set_valve_position(0)
247  return
248  await self.async_close_valveasync_close_valve()
249 
250  async def async_toggle(self) -> None:
251  """Toggle the entity."""
252  if self.supported_featuressupported_featuressupported_features & ValveEntityFeature.STOP and (
253  self.is_closingis_closing or self.is_openingis_opening
254  ):
255  return await self.async_stop_valveasync_stop_valve()
256  if self.is_closedis_closed:
257  return await self.async_handle_open_valveasync_handle_open_valve()
258  if self.__is_last_toggle_direction_open__is_last_toggle_direction_open:
259  return await self.async_handle_close_valveasync_handle_close_valve()
260  return await self.async_handle_open_valveasync_handle_open_valve()
261 
262  def set_valve_position(self, position: int) -> None:
263  """Move the valve to a specific position."""
264  raise NotImplementedError
265 
266  async def async_set_valve_position(self, position: int) -> None:
267  """Move the valve to a specific position."""
268  await self.hasshass.async_add_executor_job(self.set_valve_positionset_valve_position, position)
269 
270  def stop_valve(self) -> None:
271  """Stop the valve."""
272  raise NotImplementedError
273 
274  async def async_stop_valve(self) -> None:
275  """Stop the valve."""
276  await self.hasshass.async_add_executor_job(self.stop_valvestop_valve)
ValveEntityFeature supported_features(self)
Definition: __init__.py:199
dict[str, Any]|None state_attributes(self)
Definition: __init__.py:192
ValveDeviceClass|None device_class(self)
Definition: __init__.py:162
None set_valve_position(self, int position)
Definition: __init__.py:262
None async_set_valve_position(self, int position)
Definition: __init__.py:266
int|None supported_features(self)
Definition: entity.py:861
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:68
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:117
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:112