33from __future__ import annotations
44
55from dataclasses import dataclass
6- from typing import TYPE_CHECKING , Any
6+ from typing import Any
77
88from tuya_sharing import CustomerDevice , Manager
99
2222from . import TuyaConfigEntry
2323from .const import TUYA_DISCOVERY_NEW , DeviceCategory , DPCode , DPType
2424from .entity import TuyaEntity
25- from .models import EnumTypeData , IntegerTypeData , find_dpcode
25+ from .models import DPCodeIntegerWrapper , find_dpcode
2626from .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 )
3077class 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
4392COVERS : 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:
179243class 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