Skip to content

Commit e5ae58c

Browse files
authored
Migrate Tuya cover (open/close/stop) to use wrapper class (home-assistant#156726)
1 parent 13e4bb4 commit e5ae58c

File tree

2 files changed

+106
-66
lines changed

2 files changed

+106
-66
lines changed

homeassistant/components/tuya/cover.py

Lines changed: 106 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@
2020
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
2121

2222
from . import TuyaConfigEntry
23-
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode, DPType
23+
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
2424
from .entity import TuyaEntity
25-
from .models import DPCodeIntegerWrapper, find_dpcode
25+
from .models import DPCodeBooleanWrapper, DPCodeEnumWrapper, DPCodeIntegerWrapper
2626
from .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)
77131
class 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

92144
COVERS: 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+
195260
async 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."""

tests/components/tuya/test_cover.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,13 @@ async def test_platform_setup_and_discovery(
6262
{},
6363
[
6464
{"code": "control", "value": "open"},
65-
{"code": "percent_control", "value": 0},
6665
],
6766
),
6867
(
6968
SERVICE_CLOSE_COVER,
7069
{},
7170
[
7271
{"code": "control", "value": "close"},
73-
{"code": "percent_control", "value": 100},
7472
],
7573
),
7674
(
@@ -229,7 +227,6 @@ async def test_clkg_wltqkykhni0papzj_state(
229227
{},
230228
[
231229
{"code": "control", "value": "open"},
232-
{"code": "percent_control", "value": 100},
233230
],
234231
),
235232
(
@@ -244,7 +241,6 @@ async def test_clkg_wltqkykhni0papzj_state(
244241
{},
245242
[
246243
{"code": "control", "value": "close"},
247-
{"code": "percent_control", "value": 0},
248244
],
249245
),
250246
],

0 commit comments

Comments
 (0)