Skip to content

Commit 8a03ab2

Browse files
authored
Add async dpcode update wrapper to Tuya (home-assistant#156230)
1 parent d2ad5b4 commit 8a03ab2

File tree

6 files changed

+77
-26
lines changed

6 files changed

+77
-26
lines changed

homeassistant/components/tuya/entity.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +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
1415

1516

1617
class TuyaEntity(Entity):
@@ -64,3 +65,12 @@ def _send_command(self, commands: list[dict[str, Any]]) -> None:
6465
"""Send command to the device."""
6566
LOGGER.debug("Sending commands for device %s: %s", self.device.id, commands)
6667
self.device_manager.send_commands(self.device.id, commands)
68+
69+
async def _async_send_dpcode_update(
70+
self, dpcode_wrapper: DPCodeWrapper, value: Any
71+
) -> None:
72+
"""Send command to the device."""
73+
await self.hass.async_add_executor_job(
74+
self._send_command,
75+
[dpcode_wrapper.get_update_command(self.device, value)],
76+
)

homeassistant/components/tuya/models.py

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
from abc import ABC, abstractmethod
56
import base64
67
from dataclasses import dataclass
78
import json
@@ -120,7 +121,7 @@ def from_json(cls, dpcode: DPCode, data: str) -> Self | None:
120121
}
121122

122123

123-
class DPCodeWrapper:
124+
class DPCodeWrapper(ABC):
124125
"""Base DPCode wrapper.
125126
126127
Used as a common interface for referring to a DPCode, and
@@ -138,9 +139,30 @@ def _read_device_status_raw(self, device: CustomerDevice) -> Any | None:
138139
"""
139140
return device.status.get(self.dpcode)
140141

142+
@abstractmethod
141143
def read_device_status(self, device: CustomerDevice) -> Any | None:
142-
"""Read the device value for the dpcode."""
143-
raise NotImplementedError("read_device_status must be implemented")
144+
"""Read the device value for the dpcode.
145+
146+
The raw device status is converted to a Home Assistant value.
147+
"""
148+
149+
@abstractmethod
150+
def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any:
151+
"""Convert a Home Assistant value back to a raw device value.
152+
153+
This is called by `get_update_command` to prepare the value for sending
154+
back to the device, and should be implemented in concrete classes.
155+
"""
156+
157+
def get_update_command(self, device: CustomerDevice, value: Any) -> dict[str, Any]:
158+
"""Get the update command for the dpcode.
159+
160+
The Home Assistant value is converted back to a raw device value.
161+
"""
162+
return {
163+
"code": self.dpcode,
164+
"value": self._convert_value_to_raw_value(device, value),
165+
}
144166

145167

146168
class DPCodeBooleanWrapper(DPCodeWrapper):
@@ -155,6 +177,16 @@ def read_device_status(self, device: CustomerDevice) -> bool | None:
155177
return raw_value
156178
return None
157179

180+
def _convert_value_to_raw_value(
181+
self, device: CustomerDevice, value: Any
182+
) -> Any | None:
183+
"""Convert a Home Assistant value back to a raw device value."""
184+
if value in (True, False):
185+
return value
186+
# Currently only called with boolean values
187+
# Safety net in case of future changes
188+
raise ValueError(f"Invalid boolean value `{value}`")
189+
158190

159191
class DPCodeTypeInformationWrapper[T: TypeInformation](DPCodeWrapper):
160192
"""Base DPCode wrapper with Type Information."""
@@ -202,6 +234,16 @@ def read_device_status(self, device: CustomerDevice) -> str | None:
202234
return raw_value
203235
return None
204236

237+
def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any:
238+
"""Convert a Home Assistant value back to a raw device value."""
239+
if value in self.type_information.range:
240+
return value
241+
# Guarded by select option validation
242+
# Safety net in case of future changes
243+
raise ValueError(
244+
f"Enum value `{value}` out of range: {self.type_information.range}"
245+
)
246+
205247

206248
class DPCodeIntegerWrapper(DPCodeTypeInformationWrapper[IntegerTypeData]):
207249
"""Simple wrapper for IntegerTypeData values."""
@@ -217,6 +259,18 @@ def read_device_status(self, device: CustomerDevice) -> float | None:
217259
return None
218260
return raw_value / (10**self.type_information.scale)
219261

262+
def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any:
263+
"""Convert a Home Assistant value back to a raw device value."""
264+
new_value = round(value * (10**self.type_information.scale))
265+
if self.type_information.min <= new_value <= self.type_information.max:
266+
return new_value
267+
# Guarded by number validation
268+
# Safety net in case of future changes
269+
raise ValueError(
270+
f"Value `{new_value}` (converted from `{value}`) out of range:"
271+
f" ({self.type_information.min}-{self.type_information.max})"
272+
)
273+
220274

221275
@overload
222276
def find_dpcode(

homeassistant/components/tuya/number.py

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -540,15 +540,6 @@ def native_value(self) -> float | None:
540540
"""Return the entity value to represent the entity state."""
541541
return self._dpcode_wrapper.read_device_status(self.device)
542542

543-
def set_native_value(self, value: float) -> None:
543+
async def async_set_native_value(self, value: float) -> None:
544544
"""Set new value."""
545-
self._send_command(
546-
[
547-
{
548-
"code": self._dpcode_wrapper.dpcode,
549-
"value": (
550-
self._dpcode_wrapper.type_information.scale_value_back(value)
551-
),
552-
}
553-
]
554-
)
545+
await self._async_send_dpcode_update(self._dpcode_wrapper, value)

homeassistant/components/tuya/select.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,6 @@ def current_option(self) -> str | None:
402402
"""Return the selected entity option to represent the entity state."""
403403
return self._dpcode_wrapper.read_device_status(self.device)
404404

405-
def select_option(self, option: str) -> None:
405+
async def async_select_option(self, option: str) -> None:
406406
"""Change the selected option."""
407-
self._send_command([{"code": self._dpcode_wrapper.dpcode, "value": option}])
407+
await self._async_send_dpcode_update(self._dpcode_wrapper, option)

homeassistant/components/tuya/switch.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,10 +1041,10 @@ def is_on(self) -> bool | None:
10411041
"""Return true if switch is on."""
10421042
return self._dpcode_wrapper.read_device_status(self.device)
10431043

1044-
def turn_on(self, **kwargs: Any) -> None:
1044+
async def async_turn_on(self, **kwargs: Any) -> None:
10451045
"""Turn the switch on."""
1046-
self._send_command([{"code": self._dpcode_wrapper.dpcode, "value": True}])
1046+
await self._async_send_dpcode_update(self._dpcode_wrapper, True)
10471047

1048-
def turn_off(self, **kwargs: Any) -> None:
1048+
async def async_turn_off(self, **kwargs: Any) -> None:
10491049
"""Turn the switch off."""
1050-
self._send_command([{"code": self._dpcode_wrapper.dpcode, "value": False}])
1050+
await self._async_send_dpcode_update(self._dpcode_wrapper, False)

homeassistant/components/tuya/valve.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,12 +140,8 @@ def is_closed(self) -> bool | None:
140140

141141
async def async_open_valve(self) -> None:
142142
"""Open the valve."""
143-
await self.hass.async_add_executor_job(
144-
self._send_command, [{"code": self._dpcode_wrapper.dpcode, "value": True}]
145-
)
143+
await self._async_send_dpcode_update(self._dpcode_wrapper, True)
146144

147145
async def async_close_valve(self) -> None:
148146
"""Close the valve."""
149-
await self.hass.async_add_executor_job(
150-
self._send_command, [{"code": self._dpcode_wrapper.dpcode, "value": False}]
151-
)
147+
await self._async_send_dpcode_update(self._dpcode_wrapper, False)

0 commit comments

Comments
 (0)