Home Assistant Unofficial Reference 2024.12.1
cover.py
Go to the documentation of this file.
1 """Support for Tuya Cover."""
2 
3 from __future__ import annotations
4 
5 from dataclasses import dataclass
6 from typing import Any
7 
8 from tuya_sharing import CustomerDevice, Manager
9 
11  ATTR_POSITION,
12  ATTR_TILT_POSITION,
13  CoverDeviceClass,
14  CoverEntity,
15  CoverEntityDescription,
16  CoverEntityFeature,
17 )
18 from homeassistant.core import HomeAssistant, callback
19 from homeassistant.helpers.dispatcher import async_dispatcher_connect
20 from homeassistant.helpers.entity_platform import AddEntitiesCallback
21 
22 from . import TuyaConfigEntry
23 from .const import TUYA_DISCOVERY_NEW, DPCode, DPType
24 from .entity import IntegerTypeData, TuyaEntity
25 
26 
27 @dataclass(frozen=True)
29  """Describe an Tuya cover entity."""
30 
31  current_state: DPCode | None = None
32  current_state_inverse: bool = False
33  current_position: DPCode | tuple[DPCode, ...] | None = None
34  set_position: DPCode | None = None
35  open_instruction_value: str = "open"
36  close_instruction_value: str = "close"
37  stop_instruction_value: str = "stop"
38 
39 
40 COVERS: dict[str, tuple[TuyaCoverEntityDescription, ...]] = {
41  # Curtain
42  # Note: Multiple curtains isn't documented
43  # https://developer.tuya.com/en/docs/iot/categorycl?id=Kaiuz1hnpo7df
44  "cl": (
46  key=DPCode.CONTROL,
47  translation_key="curtain",
48  current_state=DPCode.SITUATION_SET,
49  current_position=(DPCode.PERCENT_STATE, DPCode.PERCENT_CONTROL),
50  set_position=DPCode.PERCENT_CONTROL,
51  device_class=CoverDeviceClass.CURTAIN,
52  ),
54  key=DPCode.CONTROL_2,
55  translation_key="curtain_2",
56  current_position=DPCode.PERCENT_STATE_2,
57  set_position=DPCode.PERCENT_CONTROL_2,
58  device_class=CoverDeviceClass.CURTAIN,
59  ),
61  key=DPCode.CONTROL_3,
62  translation_key="curtain_3",
63  current_position=DPCode.PERCENT_STATE_3,
64  set_position=DPCode.PERCENT_CONTROL_3,
65  device_class=CoverDeviceClass.CURTAIN,
66  ),
68  key=DPCode.MACH_OPERATE,
69  translation_key="curtain",
70  current_position=DPCode.POSITION,
71  set_position=DPCode.POSITION,
72  device_class=CoverDeviceClass.CURTAIN,
73  open_instruction_value="FZ",
74  close_instruction_value="ZZ",
75  stop_instruction_value="STOP",
76  ),
77  # switch_1 is an undocumented code that behaves identically to control
78  # It is used by the Kogan Smart Blinds Driver
80  key=DPCode.SWITCH_1,
81  translation_key="blind",
82  current_position=DPCode.PERCENT_CONTROL,
83  set_position=DPCode.PERCENT_CONTROL,
84  device_class=CoverDeviceClass.BLIND,
85  ),
86  ),
87  # Garage Door Opener
88  # https://developer.tuya.com/en/docs/iot/categoryckmkzq?id=Kaiuz0ipcboee
89  "ckmkzq": (
91  key=DPCode.SWITCH_1,
92  translation_key="door",
93  current_state=DPCode.DOORCONTACT_STATE,
94  current_state_inverse=True,
95  device_class=CoverDeviceClass.GARAGE,
96  ),
98  key=DPCode.SWITCH_2,
99  translation_key="door_2",
100  current_state=DPCode.DOORCONTACT_STATE_2,
101  current_state_inverse=True,
102  device_class=CoverDeviceClass.GARAGE,
103  ),
105  key=DPCode.SWITCH_3,
106  translation_key="door_3",
107  current_state=DPCode.DOORCONTACT_STATE_3,
108  current_state_inverse=True,
109  device_class=CoverDeviceClass.GARAGE,
110  ),
111  ),
112  # Curtain Switch
113  # https://developer.tuya.com/en/docs/iot/category-clkg?id=Kaiuz0gitil39
114  "clkg": (
116  key=DPCode.CONTROL,
117  translation_key="curtain",
118  current_position=DPCode.PERCENT_CONTROL,
119  set_position=DPCode.PERCENT_CONTROL,
120  device_class=CoverDeviceClass.CURTAIN,
121  ),
123  key=DPCode.CONTROL_2,
124  translation_key="curtain_2",
125  current_position=DPCode.PERCENT_CONTROL_2,
126  set_position=DPCode.PERCENT_CONTROL_2,
127  device_class=CoverDeviceClass.CURTAIN,
128  ),
129  ),
130  # Curtain Robot
131  # Note: Not documented
132  "jdcljqr": (
134  key=DPCode.CONTROL,
135  translation_key="curtain",
136  current_position=DPCode.PERCENT_STATE,
137  set_position=DPCode.PERCENT_CONTROL,
138  device_class=CoverDeviceClass.CURTAIN,
139  ),
140  ),
141 }
142 
143 
145  hass: HomeAssistant, entry: TuyaConfigEntry, async_add_entities: AddEntitiesCallback
146 ) -> None:
147  """Set up Tuya cover dynamically through Tuya discovery."""
148  hass_data = entry.runtime_data
149 
150  @callback
151  def async_discover_device(device_ids: list[str]) -> None:
152  """Discover and add a discovered tuya cover."""
153  entities: list[TuyaCoverEntity] = []
154  for device_id in device_ids:
155  device = hass_data.manager.device_map[device_id]
156  if descriptions := COVERS.get(device.category):
157  entities.extend(
158  TuyaCoverEntity(device, hass_data.manager, description)
159  for description in descriptions
160  if (
161  description.key in device.function
162  or description.key in device.status_range
163  )
164  )
165 
166  async_add_entities(entities)
167 
168  async_discover_device([*hass_data.manager.device_map])
169 
170  entry.async_on_unload(
171  async_dispatcher_connect(hass, TUYA_DISCOVERY_NEW, async_discover_device)
172  )
173 
174 
176  """Tuya Cover Device."""
177 
178  _current_position: IntegerTypeData | None = None
179  _set_position: IntegerTypeData | None = None
180  _tilt: IntegerTypeData | None = None
181  entity_description: TuyaCoverEntityDescription
182 
183  def __init__(
184  self,
185  device: CustomerDevice,
186  device_manager: Manager,
187  description: TuyaCoverEntityDescription,
188  ) -> None:
189  """Init Tuya Cover."""
190  super().__init__(device, device_manager)
191  self.entity_descriptionentity_description = description
192  self._attr_unique_id_attr_unique_id_attr_unique_id = f"{super().unique_id}{description.key}"
193  self._attr_supported_features_attr_supported_features = CoverEntityFeature(0)
194 
195  # Check if this cover is based on a switch or has controls
196  if self.find_dpcodefind_dpcodefind_dpcodefind_dpcodefind_dpcode(description.key, prefer_function=True):
197  if device.function[description.key].type == "Boolean":
198  self._attr_supported_features_attr_supported_features |= (
199  CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
200  )
201  elif enum_type := self.find_dpcodefind_dpcodefind_dpcodefind_dpcodefind_dpcode(
202  description.key, dptype=DPType.ENUM, prefer_function=True
203  ):
204  if description.open_instruction_value in enum_type.range:
205  self._attr_supported_features_attr_supported_features |= CoverEntityFeature.OPEN
206  if description.close_instruction_value in enum_type.range:
207  self._attr_supported_features_attr_supported_features |= CoverEntityFeature.CLOSE
208  if description.stop_instruction_value in enum_type.range:
209  self._attr_supported_features_attr_supported_features |= CoverEntityFeature.STOP
210 
211  # Determine type to use for setting the position
212  if int_type := self.find_dpcodefind_dpcodefind_dpcodefind_dpcodefind_dpcode(
213  description.set_position, dptype=DPType.INTEGER, prefer_function=True
214  ):
215  self._attr_supported_features_attr_supported_features |= CoverEntityFeature.SET_POSITION
216  self._set_position_set_position = int_type
217  # Set as default, unless overwritten below
218  self._current_position_current_position = int_type
219 
220  # Determine type for getting the position
221  if int_type := self.find_dpcodefind_dpcodefind_dpcodefind_dpcodefind_dpcode(
222  description.current_position, dptype=DPType.INTEGER, prefer_function=True
223  ):
224  self._current_position_current_position = int_type
225 
226  # Determine type to use for setting the tilt
227  if int_type := self.find_dpcodefind_dpcodefind_dpcodefind_dpcodefind_dpcode(
228  (DPCode.ANGLE_HORIZONTAL, DPCode.ANGLE_VERTICAL),
229  dptype=DPType.INTEGER,
230  prefer_function=True,
231  ):
232  self._attr_supported_features_attr_supported_features |= CoverEntityFeature.SET_TILT_POSITION
233  self._tilt_tilt = int_type
234 
235  @property
236  def current_cover_position(self) -> int | None:
237  """Return cover current position."""
238  if self._current_position_current_position is None:
239  return None
240 
241  if (position := self.devicedevice.status.get(self._current_position_current_position.dpcode)) is None:
242  return None
243 
244  return round(
245  self._current_position_current_position.remap_value_to(position, 0, 100, reverse=True)
246  )
247 
248  @property
249  def current_cover_tilt_position(self) -> int | None:
250  """Return current position of cover tilt.
251 
252  None is unknown, 0 is closed, 100 is fully open.
253  """
254  if self._tilt_tilt is None:
255  return None
256 
257  if (angle := self.devicedevice.status.get(self._tilt_tilt.dpcode)) is None:
258  return None
259 
260  return round(self._tilt_tilt.remap_value_to(angle, 0, 100))
261 
262  @property
263  def is_closed(self) -> bool | None:
264  """Return true if cover is closed."""
265  if (
266  self.entity_descriptionentity_description.current_state is not None
267  and (
268  current_state := self.devicedevice.status.get(
269  self.entity_descriptionentity_description.current_state
270  )
271  )
272  is not None
273  ):
274  return self.entity_descriptionentity_description.current_state_inverse is not (
275  current_state in (True, "fully_close")
276  )
277 
278  if (position := self.current_cover_positioncurrent_cover_positioncurrent_cover_positioncurrent_cover_position) is not None:
279  return position == 0
280 
281  return None
282 
283  def open_cover(self, **kwargs: Any) -> None:
284  """Open the cover."""
285  value: bool | str = True
287  self.entity_descriptionentity_description.key, dptype=DPType.ENUM, prefer_function=True
288  ):
289  value = self.entity_descriptionentity_description.open_instruction_value
290 
291  commands: list[dict[str, str | int]] = [
292  {"code": self.entity_descriptionentity_description.key, "value": value}
293  ]
294 
295  if self._set_position_set_position is not None:
296  commands.append(
297  {
298  "code": self._set_position_set_position.dpcode,
299  "value": round(
300  self._set_position_set_position.remap_value_from(100, 0, 100, reverse=True),
301  ),
302  }
303  )
304 
305  self._send_command_send_command(commands)
306 
307  def close_cover(self, **kwargs: Any) -> None:
308  """Close cover."""
309  value: bool | str = False
311  self.entity_descriptionentity_description.key, dptype=DPType.ENUM, prefer_function=True
312  ):
313  value = self.entity_descriptionentity_description.close_instruction_value
314 
315  commands: list[dict[str, str | int]] = [
316  {"code": self.entity_descriptionentity_description.key, "value": value}
317  ]
318 
319  if self._set_position_set_position is not None:
320  commands.append(
321  {
322  "code": self._set_position_set_position.dpcode,
323  "value": round(
324  self._set_position_set_position.remap_value_from(0, 0, 100, reverse=True),
325  ),
326  }
327  )
328 
329  self._send_command_send_command(commands)
330 
331  def set_cover_position(self, **kwargs: Any) -> None:
332  """Move the cover to a specific position."""
333  if self._set_position_set_position is None:
334  raise RuntimeError(
335  "Cannot set position, device doesn't provide methods to set it"
336  )
337 
338  self._send_command_send_command(
339  [
340  {
341  "code": self._set_position_set_position.dpcode,
342  "value": round(
343  self._set_position_set_position.remap_value_from(
344  kwargs[ATTR_POSITION], 0, 100, reverse=True
345  )
346  ),
347  }
348  ]
349  )
350 
351  def stop_cover(self, **kwargs: Any) -> None:
352  """Stop the cover."""
353  self._send_command_send_command(
354  [
355  {
356  "code": self.entity_descriptionentity_description.key,
357  "value": self.entity_descriptionentity_description.stop_instruction_value,
358  }
359  ]
360  )
361 
362  def set_cover_tilt_position(self, **kwargs: Any) -> None:
363  """Move the cover tilt to a specific position."""
364  if self._tilt_tilt is None:
365  raise RuntimeError(
366  "Cannot set tilt, device doesn't provide methods to set it"
367  )
368 
369  self._send_command_send_command(
370  [
371  {
372  "code": self._tilt_tilt.dpcode,
373  "value": round(
374  self._tilt_tilt.remap_value_from(
375  kwargs[ATTR_TILT_POSITION], 0, 100, reverse=True
376  )
377  ),
378  }
379  ]
380  )
None set_cover_position(self, **Any kwargs)
Definition: cover.py:331
None set_cover_tilt_position(self, **Any kwargs)
Definition: cover.py:362
None __init__(self, CustomerDevice device, Manager device_manager, TuyaCoverEntityDescription description)
Definition: cover.py:188
None _send_command(self, list[dict[str, Any]] commands)
Definition: entity.py:295
DPCode|EnumTypeData|IntegerTypeData|None find_dpcode(self, str|DPCode|tuple[DPCode,...]|None dpcodes, *bool prefer_function=False, DPType|None dptype=None)
Definition: entity.py:206
IntegerTypeData|None find_dpcode(self, str|DPCode|tuple[DPCode,...]|None dpcodes, *bool prefer_function=False, Literal[DPType.INTEGER] dptype)
Definition: entity.py:190
DPCode|None find_dpcode(self, str|DPCode|tuple[DPCode,...]|None dpcodes, *bool prefer_function=False)
Definition: entity.py:198
EnumTypeData|None find_dpcode(self, str|DPCode|tuple[DPCode,...]|None dpcodes, *bool prefer_function=False, Literal[DPType.ENUM] dptype)
Definition: entity.py:181
ElkSystem|None async_discover_device(HomeAssistant hass, str host)
Definition: discovery.py:78
None async_setup_entry(HomeAssistant hass, TuyaConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: cover.py:146
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103