diff --git a/homeassistant/components/tuya/alarm_control_panel.py b/homeassistant/components/tuya/alarm_control_panel.py index f073184882a..7838d4a2b4a 100644 --- a/homeassistant/components/tuya/alarm_control_panel.py +++ b/homeassistant/components/tuya/alarm_control_panel.py @@ -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}]) diff --git a/homeassistant/components/tuya/base.py b/homeassistant/components/tuya/base.py index 2a2a76cfe2f..b51fa361121 100644 --- a/homeassistant/components/tuya/base.py +++ b/homeassistant/components/tuya/base.py @@ -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( diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index a56f64a8aad..cbb778e34b1 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -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: diff --git a/homeassistant/components/tuya/const.py b/homeassistant/components/tuya/const.py index c8ee86b208c..ec6a6bb3b78 100644 --- a/homeassistant/components/tuya/const.py +++ b/homeassistant/components/tuya/const.py @@ -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. diff --git a/homeassistant/components/tuya/cover.py b/homeassistant/components/tuya/cover.py index 2c8f9c48aae..6a9e3767065 100644 --- a/homeassistant/components/tuya/cover.py +++ b/homeassistant/components/tuya/cover.py @@ -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 ) ), diff --git a/homeassistant/components/tuya/humidifier.py b/homeassistant/components/tuya/humidifier.py index c6cddad2759..4a56088a272 100644 --- a/homeassistant/components/tuya/humidifier.py +++ b/homeassistant/components/tuya/humidifier.py @@ -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), } ] ) diff --git a/homeassistant/components/tuya/light.py b/homeassistant/components/tuya/light.py index 4198e036d62..4fb6b4e8762 100644 --- a/homeassistant/components/tuya/light.py +++ b/homeassistant/components/tuya/light.py @@ -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 diff --git a/homeassistant/components/tuya/number.py b/homeassistant/components/tuya/number.py index a98635ef68c..9d357f78b35 100644 --- a/homeassistant/components/tuya/number.py +++ b/homeassistant/components/tuya/number.py @@ -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), } ] ) diff --git a/homeassistant/components/tuya/select.py b/homeassistant/components/tuya/select.py index 56163e9e164..c229e1998c2 100644 --- a/homeassistant/components/tuya/select.py +++ b/homeassistant/components/tuya/select.py @@ -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: diff --git a/homeassistant/components/tuya/sensor.py b/homeassistant/components/tuya/sensor.py index 0ac0bb0ce87..2799d2b82fc 100644 --- a/homeassistant/components/tuya/sensor.py +++ b/homeassistant/components/tuya/sensor.py @@ -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) diff --git a/homeassistant/components/tuya/vacuum.py b/homeassistant/components/tuya/vacuum.py index 73731efb9e9..47e9ec5aa7d 100644 --- a/homeassistant/components/tuya/vacuum.py +++ b/homeassistant/components/tuya/vacuum.py @@ -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: