99
1010from homeassistant .util .json import json_loads_object
1111
12- from .const import DPType
12+ from .const import LOGGER , DPType
1313from .util import parse_dptype , remap_value
1414
15+ # Dictionary to track logged warnings to avoid spamming logs
16+ # Keyed by device ID
17+ DEVICE_WARNINGS : dict [str , set [str ]] = {}
18+
19+
20+ def _should_log_warning (device_id : str , warning_key : str ) -> bool :
21+ """Check if a warning has already been logged for a device and add it if not.
22+
23+ Returns: True if the warning should be logged, False if it was already logged.
24+ """
25+ if (device_warnings := DEVICE_WARNINGS .get (device_id )) is None :
26+ device_warnings = set ()
27+ DEVICE_WARNINGS [device_id ] = device_warnings
28+ if warning_key in device_warnings :
29+ return False
30+ DEVICE_WARNINGS [device_id ].add (warning_key )
31+ return True
32+
1533
1634@dataclass (kw_only = True )
17- class TypeInformation :
35+ class TypeInformation [ T ] :
1836 """Type information.
1937
2038 As provided by the SDK, from `device.function` / `device.status_range`.
@@ -23,14 +41,24 @@ class TypeInformation:
2341 dpcode : str
2442 type_data : str | None = None
2543
44+ def process_raw_value (
45+ self , raw_value : Any | None , device : CustomerDevice
46+ ) -> T | None :
47+ """Read and process raw value against this type information.
48+
49+ Base implementation does no validation, subclasses may override to provide
50+ specific validation.
51+ """
52+ return raw_value
53+
2654 @classmethod
2755 def from_json (cls , dpcode : str , type_data : str ) -> Self | None :
2856 """Load JSON string and return a TypeInformation object."""
2957 return cls (dpcode = dpcode , type_data = type_data )
3058
3159
3260@dataclass (kw_only = True )
33- class BitmapTypeInformation (TypeInformation ):
61+ class BitmapTypeInformation (TypeInformation [ int ] ):
3462 """Bitmap type information."""
3563
3664 label : list [str ]
@@ -48,11 +76,62 @@ def from_json(cls, dpcode: str, type_data: str) -> Self | None:
4876
4977
5078@dataclass (kw_only = True )
51- class EnumTypeInformation (TypeInformation ):
79+ class BooleanTypeInformation (TypeInformation [bool ]):
80+ """Boolean type information."""
81+
82+ def process_raw_value (
83+ self , raw_value : Any | None , device : CustomerDevice
84+ ) -> bool | None :
85+ """Read and process raw value against this type information."""
86+ if raw_value is None :
87+ return None
88+ # Validate input against defined range
89+ if raw_value not in (True , False ):
90+ if _should_log_warning (
91+ device .id , f"boolean_out_range|{ self .dpcode } |{ raw_value } "
92+ ):
93+ LOGGER .warning (
94+ "Found invalid boolean value `%s` for datapoint `%s` in product "
95+ "id `%s`, expected one of `%s`; please report this defect to "
96+ "Tuya support" ,
97+ raw_value ,
98+ self .dpcode ,
99+ device .product_id ,
100+ (True , False ),
101+ )
102+ return None
103+ return raw_value
104+
105+
106+ @dataclass (kw_only = True )
107+ class EnumTypeInformation (TypeInformation [str ]):
52108 """Enum type information."""
53109
54110 range : list [str ]
55111
112+ def process_raw_value (
113+ self , raw_value : Any | None , device : CustomerDevice
114+ ) -> str | None :
115+ """Read and process raw value against this type information."""
116+ if raw_value is None :
117+ return None
118+ # Validate input against defined range
119+ if raw_value not in self .range :
120+ if _should_log_warning (
121+ device .id , f"enum_out_range|{ self .dpcode } |{ raw_value } "
122+ ):
123+ LOGGER .warning (
124+ "Found invalid enum value `%s` for datapoint `%s` in product "
125+ "id `%s`, expected one of `%s`; please report this defect to "
126+ "Tuya support" ,
127+ raw_value ,
128+ self .dpcode ,
129+ device .product_id ,
130+ self .range ,
131+ )
132+ return None
133+ return raw_value
134+
56135 @classmethod
57136 def from_json (cls , dpcode : str , type_data : str ) -> Self | None :
58137 """Load JSON string and return an EnumTypeInformation object."""
@@ -66,7 +145,7 @@ def from_json(cls, dpcode: str, type_data: str) -> Self | None:
66145
67146
68147@dataclass (kw_only = True )
69- class IntegerTypeInformation (TypeInformation ):
148+ class IntegerTypeInformation (TypeInformation [ float ] ):
70149 """Integer type information."""
71150
72151 min : int
@@ -118,6 +197,31 @@ def remap_value_from(
118197 """Remap a value from its current range to this range."""
119198 return remap_value (value , from_min , from_max , self .min , self .max , reverse )
120199
200+ def process_raw_value (
201+ self , raw_value : Any | None , device : CustomerDevice
202+ ) -> float | None :
203+ """Read and process raw value against this type information."""
204+ if raw_value is None :
205+ return None
206+ # Validate input against defined range
207+ if not isinstance (raw_value , int ) or not (self .min <= raw_value <= self .max ):
208+ if _should_log_warning (
209+ device .id , f"integer_out_range|{ self .dpcode } |{ raw_value } "
210+ ):
211+ LOGGER .warning (
212+ "Found invalid integer value `%s` for datapoint `%s` in product "
213+ "id `%s`, expected integer value between %s and %s; please report "
214+ "this defect to Tuya support" ,
215+ raw_value ,
216+ self .dpcode ,
217+ device .product_id ,
218+ self .min ,
219+ self .max ,
220+ )
221+
222+ return None
223+ return raw_value / (10 ** self .scale )
224+
121225 @classmethod
122226 def from_json (cls , dpcode : str , type_data : str ) -> Self | None :
123227 """Load JSON string and return an IntegerTypeInformation object."""
@@ -137,7 +241,7 @@ def from_json(cls, dpcode: str, type_data: str) -> Self | None:
137241
138242_TYPE_INFORMATION_MAPPINGS : dict [DPType , type [TypeInformation ]] = {
139243 DPType .BITMAP : BitmapTypeInformation ,
140- DPType .BOOLEAN : TypeInformation ,
244+ DPType .BOOLEAN : BooleanTypeInformation ,
141245 DPType .ENUM : EnumTypeInformation ,
142246 DPType .INTEGER : IntegerTypeInformation ,
143247 DPType .JSON : TypeInformation ,
@@ -156,6 +260,16 @@ def find_dpcode(
156260) -> BitmapTypeInformation | None : ...
157261
158262
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+
159273@overload
160274def find_dpcode (
161275 device : CustomerDevice ,
@@ -182,7 +296,7 @@ def find_dpcode(
182296 dpcodes : str | tuple [str , ...] | None ,
183297 * ,
184298 prefer_function : bool = False ,
185- dptype : Literal [DPType .BOOLEAN , DPType . JSON , DPType .RAW ],
299+ dptype : Literal [DPType .JSON , DPType .RAW ],
186300) -> TypeInformation | None : ...
187301
188302
0 commit comments