Skip to content

Commit 9cb9efe

Browse files
authored
Make Tuya find_dpcode a class method (home-assistant#158028)
1 parent ca31134 commit 9cb9efe

File tree

3 files changed

+87
-119
lines changed

3 files changed

+87
-119
lines changed

homeassistant/components/tuya/models.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@
99

1010
from homeassistant.util.json import json_loads
1111

12-
from .const import DPType
1312
from .type_information import (
13+
BitmapTypeInformation,
14+
BooleanTypeInformation,
1415
EnumTypeInformation,
1516
IntegerTypeInformation,
17+
JsonTypeInformation,
18+
RawTypeInformation,
19+
StringTypeInformation,
1620
TypeInformation,
17-
find_dpcode,
1821
)
1922

2023

@@ -79,7 +82,7 @@ def get_update_commands(
7982
class DPCodeTypeInformationWrapper[T: TypeInformation](DPCodeWrapper):
8083
"""Base DPCode wrapper with Type Information."""
8184

82-
DPTYPE: DPType
85+
_DPTYPE: type[T]
8386
type_information: T
8487

8588
def __init__(self, dpcode: str, type_information: T) -> None:
@@ -102,19 +105,19 @@ def find_dpcode(
102105
prefer_function: bool = False,
103106
) -> Self | None:
104107
"""Find and return a DPCodeTypeInformationWrapper for the given DP codes."""
105-
if type_information := find_dpcode( # type: ignore[call-overload]
106-
device, dpcodes, dptype=cls.DPTYPE, prefer_function=prefer_function
108+
if type_information := cls._DPTYPE.find_dpcode(
109+
device, dpcodes, prefer_function=prefer_function
107110
):
108111
return cls(
109112
dpcode=type_information.dpcode, type_information=type_information
110113
)
111114
return None
112115

113116

114-
class DPCodeBase64Wrapper(DPCodeTypeInformationWrapper[TypeInformation]):
117+
class DPCodeBase64Wrapper(DPCodeTypeInformationWrapper[RawTypeInformation]):
115118
"""Wrapper to extract information from a RAW/binary value."""
116119

117-
DPTYPE = DPType.RAW
120+
_DPTYPE = RawTypeInformation
118121

119122
def read_bytes(self, device: CustomerDevice) -> bytes | None:
120123
"""Read the device value for the dpcode."""
@@ -125,13 +128,13 @@ def read_bytes(self, device: CustomerDevice) -> bytes | None:
125128
return decoded
126129

127130

128-
class DPCodeBooleanWrapper(DPCodeTypeInformationWrapper[TypeInformation]):
131+
class DPCodeBooleanWrapper(DPCodeTypeInformationWrapper[BooleanTypeInformation]):
129132
"""Simple wrapper for boolean values.
130133
131134
Supports True/False only.
132135
"""
133136

134-
DPTYPE = DPType.BOOLEAN
137+
_DPTYPE = BooleanTypeInformation
135138

136139
def _convert_value_to_raw_value(
137140
self, device: CustomerDevice, value: Any
@@ -144,10 +147,10 @@ def _convert_value_to_raw_value(
144147
raise ValueError(f"Invalid boolean value `{value}`")
145148

146149

147-
class DPCodeJsonWrapper(DPCodeTypeInformationWrapper[TypeInformation]):
150+
class DPCodeJsonWrapper(DPCodeTypeInformationWrapper[JsonTypeInformation]):
148151
"""Wrapper to extract information from a JSON value."""
149152

150-
DPTYPE = DPType.JSON
153+
_DPTYPE = JsonTypeInformation
151154

152155
def read_json(self, device: CustomerDevice) -> Any | None:
153156
"""Read the device value for the dpcode."""
@@ -159,7 +162,7 @@ def read_json(self, device: CustomerDevice) -> Any | None:
159162
class DPCodeEnumWrapper(DPCodeTypeInformationWrapper[EnumTypeInformation]):
160163
"""Simple wrapper for EnumTypeInformation values."""
161164

162-
DPTYPE = DPType.ENUM
165+
_DPTYPE = EnumTypeInformation
163166

164167
def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any:
165168
"""Convert a Home Assistant value back to a raw device value."""
@@ -175,7 +178,7 @@ def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any
175178
class DPCodeIntegerWrapper(DPCodeTypeInformationWrapper[IntegerTypeInformation]):
176179
"""Simple wrapper for IntegerTypeInformation values."""
177180

178-
DPTYPE = DPType.INTEGER
181+
_DPTYPE = IntegerTypeInformation
179182

180183
def __init__(self, dpcode: str, type_information: IntegerTypeInformation) -> None:
181184
"""Init DPCodeIntegerWrapper."""
@@ -195,10 +198,10 @@ def _convert_value_to_raw_value(self, device: CustomerDevice, value: Any) -> Any
195198
)
196199

197200

198-
class DPCodeStringWrapper(DPCodeTypeInformationWrapper[TypeInformation]):
201+
class DPCodeStringWrapper(DPCodeTypeInformationWrapper[StringTypeInformation]):
199202
"""Wrapper to extract information from a STRING value."""
200203

201-
DPTYPE = DPType.STRING
204+
_DPTYPE = StringTypeInformation
202205

203206

204207
class DPCodeBitmapBitWrapper(DPCodeWrapper):
@@ -225,7 +228,7 @@ def find_dpcode(
225228
) -> Self | None:
226229
"""Find and return a DPCodeBitmapBitWrapper for the given DP codes."""
227230
if (
228-
type_information := find_dpcode(device, dpcodes, dptype=DPType.BITMAP)
231+
type_information := BitmapTypeInformation.find_dpcode(device, dpcodes)
229232
) and bitmap_key in type_information.label:
230233
return cls(
231234
type_information.dpcode, type_information.label.index(bitmap_key)

homeassistant/components/tuya/sensor.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@
3636
TUYA_DISCOVERY_NEW,
3737
DeviceCategory,
3838
DPCode,
39-
DPType,
4039
)
4140
from .entity import TuyaEntity
4241
from .models import (
@@ -54,7 +53,7 @@
5453
class _WindDirectionWrapper(DPCodeTypeInformationWrapper[EnumTypeInformation]):
5554
"""Custom DPCode Wrapper for converting enum to wind direction."""
5655

57-
DPTYPE = DPType.ENUM
56+
_DPTYPE = EnumTypeInformation
5857

5958
_WIND_DIRECTIONS = {
6059
"north": 0.0,

homeassistant/components/tuya/type_information.py

Lines changed: 67 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
from dataclasses import dataclass
6-
from typing import Any, Literal, Self, cast, overload
6+
from typing import Any, ClassVar, Self, cast
77

88
from tuya_sharing import CustomerDevice
99

@@ -38,6 +38,7 @@ class TypeInformation[T]:
3838
As provided by the SDK, from `device.function` / `device.status_range`.
3939
"""
4040

41+
_DPTYPE: ClassVar[DPType]
4142
dpcode: str
4243
type_data: str | None = None
4344

@@ -52,19 +53,57 @@ def process_raw_value(
5253
return raw_value
5354

5455
@classmethod
55-
def from_json(cls, dpcode: str, type_data: str) -> Self | None:
56+
def _from_json(cls, dpcode: str, type_data: str) -> Self | None:
5657
"""Load JSON string and return a TypeInformation object."""
5758
return cls(dpcode=dpcode, type_data=type_data)
5859

60+
@classmethod
61+
def find_dpcode(
62+
cls,
63+
device: CustomerDevice,
64+
dpcodes: str | tuple[str, ...] | None,
65+
*,
66+
prefer_function: bool = False,
67+
) -> Self | None:
68+
"""Find type information for a matching DP code available for this device."""
69+
if dpcodes is None:
70+
return None
71+
72+
if not isinstance(dpcodes, tuple):
73+
dpcodes = (dpcodes,)
74+
75+
lookup_tuple = (
76+
(device.function, device.status_range)
77+
if prefer_function
78+
else (device.status_range, device.function)
79+
)
80+
81+
for dpcode in dpcodes:
82+
for device_specs in lookup_tuple:
83+
if (
84+
(current_definition := device_specs.get(dpcode))
85+
and parse_dptype(current_definition.type) is cls._DPTYPE
86+
and (
87+
type_information := cls._from_json(
88+
dpcode=dpcode, type_data=current_definition.values
89+
)
90+
)
91+
):
92+
return type_information
93+
94+
return None
95+
5996

6097
@dataclass(kw_only=True)
6198
class BitmapTypeInformation(TypeInformation[int]):
6299
"""Bitmap type information."""
63100

101+
_DPTYPE = DPType.BITMAP
102+
64103
label: list[str]
65104

66105
@classmethod
67-
def from_json(cls, dpcode: str, type_data: str) -> Self | None:
106+
def _from_json(cls, dpcode: str, type_data: str) -> Self | None:
68107
"""Load JSON string and return a BitmapTypeInformation object."""
69108
if not (parsed := json_loads_object(type_data)):
70109
return None
@@ -79,6 +118,8 @@ def from_json(cls, dpcode: str, type_data: str) -> Self | None:
79118
class BooleanTypeInformation(TypeInformation[bool]):
80119
"""Boolean type information."""
81120

121+
_DPTYPE = DPType.BOOLEAN
122+
82123
def process_raw_value(
83124
self, raw_value: Any | None, device: CustomerDevice
84125
) -> bool | None:
@@ -107,6 +148,8 @@ def process_raw_value(
107148
class EnumTypeInformation(TypeInformation[str]):
108149
"""Enum type information."""
109150

151+
_DPTYPE = DPType.ENUM
152+
110153
range: list[str]
111154

112155
def process_raw_value(
@@ -133,7 +176,7 @@ def process_raw_value(
133176
return raw_value
134177

135178
@classmethod
136-
def from_json(cls, dpcode: str, type_data: str) -> Self | None:
179+
def _from_json(cls, dpcode: str, type_data: str) -> Self | None:
137180
"""Load JSON string and return an EnumTypeInformation object."""
138181
if not (parsed := json_loads_object(type_data)):
139182
return None
@@ -148,6 +191,8 @@ def from_json(cls, dpcode: str, type_data: str) -> Self | None:
148191
class IntegerTypeInformation(TypeInformation[float]):
149192
"""Integer type information."""
150193

194+
_DPTYPE = DPType.INTEGER
195+
151196
min: int
152197
max: int
153198
scale: int
@@ -223,7 +268,7 @@ def process_raw_value(
223268
return raw_value / (10**self.scale)
224269

225270
@classmethod
226-
def from_json(cls, dpcode: str, type_data: str) -> Self | None:
271+
def _from_json(cls, dpcode: str, type_data: str) -> Self | None:
227272
"""Load JSON string and return an IntegerTypeInformation object."""
228273
if not (parsed := cast(dict[str, Any] | None, json_loads_object(type_data))):
229274
return None
@@ -239,101 +284,22 @@ def from_json(cls, dpcode: str, type_data: str) -> Self | None:
239284
)
240285

241286

242-
_TYPE_INFORMATION_MAPPINGS: dict[DPType, type[TypeInformation]] = {
243-
DPType.BITMAP: BitmapTypeInformation,
244-
DPType.BOOLEAN: BooleanTypeInformation,
245-
DPType.ENUM: EnumTypeInformation,
246-
DPType.INTEGER: IntegerTypeInformation,
247-
DPType.JSON: TypeInformation,
248-
DPType.RAW: TypeInformation,
249-
DPType.STRING: TypeInformation,
250-
}
251-
252-
253-
@overload
254-
def find_dpcode(
255-
device: CustomerDevice,
256-
dpcodes: str | tuple[str, ...] | None,
257-
*,
258-
prefer_function: bool = False,
259-
dptype: Literal[DPType.BITMAP],
260-
) -> BitmapTypeInformation | None: ...
261-
262-
263-
@overload
264-
def find_dpcode(
265-
device: CustomerDevice,
266-
dpcodes: str | tuple[str, ...] | None,
267-
*,
268-
prefer_function: bool = False,
269-
dptype: Literal[DPType.BOOLEAN],
270-
) -> BooleanTypeInformation | None: ...
271-
272-
273-
@overload
274-
def find_dpcode(
275-
device: CustomerDevice,
276-
dpcodes: str | tuple[str, ...] | None,
277-
*,
278-
prefer_function: bool = False,
279-
dptype: Literal[DPType.ENUM],
280-
) -> EnumTypeInformation | None: ...
281-
282-
283-
@overload
284-
def find_dpcode(
285-
device: CustomerDevice,
286-
dpcodes: str | tuple[str, ...] | None,
287-
*,
288-
prefer_function: bool = False,
289-
dptype: Literal[DPType.INTEGER],
290-
) -> IntegerTypeInformation | None: ...
291-
292-
293-
@overload
294-
def find_dpcode(
295-
device: CustomerDevice,
296-
dpcodes: str | tuple[str, ...] | None,
297-
*,
298-
prefer_function: bool = False,
299-
dptype: Literal[DPType.JSON, DPType.RAW],
300-
) -> TypeInformation | None: ...
301-
302-
303-
def find_dpcode(
304-
device: CustomerDevice,
305-
dpcodes: str | tuple[str, ...] | None,
306-
*,
307-
prefer_function: bool = False,
308-
dptype: DPType,
309-
) -> TypeInformation | None:
310-
"""Find type information for a matching DP code available for this device."""
311-
if not (type_information_cls := _TYPE_INFORMATION_MAPPINGS.get(dptype)):
312-
raise NotImplementedError(f"find_dpcode not supported for {dptype}")
313-
314-
if dpcodes is None:
315-
return None
287+
@dataclass(kw_only=True)
288+
class JsonTypeInformation(TypeInformation[Any]):
289+
"""Json type information."""
316290

317-
if not isinstance(dpcodes, tuple):
318-
dpcodes = (dpcodes,)
319-
320-
lookup_tuple = (
321-
(device.function, device.status_range)
322-
if prefer_function
323-
else (device.status_range, device.function)
324-
)
325-
326-
for dpcode in dpcodes:
327-
for device_specs in lookup_tuple:
328-
if (
329-
(current_definition := device_specs.get(dpcode))
330-
and parse_dptype(current_definition.type) is dptype
331-
and (
332-
type_information := type_information_cls.from_json(
333-
dpcode=dpcode, type_data=current_definition.values
334-
)
335-
)
336-
):
337-
return type_information
291+
_DPTYPE = DPType.JSON
292+
293+
294+
@dataclass(kw_only=True)
295+
class RawTypeInformation(TypeInformation[Any]):
296+
"""Raw type information."""
297+
298+
_DPTYPE = DPType.RAW
299+
300+
301+
@dataclass(kw_only=True)
302+
class StringTypeInformation(TypeInformation[str]):
303+
"""String type information."""
338304

339-
return None
305+
_DPTYPE = DPType.STRING

0 commit comments

Comments
 (0)