2626from .models import (
2727 DPCodeBooleanWrapper ,
2828 DPCodeEnumWrapper ,
29+ DPCodeIntegerWrapper ,
2930 EnumTypeData ,
30- IntegerTypeData ,
3131 find_dpcode ,
3232)
3333from .util import get_dpcode
@@ -79,6 +79,55 @@ def _has_a_valid_dpcode(device: CustomerDevice) -> bool:
7979 return any (get_dpcode (device , code ) for code in properties_to_check )
8080
8181
82+ class _FanSpeedEnumWrapper (DPCodeEnumWrapper ):
83+ """Wrapper for fan speed DP code (from an enum)."""
84+
85+ def get_speed_count (self ) -> int :
86+ """Get the number of speeds supported by the fan."""
87+ return len (self .type_information .range )
88+
89+ def read_device_status (self , device : CustomerDevice ) -> int | None : # type: ignore[override]
90+ """Get the current speed as a percentage."""
91+ if (value := super ().read_device_status (device )) is None :
92+ return None
93+ return ordered_list_item_to_percentage (self .type_information .range , value )
94+
95+ def _convert_value_to_raw_value (self , device : CustomerDevice , value : Any ) -> Any :
96+ """Convert a Home Assistant value back to a raw device value."""
97+ return percentage_to_ordered_list_item (self .type_information .range , value )
98+
99+
100+ class _FanSpeedIntegerWrapper (DPCodeIntegerWrapper ):
101+ """Wrapper for fan speed DP code (from an integer)."""
102+
103+ def get_speed_count (self ) -> int :
104+ """Get the number of speeds supported by the fan."""
105+ return 100
106+
107+ def read_device_status (self , device : CustomerDevice ) -> int | None :
108+ """Get the current speed as a percentage."""
109+ if (value := super ().read_device_status (device )) is None :
110+ return None
111+ return round (self .type_information .remap_value_to (value , 1 , 100 ))
112+
113+ def _convert_value_to_raw_value (self , device : CustomerDevice , value : Any ) -> Any :
114+ """Convert a Home Assistant value back to a raw device value."""
115+ return round (self .type_information .remap_value_from (value , 1 , 100 ))
116+
117+
118+ def _get_speed_wrapper (
119+ device : CustomerDevice ,
120+ ) -> _FanSpeedEnumWrapper | _FanSpeedIntegerWrapper | None :
121+ """Get the speed wrapper for the device."""
122+ if int_wrapper := _FanSpeedIntegerWrapper .find_dpcode (
123+ device , _SPEED_DPCODES , prefer_function = True
124+ ):
125+ return int_wrapper
126+ return _FanSpeedEnumWrapper .find_dpcode (
127+ device , _SPEED_DPCODES , prefer_function = True
128+ )
129+
130+
82131async def async_setup_entry (
83132 hass : HomeAssistant ,
84133 entry : TuyaConfigEntry ,
@@ -104,6 +153,7 @@ def async_discover_device(device_ids: list[str]) -> None:
104153 mode_wrapper = DPCodeEnumWrapper .find_dpcode (
105154 device , _MODE_DPCODES , prefer_function = True
106155 ),
156+ speed_wrapper = _get_speed_wrapper (device ),
107157 switch_wrapper = DPCodeBooleanWrapper .find_dpcode (
108158 device , _SWITCH_DPCODES , prefer_function = True
109159 ),
@@ -122,8 +172,6 @@ class TuyaFanEntity(TuyaEntity, FanEntity):
122172 """Tuya Fan Device."""
123173
124174 _oscillate : DPCode | None = None
125- _speed : IntegerTypeData | None = None
126- _speeds : EnumTypeData | None = None
127175 _attr_name = None
128176
129177 def __init__ (
@@ -133,29 +181,23 @@ def __init__(
133181 * ,
134182 direction_wrapper : _DirectionEnumWrapper | None ,
135183 mode_wrapper : DPCodeEnumWrapper | None ,
184+ speed_wrapper : _FanSpeedEnumWrapper | _FanSpeedIntegerWrapper | None ,
136185 switch_wrapper : DPCodeBooleanWrapper | None ,
137186 ) -> None :
138187 """Init Tuya Fan Device."""
139188 super ().__init__ (device , device_manager )
140189 self ._direction_wrapper = direction_wrapper
141190 self ._mode_wrapper = mode_wrapper
191+ self ._speed_wrapper = speed_wrapper
142192 self ._switch_wrapper = switch_wrapper
143193
144194 if mode_wrapper :
145195 self ._attr_supported_features |= FanEntityFeature .PRESET_MODE
146196 self ._attr_preset_modes = mode_wrapper .type_information .range
147197
148- # Find speed controls, can be either percentage or a set of speeds
149- if int_type := find_dpcode (
150- self .device , _SPEED_DPCODES , dptype = DPType .INTEGER , prefer_function = True
151- ):
152- self ._attr_supported_features |= FanEntityFeature .SET_SPEED
153- self ._speed = int_type
154- elif enum_type := find_dpcode (
155- self .device , _SPEED_DPCODES , dptype = DPType .ENUM , prefer_function = True
156- ):
198+ if speed_wrapper :
157199 self ._attr_supported_features |= FanEntityFeature .SET_SPEED
158- self ._speeds = enum_type
200+ self ._attr_speed_count = speed_wrapper . get_speed_count ()
159201
160202 if dpcode := get_dpcode (self .device , _OSCILLATE_DPCODES ):
161203 self ._oscillate = dpcode
@@ -176,30 +218,9 @@ async def async_set_direction(self, direction: str) -> None:
176218 """Set the direction of the fan."""
177219 await self ._async_send_dpcode_update (self ._direction_wrapper , direction )
178220
179- def set_percentage (self , percentage : int ) -> None :
221+ async def async_set_percentage (self , percentage : int ) -> None :
180222 """Set the speed of the fan, as a percentage."""
181- if self ._speed is not None :
182- self ._send_command (
183- [
184- {
185- "code" : self ._speed .dpcode ,
186- "value" : int (self ._speed .remap_value_from (percentage , 1 , 100 )),
187- }
188- ]
189- )
190- return
191-
192- if self ._speeds is not None :
193- self ._send_command (
194- [
195- {
196- "code" : self ._speeds .dpcode ,
197- "value" : percentage_to_ordered_list_item (
198- self ._speeds .range , percentage
199- ),
200- }
201- ]
202- )
223+ await self ._async_send_dpcode_update (self ._speed_wrapper , percentage )
203224
204225 async def async_turn_off (self , ** kwargs : Any ) -> None :
205226 """Turn the fan off."""
@@ -219,22 +240,9 @@ async def async_turn_on(
219240 self ._switch_wrapper .get_update_command (self .device , True )
220241 ]
221242
222- if percentage is not None and self ._speed is not None :
243+ if percentage is not None and self ._speed_wrapper is not None :
223244 commands .append (
224- {
225- "code" : self ._speed .dpcode ,
226- "value" : int (self ._speed .remap_value_from (percentage , 1 , 100 )),
227- }
228- )
229-
230- if percentage is not None and self ._speeds is not None :
231- commands .append (
232- {
233- "code" : self ._speeds .dpcode ,
234- "value" : percentage_to_ordered_list_item (
235- self ._speeds .range , percentage
236- ),
237- }
245+ self ._speed_wrapper .get_update_command (self .device , percentage )
238246 )
239247
240248 if preset_mode is not None and self ._mode_wrapper :
@@ -274,23 +282,4 @@ def preset_mode(self) -> str | None:
274282 @property
275283 def percentage (self ) -> int | None :
276284 """Return the current speed."""
277- if self ._speed is not None :
278- if (value := self .device .status .get (self ._speed .dpcode )) is None :
279- return None
280- return int (self ._speed .remap_value_to (value , 1 , 100 ))
281-
282- if self ._speeds is not None :
283- if (
284- value := self .device .status .get (self ._speeds .dpcode )
285- ) is None or value not in self ._speeds .range :
286- return None
287- return ordered_list_item_to_percentage (self ._speeds .range , value )
288-
289- return None
290-
291- @property
292- def speed_count (self ) -> int :
293- """Return the number of speeds the fan supports."""
294- if self ._speeds is not None :
295- return len (self ._speeds .range )
296- return 100
285+ return self ._read_wrapper (self ._speed_wrapper )
0 commit comments