Skip to content

Commit d52749c

Browse files
authored
Add wrapper class for boolean values in Tuya models (home-assistant#155905)
1 parent 5eb5b93 commit d52749c

File tree

5 files changed

+175
-13
lines changed

5 files changed

+175
-13
lines changed

homeassistant/components/tuya/models.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@ def read_device_status(self, device: CustomerDevice) -> Any | None:
3636
raise NotImplementedError("read_device_value must be implemented")
3737

3838

39+
@dataclass
40+
class DPCodeBooleanWrapper(DPCodeWrapper):
41+
"""Simple wrapper for boolean values.
42+
43+
Supports True/False only.
44+
"""
45+
46+
def read_device_status(self, device: CustomerDevice) -> bool | None:
47+
"""Read the device value for the dpcode."""
48+
if (raw_value := self._read_device_status_raw(device)) in (True, False):
49+
return raw_value
50+
return None
51+
52+
3953
@dataclass(kw_only=True)
4054
class DPCodeEnumWrapper(DPCodeWrapper):
4155
"""Simple wrapper for EnumTypeData values."""

homeassistant/components/tuya/switch.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
from . import TuyaConfigEntry
2828
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
2929
from .entity import TuyaEntity
30+
from .models import DPCodeBooleanWrapper
3031

3132

3233
@dataclass(frozen=True, kw_only=True)
@@ -938,7 +939,12 @@ def async_discover_device(device_ids: list[str]) -> None:
938939
device = manager.device_map[device_id]
939940
if descriptions := SWITCHES.get(device.category):
940941
entities.extend(
941-
TuyaSwitchEntity(device, manager, description)
942+
TuyaSwitchEntity(
943+
device,
944+
manager,
945+
description,
946+
DPCodeBooleanWrapper(description.key),
947+
)
942948
for description in descriptions
943949
if description.key in device.status
944950
and _check_deprecation(
@@ -1015,21 +1021,23 @@ def __init__(
10151021
device: CustomerDevice,
10161022
device_manager: Manager,
10171023
description: SwitchEntityDescription,
1024+
dpcode_wrapper: DPCodeBooleanWrapper,
10181025
) -> None:
10191026
"""Init TuyaHaSwitch."""
10201027
super().__init__(device, device_manager)
10211028
self.entity_description = description
10221029
self._attr_unique_id = f"{super().unique_id}{description.key}"
1030+
self._dpcode_wrapper = dpcode_wrapper
10231031

10241032
@property
1025-
def is_on(self) -> bool:
1033+
def is_on(self) -> bool | None:
10261034
"""Return true if switch is on."""
1027-
return self.device.status.get(self.entity_description.key, False)
1035+
return self._dpcode_wrapper.read_device_status(self.device)
10281036

10291037
def turn_on(self, **kwargs: Any) -> None:
10301038
"""Turn the switch on."""
1031-
self._send_command([{"code": self.entity_description.key, "value": True}])
1039+
self._send_command([{"code": self._dpcode_wrapper.dpcode, "value": True}])
10321040

10331041
def turn_off(self, **kwargs: Any) -> None:
10341042
"""Turn the switch off."""
1035-
self._send_command([{"code": self.entity_description.key, "value": False}])
1043+
self._send_command([{"code": self._dpcode_wrapper.dpcode, "value": False}])

homeassistant/components/tuya/valve.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from . import TuyaConfigEntry
1818
from .const import TUYA_DISCOVERY_NEW, DeviceCategory, DPCode
1919
from .entity import TuyaEntity
20+
from .models import DPCodeBooleanWrapper
2021

2122
VALVES: dict[DeviceCategory, tuple[ValveEntityDescription, ...]] = {
2223
DeviceCategory.SFKZQ: (
@@ -93,7 +94,12 @@ def async_discover_device(device_ids: list[str]) -> None:
9394
device = manager.device_map[device_id]
9495
if descriptions := VALVES.get(device.category):
9596
entities.extend(
96-
TuyaValveEntity(device, manager, description)
97+
TuyaValveEntity(
98+
device,
99+
manager,
100+
description,
101+
DPCodeBooleanWrapper(description.key),
102+
)
97103
for description in descriptions
98104
if description.key in device.status
99105
)
@@ -117,25 +123,29 @@ def __init__(
117123
device: CustomerDevice,
118124
device_manager: Manager,
119125
description: ValveEntityDescription,
126+
dpcode_wrapper: DPCodeBooleanWrapper,
120127
) -> None:
121128
"""Init TuyaValveEntity."""
122129
super().__init__(device, device_manager)
123130
self.entity_description = description
124131
self._attr_unique_id = f"{super().unique_id}{description.key}"
132+
self._dpcode_wrapper = dpcode_wrapper
125133

126134
@property
127-
def is_closed(self) -> bool:
135+
def is_closed(self) -> bool | None:
128136
"""Return if the valve is closed."""
129-
return not self.device.status.get(self.entity_description.key, False)
137+
if (is_open := self._dpcode_wrapper.read_device_status(self.device)) is None:
138+
return None
139+
return not is_open
130140

131141
async def async_open_valve(self) -> None:
132142
"""Open the valve."""
133143
await self.hass.async_add_executor_job(
134-
self._send_command, [{"code": self.entity_description.key, "value": True}]
144+
self._send_command, [{"code": self._dpcode_wrapper.dpcode, "value": True}]
135145
)
136146

137147
async def async_close_valve(self) -> None:
138148
"""Close the valve."""
139149
await self.hass.async_add_executor_job(
140-
self._send_command, [{"code": self.entity_description.key, "value": False}]
150+
self._send_command, [{"code": self._dpcode_wrapper.dpcode, "value": False}]
141151
)

tests/components/tuya/test_switch.py

Lines changed: 99 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,20 @@
22

33
from __future__ import annotations
44

5+
from typing import Any
56
from unittest.mock import patch
67

78
import pytest
89
from syrupy.assertion import SnapshotAssertion
910
from tuya_sharing import CustomerDevice, Manager
1011

11-
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
12+
from homeassistant.components.switch import (
13+
DOMAIN as SWITCH_DOMAIN,
14+
SERVICE_TURN_OFF,
15+
SERVICE_TURN_ON,
16+
)
1217
from homeassistant.components.tuya import DOMAIN
13-
from homeassistant.const import Platform
18+
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform
1419
from homeassistant.core import HomeAssistant
1520
from homeassistant.helpers import entity_registry as er, issue_registry as ir
1621

@@ -83,3 +88,95 @@ async def test_sfkzq_deprecated_switch(
8388
)
8489
is not None
8590
) is expected_issue
91+
92+
93+
@patch("homeassistant.components.tuya.PLATFORMS", [Platform.SWITCH])
94+
@pytest.mark.parametrize(
95+
"mock_device_code",
96+
["cz_PGEkBctAbtzKOZng"],
97+
)
98+
async def test_turn_on(
99+
hass: HomeAssistant,
100+
mock_manager: Manager,
101+
mock_config_entry: MockConfigEntry,
102+
mock_device: CustomerDevice,
103+
) -> None:
104+
"""Test turning on a switch."""
105+
entity_id = "switch.din_socket"
106+
await initialize_entry(hass, mock_manager, mock_config_entry, mock_device)
107+
108+
state = hass.states.get(entity_id)
109+
assert state is not None, f"{entity_id} does not exist"
110+
await hass.services.async_call(
111+
SWITCH_DOMAIN,
112+
SERVICE_TURN_ON,
113+
{
114+
ATTR_ENTITY_ID: entity_id,
115+
},
116+
blocking=True,
117+
)
118+
mock_manager.send_commands.assert_called_once_with(
119+
mock_device.id, [{"code": "switch", "value": True}]
120+
)
121+
122+
123+
@patch("homeassistant.components.tuya.PLATFORMS", [Platform.SWITCH])
124+
@pytest.mark.parametrize(
125+
"mock_device_code",
126+
["cz_PGEkBctAbtzKOZng"],
127+
)
128+
async def test_turn_off(
129+
hass: HomeAssistant,
130+
mock_manager: Manager,
131+
mock_config_entry: MockConfigEntry,
132+
mock_device: CustomerDevice,
133+
) -> None:
134+
"""Test turning off a switch."""
135+
entity_id = "switch.din_socket"
136+
await initialize_entry(hass, mock_manager, mock_config_entry, mock_device)
137+
138+
state = hass.states.get(entity_id)
139+
assert state is not None, f"{entity_id} does not exist"
140+
await hass.services.async_call(
141+
SWITCH_DOMAIN,
142+
SERVICE_TURN_OFF,
143+
{
144+
ATTR_ENTITY_ID: entity_id,
145+
},
146+
blocking=True,
147+
)
148+
mock_manager.send_commands.assert_called_once_with(
149+
mock_device.id, [{"code": "switch", "value": False}]
150+
)
151+
152+
153+
@patch("homeassistant.components.tuya.PLATFORMS", [Platform.SWITCH])
154+
@pytest.mark.parametrize(
155+
"mock_device_code",
156+
["cz_PGEkBctAbtzKOZng"],
157+
)
158+
@pytest.mark.parametrize(
159+
("initial_status", "expected_state"),
160+
[
161+
(True, "on"),
162+
(False, "off"),
163+
(None, STATE_UNKNOWN),
164+
("some string", STATE_UNKNOWN),
165+
],
166+
)
167+
async def test_state(
168+
hass: HomeAssistant,
169+
mock_manager: Manager,
170+
mock_config_entry: MockConfigEntry,
171+
mock_device: CustomerDevice,
172+
initial_status: Any,
173+
expected_state: str,
174+
) -> None:
175+
"""Test switch state."""
176+
entity_id = "switch.din_socket"
177+
mock_device.status["switch"] = initial_status
178+
await initialize_entry(hass, mock_manager, mock_config_entry, mock_device)
179+
180+
state = hass.states.get(entity_id)
181+
assert state is not None, f"{entity_id} does not exist"
182+
assert state.state == expected_state

tests/components/tuya/test_valve.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
from typing import Any
56
from unittest.mock import patch
67

78
import pytest
@@ -13,7 +14,7 @@
1314
SERVICE_CLOSE_VALVE,
1415
SERVICE_OPEN_VALVE,
1516
)
16-
from homeassistant.const import ATTR_ENTITY_ID, Platform
17+
from homeassistant.const import ATTR_ENTITY_ID, STATE_UNKNOWN, Platform
1718
from homeassistant.core import HomeAssistant
1819
from homeassistant.helpers import entity_registry as er
1920

@@ -95,3 +96,35 @@ async def test_close_valve(
9596
mock_manager.send_commands.assert_called_once_with(
9697
mock_device.id, [{"code": "switch_1", "value": False}]
9798
)
99+
100+
101+
@patch("homeassistant.components.tuya.PLATFORMS", [Platform.VALVE])
102+
@pytest.mark.parametrize(
103+
"mock_device_code",
104+
["sfkzq_ed7frwissyqrejic"],
105+
)
106+
@pytest.mark.parametrize(
107+
("initial_status", "expected_state"),
108+
[
109+
(True, "open"),
110+
(False, "closed"),
111+
(None, STATE_UNKNOWN),
112+
("some string", STATE_UNKNOWN),
113+
],
114+
)
115+
async def test_state(
116+
hass: HomeAssistant,
117+
mock_manager: Manager,
118+
mock_config_entry: MockConfigEntry,
119+
mock_device: CustomerDevice,
120+
initial_status: Any,
121+
expected_state: str,
122+
) -> None:
123+
"""Test valve state."""
124+
entity_id = "valve.jie_hashui_fa_valve_1"
125+
mock_device.status["switch_1"] = initial_status
126+
await initialize_entry(hass, mock_manager, mock_config_entry, mock_device)
127+
128+
state = hass.states.get(entity_id)
129+
assert state is not None, f"{entity_id} does not exist"
130+
assert state.state == expected_state

0 commit comments

Comments
 (0)