Repository: zigpy/zha-device-handlers

Issue:
[BUG] Negative temperatures wrap around to 6000°C with Tuya Smart Air Detector 6 in 1
**Describe the bug**

[Tuya Smart Air Detector 6 in 1](https://zigbee.blakadder.com/Tuya_DCR-KQG.html) has working temperature range 0-60°C by specs.  
But handling of temps out of specs leads to wacky statistics.

Issue here is that apparently either raw reported values or their source representation uses unsigned 16 bit numbers and negative values written as [2's complement](https://en.wikipedia.org/wiki/Two%27s_complement) signed integer, which makes this numbers in range of ~65500-ish.

This leads to reported temperatures and temperatures displayed in HA to be above 6000°C which by any means doesn't make any sense and makes charts unusable since all other sensors will have temperatures around 0°C at this scale.

However, despite temperatures being in unsupported range, I think it will be more convenient to convert them back from 2's complement to signed form. For example, it could be done like so:
```(((x&0xFFFF)^0x8000) - 0x8000)```

`x` here is 16 bit _integer_ value. Hence if it is, let's say, 65528, then we'll get -8 as the result.  
Which converts to "real" temperature value of -0.8°C instead of current 6552.8°C.

It brings results back to normal range and, despite them not being inside supported value range, still makes them usable.

Actually, values themselves, after being converted, are quite accurate.  
For example, when sensor reports temperature 6549.9°C, it gives -3.7°C after conversion using formula above. And sensor rated for negative temps gives -3.5°C (I added this as 3rd chart for the reference).

**To Reproduce**
Steps to reproduce the behavior:
1. Go to place with negative ambient temps
2. Plug sensor there
3. Connect it to HA
4. See high temperatures

**Expected behavior**
Don't see temperatures outside of normal ambient temperatures range

**Screenshots**
![Charts](https://user-images.githubusercontent.com/1712806/205030648-6ce7dbe8-60ae-4ce5-82c2-4cd33c4ea522.png)

![Bad temp](https://user-images.githubusercontent.com/1712806/205030668-2686982b-9fe4-4eab-b3d6-4dabdba59fd2.png)

![Real temp](https://user-images.githubusercontent.com/1712806/205031836-ec0255aa-63e9-4539-9edc-d6ae30827d29.png)




<details>
<!-- Device signature can be acquired by clicking on the "Zigbee Device Signature" button in the device settings view -->
<summary>Device signature</summary>

```yaml
{
  "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 142>, manufacturer_code=4417, maximum_buffer_size=66, maximum_incoming_transfer_size=66, server_mask=10752, maximum_outgoing_transfer_size=66, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)",
  "endpoints": {
    "1": {
      "profile_id": 260,
      "device_type": "0x0100",
      "in_clusters": [
        "0x0000",
        "0x0004",
        "0x0005",
        "0x0402",
        "0x0405",
        "0x040d",
        "0x042a",
        "0x042b",
        "0x042e",
        "0xef00"
      ],
      "out_clusters": [
        "0x000a",
        "0x0019"
      ]
    },
    "242": {
      "profile_id": 41440,
      "device_type": "0x0061",
      "in_clusters": [],
      "out_clusters": [
        "0x0021"
      ]
    }
  },
  "manufacturer": "_TZE200_dwcarsat",
  "model": "TS0601",
  "class": "ts0601_air_quality.TuyaCO2Sensor2GPP"
}
```

</details>

<details>
<!-- Diagnostic information can be acquired by clicking on the "Download Diagnostics" button in the device settings view -->
<summary>Diagnostic information</summary>

```yaml
Paste the diagnostic information here.
Don't remove the extra line breaks outside the ``` marks.
```

</details>

<details>
<summary>Additional logs</summary>

```
```

</details>

**Additional context**




Discussion:
So, if we considerer all the temp measures as 2's complement the device range would be [409.5 ºC, -409.6 ºC], isn't it? 
I think that wouldn't be a problem if all temperatures are 'converted' for that device and would be in inside the manufacturer work range.

I see that you have a local quirk. Do you have already implemented the conversion? 
You can create the PR with the modification, this way we can comment the changes.
> So, if we considerer all the temp measures as 2's complement the device range would be [409.5 ºC, -409.6 ºC], isn't it?

I think so. I am not entirely sure if my conclusion is correct but calculations look believable enough.  
But you are right that this possible range in signed space is enough to fit device value range and some more into negative values.

But I think for int16 range would be [-3276.8°C, 3276.7°C] in this case, or am I missing something?

> I see that you have a local quirk. Do you have already implemented the conversion?

No, that's quirk for swapping DP for PM2.5 and VOC sensor as described [here](https://github.com/zigpy/zha-device-handlers/pull/1740#issuecomment-1243663998) and also to filter out bad PM2.5 values.  
I maybe add PR for that eventually but I am not sure if what I did there is correct.

I am not quite sure how data mapping works inside ZHA and HA so it'd be quite hard for me to do. From numbers alone it is very clear that at some point these numbers surely were 16 bit integers.  
But inside `ts0601_air_quality.py` temp multiplied by 10. Does this mean that raw value from device arrives as float divided by 10? Or it is something needed for HA and those are actually in some different form?

Resolving patch:
diff --git a/zhaquirks/tuya/__init__.py b/zhaquirks/tuya/__init__.py
--- a/zhaquirks/tuya/__init__.py
+++ b/zhaquirks/tuya/__init__.py
@@ -153,7 +153,7 @@ class TuyaData(t.Struct):
     def payload(
         self,
     ) -> Union[
-        t.uint32_t_be,
+        t.int32s_be,
         t.Bool,
         t.CharacterString,
         t.enum8,
@@ -164,7 +164,7 @@ def payload(
     ]:
         """Payload accordingly to data point type."""
         if self.dp_type == TuyaDPType.VALUE:
-            return t.uint32_t_be.deserialize(self.raw)[0]
+            return t.int32s_be.deserialize(self.raw)[0]
         elif self.dp_type == TuyaDPType.BOOL:
             return t.Bool.deserialize(self.raw)[0]
         elif self.dp_type == TuyaDPType.STRING:
@@ -186,7 +186,7 @@ def payload(
     def payload(self, value):
         """Set payload accordingly to data point type."""
         if self.dp_type == TuyaDPType.VALUE:
-            self.raw = t.uint32_t_be(value).serialize()
+            self.raw = t.int32s_be(value).serialize()
         elif self.dp_type == TuyaDPType.BOOL:
             self.raw = t.Bool(value).serialize()
         elif self.dp_type == TuyaDPType.STRING:


Test patch:
diff --git a/tests/test_tuya_clusters.py b/tests/test_tuya_clusters.py
--- a/tests/test_tuya_clusters.py
+++ b/tests/test_tuya_clusters.py
@@ -65,6 +65,20 @@ def test_tuya_data_value():
     assert r.raw == b"\x00\x00\x02\x46"
 
 
+def test_tuya_negative_value():
+    """Test tuya negative "Value" datatype."""
+
+    data = b"\x02\x00\x04\xff\xff\xff\xf8"
+    extra = b"extra data"
+
+    r, rest = TuyaData.deserialize(data + extra)
+    assert rest == extra
+
+    assert r.dp_type == 2
+    assert r.raw == b"\xff\xff\xff\xf8"
+    assert r.payload == -8
+
+
 def test_tuya_data_bool():
     """Test tuya Bool datatype."""