EPTTECH TS0601 TZE200 water level sensor ZHA

I got zigbee ultrasonic water level sensor made by EPTTECH, it works fine in Tuya application, however neithe ZHA supports it nor Local Tuya shows level in Home Assistant.
Unfortunatelly my custom quirk did not work either, or I may missed something in it…

"""Custom quirk for EPTTECH TLC2206-ZB Tank Level Sensor."""

from zigpy.profiles import zha
from zigpy.quirks import CustomDevice
from zigpy.zcl.clusters.general import Basic, Groups, Scenes, Ota, Time
from zigpy.zcl.clusters.measurement import FlowMeasurement
from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)
from zhaquirks.tuya import TuyaLocalCluster
from zhaquirks.tuya.mcu import DPToAttributeMapping, TuyaMCUCluster
import zigpy.types as t

# Define the data points (DPs) for the sensor
TUYA_TANK_LEVEL_PERCENT_DP = 22
TUYA_TANK_DEPTH_DP = 2
TUYA_TANK_STATE_DP = 1
TUYA_INSTALLATION_HEIGHT_DP = 19
TUYA_MAX_SET_DP = 7
TUYA_MIN_SET_DP = 8
TUYA_DEPTH_MAX_DP = 21

class LiquidLevelStatus(t.enum8):
    """Enum for liquid level status."""
    NORMAL = 0x00
    LOW = 0x01
    HIGH = 0x02

class TuyaTankLevelMeasurement(TuyaLocalCluster):
    """Custom cluster for tank level measurement."""

    cluster_id = FlowMeasurement.cluster_id
    name = "Tank Level Measurement"
    ep_attribute = "liquid_level_measurement"

    attributes = {
        0x0000: ("measured_value", t.uint16_t, True),
        0x0001: ("min_measured_value", t.uint16_t, True),
        0x0002: ("max_measured_value", t.uint16_t, True),
        0xFF00: ("liquid_level_percent", t.uint8_t, True),
        0xFF01: ("liquid_depth", t.uint16_t, True),  # Assuming depth is in mm
        0xFF02: ("liquid_state", LiquidLevelStatus, True),
        0xFF03: ("installation_height", t.uint16_t, True),  # mm
        0xFF04: ("max_set", t.uint8_t, True),  # Percentage
        0xFF05: ("mini_set", t.uint8_t, True),  # Percentage
        0xFF06: ("liquid_depth_max", t.uint16_t, True),  # mm
    }

class TuyaTankLevelCluster(TuyaMCUCluster):
    """Custom cluster for handling EPTTECH TLC2206-ZB tank level sensor data points."""

    dp_to_attribute: dict[int, DPToAttributeMapping] = {
        TUYA_TANK_LEVEL_PERCENT_DP: DPToAttributeMapping(
            TuyaTankLevelMeasurement.ep_attribute,
            "liquid_level_percent",
        ),
        TUYA_TANK_DEPTH_DP: DPToAttributeMapping(
            TuyaTankLevelMeasurement.ep_attribute,
            "liquid_depth",
            converter=lambda x: x / 100,  # Convert mm to meters
        ),
        TUYA_TANK_STATE_DP: DPToAttributeMapping(
            TuyaTankLevelMeasurement.ep_attribute,
            "liquid_state",
            converter=lambda x: LiquidLevelStatus(x),
        ),
        TUYA_INSTALLATION_HEIGHT_DP: DPToAttributeMapping(
            TuyaTankLevelMeasurement.ep_attribute,
            "installation_height",
        ),
        TUYA_MAX_SET_DP: DPToAttributeMapping(
            TuyaTankLevelMeasurement.ep_attribute,
            "max_set",
        ),
        TUYA_MIN_SET_DP: DPToAttributeMapping(
            TuyaTankLevelMeasurement.ep_attribute,
            "mini_set",
        ),
        TUYA_DEPTH_MAX_DP: DPToAttributeMapping(
            TuyaTankLevelMeasurement.ep_attribute,
            "liquid_depth_max",
        ),
    }

    data_point_handlers = {
        TUYA_TANK_LEVEL_PERCENT_DP: "_dp_2_attr_update",
        TUYA_TANK_DEPTH_DP: "_dp_2_attr_update",
        TUYA_TANK_STATE_DP: "_dp_2_attr_update",
        TUYA_INSTALLATION_HEIGHT_DP: "_dp_2_attr_update",
        TUYA_MAX_SET_DP: "_dp_2_attr_update",
        TUYA_MIN_SET_DP: "_dp_2_attr_update",
        TUYA_DEPTH_MAX_DP: "_dp_2_attr_update",
    }

class EPTTECH_TLC2206ZB(CustomDevice):
    """Custom quirk for EPTTECH TLC2206-ZB Tank Level Sensor."""

    signature = {
        MODELS_INFO: [("TS0601", "_TZE200_lvkk0hdg")],
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.IAS_ZONE,  # Device type commonly used for sensors
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    TuyaTankLevelCluster.cluster_id,  # Custom cluster for tank level
                ],
                OUTPUT_CLUSTERS: [
                    Ota.cluster_id,
                    Time.cluster_id,
                ],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.IAS_ZONE,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    TuyaTankLevelCluster,  # Custom cluster for tank level
                ],
                OUTPUT_CLUSTERS: [
                    Ota.cluster_id,
                    Time.cluster_id,
                ],
            },
        },
    }

Help is much appreciated!

Could someone address it to ZHA developers?

1 Like

I’m looking at this sensor as well - it would be great if it is made to work, as I don’t think there is an alternative?

Are those the correct Tuya data points? If you are copying from a z2m converter, do you have a link?

Did you get this working? I’m trying to set it up as well, and this is my first experience with ZHA Quirks, so I’m not entirely certain I know what I’m doing. Is there any kind of repository for custom quirks, or do we just find them in forums like this?

There is a similar device and quirk here [Device Support Request] Tuya ultrasonic level sensor (TS0601 by _TZE284_kyyu8rbj) · Issue #3356 · zigpy/zha-device-handlers · GitHub

Usually these have similar Datapoints, so it may work with just modifying the manufacturer, otherwise we need the Datapoints to do anything.

1 Like

Finally!
I got it working guys!
I have edited custom quirk loading in my configuration.yaml to

zha:
   database_path: zigbee.db
   enable_quirks: true
   custom_quirks_path: custom_zha_quirks/

Added the following into my custom quirk file stored in /homeassistant/custom_zha_quirks/tlc2206zb_quirk.py

from zigpy.quirks.v2 import EntityType
from zigpy.quirks.v2.homeassistant import PERCENTAGE, UnitOfLength
from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass, SensorStateClass
import zigpy.types as t

from zhaquirks.tuya.builder import TuyaQuirkBuilder

class LiquidLevelStatus(t.enum8):
    """Enum for liquid level status."""
    NORMAL = 0x00
    LOW = 0x01
    HIGH = 0x02

(
    TuyaQuirkBuilder("_TZE200_lvkk0hdg", "TS0601")
    .tuya_enum(
        dp_id=1,
        attribute_name="liquid_state",
        enum_class=LiquidLevelStatus,
        entity_type=EntityType.DIAGNOSTIC,
        translation_key="liquid_state",
        fallback_name="Liquid state",
    )
    .tuya_sensor(
        dp_id=2,
        attribute_name="liquid_depth",
        type=t.uint16_t,
        state_class=SensorStateClass.MEASUREMENT,
        device_class=SensorDeviceClass.DISTANCE,
        unit=UnitOfLength.METERS,
        entity_type=EntityType.STANDARD,
        converter=lambda x: x / 100,  # Convert mm to meters
        translation_key="liquid_depth",
        fallback_name="Liquid depth",
    )
    .tuya_sensor(
        dp_id=22,
        attribute_name="liquid_level_percent",
        type=t.uint8_t,
        state_class=SensorStateClass.MEASUREMENT,
        entity_type=EntityType.STANDARD,
        translation_key="liquid_level_percent",
        fallback_name="Liquid level ratio",
    )
    .tuya_number(
        dp_id=7,
        attribute_name="max_set",
        type=t.uint8_t,
        min_value=0,
        max_value=100,
        step=1,
        translation_key="max_set",
        fallback_name="Liquid max percentage",
    )
    .tuya_number(
        dp_id=8,
        attribute_name="mini_set",
        type=t.uint8_t,
        min_value=0,
        max_value=100,
        step=1,
        translation_key="mini_set",
        fallback_name="Liquid minimal percentage",
    )
    .tuya_number(
        dp_id=19,
        attribute_name="installation_height",
        type=t.uint16_t,
        device_class=SensorDeviceClass.DISTANCE,
        unit=UnitOfLength.CENTIMETERS,
        min_value=10,
        max_value=400,
        step=1,
        translation_key="installation_height",
        fallback_name="Height from sensor to tank bottom",
    )
    .tuya_number(
        dp_id=21,
        attribute_name="liquid_depth_max",
        type=t.uint16_t,
        device_class=SensorDeviceClass.DISTANCE,
        unit=UnitOfLength.CENTIMETERS,
        min_value=10,
        max_value=400,
        step=1,
        translation_key="liquid_depth_max",
        fallback_name="Height from sensor to liquid level",
    )
    .skip_configuration()
    .add_to_registry()
)

And then restarted HA

Initiated the device and added it through ZHA integration and here it is:

Now I need to figure out how to fix wrong level ))

Hi Elik, did you get it to work?

First of all, big thanks for sharing!

I have been playing around with this for a long time and in the end I found out that installation_height (and probably liquid_depth_max too) are measured in mm, not in cm. When I changed the max_value for these attributes to 4000 and CENTIMETERS to MILLIMETERS in the quirk and set the “height from sensor to tank bottom” at 1600 instead of 160 (my tank is 1,6m deep), it worked perfectly.

Also, I deleted the “converter=lambda x: x / 100, # Convert mm to meters” and rather changed the unit from METERS to CENTIMETERS. This gave the same result, but a more accurate reading as before the number was round to decimals and now to cm.

Hope this info helps you and please share if you find out anything more!

Here’s my full custom quirk with above changes.

Cheers!

from zigpy.quirks.v2 import EntityType
from zigpy.quirks.v2.homeassistant import PERCENTAGE, UnitOfLength
from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass, SensorStateClass
import zigpy.types as t

from zhaquirks.tuya.builder import TuyaQuirkBuilder

class LiquidLevelStatus(t.enum8):
    """Enum for liquid level status."""
    NORMAL = 0x00
    LOW = 0x01
    HIGH = 0x02

(
    TuyaQuirkBuilder("_TZE200_lvkk0hdg", "TS0601")
    .tuya_enum(
        dp_id=1,
        attribute_name="liquid_state",
        enum_class=LiquidLevelStatus,
        entity_type=EntityType.DIAGNOSTIC,
        translation_key="liquid_state",
        fallback_name="Liquid state",
    )
    .tuya_sensor(
        dp_id=2,
        attribute_name="liquid_depth",
        type=t.uint16_t,
        state_class=SensorStateClass.MEASUREMENT,
        device_class=SensorDeviceClass.DISTANCE,
        unit=UnitOfLength.CENTIMETERS,
        entity_type=EntityType.STANDARD,
        translation_key="liquid_depth",
        fallback_name="Liquid depth",
    )
    .tuya_sensor(
        dp_id=22,
        attribute_name="liquid_level_percent",
        type=t.uint8_t,
        state_class=SensorStateClass.MEASUREMENT,
        entity_type=EntityType.STANDARD,
        translation_key="liquid_level_percent",
        fallback_name="Liquid level ratio",
    )
    .tuya_number(
        dp_id=7,
        attribute_name="max_set",
        type=t.uint8_t,
        min_value=0,
        max_value=100,
        step=1,
        translation_key="max_set",
        fallback_name="Liquid max percentage",
    )
    .tuya_number(
        dp_id=8,
        attribute_name="mini_set",
        type=t.uint8_t,
        min_value=0,
        max_value=100,
        step=1,
        translation_key="mini_set",
        fallback_name="Liquid minimal percentage",
    )
    .tuya_number(
        dp_id=19,
        attribute_name="installation_height",
        type=t.uint16_t,
        device_class=SensorDeviceClass.DISTANCE,
        unit=UnitOfLength.MILLIMETERS,
        min_value=10,
        max_value=4000,
        step=1,
        translation_key="installation_height",
        fallback_name="Height from sensor to tank bottom",
    )
    .tuya_number(
        dp_id=21,
        attribute_name="liquid_depth_max",
        type=t.uint16_t,
        device_class=SensorDeviceClass.DISTANCE,
        unit=UnitOfLength.MILLIMETERS,
        min_value=10,
        max_value=4000,
        step=1,
        translation_key="liquid_depth_max",
        fallback_name="Height from sensor to liquid level",
    )
    .skip_configuration()
    .add_to_registry()
)
1 Like

Perfect, Thanks for the update, worked for me too!

type: custom:fluid-level-background-card
entity: sensor.water_tank_level_sensor_liquid_level_ratio
full_value: 100
level_color: rgba(0, 123, 255, 0.7)
background_color: "#e0e0e0"
card:
  type: entities
  title: Water Tank Level
  entities:
    - entity: sensor.water_tank_level_sensor_liquid_level_ratio
      name: Liquid Ratio
    - entity: sensor.water_tank_level_sensor_distance
      name: Distance
  state_color: true
  show_header_toggle: true
  card_mod:
    style: |
      ha-card h1 {
        color: black !important; /* Title color */
      }
      ha-card .card-content {
        color: black !important; /* Entities text color */
      }
      ha-card .entity-row {
        color: black !important; /* Additional safeguard for entities rows */
      }

to get it working you need to install from HACS the following:
card-mod
fluid-level-background-card

If you have a working quirk, please don’t forget to PR it so other can benefit.