Home Assistant Unofficial Reference 2024.12.1
cover.py
Go to the documentation of this file.
1 """Support for Motionblinds using their WLAN API."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any
7 
8 from motionblinds import BlindType
9 import voluptuous as vol
10 
12  ATTR_POSITION,
13  ATTR_TILT_POSITION,
14  CoverDeviceClass,
15  CoverEntity,
16  CoverEntityFeature,
17 )
18 from homeassistant.config_entries import ConfigEntry
19 from homeassistant.core import HomeAssistant
20 from homeassistant.helpers import config_validation as cv, entity_platform
21 from homeassistant.helpers.entity_platform import AddEntitiesCallback
22 from homeassistant.helpers.typing import VolDictType
23 
24 from .const import (
25  ATTR_ABSOLUTE_POSITION,
26  ATTR_AVAILABLE,
27  ATTR_WIDTH,
28  DOMAIN,
29  KEY_COORDINATOR,
30  KEY_GATEWAY,
31  SERVICE_SET_ABSOLUTE_POSITION,
32  UPDATE_DELAY_STOP,
33 )
34 from .entity import MotionCoordinatorEntity
35 
36 _LOGGER = logging.getLogger(__name__)
37 
38 
39 POSITION_DEVICE_MAP = {
40  BlindType.RollerBlind: CoverDeviceClass.SHADE,
41  BlindType.RomanBlind: CoverDeviceClass.SHADE,
42  BlindType.HoneycombBlind: CoverDeviceClass.SHADE,
43  BlindType.DimmingBlind: CoverDeviceClass.SHADE,
44  BlindType.DayNightBlind: CoverDeviceClass.SHADE,
45  BlindType.RollerShutter: CoverDeviceClass.SHUTTER,
46  BlindType.Switch: CoverDeviceClass.SHUTTER,
47  BlindType.RollerGate: CoverDeviceClass.GATE,
48  BlindType.Awning: CoverDeviceClass.AWNING,
49  BlindType.Curtain: CoverDeviceClass.CURTAIN,
50  BlindType.CurtainLeft: CoverDeviceClass.CURTAIN,
51  BlindType.CurtainRight: CoverDeviceClass.CURTAIN,
52  BlindType.SkylightBlind: CoverDeviceClass.SHADE,
53  BlindType.InsectScreen: CoverDeviceClass.SHADE,
54 }
55 
56 TILT_DEVICE_MAP = {
57  BlindType.VenetianBlind: CoverDeviceClass.BLIND,
58  BlindType.ShangriLaBlind: CoverDeviceClass.BLIND,
59  BlindType.DoubleRoller: CoverDeviceClass.SHADE,
60  BlindType.DualShade: CoverDeviceClass.SHADE,
61  BlindType.VerticalBlind: CoverDeviceClass.BLIND,
62  BlindType.VerticalBlindLeft: CoverDeviceClass.BLIND,
63  BlindType.VerticalBlindRight: CoverDeviceClass.BLIND,
64 }
65 
66 TILT_ONLY_DEVICE_MAP = {
67  BlindType.WoodShutter: CoverDeviceClass.BLIND,
68 }
69 
70 TDBU_DEVICE_MAP = {
71  BlindType.TopDownBottomUp: CoverDeviceClass.SHADE,
72  BlindType.TriangleBlind: CoverDeviceClass.BLIND,
73 }
74 
75 
76 SET_ABSOLUTE_POSITION_SCHEMA: VolDictType = {
77  vol.Required(ATTR_ABSOLUTE_POSITION): vol.All(cv.positive_int, vol.Range(max=100)),
78  vol.Optional(ATTR_TILT_POSITION): vol.All(cv.positive_int, vol.Range(max=100)),
79  vol.Optional(ATTR_WIDTH): vol.All(cv.positive_int, vol.Range(max=100)),
80 }
81 
82 
84  hass: HomeAssistant,
85  config_entry: ConfigEntry,
86  async_add_entities: AddEntitiesCallback,
87 ) -> None:
88  """Set up the Motion Blind from a config entry."""
89  entities: list[MotionBaseDevice] = []
90  motion_gateway = hass.data[DOMAIN][config_entry.entry_id][KEY_GATEWAY]
91  coordinator = hass.data[DOMAIN][config_entry.entry_id][KEY_COORDINATOR]
92 
93  for blind in motion_gateway.device_list.values():
94  if blind.type in POSITION_DEVICE_MAP:
95  entities.append(
97  coordinator,
98  blind,
99  POSITION_DEVICE_MAP[blind.type],
100  )
101  )
102 
103  elif blind.type in TILT_DEVICE_MAP:
104  entities.append(
106  coordinator,
107  blind,
108  TILT_DEVICE_MAP[blind.type],
109  )
110  )
111 
112  elif blind.type in TILT_ONLY_DEVICE_MAP:
113  entities.append(
115  coordinator,
116  blind,
117  TILT_ONLY_DEVICE_MAP[blind.type],
118  )
119  )
120 
121  elif blind.type in TDBU_DEVICE_MAP:
122  entities.append(
124  coordinator,
125  blind,
126  TDBU_DEVICE_MAP[blind.type],
127  "Top",
128  )
129  )
130  entities.append(
132  coordinator,
133  blind,
134  TDBU_DEVICE_MAP[blind.type],
135  "Bottom",
136  )
137  )
138  entities.append(
140  coordinator,
141  blind,
142  TDBU_DEVICE_MAP[blind.type],
143  "Combined",
144  )
145  )
146 
147  else:
148  _LOGGER.warning(
149  "Blind type '%s' not yet supported, assuming RollerBlind",
150  blind.blind_type,
151  )
152  entities.append(
154  coordinator,
155  blind,
156  POSITION_DEVICE_MAP[BlindType.RollerBlind],
157  )
158  )
159 
160  async_add_entities(entities)
161 
162  platform = entity_platform.async_get_current_platform()
163  platform.async_register_entity_service(
164  SERVICE_SET_ABSOLUTE_POSITION,
165  SET_ABSOLUTE_POSITION_SCHEMA,
166  "async_set_absolute_position",
167  )
168 
169 
171  """Representation of a Motionblinds Device."""
172 
173  _restore_tilt = False
174 
175  def __init__(self, coordinator, blind, device_class):
176  """Initialize the blind."""
177  super().__init__(coordinator, blind)
178 
179  self._attr_device_class_attr_device_class = device_class
180  self._attr_unique_id_attr_unique_id = blind.mac
181 
182  @property
183  def available(self) -> bool:
184  """Return True if entity is available."""
185  if self.coordinator.data is None:
186  return False
187 
188  if not self.coordinator.data[KEY_GATEWAY][ATTR_AVAILABLE]:
189  return False
190 
191  return self.coordinator.data[self._blind_blind.mac][ATTR_AVAILABLE]
192 
193  @property
194  def current_cover_position(self) -> int | None:
195  """Return current position of cover.
196 
197  None is unknown, 0 is open, 100 is closed.
198  """
199  if self._blind_blind.position is None:
200  return None
201  return 100 - self._blind_blind.position
202 
203  @property
204  def is_closed(self) -> bool | None:
205  """Return if the cover is closed or not."""
206  if self._blind_blind.position is None:
207  return None
208  return self._blind_blind.position == 100
209 
210  async def async_open_cover(self, **kwargs: Any) -> None:
211  """Open the cover."""
212  async with self._api_lock_api_lock:
213  await self.hasshasshass.async_add_executor_job(self._blind_blind.Open)
214  await self.async_request_position_till_stopasync_request_position_till_stop()
215 
216  async def async_close_cover(self, **kwargs: Any) -> None:
217  """Close cover."""
218  async with self._api_lock_api_lock:
219  await self.hasshasshass.async_add_executor_job(self._blind_blind.Close)
220  await self.async_request_position_till_stopasync_request_position_till_stop()
221 
222  async def async_set_cover_position(self, **kwargs: Any) -> None:
223  """Move the cover to a specific position."""
224  position = kwargs[ATTR_POSITION]
225  async with self._api_lock_api_lock:
226  await self.hasshasshass.async_add_executor_job(
227  self._blind_blind.Set_position,
228  100 - position,
229  None,
230  self._restore_tilt_restore_tilt,
231  )
232  await self.async_request_position_till_stopasync_request_position_till_stop()
233 
234  async def async_set_absolute_position(self, **kwargs):
235  """Move the cover to a specific absolute position (see TDBU)."""
236  position = kwargs[ATTR_ABSOLUTE_POSITION]
237  angle = kwargs.get(ATTR_TILT_POSITION)
238  if angle is not None:
239  angle = angle * 180 / 100
240  async with self._api_lock_api_lock:
241  await self.hasshasshass.async_add_executor_job(
242  self._blind_blind.Set_position,
243  100 - position,
244  angle,
245  self._restore_tilt_restore_tilt,
246  )
247  await self.async_request_position_till_stopasync_request_position_till_stop()
248 
249  async def async_stop_cover(self, **kwargs: Any) -> None:
250  """Stop the cover."""
251  async with self._api_lock_api_lock:
252  await self.hasshasshass.async_add_executor_job(self._blind_blind.Stop)
253 
254  await self.async_request_position_till_stopasync_request_position_till_stop(delay=UPDATE_DELAY_STOP)
255 
256 
258  """Representation of a Motion Blind Device."""
259 
260  _attr_name = None
261 
262 
264  """Representation of a Motionblinds Device."""
265 
266  _restore_tilt = True
267 
268  @property
269  def current_cover_tilt_position(self) -> int | None:
270  """Return current angle of cover.
271 
272  None is unknown, 0 is closed/minimum tilt, 100 is fully open/maximum tilt.
273  """
274  if self._blind_blind.angle is None:
275  return None
276  return self._blind_blind.angle * 100 / 180
277 
278  @property
279  def is_closed(self) -> bool | None:
280  """Return if the cover is closed or not."""
281  if self._blind_blind.position is None:
282  return None
283  return self._blind_blind.position >= 95
284 
285  async def async_open_cover_tilt(self, **kwargs: Any) -> None:
286  """Open the cover tilt."""
287  async with self._api_lock_api_lock:
288  await self.hasshasshass.async_add_executor_job(self._blind_blind.Set_angle, 180)
289 
290  async def async_close_cover_tilt(self, **kwargs: Any) -> None:
291  """Close the cover tilt."""
292  async with self._api_lock_api_lock:
293  await self.hasshasshass.async_add_executor_job(self._blind_blind.Set_angle, 0)
294 
295  async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
296  """Move the cover tilt to a specific position."""
297  angle = kwargs[ATTR_TILT_POSITION] * 180 / 100
298  async with self._api_lock_api_lock:
299  await self.hasshasshass.async_add_executor_job(self._blind_blind.Set_angle, angle)
300 
301  async def async_stop_cover_tilt(self, **kwargs: Any) -> None:
302  """Stop the cover."""
303  async with self._api_lock_api_lock:
304  await self.hasshasshass.async_add_executor_job(self._blind_blind.Stop)
305 
306  await self.async_request_position_till_stopasync_request_position_till_stop(delay=UPDATE_DELAY_STOP)
307 
308 
310  """Representation of a Motionblinds Device."""
311 
312  _restore_tilt = False
313 
314  @property
315  def supported_features(self) -> CoverEntityFeature:
316  """Flag supported features."""
317  supported_features = (
318  CoverEntityFeature.OPEN_TILT
319  | CoverEntityFeature.CLOSE_TILT
320  | CoverEntityFeature.STOP_TILT
321  )
322 
324  supported_features |= CoverEntityFeature.SET_TILT_POSITION
325 
326  return supported_features
327 
328  @property
329  def current_cover_position(self) -> None:
330  """Return current position of cover."""
331  return None
332 
333  @property
334  def current_cover_tilt_position(self) -> int | None:
335  """Return current angle of cover.
336 
337  None is unknown, 0 is closed/minimum tilt, 100 is fully open/maximum tilt.
338  """
339  if self._blind_blind.position is None:
340  if self._blind_blind.angle is None:
341  return None
342  return self._blind_blind.angle * 100 / 180
343 
344  return self._blind_blind.position
345 
346  @property
347  def is_closed(self) -> bool | None:
348  """Return if the cover is closed or not."""
349  if self._blind_blind.position is None:
350  if self._blind_blind.angle is None:
351  return None
352  return self._blind_blind.angle == 0
353 
354  return self._blind_blind.position == 0
355 
356  async def async_open_cover_tilt(self, **kwargs: Any) -> None:
357  """Open the cover tilt."""
358  async with self._api_lock_api_lock:
359  await self.hasshasshass.async_add_executor_job(self._blind_blind.Open)
360 
361  async def async_close_cover_tilt(self, **kwargs: Any) -> None:
362  """Close the cover tilt."""
363  async with self._api_lock_api_lock:
364  await self.hasshasshass.async_add_executor_job(self._blind_blind.Close)
365 
366  async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
367  """Move the cover tilt to a specific position."""
368  angle = kwargs[ATTR_TILT_POSITION]
369  if self._blind_blind.position is None:
370  angle = angle * 180 / 100
371  async with self._api_lock_api_lock:
372  await self.hasshasshass.async_add_executor_job(self._blind_blind.Set_angle, angle)
373  else:
374  async with self._api_lock_api_lock:
375  await self.hasshasshass.async_add_executor_job(self._blind_blind.Set_position, angle)
376 
377  async def async_set_absolute_position(self, **kwargs):
378  """Move the cover to a specific absolute position (see TDBU)."""
379  angle = kwargs.get(ATTR_TILT_POSITION)
380  if angle is None:
381  return
382 
383  if self._blind_blind.position is None:
384  angle = angle * 180 / 100
385  async with self._api_lock_api_lock:
386  await self.hasshasshass.async_add_executor_job(self._blind_blind.Set_angle, angle)
387  else:
388  async with self._api_lock_api_lock:
389  await self.hasshasshass.async_add_executor_job(self._blind_blind.Set_position, angle)
390 
391 
393  """Representation of a Motion Top Down Bottom Up blind Device."""
394 
395  def __init__(self, coordinator, blind, device_class, motor):
396  """Initialize the blind."""
397  super().__init__(coordinator, blind, device_class)
398  self._motor_motor = motor
399  self._motor_key_motor_key = motor[0]
400  self._attr_translation_key_attr_translation_key = motor.lower()
401  self._attr_unique_id_attr_unique_id_attr_unique_id = f"{blind.mac}-{motor}"
402 
403  if self._motor_motor not in ["Bottom", "Top", "Combined"]:
404  _LOGGER.error("Unknown motor '%s'", self._motor_motor)
405 
406  @property
407  def current_cover_position(self) -> int | None:
408  """Return current position of cover.
409 
410  None is unknown, 0 is open, 100 is closed.
411  """
412  if self._blind_blind.scaled_position is None:
413  return None
414 
415  return 100 - self._blind_blind.scaled_position[self._motor_key_motor_key]
416 
417  @property
418  def is_closed(self) -> bool | None:
419  """Return if the cover is closed or not."""
420  if self._blind_blind.position is None:
421  return None
422 
423  if self._motor_motor == "Combined":
424  return self._blind_blind.width == 100
425 
426  return self._blind_blind.position[self._motor_key_motor_key] == 100
427 
428  @property
429  def extra_state_attributes(self) -> dict[str, Any]:
430  """Return device specific state attributes."""
431  attributes = {}
432  if self._blind_blind.position is not None:
433  attributes[ATTR_ABSOLUTE_POSITION] = (
434  100 - self._blind_blind.position[self._motor_key_motor_key]
435  )
436  if self._blind_blind.width is not None:
437  attributes[ATTR_WIDTH] = self._blind_blind.width
438  return attributes
439 
440  async def async_open_cover(self, **kwargs: Any) -> None:
441  """Open the cover."""
442  async with self._api_lock_api_lock:
443  await self.hasshasshass.async_add_executor_job(self._blind_blind.Open, self._motor_key_motor_key)
444  await self.async_request_position_till_stopasync_request_position_till_stop()
445 
446  async def async_close_cover(self, **kwargs: Any) -> None:
447  """Close cover."""
448  async with self._api_lock_api_lock:
449  await self.hasshasshass.async_add_executor_job(self._blind_blind.Close, self._motor_key_motor_key)
450  await self.async_request_position_till_stopasync_request_position_till_stop()
451 
452  async def async_set_cover_position(self, **kwargs: Any) -> None:
453  """Move the cover to a specific scaled position."""
454  position = kwargs[ATTR_POSITION]
455  async with self._api_lock_api_lock:
456  await self.hasshasshass.async_add_executor_job(
457  self._blind_blind.Set_scaled_position, 100 - position, self._motor_key_motor_key
458  )
459  await self.async_request_position_till_stopasync_request_position_till_stop()
460 
461  async def async_set_absolute_position(self, **kwargs):
462  """Move the cover to a specific absolute position."""
463  position = kwargs[ATTR_ABSOLUTE_POSITION]
464  target_width = kwargs.get(ATTR_WIDTH)
465 
466  async with self._api_lock_api_lock:
467  await self.hasshasshass.async_add_executor_job(
468  self._blind_blind.Set_position, 100 - position, self._motor_key_motor_key, target_width
469  )
470 
471  await self.async_request_position_till_stopasync_request_position_till_stop()
472 
473  async def async_stop_cover(self, **kwargs: Any) -> None:
474  """Stop the cover."""
475  async with self._api_lock_api_lock:
476  await self.hasshasshass.async_add_executor_job(self._blind_blind.Stop, self._motor_key_motor_key)
477 
478  await self.async_request_position_till_stopasync_request_position_till_stop(delay=UPDATE_DELAY_STOP)
def __init__(self, coordinator, blind, device_class)
Definition: cover.py:175
def __init__(self, coordinator, blind, device_class, motor)
Definition: cover.py:395
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: cover.py:87