Skip to content

Commit f3a185f

Browse files
epenetCopilot
andauthored
Migrate Tuya cover to use wrapper class (home-assistant#156558)
Co-authored-by: Copilot <[email protected]>
1 parent 5a5a106 commit f3a185f

File tree

1 file changed

+89
-136
lines changed

1 file changed

+89
-136
lines changed

homeassistant/components/tuya/cover.py

Lines changed: 89 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
from dataclasses import dataclass
6-
from typing import TYPE_CHECKING, Any
6+
from typing import Any
77

88
from tuya_sharing import CustomerDevice, Manager
99

@@ -22,22 +22,71 @@
2222
from . import TuyaConfigEntry
2323
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType
2424
from .entity import TuyaEntity
25-
from .models import EnumTypeData, IntegerTypeData, find_dpcode
25+
from .models import DPCodeIntegerWrapper, find_dpcode
2626
from .util import get_dpcode
2727

2828

29+
class _DPCodePercentageMappingWrapper(DPCodeIntegerWrapper):
30+
"""Wrapper for DPCode position values mapping to 0-100 range."""
31+
32+
def _position_reversed(self, device: CustomerDevice) -> bool:
33+
"""Check if the position and direction should be reversed."""
34+
return False
35+
36+
def read_device_status(self, device: CustomerDevice) -> float | None:
37+
if (value := self._read_device_status_raw(device)) is None:
38+
return None
39+
40+
return round(
41+
self.type_information.remap_value_to(
42+
value,
43+
0,
44+
100,
45+
self._position_reversed(device),
46+
)
47+
)
48+
49+
def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any:
50+
return round(
51+
self.type_information.remap_value_from(
52+
value,
53+
0,
54+
100,
55+
self._position_reversed(device),
56+
)
57+
)
58+
59+
60+
class _InvertedPercentageMappingWrapper(_DPCodePercentageMappingWrapper):
61+
"""Wrapper for DPCode position values mapping to 0-100 range."""
62+
63+
def _position_reversed(self, device: CustomerDevice) -> bool:
64+
"""Check if the position and direction should be reversed."""
65+
return True
66+
67+
68+
class _ControlBackModePercentageMappingWrapper(_DPCodePercentageMappingWrapper):
69+
"""Wrapper for DPCode position values with control_back_mode support."""
70+
71+
def _position_reversed(self, device: CustomerDevice) -> bool:
72+
"""Check if the position and direction should be reversed."""
73+
return device.status.get(DPCode.CONTROL_BACK_MODE) != "back"
74+
75+
2976
@dataclass(frozen=True)
3077
class TuyaCoverEntityDescription(CoverEntityDescription):
3178
"""Describe an Tuya cover entity."""
3279

3380
current_state: DPCode | tuple[DPCode, ...] | None = None
3481
current_state_inverse: bool = False
3582
current_position: DPCode | tuple[DPCode, ...] | None = None
83+
position_wrapper: type[_DPCodePercentageMappingWrapper] = (
84+
_InvertedPercentageMappingWrapper
85+
)
3686
set_position: DPCode | None = None
3787
open_instruction_value: str = "open"
3888
close_instruction_value: str = "close"
3989
stop_instruction_value: str = "stop"
40-
motor_reverse_mode: DPCode | None = None
4190

4291

4392
COVERS: dict[DeviceCategory, tuple[TuyaCoverEntityDescription, ...]] = {
@@ -117,17 +166,17 @@ class TuyaCoverEntityDescription(CoverEntityDescription):
117166
key=DPCode.CONTROL,
118167
translation_key="curtain",
119168
current_position=DPCode.PERCENT_CONTROL,
169+
position_wrapper=_ControlBackModePercentageMappingWrapper,
120170
set_position=DPCode.PERCENT_CONTROL,
121-
motor_reverse_mode=DPCode.CONTROL_BACK_MODE,
122171
device_class=CoverDeviceClass.CURTAIN,
123172
),
124173
TuyaCoverEntityDescription(
125174
key=DPCode.CONTROL_2,
126175
translation_key="indexed_curtain",
127176
translation_placeholders={"index": "2"},
128177
current_position=DPCode.PERCENT_CONTROL_2,
178+
position_wrapper=_ControlBackModePercentageMappingWrapper,
129179
set_position=DPCode.PERCENT_CONTROL_2,
130-
motor_reverse_mode=DPCode.CONTROL_BACK_MODE,
131180
device_class=CoverDeviceClass.CURTAIN,
132181
),
133182
),
@@ -159,7 +208,22 @@ def async_discover_device(device_ids: list[str]) -> None:
159208
device = manager.device_map[device_id]
160209
if descriptions := COVERS.get(device.category):
161210
entities.extend(
162-
TuyaCoverEntity(device, manager, description)
211+
TuyaCoverEntity(
212+
device,
213+
manager,
214+
description,
215+
current_position=description.position_wrapper.find_dpcode(
216+
device, description.current_position
217+
),
218+
set_position=description.position_wrapper.find_dpcode(
219+
device, description.set_position, prefer_function=True
220+
),
221+
tilt_position=description.position_wrapper.find_dpcode(
222+
device,
223+
(DPCode.ANGLE_HORIZONTAL, DPCode.ANGLE_VERTICAL),
224+
prefer_function=True,
225+
),
226+
)
163227
for description in descriptions
164228
if (
165229
description.key in device.function
@@ -179,25 +243,29 @@ def async_discover_device(device_ids: list[str]) -> None:
179243
class TuyaCoverEntity(TuyaEntity, CoverEntity):
180244
"""Tuya Cover Device."""
181245

182-
_current_position: IntegerTypeData | None = None
183246
_current_state: DPCode | None = None
184-
_set_position: IntegerTypeData | None = None
185-
_tilt: IntegerTypeData | None = None
186-
_motor_reverse_mode_enum: EnumTypeData | None = None
187247
entity_description: TuyaCoverEntityDescription
188248

189249
def __init__(
190250
self,
191251
device: CustomerDevice,
192252
device_manager: Manager,
193253
description: TuyaCoverEntityDescription,
254+
*,
255+
current_position: _DPCodePercentageMappingWrapper | None = None,
256+
set_position: _DPCodePercentageMappingWrapper | None = None,
257+
tilt_position: _DPCodePercentageMappingWrapper | None = None,
194258
) -> None:
195259
"""Init Tuya Cover."""
196260
super().__init__(device, device_manager)
197261
self.entity_description = description
198262
self._attr_unique_id = f"{super().unique_id}{description.key}"
199263
self._attr_supported_features = CoverEntityFeature(0)
200264

265+
self._current_position = current_position or set_position
266+
self._set_position = set_position
267+
self._tilt_position = tilt_position
268+
201269
# Check if this cover is based on a switch or has controls
202270
if get_dpcode(self.device, description.key):
203271
if device.function[description.key].type == "Boolean":
@@ -216,86 +284,23 @@ def __init__(
216284

217285
self._current_state = get_dpcode(self.device, description.current_state)
218286

219-
# Determine type to use for setting the position
220-
if int_type := find_dpcode(
221-
self.device,
222-
description.set_position,
223-
dptype=DPType.INTEGER,
224-
prefer_function=True,
225-
):
287+
if set_position:
226288
self._attr_supported_features |= CoverEntityFeature.SET_POSITION
227-
self._set_position = int_type
228-
# Set as default, unless overwritten below
229-
self._current_position = int_type
230-
231-
# Determine type for getting the position
232-
if int_type := find_dpcode(
233-
self.device,
234-
description.current_position,
235-
dptype=DPType.INTEGER,
236-
prefer_function=True,
237-
):
238-
self._current_position = int_type
239-
240-
# Determine type to use for setting the tilt
241-
if int_type := find_dpcode(
242-
self.device,
243-
(DPCode.ANGLE_HORIZONTAL, DPCode.ANGLE_VERTICAL),
244-
dptype=DPType.INTEGER,
245-
prefer_function=True,
246-
):
289+
if tilt_position:
247290
self._attr_supported_features |= CoverEntityFeature.SET_TILT_POSITION
248-
self._tilt = int_type
249-
250-
# Determine type to use for checking motor reverse mode
251-
if (motor_mode := description.motor_reverse_mode) and (
252-
enum_type := find_dpcode(
253-
self.device,
254-
motor_mode,
255-
dptype=DPType.ENUM,
256-
prefer_function=True,
257-
)
258-
):
259-
self._motor_reverse_mode_enum = enum_type
260-
261-
@property
262-
def _is_position_reversed(self) -> bool:
263-
"""Check if the cover position and direction should be reversed."""
264-
# The default is True
265-
# Having motor_reverse_mode == "back" cancels the inversion
266-
return not (
267-
self._motor_reverse_mode_enum
268-
and self.device.status.get(self._motor_reverse_mode_enum.dpcode) == "back"
269-
)
270291

271292
@property
272293
def current_cover_position(self) -> int | None:
273294
"""Return cover current position."""
274-
if self._current_position is None:
275-
return None
276-
277-
if (position := self.device.status.get(self._current_position.dpcode)) is None:
278-
return None
279-
280-
return round(
281-
self._current_position.remap_value_to(
282-
position, 0, 100, reverse=self._is_position_reversed
283-
)
284-
)
295+
return self._read_wrapper(self._current_position)
285296

286297
@property
287298
def current_cover_tilt_position(self) -> int | None:
288299
"""Return current position of cover tilt.
289300
290301
None is unknown, 0 is closed, 100 is fully open.
291302
"""
292-
if self._tilt is None:
293-
return None
294-
295-
if (angle := self.device.status.get(self._tilt.dpcode)) is None:
296-
return None
297-
298-
return round(self._tilt.remap_value_to(angle, 0, 100))
303+
return self._read_wrapper(self._tilt_position)
299304

300305
@property
301306
def is_closed(self) -> bool | None:
@@ -332,16 +337,7 @@ def open_cover(self, **kwargs: Any) -> None:
332337
]
333338

334339
if self._set_position is not None:
335-
commands.append(
336-
{
337-
"code": self._set_position.dpcode,
338-
"value": round(
339-
self._set_position.remap_value_from(
340-
100, 0, 100, reverse=self._is_position_reversed
341-
),
342-
),
343-
}
344-
)
340+
commands.append(self._set_position.get_update_command(self.device, 100))
345341

346342
self._send_command(commands)
347343

@@ -361,40 +357,13 @@ def close_cover(self, **kwargs: Any) -> None:
361357
]
362358

363359
if self._set_position is not None:
364-
commands.append(
365-
{
366-
"code": self._set_position.dpcode,
367-
"value": round(
368-
self._set_position.remap_value_from(
369-
0, 0, 100, reverse=self._is_position_reversed
370-
),
371-
),
372-
}
373-
)
360+
commands.append(self._set_position.get_update_command(self.device, 0))
374361

375362
self._send_command(commands)
376363

377-
def set_cover_position(self, **kwargs: Any) -> None:
364+
async def async_set_cover_position(self, **kwargs: Any) -> None:
378365
"""Move the cover to a specific position."""
379-
if TYPE_CHECKING:
380-
# guarded by CoverEntityFeature.SET_POSITION
381-
assert self._set_position is not None
382-
383-
self._send_command(
384-
[
385-
{
386-
"code": self._set_position.dpcode,
387-
"value": round(
388-
self._set_position.remap_value_from(
389-
kwargs[ATTR_POSITION],
390-
0,
391-
100,
392-
reverse=self._is_position_reversed,
393-
)
394-
),
395-
}
396-
]
397-
)
366+
await self._async_send_dpcode_update(self._set_position, kwargs[ATTR_POSITION])
398367

399368
def stop_cover(self, **kwargs: Any) -> None:
400369
"""Stop the cover."""
@@ -407,24 +376,8 @@ def stop_cover(self, **kwargs: Any) -> None:
407376
]
408377
)
409378

410-
def set_cover_tilt_position(self, **kwargs: Any) -> None:
379+
async def async_set_cover_tilt_position(self, **kwargs: Any) -> None:
411380
"""Move the cover tilt to a specific position."""
412-
if TYPE_CHECKING:
413-
# guarded by CoverEntityFeature.SET_TILT_POSITION
414-
assert self._tilt is not None
415-
416-
self._send_command(
417-
[
418-
{
419-
"code": self._tilt.dpcode,
420-
"value": round(
421-
self._tilt.remap_value_from(
422-
kwargs[ATTR_TILT_POSITION],
423-
0,
424-
100,
425-
reverse=self._is_position_reversed,
426-
)
427-
),
428-
}
429-
]
381+
await self._async_send_dpcode_update(
382+
self._tilt_position, kwargs[ATTR_TILT_POSITION]
430383
)

0 commit comments

Comments
 (0)