1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-08 17:49:37 +01:00

Add async dpcode update wrapper to Tuya (#156230)

This commit is contained in:
epenet
2025-11-10 10:09:22 +01:00
committed by GitHub
parent d2ad5b43f2
commit 8a03ab2f64
6 changed files with 77 additions and 26 deletions
+10
View File
@@ -11,6 +11,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from .const import DOMAIN, LOGGER, TUYA_HA_SIGNAL_UPDATE_ENTITY
from .models import DPCodeWrapper
class TuyaEntity(Entity):
@@ -64,3 +65,12 @@ class TuyaEntity(Entity):
"""Send command to the device."""
LOGGER.debug("Sending commands for device %s: %s", self.device.id, commands)
self.device_manager.send_commands(self.device.id, commands)
async def _async_send_dpcode_update(
self, dpcode_wrapper: DPCodeWrapper, value: Any
) -> None:
"""Send command to the device."""
await self.hass.async_add_executor_job(
self._send_command,
[dpcode_wrapper.get_update_command(self.device, value)],
)
+57 -3
View File
@@ -2,6 +2,7 @@
from __future__ import annotations
from abc import ABC, abstractmethod
import base64
from dataclasses import dataclass
import json
@@ -120,7 +121,7 @@ _TYPE_INFORMATION_MAPPINGS: dict[DPType, type[TypeInformation]] = {
}
class DPCodeWrapper:
class DPCodeWrapper(ABC):
"""Base DPCode wrapper.
Used as a common interface for referring to a DPCode, and
@@ -138,9 +139,30 @@ class DPCodeWrapper:
"""
return device.status.get(self.dpcode)
@abstractmethod
def read_device_status(self, device: CustomerDevice) -> Any | None:
"""Read the device value for the dpcode."""
raise NotImplementedError("read_device_status must be implemented")
"""Read the device value for the dpcode.
The raw device status is converted to a Home Assistant value.
"""
@abstractmethod
def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any:
"""Convert a Home Assistant value back to a raw device value.
This is called by `get_update_command` to prepare the value for sending
back to the device, and should be implemented in concrete classes.
"""
def get_update_command(self, device: CustomerDevice, value: Any) -> dict[str, Any]:
"""Get the update command for the dpcode.
The Home Assistant value is converted back to a raw device value.
"""
return {
"code": self.dpcode,
"value": self._convert_value_to_raw_value(device, value),
}
class DPCodeBooleanWrapper(DPCodeWrapper):
@@ -155,6 +177,16 @@ class DPCodeBooleanWrapper(DPCodeWrapper):
return raw_value
return None
def _convert_value_to_raw_value(
self, device: CustomerDevice, value: Any
) -> Any | None:
"""Convert a Home Assistant value back to a raw device value."""
if value in (True, False):
return value
# Currently only called with boolean values
# Safety net in case of future changes
raise ValueError(f"Invalid boolean value `{value}`")
class DPCodeTypeInformationWrapper[T: TypeInformation](DPCodeWrapper):
"""Base DPCode wrapper with Type Information."""
@@ -202,6 +234,16 @@ class DPCodeEnumWrapper(DPCodeTypeInformationWrapper[EnumTypeData]):
return raw_value
return None
def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any:
"""Convert a Home Assistant value back to a raw device value."""
if value in self.type_information.range:
return value
# Guarded by select option validation
# Safety net in case of future changes
raise ValueError(
f"Enum value `{value}` out of range: {self.type_information.range}"
)
class DPCodeIntegerWrapper(DPCodeTypeInformationWrapper[IntegerTypeData]):
"""Simple wrapper for IntegerTypeData values."""
@@ -217,6 +259,18 @@ class DPCodeIntegerWrapper(DPCodeTypeInformationWrapper[IntegerTypeData]):
return None
return raw_value / (10**self.type_information.scale)
def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any:
"""Convert a Home Assistant value back to a raw device value."""
new_value = round(value * (10**self.type_information.scale))
if self.type_information.min <= new_value <= self.type_information.max:
return new_value
# Guarded by number validation
# Safety net in case of future changes
raise ValueError(
f"Value `{new_value}` (converted from `{value}`) out of range:"
f" ({self.type_information.min}-{self.type_information.max})"
)
@overload
def find_dpcode(
+2 -11
View File
@@ -540,15 +540,6 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity):
"""Return the entity value to represent the entity state."""
return self._dpcode_wrapper.read_device_status(self.device)
def set_native_value(self, value: float) -> None:
async def async_set_native_value(self, value: float) -> None:
"""Set new value."""
self._send_command(
[
{
"code": self._dpcode_wrapper.dpcode,
"value": (
self._dpcode_wrapper.type_information.scale_value_back(value)
),
}
]
)
await self._async_send_dpcode_update(self._dpcode_wrapper, value)
+2 -2
View File
@@ -402,6 +402,6 @@ class TuyaSelectEntity(TuyaEntity, SelectEntity):
"""Return the selected entity option to represent the entity state."""
return self._dpcode_wrapper.read_device_status(self.device)
def select_option(self, option: str) -> None:
async def async_select_option(self, option: str) -> None:
"""Change the selected option."""
self._send_command([{"code": self._dpcode_wrapper.dpcode, "value": option}])
await self._async_send_dpcode_update(self._dpcode_wrapper, option)
+4 -4
View File
@@ -1041,10 +1041,10 @@ class TuyaSwitchEntity(TuyaEntity, SwitchEntity):
"""Return true if switch is on."""
return self._dpcode_wrapper.read_device_status(self.device)
def turn_on(self, **kwargs: Any) -> None:
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
self._send_command([{"code": self._dpcode_wrapper.dpcode, "value": True}])
await self._async_send_dpcode_update(self._dpcode_wrapper, True)
def turn_off(self, **kwargs: Any) -> None:
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
self._send_command([{"code": self._dpcode_wrapper.dpcode, "value": False}])
await self._async_send_dpcode_update(self._dpcode_wrapper, False)
+2 -6
View File
@@ -140,12 +140,8 @@ class TuyaValveEntity(TuyaEntity, ValveEntity):
async def async_open_valve(self) -> None:
"""Open the valve."""
await self.hass.async_add_executor_job(
self._send_command, [{"code": self._dpcode_wrapper.dpcode, "value": True}]
)
await self._async_send_dpcode_update(self._dpcode_wrapper, True)
async def async_close_valve(self) -> None:
"""Close the valve."""
await self.hass.async_add_executor_job(
self._send_command, [{"code": self._dpcode_wrapper.dpcode, "value": False}]
)
await self._async_send_dpcode_update(self._dpcode_wrapper, False)