Skip to content

Commit ec69efe

Browse files
authored
Fix parsing of Tuya electricity RAW values (home-assistant#157039)
1 parent dbcde54 commit ec69efe

File tree

3 files changed

+92
-22
lines changed

3 files changed

+92
-22
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""Parsers for RAW (base64-encoded bytes) values."""
2+
3+
from dataclasses import dataclass
4+
import struct
5+
from typing import Self
6+
7+
8+
@dataclass(kw_only=True)
9+
class ElectricityData:
10+
"""Electricity RAW value."""
11+
12+
current: float
13+
power: float
14+
voltage: float
15+
16+
@classmethod
17+
def from_bytes(cls, raw: bytes) -> Self | None:
18+
"""Parse bytes and return an ElectricityValue object."""
19+
# Format:
20+
# - legacy: 8 bytes
21+
# - v01: [ver=0x01][len=0x0F][data(15 bytes)]
22+
# - v02: [ver=0x02][len=0x0F][data(15 bytes)][sign_bitmap(1 byte)]
23+
# Data layout (big-endian):
24+
# - voltage: 2B, unit 0.1 V
25+
# - current: 3B, unit 0.001 A (i.e., mA)
26+
# - active power: 3B, unit 0.001 kW (i.e., W)
27+
# - reactive power: 3B, unit 0.001 kVar
28+
# - apparent power: 3B, unit 0.001 kVA
29+
# - power factor: 1B, unit 0.01
30+
# Sign bitmap (v02 only, 1 bit means negative):
31+
# - bit0 current
32+
# - bit1 active power
33+
# - bit2 reactive
34+
# - bit3 power factor
35+
36+
is_v1 = len(raw) == 17 and raw[0:2] == b"\x01\x0f"
37+
is_v2 = len(raw) == 18 and raw[0:2] == b"\x02\x0f"
38+
if is_v1 or is_v2:
39+
data = raw[2:17]
40+
41+
voltage = struct.unpack(">H", data[0:2])[0] / 10.0
42+
current = struct.unpack(">L", b"\x00" + data[2:5])[0]
43+
power = struct.unpack(">L", b"\x00" + data[5:8])[0]
44+
45+
if is_v2:
46+
sign_bitmap = raw[17]
47+
if sign_bitmap & 0x01:
48+
current = -current
49+
if sign_bitmap & 0x02:
50+
power = -power
51+
52+
return cls(current=current, power=power, voltage=voltage)
53+
54+
if len(raw) >= 8:
55+
voltage = struct.unpack(">H", raw[0:2])[0] / 10.0
56+
current = struct.unpack(">L", b"\x00" + raw[2:5])[0]
57+
power = struct.unpack(">L", b"\x00" + raw[5:8])[0]
58+
return cls(current=current, power=power, voltage=voltage)
59+
60+
return None

homeassistant/components/tuya/sensor.py

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

55
from dataclasses import dataclass
6-
import struct
76

87
from tuya_sharing import CustomerDevice, Manager
98

@@ -49,6 +48,7 @@
4948
DPCodeWrapper,
5049
EnumTypeData,
5150
)
51+
from .raw_data_models import ElectricityData
5252

5353

5454
class _WindDirectionWrapper(DPCodeTypeInformationWrapper[EnumTypeData]):
@@ -120,42 +120,52 @@ def read_device_status(self, device: CustomerDevice) -> float | None:
120120
return raw_value.get("voltage")
121121

122122

123-
class _RawElectricityCurrentWrapper(DPCodeBase64Wrapper):
124-
"""Custom DPCode Wrapper for extracting electricity current from base64."""
123+
class _RawElectricityDataWrapper(DPCodeBase64Wrapper):
124+
"""Custom DPCode Wrapper for extracting ElectricityData from base64."""
125125

126-
native_unit = UnitOfElectricCurrent.MILLIAMPERE
127-
suggested_unit = UnitOfElectricCurrent.AMPERE
126+
def _convert(self, value: ElectricityData) -> float:
127+
"""Extract specific value from T."""
128+
raise NotImplementedError
128129

129130
def read_device_status(self, device: CustomerDevice) -> float | None:
130131
"""Read the device value for the dpcode."""
131-
if (raw_value := super().read_bytes(device)) is None:
132+
if (raw_value := super().read_bytes(device)) is None or (
133+
value := ElectricityData.from_bytes(raw_value)
134+
) is None:
132135
return None
133-
return struct.unpack(">L", b"\x00" + raw_value[2:5])[0]
136+
return self._convert(value)
137+
138+
139+
class _RawElectricityCurrentWrapper(_RawElectricityDataWrapper):
140+
"""Custom DPCode Wrapper for extracting electricity current from base64."""
141+
142+
native_unit = UnitOfElectricCurrent.MILLIAMPERE
143+
suggested_unit = UnitOfElectricCurrent.AMPERE
144+
145+
def _convert(self, value: ElectricityData) -> float:
146+
"""Extract specific value from ElectricityData."""
147+
return value.current
134148

135149

136-
class _RawElectricityPowerWrapper(DPCodeBase64Wrapper):
150+
class _RawElectricityPowerWrapper(_RawElectricityDataWrapper):
137151
"""Custom DPCode Wrapper for extracting electricity power from base64."""
138152

139153
native_unit = UnitOfPower.WATT
140154
suggested_unit = UnitOfPower.KILO_WATT
141155

142-
def read_device_status(self, device: CustomerDevice) -> float | None:
143-
"""Read the device value for the dpcode."""
144-
if (raw_value := super().read_bytes(device)) is None:
145-
return None
146-
return struct.unpack(">L", b"\x00" + raw_value[5:8])[0]
156+
def _convert(self, value: ElectricityData) -> float:
157+
"""Extract specific value from ElectricityData."""
158+
return value.power
147159

148160

149-
class _RawElectricityVoltageWrapper(DPCodeBase64Wrapper):
161+
class _RawElectricityVoltageWrapper(_RawElectricityDataWrapper):
150162
"""Custom DPCode Wrapper for extracting electricity voltage from base64."""
151163

152164
native_unit = UnitOfElectricPotential.VOLT
153165

154-
def read_device_status(self, device: CustomerDevice) -> float | None:
155-
"""Read the device value for the dpcode."""
156-
if (raw_value := super().read_bytes(device)) is None:
157-
return None
158-
return struct.unpack(">H", raw_value[0:2])[0] / 10.0
166+
def _convert(self, value: ElectricityData) -> float:
167+
"""Extract specific value from ElectricityData."""
168+
return value.voltage
159169

160170

161171
CURRENT_WRAPPER = (_RawElectricityCurrentWrapper, _JsonElectricityCurrentWrapper)

tests/components/tuya/snapshots/test_sensor.ambr

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5630,7 +5630,7 @@
56305630
'last_changed': <ANY>,
56315631
'last_reported': <ANY>,
56325632
'last_updated': <ANY>,
5633-
'state': '599.296',
5633+
'state': '0.072',
56345634
})
56355635
# ---
56365636
# name: test_platform_setup_and_discovery[sensor.duan_lu_qi_ha_phase_a_power-entry]
@@ -5689,7 +5689,7 @@
56895689
'last_changed': <ANY>,
56905690
'last_reported': <ANY>,
56915691
'last_updated': <ANY>,
5692-
'state': '18.432',
5692+
'state': '0.008',
56935693
})
56945694
# ---
56955695
# name: test_platform_setup_and_discovery[sensor.duan_lu_qi_ha_phase_a_voltage-entry]
@@ -5745,7 +5745,7 @@
57455745
'last_changed': <ANY>,
57465746
'last_reported': <ANY>,
57475747
'last_updated': <ANY>,
5748-
'state': '52.7',
5748+
'state': '234.1',
57495749
})
57505750
# ---
57515751
# name: test_platform_setup_and_discovery[sensor.duan_lu_qi_ha_supply_frequency-entry]

0 commit comments

Comments
 (0)