Forráskód Böngészése

Update device_config.py - add support for bitwise masking on integer DPs (#4220)

* Update device_config.py - add support for bitwise masking on integer DPs

Its been a while since I was this deep in the weeds with Python but here we go...

This PR addresses an issue where using the mask parameter on data points of type: integer would result in correctable crashes and incorrect data parsing. This is required due to at least one device (Inkbird IVC-001W) that presents DPs in this fashion but I am sure that there are others out there.

Previously, the masking logic in device_config.py assumed that any masked value was a byte array (hex/base64). This caused two issues for packed integers (e.g., two 16-bit values packed into a 32-bit integer):

On read: The mask was ignored, returning the full raw integer instead of the specific bit range.

On write: The integration crashed with cannot convert 'float' object to bytes because it attempted to pass a numeric value into int.from_bytes.

Changes:

Updated get_value to check if the raw value is an integer. If so, it applies the mask and bit-shift directly without byte conversion.

Updated get_values_to_set to implement a "Read-Modify-Write" cycle for integers. It reads the current device state, clears the masked bits, and merges the new value, ensuring the unmasked parts of the integer remain untouched.

Backwards compatibility notes: The new logic only triggers if mask is defined and the raw value is an integer; existing hex/base64 implementations are unaffected.

* Update device_config.py - fixed incorrect indenting / spaces

* Update device_config.py - fixed yet more linting issues

* cleanup (device_config): simplify logic

Simplify value decoding logic by removing conditional checks for raw types. decode_value is already designed to return the input if there is no decoding needed.

PR #4220

---------

Co-authored-by: Jason Rumney <make-all@users.noreply.github.com>
baronorder 2 hónapja
szülő
commit
e504103177
1 módosított fájl, 29 hozzáadás és 8 törlés
  1. 29 8
      custom_components/tuya_local/helpers/device_config.py

+ 29 - 8
custom_components/tuya_local/helpers/device_config.py

@@ -476,7 +476,10 @@ class TuyaDpsConfig:
     def get_value(self, device):
         """Return the value of the dps from the given device."""
         mask = self.mask
-        bytevalue = self.decoded_value(device)
+        # Get raw value directly avoiding accidental scaling by decoded_value()
+        raw_from_device = device.get_property(self.id)
+        bytevalue = self.decode_value(raw_from_device, device)
+
         if mask and isinstance(bytevalue, bytes):
             value = int.from_bytes(bytevalue, self.endianness)
             scale = mask & (1 + ~mask)
@@ -489,8 +492,15 @@ class TuyaDpsConfig:
                 raw_result = to_signed(raw_result, bit_count)
 
             return self._map_from_dps(raw_result, device)
+
+        elif mask and isinstance(bytevalue, int):
+            # Handle masking for integer DPs
+            scale = mask & (1 + ~mask)
+            raw_result = (bytevalue & mask) // scale
+            return self._map_from_dps(raw_result, device)
+
         else:
-            return self._map_from_dps(device.get_property(self.id), device)
+            return self._map_from_dps(raw_from_device, device)
 
     def decoded_value(self, device):
         v = self._map_from_dps(device.get_property(self.id), device)
@@ -1072,14 +1082,25 @@ class TuyaDpsConfig:
             # Convert to int
             endianness = self.endianness
             mask_scale = mask & (1 + ~mask)
-            decoded_value = self.decoded_value(device)
-            # if we have already updated it as part of this update,
-            # use it to preserve bits
+
+            # Get raw current value directly (avoids scaling being auto applied as it causes issues)
+            raw_current = device.get_property(self.id)
             if self.id in pending_map:
                 decoded_value = self.decode_value(pending_map[self.id], device)
-            current_value = int.from_bytes(decoded_value, endianness)
-            result = (current_value & ~mask) | (mask & int(result * mask_scale))
-            result = self.encode_value(result.to_bytes(length, endianness))
+            else:
+                decoded_value = self.decode_value(raw_current, device)
+
+            if isinstance(decoded_value, int):
+                current_value = decoded_value
+                result = (current_value & ~mask) | (mask & int(result * mask_scale))
+                # Only convert back to bytes if the DP is actually hex/base64
+                if self.rawtype in ["hex", "base64", "utf16b64"]:
+                    result = self.encode_value(result.to_bytes(length, endianness))
+            else:
+                # Bytes path (original logic)
+                current_value = int.from_bytes(decoded_value, endianness)
+                result = (current_value & ~mask) | (mask & int(result * mask_scale))
+                result = self.encode_value(result.to_bytes(length, endianness))
 
         dps_map[self.id] = self._correct_type(result)
         return dps_map