Refactor Tuya DPCode and data type handling (#64707)
This commit is contained in:
parent
a5fb60fd3a
commit
db979fef6c
11 changed files with 429 additions and 500 deletions
|
@ -23,8 +23,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import HomeAssistantTuyaData
|
from . import HomeAssistantTuyaData
|
||||||
from .base import EnumTypeData, TuyaEntity
|
from .base import TuyaEntity
|
||||||
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode
|
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, DPType
|
||||||
|
|
||||||
|
|
||||||
class Mode(StrEnum):
|
class Mode(StrEnum):
|
||||||
|
@ -105,36 +105,39 @@ class TuyaAlarmEntity(TuyaEntity, AlarmControlPanelEntity):
|
||||||
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
||||||
|
|
||||||
# Determine supported modes
|
# Determine supported modes
|
||||||
supported_mode = EnumTypeData.from_json(
|
if supported_modes := self.find_dpcode(
|
||||||
device.function[DPCode.MASTER_MODE].values
|
description.key, dptype=DPType.ENUM, prefer_function=True
|
||||||
).range
|
):
|
||||||
|
if Mode.HOME in supported_modes.range:
|
||||||
|
self._attr_supported_features |= SUPPORT_ALARM_ARM_HOME
|
||||||
|
|
||||||
if Mode.HOME in supported_mode:
|
if Mode.ARM in supported_modes.range:
|
||||||
self._attr_supported_features |= SUPPORT_ALARM_ARM_HOME
|
self._attr_supported_features |= SUPPORT_ALARM_ARM_AWAY
|
||||||
|
|
||||||
if Mode.ARM in supported_mode:
|
if Mode.SOS in supported_modes.range:
|
||||||
self._attr_supported_features |= SUPPORT_ALARM_ARM_AWAY
|
self._attr_supported_features |= SUPPORT_ALARM_TRIGGER
|
||||||
|
|
||||||
if Mode.SOS in supported_mode:
|
|
||||||
self._attr_supported_features |= SUPPORT_ALARM_TRIGGER
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the device."""
|
"""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:
|
def alarm_disarm(self, code: str | None = None) -> None:
|
||||||
"""Send Disarm command."""
|
"""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:
|
def alarm_arm_home(self, code: str | None = None) -> None:
|
||||||
"""Send Home command."""
|
"""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:
|
def alarm_arm_away(self, code: str | None = None) -> None:
|
||||||
"""Send Arm command."""
|
"""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:
|
def alarm_trigger(self, code: str | None = None) -> None:
|
||||||
"""Send SOS command."""
|
"""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
|
from dataclasses import dataclass
|
||||||
import json
|
import json
|
||||||
import struct
|
import struct
|
||||||
from typing import Any
|
from typing import Any, Literal, overload
|
||||||
|
|
||||||
from tuya_iot import TuyaDevice, TuyaDeviceManager
|
from tuya_iot import TuyaDevice, TuyaDeviceManager
|
||||||
|
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
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
|
from .util import remap_value
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@ from .util import remap_value
|
||||||
class IntegerTypeData:
|
class IntegerTypeData:
|
||||||
"""Integer Type Data."""
|
"""Integer Type Data."""
|
||||||
|
|
||||||
|
dpcode: DPCode
|
||||||
min: int
|
min: int
|
||||||
max: int
|
max: int
|
||||||
scale: float
|
scale: float
|
||||||
|
@ -71,21 +72,22 @@ class IntegerTypeData:
|
||||||
return remap_value(value, from_min, from_max, self.min, self.max, reverse)
|
return remap_value(value, from_min, from_max, self.min, self.max, reverse)
|
||||||
|
|
||||||
@classmethod
|
@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."""
|
"""Load JSON string and return a IntegerTypeData object."""
|
||||||
return cls(**json.loads(data))
|
return cls(dpcode, **json.loads(data))
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class EnumTypeData:
|
class EnumTypeData:
|
||||||
"""Enum Type Data."""
|
"""Enum Type Data."""
|
||||||
|
|
||||||
|
dpcode: DPCode
|
||||||
range: list[str]
|
range: list[str]
|
||||||
|
|
||||||
@classmethod
|
@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."""
|
"""Load JSON string and return a EnumTypeData object."""
|
||||||
return cls(**json.loads(data))
|
return cls(dpcode, **json.loads(data))
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -149,6 +151,101 @@ class TuyaEntity(Entity):
|
||||||
"""Return if the device is available."""
|
"""Return if the device is available."""
|
||||||
return self.device.online
|
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:
|
async def async_added_to_hass(self) -> None:
|
||||||
"""Call when entity is added to hass."""
|
"""Call when entity is added to hass."""
|
||||||
self.async_on_remove(
|
self.async_on_remove(
|
||||||
|
|
|
@ -31,8 +31,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import HomeAssistantTuyaData
|
from . import HomeAssistantTuyaData
|
||||||
from .base import EnumTypeData, IntegerTypeData, TuyaEntity
|
from .base import IntegerTypeData, TuyaEntity
|
||||||
from .const import DOMAIN, LOGGER, TUYA_DISCOVERY_NEW, DPCode
|
from .const import DOMAIN, LOGGER, TUYA_DISCOVERY_NEW, DPCode, DPType
|
||||||
|
|
||||||
TUYA_HVAC_TO_HA = {
|
TUYA_HVAC_TO_HA = {
|
||||||
"auto": HVAC_MODE_HEAT_COOL,
|
"auto": HVAC_MODE_HEAT_COOL,
|
||||||
|
@ -114,18 +114,14 @@ async def async_setup_entry(
|
||||||
class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
||||||
"""Tuya Climate Device."""
|
"""Tuya Climate Device."""
|
||||||
|
|
||||||
_current_humidity_dpcode: DPCode | None = None
|
_current_humidity: IntegerTypeData | None = None
|
||||||
_current_humidity_type: IntegerTypeData | None = None
|
_current_temperature: IntegerTypeData | None = None
|
||||||
_current_temperature_dpcode: DPCode | None = None
|
|
||||||
_current_temperature_type: IntegerTypeData | None = None
|
|
||||||
_hvac_to_tuya: dict[str, str]
|
_hvac_to_tuya: dict[str, str]
|
||||||
_set_humidity_dpcode: DPCode | None = None
|
_set_humidity: IntegerTypeData | None = None
|
||||||
_set_humidity_type: IntegerTypeData | None = None
|
_set_temperature: IntegerTypeData | None = None
|
||||||
_set_temperature_dpcode: DPCode | None = None
|
|
||||||
_set_temperature_type: IntegerTypeData | None = None
|
|
||||||
entity_description: TuyaClimateEntityDescription
|
entity_description: TuyaClimateEntityDescription
|
||||||
|
|
||||||
def __init__( # noqa: C901
|
def __init__(
|
||||||
self,
|
self,
|
||||||
device: TuyaDevice,
|
device: TuyaDevice,
|
||||||
device_manager: TuyaDeviceManager,
|
device_manager: TuyaDeviceManager,
|
||||||
|
@ -140,160 +136,117 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
||||||
|
|
||||||
# If both temperature values for celsius and fahrenheit are present,
|
# If both temperature values for celsius and fahrenheit are present,
|
||||||
# use whatever the device is set to, with a fallback to celsius.
|
# use whatever the device is set to, with a fallback to celsius.
|
||||||
|
prefered_temperature_unit = None
|
||||||
if all(
|
if all(
|
||||||
dpcode in device.status
|
dpcode in device.status
|
||||||
for dpcode in (DPCode.TEMP_CURRENT, DPCode.TEMP_CURRENT_F)
|
for dpcode in (DPCode.TEMP_CURRENT, DPCode.TEMP_CURRENT_F)
|
||||||
) or all(
|
) or all(
|
||||||
dpcode in device.status for dpcode in (DPCode.TEMP_SET, DPCode.TEMP_SET_F)
|
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(
|
if any(
|
||||||
"f" in device.status[dpcode].lower()
|
"f" in device.status[dpcode].lower()
|
||||||
for dpcode in (DPCode.C_F, DPCode.TEMP_UNIT_CONVERT)
|
for dpcode in (DPCode.C_F, DPCode.TEMP_UNIT_CONVERT)
|
||||||
if isinstance(device.status.get(dpcode), str)
|
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.
|
# Default to Celsius
|
||||||
elif any(
|
self._attr_temperature_unit = TEMP_CELSIUS
|
||||||
dpcode in device.status for dpcode in (DPCode.TEMP_CURRENT, DPCode.TEMP_SET)
|
|
||||||
):
|
|
||||||
self._attr_temperature_unit = TEMP_CELSIUS
|
|
||||||
|
|
||||||
# If any DPCode handling fahrenheit is present, use celsius.
|
# Figure out current temperature, use preferred unit or what is available
|
||||||
elif any(
|
celsius_type = self.find_dpcode(DPCode.TEMP_CURRENT, dptype=DPType.INTEGER)
|
||||||
dpcode in device.status
|
farhenheit_type = self.find_dpcode(DPCode.TEMP_CURRENT_F, dptype=DPType.INTEGER)
|
||||||
for dpcode in (DPCode.TEMP_CURRENT_F, DPCode.TEMP_SET_F)
|
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._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
|
# Figure out setting temperature, use preferred unit or what is available
|
||||||
if all(
|
celsius_type = self.find_dpcode(
|
||||||
dpcode in device.status for dpcode in (DPCode.TEMP_SET, DPCode.TEMP_SET_F)
|
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
|
self._set_temperature = farhenheit_type
|
||||||
if self._attr_temperature_unit == TEMP_FAHRENHEIT:
|
elif celsius_type:
|
||||||
self._set_temperature_dpcode = DPCode.TEMP_SET_F
|
self._set_temperature = celsius_type
|
||||||
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
|
|
||||||
|
|
||||||
# Get integer type data for the dpcode to set temperature, use
|
# Get integer type data for the dpcode to set temperature, use
|
||||||
# it to define min, max & step temperatures
|
# it to define min, max & step temperatures
|
||||||
if (
|
if self._set_temperature:
|
||||||
self._set_temperature_dpcode
|
|
||||||
and self._set_temperature_dpcode in device.function
|
|
||||||
):
|
|
||||||
type_data = IntegerTypeData.from_json(
|
|
||||||
device.function[self._set_temperature_dpcode].values
|
|
||||||
)
|
|
||||||
self._attr_supported_features |= SUPPORT_TARGET_TEMPERATURE
|
self._attr_supported_features |= SUPPORT_TARGET_TEMPERATURE
|
||||||
self._set_temperature_type = type_data
|
self._attr_max_temp = self._set_temperature.max_scaled
|
||||||
self._attr_max_temp = type_data.max_scaled
|
self._attr_min_temp = self._set_temperature.min_scaled
|
||||||
self._attr_min_temp = type_data.min_scaled
|
self._attr_target_temperature_step = self._set_temperature.step_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
|
|
||||||
)
|
|
||||||
|
|
||||||
# Determine HVAC modes
|
# Determine HVAC modes
|
||||||
self._attr_hvac_modes = []
|
self._attr_hvac_modes = []
|
||||||
self._hvac_to_tuya = {}
|
self._hvac_to_tuya = {}
|
||||||
if DPCode.MODE in device.function:
|
if enum_type := self.find_dpcode(
|
||||||
data_type = EnumTypeData.from_json(device.function[DPCode.MODE].values)
|
DPCode.MODE, dptype=DPType.ENUM, prefer_function=True
|
||||||
|
):
|
||||||
self._attr_hvac_modes = [HVAC_MODE_OFF]
|
self._attr_hvac_modes = [HVAC_MODE_OFF]
|
||||||
for tuya_mode, ha_mode in TUYA_HVAC_TO_HA.items():
|
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._hvac_to_tuya[ha_mode] = tuya_mode
|
||||||
self._attr_hvac_modes.append(ha_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 = [
|
self._attr_hvac_modes = [
|
||||||
HVAC_MODE_OFF,
|
HVAC_MODE_OFF,
|
||||||
description.switch_only_hvac_mode,
|
description.switch_only_hvac_mode,
|
||||||
]
|
]
|
||||||
|
|
||||||
# Determine dpcode to use for setting the humidity
|
# 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._attr_supported_features |= SUPPORT_TARGET_HUMIDITY
|
||||||
self._set_humidity_dpcode = DPCode.HUMIDITY_SET
|
self._set_humidity = int_type
|
||||||
type_data = IntegerTypeData.from_json(
|
self._attr_min_humidity = int(int_type.min_scaled)
|
||||||
device.function[DPCode.HUMIDITY_SET].values
|
self._attr_max_humidity = int(int_type.max_scaled)
|
||||||
)
|
|
||||||
self._set_humidity_type = type_data
|
|
||||||
self._attr_min_humidity = int(type_data.min_scaled)
|
|
||||||
self._attr_max_humidity = int(type_data.max_scaled)
|
|
||||||
|
|
||||||
# Determine dpcode to use for getting the current humidity
|
# Determine dpcode to use for getting the current humidity
|
||||||
if (
|
self._current_humidity = self.find_dpcode(
|
||||||
DPCode.HUMIDITY_CURRENT in device.status
|
DPCode.HUMIDITY_CURRENT, dptype=DPType.INTEGER
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
# Determine fan modes
|
# Determine fan modes
|
||||||
if (
|
if enum_type := self.find_dpcode(
|
||||||
DPCode.FAN_SPEED_ENUM in device.status
|
DPCode.FAN_SPEED_ENUM, dptype=DPType.ENUM, prefer_function=True
|
||||||
and DPCode.FAN_SPEED_ENUM in device.function
|
|
||||||
):
|
):
|
||||||
self._attr_supported_features |= SUPPORT_FAN_MODE
|
self._attr_supported_features |= SUPPORT_FAN_MODE
|
||||||
self._attr_fan_modes = EnumTypeData.from_json(
|
self._attr_fan_modes = enum_type.range
|
||||||
device.status_range[DPCode.FAN_SPEED_ENUM].values
|
|
||||||
).range
|
|
||||||
|
|
||||||
# Determine swing modes
|
# Determine swing modes
|
||||||
if any(
|
if self.find_dpcode(
|
||||||
dpcode in device.function
|
(
|
||||||
for dpcode in (
|
|
||||||
DPCode.SHAKE,
|
DPCode.SHAKE,
|
||||||
DPCode.SWING,
|
DPCode.SWING,
|
||||||
DPCode.SWITCH_HORIZONTAL,
|
DPCode.SWITCH_HORIZONTAL,
|
||||||
DPCode.SWITCH_VERTICAL,
|
DPCode.SWITCH_VERTICAL,
|
||||||
)
|
),
|
||||||
|
prefer_function=True,
|
||||||
):
|
):
|
||||||
self._attr_supported_features |= SUPPORT_SWING_MODE
|
self._attr_supported_features |= SUPPORT_SWING_MODE
|
||||||
self._attr_swing_modes = [SWING_OFF]
|
self._attr_swing_modes = [SWING_OFF]
|
||||||
if any(
|
if self.find_dpcode((DPCode.SHAKE, DPCode.SWING), prefer_function=True):
|
||||||
dpcode in device.function for dpcode in (DPCode.SHAKE, DPCode.SWING)
|
|
||||||
):
|
|
||||||
self._attr_swing_modes.append(SWING_ON)
|
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)
|
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)
|
self._attr_swing_modes.append(SWING_VERTICAL)
|
||||||
|
|
||||||
async def async_added_to_hass(self) -> None:
|
async def async_added_to_hass(self) -> None:
|
||||||
|
@ -301,9 +254,10 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
||||||
await super().async_added_to_hass()
|
await super().async_added_to_hass()
|
||||||
|
|
||||||
# Log unknown modes
|
# Log unknown modes
|
||||||
if DPCode.MODE in self.device.function:
|
if enum_type := self.find_dpcode(
|
||||||
data_type = EnumTypeData.from_json(self.device.function[DPCode.MODE].values)
|
DPCode.MODE, dptype=DPType.ENUM, prefer_function=True
|
||||||
for tuya_mode in data_type.range:
|
):
|
||||||
|
for tuya_mode in enum_type.range:
|
||||||
if tuya_mode not in TUYA_HVAC_TO_HA:
|
if tuya_mode not in TUYA_HVAC_TO_HA:
|
||||||
LOGGER.warning(
|
LOGGER.warning(
|
||||||
"Unknown HVAC mode '%s' for device %s; assuming it as off",
|
"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:
|
def set_humidity(self, humidity: float) -> None:
|
||||||
"""Set new target humidity."""
|
"""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(
|
raise RuntimeError(
|
||||||
"Cannot set humidity, device doesn't provide methods to set it"
|
"Cannot set humidity, device doesn't provide methods to set it"
|
||||||
)
|
)
|
||||||
|
@ -334,8 +288,8 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
||||||
self._send_command(
|
self._send_command(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"code": self._set_humidity_dpcode,
|
"code": self._set_humidity.dpcode,
|
||||||
"value": self._set_humidity_type.scale_value_back(humidity),
|
"value": self._set_humidity.scale_value_back(humidity),
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -367,7 +321,7 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
||||||
|
|
||||||
def set_temperature(self, **kwargs: Any) -> None:
|
def set_temperature(self, **kwargs: Any) -> None:
|
||||||
"""Set new target temperature."""
|
"""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(
|
raise RuntimeError(
|
||||||
"Cannot set target temperature, device doesn't provide methods to set it"
|
"Cannot set target temperature, device doesn't provide methods to set it"
|
||||||
)
|
)
|
||||||
|
@ -375,11 +329,9 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
||||||
self._send_command(
|
self._send_command(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"code": self._set_temperature_dpcode,
|
"code": self._set_temperature.dpcode,
|
||||||
"value": round(
|
"value": round(
|
||||||
self._set_temperature_type.scale_value_back(
|
self._set_temperature.scale_value_back(kwargs["temperature"])
|
||||||
kwargs["temperature"]
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -388,53 +340,50 @@ class TuyaClimateEntity(TuyaEntity, ClimateEntity):
|
||||||
@property
|
@property
|
||||||
def current_temperature(self) -> float | None:
|
def current_temperature(self) -> float | None:
|
||||||
"""Return the current temperature."""
|
"""Return the current temperature."""
|
||||||
if (
|
if self._current_temperature is None:
|
||||||
self._current_temperature_dpcode is None
|
|
||||||
or self._current_temperature_type is None
|
|
||||||
):
|
|
||||||
return 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:
|
if temperature is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self._current_temperature_type.scale_value(temperature)
|
return self._current_temperature.scale_value(temperature)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_humidity(self) -> int | None:
|
def current_humidity(self) -> int | None:
|
||||||
"""Return the current humidity."""
|
"""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
|
return None
|
||||||
|
|
||||||
humidity = self.device.status.get(self._current_humidity_dpcode)
|
humidity = self.device.status.get(self._current_humidity.dpcode)
|
||||||
if humidity is None:
|
if humidity is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return round(self._current_humidity_type.scale_value(humidity))
|
return round(self._current_humidity.scale_value(humidity))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_temperature(self) -> float | None:
|
def target_temperature(self) -> float | None:
|
||||||
"""Return the temperature currently set to be reached."""
|
"""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
|
return None
|
||||||
|
|
||||||
temperature = self.device.status.get(self._set_temperature_dpcode)
|
temperature = self.device.status.get(self._set_temperature.dpcode)
|
||||||
if temperature is None:
|
if temperature is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self._set_temperature_type.scale_value(temperature)
|
return self._set_temperature.scale_value(temperature)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def target_humidity(self) -> int | None:
|
def target_humidity(self) -> int | None:
|
||||||
"""Return the humidity currently set to be reached."""
|
"""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
|
return None
|
||||||
|
|
||||||
humidity = self.device.status.get(self._set_humidity_dpcode)
|
humidity = self.device.status.get(self._set_humidity.dpcode)
|
||||||
if humidity is None:
|
if humidity is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return round(self._set_humidity_type.scale_value(humidity))
|
return round(self._set_humidity.scale_value(humidity))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hvac_mode(self) -> str:
|
def hvac_mode(self) -> str:
|
||||||
|
|
|
@ -112,6 +112,17 @@ class WorkMode(StrEnum):
|
||||||
WHITE = "white"
|
WHITE = "white"
|
||||||
|
|
||||||
|
|
||||||
|
class DPType(StrEnum):
|
||||||
|
"""Data point types."""
|
||||||
|
|
||||||
|
BOOLEAN = "Boolean"
|
||||||
|
ENUM = "Enum"
|
||||||
|
INTEGER = "Integer"
|
||||||
|
JSON = "Json"
|
||||||
|
RAW = "Raw"
|
||||||
|
STRING = "String"
|
||||||
|
|
||||||
|
|
||||||
class DPCode(StrEnum):
|
class DPCode(StrEnum):
|
||||||
"""Data Point Codes used by Tuya.
|
"""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 homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import HomeAssistantTuyaData
|
from . import HomeAssistantTuyaData
|
||||||
from .base import EnumTypeData, IntegerTypeData, TuyaEntity
|
from .base import IntegerTypeData, TuyaEntity
|
||||||
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode
|
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, DPType
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -177,11 +177,9 @@ async def async_setup_entry(
|
||||||
class TuyaCoverEntity(TuyaEntity, CoverEntity):
|
class TuyaCoverEntity(TuyaEntity, CoverEntity):
|
||||||
"""Tuya Cover Device."""
|
"""Tuya Cover Device."""
|
||||||
|
|
||||||
_current_position_type: IntegerTypeData | None = None
|
_current_position: IntegerTypeData | None = None
|
||||||
_set_position_type: IntegerTypeData | None = None
|
_set_position: IntegerTypeData | None = None
|
||||||
_tilt_dpcode: DPCode | None = None
|
_tilt: IntegerTypeData | None = None
|
||||||
_tilt_type: IntegerTypeData | None = None
|
|
||||||
_position_dpcode: DPCode | None = None
|
|
||||||
entity_description: TuyaCoverEntityDescription
|
entity_description: TuyaCoverEntityDescription
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -197,85 +195,54 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
|
||||||
self._attr_supported_features = 0
|
self._attr_supported_features = 0
|
||||||
|
|
||||||
# Check if this cover is based on a switch or has controls
|
# Check if this cover is based on a switch or has controls
|
||||||
if device.function[description.key].type == "Boolean":
|
if self.find_dpcode(description.key, prefer_function=True):
|
||||||
self._attr_supported_features |= SUPPORT_OPEN | SUPPORT_CLOSE
|
if device.function[description.key].type == "Boolean":
|
||||||
elif device.function[description.key].type == "Enum":
|
self._attr_supported_features |= SUPPORT_OPEN | SUPPORT_CLOSE
|
||||||
data_type = EnumTypeData.from_json(device.function[description.key].values)
|
elif enum_type := self.find_dpcode(
|
||||||
if description.open_instruction_value in data_type.range:
|
description.key, dptype=DPType.ENUM, prefer_function=True
|
||||||
self._attr_supported_features |= SUPPORT_OPEN
|
):
|
||||||
if description.close_instruction_value in data_type.range:
|
if description.open_instruction_value in enum_type.range:
|
||||||
self._attr_supported_features |= SUPPORT_CLOSE
|
self._attr_supported_features |= SUPPORT_OPEN
|
||||||
if description.stop_instruction_value in data_type.range:
|
if description.close_instruction_value in enum_type.range:
|
||||||
self._attr_supported_features |= SUPPORT_STOP
|
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
|
# Determine type to use for setting the position
|
||||||
if (
|
if int_type := self.find_dpcode(
|
||||||
description.set_position is not None
|
description.set_position, dptype=DPType.INTEGER, prefer_function=True
|
||||||
and description.set_position in device.status_range
|
|
||||||
):
|
):
|
||||||
self._attr_supported_features |= SUPPORT_SET_POSITION
|
self._attr_supported_features |= SUPPORT_SET_POSITION
|
||||||
self._set_position_type = IntegerTypeData.from_json(
|
self._set_position = int_type
|
||||||
device.status_range[description.set_position].values
|
|
||||||
)
|
|
||||||
# Set as default, unless overwritten below
|
# 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
|
# Determine type for getting the position
|
||||||
if (
|
if int_type := self.find_dpcode(
|
||||||
description.current_position is not None
|
description.current_position, dptype=DPType.INTEGER, prefer_function=True
|
||||||
and description.current_position in device.status_range
|
|
||||||
):
|
):
|
||||||
self._current_position_type = IntegerTypeData.from_json(
|
self._current_position = int_type
|
||||||
device.status_range[description.current_position].values
|
|
||||||
)
|
|
||||||
|
|
||||||
# Determine type to use for setting the tilt
|
# Determine type to use for setting the tilt
|
||||||
if tilt_dpcode := next(
|
if int_type := self.find_dpcode(
|
||||||
(
|
(DPCode.ANGLE_HORIZONTAL, DPCode.ANGLE_VERTICAL),
|
||||||
dpcode
|
dptype=DPType.INTEGER,
|
||||||
for dpcode in (DPCode.ANGLE_HORIZONTAL, DPCode.ANGLE_VERTICAL)
|
prefer_function=True,
|
||||||
if dpcode in device.function
|
|
||||||
),
|
|
||||||
None,
|
|
||||||
):
|
):
|
||||||
self._attr_supported_features |= SUPPORT_SET_TILT_POSITION
|
self._attr_supported_features |= SUPPORT_SET_TILT_POSITION
|
||||||
self._tilt_dpcode = tilt_dpcode
|
self._tilt = int_type
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_cover_position(self) -> int | None:
|
def current_cover_position(self) -> int | None:
|
||||||
"""Return cover current position."""
|
"""Return cover current position."""
|
||||||
if self._current_position_type is None:
|
if self._current_position is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not self._position_dpcode:
|
if (position := self.device.status.get(self._current_position.dpcode)) is None:
|
||||||
return None
|
|
||||||
|
|
||||||
if (position := self.device.status.get(self._position_dpcode)) is None:
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return round(
|
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
|
@property
|
||||||
|
@ -284,13 +251,13 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
|
||||||
|
|
||||||
None is unknown, 0 is closed, 100 is fully open.
|
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
|
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 None
|
||||||
|
|
||||||
return round(self._tilt_type.remap_value_to(angle, 0, 100))
|
return round(self._tilt.remap_value_to(angle, 0, 100))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_closed(self) -> bool | None:
|
def is_closed(self) -> bool | None:
|
||||||
|
@ -316,24 +283,21 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
|
||||||
def open_cover(self, **kwargs: Any) -> None:
|
def open_cover(self, **kwargs: Any) -> None:
|
||||||
"""Open the cover."""
|
"""Open the cover."""
|
||||||
value: bool | str = True
|
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
|
value = self.entity_description.open_instruction_value
|
||||||
|
|
||||||
commands: list[dict[str, str | int]] = [
|
commands: list[dict[str, str | int]] = [
|
||||||
{"code": self.entity_description.key, "value": value}
|
{"code": self.entity_description.key, "value": value}
|
||||||
]
|
]
|
||||||
|
|
||||||
if (
|
if self._set_position is not None:
|
||||||
self.entity_description.set_position is not None
|
|
||||||
and self._set_position_type is not None
|
|
||||||
):
|
|
||||||
commands.append(
|
commands.append(
|
||||||
{
|
{
|
||||||
"code": self.entity_description.set_position,
|
"code": self._set_position.dpcode,
|
||||||
"value": round(
|
"value": round(
|
||||||
self._set_position_type.remap_value_from(
|
self._set_position.remap_value_from(100, 0, 100, reverse=True),
|
||||||
100, 0, 100, reverse=True
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -343,24 +307,21 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
|
||||||
def close_cover(self, **kwargs: Any) -> None:
|
def close_cover(self, **kwargs: Any) -> None:
|
||||||
"""Close cover."""
|
"""Close cover."""
|
||||||
value: bool | str = False
|
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
|
value = self.entity_description.close_instruction_value
|
||||||
|
|
||||||
commands: list[dict[str, str | int]] = [
|
commands: list[dict[str, str | int]] = [
|
||||||
{"code": self.entity_description.key, "value": value}
|
{"code": self.entity_description.key, "value": value}
|
||||||
]
|
]
|
||||||
|
|
||||||
if (
|
if self._set_position is not None:
|
||||||
self.entity_description.set_position is not None
|
|
||||||
and self._set_position_type is not None
|
|
||||||
):
|
|
||||||
commands.append(
|
commands.append(
|
||||||
{
|
{
|
||||||
"code": self.entity_description.set_position,
|
"code": self._set_position.dpcode,
|
||||||
"value": round(
|
"value": round(
|
||||||
self._set_position_type.remap_value_from(
|
self._set_position.remap_value_from(0, 0, 100, reverse=True),
|
||||||
0, 0, 100, reverse=True
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -369,7 +330,7 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
|
||||||
|
|
||||||
def set_cover_position(self, **kwargs: Any) -> None:
|
def set_cover_position(self, **kwargs: Any) -> None:
|
||||||
"""Move the cover to a specific position."""
|
"""Move the cover to a specific position."""
|
||||||
if self._set_position_type is None:
|
if self._set_position is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Cannot set position, device doesn't provide methods to set it"
|
"Cannot set position, device doesn't provide methods to set it"
|
||||||
)
|
)
|
||||||
|
@ -377,9 +338,9 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
|
||||||
self._send_command(
|
self._send_command(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"code": self.entity_description.set_position,
|
"code": self._set_position.dpcode,
|
||||||
"value": round(
|
"value": round(
|
||||||
self._set_position_type.remap_value_from(
|
self._set_position.remap_value_from(
|
||||||
kwargs[ATTR_POSITION], 0, 100, reverse=True
|
kwargs[ATTR_POSITION], 0, 100, reverse=True
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
@ -400,7 +361,7 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
|
||||||
|
|
||||||
def set_cover_tilt_position(self, **kwargs):
|
def set_cover_tilt_position(self, **kwargs):
|
||||||
"""Move the cover tilt to a specific position."""
|
"""Move the cover tilt to a specific position."""
|
||||||
if self._tilt_type is None:
|
if self._tilt is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Cannot set tilt, device doesn't provide methods to set it"
|
"Cannot set tilt, device doesn't provide methods to set it"
|
||||||
)
|
)
|
||||||
|
@ -408,9 +369,9 @@ class TuyaCoverEntity(TuyaEntity, CoverEntity):
|
||||||
self._send_command(
|
self._send_command(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"code": self._tilt_dpcode,
|
"code": self._tilt.dpcode,
|
||||||
"value": round(
|
"value": round(
|
||||||
self._tilt_type.remap_value_from(
|
self._tilt.remap_value_from(
|
||||||
kwargs[ATTR_TILT_POSITION], 0, 100, reverse=True
|
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 homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import HomeAssistantTuyaData
|
from . import HomeAssistantTuyaData
|
||||||
from .base import EnumTypeData, IntegerTypeData, TuyaEntity
|
from .base import IntegerTypeData, TuyaEntity
|
||||||
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode
|
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, DPType
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
@ -79,7 +79,7 @@ async def async_setup_entry(
|
||||||
class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity):
|
class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity):
|
||||||
"""Tuya (de)humidifier Device."""
|
"""Tuya (de)humidifier Device."""
|
||||||
|
|
||||||
_set_humidity_type: IntegerTypeData | None = None
|
_set_humidity: IntegerTypeData | None = None
|
||||||
_switch_dpcode: DPCode | None = None
|
_switch_dpcode: DPCode | None = None
|
||||||
entity_description: TuyaHumidifierEntityDescription
|
entity_description: TuyaHumidifierEntityDescription
|
||||||
|
|
||||||
|
@ -96,30 +96,24 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity):
|
||||||
self._attr_supported_features = 0
|
self._attr_supported_features = 0
|
||||||
|
|
||||||
# Determine main switch DPCode
|
# Determine main switch DPCode
|
||||||
possible_dpcodes = description.dpcode or description.key
|
self._switch_dpcode = self.find_dpcode(
|
||||||
if isinstance(possible_dpcodes, DPCode) and possible_dpcodes in device.function:
|
description.dpcode or DPCode(description.key), prefer_function=True
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Determine humidity parameters
|
# Determine humidity parameters
|
||||||
if description.humidity in device.status_range:
|
if int_type := self.find_dpcode(
|
||||||
type_data = IntegerTypeData.from_json(
|
description.humidity, dptype=DPType.INTEGER, prefer_function=True
|
||||||
device.status_range[description.humidity].values
|
):
|
||||||
)
|
self._set_humiditye = int_type
|
||||||
self._set_humidity_type = type_data
|
self._attr_min_humidity = int(int_type.min_scaled)
|
||||||
self._attr_min_humidity = int(type_data.min_scaled)
|
self._attr_max_humidity = int(int_type.max_scaled)
|
||||||
self._attr_max_humidity = int(type_data.max_scaled)
|
|
||||||
|
|
||||||
# Determine mode support and provided modes
|
# 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_supported_features |= SUPPORT_MODES
|
||||||
self._attr_available_modes = EnumTypeData.from_json(
|
self._attr_available_modes = enum_type.range
|
||||||
device.function[DPCode.MODE].values
|
|
||||||
).range
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self) -> bool:
|
def is_on(self) -> bool:
|
||||||
|
@ -136,14 +130,14 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity):
|
||||||
@property
|
@property
|
||||||
def target_humidity(self) -> int | None:
|
def target_humidity(self) -> int | None:
|
||||||
"""Return the humidity we try to reach."""
|
"""Return the humidity we try to reach."""
|
||||||
if self._set_humidity_type is None:
|
if self._set_humidity is None:
|
||||||
return 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:
|
if humidity is None:
|
||||||
return 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):
|
def turn_on(self, **kwargs):
|
||||||
"""Turn the device on."""
|
"""Turn the device on."""
|
||||||
|
@ -155,7 +149,7 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity):
|
||||||
|
|
||||||
def set_humidity(self, humidity):
|
def set_humidity(self, humidity):
|
||||||
"""Set new target humidity."""
|
"""Set new target humidity."""
|
||||||
if self._set_humidity_type is None:
|
if self._set_humidity is None:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"Cannot set humidity, device doesn't provide methods to set it"
|
"Cannot set humidity, device doesn't provide methods to set it"
|
||||||
)
|
)
|
||||||
|
@ -163,8 +157,8 @@ class TuyaHumidifierEntity(TuyaEntity, HumidifierEntity):
|
||||||
self._send_command(
|
self._send_command(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"code": self.entity_description.humidity,
|
"code": self._set_humidity.dpcode,
|
||||||
"value": self._set_humidity_type.scale_value_back(humidity),
|
"value": self._set_humidity.scale_value_back(humidity),
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
import json
|
import json
|
||||||
from typing import Any
|
from typing import Any, cast
|
||||||
|
|
||||||
from tuya_iot import TuyaDevice, TuyaDeviceManager
|
from tuya_iot import TuyaDevice, TuyaDeviceManager
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import HomeAssistantTuyaData
|
from . import HomeAssistantTuyaData
|
||||||
from .base import IntegerTypeData, TuyaEntity
|
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
|
from .util import remap_value
|
||||||
|
|
||||||
|
|
||||||
|
@ -40,15 +40,15 @@ class ColorTypeData:
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_COLOR_TYPE_DATA = ColorTypeData(
|
DEFAULT_COLOR_TYPE_DATA = ColorTypeData(
|
||||||
h_type=IntegerTypeData(min=1, scale=0, max=360, step=1),
|
h_type=IntegerTypeData(DPCode.COLOUR_DATA_HSV, min=1, scale=0, max=360, step=1),
|
||||||
s_type=IntegerTypeData(min=1, scale=0, max=255, step=1),
|
s_type=IntegerTypeData(DPCode.COLOUR_DATA_HSV, min=1, scale=0, max=255, step=1),
|
||||||
v_type=IntegerTypeData(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(
|
DEFAULT_COLOR_TYPE_DATA_V2 = ColorTypeData(
|
||||||
h_type=IntegerTypeData(min=1, scale=0, max=360, step=1),
|
h_type=IntegerTypeData(DPCode.COLOUR_DATA_HSV, min=1, scale=0, max=360, step=1),
|
||||||
s_type=IntegerTypeData(min=1, scale=0, max=1000, step=1),
|
s_type=IntegerTypeData(DPCode.COLOUR_DATA_HSV, min=1, scale=0, max=1000, step=1),
|
||||||
v_type=IntegerTypeData(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."""
|
"""Tuya light device."""
|
||||||
|
|
||||||
entity_description: TuyaLightEntityDescription
|
entity_description: TuyaLightEntityDescription
|
||||||
_brightness_dpcode: DPCode | None = None
|
|
||||||
_brightness_max_type: IntegerTypeData | None = None
|
_brightness_max: IntegerTypeData | None = None
|
||||||
_brightness_min_type: IntegerTypeData | None = None
|
_brightness_min: IntegerTypeData | None = None
|
||||||
_brightness_type: IntegerTypeData | None = None
|
_brightness: IntegerTypeData | None = None
|
||||||
_color_data_dpcode: DPCode | None = None
|
_color_data_dpcode: DPCode | None = None
|
||||||
_color_data_type: ColorTypeData | None = None
|
_color_data_type: ColorTypeData | None = None
|
||||||
_color_mode_dpcode: DPCode | None = None
|
_color_mode: DPCode | None = None
|
||||||
_color_temp_dpcode: DPCode | None = None
|
_color_temp: IntegerTypeData | None = None
|
||||||
_color_temp_type: IntegerTypeData | None = None
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -345,106 +344,51 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
|
||||||
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
||||||
self._attr_supported_color_modes = {COLOR_MODE_ONOFF}
|
self._attr_supported_color_modes = {COLOR_MODE_ONOFF}
|
||||||
|
|
||||||
# Determine brightness DPCodes
|
# Determine DPCodes
|
||||||
if (
|
self._color_mode_dpcode = self.find_dpcode(
|
||||||
isinstance(description.brightness, DPCode)
|
description.color_mode, prefer_function=True
|
||||||
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 color mode DPCode
|
if int_type := self.find_dpcode(
|
||||||
if (
|
description.brightness, dptype=DPType.INTEGER, prefer_function=True
|
||||||
description.color_mode is not None
|
|
||||||
and description.color_mode in device.function
|
|
||||||
):
|
):
|
||||||
self._color_mode_dpcode = description.color_mode
|
self._brightness = int_type
|
||||||
|
|
||||||
# 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._attr_supported_color_modes.add(COLOR_MODE_BRIGHTNESS)
|
self._attr_supported_color_modes.add(COLOR_MODE_BRIGHTNESS)
|
||||||
self._brightness_type = IntegerTypeData.from_json(
|
self._brightness_max = self.find_dpcode(
|
||||||
device.function[self._brightness_dpcode].values
|
description.brightness_max, dptype=DPType.INTEGER
|
||||||
|
)
|
||||||
|
self._brightness_min = self.find_dpcode(
|
||||||
|
description.brightness_min, dptype=DPType.INTEGER
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check if min/max capable
|
if int_type := self.find_dpcode(
|
||||||
if (
|
description.color_temp, dptype=DPType.INTEGER, prefer_function=True
|
||||||
description.brightness_max is not None
|
):
|
||||||
and description.brightness_min is not None
|
self._color_temp = int_type
|
||||||
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:
|
|
||||||
self._attr_supported_color_modes.add(COLOR_MODE_COLOR_TEMP)
|
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 (
|
||||||
if self._color_data_dpcode:
|
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)
|
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
|
# Fetch color data type information
|
||||||
if function_data := json.loads(
|
if function_data := json.loads(values):
|
||||||
self.device.function[self._color_data_dpcode].values
|
|
||||||
):
|
|
||||||
self._color_data_type = ColorTypeData(
|
self._color_data_type = ColorTypeData(
|
||||||
h_type=IntegerTypeData(**function_data["h"]),
|
h_type=IntegerTypeData(dpcode, **function_data["h"]),
|
||||||
s_type=IntegerTypeData(**function_data["s"]),
|
s_type=IntegerTypeData(dpcode, **function_data["s"]),
|
||||||
v_type=IntegerTypeData(**function_data["v"]),
|
v_type=IntegerTypeData(dpcode, **function_data["v"]),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# If no type is found, use a default one
|
# If no type is found, use a default one
|
||||||
self._color_data_type = self.entity_description.default_color_type
|
self._color_data_type = self.entity_description.default_color_type
|
||||||
if self._color_data_dpcode == DPCode.COLOUR_DATA_V2 or (
|
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
|
self._color_data_type = DEFAULT_COLOR_TYPE_DATA_V2
|
||||||
|
|
||||||
|
@ -457,7 +401,7 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
|
||||||
"""Turn on or control the light."""
|
"""Turn on or control the light."""
|
||||||
commands = [{"code": self.entity_description.key, "value": True}]
|
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:
|
if self._color_mode_dpcode:
|
||||||
commands += [
|
commands += [
|
||||||
{
|
{
|
||||||
|
@ -468,9 +412,9 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
|
||||||
|
|
||||||
commands += [
|
commands += [
|
||||||
{
|
{
|
||||||
"code": self._color_temp_dpcode,
|
"code": self._color_temp.dpcode,
|
||||||
"value": round(
|
"value": round(
|
||||||
self._color_temp_type.remap_value_from(
|
self._color_temp.remap_value_from(
|
||||||
kwargs[ATTR_COLOR_TEMP],
|
kwargs[ATTR_COLOR_TEMP],
|
||||||
self.min_mireds,
|
self.min_mireds,
|
||||||
self.max_mireds,
|
self.max_mireds,
|
||||||
|
@ -525,37 +469,31 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
|
||||||
if (
|
if (
|
||||||
ATTR_BRIGHTNESS in kwargs
|
ATTR_BRIGHTNESS in kwargs
|
||||||
and self.color_mode != COLOR_MODE_HS
|
and self.color_mode != COLOR_MODE_HS
|
||||||
and self._brightness_type
|
and self._brightness
|
||||||
):
|
):
|
||||||
brightness = kwargs[ATTR_BRIGHTNESS]
|
brightness = kwargs[ATTR_BRIGHTNESS]
|
||||||
|
|
||||||
# If there is a min/max value, the brightness is actually limited.
|
# If there is a min/max value, the brightness is actually limited.
|
||||||
# Meaning it is actually not on a 0-255 scale.
|
# Meaning it is actually not on a 0-255 scale.
|
||||||
if (
|
if (
|
||||||
self._brightness_max_type is not None
|
self._brightness_max is not None
|
||||||
and self._brightness_min_type is not None
|
and self._brightness_min is not None
|
||||||
and self.entity_description.brightness_max is not None
|
|
||||||
and self.entity_description.brightness_min is not None
|
|
||||||
and (
|
and (
|
||||||
brightness_max := self.device.status.get(
|
brightness_max := self.device.status.get(
|
||||||
self.entity_description.brightness_max
|
self._brightness_max.dpcode
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
is not None
|
is not None
|
||||||
and (
|
and (
|
||||||
brightness_min := self.device.status.get(
|
brightness_min := self.device.status.get(
|
||||||
self.entity_description.brightness_min
|
self._brightness_min.dpcode
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
is not None
|
is not None
|
||||||
):
|
):
|
||||||
# Remap values onto our scale
|
# Remap values onto our scale
|
||||||
brightness_max = self._brightness_max_type.remap_value_to(
|
brightness_max = self._brightness_max.remap_value_to(brightness_max)
|
||||||
brightness_max
|
brightness_min = self._brightness_min.remap_value_to(brightness_min)
|
||||||
)
|
|
||||||
brightness_min = self._brightness_min_type.remap_value_to(
|
|
||||||
brightness_min
|
|
||||||
)
|
|
||||||
|
|
||||||
# Remap the brightness value from their min-max to our 0-255 scale
|
# Remap the brightness value from their min-max to our 0-255 scale
|
||||||
brightness = remap_value(
|
brightness = remap_value(
|
||||||
|
@ -566,8 +504,8 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
|
||||||
|
|
||||||
commands += [
|
commands += [
|
||||||
{
|
{
|
||||||
"code": self._brightness_dpcode,
|
"code": self._brightness.dpcode,
|
||||||
"value": round(self._brightness_type.remap_value_from(brightness)),
|
"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()):
|
if self.color_mode == COLOR_MODE_HS and (color_data := self._get_color_data()):
|
||||||
return color_data.brightness
|
return color_data.brightness
|
||||||
|
|
||||||
if not self._brightness_dpcode or not self._brightness_type:
|
if not self._brightness:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
brightness = self.device.status.get(self._brightness_dpcode)
|
brightness = self.device.status.get(self._brightness.dpcode)
|
||||||
if brightness is None:
|
if brightness is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Remap value to our scale
|
# 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.
|
# If there is a min/max value, the brightness is actually limited.
|
||||||
# Meaning it is actually not on a 0-255 scale.
|
# Meaning it is actually not on a 0-255 scale.
|
||||||
if (
|
if (
|
||||||
self._brightness_max_type is not None
|
self._brightness_max is not None
|
||||||
and self._brightness_min_type is not None
|
and self._brightness_min is not None
|
||||||
and self.entity_description.brightness_max is not None
|
and (brightness_max := self.device.status.get(self._brightness_max.dpcode))
|
||||||
and self.entity_description.brightness_min is not None
|
|
||||||
and (
|
|
||||||
brightness_max := self.device.status.get(
|
|
||||||
self.entity_description.brightness_max
|
|
||||||
)
|
|
||||||
)
|
|
||||||
is not None
|
is not None
|
||||||
and (
|
and (brightness_min := self.device.status.get(self._brightness_min.dpcode))
|
||||||
brightness_min := self.device.status.get(
|
|
||||||
self.entity_description.brightness_min
|
|
||||||
)
|
|
||||||
)
|
|
||||||
is not None
|
is not None
|
||||||
):
|
):
|
||||||
# Remap values onto our scale
|
# Remap values onto our scale
|
||||||
brightness_max = self._brightness_max_type.remap_value_to(brightness_max)
|
brightness_max = self._brightness_max.remap_value_to(brightness_max)
|
||||||
brightness_min = self._brightness_min_type.remap_value_to(brightness_min)
|
brightness_min = self._brightness_min.remap_value_to(brightness_min)
|
||||||
|
|
||||||
# Remap the brightness value from their min-max to our 0-255 scale
|
# Remap the brightness value from their min-max to our 0-255 scale
|
||||||
brightness = remap_value(
|
brightness = remap_value(
|
||||||
|
@ -630,15 +558,15 @@ class TuyaLightEntity(TuyaEntity, LightEntity):
|
||||||
@property
|
@property
|
||||||
def color_temp(self) -> int | None:
|
def color_temp(self) -> int | None:
|
||||||
"""Return the color_temp of the light."""
|
"""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
|
return None
|
||||||
|
|
||||||
temperature = self.device.status.get(self._color_temp_dpcode)
|
temperature = self.device.status.get(self._color_temp.dpcode)
|
||||||
if temperature is None:
|
if temperature is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return round(
|
return round(
|
||||||
self._color_temp_type.remap_value_to(
|
self._color_temp.remap_value_to(
|
||||||
temperature, self.min_mireds, self.max_mireds, reverse=True
|
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
|
and self.device.status.get(self._color_mode_dpcode) != WorkMode.WHITE
|
||||||
):
|
):
|
||||||
return COLOR_MODE_HS
|
return COLOR_MODE_HS
|
||||||
if self._color_temp_dpcode:
|
if self._color_temp:
|
||||||
return COLOR_MODE_COLOR_TEMP
|
return COLOR_MODE_COLOR_TEMP
|
||||||
if self._brightness_dpcode:
|
if self._brightness:
|
||||||
return COLOR_MODE_BRIGHTNESS
|
return COLOR_MODE_BRIGHTNESS
|
||||||
return COLOR_MODE_ONOFF
|
return COLOR_MODE_ONOFF
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
"""Support for Tuya number."""
|
"""Support for Tuya number."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from tuya_iot import TuyaDevice, TuyaDeviceManager
|
from tuya_iot import TuyaDevice, TuyaDeviceManager
|
||||||
from tuya_iot.device import TuyaDeviceStatusRange
|
|
||||||
|
|
||||||
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
from homeassistant.components.number import NumberEntity, NumberEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
@ -15,7 +12,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import HomeAssistantTuyaData
|
from . import HomeAssistantTuyaData
|
||||||
from .base import IntegerTypeData, TuyaEntity
|
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
|
# All descriptions can be found here. Mostly the Integer data types in the
|
||||||
# default instructions set of each category end up being a number.
|
# default instructions set of each category end up being a number.
|
||||||
|
@ -280,8 +277,7 @@ async def async_setup_entry(
|
||||||
class TuyaNumberEntity(TuyaEntity, NumberEntity):
|
class TuyaNumberEntity(TuyaEntity, NumberEntity):
|
||||||
"""Tuya Number Entity."""
|
"""Tuya Number Entity."""
|
||||||
|
|
||||||
_status_range: TuyaDeviceStatusRange | None = None
|
_number: IntegerTypeData | None = None
|
||||||
_type_data: IntegerTypeData | None = None
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -294,45 +290,39 @@ class TuyaNumberEntity(TuyaEntity, NumberEntity):
|
||||||
self.entity_description = description
|
self.entity_description = description
|
||||||
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
self._attr_unique_id = f"{super().unique_id}{description.key}"
|
||||||
|
|
||||||
if status_range := device.status_range.get(description.key):
|
if int_type := self.find_dpcode(
|
||||||
self._status_range = cast(TuyaDeviceStatusRange, status_range)
|
description.key, dptype=DPType.INTEGER, prefer_function=True
|
||||||
|
):
|
||||||
# Extract type data from integer status range,
|
self._number = int_type
|
||||||
# and determine unit of measurement
|
self._attr_max_value = self._number.max_scaled
|
||||||
if self._status_range.type == "Integer":
|
self._attr_min_value = self._number.min_scaled
|
||||||
self._type_data = IntegerTypeData.from_json(self._status_range.values)
|
self._attr_step = self._number.step_scaled
|
||||||
self._attr_max_value = self._type_data.max_scaled
|
if description.unit_of_measurement is None:
|
||||||
self._attr_min_value = self._type_data.min_scaled
|
self._attr_unit_of_measurement = self._number.unit
|
||||||
self._attr_step = self._type_data.step_scaled
|
|
||||||
if description.unit_of_measurement is None:
|
|
||||||
self._attr_unit_of_measurement = self._type_data.unit
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def value(self) -> float | None:
|
def value(self) -> float | None:
|
||||||
"""Return the entity value to represent the entity state."""
|
"""Return the entity value to represent the entity state."""
|
||||||
# Unknown or unsupported data type
|
# Unknown or unsupported data type
|
||||||
if self._status_range is None or self._status_range.type != "Integer":
|
if self._number is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Raw value
|
# 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
|
return self._number.scale_value(value)
|
||||||
if value is not None and isinstance(self._type_data, IntegerTypeData):
|
|
||||||
return self._type_data.scale_value(value)
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
def set_value(self, value: float) -> None:
|
def set_value(self, value: float) -> None:
|
||||||
"""Set new value."""
|
"""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")
|
raise RuntimeError("Cannot set value, device doesn't provide type data")
|
||||||
|
|
||||||
self._send_command(
|
self._send_command(
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
"code": self.entity_description.key,
|
"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."""
|
"""Support for Tuya select."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from tuya_iot import TuyaDevice, TuyaDeviceManager
|
from tuya_iot import TuyaDevice, TuyaDeviceManager
|
||||||
from tuya_iot.device import TuyaDeviceStatusRange
|
|
||||||
|
|
||||||
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
from homeassistant.components.select import SelectEntity, SelectEntityDescription
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
@ -14,8 +11,8 @@ from homeassistant.helpers.entity import EntityCategory
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import HomeAssistantTuyaData
|
from . import HomeAssistantTuyaData
|
||||||
from .base import EnumTypeData, TuyaEntity
|
from .base import TuyaEntity
|
||||||
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, TuyaDeviceClass
|
from .const import DOMAIN, TUYA_DISCOVERY_NEW, DPCode, DPType, TuyaDeviceClass
|
||||||
|
|
||||||
# All descriptions can be found here. Mostly the Enum data types in the
|
# All descriptions can be found here. Mostly the Enum data types in the
|
||||||
# default instructions set of each category end up being a select.
|
# 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_unique_id = f"{super().unique_id}{description.key}"
|
||||||
|
|
||||||
self._attr_opions: list[str] = []
|
self._attr_opions: list[str] = []
|
||||||
if status_range := device.status_range.get(description.key):
|
if enum_type := self.find_dpcode(
|
||||||
self._status_range = cast(TuyaDeviceStatusRange, status_range)
|
description.key, dptype=DPType.ENUM, prefer_function=True
|
||||||
|
):
|
||||||
# Extract type data from enum status range,
|
self._attr_options = enum_type.range
|
||||||
if self._status_range.type == "Enum":
|
|
||||||
type_data = EnumTypeData.from_json(self._status_range.values)
|
|
||||||
self._attr_options = type_data.range
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def current_option(self) -> str | None:
|
def current_option(self) -> str | None:
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import cast
|
|
||||||
|
|
||||||
from tuya_iot import TuyaDevice, TuyaDeviceManager
|
from tuya_iot import TuyaDevice, TuyaDeviceManager
|
||||||
from tuya_iot.device import TuyaDeviceStatusRange
|
from tuya_iot.device import TuyaDeviceStatusRange
|
||||||
|
@ -33,6 +32,7 @@ from .const import (
|
||||||
DOMAIN,
|
DOMAIN,
|
||||||
TUYA_DISCOVERY_NEW,
|
TUYA_DISCOVERY_NEW,
|
||||||
DPCode,
|
DPCode,
|
||||||
|
DPType,
|
||||||
TuyaDeviceClass,
|
TuyaDeviceClass,
|
||||||
UnitOfMeasurement,
|
UnitOfMeasurement,
|
||||||
)
|
)
|
||||||
|
@ -776,6 +776,7 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
|
||||||
entity_description: TuyaSensorEntityDescription
|
entity_description: TuyaSensorEntityDescription
|
||||||
|
|
||||||
_status_range: TuyaDeviceStatusRange | None = None
|
_status_range: TuyaDeviceStatusRange | None = None
|
||||||
|
_type: DPType | None = None
|
||||||
_type_data: IntegerTypeData | EnumTypeData | None = None
|
_type_data: IntegerTypeData | EnumTypeData | None = None
|
||||||
_uom: UnitOfMeasurement | None = None
|
_uom: UnitOfMeasurement | None = None
|
||||||
|
|
||||||
|
@ -792,19 +793,18 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
|
||||||
f"{super().unique_id}{description.key}{description.subkey or ''}"
|
f"{super().unique_id}{description.key}{description.subkey or ''}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if status_range := device.status_range.get(description.key):
|
if int_type := self.find_dpcode(description.key, dptype=DPType.INTEGER):
|
||||||
self._status_range = cast(TuyaDeviceStatusRange, status_range)
|
self._type_data = int_type
|
||||||
|
self._type = DPType.INTEGER
|
||||||
# Extract type data from integer status range,
|
if description.native_unit_of_measurement is None:
|
||||||
# and determine unit of measurement
|
self._attr_native_unit_of_measurement = int_type.unit
|
||||||
if self._status_range.type == "Integer":
|
elif enum_type := self.find_dpcode(
|
||||||
self._type_data = IntegerTypeData.from_json(self._status_range.values)
|
description.key, dptype=DPType.ENUM, prefer_function=True
|
||||||
if description.native_unit_of_measurement is None:
|
):
|
||||||
self._attr_native_unit_of_measurement = self._type_data.unit
|
self._type_data = enum_type
|
||||||
|
self._type = DPType.ENUM
|
||||||
# Extract type data from enum status range
|
else:
|
||||||
elif self._status_range.type == "Enum":
|
self._type = self.get_dptype(DPCode(description.key))
|
||||||
self._type_data = EnumTypeData.from_json(self._status_range.values)
|
|
||||||
|
|
||||||
# Logic to ensure the set device class and API received Unit Of Measurement
|
# Logic to ensure the set device class and API received Unit Of Measurement
|
||||||
# match Home Assistants requirements.
|
# match Home Assistants requirements.
|
||||||
|
@ -841,13 +841,13 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> StateType:
|
def native_value(self) -> StateType:
|
||||||
"""Return the value reported by the sensor."""
|
"""Return the value reported by the sensor."""
|
||||||
# Unknown or unsupported data type
|
# Only continue if data type is known
|
||||||
if self._status_range is None or self._status_range.type not in (
|
if self._type not in (
|
||||||
"Integer",
|
DPType.INTEGER,
|
||||||
"String",
|
DPType.STRING,
|
||||||
"Enum",
|
DPType.ENUM,
|
||||||
"Json",
|
DPType.JSON,
|
||||||
"Raw",
|
DPType.RAW,
|
||||||
):
|
):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -871,13 +871,13 @@ class TuyaSensorEntity(TuyaEntity, SensorEntity):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Get subkey value from Json string.
|
# 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:
|
if self.entity_description.subkey is None:
|
||||||
return None
|
return None
|
||||||
values = ElectricityTypeData.from_json(value)
|
values = ElectricityTypeData.from_json(value)
|
||||||
return getattr(values, self.entity_description.subkey)
|
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:
|
if self.entity_description.subkey is None:
|
||||||
return None
|
return None
|
||||||
values = ElectricityTypeData.from_raw(value)
|
values = ElectricityTypeData.from_raw(value)
|
||||||
|
|
|
@ -30,7 +30,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from . import HomeAssistantTuyaData
|
from . import HomeAssistantTuyaData
|
||||||
from .base import EnumTypeData, IntegerTypeData, TuyaEntity
|
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 = {
|
TUYA_STATUS_TO_HA = {
|
||||||
"charge_done": STATE_DOCKED,
|
"charge_done": STATE_DOCKED,
|
||||||
|
@ -81,48 +81,50 @@ async def async_setup_entry(
|
||||||
class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity):
|
class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity):
|
||||||
"""Tuya Vacuum Device."""
|
"""Tuya Vacuum Device."""
|
||||||
|
|
||||||
_fan_speed_type: EnumTypeData | None = None
|
_fan_speed: EnumTypeData | None = None
|
||||||
_battery_level_type: IntegerTypeData | None = None
|
_battery_level: IntegerTypeData | None = None
|
||||||
_supported_features = 0
|
_supported_features = 0
|
||||||
|
|
||||||
def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None:
|
def __init__(self, device: TuyaDevice, device_manager: TuyaDeviceManager) -> None:
|
||||||
"""Init Tuya vacuum."""
|
"""Init Tuya vacuum."""
|
||||||
super().__init__(device, device_manager)
|
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
|
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
|
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
|
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
|
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
|
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
|
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._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._supported_features |= SUPPORT_BATTERY
|
||||||
self._battery_level_type = IntegerTypeData.from_json(status_range.values)
|
self._battery_level = int_type
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def battery_level(self) -> int | None:
|
def battery_level(self) -> int | None:
|
||||||
"""Return Tuya device state."""
|
"""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)
|
status := self.device.status.get(DPCode.ELECTRICITY_LEFT)
|
||||||
):
|
):
|
||||||
return None
|
return None
|
||||||
return round(self._battery_level_type.scale_value(status))
|
return round(self._battery_level.scale_value(status))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def fan_speed(self) -> str | None:
|
def fan_speed(self) -> str | None:
|
||||||
|
@ -132,9 +134,9 @@ class TuyaVacuumEntity(TuyaEntity, StateVacuumEntity):
|
||||||
@property
|
@property
|
||||||
def fan_speed_list(self) -> list[str]:
|
def fan_speed_list(self) -> list[str]:
|
||||||
"""Get the list of available fan speed steps of the vacuum cleaner."""
|
"""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 []
|
||||||
return self._fan_speed_type.range
|
return self._fan_speed.range
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> str | None:
|
def state(self) -> str | None:
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue