Refactor Tuya DPCode and data type handling (#64707)
parent
a5fb60fd3a
commit
db979fef6c
|
@ -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}])
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
),
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
]
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
]
|
||||
)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
Loading…
Reference in New Issue