Home Assistant Unofficial Reference 2024.12.1
cover.py
Go to the documentation of this file.
1 """Support for Z-Wave cover devices."""
2 
3 from __future__ import annotations
4 
5 from typing import Any, cast
6 
7 from zwave_js_server.client import Client as ZwaveClient
8 from zwave_js_server.const import (
9  CURRENT_VALUE_PROPERTY,
10  TARGET_STATE_PROPERTY,
11  TARGET_VALUE_PROPERTY,
12 )
13 from zwave_js_server.const.command_class.barrier_operator import BarrierState
14 from zwave_js_server.const.command_class.multilevel_switch import (
15  COVER_ON_PROPERTY,
16  COVER_OPEN_PROPERTY,
17  COVER_UP_PROPERTY,
18 )
19 from zwave_js_server.const.command_class.window_covering import (
20  NO_POSITION_PROPERTY_KEYS,
21  NO_POSITION_SUFFIX,
22  WINDOW_COVERING_LEVEL_CHANGE_DOWN_PROPERTY,
23  WINDOW_COVERING_LEVEL_CHANGE_UP_PROPERTY,
24  SlatStates,
25 )
26 from zwave_js_server.model.driver import Driver
27 from zwave_js_server.model.value import Value as ZwaveValue
28 
30  ATTR_POSITION,
31  ATTR_TILT_POSITION,
32  DOMAIN as COVER_DOMAIN,
33  CoverDeviceClass,
34  CoverEntity,
35  CoverEntityFeature,
36 )
37 from homeassistant.config_entries import ConfigEntry
38 from homeassistant.core import HomeAssistant, callback
39 from homeassistant.helpers.dispatcher import async_dispatcher_connect
40 from homeassistant.helpers.entity_platform import AddEntitiesCallback
41 
42 from .const import (
43  COVER_POSITION_PROPERTY_KEYS,
44  COVER_TILT_PROPERTY_KEYS,
45  DATA_CLIENT,
46  DOMAIN,
47 )
48 from .discovery import ZwaveDiscoveryInfo
49 from .discovery_data_template import CoverTiltDataTemplate
50 from .entity import ZWaveBaseEntity
51 
52 PARALLEL_UPDATES = 0
53 
54 
56  hass: HomeAssistant,
57  config_entry: ConfigEntry,
58  async_add_entities: AddEntitiesCallback,
59 ) -> None:
60  """Set up Z-Wave Cover from Config Entry."""
61  client: ZwaveClient = config_entry.runtime_data[DATA_CLIENT]
62 
63  @callback
64  def async_add_cover(info: ZwaveDiscoveryInfo) -> None:
65  """Add Z-Wave cover."""
66  driver = client.driver
67  assert driver is not None # Driver is ready before platforms are loaded.
68  entities: list[ZWaveBaseEntity] = []
69  if info.platform_hint == "window_covering":
70  entities.append(ZWaveWindowCovering(config_entry, driver, info))
71  elif info.platform_hint == "motorized_barrier":
72  entities.append(ZwaveMotorizedBarrier(config_entry, driver, info))
73  elif info.platform_hint and info.platform_hint.endswith("tilt"):
74  entities.append(ZWaveTiltCover(config_entry, driver, info))
75  else:
76  entities.append(ZWaveMultilevelSwitchCover(config_entry, driver, info))
77  async_add_entities(entities)
78 
79  config_entry.async_on_unload(
81  hass,
82  f"{DOMAIN}_{config_entry.entry_id}_add_{COVER_DOMAIN}",
83  async_add_cover,
84  )
85  )
86 
87 
89  """Mix-in class for cover with position support."""
90 
91  _current_position_value: ZwaveValue | None = None
92  _target_position_value: ZwaveValue | None = None
93  _stop_position_value: ZwaveValue | None = None
94 
96  self,
97  current_value: ZwaveValue,
98  target_value: ZwaveValue | None = None,
99  stop_value: ZwaveValue | None = None,
100  ) -> None:
101  """Set values for position."""
102  self._attr_supported_features_attr_supported_features = (
103  (self._attr_supported_features_attr_supported_features or 0)
104  | CoverEntityFeature.OPEN
105  | CoverEntityFeature.CLOSE
106  | CoverEntityFeature.SET_POSITION
107  )
108  self._current_position_value_current_position_value = current_value
109  self._target_position_value_target_position_value = target_value or self.get_zwave_valueget_zwave_value(
110  TARGET_VALUE_PROPERTY, value_property_key=current_value.property_key
111  )
112 
113  if stop_value:
114  self._stop_position_value_stop_position_value = stop_value
115  self._attr_supported_features_attr_supported_features |= CoverEntityFeature.STOP
116 
117  def percent_to_zwave_position(self, value: int) -> int:
118  """Convert position in 0-100 scale to closed_value-open_value scale."""
119  return (
120  round(max(min(1, (value / 100)), 0) * self._position_range_position_range)
121  + self._fully_closed_position_fully_closed_position
122  )
123 
124  def zwave_to_percent_position(self, value: int) -> int:
125  """Convert closed_value-open_value scale to position in 0-100 scale."""
126  return round(
127  ((value - self._fully_closed_position_fully_closed_position) / self._position_range_position_range) * 100
128  )
129 
130  @property
131  def _fully_open_position(self) -> int:
132  """Return value that represents fully opened position."""
133  max_ = self.infoinfo.primary_value.metadata.max
134  return 99 if max_ is None else max_
135 
136  @property
137  def _fully_closed_position(self) -> int:
138  """Return value that represents fully closed position."""
139  min_ = self.infoinfo.primary_value.metadata.min
140  return 0 if min_ is None else min_
141 
142  @property
143  def _position_range(self) -> int:
144  """Return range between fully opened and fully closed position."""
145  return self._fully_open_position_fully_open_position - self._fully_closed_position_fully_closed_position
146 
147  @property
148  def is_closed(self) -> bool | None:
149  """Return true if cover is closed."""
150  if not (value := self._current_position_value_current_position_value) or value.value is None:
151  return None
152  return bool(value.value == self._fully_closed_position_fully_closed_position)
153 
154  @property
155  def current_cover_position(self) -> int | None:
156  """Return the current position of cover where 0 means closed and 100 is fully open."""
157  if (
158  self._current_position_value_current_position_value is None
159  or self._current_position_value_current_position_value.value is None
160  ):
161  # guard missing value
162  return None
163  return self.zwave_to_percent_positionzwave_to_percent_position(self._current_position_value_current_position_value.value)
164 
165  async def async_set_cover_position(self, **kwargs: Any) -> None:
166  """Move the cover to a specific position."""
167  assert self._target_position_value_target_position_value
168  await self._async_set_value_async_set_value(
169  self._target_position_value_target_position_value,
170  self.percent_to_zwave_positionpercent_to_zwave_position(kwargs[ATTR_POSITION]),
171  )
172 
173  async def async_open_cover(self, **kwargs: Any) -> None:
174  """Open the cover."""
175  assert self._target_position_value_target_position_value
176  await self._async_set_value_async_set_value(
177  self._target_position_value_target_position_value, self._fully_open_position_fully_open_position
178  )
179 
180  async def async_close_cover(self, **kwargs: Any) -> None:
181  """Close cover."""
182  assert self._target_position_value_target_position_value
183  await self._async_set_value_async_set_value(
184  self._target_position_value_target_position_value, self._fully_closed_position_fully_closed_position
185  )
186 
187  async def async_stop_cover(self, **kwargs: Any) -> None:
188  """Stop cover."""
189  assert self._stop_position_value_stop_position_value
190  # Stop the cover, will stop regardless of the actual direction of travel.
191  await self._async_set_value_async_set_value(self._stop_position_value_stop_position_value, False)
192 
193 
195  """Mix-in class for cover with tilt support."""
196 
197  _current_tilt_value: ZwaveValue | None = None
198  _target_tilt_value: ZwaveValue | None = None
199  _stop_tilt_value: ZwaveValue | None = None
200 
202  self,
203  current_value: ZwaveValue,
204  target_value: ZwaveValue | None = None,
205  stop_value: ZwaveValue | None = None,
206  ) -> None:
207  """Set values for tilt."""
208  self._attr_supported_features_attr_supported_features = (
209  (self._attr_supported_features_attr_supported_features or 0)
210  | CoverEntityFeature.OPEN_TILT
211  | CoverEntityFeature.CLOSE_TILT
212  | CoverEntityFeature.SET_TILT_POSITION
213  )
214  self._current_tilt_value_current_tilt_value = current_value
215  self._target_tilt_value_target_tilt_value = target_value or self.get_zwave_valueget_zwave_value(
216  TARGET_VALUE_PROPERTY, value_property_key=current_value.property_key
217  )
218 
219  if stop_value:
220  self._stop_tilt_value_stop_tilt_value = stop_value
221  self._attr_supported_features_attr_supported_features |= CoverEntityFeature.STOP_TILT
222 
223  def percent_to_zwave_tilt(self, value: int) -> int:
224  """Convert position in 0-100 scale to closed_value-open_value scale."""
225  return (
226  round(max(min(1, (value / 100)), 0) * self._tilt_range_tilt_range)
227  + self._fully_closed_tilt_fully_closed_tilt
228  )
229 
230  def zwave_to_percent_tilt(self, value: int) -> int:
231  """Convert closed_value-open_value scale to position in 0-100 scale."""
232  return round(((value - self._fully_closed_tilt_fully_closed_tilt) / self._tilt_range_tilt_range) * 100)
233 
234  @property
235  def _fully_open_tilt(self) -> int:
236  """Return value that represents fully opened tilt."""
237  max_ = self.infoinfo.primary_value.metadata.max
238  return 99 if max_ is None else max_
239 
240  @property
241  def _fully_closed_tilt(self) -> int:
242  """Return value that represents fully closed tilt."""
243  min_ = self.infoinfo.primary_value.metadata.min
244  return 0 if min_ is None else min_
245 
246  @property
247  def _tilt_range(self) -> int:
248  """Return range between fully opened and fully closed tilt."""
249  return self._fully_open_tilt_fully_open_tilt - self._fully_closed_tilt_fully_closed_tilt
250 
251  @property
252  def current_cover_tilt_position(self) -> int | None:
253  """Return current position of cover tilt.
254 
255  None is unknown, 0 is closed, 100 is fully open.
256  """
257  if (value := self._current_tilt_value_current_tilt_value) is None or value.value is None:
258  return None
259  return self.zwave_to_percent_tiltzwave_to_percent_tilt(int(value.value))
260 
261  async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
262  """Move the cover tilt to a specific position."""
263  assert self._target_tilt_value_target_tilt_value
264  await self._async_set_value_async_set_value(
265  self._target_tilt_value_target_tilt_value,
266  self.percent_to_zwave_tiltpercent_to_zwave_tilt(kwargs[ATTR_TILT_POSITION]),
267  )
268 
269  async def async_open_cover_tilt(self, **kwargs: Any) -> None:
270  """Open the cover tilt."""
271  assert self._target_tilt_value_target_tilt_value
272  await self._async_set_value_async_set_value(self._target_tilt_value_target_tilt_value, self._fully_open_tilt_fully_open_tilt)
273 
274  async def async_close_cover_tilt(self, **kwargs: Any) -> None:
275  """Close the cover tilt."""
276  assert self._target_tilt_value_target_tilt_value
277  await self._async_set_value_async_set_value(self._target_tilt_value_target_tilt_value, self._fully_closed_tilt_fully_closed_tilt)
278 
279  async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
280  """Stop the cover tilt."""
281  assert self._stop_tilt_value_stop_tilt_value
282  # Stop the tilt, will stop regardless of the actual direction of travel.
283  await self._async_set_value_async_set_value(self._stop_tilt_value_stop_tilt_value, False)
284 
285 
287  """Representation of a Z-Wave Cover that uses Multilevel Switch CC for position."""
288 
289  def __init__(
290  self,
291  config_entry: ConfigEntry,
292  driver: Driver,
293  info: ZwaveDiscoveryInfo,
294  ) -> None:
295  """Initialize a ZWaveCover entity."""
296  super().__init__(config_entry, driver, info)
297  self._set_position_values_set_position_values(
298  self.infoinfo.primary_value,
299  stop_value=(
300  self.get_zwave_valueget_zwave_value(COVER_OPEN_PROPERTY)
301  or self.get_zwave_valueget_zwave_value(COVER_UP_PROPERTY)
302  or self.get_zwave_valueget_zwave_value(COVER_ON_PROPERTY)
303  ),
304  )
305 
306  # Entity class attributes
307  self._attr_device_class_attr_device_class = CoverDeviceClass.WINDOW
308  if self.infoinfo.platform_hint and self.infoinfo.platform_hint.startswith("shutter"):
309  self._attr_device_class_attr_device_class = CoverDeviceClass.SHUTTER
310  elif self.infoinfo.platform_hint and self.infoinfo.platform_hint.startswith("blind"):
311  self._attr_device_class_attr_device_class = CoverDeviceClass.BLIND
312  elif self.infoinfo.platform_hint and self.infoinfo.platform_hint.startswith("gate"):
313  self._attr_device_class_attr_device_class = CoverDeviceClass.GATE
314 
315 
317  """Representation of a Z-Wave cover device with tilt."""
318 
319  def __init__(
320  self,
321  config_entry: ConfigEntry,
322  driver: Driver,
323  info: ZwaveDiscoveryInfo,
324  ) -> None:
325  """Initialize a ZWaveCover entity."""
326  super().__init__(config_entry, driver, info)
327 
328  template = cast(CoverTiltDataTemplate, self.infoinfo.platform_data_template)
329  self._set_tilt_values_set_tilt_values(
330  template.current_tilt_value(self.infoinfo.platform_data),
331  template.target_tilt_value(self.infoinfo.platform_data),
332  )
333 
334 
336  """Representation of a Z-Wave Window Covering cover device."""
337 
338  def __init__(
339  self, config_entry: ConfigEntry, driver: Driver, info: ZwaveDiscoveryInfo
340  ) -> None:
341  """Initialize."""
342  super().__init__(config_entry, driver, info)
343  pos_value: ZwaveValue | None = None
344  tilt_value: ZwaveValue | None = None
345  self._up_value_up_value = cast(
346  ZwaveValue,
347  self.get_zwave_valueget_zwave_value(
348  WINDOW_COVERING_LEVEL_CHANGE_UP_PROPERTY,
349  value_property_key=info.primary_value.property_key,
350  ),
351  )
352  self._down_value_down_value = cast(
353  ZwaveValue,
354  self.get_zwave_valueget_zwave_value(
355  WINDOW_COVERING_LEVEL_CHANGE_DOWN_PROPERTY,
356  value_property_key=info.primary_value.property_key,
357  ),
358  )
359 
360  # If primary value is for position, we have to search for a tilt value
361  if info.primary_value.property_key in COVER_POSITION_PROPERTY_KEYS:
362  pos_value = info.primary_value
363  tilt_value = next(
364  (
365  value
366  for property_key in COVER_TILT_PROPERTY_KEYS
367  if (
368  value := self.get_zwave_valueget_zwave_value(
369  CURRENT_VALUE_PROPERTY, value_property_key=property_key
370  )
371  )
372  ),
373  None,
374  )
375  # If primary value is for tilt, there is no position value
376  else:
377  tilt_value = info.primary_value
378 
379  # Set position and tilt values if they exist. If the corresponding value is of
380  # the type No Position, we remove the corresponding set position feature.
381  for set_values_func, value, set_position_feature in (
382  (self._set_position_values_set_position_values, pos_value, CoverEntityFeature.SET_POSITION),
383  (self._set_tilt_values_set_tilt_values, tilt_value, CoverEntityFeature.SET_TILT_POSITION),
384  ):
385  if value:
386  set_values_func(
387  value,
388  stop_value=self.get_zwave_valueget_zwave_value(
389  WINDOW_COVERING_LEVEL_CHANGE_UP_PROPERTY,
390  value_property_key=value.property_key,
391  ),
392  )
393  if value.property_key in NO_POSITION_PROPERTY_KEYS:
394  assert self._attr_supported_features_attr_supported_features_attr_supported_features
395  self._attr_supported_features_attr_supported_features_attr_supported_features ^= set_position_feature
396 
397  additional_info: list[str] = [
398  value.property_key_name.removesuffix(f" {NO_POSITION_SUFFIX}")
399  for value in (self._current_position_value_current_position_value, self._current_tilt_value_current_tilt_value)
400  if value and value.property_key_name
401  ]
402  self._attr_name_attr_name_attr_name = self.generate_namegenerate_name(additional_info=additional_info)
403  self._attr_device_class_attr_device_class = CoverDeviceClass.WINDOW
404 
405  @property
406  def _fully_open_tilt(self) -> int:
407  """Return position to open cover tilt."""
408  return SlatStates.OPEN
409 
410  @property
411  def _fully_closed_tilt(self) -> int:
412  """Return position to close cover tilt."""
413  return SlatStates.CLOSED_1
414 
415  @property
416  def _tilt_range(self) -> int:
417  """Return range of valid tilt positions."""
418  return abs(SlatStates.CLOSED_2 - SlatStates.CLOSED_1)
419 
420  async def async_open_cover(self, **kwargs: Any) -> None:
421  """Open the cover."""
422  await self._async_set_value_async_set_value(self._up_value_up_value, True)
423 
424  async def async_close_cover(self, **kwargs: Any) -> None:
425  """Close the cover."""
426  await self._async_set_value_async_set_value(self._down_value_down_value, True)
427 
428  async def async_stop_cover(self, **kwargs: Any) -> None:
429  """Stop the cover."""
430  await self._async_set_value_async_set_value(self._up_value_up_value, False)
431 
432 
434  """Representation of a Z-Wave motorized barrier device."""
435 
436  _attr_supported_features = CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
437  _attr_device_class = CoverDeviceClass.GARAGE
438 
439  def __init__(
440  self,
441  config_entry: ConfigEntry,
442  driver: Driver,
443  info: ZwaveDiscoveryInfo,
444  ) -> None:
445  """Initialize a ZwaveMotorizedBarrier entity."""
446  super().__init__(config_entry, driver, info)
447  # TARGET_STATE_PROPERTY is required in the discovery schema.
448  self._target_state_target_state = cast(
449  ZwaveValue,
450  self.get_zwave_valueget_zwave_value(TARGET_STATE_PROPERTY, add_to_watched_value_ids=False),
451  )
452 
453  @property
454  def is_opening(self) -> bool | None:
455  """Return if the cover is opening or not."""
456  if self.infoinfo.primary_value.value is None:
457  return None
458  return bool(self.infoinfo.primary_value.value == BarrierState.OPENING)
459 
460  @property
461  def is_closing(self) -> bool | None:
462  """Return if the cover is closing or not."""
463  if self.infoinfo.primary_value.value is None:
464  return None
465  return bool(self.infoinfo.primary_value.value == BarrierState.CLOSING)
466 
467  @property
468  def is_closed(self) -> bool | None:
469  """Return if the cover is closed or not."""
470  if self.infoinfo.primary_value.value is None:
471  return None
472  # If a barrier is in the stopped state, the only way to proceed is by
473  # issuing an open cover command. Return None in this case which
474  # produces an unknown state and allows it to be resolved with an open
475  # command.
476  if self.infoinfo.primary_value.value == BarrierState.STOPPED:
477  return None
478 
479  return bool(self.infoinfo.primary_value.value == BarrierState.CLOSED)
480 
481  async def async_open_cover(self, **kwargs: Any) -> None:
482  """Open the garage door."""
483  await self._async_set_value_async_set_value(self._target_state_target_state, BarrierState.OPEN)
484 
485  async def async_close_cover(self, **kwargs: Any) -> None:
486  """Close the garage door."""
487  await self._async_set_value_async_set_value(self._target_state_target_state, BarrierState.CLOSED)
None _set_position_values(self, ZwaveValue current_value, ZwaveValue|None target_value=None, ZwaveValue|None stop_value=None)
Definition: cover.py:100
None async_set_cover_tilt_position(self, **Any kwargs)
Definition: cover.py:261
None _set_tilt_values(self, ZwaveValue current_value, ZwaveValue|None target_value=None, ZwaveValue|None stop_value=None)
Definition: cover.py:206
None __init__(self, ConfigEntry config_entry, Driver driver, ZwaveDiscoveryInfo info)
Definition: cover.py:294
None __init__(self, ConfigEntry config_entry, Driver driver, ZwaveDiscoveryInfo info)
Definition: cover.py:324
None __init__(self, ConfigEntry config_entry, Driver driver, ZwaveDiscoveryInfo info)
Definition: cover.py:340
None __init__(self, ConfigEntry config_entry, Driver driver, ZwaveDiscoveryInfo info)
Definition: cover.py:444
str generate_name(self, bool include_value_name=False, str|None alternate_value_name=None, Sequence[str|None]|None additional_info=None, str|None name_prefix=None)
Definition: entity.py:163
SetValueResult|None _async_set_value(self, ZwaveValue value, Any new_value, dict|None options=None, bool|None wait_for_result=None)
Definition: entity.py:330
ZwaveValue|None get_zwave_value(self, str|int value_property, int|None command_class=None, int|None endpoint=None, int|str|None value_property_key=None, bool add_to_watched_value_ids=True, bool check_all_endpoints=False)
Definition: entity.py:280
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: cover.py:59
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103