Skip to content

Commit 936151f

Browse files
authored
Use dpcode_wrapper in tuya sensor platform (home-assistant#156277)
1 parent 9760eb7 commit 936151f

File tree

5 files changed

+475
-231
lines changed

5 files changed

+475
-231
lines changed

homeassistant/components/tuya/models.py

Lines changed: 50 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
import base64
77
from dataclasses import dataclass
88
import json
9-
import struct
109
from typing import Any, Literal, Self, overload
1110

1211
from tuya_sharing import CustomerDevice
1312

13+
from homeassistant.util.json import json_loads
14+
1415
from .const import DPCode, DPType
15-
from .util import remap_value
16+
from .util import parse_dptype, remap_value
1617

1718

1819
@dataclass
@@ -134,6 +135,8 @@ def from_json(cls, dpcode: DPCode, data: str) -> Self | None:
134135
DPType.BOOLEAN: TypeInformation,
135136
DPType.ENUM: EnumTypeData,
136137
DPType.INTEGER: IntegerTypeData,
138+
DPType.JSON: TypeInformation,
139+
DPType.RAW: TypeInformation,
137140
}
138141

139142

@@ -144,6 +147,9 @@ class DPCodeWrapper(ABC):
144147
access read conversion routines.
145148
"""
146149

150+
native_unit: str | None = None
151+
suggested_unit: str | None = None
152+
147153
def __init__(self, dpcode: str) -> None:
148154
"""Init DPCodeWrapper."""
149155
self.dpcode = dpcode
@@ -210,6 +216,20 @@ def find_dpcode(
210216
return None
211217

212218

219+
class DPCodeBase64Wrapper(DPCodeTypeInformationWrapper[TypeInformation]):
220+
"""Wrapper to extract information from a RAW/binary value."""
221+
222+
DPTYPE = DPType.RAW
223+
224+
def read_bytes(self, device: CustomerDevice) -> bytes | None:
225+
"""Read the device value for the dpcode."""
226+
if (raw_value := self._read_device_status_raw(device)) is None or (
227+
len(decoded := base64.b64decode(raw_value)) == 0
228+
):
229+
return None
230+
return decoded
231+
232+
213233
class DPCodeBooleanWrapper(DPCodeTypeInformationWrapper[TypeInformation]):
214234
"""Simple wrapper for boolean values.
215235
@@ -235,6 +255,18 @@ def _convert_value_to_raw_value(
235255
raise ValueError(f"Invalid boolean value `{value}`")
236256

237257

258+
class DPCodeJsonWrapper(DPCodeTypeInformationWrapper[TypeInformation]):
259+
"""Wrapper to extract information from a JSON value."""
260+
261+
DPTYPE = DPType.JSON
262+
263+
def read_json(self, device: CustomerDevice) -> Any | None:
264+
"""Read the device value for the dpcode."""
265+
if (raw_value := self._read_device_status_raw(device)) is None:
266+
return None
267+
return json_loads(raw_value)
268+
269+
238270
class DPCodeEnumWrapper(DPCodeTypeInformationWrapper[EnumTypeData]):
239271
"""Simple wrapper for EnumTypeData values."""
240272

@@ -268,6 +300,11 @@ class DPCodeIntegerWrapper(DPCodeTypeInformationWrapper[IntegerTypeData]):
268300

269301
DPTYPE = DPType.INTEGER
270302

303+
def __init__(self, dpcode: str, type_information: IntegerTypeData) -> None:
304+
"""Init DPCodeIntegerWrapper."""
305+
super().__init__(dpcode, type_information)
306+
self.native_unit = type_information.unit
307+
271308
def read_device_status(self, device: CustomerDevice) -> float | None:
272309
"""Read the device value for the dpcode.
273310
@@ -352,6 +389,16 @@ def find_dpcode(
352389
) -> IntegerTypeData | None: ...
353390

354391

392+
@overload
393+
def find_dpcode(
394+
device: CustomerDevice,
395+
dpcodes: str | DPCode | tuple[DPCode, ...] | None,
396+
*,
397+
prefer_function: bool = False,
398+
dptype: Literal[DPType.BOOLEAN, DPType.JSON, DPType.RAW],
399+
) -> TypeInformation | None: ...
400+
401+
355402
def find_dpcode(
356403
device: CustomerDevice,
357404
dpcodes: str | DPCode | tuple[DPCode, ...] | None,
@@ -381,7 +428,7 @@ def find_dpcode(
381428
for device_specs in lookup_tuple:
382429
if (
383430
(current_definition := device_specs.get(dpcode))
384-
and current_definition.type == dptype
431+
and parse_dptype(current_definition.type) is dptype
385432
and (
386433
type_information := type_information_cls.from_json(
387434
dpcode, current_definition.values
@@ -391,44 +438,3 @@ def find_dpcode(
391438
return type_information
392439

393440
return None
394-
395-
396-
class ComplexValue:
397-
"""Complex value (for JSON/RAW parsing)."""
398-
399-
@classmethod
400-
def from_json(cls, data: str) -> Self:
401-
"""Load JSON string and return a ComplexValue object."""
402-
raise NotImplementedError("from_json is not implemented for this type")
403-
404-
@classmethod
405-
def from_raw(cls, data: str) -> Self | None:
406-
"""Decode base64 string and return a ComplexValue object."""
407-
raise NotImplementedError("from_raw is not implemented for this type")
408-
409-
410-
@dataclass
411-
class ElectricityValue(ComplexValue):
412-
"""Electricity complex value."""
413-
414-
electriccurrent: str | None = None
415-
power: str | None = None
416-
voltage: str | None = None
417-
418-
@classmethod
419-
def from_json(cls, data: str) -> Self:
420-
"""Load JSON string and return a ElectricityValue object."""
421-
return cls(**json.loads(data.lower()))
422-
423-
@classmethod
424-
def from_raw(cls, data: str) -> Self | None:
425-
"""Decode base64 string and return a ElectricityValue object."""
426-
raw = base64.b64decode(data)
427-
if len(raw) == 0:
428-
return None
429-
voltage = struct.unpack(">H", raw[0:2])[0] / 10.0
430-
electriccurrent = struct.unpack(">L", b"\x00" + raw[2:5])[0] / 1000.0
431-
power = struct.unpack(">L", b"\x00" + raw[5:8])[0] / 1000.0
432-
return cls(
433-
electriccurrent=str(electriccurrent), power=str(power), voltage=str(voltage)
434-
)

homeassistant/components/tuya/number.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -502,14 +502,19 @@ def __init__(
502502
self._attr_native_min_value = dpcode_wrapper.type_information.min_scaled
503503
self._attr_native_step = dpcode_wrapper.type_information.step_scaled
504504
if description.native_unit_of_measurement is None:
505-
self._attr_native_unit_of_measurement = dpcode_wrapper.type_information.unit
505+
self._attr_native_unit_of_measurement = dpcode_wrapper.native_unit
506+
507+
self._validate_device_class_unit()
508+
509+
def _validate_device_class_unit(self) -> None:
510+
"""Validate device class unit compatibility."""
506511

507512
# Logic to ensure the set device class and API received Unit Of Measurement
508513
# match Home Assistants requirements.
509514
if (
510515
self.device_class is not None
511516
and not self.device_class.startswith(DOMAIN)
512-
and description.native_unit_of_measurement is None
517+
and self.entity_description.native_unit_of_measurement is None
513518
# we do not need to check mappings if the API UOM is allowed
514519
and self.native_unit_of_measurement
515520
not in NUMBER_DEVICE_CLASS_UNITS[self.device_class]

0 commit comments

Comments
 (0)