Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions homeassistant/components/tuya/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
DPCodeEnumWrapper,
DPCodeIntegerWrapper,
DPCodeJsonWrapper,
IntegerTypeData,
)
from .type_information import IntegerTypeInformation
from .util import remap_value


Expand Down Expand Up @@ -138,24 +138,24 @@ def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any
)


DEFAULT_H_TYPE = IntegerTypeData(
DEFAULT_H_TYPE = IntegerTypeInformation(
dpcode=DPCode.COLOUR_DATA_HSV, min=1, scale=0, max=360, step=1
)
DEFAULT_S_TYPE = IntegerTypeData(
DEFAULT_S_TYPE = IntegerTypeInformation(
dpcode=DPCode.COLOUR_DATA_HSV, min=1, scale=0, max=255, step=1
)
DEFAULT_V_TYPE = IntegerTypeData(
DEFAULT_V_TYPE = IntegerTypeInformation(
dpcode=DPCode.COLOUR_DATA_HSV, min=1, scale=0, max=255, step=1
)


DEFAULT_H_TYPE_V2 = IntegerTypeData(
DEFAULT_H_TYPE_V2 = IntegerTypeInformation(
dpcode=DPCode.COLOUR_DATA_HSV, min=1, scale=0, max=360, step=1
)
DEFAULT_S_TYPE_V2 = IntegerTypeData(
DEFAULT_S_TYPE_V2 = IntegerTypeInformation(
dpcode=DPCode.COLOUR_DATA_HSV, min=1, scale=0, max=1000, step=1
)
DEFAULT_V_TYPE_V2 = IntegerTypeData(
DEFAULT_V_TYPE_V2 = IntegerTypeInformation(
dpcode=DPCode.COLOUR_DATA_HSV, min=1, scale=0, max=1000, step=1
)

Expand Down Expand Up @@ -578,15 +578,15 @@ def _get_color_data_wrapper(
if function_data := json_loads_object(
cast(str, color_data_wrapper.type_information.type_data)
):
color_data_wrapper.h_type = IntegerTypeData(
color_data_wrapper.h_type = IntegerTypeInformation(
dpcode=color_data_wrapper.dpcode,
**cast(dict, function_data["h"]),
)
color_data_wrapper.s_type = IntegerTypeData(
color_data_wrapper.s_type = IntegerTypeInformation(
dpcode=color_data_wrapper.dpcode,
**cast(dict, function_data["s"]),
)
color_data_wrapper.v_type = IntegerTypeData(
color_data_wrapper.v_type = IntegerTypeInformation(
dpcode=color_data_wrapper.dpcode,
**cast(dict, function_data["v"]),
)
Expand Down
234 changes: 13 additions & 221 deletions homeassistant/components/tuya/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,19 @@
from __future__ import annotations

import base64
from dataclasses import dataclass
from typing import Any, Literal, Self, cast, overload
from typing import Any, Self

from tuya_sharing import CustomerDevice

from homeassistant.util.json import json_loads, json_loads_object
from homeassistant.util.json import json_loads

from .const import LOGGER, DPType
from .util import parse_dptype, remap_value
from .type_information import (
EnumTypeInformation,
IntegerTypeInformation,
TypeInformation,
Comment thread
epenet marked this conversation as resolved.
find_dpcode,
)

# Dictionary to track logged warnings to avoid spamming logs
# Keyed by device ID
Expand All @@ -32,139 +36,6 @@ def _should_log_warning(device_id: str, warning_key: str) -> bool:
return True


@dataclass(kw_only=True)
class TypeInformation:
"""Type information.

As provided by the SDK, from `device.function` / `device.status_range`.
"""

dpcode: str
type_data: str | None = None

@classmethod
def from_json(cls, dpcode: str, type_data: str) -> Self | None:
"""Load JSON string and return a TypeInformation object."""
return cls(dpcode=dpcode, type_data=type_data)


@dataclass(kw_only=True)
class IntegerTypeData(TypeInformation):
"""Integer Type Data."""

min: int
max: int
scale: int
step: int
unit: str | None = None

@property
def max_scaled(self) -> float:
"""Return the max scaled."""
return self.scale_value(self.max)

@property
def min_scaled(self) -> float:
"""Return the min scaled."""
return self.scale_value(self.min)

@property
def step_scaled(self) -> float:
"""Return the step scaled."""
return self.step / (10**self.scale)

def scale_value(self, value: int) -> float:
"""Scale a value."""
return value / (10**self.scale)

def scale_value_back(self, value: float) -> int:
"""Return raw value for scaled."""
return round(value * (10**self.scale))

def remap_value_to(
self,
value: float,
to_min: float = 0,
to_max: float = 255,
reverse: bool = False,
) -> float:
"""Remap a value from this range to a new range."""
return remap_value(value, self.min, self.max, to_min, to_max, reverse)

def remap_value_from(
self,
value: float,
from_min: float = 0,
from_max: float = 255,
reverse: bool = False,
) -> float:
"""Remap a value from its current range to this range."""
return remap_value(value, from_min, from_max, self.min, self.max, reverse)

@classmethod
def from_json(cls, dpcode: str, type_data: str) -> Self | None:
"""Load JSON string and return a IntegerTypeData object."""
if not (parsed := cast(dict[str, Any] | None, json_loads_object(type_data))):
return None

return cls(
dpcode=dpcode,
type_data=type_data,
min=int(parsed["min"]),
max=int(parsed["max"]),
scale=int(parsed["scale"]),
step=int(parsed["step"]),
unit=parsed.get("unit"),
)


@dataclass(kw_only=True)
class BitmapTypeInformation(TypeInformation):
"""Bitmap type information."""

label: list[str]

@classmethod
def from_json(cls, dpcode: str, type_data: str) -> Self | None:
"""Load JSON string and return a BitmapTypeInformation object."""
if not (parsed := json_loads_object(type_data)):
return None
return cls(
dpcode=dpcode,
type_data=type_data,
**cast(dict[str, list[str]], parsed),
)


@dataclass(kw_only=True)
class EnumTypeData(TypeInformation):
"""Enum Type Data."""

range: list[str]

@classmethod
def from_json(cls, dpcode: str, type_data: str) -> Self | None:
"""Load JSON string and return a EnumTypeData object."""
if not (parsed := json_loads_object(type_data)):
return None
return cls(
dpcode=dpcode,
type_data=type_data,
**cast(dict[str, list[str]], parsed),
)


_TYPE_INFORMATION_MAPPINGS: dict[DPType, type[TypeInformation]] = {
DPType.BITMAP: BitmapTypeInformation,
DPType.BOOLEAN: TypeInformation,
DPType.ENUM: EnumTypeData,
DPType.INTEGER: IntegerTypeData,
DPType.JSON: TypeInformation,
DPType.RAW: TypeInformation,
DPType.STRING: TypeInformation,
}


class DeviceWrapper:
"""Base device wrapper."""

Expand Down Expand Up @@ -303,8 +174,8 @@ def read_json(self, device: CustomerDevice) -> Any | None:
return json_loads(raw_value)


class DPCodeEnumWrapper(DPCodeTypeInformationWrapper[EnumTypeData]):
"""Simple wrapper for EnumTypeData values."""
class DPCodeEnumWrapper(DPCodeTypeInformationWrapper[EnumTypeInformation]):
"""Simple wrapper for EnumTypeInformation values."""

DPTYPE = DPType.ENUM

Expand Down Expand Up @@ -342,12 +213,12 @@ def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any
)


class DPCodeIntegerWrapper(DPCodeTypeInformationWrapper[IntegerTypeData]):
"""Simple wrapper for IntegerTypeData values."""
class DPCodeIntegerWrapper(DPCodeTypeInformationWrapper[IntegerTypeInformation]):
"""Simple wrapper for IntegerTypeInformation values."""

DPTYPE = DPType.INTEGER

def __init__(self, dpcode: str, type_information: IntegerTypeData) -> None:
def __init__(self, dpcode: str, type_information: IntegerTypeInformation) -> None:
"""Init DPCodeIntegerWrapper."""
super().__init__(dpcode, type_information)
self.native_unit = type_information.unit
Expand Down Expand Up @@ -414,82 +285,3 @@ def find_dpcode(
type_information.dpcode, type_information.label.index(bitmap_key)
)
return None


@overload
def find_dpcode(
device: CustomerDevice,
dpcodes: str | tuple[str, ...] | None,
*,
prefer_function: bool = False,
dptype: Literal[DPType.BITMAP],
) -> BitmapTypeInformation | None: ...


@overload
def find_dpcode(
device: CustomerDevice,
dpcodes: str | tuple[str, ...] | None,
*,
prefer_function: bool = False,
dptype: Literal[DPType.ENUM],
) -> EnumTypeData | None: ...


@overload
def find_dpcode(
device: CustomerDevice,
dpcodes: str | tuple[str, ...] | None,
*,
prefer_function: bool = False,
dptype: Literal[DPType.INTEGER],
) -> IntegerTypeData | None: ...


@overload
def find_dpcode(
device: CustomerDevice,
dpcodes: str | tuple[str, ...] | None,
*,
prefer_function: bool = False,
dptype: Literal[DPType.BOOLEAN, DPType.JSON, DPType.RAW],
) -> TypeInformation | None: ...


def find_dpcode(
device: CustomerDevice,
dpcodes: str | tuple[str, ...] | None,
*,
prefer_function: bool = False,
dptype: DPType,
) -> TypeInformation | None:
"""Find type information for a matching DP code available for this device."""
if not (type_information_cls := _TYPE_INFORMATION_MAPPINGS.get(dptype)):
raise NotImplementedError(f"find_dpcode not supported for {dptype}")

if dpcodes is None:
return None

if not isinstance(dpcodes, tuple):
dpcodes = (dpcodes,)

lookup_tuple = (
(device.function, device.status_range)
if prefer_function
else (device.status_range, device.function)
)

for dpcode in dpcodes:
for device_specs in lookup_tuple:
if (
(current_definition := device_specs.get(dpcode))
and parse_dptype(current_definition.type) is dptype
and (
type_information := type_information_cls.from_json(
dpcode=dpcode, type_data=current_definition.values
)
)
):
return type_information

return None
4 changes: 1 addition & 3 deletions homeassistant/components/tuya/number.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
DPCode,
)
from .entity import TuyaEntity
from .models import DPCodeIntegerWrapper, IntegerTypeData
from .models import DPCodeIntegerWrapper

NUMBERS: dict[DeviceCategory, tuple[NumberEntityDescription, ...]] = {
DeviceCategory.BH: (
Expand Down Expand Up @@ -483,8 +483,6 @@ def async_discover_device(device_ids: list[str]) -> None:
class TuyaNumberEntity(TuyaEntity, NumberEntity):
"""Tuya Number Entity."""

_number: IntegerTypeData | None = None

Comment on lines -486 to -487
Copy link
Copy Markdown
Contributor Author

@epenet epenet Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The attribute is fully unused (missed in previous wrapper migration) - better to just remove it than update the import and the type name.

def __init__(
self,
device: CustomerDevice,
Expand Down
4 changes: 2 additions & 2 deletions homeassistant/components/tuya/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@
DPCodeJsonWrapper,
DPCodeTypeInformationWrapper,
DPCodeWrapper,
EnumTypeData,
)
from .raw_data_models import ElectricityData
from .type_information import EnumTypeInformation


class _WindDirectionWrapper(DPCodeTypeInformationWrapper[EnumTypeData]):
class _WindDirectionWrapper(DPCodeTypeInformationWrapper[EnumTypeInformation]):
"""Custom DPCode Wrapper for converting enum to wind direction."""

DPTYPE = DPType.ENUM
Expand Down
Loading
Loading