diff --git a/homeassistant/components/tuya/entity.py b/homeassistant/components/tuya/entity.py index ad62af67114..42dbbf427fd 100644 --- a/homeassistant/components/tuya/entity.py +++ b/homeassistant/components/tuya/entity.py @@ -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)], + ) diff --git a/homeassistant/components/tuya/models.py b/homeassistant/components/tuya/models.py index d2339365215..73487780f69 100644 --- a/homeassistant/components/tuya/models.py +++ b/homeassistant/components/tuya/models.py @@ -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( diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index 29fb40ed239..e30a1f904db 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -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) diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index e15d6e5f25c..b631caffc7e 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -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) diff --git a/homeassistant/components/tuya/switch.py b/homeassistant/components/tuya/switch.py index a1e6d92603a..3cc58aa7d58 100644 --- a/homeassistant/components/tuya/switch.py +++ b/homeassistant/components/tuya/switch.py @@ -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) diff --git a/homeassistant/components/tuya/valve.py b/homeassistant/components/tuya/valve.py index ddcd0314aba..effb62afb3d 100644 --- a/homeassistant/components/tuya/valve.py +++ b/homeassistant/components/tuya/valve.py @@ -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)