33from __future__ import annotations
44
55from dataclasses import dataclass
6- from typing import Any
6+ from typing import Any , Self
77
88from tuya_sharing import CustomerDevice , Manager
99
3232 DPCode ,
3333)
3434from .entity import TuyaEntity
35- from .models import DPCodeBooleanWrapper , DPCodeEnumWrapper , DPCodeIntegerWrapper
35+ from .models import (
36+ DeviceWrapper ,
37+ DPCodeBooleanWrapper ,
38+ DPCodeEnumWrapper ,
39+ DPCodeIntegerWrapper ,
40+ )
3641
3742TUYA_HVAC_TO_HA = {
3843 "auto" : HVACMode .HEAT_COOL ,
@@ -56,6 +61,84 @@ def read_device_status(self, device: CustomerDevice) -> int | None:
5661 return round (value )
5762
5863
64+ @dataclass (kw_only = True )
65+ class _SwingModeWrapper (DeviceWrapper ):
66+ """Wrapper for managing climate swing mode operations across multiple DPCodes."""
67+
68+ on_off : DPCodeBooleanWrapper | None = None
69+ horizontal : DPCodeBooleanWrapper | None = None
70+ vertical : DPCodeBooleanWrapper | None = None
71+ modes : list [str ]
72+
73+ @classmethod
74+ def find_dpcode (cls , device : CustomerDevice ) -> Self | None :
75+ """Find and return a _SwingModeWrapper for the given DP codes."""
76+ on_off = DPCodeBooleanWrapper .find_dpcode (
77+ device , (DPCode .SWING , DPCode .SHAKE ), prefer_function = True
78+ )
79+ horizontal = DPCodeBooleanWrapper .find_dpcode (
80+ device , DPCode .SWITCH_HORIZONTAL , prefer_function = True
81+ )
82+ vertical = DPCodeBooleanWrapper .find_dpcode (
83+ device , DPCode .SWITCH_VERTICAL , prefer_function = True
84+ )
85+ if on_off or horizontal or vertical :
86+ modes = [SWING_OFF ]
87+ if on_off :
88+ modes .append (SWING_ON )
89+ if horizontal :
90+ modes .append (SWING_HORIZONTAL )
91+ if vertical :
92+ modes .append (SWING_VERTICAL )
93+ return cls (
94+ on_off = on_off ,
95+ horizontal = horizontal ,
96+ vertical = vertical ,
97+ modes = modes ,
98+ )
99+ return None
100+
101+ def read_device_status (self , device : CustomerDevice ) -> str | None :
102+ """Read the device swing mode."""
103+ if self .on_off and self .on_off .read_device_status (device ):
104+ return SWING_ON
105+
106+ horizontal = (
107+ self .horizontal .read_device_status (device ) if self .horizontal else None
108+ )
109+ vertical = self .vertical .read_device_status (device ) if self .vertical else None
110+ if horizontal and vertical :
111+ return SWING_BOTH
112+ if horizontal :
113+ return SWING_HORIZONTAL
114+ if vertical :
115+ return SWING_VERTICAL
116+
117+ return SWING_OFF
118+
119+ def get_update_commands (
120+ self , device : CustomerDevice , value : str
121+ ) -> list [dict [str , Any ]]:
122+ """Set new target swing operation."""
123+ commands = []
124+ if self .on_off :
125+ commands .extend (self .on_off .get_update_commands (device , value == SWING_ON ))
126+
127+ if self .vertical :
128+ commands .extend (
129+ self .vertical .get_update_commands (
130+ device , value in (SWING_BOTH , SWING_VERTICAL )
131+ )
132+ )
133+ if self .horizontal :
134+ commands .extend (
135+ self .horizontal .get_update_commands (
136+ device , value in (SWING_BOTH , SWING_HORIZONTAL )
137+ )
138+ )
139+ return commands
140+
141+
59142@dataclass (frozen = True , kw_only = True )
60143class TuyaClimateEntityDescription (ClimateEntityDescription ):
61144 """Describe an Tuya climate entity."""
@@ -205,15 +288,7 @@ def async_discover_device(device_ids: list[str]) -> None:
205288 device , DPCode .MODE , prefer_function = True
206289 ),
207290 set_temperature_wrapper = temperature_wrappers [1 ],
208- swing_wrapper = DPCodeBooleanWrapper .find_dpcode (
209- device , (DPCode .SWING , DPCode .SHAKE ), prefer_function = True
210- ),
211- swing_h_wrapper = DPCodeBooleanWrapper .find_dpcode (
212- device , DPCode .SWITCH_HORIZONTAL , prefer_function = True
213- ),
214- swing_v_wrapper = DPCodeBooleanWrapper .find_dpcode (
215- device , DPCode .SWITCH_VERTICAL , prefer_function = True
216- ),
291+ swing_wrapper = _SwingModeWrapper .find_dpcode (device ),
217292 switch_wrapper = DPCodeBooleanWrapper .find_dpcode (
218293 device , DPCode .SWITCH , prefer_function = True
219294 ),
@@ -250,9 +325,7 @@ def __init__(
250325 fan_mode_wrapper : DPCodeEnumWrapper | None ,
251326 hvac_mode_wrapper : DPCodeEnumWrapper | None ,
252327 set_temperature_wrapper : DPCodeIntegerWrapper | None ,
253- swing_wrapper : DPCodeBooleanWrapper | None ,
254- swing_h_wrapper : DPCodeBooleanWrapper | None ,
255- swing_v_wrapper : DPCodeBooleanWrapper | None ,
328+ swing_wrapper : _SwingModeWrapper | None ,
256329 switch_wrapper : DPCodeBooleanWrapper | None ,
257330 target_humidity_wrapper : _RoundedIntegerWrapper | None ,
258331 temperature_unit : UnitOfTemperature ,
@@ -268,8 +341,6 @@ def __init__(
268341 self ._hvac_mode_wrapper = hvac_mode_wrapper
269342 self ._set_temperature = set_temperature_wrapper
270343 self ._swing_wrapper = swing_wrapper
271- self ._swing_h_wrapper = swing_h_wrapper
272- self ._swing_v_wrapper = swing_v_wrapper
273344 self ._switch_wrapper = switch_wrapper
274345 self ._target_humidity_wrapper = target_humidity_wrapper
275346 self ._attr_temperature_unit = temperature_unit
@@ -324,17 +395,9 @@ def __init__(
324395 self ._attr_fan_modes = fan_mode_wrapper .type_information .range
325396
326397 # Determine swing modes
327- if swing_wrapper or swing_h_wrapper or swing_v_wrapper :
398+ if swing_wrapper :
328399 self ._attr_supported_features |= ClimateEntityFeature .SWING_MODE
329- self ._attr_swing_modes = [SWING_OFF ]
330- if swing_wrapper :
331- self ._attr_swing_modes .append (SWING_ON )
332-
333- if swing_h_wrapper :
334- self ._attr_swing_modes .append (SWING_HORIZONTAL )
335-
336- if swing_v_wrapper :
337- self ._attr_swing_modes .append (SWING_VERTICAL )
400+ self ._attr_swing_modes = swing_wrapper .modes
338401
339402 if switch_wrapper :
340403 self ._attr_supported_features |= (
@@ -372,27 +435,7 @@ async def async_set_humidity(self, humidity: int) -> None:
372435
373436 async def async_set_swing_mode (self , swing_mode : str ) -> None :
374437 """Set new target swing operation."""
375- commands = []
376- if self ._swing_wrapper :
377- commands .extend (
378- self ._swing_wrapper .get_update_commands (
379- self .device , swing_mode == SWING_ON
380- )
381- )
382- if self ._swing_v_wrapper :
383- commands .extend (
384- self ._swing_v_wrapper .get_update_commands (
385- self .device , swing_mode in (SWING_BOTH , SWING_VERTICAL )
386- )
387- )
388- if self ._swing_h_wrapper :
389- commands .extend (
390- self ._swing_h_wrapper .get_update_commands (
391- self .device , swing_mode in (SWING_BOTH , SWING_HORIZONTAL )
392- )
393- )
394- if commands :
395- await self ._async_send_commands (commands )
438+ await self ._async_send_wrapper_updates (self ._swing_wrapper , swing_mode )
396439
397440 async def async_set_temperature (self , ** kwargs : Any ) -> None :
398441 """Set new target temperature."""
@@ -457,21 +500,9 @@ def fan_mode(self) -> str | None:
457500 return self ._read_wrapper (self ._fan_mode_wrapper )
458501
459502 @property
460- def swing_mode (self ) -> str :
503+ def swing_mode (self ) -> str | None :
461504 """Return swing mode."""
462- if self ._read_wrapper (self ._swing_wrapper ):
463- return SWING_ON
464-
465- horizontal = self ._read_wrapper (self ._swing_h_wrapper )
466- vertical = self ._read_wrapper (self ._swing_v_wrapper )
467- if horizontal and vertical :
468- return SWING_BOTH
469- if horizontal :
470- return SWING_HORIZONTAL
471- if vertical :
472- return SWING_VERTICAL
473-
474- return SWING_OFF
505+ return self ._read_wrapper (self ._swing_wrapper )
475506
476507 async def async_turn_on (self ) -> None :
477508 """Turn the device on, retaining current HVAC (if supported)."""
0 commit comments