Skip to content

Commit bfdff46

Browse files
authored
Migrate Tuya fan (speed) to use wrapper class (home-assistant#156976)
1 parent 9a22808 commit bfdff46

File tree

2 files changed

+61
-72
lines changed

2 files changed

+61
-72
lines changed

homeassistant/components/tuya/fan.py

Lines changed: 60 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@
2626
from .models import (
2727
DPCodeBooleanWrapper,
2828
DPCodeEnumWrapper,
29+
DPCodeIntegerWrapper,
2930
EnumTypeData,
30-
IntegerTypeData,
3131
find_dpcode,
3232
)
3333
from .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+
82131
async 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)

tests/components/tuya/snapshots/test_fan.ambr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@
148148
'attributes': ReadOnlyDict({
149149
'direction': 'forward',
150150
'friendly_name': 'ceiling fan/Light v2',
151-
'percentage': 20,
151+
'percentage': 21,
152152
'percentage_step': 1.0,
153153
'preset_mode': None,
154154
'preset_modes': None,

0 commit comments

Comments
 (0)