2020from homeassistant .helpers .entity_platform import AddConfigEntryEntitiesCallback
2121
2222from . import TuyaConfigEntry
23- from .const import TUYA_DISCOVERY_NEW , DeviceCategory , DPCode , DPType
23+ from .const import TUYA_DISCOVERY_NEW , DeviceCategory , DPCode
2424from .entity import TuyaEntity
25- from .models import DPCodeIntegerWrapper , find_dpcode
25+ from .models import DPCodeBooleanWrapper , DPCodeEnumWrapper , DPCodeIntegerWrapper
2626from .util import get_dpcode
2727
2828
@@ -73,20 +73,72 @@ def _position_reversed(self, device: CustomerDevice) -> bool:
7373 return device .status .get (DPCode .CONTROL_BACK_MODE ) != "back"
7474
7575
76+ class _InstructionWrapper :
77+ """Default wrapper for sending open/close/stop instructions."""
78+
79+ def get_open_command (self , device : CustomerDevice ) -> dict [str , Any ] | None :
80+ return None
81+
82+ def get_close_command (self , device : CustomerDevice ) -> dict [str , Any ] | None :
83+ return None
84+
85+ def get_stop_command (self , device : CustomerDevice ) -> dict [str , Any ] | None :
86+ return None
87+
88+
89+ class _InstructionBooleanWrapper (DPCodeBooleanWrapper , _InstructionWrapper ):
90+ """Wrapper for boolean-based open/close instructions."""
91+
92+ def get_open_command (self , device : CustomerDevice ) -> dict [str , Any ] | None :
93+ return {"code" : self .dpcode , "value" : True }
94+
95+ def get_close_command (self , device : CustomerDevice ) -> dict [str , Any ] | None :
96+ return {"code" : self .dpcode , "value" : False }
97+
98+
99+ class _InstructionEnumWrapper (DPCodeEnumWrapper , _InstructionWrapper ):
100+ """Wrapper for enum-based open/close/stop instructions."""
101+
102+ open_instruction = "open"
103+ close_instruction = "close"
104+ stop_instruction = "stop"
105+
106+ def get_open_command (self , device : CustomerDevice ) -> dict [str , Any ] | None :
107+ if self .open_instruction in self .type_information .range :
108+ return {"code" : self .dpcode , "value" : self .open_instruction }
109+ return None
110+
111+ def get_close_command (self , device : CustomerDevice ) -> dict [str , Any ] | None :
112+ if self .close_instruction in self .type_information .range :
113+ return {"code" : self .dpcode , "value" : self .close_instruction }
114+ return None
115+
116+ def get_stop_command (self , device : CustomerDevice ) -> dict [str , Any ] | None :
117+ if self .stop_instruction in self .type_information .range :
118+ return {"code" : self .dpcode , "value" : self .stop_instruction }
119+ return None
120+
121+
122+ class _SpecialInstructionEnumWrapper (_InstructionEnumWrapper ):
123+ """Wrapper for enum-based instructions with special values (FZ/ZZ/STOP)."""
124+
125+ open_instruction = "FZ"
126+ close_instruction = "ZZ"
127+ stop_instruction = "STOP"
128+
129+
76130@dataclass (frozen = True )
77131class TuyaCoverEntityDescription (CoverEntityDescription ):
78132 """Describe an Tuya cover entity."""
79133
80134 current_state : DPCode | tuple [DPCode , ...] | None = None
81135 current_state_inverse : bool = False
82136 current_position : DPCode | tuple [DPCode , ...] | None = None
137+ instruction_wrapper : type [_InstructionEnumWrapper ] = _InstructionEnumWrapper
83138 position_wrapper : type [_DPCodePercentageMappingWrapper ] = (
84139 _InvertedPercentageMappingWrapper
85140 )
86141 set_position : DPCode | None = None
87- open_instruction_value : str = "open"
88- close_instruction_value : str = "close"
89- stop_instruction_value : str = "stop"
90142
91143
92144COVERS : dict [DeviceCategory , tuple [TuyaCoverEntityDescription , ...]] = {
@@ -147,9 +199,7 @@ class TuyaCoverEntityDescription(CoverEntityDescription):
147199 current_position = DPCode .POSITION ,
148200 set_position = DPCode .POSITION ,
149201 device_class = CoverDeviceClass .CURTAIN ,
150- open_instruction_value = "FZ" ,
151- close_instruction_value = "ZZ" ,
152- stop_instruction_value = "STOP" ,
202+ instruction_wrapper = _SpecialInstructionEnumWrapper ,
153203 ),
154204 # switch_1 is an undocumented code that behaves identically to control
155205 # It is used by the Kogan Smart Blinds Driver
@@ -192,6 +242,21 @@ class TuyaCoverEntityDescription(CoverEntityDescription):
192242}
193243
194244
245+ def _get_instruction_wrapper (
246+ device : CustomerDevice , description : TuyaCoverEntityDescription
247+ ) -> _InstructionWrapper | None :
248+ """Get the instruction wrapper for the cover entity."""
249+ if enum_wrapper := description .instruction_wrapper .find_dpcode (
250+ device , description .key , prefer_function = True
251+ ):
252+ return enum_wrapper
253+
254+ # Fallback to a boolean wrapper if available
255+ return _InstructionBooleanWrapper .find_dpcode (
256+ device , description .key , prefer_function = True
257+ )
258+
259+
195260async def async_setup_entry (
196261 hass : HomeAssistant ,
197262 entry : TuyaConfigEntry ,
@@ -215,6 +280,9 @@ def async_discover_device(device_ids: list[str]) -> None:
215280 current_position = description .position_wrapper .find_dpcode (
216281 device , description .current_position
217282 ),
283+ instruction_wrapper = _get_instruction_wrapper (
284+ device , description
285+ ),
218286 set_position = description .position_wrapper .find_dpcode (
219287 device , description .set_position , prefer_function = True
220288 ),
@@ -253,6 +321,7 @@ def __init__(
253321 description : TuyaCoverEntityDescription ,
254322 * ,
255323 current_position : _DPCodePercentageMappingWrapper | None = None ,
324+ instruction_wrapper : _InstructionWrapper | None = None ,
256325 set_position : _DPCodePercentageMappingWrapper | None = None ,
257326 tilt_position : _DPCodePercentageMappingWrapper | None = None ,
258327 ) -> None :
@@ -263,24 +332,17 @@ def __init__(
263332 self ._attr_supported_features = CoverEntityFeature (0 )
264333
265334 self ._current_position = current_position or set_position
335+ self ._instruction_wrapper = instruction_wrapper
266336 self ._set_position = set_position
267337 self ._tilt_position = tilt_position
268338
269- # Check if this cover is based on a switch or has controls
270- if get_dpcode (self .device , description .key ):
271- if device .function [description .key ].type == "Boolean" :
272- self ._attr_supported_features |= (
273- CoverEntityFeature .OPEN | CoverEntityFeature .CLOSE
274- )
275- elif enum_type := find_dpcode (
276- self .device , description .key , dptype = DPType .ENUM , prefer_function = True
277- ):
278- if description .open_instruction_value in enum_type .range :
279- self ._attr_supported_features |= CoverEntityFeature .OPEN
280- if description .close_instruction_value in enum_type .range :
281- self ._attr_supported_features |= CoverEntityFeature .CLOSE
282- if description .stop_instruction_value in enum_type .range :
283- self ._attr_supported_features |= CoverEntityFeature .STOP
339+ if instruction_wrapper :
340+ if instruction_wrapper .get_open_command (device ) is not None :
341+ self ._attr_supported_features |= CoverEntityFeature .OPEN
342+ if instruction_wrapper .get_close_command (device ) is not None :
343+ self ._attr_supported_features |= CoverEntityFeature .CLOSE
344+ if instruction_wrapper .get_stop_command (device ) is not None :
345+ self ._attr_supported_features |= CoverEntityFeature .STOP
284346
285347 self ._current_state = get_dpcode (self .device , description .current_state )
286348
@@ -321,60 +383,42 @@ def is_closed(self) -> bool | None:
321383
322384 return None
323385
324- def open_cover (self , ** kwargs : Any ) -> None :
386+ async def async_open_cover (self , ** kwargs : Any ) -> None :
325387 """Open the cover."""
326- value : bool | str = True
327- if find_dpcode (
328- self .device ,
329- self .entity_description .key ,
330- dptype = DPType .ENUM ,
331- prefer_function = True ,
388+ if self ._instruction_wrapper and (
389+ command := self ._instruction_wrapper .get_open_command (self .device )
332390 ):
333- value = self .entity_description .open_instruction_value
334-
335- commands : list [dict [str , str | int ]] = [
336- {"code" : self .entity_description .key , "value" : value }
337- ]
391+ await self ._async_send_commands ([command ])
392+ return
338393
339394 if self ._set_position is not None :
340- commands . append ( self ._set_position . get_update_command ( self . device , 100 ))
341-
342- self . _send_command ( commands )
395+ await self ._async_send_commands (
396+ [ self . _set_position . get_update_command ( self . device , 100 )]
397+ )
343398
344- def close_cover (self , ** kwargs : Any ) -> None :
399+ async def async_close_cover (self , ** kwargs : Any ) -> None :
345400 """Close cover."""
346- value : bool | str = False
347- if find_dpcode (
348- self .device ,
349- self .entity_description .key ,
350- dptype = DPType .ENUM ,
351- prefer_function = True ,
401+ if self ._instruction_wrapper and (
402+ command := self ._instruction_wrapper .get_close_command (self .device )
352403 ):
353- value = self .entity_description .close_instruction_value
354-
355- commands : list [dict [str , str | int ]] = [
356- {"code" : self .entity_description .key , "value" : value }
357- ]
404+ await self ._async_send_commands ([command ])
405+ return
358406
359407 if self ._set_position is not None :
360- commands . append ( self ._set_position . get_update_command ( self . device , 0 ))
361-
362- self . _send_command ( commands )
408+ await self ._async_send_commands (
409+ [ self . _set_position . get_update_command ( self . device , 0 )]
410+ )
363411
364412 async def async_set_cover_position (self , ** kwargs : Any ) -> None :
365413 """Move the cover to a specific position."""
366414 await self ._async_send_dpcode_update (self ._set_position , kwargs [ATTR_POSITION ])
367415
368- def stop_cover (self , ** kwargs : Any ) -> None :
416+ async def async_stop_cover (self , ** kwargs : Any ) -> None :
369417 """Stop the cover."""
370- self ._send_command (
371- [
372- {
373- "code" : self .entity_description .key ,
374- "value" : self .entity_description .stop_instruction_value ,
375- }
376- ]
377- )
418+ if self ._instruction_wrapper and (
419+ command := self ._instruction_wrapper .get_stop_command (self .device )
420+ ):
421+ await self ._async_send_commands ([command ])
378422
379423 async def async_set_cover_tilt_position (self , ** kwargs : Any ) -> None :
380424 """Move the cover tilt to a specific position."""
0 commit comments