Просмотр исходного кода

Add mask for extracting parts of an encoded string.

- add mask attribute for dps config.  Format is hex string
- when mask is specified mask and bitshift the dp value
- use it in TOMPD63LW config to extract voltage, current and power
  from phase_a dp as separate sensors.
- add a unit test for this new function.
Jason Rumney 2 лет назад
Родитель
Сommit
94aa555ba2

+ 5 - 0
custom_components/tuya_local/devices/README.md

@@ -254,6 +254,11 @@ For base64 and hex types, this specifies how to decode the binary data (after he
 This is a container field, the contents of which should be a list consisting of `name`, `bytes` and `range` fields.  `range` is as described above.  `bytes` is the number of bytes for the field, which can be `1`, `2`, or `4`.  `name` is a name for the field, which will have special handling depending on
 the device type.
 
+### `mask`
+
+*Optional.*
+
+For base64 and hex types, this specifies how to extract a single numeric value from the binary data.  The value should be a hex bit mask (eg 00FF00 to extract the middle byte of a 3 byte value).  Unlike format, this does not require special handling in the entity platform, as only a single value is being extracted.
 
 ## Mapping Rules
 

+ 39 - 1
custom_components/tuya_local/devices/tompd_63lw_breaker.yaml

@@ -20,7 +20,7 @@ primary_entity:
       type: string
     - id: 6
       name: phase_a
-      type: string
+      type: base64
       optional: true
     - id: 9
       name: fault
@@ -85,6 +85,7 @@ secondary_entities:
         name: sensor
         unit: mA
         class: measurement
+        optional: true
   - entity: button
     name: Earth leak test
     icon: "mdi:current-ac"
@@ -105,3 +106,40 @@ secondary_entities:
         class: total_increasing
         mapping:
           - scale: 100
+  - entity: sensor
+    name: Voltage A
+    class: voltage
+    category: diagnostic
+    dps:
+      - id: 6
+        type: base64
+        name: sensor
+        optional: true
+        unit: V
+        mapping:
+          - mask: "FFFFFF0000000000000000"
+            scale: 10
+  - entity: sensor
+    name: Current A
+    class: current
+    category: diagnostic
+    dps:
+      - id: 6
+        type: base64
+        name: sensor
+        optional: true
+        unit: mA
+        mapping:
+          - mask: "000000FFFFFF0000000000"
+  - entity: sensor
+    name: Power A
+    class: power
+    category: diagnostic
+    dps:
+      - id: 6
+        type: base64
+        name: sensor
+        optional: true
+        unit: W
+        mapping:
+          - mask: "000000000000FFFFFF0000"

+ 28 - 4
custom_components/tuya_local/helpers/device_config.py

@@ -349,12 +349,27 @@ class TuyaDpsConfig:
 
         return None
 
+    def mask(self, device):
+        mapping = self._find_map_for_dps(device.get_property(self.id))
+        if mapping:
+            mask = mapping.get("mask")
+            if mask:
+                return int(mask, 16)
+
     def get_value(self, device):
         """Return the value of the dps from the given device."""
-        return self._map_from_dps(device.get_property(self.id), device)
+        mask = self.mask(device)
+        if mask:
+            bytevalue = self.decoded_value(device)
+            value = int.from_bytes(bytevalue, "big")
+            scale = mask & (1 + ~mask)
+            map_scale = self.scale(device)
+            return ((value & mask) // scale) / map_scale
+        else:
+            return self._map_from_dps(device.get_property(self.id), device)
 
     def decoded_value(self, device):
-        v = self.get_value(device)
+        v = self._map_from_dps(device.get_property(self.id), device)
         if self.rawtype == "hex" and isinstance(v, str):
             try:
                 return bytes.fromhex(v)
@@ -405,7 +420,6 @@ class TuyaDpsConfig:
             raise TypeError(f"{self.name} is read only")
         if self.invalid_for(value, device):
             raise AttributeError(f"{self.name} cannot be set at this time")
-
         settings = self.get_values_to_set(device, value)
         await device.async_set_properties(settings)
 
@@ -720,11 +734,12 @@ class TuyaDpsConfig:
 
         mapping = self._find_map_for_value(value, device)
         scale = self.scale(device)
+        mask = None
         if mapping:
             replaced = False
             redirect = mapping.get("value_redirect")
             invert = mapping.get("invert", False)
-
+            mask = mapping.get("mask")
             step = mapping.get("step")
             if not isinstance(step, Number):
                 step = None
@@ -808,6 +823,15 @@ class TuyaDpsConfig:
                 mx = r["max"]
                 raise ValueError(f"{self.name} ({value}) must be between {mn} and {mx}")
 
+        if mask and isinstance(result, Number):
+            # Convert to int
+            length = len(mask)
+            mask = int(mask, 16)
+            mask_scale = mask & (1 + ~mask)
+            current_value = int.from_bytes(self.decoded_value(device), "big")
+            result = (current_value & ~mask) | (mask & (result * mask_scale))
+            result = self.encode_value(result.to_bytes(length, "big"))
+
         dps_map[self.id] = self._correct_type(result)
         return dps_map
 

+ 17 - 0
tests/const.py

@@ -1581,3 +1581,20 @@ MOEBOT_PAYLOAD = {
     "106": 1343,
     "114": "AutoMode",
 }
+
+TOMPD63LW_SOCKET_PAYLOAD = {
+    "1": 139470,
+    "6": "CHoAQgQADlwAAA==",
+    "9": 0,
+    "11": False,
+    "12": False,
+    "13": 0,
+    "16": True,
+    "19": "FSE-F723C46A04FC6C",
+    # "101": 275,
+    # "102": 170,
+    # "103": 40,
+    # "104": 30,
+    # "105": False,
+    # "106": False,
+}

+ 72 - 0
tests/devices/test_tompd63lw_breaker.py

@@ -0,0 +1,72 @@
+"""Tests for the switch entity."""
+from homeassistant.components.sensor import SensorDeviceClass
+from homeassistant.const import (
+    UnitOfElectricCurrent,
+    UnitOfElectricPotential,
+    UnitOfTime,
+    UnitOfPower,
+)
+
+from ..const import TOMPD63LW_SOCKET_PAYLOAD
+from ..mixins.sensor import MultiSensorTests
+from .base_device_tests import TuyaDeviceTestCase
+
+ENERGY_DP = "1"
+PHASEA_DP = "6"
+FAULT_DP = "9"
+PREPAY_DP = "11"
+RESET_DP = "12"
+BALANCE_DP = "13"
+CHARGE_DP = "14"
+LEAKAGE_DP = "15"
+SWITCH_DP = "16"
+ALARM1_DP = "17"
+ALARM2_DP = "18"
+IDNUM_DP = "19"
+TEST_DP = "21"
+
+
+class TestTOMPD63lw(MultiSensorTests, TuyaDeviceTestCase):
+    __test__ = True
+
+    def setUp(self):
+        self.setUpForConfig("tompd_63lw_breaker.yaml", TOMPD63LW_SOCKET_PAYLOAD)
+        self.subject = self.entities.get("switch")
+        self.setUpMultiSensors(
+            [
+                {
+                    "name": "sensor_voltage_a",
+                    "dps": PHASEA_DP,
+                    "unit": UnitOfElectricPotential.VOLT,
+                    "device_class": SensorDeviceClass.VOLTAGE,
+                    "testdata": ("CHoAQgQADlwAAA==", 217.0),
+                },
+                {
+                    "name": "sensor_current_a",
+                    "dps": PHASEA_DP,
+                    "unit": UnitOfElectricCurrent.MILLIAMPERE,
+                    "device_class": SensorDeviceClass.CURRENT,
+                    "testdata": ("CHoAQgQADlwAAA==", 16900),
+                },
+                {
+                    "name": "sensor_power_a",
+                    "dps": PHASEA_DP,
+                    "unit": UnitOfPower.WATT,
+                    "device_class": SensorDeviceClass.POWER,
+                    "testdata": ("CHoAQgQADlwAAA==", 3676),
+                },
+            ]
+        )
+        self.mark_secondary(
+            [
+                "button_earth_leak_test",
+                "button_energy_reset",
+                "number_charge_energy",
+                "sensor_balance_energy",
+                "sensor_current_a",
+                "sensor_leakage_current",
+                "sensor_power_a",
+                "sensor_voltage_a",
+                "switch_prepayment",
+            ]
+        )