22
33from __future__ import annotations
44
5+ from abc import ABC , abstractmethod
56import base64
67from dataclasses import dataclass
78import json
@@ -120,7 +121,7 @@ def from_json(cls, dpcode: DPCode, data: str) -> Self | None:
120121}
121122
122123
123- class DPCodeWrapper :
124+ class DPCodeWrapper ( ABC ) :
124125 """Base DPCode wrapper.
125126
126127 Used as a common interface for referring to a DPCode, and
@@ -138,9 +139,30 @@ def _read_device_status_raw(self, device: CustomerDevice) -> Any | None:
138139 """
139140 return device .status .get (self .dpcode )
140141
142+ @abstractmethod
141143 def read_device_status (self , device : CustomerDevice ) -> Any | None :
142- """Read the device value for the dpcode."""
143- raise NotImplementedError ("read_device_status must be implemented" )
144+ """Read the device value for the dpcode.
145+
146+ The raw device status is converted to a Home Assistant value.
147+ """
148+
149+ @abstractmethod
150+ def _convert_value_to_raw_value (self , device : CustomerDevice , value : Any ) -> Any :
151+ """Convert a Home Assistant value back to a raw device value.
152+
153+ This is called by `get_update_command` to prepare the value for sending
154+ back to the device, and should be implemented in concrete classes.
155+ """
156+
157+ def get_update_command (self , device : CustomerDevice , value : Any ) -> dict [str , Any ]:
158+ """Get the update command for the dpcode.
159+
160+ The Home Assistant value is converted back to a raw device value.
161+ """
162+ return {
163+ "code" : self .dpcode ,
164+ "value" : self ._convert_value_to_raw_value (device , value ),
165+ }
144166
145167
146168class DPCodeBooleanWrapper (DPCodeWrapper ):
@@ -155,6 +177,16 @@ def read_device_status(self, device: CustomerDevice) -> bool | None:
155177 return raw_value
156178 return None
157179
180+ def _convert_value_to_raw_value (
181+ self , device : CustomerDevice , value : Any
182+ ) -> Any | None :
183+ """Convert a Home Assistant value back to a raw device value."""
184+ if value in (True , False ):
185+ return value
186+ # Currently only called with boolean values
187+ # Safety net in case of future changes
188+ raise ValueError (f"Invalid boolean value `{ value } `" )
189+
158190
159191class DPCodeTypeInformationWrapper [T : TypeInformation ](DPCodeWrapper ):
160192 """Base DPCode wrapper with Type Information."""
@@ -202,6 +234,16 @@ def read_device_status(self, device: CustomerDevice) -> str | None:
202234 return raw_value
203235 return None
204236
237+ def _convert_value_to_raw_value (self , device : CustomerDevice , value : Any ) -> Any :
238+ """Convert a Home Assistant value back to a raw device value."""
239+ if value in self .type_information .range :
240+ return value
241+ # Guarded by select option validation
242+ # Safety net in case of future changes
243+ raise ValueError (
244+ f"Enum value `{ value } ` out of range: { self .type_information .range } "
245+ )
246+
205247
206248class DPCodeIntegerWrapper (DPCodeTypeInformationWrapper [IntegerTypeData ]):
207249 """Simple wrapper for IntegerTypeData values."""
@@ -217,6 +259,18 @@ def read_device_status(self, device: CustomerDevice) -> float | None:
217259 return None
218260 return raw_value / (10 ** self .type_information .scale )
219261
262+ def _convert_value_to_raw_value (self , device : CustomerDevice , value : Any ) -> Any :
263+ """Convert a Home Assistant value back to a raw device value."""
264+ new_value = round (value * (10 ** self .type_information .scale ))
265+ if self .type_information .min <= new_value <= self .type_information .max :
266+ return new_value
267+ # Guarded by number validation
268+ # Safety net in case of future changes
269+ raise ValueError (
270+ f"Value `{ new_value } ` (converted from `{ value } `) out of range:"
271+ f" ({ self .type_information .min } -{ self .type_information .max } )"
272+ )
273+
220274
221275@overload
222276def find_dpcode (
0 commit comments