Repository: zigpy/zha-device-handlers

Issue:
[BUG] TS0601 _TZE200_9yapgbuv battery status is not reporting
**Describe the bug**
The battery status is reported as "Unknown" 

**To Reproduce**
Add the sensor TS0601 _TZE200_9yapgbuv to home assistant. Temperature and Humidity will appear after some time battery status not. Tried for more than 24h without showing it up.

**Expected behavior**
As far as I understand the sensor doesn't support battery percentage reporting. Its only reports the battery state as low, mid or high.
I tested the sensor with the tuya app and the app only shows low, mid and high.

**Screenshots**
![batteryUnknown](https://user-images.githubusercontent.com/5890132/226131046-88ba08b7-59f2-4b75-8d55-f5719a1a682f.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.EndDevice: 2>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress: 128>, 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=True, *is_full_function_device=False, *is_mains_powered=False, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False)",
  "endpoints": {
    "1": {
      "profile_id": 260,
      "device_type": "0x0302",
      "in_clusters": [
        "0x0000",
        "0x0001",
        "0x0004",
        "0x0005",
        "0x0402",
        "0x0405",
        "0xef00"
      ],
      "out_clusters": [
        "0x000a",
        "0x0019"
      ]
    }
  },
  "manufacturer": "_TZE200_9yapgbuv",
  "model": "TS0601",
  "class": "zhaquirks.tuya.ts0601_sensor.TuyaTempHumiditySensorVar03"
}
```

</details>

<details>
<!-- Diagnostic information can be acquired by clicking on the "Download Diagnostics" button in the device settings view -->
<summary>Diagnostic information</summary>

```yaml
{
  "home_assistant": {
    "installation_type": "Home Assistant OS",
    "version": "2023.3.4",
    "dev": false,
    "hassio": true,
    "virtualenv": false,
    "python_version": "3.10.10",
    "docker": true,
    "arch": "aarch64",
    "timezone": "Europe/Berlin",
    "os_name": "Linux",
    "os_version": "5.15.90",
    "supervisor": "2023.03.1",
    "host_os": "Home Assistant OS 9.5",
    "docker_version": "20.10.22",
    "chassis": "embedded",
    "run_as_root": true
  },
  "custom_components": {},
  "integration_manifest": {
    "domain": "zha",
    "name": "Zigbee Home Automation",
    "after_dependencies": [
      "onboarding",
      "usb"
    ],
    "codeowners": [
      "@dmulcahey",
      "@adminiuga",
      "@puddly"
    ],
    "config_flow": true,
    "dependencies": [
      "file_upload"
    ],
    "documentation": "https://www.home-assistant.io/integrations/zha",
    "iot_class": "local_polling",
    "loggers": [
      "aiosqlite",
      "bellows",
      "crccheck",
      "pure_pcapy3",
      "zhaquirks",
      "zigpy",
      "zigpy_deconz",
      "zigpy_xbee",
      "zigpy_zigate",
      "zigpy_znp"
    ],
    "requirements": [
      "bellows==0.34.10",
      "pyserial==3.5",
      "pyserial-asyncio==0.6",
      "zha-quirks==0.0.94",
      "zigpy-deconz==0.19.2",
      "zigpy==0.53.2",
      "zigpy-xbee==0.16.2",
      "zigpy-zigate==0.10.3",
      "zigpy-znp==0.9.3"
    ],
    "usb": [
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*2652*",
        "known_devices": [
          "slae.sh cc2652rb stick"
        ]
      },
      {
        "vid": "1A86",
        "pid": "55D4",
        "description": "*sonoff*plus*",
        "known_devices": [
          "sonoff zigbee dongle plus v2"
        ]
      },
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*sonoff*plus*",
        "known_devices": [
          "sonoff zigbee dongle plus"
        ]
      },
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*tubeszb*",
        "known_devices": [
          "TubesZB Coordinator"
        ]
      },
      {
        "vid": "1A86",
        "pid": "7523",
        "description": "*tubeszb*",
        "known_devices": [
          "TubesZB Coordinator"
        ]
      },
      {
        "vid": "1A86",
        "pid": "7523",
        "description": "*zigstar*",
        "known_devices": [
          "ZigStar Coordinators"
        ]
      },
      {
        "vid": "1CF1",
        "pid": "0030",
        "description": "*conbee*",
        "known_devices": [
          "Conbee II"
        ]
      },
      {
        "vid": "10C4",
        "pid": "8A2A",
        "description": "*zigbee*",
        "known_devices": [
          "Nortek HUSBZB-1"
        ]
      },
      {
        "vid": "0403",
        "pid": "6015",
        "description": "*zigate*",
        "known_devices": [
          "ZiGate+"
        ]
      },
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*zigate*",
        "known_devices": [
          "ZiGate"
        ]
      },
      {
        "vid": "10C4",
        "pid": "8B34",
        "description": "*bv 2010/10*",
        "known_devices": [
          "Bitron Video AV2010/10"
        ]
      }
    ],
    "zeroconf": [
      {
        "type": "_esphomelib._tcp.local.",
        "name": "tube*"
      },
      {
        "type": "_zigate-zigbee-gateway._tcp.local.",
        "name": "*zigate*"
      },
      {
        "type": "_zigstar_gw._tcp.local.",
        "name": "*zigstar*"
      },
      {
        "type": "_slzb-06._tcp.local.",
        "name": "slzb-06*"
      }
    ],
    "is_built_in": true
  },
  "data": {
    "ieee": "**REDACTED**",
    "nwk": 15097,
    "manufacturer": "_TZE200_9yapgbuv",
    "model": "TS0601",
    "name": "_TZE200_9yapgbuv TS0601",
    "quirk_applied": true,
    "quirk_class": "zhaquirks.tuya.ts0601_sensor.TuyaTempHumiditySensorVar03",
    "manufacturer_code": 4417,
    "power_source": "Battery or Unknown",
    "lqi": 247,
    "rssi": null,
    "last_seen": "2023-03-18T20:05:16",
    "available": true,
    "device_type": "EndDevice",
    "signature": {
      "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.EndDevice: 2>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress: 128>, 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=True, *is_full_function_device=False, *is_mains_powered=False, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False)",
      "endpoints": {
        "1": {
          "profile_id": 260,
          "device_type": "0x0302",
          "in_clusters": [
            "0x0000",
            "0x0001",
            "0x0004",
            "0x0005",
            "0x0402",
            "0x0405",
            "0xef00"
          ],
          "out_clusters": [
            "0x000a",
            "0x0019"
          ]
        }
      }
    },
    "active_coordinator": false,
    "entities": [
      {
        "entity_id": "sensor.tze200_9yapgbuv_ts0601_battery",
        "name": "_TZE200_9yapgbuv TS0601"
      },
      {
        "entity_id": "sensor.tze200_9yapgbuv_ts0601_temperature",
        "name": "_TZE200_9yapgbuv TS0601"
      },
      {
        "entity_id": "sensor.tze200_9yapgbuv_ts0601_humidity",
        "name": "_TZE200_9yapgbuv TS0601"
      }
    ],
    "neighbors": [],
    "routes": [],
    "endpoint_names": [
      {
        "name": "TEMPERATURE_SENSOR"
      }
    ],
    "user_given_name": null,
    "device_reg_id": "40d5b55de990afd8c377d3f4a65cf9c2",
    "area_id": "office",
    "cluster_details": {
      "1": {
        "device_type": {
          "name": "TEMPERATURE_SENSOR",
          "id": 770
        },
        "profile_id": 260,
        "in_clusters": {
          "0x0000": {
            "endpoint_attribute": "basic",
            "attributes": {
              "0x0001": {
                "attribute_name": "app_version",
                "value": 72
              }
            },
            "unsupported_attributes": {}
          },
          "0x0004": {
            "endpoint_attribute": "groups",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0005": {
            "endpoint_attribute": "scenes",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0xef00": {
            "endpoint_attribute": "tuya_manufacturer",
            "attributes": {
              "0xef00": {
                "attribute_name": "mcu_version",
                "value": "1.0.0"
              }
            },
            "unsupported_attributes": {}
          },
          "0x0402": {
            "endpoint_attribute": "temperature",
            "attributes": {
              "0x0000": {
                "attribute_name": "measured_value",
                "value": 1910
              }
            },
            "unsupported_attributes": {}
          },
          "0x0405": {
            "endpoint_attribute": "humidity",
            "attributes": {
              "0x0000": {
                "attribute_name": "measured_value",
                "value": 6700
              }
            },
            "unsupported_attributes": {}
          },
          "0x0001": {
            "endpoint_attribute": "power",
            "attributes": {},
            "unsupported_attributes": {}
          }
        },
        "out_clusters": {
          "0x0019": {
            "endpoint_attribute": "ota",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x000a": {
            "endpoint_attribute": "time",
            "attributes": {},
            "unsupported_attributes": {}
          }
        }
      }
    }
  }
}
```

</details>

**Additional context**
I used the [zigbee2mqtt guide](https://www.zigbee2mqtt.io/advanced/support-new-devices/03_find_tuya_data_points.html) to get the datapoint. Hope this is helpful.
![battery](https://user-images.githubusercontent.com/5890132/226131142-05838106-080a-457d-ac96-8bd7c048b599.PNG)

Im very novice with python and don't fully understand how the zha python class work. But I tried to fix the problem myself with some success. I no longer get Unknown but 0-2 values depending on the battery state. 0 representing low, 1 mid and 2 high.

![image](https://user-images.githubusercontent.com/5890132/226132588-4f43281c-de90-44a4-a6b8-88f8137bc141.png)

<details>
<summary>custom quirk</summary>

```
"""Tuya temp and humidity sensors."""

from typing import Any, Dict

from zigpy.profiles import zha
from zigpy.quirks import CustomDevice
import zigpy.types as t
from zigpy.zcl.clusters.general import Basic, Groups, Ota, Scenes, Time
from zigpy.zcl.clusters.measurement import (
    RelativeHumidity,
    SoilMoisture,
    TemperatureMeasurement,
)

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
    SKIP_CONFIGURATION,
)
from zhaquirks.tuya import TuyaLocalCluster, TuyaPowerConfigurationCluster2AAA, TuyaManufClusterAttributes
from zhaquirks.tuya.mcu import DPToAttributeMapping, TuyaMCUCluster


class TuyaTemperatureMeasurement(TemperatureMeasurement, TuyaLocalCluster):
    """Tuya local TemperatureMeasurement cluster."""


class TuyaSoilMoisture(SoilMoisture, TuyaLocalCluster):
    """Tuya local SoilMoisture cluster with a device RH_MULTIPLIER factor if required."""

class TuyaRelativeHumidity(RelativeHumidity, TuyaLocalCluster):
    """Tuya local RelativeHumidity cluster with a device RH_MULTIPLIER factor."""

    def update_attribute(self, attr_name: str, value: Any) -> None:
        """Apply a correction factor to value."""

        if attr_name == "measured_value":
            value = value * (
                self.endpoint.device.RH_MULTIPLIER
                if hasattr(self.endpoint.device, "RH_MULTIPLIER")
                else 100
            )
        return super().update_attribute(attr_name, value)


class TemperatureHumidityManufCluster(TuyaMCUCluster, TuyaManufClusterAttributes):
    """Tuya Manufacturer Cluster with Temperature and Humidity data points."""

    dp_to_attribute: Dict[int, DPToAttributeMapping] = {
        1: DPToAttributeMapping(
            TuyaTemperatureMeasurement.ep_attribute,
            "measured_value",
            converter=lambda x: x * 10,  # decidegree to centidegree
        ),
        2: DPToAttributeMapping(
            TuyaRelativeHumidity.ep_attribute,
            "measured_value",
            # converter=lambda x: x * 10,  --> move conversion to TuyaRelativeHumidity cluster
        ),
        3: DPToAttributeMapping(
            TuyaPowerConfigurationCluster2AAA.ep_attribute,
            "battery_percentage_remaining",
            converter=lambda x: x * 2,  # double reported percentage
        ),
        4: DPToAttributeMapping(
            TuyaPowerConfigurationCluster2AAA.ep_attribute,
            "battery_percentage_remaining",
            converter=lambda x: x * 2,  # double reported percentage
        ),
    }

    data_point_handlers = {
        1: "_dp_2_attr_update",
        2: "_dp_2_attr_update",
        3: "_dp_2_attr_update",
        4: "_dp_2_attr_update",
    }
    


class TuyaTempHumiditySensorVar03(CustomDevice):
    """Tuya temp and humidity sensor (variation 03)."""

    signature = {
        # "profile_id": 260,
        # "device_type": "0x0051",
        # "in_clusters": ["0x0000","0x0004","0x0005","0xef00"],
        # "out_clusters": ["0x000a","0x0019"]
        MODELS_INFO: [
            ("_TZE200_yjjdcqsq", "TS0601"),
            ("_TZE200_9yapgbuv", "TS0601"),
            ("_TZE200_qyflbnbj", "TS0601"),
        ],
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TemperatureHumidityManufCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id],
            }
        },
    }

    replacement = {
        SKIP_CONFIGURATION: True,
        ENDPOINTS: {
            1: {
                DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TemperatureHumidityManufCluster,
                    TuyaTemperatureMeasurement,
                    TuyaRelativeHumidity,
                    TuyaPowerConfigurationCluster2AAA,
                    TuyaPowerConfigurationCluster2AAA,
                ],
                OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id],
            }
        },
    }
```

</details>


Discussion:
In Z2M this device is reporting `batteryPercentageRemaining` and `batteryVoltage`.
Your quirk is adding the DP3. Z2M implements DP3 as `tthBatteryLevel` that report `low`|`middle`|`high` value. These values aren't managed as battery levels in HA.

In the current state I would suggest to manage it as a Tuya manufacturer attribute and add it as an ZHA attribute (a diagnostic one).
The hard part could be how to know which devices must initialize the value (to make it available in HA). Maybe all the `TuyaTempHumiditySensorVar03`?

Thanks for the feedback. Is there a guideline or a other issue already discussing this? Would it be possible to have low, mid and high just report as 0%, 50% and 100% for the normal battery percentage?

Also if possible can someone with more knowledge implement this quirk properly? My solution isn't really clean.

> The hard part could be how to know which devices must initialize the value (to make it available in HA). Maybe all the `TuyaTempHumiditySensorVar03`?

Maybe just implement it for _TZE200_9yapgbuv for now and see if others got similar problems on other devices later. I also ordered a few more of this sensors from china maybe I get a different version and can give an update.
EDIT: I now also got _TZE200_yjjdcqsq sensor with the same behaviour. So it seems like all TuyaTempHumiditySensorVar03 behave the same.
There hasn't been any activity on this issue recently. Due to the high number of incoming GitHub notifications, we have to clean some of the old issues, as many of them have already been resolved with the latest updates. Please make sure to update to the latest version and check if that solves the issue. Let us know if that works for you by adding a comment 👍 This issue has now been marked as stale and will be closed if no further activity occurs. Thank you for your contributions.
The issue still exists.

Resolving patch:
diff --git a/zhaquirks/tuya/ts0601_sensor.py b/zhaquirks/tuya/ts0601_sensor.py
--- a/zhaquirks/tuya/ts0601_sensor.py
+++ b/zhaquirks/tuya/ts0601_sensor.py
@@ -75,6 +75,26 @@ class TemperatureHumidityManufCluster(TuyaMCUCluster):
     }
 
 
+class TemperatureHumidityBatteryStatesManufCluster(TuyaMCUCluster):
+    """Tuya Manufacturer Cluster with Temperature and Humidity data points. Battery states 25, 50 and 100%."""
+
+    dp_to_attribute: Dict[int, DPToAttributeMapping] = {
+        1: TemperatureHumidityManufCluster.dp_to_attribute[1],
+        2: TemperatureHumidityManufCluster.dp_to_attribute[2],
+        3: DPToAttributeMapping(
+            TuyaPowerConfigurationCluster2AAA.ep_attribute,
+            "battery_percentage_remaining",
+            converter=lambda x: {0: 25, 1: 50, 2: 100}[x],  # double reported percentage
+        ),
+    }
+
+    data_point_handlers = {
+        1: "_dp_2_attr_update",
+        2: "_dp_2_attr_update",
+        3: "_dp_2_attr_update",
+    }
+
+
 class TuyaTempHumiditySensor(CustomDevice):
     """Custom device representing tuya temp and humidity sensor with e-ink screen."""
 
@@ -181,8 +201,6 @@ class TuyaTempHumiditySensorVar03(CustomDevice):
         # "in_clusters": ["0x0000","0x0004","0x0005","0xef00"],
         # "out_clusters": ["0x000a","0x0019"]
         MODELS_INFO: [
-            ("_TZE200_yjjdcqsq", "TS0601"),
-            ("_TZE200_9yapgbuv", "TS0601"),
             ("_TZE200_qyflbnbj", "TS0601"),
             ("_TZE200_utkemkbs", "TS0601"),
         ],
@@ -221,6 +239,53 @@ class TuyaTempHumiditySensorVar03(CustomDevice):
     }
 
 
+class TuyaTempHumiditySensorVar04(CustomDevice):
+    """Tuya temp and humidity sensor (variation 04)."""
+
+    signature = {
+        # "profile_id": 260,
+        # "device_type": "0x0051",
+        # "in_clusters": ["0x0000","0x0004","0x0005","0xef00"],
+        # "out_clusters": ["0x000a","0x0019"]
+        MODELS_INFO: [
+            ("_TZE200_yjjdcqsq", "TS0601"),
+            ("_TZE200_9yapgbuv", "TS0601"),
+        ],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TemperatureHumidityManufCluster.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id],
+            }
+        },
+    }
+
+    replacement = {
+        SKIP_CONFIGURATION: True,
+        ENDPOINTS: {
+            1: {
+                DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TemperatureHumidityBatteryStatesManufCluster,
+                    TuyaTemperatureMeasurement,
+                    TuyaRelativeHumidity,
+                    TuyaPowerConfigurationCluster2AAA,
+                ],
+                OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id],
+            }
+        },
+    }
+
+
 class SoilManufCluster(TuyaMCUCluster):
     """Tuya Manufacturer Cluster with Temperature and Humidity data points."""
 


Test patch: