Refactor Tuya DPCode and data type handling (#64707)

pull/61182/head^2
Franck Nijhof 2022-01-23 09:01:10 +01:00 committed by GitHub
parent a5fb60fd3a
commit db979fef6c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 429 additions and 500 deletions

View File

@ -23,8 +23,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import HomeAssistantTuyaData
from .base import EnumTypeData, TuyaEntity
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode
from .base import TuyaEntity
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, DPType
class Mode(StrEnum):
@ -105,36 +105,39 @@ class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity):
self._attr_unique_id = f"{super().unique_id}{description.key}"
# Determine supported modes
supported_mode = EnumTypeData.from_json(
device.function[DPCode.MASTER_MODE].values
).range
if supported_modes := self.find_dpcode(
description.key, dptype=DPType.ENUM, prefer_function=True
):
if Mode.HOME in supported_modes.range:
self._attr_supported_features |= SUPPORT_ALARM_ARM_HOME
if Mode.HOME in supported_mode:
self._attr_supported_features |= SUPPORT_ALARM_ARM_HOME
if Mode.ARM in supported_modes.range:
self._attr_supported_features |= SUPPORT_ALARM_ARM_AWAY
if Mode.ARM in supported_mode:
self._attr_supported_features |= SUPPORT_ALARM_ARM_AWAY
if Mode.SOS in supported_mode:
self._attr_supported_features |= SUPPORT_ALARM_TRIGGER
if Mode.SOS in supported_modes.range:
self._attr_supported_features |= SUPPORT_ALARM_TRIGGER
@property
def state(self):
"""Return the state of the device."""
return STATE_MAPPING.get(self.device.status.get(DPCode.MASTER_MODE))
if not (status := self.device.status.get(self.entity_description.key)):
return None
return STATE_MAPPING.get(status)
def alarm_disarm(self, code: str | None = None) -> None:
"""Send Disarm command."""
self._send_command([{"code": DPCode.MASTER_MODE, "value": Mode.DISARMED}])
self._send_command(
[{"code": self.entity_description.key, "value": Mode.DISARMED}]
)
def alarm_arm_home(self, code: str | None = None) -> None:
"""Send Home command."""
self._send_command([{"code": DPCode.MASTER_MODE, "value": Mode.HOME}])
self._send_command([{"code": self.entity_description.key, "value": Mode.HOME}])
def alarm_arm_away(self, code: str | None = None) -> None:
"""Send Arm command."""
self._send_command([{"code": DPCode.MASTER_MODE, "value": Mode.ARM}])
self._send_command([{"code": self.entity_description.key, "value": Mode.ARM}])
def alarm_trigger(self, code: str | None = None) -> None:
"""Send SOS command."""
self._send_command([{"code": DPCode.MASTER_MODE, "value": Mode.SOS}])
self._send_command([{"code": self.entity_description.key, "value": Mode.SOS}])

View File

@ -5,14 +5,14 @@ import base64
from dataclasses import dataclass
import json
import struct
from typing import Any
from typing import Any, Literal, overload
from tuya_iot import TuyaDevice, TuyaDeviceManager
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import DeviceInfo, Entity
from .const import DOMAIN, LOGGER, TUYA_HA_SIGNAL_UPDATE_ENTITY
from .const import DOMAIN, LOGGER, TUYA_HA_SIGNAL_UPDATE_ENTITY, DPCode, DPType
from .util import remap_value
@ -20,6 +20,7 @@ from .util import remap_value
class IntegerTypeData:
"""Integer Type Data."""
dpcode: DPCode
min: int
max: int
scale: float
@ -71,21 +72,22 @@ class IntegerTypeData:
return remap_value(value, from_min, from_max, self.min, self.max, reverse)
@classmethod
def from_json(cls, data: str) -> IntegerTypeData:
def from_json(cls, dpcode: DPCode, data: str) -> IntegerTypeData:
"""Load JSON string and return a IntegerTypeData object."""
return cls(**json.loads(data))
return cls(dpcode, **json.loads(data))
@dataclass
class EnumTypeData:
"""Enum Type Data."""
dpcode: DPCode
range: list[str]
@classmethod
def from_json(cls, data: str) -> EnumTypeData:
def from_json(cls, dpcode: DPCode, data: str) -> EnumTypeData:
"""Load JSON string and return a EnumTypeData object."""
return cls(**json.loads(data))
return cls(dpcode, **json.loads(data))
@dataclass
@ -149,6 +151,101 @@ class TuyaEntity(Entity):
"""Return if the device is available."""
return self.device.online
@overload
def find_dpcode(
self,
dpcodes: str | DPCode | tuple[DPCode, ...] | None,
*,
prefer_function: bool = False,
dptype: Literal[DPType.ENUM],
) -> EnumTypeData | None:
...
@overload
def find_dpcode(
self,
dpcodes: str | DPCode | tuple[DPCode, ...] | None,
*,
prefer_function: bool = False,
dptype: Literal[DPType.INTEGER],
) -> IntegerTypeData | None:
...
@overload
def find_dpcode(
self,
dpcodes: str | DPCode | tuple[DPCode, ...] | None,
*,
prefer_function: bool = False,
) -> DPCode | None:
...
def find_dpcode(
self,
dpcodes: str | DPCode | tuple[DPCode, ...] | None,
*,
prefer_function: bool = False,
dptype: DPType = None,
) -> DPCode | EnumTypeData | IntegerTypeData | None:
"""Find a matching DP code available on for this device."""
if dpcodes is None:
return None
if isinstance(dpcodes, str):
dpcodes = (DPCode(dpcodes),)
elif not isinstance(dpcodes, tuple):
dpcodes = (dpcodes,)
order = ["status_range", "function"]
if prefer_function:
order = ["function", "status_range"]
# When we are not looking for a specific datatype, we can append status for
# searching
if not dptype:
order.append("status")
for dpcode in dpcodes:
for key in order:
if dpcode not in getattr(self.device, key):
continue
if (
dptype == DPType.ENUM
and getattr(self.device, key)[dpcode].type == DPType.ENUM
):
return EnumTypeData.from_json(
dpcode, getattr(self.device, key)[dpcode].values
)
if (
dptype == DPType.INTEGER
and getattr(self.device, key)[dpcode].type == DPType.INTEGER
):
return IntegerTypeData.from_json(
dpcode, getattr(self.device, key)[dpcode].values
)
if dptype not in (DPType.ENUM, DPType.INTEGER):
return dpcode
return None
def get_dptype(
self, dpcode: DPCode | None, prefer_function: bool = False
) -> DPType | None:
"""Find a matching DPCode data type available on for this device."""
if dpcode is None:
return None
order = ["status_range", "function"]
if prefer_function:
order = ["function", "status_range"]
for key in order:
if dpcode in getattr(self.device, key):
return DPType(getattr(self.device, key)[dpcode].type)
return None
async def async_added_to_hass(self) -> None:
"""Call when entity is added to hass."""
self.async_on_remove(

View File

@ -31,8 +31,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import HomeAssistantTuyaData
from .base import EnumTypeData, IntegerTypeData, TuyaEntity
from .const import DOMAIN, LOGGER, TUYA_DISCOVERY_NEW, DPCode
from .base import IntegerTypeData, TuyaEntity
from .const import DOMAIN, LOGGER, TUYA_DISCOVERY_NEW, DPCode, DPType
TUYA_HVAC_TO_HA = {
"auto": HVAC_MODE_HEAT_COOL,
@ -114,18 +114,14 @@ async def async_setup_entry(
class TuyaClimateEntity(TuyaEntity, ClimateEntity):
"""Tuya Climate Device."""
_current_humidity_dpcode: DPCode | None = None
_current_humidity_type: IntegerTypeData | None = None
_current_temperature_dpcode: DPCode | None = None
_current_temperature_type: IntegerTypeData | None = None
_current_humidity: IntegerTypeData | None = None
_current_temperature: IntegerTypeData | None = None
_hvac_to_tuya: dict[str, str]
_set_humidity_dpcode: DPCode | None = None
_set_humidity_type: IntegerTypeData | None = None
_set_temperature_dpcode: DPCode | None = None
_set_temperature_type: IntegerTypeData | None = None
_set_humidity: IntegerTypeData | None = None
_set_temperature: IntegerTypeData | None = None
entity_description: TuyaClimateEntityDescription
def __init__( # noqa: C901
def __init__(
self,
device: TuyaDevice,
device_manager: TuyaDeviceManager,
@ -140,160 +136,117 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
# If both temperature values for celsius and fahrenheit are present,
# use whatever the device is set to, with a fallback to celsius.
prefered_temperature_unit = None
if all(
dpcode in device.status
for dpcode in (DPCode.TEMP_CURRENT, DPCode.TEMP_CURRENT_F)
) or all(
dpcode in device.status for dpcode in (DPCode.TEMP_SET, DPCode.TEMP_SET_F)
):
self._attr_temperature_unit = TEMP_CELSIUS
prefered_temperature_unit = TEMP_CELSIUS
if any(
"f" in device.status[dpcode].lower()
for dpcode in (DPCode.C_F, DPCode.TEMP_UNIT_CONVERT)
if isinstance(device.status.get(dpcode), str)
):
self._attr_temperature_unit = TEMP_FAHRENHEIT
prefered_temperature_unit = TEMP_FAHRENHEIT
# If any DPCode handling celsius is present, use celsius.
elif any(
dpcode in device.status for dpcode in (DPCode.TEMP_CURRENT, DPCode.TEMP_SET)
):
self._attr_temperature_unit = TEMP_CELSIUS
# Default to Celsius
self._attr_temperature_unit = TEMP_CELSIUS
# If any DPCode handling fahrenheit is present, use celsius.
elif any(
dpcode in device.status
for dpcode in (DPCode.TEMP_CURRENT_F, DPCode.TEMP_SET_F)
# Figure out current temperature, use preferred unit or what is available
celsius_type = self.find_dpcode(DPCode.TEMP_CURRENT, dptype=DPType.INTEGER)
farhenheit_type = self.find_dpcode(DPCode.TEMP_CURRENT_F, dptype=DPType.INTEGER)
if farhenheit_type and (
prefered_temperature_unit == TEMP_FAHRENHEIT
or (prefered_temperature_unit == TEMP_CELSIUS and not celsius_type)
):
self._attr_temperature_unit = TEMP_FAHRENHEIT
self._current_temperature = farhenheit_type
elif celsius_type:
self._attr_temperature_unit = TEMP_CELSIUS
self._current_temperature = celsius_type
# Determine dpcode to use for setting temperature
if all(
dpcode in device.status for dpcode in (DPCode.TEMP_SET, DPCode.TEMP_SET_F)
# Figure out setting temperature, use preferred unit or what is available
celsius_type = self.find_dpcode(
DPCode.TEMP_SET, dptype=DPType.INTEGER, prefer_function=True
)
farhenheit_type = self.find_dpcode(
DPCode.TEMP_SET_F, dptype=DPType.INTEGER, prefer_function=True
)
if farhenheit_type and (
prefered_temperature_unit == TEMP_FAHRENHEIT
or (prefered_temperature_unit == TEMP_CELSIUS and not celsius_type)
):
self._set_temperature_dpcode = DPCode.TEMP_SET
if self._attr_temperature_unit == TEMP_FAHRENHEIT:
self._set_temperature_dpcode = DPCode.TEMP_SET_F
elif DPCode.TEMP_SET in device.status:
self._set_temperature_dpcode = DPCode.TEMP_SET
elif DPCode.TEMP_SET_F in device.status:
self._set_temperature_dpcode = DPCode.TEMP_SET_F
self._set_temperature = farhenheit_type
elif celsius_type:
self._set_temperature = celsius_type
# Get integer type data for the dpcode to set temperature, use
# it to define min, max & step temperatures
if (
self._set_temperature_dpcode
and self._set_temperature_dpcode in device.function
):
type_data = IntegerTypeData.from_json(
device.function[self._set_temperature_dpcode].values
)
if self._set_temperature:
self._attr_supported_features |= SUPPORT_TARGET_TEMPERATURE
self._set_temperature_type = type_data
self._attr_max_temp = type_data.max_scaled
self._attr_min_temp = type_data.min_scaled
self._attr_target_temperature_step = type_data.step_scaled
# Determine dpcode to use for getting the current temperature
if all(
dpcode in device.status
for dpcode in (DPCode.TEMP_CURRENT, DPCode.TEMP_CURRENT_F)
):
self._current_temperature_dpcode = DPCode.TEMP_CURRENT
if self._attr_temperature_unit == TEMP_FAHRENHEIT:
self._current_temperature_dpcode = DPCode.TEMP_CURRENT_F
elif DPCode.TEMP_CURRENT in device.status:
self._current_temperature_dpcode = DPCode.TEMP_CURRENT
elif DPCode.TEMP_CURRENT_F in device.status:
self._current_temperature_dpcode = DPCode.TEMP_CURRENT_F
# If we have a current temperature dpcode, get the integer type data
if (
self._current_temperature_dpcode
and self._current_temperature_dpcode in device.status_range
):
self._current_temperature_type = IntegerTypeData.from_json(
device.status_range[self._current_temperature_dpcode].values
)
self._attr_max_temp = self._set_temperature.max_scaled
self._attr_min_temp = self._set_temperature.min_scaled
self._attr_target_temperature_step = self._set_temperature.step_scaled
# Determine HVAC modes
self._attr_hvac_modes = []
self._hvac_to_tuya = {}
if DPCode.MODE in device.function:
data_type = EnumTypeData.from_json(device.function[DPCode.MODE].values)
if enum_type := self.find_dpcode(
DPCode.MODE, dptype=DPType.ENUM, prefer_function=True
):
self._attr_hvac_modes = [HVAC_MODE_OFF]
for tuya_mode, ha_mode in TUYA_HVAC_TO_HA.items():
if tuya_mode in data_type.range:
if tuya_mode in enum_type.range:
self._hvac_to_tuya[ha_mode] = tuya_mode
self._attr_hvac_modes.append(ha_mode)
elif DPCode.SWITCH in device.function:
elif self.find_dpcode(DPCode.SWITCH, prefer_function=True):
self._attr_hvac_modes = [
HVAC_MODE_OFF,
description.switch_only_hvac_mode,
]
# Determine dpcode to use for setting the humidity
if DPCode.HUMIDITY_SET in device.function:
if int_type := self.find_dpcode(
DPCode.HUMIDITY_SET, dptype=DPType.INTEGER, prefer_function=True
):
self._attr_supported_features |= SUPPORT_TARGET_HUMIDITY
self._set_humidity_dpcode = DPCode.HUMIDITY_SET
type_data = IntegerTypeData.from_json(
device.function[DPCode.HUMIDITY_SET].values
)
self._set_humidity_type = type_data
self._attr_min_humidity = int(type_data.min_scaled)
self._attr_max_humidity = int(type_data.max_scaled)
self._set_humidity = int_type
self._attr_min_humidity = int(int_type.min_scaled)
self._attr_max_humidity = int(int_type.max_scaled)
# Determine dpcode to use for getting the current humidity
if (
DPCode.HUMIDITY_CURRENT in device.status
and DPCode.HUMIDITY_CURRENT in device.status_range
):
self._current_humidity_dpcode = DPCode.HUMIDITY_CURRENT
self._current_humidity_type = IntegerTypeData.from_json(
device.status_range[DPCode.HUMIDITY_CURRENT].values
)
# Determine dpcode to use for getting the current humidity
if (
DPCode.HUMIDITY_CURRENT in device.status
and DPCode.HUMIDITY_CURRENT in device.status_range
):
self._current_humidity_dpcode = DPCode.HUMIDITY_CURRENT
self._current_humidity_type = IntegerTypeData.from_json(
device.status_range[DPCode.HUMIDITY_CURRENT].values
)
self._current_humidity = self.find_dpcode(
DPCode.HUMIDITY_CURRENT, dptype=DPType.INTEGER
)
# Determine fan modes
if (
DPCode.FAN_SPEED_ENUM in device.status
and DPCode.FAN_SPEED_ENUM in device.function
if enum_type := self.find_dpcode(
DPCode.FAN_SPEED_ENUM, dptype=DPType.ENUM, prefer_function=True
):
self._attr_supported_features |= SUPPORT_FAN_MODE
self._attr_fan_modes = EnumTypeData.from_json(
device.status_range[DPCode.FAN_SPEED_ENUM].values
).range
self._attr_fan_modes = enum_type.range
# Determine swing modes
if any(
dpcode in device.function
for dpcode in (
if self.find_dpcode(
(
DPCode.SHAKE,
DPCode.SWING,
DPCode.SWITCH_HORIZONTAL,
DPCode.SWITCH_VERTICAL,
)
),
prefer_function=True,
):
self._attr_supported_features |= SUPPORT_SWING_MODE
self._attr_swing_modes = [SWING_OFF]
if any(
dpcode in device.function for dpcode in (DPCode.SHAKE, DPCode.SWING)
):
if self.find_dpcode((DPCode.SHAKE, DPCode.SWING), prefer_function=True):
self._attr_swing_modes.append(SWING_ON)
if DPCode.SWITCH_HORIZONTAL in device.function:
if self.find_dpcode(DPCode.SWITCH_HORIZONTAL, prefer_function=True):
self._attr_swing_modes.append(SWING_HORIZONTAL)
if DPCode.SWITCH_VERTICAL in device.function:
if self.find_dpcode(DPCode.SWITCH_VERTICAL, prefer_function=True):
self._attr_swing_modes.append(SWING_VERTICAL)
async def async_added_to_hass(self) -> None:
@ -301,9 +254,10 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
await super().async_added_to_hass()
# Log unknown modes
if DPCode.MODE in self.device.function:
data_type = EnumTypeData.from_json(self.device.function[DPCode.MODE].values)
for tuya_mode in data_type.range:
if enum_type := self.find_dpcode(
DPCode.MODE, dptype=DPType.ENUM, prefer_function=True
):
for tuya_mode in enum_type.range:
if tuya_mode not in TUYA_HVAC_TO_HA:
LOGGER.warning(
"Unknown HVAC mode '%s' for device %s; assuming it as off",
@ -326,7 +280,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
def set_humidity(self, humidity: float) -> None:
"""Set new target humidity."""
if self._set_humidity_dpcode is None or self._set_humidity_type is None:
if self._set_humidity is None:
raise RuntimeError(
"Cannot set humidity, device doesn't provide methods to set it"
)
@ -334,8 +288,8 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
self._send_command(
[
{
"code": self._set_humidity_dpcode,
"value": self._set_humidity_type.scale_value_back(humidity),
"code": self._set_humidity.dpcode,
"value": self._set_humidity.scale_value_back(humidity),
}
]
)
@ -367,7 +321,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
def set_temperature(self, **kwargs: Any) -> None:
"""Set new target temperature."""
if self._set_temperature_dpcode is None or self._set_temperature_type is None:
if self._set_temperature is None:
raise RuntimeError(
"Cannot set target temperature, device doesn't provide methods to set it"
)
@ -375,11 +329,9 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
self._send_command(
[
{
"code": self._set_temperature_dpcode,
"code": self._set_temperature.dpcode,
"value": round(
self._set_temperature_type.scale_value_back(
kwargs["temperature"]
)
self._set_temperature.scale_value_back(kwargs["temperature"])
),
}
]
@ -388,53 +340,50 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
@property
def current_temperature(self) -> float | None:
"""Return the current temperature."""
if (
self._current_temperature_dpcode is None
or self._current_temperature_type is None
):
if self._current_temperature is None:
return None
temperature = self.device.status.get(self._current_temperature_dpcode)
temperature = self.device.status.get(self._current_temperature.dpcode)
if temperature is None:
return None
return self._current_temperature_type.scale_value(temperature)
return self._current_temperature.scale_value(temperature)
@property
def current_humidity(self) -> int | None:
"""Return the current humidity."""
if self._current_humidity_dpcode is None or self._current_humidity_type is None:
if self._current_humidity is None:
return None
humidity = self.device.status.get(self._current_humidity_dpcode)
humidity = self.device.status.get(self._current_humidity.dpcode)
if humidity is None:
return None
return round(self._current_humidity_type.scale_value(humidity))
return round(self._current_humidity.scale_value(humidity))
@property
def target_temperature(self) -> float | None:
"""Return the temperature currently set to be reached."""
if self._set_temperature_dpcode is None or self._set_temperature_type is None:
if self._set_temperature is None:
return None
temperature = self.device.status.get(self._set_temperature_dpcode)
temperature = self.device.status.get(self._set_temperature.dpcode)
if temperature is None:
return None
return self._set_temperature_type.scale_value(temperature)
return self._set_temperature.scale_value(temperature)
@property
def target_humidity(self) -> int | None:
"""Return the humidity currently set to be reached."""
if self._set_humidity_dpcode is None or self._set_humidity_type is None:
if self._set_humidity is None:
return None
humidity = self.device.status.get(self._set_humidity_dpcode)
humidity = self.device.status.get(self._set_humidity.dpcode)
if humidity is None:
return None
return round(self._set_humidity_type.scale_value(humidity))
return round(self._set_humidity.scale_value(humidity))
@property
def hvac_mode(self) -> str:

View File

@ -112,6 +112,17 @@ class WorkMode(StrEnum):
WHITE = "white"
class DPType(StrEnum):
"""Data point types."""
BOOLEAN = "Boolean"
ENUM = "Enum"
INTEGER = "Integer"
JSON = "Json"
RAW = "Raw"
STRING = "String"
class DPCode(StrEnum):
"""Data Point Codes used by Tuya.

View File

@ -24,8 +24,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import HomeAssistantTuyaData
from .base import EnumTypeData, IntegerTypeData, TuyaEntity
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode
from .base import IntegerTypeData, TuyaEntity
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, DPType
@dataclass
@ -177,11 +177,9 @@ async def async_setup_entry(
class TuyaCoverEntity(TuyaEntity, CoverEntity):
"""Tuya Cover Device."""
_current_position_type: IntegerTypeData | None = None
_set_position_type: IntegerTypeData | None = None
_tilt_dpcode: DPCode | None = None
_tilt_type: IntegerTypeData | None = None
_position_dpcode: DPCode | None = None
_current_position: IntegerTypeData | None = None
_set_position: IntegerTypeData | None = None
_tilt: IntegerTypeData | None = None
entity_description: TuyaCoverEntityDescription
def __init__(
@ -197,85 +195,54 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
self._attr_supported_features = 0
# Check if this cover is based on a switch or has controls
if device.function[description.key].type == "Boolean":
self._attr_supported_features |= SUPPORT_OPEN | SUPPORT_CLOSE
elif device.function[description.key].type == "Enum":
data_type = EnumTypeData.from_json(device.function[description.key].values)
if description.open_instruction_value in data_type.range:
self._attr_supported_features |= SUPPORT_OPEN
if description.close_instruction_value in data_type.range:
self._attr_supported_features |= SUPPORT_CLOSE
if description.stop_instruction_value in data_type.range:
self._attr_supported_features |= SUPPORT_STOP
if self.find_dpcode(description.key, prefer_function=True):
if device.function[description.key].type == "Boolean":
self._attr_supported_features |= SUPPORT_OPEN | SUPPORT_CLOSE
elif enum_type := self.find_dpcode(
description.key, dptype=DPType.ENUM, prefer_function=True
):
if description.open_instruction_value in enum_type.range:
self._attr_supported_features |= SUPPORT_OPEN
if description.close_instruction_value in enum_type.range:
self._attr_supported_features |= SUPPORT_CLOSE
if description.stop_instruction_value in enum_type.range:
self._attr_supported_features |= SUPPORT_STOP
# Determine type to use for setting the position
if (
description.set_position is not None
and description.set_position in device.status_range
if int_type := self.find_dpcode(
description.set_position, dptype=DPType.INTEGER, prefer_function=True
):
self._attr_supported_features |= SUPPORT_SET_POSITION
self._set_position_type = IntegerTypeData.from_json(
device.status_range[description.set_position].values
)
self._set_position = int_type
# Set as default, unless overwritten below
self._current_position_type = self._set_position_type
self._current_position = int_type
# Determine type for getting the position
if (
description.current_position is not None
and description.current_position in device.status_range
if int_type := self.find_dpcode(
description.current_position, dptype=DPType.INTEGER, prefer_function=True
):
self._current_position_type = IntegerTypeData.from_json(
device.status_range[description.current_position].values
)
self._current_position = int_type
# Determine type to use for setting the tilt
if tilt_dpcode := next(
(
dpcode
for dpcode in (DPCode.ANGLE_HORIZONTAL, DPCode.ANGLE_VERTICAL)
if dpcode in device.function
),
None,
if int_type := self.find_dpcode(
(DPCode.ANGLE_HORIZONTAL, DPCode.ANGLE_VERTICAL),
dptype=DPType.INTEGER,
prefer_function=True,
):
self._attr_supported_features |= SUPPORT_SET_TILT_POSITION
self._tilt_dpcode = tilt_dpcode
self._tilt_type = IntegerTypeData.from_json(
device.status_range[tilt_dpcode].values
)
# Determine current_position DPCodes
if (
self.entity_description.current_position is None
and self.entity_description.set_position is not None
):
self._position_dpcode = self.entity_description.set_position
elif isinstance(self.entity_description.current_position, DPCode):
self._position_dpcode = self.entity_description.current_position
elif isinstance(self.entity_description.current_position, tuple):
self._position_dpcode = next(
(
dpcode
for dpcode in self.entity_description.current_position
if self.device.status.get(dpcode) is not None
),
None,
)
self._tilt = int_type
@property
def current_cover_position(self) -> int | None:
"""Return cover current position."""
if self._current_position_type is None:
if self._current_position is None:
return None
if not self._position_dpcode:
return None
if (position := self.device.status.get(self._position_dpcode)) is None:
if (position := self.device.status.get(self._current_position.dpcode)) is None:
return None
return round(
self._current_position_type.remap_value_to(position, 0, 100, reverse=True)
self._current_position.remap_value_to(position, 0, 100, reverse=True)
)
@property
@ -284,13 +251,13 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
None is unknown, 0 is closed, 100 is fully open.
"""
if self._tilt_dpcode is None or self._tilt_type is None:
if self._tilt is None:
return None
if (angle := self.device.status.get(self._tilt_dpcode)) is None:
if (angle := self.device.status.get(self._tilt.dpcode)) is None:
return None
return round(self._tilt_type.remap_value_to(angle, 0, 100))
return round(self._tilt.remap_value_to(angle, 0, 100))
@property
def is_closed(self) -> bool | None:
@ -316,24 +283,21 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
def open_cover(self, **kwargs: Any) -> None:
"""Open the cover."""
value: bool | str = True
if self.device.function[self.entity_description.key].type == "Enum":
if self.find_dpcode(
self.entity_description.key, dptype=DPType.ENUM, prefer_function=True
):
value = self.entity_description.open_instruction_value
commands: list[dict[str, str | int]] = [
{"code": self.entity_description.key, "value": value}
]
if (
self.entity_description.set_position is not None
and self._set_position_type is not None
):
if self._set_position is not None:
commands.append(
{
"code": self.entity_description.set_position,
"code": self._set_position.dpcode,
"value": round(
self._set_position_type.remap_value_from(
100, 0, 100, reverse=True
),
self._set_position.remap_value_from(100, 0, 100, reverse=True),
),
}
)
@ -343,24 +307,21 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
def close_cover(self, **kwargs: Any) -> None:
"""Close cover."""
value: bool | str = False
if self.device.function[self.entity_description.key].type == "Enum":
if self.find_dpcode(
self.entity_description.key, dptype=DPType.ENUM, prefer_function=True
):
value = self.entity_description.close_instruction_value
commands: list[dict[str, str | int]] = [
{"code": self.entity_description.key, "value": value}
]
if (
self.entity_description.set_position is not None
and self._set_position_type is not None
):
if self._set_position is not None:
commands.append(
{
"code": self.entity_description.set_position,
"code": self._set_position.dpcode,
"value": round(
self._set_position_type.remap_value_from(
0, 0, 100, reverse=True
),
self._set_position.remap_value_from(0, 0, 100, reverse=True),
),
}
)
@ -369,7 +330,7 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
def set_cover_position(self, **kwargs: Any) -> None:
"""Move the cover to a specific position."""
if self._set_position_type is None:
if self._set_position is None:
raise RuntimeError(
"Cannot set position, device doesn't provide methods to set it"
)
@ -377,9 +338,9 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
self._send_command(
[
{
"code": self.entity_description.set_position,
"code": self._set_position.dpcode,
"value": round(
self._set_position_type.remap_value_from(
self._set_position.remap_value_from(
kwargs[ATTR_POSITION], 0, 100, reverse=True
)
),
@ -400,7 +361,7 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
def set_cover_tilt_position(self, **kwargs):
"""Move the cover tilt to a specific position."""
if self._tilt_type is None:
if self._tilt is None:
raise RuntimeError(
"Cannot set tilt, device doesn't provide methods to set it"
)
@ -408,9 +369,9 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
self._send_command(
[
{
"code": self._tilt_dpcode,
"code": self._tilt.dpcode,
"value": round(
self._tilt_type.remap_value_from(
self._tilt.remap_value_from(
kwargs[ATTR_TILT_POSITION], 0, 100, reverse=True
)
),

View File

@ -17,8 +17,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import HomeAssistantTuyaData
from .base import EnumTypeData, IntegerTypeData, TuyaEntity
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode
from .base import IntegerTypeData, TuyaEntity
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, DPType
@dataclass
@ -79,7 +79,7 @@ async def async_setup_entry(
class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity):
"""Tuya (de)humidifier Device."""
_set_humidity_type: IntegerTypeData | None = None
_set_humidity: IntegerTypeData | None = None
_switch_dpcode: DPCode | None = None
entity_description: TuyaHumidifierEntityDescription
@ -96,30 +96,24 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity):
self._attr_supported_features = 0
# Determine main switch DPCode
possible_dpcodes = description.dpcode or description.key
if isinstance(possible_dpcodes, DPCode) and possible_dpcodes in device.function:
self._switch_dpcode = possible_dpcodes
elif isinstance(possible_dpcodes, tuple):
self._switch_dpcode = next(
(dpcode for dpcode in possible_dpcodes if dpcode in device.function),
None,
)
self._switch_dpcode = self.find_dpcode(
description.dpcode or DPCode(description.key), prefer_function=True
)
# Determine humidity parameters
if description.humidity in device.status_range:
type_data = IntegerTypeData.from_json(
device.status_range[description.humidity].values
)
self._set_humidity_type = type_data
self._attr_min_humidity = int(type_data.min_scaled)
self._attr_max_humidity = int(type_data.max_scaled)
if int_type := self.find_dpcode(
description.humidity, dptype=DPType.INTEGER, prefer_function=True
):
self._set_humiditye = int_type
self._attr_min_humidity = int(int_type.min_scaled)
self._attr_max_humidity = int(int_type.max_scaled)
# Determine mode support and provided modes
if DPCode.MODE in device.function:
if enum_type := self.find_dpcode(
DPCode.MODE, dptype=DPType.ENUM, prefer_function=True
):
self._attr_supported_features |= SUPPORT_MODES
self._attr_available_modes = EnumTypeData.from_json(
device.function[DPCode.MODE].values
).range
self._attr_available_modes = enum_type.range
@property
def is_on(self) -> bool:
@ -136,14 +130,14 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity):
@property
def target_humidity(self) -> int | None:
"""Return the humidity we try to reach."""
if self._set_humidity_type is None:
if self._set_humidity is None:
return None
humidity = self.device.status.get(self.entity_description.humidity)
humidity = self.device.status.get(self._set_humidity.dpcode)
if humidity is None:
return None
return round(self._set_humidity_type.scale_value(humidity))
return round(self._set_humidity.scale_value(humidity))
def turn_on(self, **kwargs):
"""Turn the device on."""
@ -155,7 +149,7 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity):
def set_humidity(self, humidity):
"""Set new target humidity."""
if self._set_humidity_type is None:
if self._set_humidity is None:
raise RuntimeError(
"Cannot set humidity, device doesn't provide methods to set it"
)
@ -163,8 +157,8 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity):
self._send_command(
[
{
"code": self.entity_description.humidity,
"value": self._set_humidity_type.scale_value_back(humidity),
"code": self._set_humidity.dpcode,
"value": self._set_humidity.scale_value_back(humidity),
}
]
)

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from dataclasses import dataclass
import json
from typing import Any
from typing import Any, cast
from tuya_iot import TuyaDevice, TuyaDeviceManager
@ -26,7 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import HomeAssistantTuyaData
from .base import IntegerTypeData, TuyaEntity
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, WorkMode
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, DPType, WorkMode
from .util import remap_value
@ -40,15 +40,15 @@ class ColorTypeData:
DEFAULT_COLOR_TYPE_DATA = ColorTypeData(
h_type=IntegerTypeData(min=1, scale=0, max=360, step=1),
s_type=IntegerTypeData(min=1, scale=0, max=255, step=1),
v_type=IntegerTypeData(min=1, scale=0, max=255, step=1),
h_type=IntegerTypeData(DPCode.COLOUR_DATA_HSV, min=1, scale=0, max=360, step=1),
s_type=IntegerTypeData(DPCode.COLOUR_DATA_HSV, min=1, scale=0, max=255, step=1),
v_type=IntegerTypeData(DPCode.COLOUR_DATA_HSV, min=1, scale=0, max=255, step=1),
)
DEFAULT_COLOR_TYPE_DATA_V2 = ColorTypeData(
h_type=IntegerTypeData(min=1, scale=0, max=360, step=1),
s_type=IntegerTypeData(min=1, scale=0, max=1000, step=1),
v_type=IntegerTypeData(min=1, scale=0, max=1000, step=1),
h_type=IntegerTypeData(DPCode.COLOUR_DATA_HSV, min=1, scale=0, max=360, step=1),
s_type=IntegerTypeData(DPCode.COLOUR_DATA_HSV, min=1, scale=0, max=1000, step=1),
v_type=IntegerTypeData(DPCode.COLOUR_DATA_HSV, min=1, scale=0, max=1000, step=1),
)
@ -323,15 +323,14 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
"""Tuya light device."""
entity_description: TuyaLightEntityDescription
_brightness_dpcode: DPCode | None = None
_brightness_max_type: IntegerTypeData | None = None
_brightness_min_type: IntegerTypeData | None = None
_brightness_type: IntegerTypeData | None = None
_brightness_max: IntegerTypeData | None = None
_brightness_min: IntegerTypeData | None = None
_brightness: IntegerTypeData | None = None
_color_data_dpcode: DPCode | None = None
_color_data_type: ColorTypeData | None = None
_color_mode_dpcode: DPCode | None = None
_color_temp_dpcode: DPCode | None = None
_color_temp_type: IntegerTypeData | None = None
_color_mode: DPCode | None = None
_color_temp: IntegerTypeData | None = None
def __init__(
self,
@ -345,106 +344,51 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
self._attr_unique_id = f"{super().unique_id}{description.key}"
self._attr_supported_color_modes = {COLOR_MODE_ONOFF}
# Determine brightness DPCodes
if (
isinstance(description.brightness, DPCode)
and description.brightness in device.function
):
self._brightness_dpcode = description.brightness
elif isinstance(description.brightness, tuple):
self._brightness_dpcode = next(
(
dpcode
for dpcode in description.brightness
if dpcode in device.function
),
None,
)
# Determine DPCodes
self._color_mode_dpcode = self.find_dpcode(
description.color_mode, prefer_function=True
)
# Determine color mode DPCode
if (
description.color_mode is not None
and description.color_mode in device.function
if int_type := self.find_dpcode(
description.brightness, dptype=DPType.INTEGER, prefer_function=True
):
self._color_mode_dpcode = description.color_mode
# Determine DPCodes for color temperature
if (
isinstance(description.color_temp, DPCode)
and description.color_temp in device.function
):
self._color_temp_dpcode = description.color_temp
elif isinstance(description.color_temp, tuple):
self._color_temp_dpcode = next(
(
dpcode
for dpcode in description.color_temp
if dpcode in device.function
),
None,
)
# Determine DPCodes for color data
if (
isinstance(description.color_data, DPCode)
and description.color_data in device.function
):
self._color_data_dpcode = description.color_data
elif isinstance(description.color_data, tuple):
self._color_data_dpcode = next(
(
dpcode
for dpcode in description.color_data
if dpcode in device.function
),
None,
)
# Update internals based on found brightness dpcode
if self._brightness_dpcode:
self._brightness = int_type
self._attr_supported_color_modes.add(COLOR_MODE_BRIGHTNESS)
self._brightness_type = IntegerTypeData.from_json(
device.function[self._brightness_dpcode].values
self._brightness_max = self.find_dpcode(
description.brightness_max, dptype=DPType.INTEGER
)
self._brightness_min = self.find_dpcode(
description.brightness_min, dptype=DPType.INTEGER
)
# Check if min/max capable
if (
description.brightness_max is not None
and description.brightness_min is not None
and description.brightness_max in device.function
and description.brightness_min in device.function
):
self._brightness_max_type = IntegerTypeData.from_json(
device.function[description.brightness_max].values
)
self._brightness_min_type = IntegerTypeData.from_json(
device.function[description.brightness_min].values
)
# Update internals based on found color temperature dpcode
if self._color_temp_dpcode:
if int_type := self.find_dpcode(
description.color_temp, dptype=DPType.INTEGER, prefer_function=True
):
self._color_temp = int_type
self._attr_supported_color_modes.add(COLOR_MODE_COLOR_TEMP)
self._color_temp_type = IntegerTypeData.from_json(
device.function[self._color_temp_dpcode].values
)
# Update internals based on found color data dpcode
if self._color_data_dpcode:
if (
dpcode := self.find_dpcode(description.color_data, prefer_function=True)
) and self.get_dptype(dpcode) == DPType.JSON:
self._color_data_dpcode = dpcode
self._attr_supported_color_modes.add(COLOR_MODE_HS)
if dpcode in self.device.function:
values = cast(str, self.device.function[dpcode].values)
else:
values = self.device.status_range[dpcode].values
# Fetch color data type information
if function_data := json.loads(
self.device.function[self._color_data_dpcode].values
):
if function_data := json.loads(values):
self._color_data_type = ColorTypeData(
h_type=IntegerTypeData(**function_data["h"]),
s_type=IntegerTypeData(**function_data["s"]),
v_type=IntegerTypeData(**function_data["v"]),
h_type=IntegerTypeData(dpcode, **function_data["h"]),
s_type=IntegerTypeData(dpcode, **function_data["s"]),
v_type=IntegerTypeData(dpcode, **function_data["v"]),
)
else:
# If no type is found, use a default one
self._color_data_type = self.entity_description.default_color_type
if self._color_data_dpcode == DPCode.COLOUR_DATA_V2 or (
self._brightness_type and self._brightness_type.max > 255
self._brightness and self._brightness.max > 255
):
self._color_data_type = DEFAULT_COLOR_TYPE_DATA_V2
@ -457,7 +401,7 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
"""Turn on or control the light."""
commands = [{"code": self.entity_description.key, "value": True}]
if self._color_temp_type and ATTR_COLOR_TEMP in kwargs:
if self._color_temp and ATTR_COLOR_TEMP in kwargs:
if self._color_mode_dpcode:
commands += [
{
@ -468,9 +412,9 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
commands += [
{
"code": self._color_temp_dpcode,
"code": self._color_temp.dpcode,
"value": round(
self._color_temp_type.remap_value_from(
self._color_temp.remap_value_from(
kwargs[ATTR_COLOR_TEMP],
self.min_mireds,
self.max_mireds,
@ -525,37 +469,31 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
if (
ATTR_BRIGHTNESS in kwargs
and self.color_mode != COLOR_MODE_HS
and self._brightness_type
and self._brightness
):
brightness = kwargs[ATTR_BRIGHTNESS]
# If there is a min/max value, the brightness is actually limited.
# Meaning it is actually not on a 0-255 scale.
if (
self._brightness_max_type is not None
and self._brightness_min_type is not None
and self.entity_description.brightness_max is not None
and self.entity_description.brightness_min is not None
self._brightness_max is not None
and self._brightness_min is not None
and (
brightness_max := self.device.status.get(
self.entity_description.brightness_max
self._brightness_max.dpcode
)
)
is not None
and (
brightness_min := self.device.status.get(
self.entity_description.brightness_min
self._brightness_min.dpcode
)
)
is not None
):
# Remap values onto our scale
brightness_max = self._brightness_max_type.remap_value_to(
brightness_max
)
brightness_min = self._brightness_min_type.remap_value_to(
brightness_min
)
brightness_max = self._brightness_max.remap_value_to(brightness_max)
brightness_min = self._brightness_min.remap_value_to(brightness_min)
# Remap the brightness value from their min-max to our 0-255 scale
brightness = remap_value(
@ -566,8 +504,8 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
commands += [
{
"code": self._brightness_dpcode,
"value": round(self._brightness_type.remap_value_from(brightness)),
"code": self._brightness.dpcode,
"value": round(self._brightness.remap_value_from(brightness)),
},
]
@ -584,39 +522,29 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
if self.color_mode == COLOR_MODE_HS and (color_data := self._get_color_data()):
return color_data.brightness
if not self._brightness_dpcode or not self._brightness_type:
if not self._brightness:
return None
brightness = self.device.status.get(self._brightness_dpcode)
brightness = self.device.status.get(self._brightness.dpcode)
if brightness is None:
return None
# Remap value to our scale
brightness = self._brightness_type.remap_value_to(brightness)
brightness = self._brightness.remap_value_to(brightness)
# If there is a min/max value, the brightness is actually limited.
# Meaning it is actually not on a 0-255 scale.
if (
self._brightness_max_type is not None
and self._brightness_min_type is not None
and self.entity_description.brightness_max is not None
and self.entity_description.brightness_min is not None
and (
brightness_max := self.device.status.get(
self.entity_description.brightness_max
)
)
self._brightness_max is not None
and self._brightness_min is not None
and (brightness_max := self.device.status.get(self._brightness_max.dpcode))
is not None
and (
brightness_min := self.device.status.get(
self.entity_description.brightness_min
)
)
and (brightness_min := self.device.status.get(self._brightness_min.dpcode))
is not None
):
# Remap values onto our scale
brightness_max = self._brightness_max_type.remap_value_to(brightness_max)
brightness_min = self._brightness_min_type.remap_value_to(brightness_min)
brightness_max = self._brightness_max.remap_value_to(brightness_max)
brightness_min = self._brightness_min.remap_value_to(brightness_min)
# Remap the brightness value from their min-max to our 0-255 scale
brightness = remap_value(
@ -630,15 +558,15 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
@property
def color_temp(self) -> int | None:
"""Return the color_temp of the light."""
if not self._color_temp_dpcode or not self._color_temp_type:
if not self._color_temp:
return None
temperature = self.device.status.get(self._color_temp_dpcode)
temperature = self.device.status.get(self._color_temp.dpcode)
if temperature is None:
return None
return round(
self._color_temp_type.remap_value_to(
self._color_temp.remap_value_to(
temperature, self.min_mireds, self.max_mireds, reverse=True
)
)
@ -662,9 +590,9 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
and self.device.status.get(self._color_mode_dpcode) != WorkMode.WHITE
):
return COLOR_MODE_HS
if self._color_temp_dpcode:
if self._color_temp:
return COLOR_MODE_COLOR_TEMP
if self._brightness_dpcode:
if self._brightness:
return COLOR_MODE_BRIGHTNESS
return COLOR_MODE_ONOFF

View File

@ -1,10 +1,7 @@
"""Support for Tuya number."""
from __future__ import annotations
from typing import cast
from tuya_iot import TuyaDevice, TuyaDeviceManager
from tuya_iot.device import TuyaDeviceStatusRange
from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.config_entries import ConfigEntry
@ -15,7 +12,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import HomeAssistantTuyaData
from .base import IntegerTypeData, TuyaEntity
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, DPType
# All descriptions can be found here. Mostly the Integer data types in the
# default instructions set of each category end up being a number.
@ -280,8 +277,7 @@ async def async_setup_entry(
class TuyaNumberEntity(TuyaEntity, NumberEntity):
"""Tuya Number Entity."""
_status_range: TuyaDeviceStatusRange | None = None
_type_data: IntegerTypeData | None = None
_number: IntegerTypeData | None = None
def __init__(
self,
@ -294,45 +290,39 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity):
self.entity_description = description
self._attr_unique_id = f"{super().unique_id}{description.key}"
if status_range := device.status_range.get(description.key):
self._status_range = cast(TuyaDeviceStatusRange, status_range)
# Extract type data from integer status range,
# and determine unit of measurement
if self._status_range.type == "Integer":
self._type_data = IntegerTypeData.from_json(self._status_range.values)
self._attr_max_value = self._type_data.max_scaled
self._attr_min_value = self._type_data.min_scaled
self._attr_step = self._type_data.step_scaled
if description.unit_of_measurement is None:
self._attr_unit_of_measurement = self._type_data.unit
if int_type := self.find_dpcode(
description.key, dptype=DPType.INTEGER, prefer_function=True
):
self._number = int_type
self._attr_max_value = self._number.max_scaled
self._attr_min_value = self._number.min_scaled
self._attr_step = self._number.step_scaled
if description.unit_of_measurement is None:
self._attr_unit_of_measurement = self._number.unit
@property
def value(self) -> float | None:
"""Return the entity value to represent the entity state."""
# Unknown or unsupported data type
if self._status_range is None or self._status_range.type != "Integer":
if self._number is None:
return None
# Raw value
value = self.device.status.get(self.entity_description.key)
if not (value := self.device.status.get(self.entity_description.key)):
return None
# Scale integer/float value
if value is not None and isinstance(self._type_data, IntegerTypeData):
return self._type_data.scale_value(value)
return None
return self._number.scale_value(value)
def set_value(self, value: float) -> None:
"""Set new value."""
if self._type_data is None:
if self._number is None:
raise RuntimeError("Cannot set value, device doesn't provide type data")
self._send_command(
[
{
"code": self.entity_description.key,
"value": self._type_data.scale_value_back(value),
"value": self._number.scale_value_back(value),
}
]
)

View File

@ -1,10 +1,7 @@
"""Support for Tuya select."""
from __future__ import annotations
from typing import cast
from tuya_iot import TuyaDevice, TuyaDeviceManager
from tuya_iot.device import TuyaDeviceStatusRange
from homeassistant.components.select import SelectEntity, SelectEntityDescription
from homeassistant.config_entries import ConfigEntry
@ -14,8 +11,8 @@ from homeassistant.helpers.entity import EntityCategory
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import HomeAssistantTuyaData
from .base import EnumTypeData, TuyaEntity
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, TuyaDeviceClass
from .base import TuyaEntity
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, DPType, TuyaDeviceClass
# All descriptions can be found here. Mostly the Enum data types in the
# default instructions set of each category end up being a select.
@ -287,13 +284,10 @@ class TuyaSelectEntity(TuyaEntity, SelectEntity):
self._attr_unique_id = f"{super().unique_id}{description.key}"
self._attr_opions: list[str] = []
if status_range := device.status_range.get(description.key):
self._status_range = cast(TuyaDeviceStatusRange, status_range)
# Extract type data from enum status range,
if self._status_range.type == "Enum":
type_data = EnumTypeData.from_json(self._status_range.values)
self._attr_options = type_data.range
if enum_type := self.find_dpcode(
description.key, dptype=DPType.ENUM, prefer_function=True
):
self._attr_options = enum_type.range
@property
def current_option(self) -> str | None:

View File

@ -2,7 +2,6 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import cast
from tuya_iot import TuyaDevice, TuyaDeviceManager
from tuya_iot.device import TuyaDeviceStatusRange
@ -33,6 +32,7 @@ from .const import (
DOMAIN,
TUYA_DISCOVERY_NEW,
DPCode,
DPType,
TuyaDeviceClass,
UnitOfMeasurement,
)
@ -776,6 +776,7 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
entity_description: TuyaSensorEntityDescription
_status_range: TuyaDeviceStatusRange | None = None
_type: DPType | None = None
_type_data: IntegerTypeData | EnumTypeData | None = None
_uom: UnitOfMeasurement | None = None
@ -792,19 +793,18 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
f"{super().unique_id}{description.key}{description.subkey or ''}"
)
if status_range := device.status_range.get(description.key):
self._status_range = cast(TuyaDeviceStatusRange, status_range)
# Extract type data from integer status range,
# and determine unit of measurement
if self._status_range.type == "Integer":
self._type_data = IntegerTypeData.from_json(self._status_range.values)
if description.native_unit_of_measurement is None:
self._attr_native_unit_of_measurement = self._type_data.unit
# Extract type data from enum status range
elif self._status_range.type == "Enum":
self._type_data = EnumTypeData.from_json(self._status_range.values)
if int_type := self.find_dpcode(description.key, dptype=DPType.INTEGER):
self._type_data = int_type
self._type = DPType.INTEGER
if description.native_unit_of_measurement is None:
self._attr_native_unit_of_measurement = int_type.unit
elif enum_type := self.find_dpcode(
description.key, dptype=DPType.ENUM, prefer_function=True
):
self._type_data = enum_type
self._type = DPType.ENUM
else:
self._type = self.get_dptype(DPCode(description.key))
# Logic to ensure the set device class and API received Unit Of Measurement
# match Home Assistants requirements.
@ -841,13 +841,13 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
@property
def native_value(self) -> StateType:
"""Return the value reported by the sensor."""
# Unknown or unsupported data type
if self._status_range is None or self._status_range.type not in (
"Integer",
"String",
"Enum",
"Json",
"Raw",
# Only continue if data type is known
if self._type not in (
DPType.INTEGER,
DPType.STRING,
DPType.ENUM,
DPType.JSON,
DPType.RAW,
):
return None
@ -871,13 +871,13 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
return None
# Get subkey value from Json string.
if self._status_range.type == "Json":
if self._type is DPType.JSON:
if self.entity_description.subkey is None:
return None
values = ElectricityTypeData.from_json(value)
return getattr(values, self.entity_description.subkey)
if self._status_range.type == "Raw":
if self._type is DPType.RAW:
if self.entity_description.subkey is None:
return None
values = ElectricityTypeData.from_raw(value)

View File

@ -30,7 +30,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import HomeAssistantTuyaData
from .base import EnumTypeData, IntegerTypeData, TuyaEntity
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, DPType
TUYA_STATUS_TO_HA = {
"charge_done": STATE_DOCKED,
@ -81,48 +81,50 @@ async def async_setup_entry(
class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity):
"""Tuya Vacuum Device."""
_fan_speed_type: EnumTypeData | None = None
_battery_level_type: IntegerTypeData | None = None
_fan_speed: EnumTypeData | None = None
_battery_level: IntegerTypeData | None = None
_supported_features = 0
def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None:
"""Init Tuya vacuum."""
super().__init__(device, device_manager)
if DPCode.PAUSE in self.device.status:
if self.find_dpcode(DPCode.PAUSE, prefer_function=True):
self._supported_features |= SUPPORT_PAUSE
if DPCode.SWITCH_CHARGE in self.device.status:
if self.find_dpcode(DPCode.SWITCH_CHARGE, prefer_function=True):
self._supported_features |= SUPPORT_RETURN_HOME
if DPCode.SEEK in self.device.status:
if self.find_dpcode(DPCode.SEEK, prefer_function=True):
self._supported_features |= SUPPORT_LOCATE
if DPCode.STATUS in self.device.status:
if self.find_dpcode(DPCode.STATUS, prefer_function=True):
self._supported_features |= SUPPORT_STATE | SUPPORT_STATUS
if DPCode.POWER in self.device.status:
if self.find_dpcode(DPCode.POWER, prefer_function=True):
self._supported_features |= SUPPORT_TURN_ON | SUPPORT_TURN_OFF
if DPCode.POWER_GO in self.device.status:
if self.find_dpcode(DPCode.POWER_GO, prefer_function=True):
self._supported_features |= SUPPORT_STOP | SUPPORT_START
if function := device.function.get(DPCode.SUCTION):
if enum_type := self.find_dpcode(
DPCode.SUCTION, dptype=DPType.ENUM, prefer_function=True
):
self._supported_features |= SUPPORT_FAN_SPEED
self._fan_speed_type = EnumTypeData.from_json(function.values)
self._fan_speed = enum_type
if status_range := device.status_range.get(DPCode.ELECTRICITY_LEFT):
if int_type := self.find_dpcode(DPCode.SUCTION, dptype=DPType.INTEGER):
self._supported_features |= SUPPORT_BATTERY
self._battery_level_type = IntegerTypeData.from_json(status_range.values)
self._battery_level = int_type
@property
def battery_level(self) -> int | None:
"""Return Tuya device state."""
if self._battery_level_type is None or not (
if self._battery_level is None or not (
status := self.device.status.get(DPCode.ELECTRICITY_LEFT)
):
return None
return round(self._battery_level_type.scale_value(status))
return round(self._battery_level.scale_value(status))
@property
def fan_speed(self) -> str | None:
@ -132,9 +134,9 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity):
@property
def fan_speed_list(self) -> list[str]:
"""Get the list of available fan speed steps of the vacuum cleaner."""
if self._fan_speed_type is None:
if self._fan_speed is None:
return []
return self._fan_speed_type.range
return self._fan_speed.range
@property
def state(self) -> str | None: