Skip to content

Commit 55c5fb7

Browse files
authored
Migrate Tuya climate (swing) to use wrapper class (home-assistant#157646)
1 parent 5d78cd3 commit 55c5fb7

File tree

3 files changed

+113
-73
lines changed

3 files changed

+113
-73
lines changed

homeassistant/components/tuya/climate.py

Lines changed: 92 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
from dataclasses import dataclass
6-
from typing import Any
6+
from typing import Any, Self
77

88
from tuya_sharing import CustomerDevice, Manager
99

@@ -32,7 +32,12 @@
3232
DPCode,
3333
)
3434
from .entity import TuyaEntity
35-
from .models import DPCodeBooleanWrapper, DPCodeEnumWrapper, DPCodeIntegerWrapper
35+
from .models import (
36+
DeviceWrapper,
37+
DPCodeBooleanWrapper,
38+
DPCodeEnumWrapper,
39+
DPCodeIntegerWrapper,
40+
)
3641

3742
TUYA_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)
60143
class 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)."""

homeassistant/components/tuya/entity.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from homeassistant.helpers.entity import Entity
1212

1313
from .const import DOMAIN, LOGGER, TUYA_HA_SIGNAL_UPDATE_ENTITY
14-
from .models import DPCodeWrapper
14+
from .models import DeviceWrapper
1515

1616

1717
class TuyaEntity(Entity):
@@ -64,18 +64,20 @@ async def _handle_state_update(
6464
async def _async_send_commands(self, commands: list[dict[str, Any]]) -> None:
6565
"""Send a list of commands to the device."""
6666
LOGGER.debug("Sending commands for device %s: %s", self.device.id, commands)
67+
if not commands:
68+
return
6769
await self.hass.async_add_executor_job(
6870
self.device_manager.send_commands, self.device.id, commands
6971
)
7072

71-
def _read_wrapper(self, wrapper: DPCodeWrapper | None) -> Any | None:
73+
def _read_wrapper(self, wrapper: DeviceWrapper | None) -> Any | None:
7274
"""Read the wrapper device status."""
7375
if wrapper is None:
7476
return None
7577
return wrapper.read_device_status(self.device)
7678

7779
async def _async_send_wrapper_updates(
78-
self, wrapper: DPCodeWrapper | None, value: Any
80+
self, wrapper: DeviceWrapper | None, value: Any
7981
) -> None:
8082
"""Send command to the device."""
8183
if wrapper is None:

homeassistant/components/tuya/models.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,22 @@ def from_json(cls, dpcode: str, type_data: str) -> Self | None:
165165
}
166166

167167

168-
class DPCodeWrapper:
169-
"""Base DPCode wrapper.
168+
class DeviceWrapper:
169+
"""Base device wrapper."""
170+
171+
def read_device_status(self, device: CustomerDevice) -> Any | None:
172+
"""Read device status and convert to a Home Assistant value."""
173+
raise NotImplementedError
174+
175+
def get_update_commands(
176+
self, device: CustomerDevice, value: Any
177+
) -> list[dict[str, Any]]:
178+
"""Generate update commands for a Home Assistant action."""
179+
raise NotImplementedError
180+
181+
182+
class DPCodeWrapper(DeviceWrapper):
183+
"""Base device wrapper for a single DPCode.
170184
171185
Used as a common interface for referring to a DPCode, and
172186
access read conversion routines.
@@ -186,13 +200,6 @@ def _read_device_status_raw(self, device: CustomerDevice) -> Any | None:
186200
"""
187201
return device.status.get(self.dpcode)
188202

189-
def read_device_status(self, device: CustomerDevice) -> Any | None:
190-
"""Read the device value for the dpcode.
191-
192-
The raw device status is converted to a Home Assistant value.
193-
"""
194-
raise NotImplementedError
195-
196203
def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any:
197204
"""Convert a Home Assistant value back to a raw device value.
198205

0 commit comments

Comments
 (0)