Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Device Support Request] TS0601 by _TZE204_wbhaespm 3 phase power meter din #3282

Open
l4red0 opened this issue Aug 3, 2024 · 3 comments
Open
Labels
Tuya Request/PR regarding a Tuya device

Comments

@l4red0
Copy link

l4red0 commented Aug 3, 2024

Problem description

https://allegro.pl/oferta/bezpiecznik-3-fazy-regulowany-1-do-63a-zigbee-tuya-licznik-miernik-energii-16097353966

The device is a 3-phase circuit breaker (63A) with power metering, mounted on a DIN rail. It is presented as a Tuya device under the RTX brand, model C63. The device is recognized by ZHA, but no entities are registered. I was able to bind the power summation delivery entity with a custom quirk and identify attributes for each of the three phases (including amps, volts, and watts). However, I am currently stuck with correctly binding that data to ZHA.

Solution description

Bind data for all of three phases to ZHA.

Screenshots/Video

Screenshots/Video

Screenshot from 2024-08-03 21-52-20

f1aa87934f40bb389deee0144758

Device signature

Device signature
    "signature": {
      "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.FullFunctionDevice|MainsPowered|RxOnWhenIdle|AllocateAddress: 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": "0x0104",
          "device_type": "0x0051",
          "input_clusters": [
            "0x0000",
            "0x0004",
            "0x0005",
            "0x0006",
            "0x0702",
            "0x0b04",
            "0xef00"
          ],
          "output_clusters": [
            "0x000a",
            "0x0019"
          ]
        },
        "242": {
          "profile_id": "0xa1e0",
          "device_type": "0x0061",
          "input_clusters": [
            "0x0006"
          ],
          "output_clusters": [
            "0x0021"
          ]
        }
      },
      "manufacturer": "_TZE204_wbhaespm",
      "model": "TS0601"
    }

Diagnostic information

Diagnostic information
{
  "home_assistant": {
    "installation_type": "Home Assistant OS",
    "version": "2024.7.4",
    "dev": false,
    "hassio": true,
    "virtualenv": false,
    "python_version": "3.12.4",
    "docker": true,
    "arch": "x86_64",
    "timezone": "Europe/Warsaw",
    "os_name": "Linux",
    "os_version": "6.6.33-haos",
    "supervisor": "2024.06.2",
    "host_os": "Home Assistant OS 12.4",
    "docker_version": "26.1.4",
    "chassis": "vm",
    "run_as_root": true
  },
  "custom_components": {
    "frigate": {
      "documentation": "https://github.com/blakeblackshear/frigate",
      "version": "5.3.0",
      "requirements": [
        "pytz"
      ]
    },
    "hacs": {
      "documentation": "https://hacs.xyz/docs/configuration/start",
      "version": "1.34.0",
      "requirements": [
        "aiogithubapi>=22.10.1"
      ]
    }
  },
  "integration_manifest": {
    "domain": "zha",
    "name": "Zigbee Home Automation",
    "after_dependencies": [
      "onboarding",
      "usb"
    ],
    "codeowners": [
      "dmulcahey",
      "adminiuga",
      "puddly",
      "TheJulianJES"
    ],
    "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",
      "universal_silabs_flasher"
    ],
    "requirements": [
      "bellows==0.39.1",
      "pyserial==3.5",
      "zha-quirks==0.0.117",
      "zigpy-deconz==0.23.2",
      "zigpy==0.64.1",
      "zigpy-xbee==0.20.1",
      "zigpy-zigate==0.12.1",
      "zigpy-znp==0.12.2",
      "universal-silabs-flasher==0.0.20",
      "pyserial-asyncio-fast==0.11"
    ],
    "usb": [
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*2652*",
        "known_devices": [
          "slae.sh cc2652rb stick"
        ]
      },
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*slzb-07*",
        "known_devices": [
          "smlight slzb-07"
        ]
      },
      {
        "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": "0403",
        "pid": "6015",
        "description": "*conbee*",
        "known_devices": [
          "Conbee III"
        ]
      },
      {
        "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": "_uzg-01._tcp.local.",
        "name": "uzg-01*"
      },
      {
        "type": "_slzb-06._tcp.local.",
        "name": "slzb-06*"
      },
      {
        "type": "_xzg._tcp.local.",
        "name": "xzg*"
      },
      {
        "type": "_czc._tcp.local.",
        "name": "czc*"
      }
    ],
    "is_built_in": true
  },
  "setup_times": {
    "null": {
      "setup": 7.86620075814426e-05
    },
    "492de3497fa124d68975859c0cea994b": {
      "wait_import_platforms": -0.0001447649992769584,
      "wait_base_component": -0.001026408004690893,
      "config_entry_setup": 17.798879611989832
    }
  },
  "data": {
    "ieee": "**REDACTED**",
    "nwk": 22747,
    "manufacturer": "_TZE204_wbhaespm",
    "model": "TS0601",
    "name": "_TZE204_wbhaespm TS0601",
    "quirk_applied": true,
    "quirk_class": "_tze204_wbhaespm_ts0601.RTXC63PowerMeter3Phase",
    "quirk_id": null,
    "manufacturer_code": 4417,
    "power_source": "Mains",
    "lqi": 214,
    "rssi": -42,
    "last_seen": "2024-08-03T21:54:40",
    "available": true,
    "device_type": "Router",
    "signature": {
      "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.FullFunctionDevice|MainsPowered|RxOnWhenIdle|AllocateAddress: 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": "0x0104",
          "device_type": "0x0051",
          "input_clusters": [
            "0x0000",
            "0x0004",
            "0x0005",
            "0x0006",
            "0x0702",
            "0x0b04",
            "0xef00"
          ],
          "output_clusters": [
            "0x000a",
            "0x0019"
          ]
        },
        "242": {
          "profile_id": "0xa1e0",
          "device_type": "0x0061",
          "input_clusters": [
            "0x0006"
          ],
          "output_clusters": [
            "0x0021"
          ]
        }
      },
      "manufacturer": "_TZE204_wbhaespm",
      "model": "TS0601"
    },
    "active_coordinator": false,
    "entities": [
      {
        "entity_id": "sensor.tze204_wbhaespm_ts0601_apparent_power",
        "name": "_TZE204_wbhaespm TS0601"
      },
      {
        "entity_id": "sensor.tze204_wbhaespm_ts0601_current",
        "name": "_TZE204_wbhaespm TS0601"
      },
      {
        "entity_id": "sensor.tze204_wbhaespm_ts0601_voltage",
        "name": "_TZE204_wbhaespm TS0601"
      },
      {
        "entity_id": "sensor.tze204_wbhaespm_ts0601_ac_frequency",
        "name": "_TZE204_wbhaespm TS0601"
      },
      {
        "entity_id": "sensor.tze204_wbhaespm_ts0601_power_factor",
        "name": "_TZE204_wbhaespm TS0601"
      },
      {
        "entity_id": "sensor.tze204_wbhaespm_ts0601_power",
        "name": "_TZE204_wbhaespm TS0601"
      },
      {
        "entity_id": "sensor.tze204_wbhaespm_ts0601_instantaneous_demand",
        "name": "_TZE204_wbhaespm TS0601"
      },
      {
        "entity_id": "sensor.tze204_wbhaespm_ts0601_summation_delivered",
        "name": "_TZE204_wbhaespm TS0601"
      },
      {
        "entity_id": "switch.tze204_wbhaespm_ts0601_switch",
        "name": "_TZE204_wbhaespm TS0601"
      },
      {
        "entity_id": "switch.tze204_wbhaespm_ts0601_switch_2",
        "name": "_TZE204_wbhaespm TS0601"
      },
      {
        "entity_id": "update.tze204_wbhaespm_ts0601_firmware",
        "name": "_TZE204_wbhaespm TS0601"
      }
    ],
    "neighbors": [],
    "routes": [],
    "endpoint_names": [
      {
        "name": "SMART_PLUG"
      },
      {
        "name": "PROXY_BASIC"
      }
    ],
    "user_given_name": null,
    "device_reg_id": "c2fac6150588bf809cbd96daba050fdc",
    "area_id": null,
    "cluster_details": {
      "1": {
        "device_type": {
          "name": "SMART_PLUG",
          "id": 81
        },
        "profile_id": 260,
        "in_clusters": {
          "0x0000": {
            "endpoint_attribute": "basic",
            "attributes": {
              "0x0001": {
                "attribute_name": "app_version",
                "value": 74
              },
              "0x0004": {
                "attribute_name": "manufacturer",
                "value": "_TZE204_wbhaespm"
              },
              "0x0005": {
                "attribute_name": "model",
                "value": "TS0601"
              }
            },
            "unsupported_attributes": {}
          },
          "0x0004": {
            "endpoint_attribute": "groups",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0005": {
            "endpoint_attribute": "scenes",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0xef00": {
            "endpoint_attribute": "tuya_manufacturer",
            "attributes": {
              "0x0201": {
                "attribute_name": "energy",
                "value": 1003
              }
            },
            "unsupported_attributes": {}
          },
          "0x0b04": {
            "endpoint_attribute": "electrical_measurement",
            "attributes": {
              "0x0603": {
                "attribute_name": "ac_current_divisor",
                "value": 1000
              },
              "0x0602": {
                "attribute_name": "ac_current_multiplier",
                "value": 1
              },
              "0x0401": {
                "attribute_name": "ac_frequency_divisor",
                "value": 100
              },
              "0x0400": {
                "attribute_name": "ac_frequency_multiplier",
                "value": 1
              },
              "0x0601": {
                "attribute_name": "ac_voltage_divisor",
                "value": 10
              },
              "0x0600": {
                "attribute_name": "ac_voltage_multiplier",
                "value": 1
              }
            },
            "unsupported_attributes": {
              "0x0000": {
                "attribute_name": "measurement_type"
              },
              "0x0300": {
                "attribute_name": "ac_frequency"
              },
              "0x0402": {
                "attribute_name": "power_multiplier"
              },
              "0x0403": {
                "attribute_name": "power_divisor"
              },
              "0x0604": {
                "attribute_name": "ac_power_multiplier"
              },
              "0x0605": {
                "attribute_name": "ac_power_divisor"
              },
              "0x050d": {
                "attribute_name": "active_power_max"
              },
              "0x050b": {
                "attribute_name": "active_power"
              },
              "0x0508": {
                "attribute_name": "rms_current"
              },
              "0x050a": {
                "attribute_name": "rms_current_max"
              },
              "0x0505": {
                "attribute_name": "rms_voltage"
              },
              "0x0507": {
                "attribute_name": "rms_voltage_max"
              },
              "0x050f": {
                "attribute_name": "apparent_power"
              },
              "0x0510": {
                "attribute_name": "power_factor"
              },
              "0x0302": {
                "attribute_name": "ac_frequency_max"
              }
            }
          },
          "0x0702": {
            "endpoint_attribute": "smartenergy_metering",
            "attributes": {
              "0x0000": {
                "attribute_name": "current_summ_delivered",
                "value": 10.03
              },
              "0x0300": {
                "attribute_name": "unit_of_measure",
                "value": 0
              }
            },
            "unsupported_attributes": {
              "0x0400": {
                "attribute_name": "instantaneous_demand"
              },
              "0x0301": {
                "attribute_name": "multiplier"
              },
              "0x0302": {
                "attribute_name": "divisor"
              },
              "0x0303": {
                "attribute_name": "summation_formatting"
              },
              "0x0304": {
                "attribute_name": "demand_formatting"
              },
              "0x0306": {
                "attribute_name": "metering_device_type"
              },
              "0x0100": {
                "attribute_name": "current_tier1_summ_delivered"
              },
              "0x0102": {
                "attribute_name": "current_tier2_summ_delivered"
              },
              "0x0104": {
                "attribute_name": "current_tier3_summ_delivered"
              },
              "0x0106": {
                "attribute_name": "current_tier4_summ_delivered"
              },
              "0x0108": {
                "attribute_name": "current_tier5_summ_delivered"
              },
              "0x010a": {
                "attribute_name": "current_tier6_summ_delivered"
              },
              "0x0001": {
                "attribute_name": "current_summ_received"
              },
              "0x0200": {
                "attribute_name": "status"
              }
            }
          },
          "0x0006": {
            "endpoint_attribute": "on_off",
            "attributes": {},
            "unsupported_attributes": {}
          }
        },
        "out_clusters": {
          "0x000a": {
            "endpoint_attribute": "time",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0019": {
            "endpoint_attribute": "ota",
            "attributes": {
              "0x0002": {
                "attribute_name": "current_file_version",
                "value": 74
              }
            },
            "unsupported_attributes": {}
          }
        }
      },
      "242": {
        "device_type": {
          "name": "PROXY_BASIC",
          "id": 97
        },
        "profile_id": 41440,
        "in_clusters": {
          "0x0006": {
            "endpoint_attribute": "on_off",
            "attributes": {},
            "unsupported_attributes": {}
          }
        },
        "out_clusters": {
          "0x0021": {
            "endpoint_attribute": "green_power",
            "attributes": {},
            "unsupported_attributes": {}
          }
        }
      }
    }
  }
}

Logs

Logs I'm positive that three last attributes (0x0006 to 0x0008) are 3 phases. The data in each array can be decoded to sane values.
data_phase_1 = [9, 109, 0, 2, 231, 0, 0, 147]
data_phase_2 = [9, 82, 0, 2, 122, 0, 0, 125]
data_phase_3 = [9, 39, 0, 1, 141, 0, 0, 82]

# Function to decode the data
def decode_vcp(data):
    voltage = int.from_bytes(data[0:2], byteorder='big') * 0.1
    current = int.from_bytes(data[2:5], byteorder='big')
    power = int.from_bytes(data[5:8], byteorder='big')
    return voltage, current, power

returns (V, mA, W) ((241.3, 743, 147), (238.60000000000002, 634, 125), (234.3, 397, 82))
Debug logs shows that data is incoming regularly:

[zhaquirks.tuya] [0xf438:1:0xef00] Received value [0, 0, 0, 0] for attribute 0x020d (command 0x0002)
[zhaquirks.tuya] [0xf438:1:0xef00] Received value [0] for attribute 0x0509 (command 0x0002)
[zhaquirks.tuya] [0xf438:1:0xef00] Received value [0, 0, 2, 126] for attribute 0x0201 (command 0x0002)
[zhaquirks.tuya] [0xf438:1:0xef00] Received value [48, 48, 48, 48, 48, 48, 48, 48, 48, 48] for attribute 0x0313 (command 0x0002)

[zhaquirks.tuya] [0xf438:1:0xef00] Received value [9, 109, 0, 2, 231, 0, 0, 147] for attribute 0x0006 (command 0x0002)
[zhaquirks.tuya] [0xf438:1:0xef00] Received value [9, 82, 0, 2, 122, 0, 0, 125] for attribute 0x0007 (command 0x0002)
[zhaquirks.tuya] [0xf438:1:0xef00] Received value [9, 39, 0, 1, 141, 0, 0, 82] for attribute 0x0008 (command 0x0002)

Custom quirk

Custom quirk In this quirk, I attempted to parse and bind data for all three phases, but the relevant attributes in ZHA (e.g., rms_power, rms_current, rms_current_ph_b, etc.) always show a value of None. Please excuse the overall structure of the quirk; this is my first attempt at creating a custom quirk.
"""Tuya Din Power Meter."""
from zigpy.profiles import zha
import zigpy.types as t
import logging
from zigpy.zcl.clusters.general import Basic, Groups, Ota, Scenes, Time, GreenPowerProxy
from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement
from zigpy.zcl.clusters.smartenergy import Metering

from zhaquirks import Bus, LocalDataCluster
from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)
from zhaquirks.tuya import TuyaManufClusterAttributes, TuyaOnOff, TuyaSwitch

_LOGGER = logging.getLogger(__name__)

# Tuya attribute IDs
TUYA_TOTAL_ENERGY_ATTR = 0x0201
TUYA_VOLT_CURR_PHASE_1 = 0x0006
TUYA_VOLT_CURR_PHASE_2 = 0x0007
TUYA_VOLT_CURR_PHASE_3 = 0x0008
TUYA_FREQ_ATTR = 0x0269
TUYA_POWER_ATTR = 0x0267
TUYA_PFACT_ATTR = 0x026F
TUYA_REACTP_ATTR = 0x026d
TUYA_TREACTP_ATTR = 0x026e

SWITCH_EVENT = "switch_event"

class TuyaManufClusterDinPower(TuyaManufClusterAttributes):
    """Manufacturer Specific Cluster of the Tuya Power Meter device."""

    attributes = {
        TUYA_TOTAL_ENERGY_ATTR: ("energy", t.uint32_t),
        TUYA_FREQ_ATTR: ("freq", t.uint32_t),
        TUYA_PFACT_ATTR: ("pfact", t.uint32_t),
        TUYA_POWER_ATTR: ("power", t.uint32_t),
        TUYA_VOLT_CURR_PHASE_1: ("volt_curr_phase_1", t.uint32_t),
        TUYA_VOLT_CURR_PHASE_2: ("volt_curr_phase_2", t.uint32_t),
        TUYA_VOLT_CURR_PHASE_3: ("volt_curr_phase_3", t.uint32_t),
    }

    def _update_attribute(self, attrid, value):
        super()._update_attribute(attrid, value)
        if attrid == TUYA_TOTAL_ENERGY_ATTR:
            self.endpoint.smartenergy_metering.energy_reported(value / 100)
        elif attrid == TUYA_FREQ_ATTR:
            self.endpoint.electrical_measurement.freq_reported(value)
        elif attrid == TUYA_REACTP_ATTR:
            self.endpoint.electrical_measurement.reactp_reported(value)
        elif attrid == TUYA_TREACTP_ATTR:
            self.endpoint.electrical_measurement.treactp_reported(value)
        elif attrid == TUYA_POWER_ATTR:
            self.endpoint.electrical_measurement.power_reported(value)
        elif attrid == TUYA_PFACT_ATTR:
            self.endpoint.electrical_measurement.pfact_reported(value / 10)
        elif attrid in [TUYA_VOLT_CURR_PHASE_1, TUYA_VOLT_CURR_PHASE_2, TUYA_VOLT_CURR_PHASE_3]:
            _LOGGER.debug(f"Received phase data: attrid={attrid}, value={value}")
            # Handling voltage and current for each phase
            power = int.from_bytes(value[0:3], byteorder="big")
            current = int.from_bytes(value[3:6], byteorder="big")
            voltage = int.from_bytes(value[6:8], byteorder="big") * 0.1
            phase_index = attrid - TUYA_VOLT_CURR_PHASE_1  # Determine phase index (0, 1, or 2)

            _LOGGER.debug(
                f"Phase {phase_index+1} - Power: {power}W, Current: {current}A, Voltage: {voltage}V"
            )
            self.endpoint.electrical_measurement.vcp_reported(voltage, current, power, phase_index)
        elif attrid == TUYA_DIN_SWITCH_ATTR:
            self.endpoint.device.switch_bus.listener_event(
                SWITCH_EVENT, self.endpoint.endpoint_id, value
            )

class TuyaPowerMeasurement(LocalDataCluster, ElectricalMeasurement):
    """Custom class for power, voltage, and current measurement."""

    cluster_id = ElectricalMeasurement.cluster_id

    AC_VOLTAGE_MULTIPLIER = 0x0600
    AC_VOLTAGE_DIVISOR = 0x0601
    AC_CURRENT_MULTIPLIER = 0x0602
    AC_CURRENT_DIVISOR = 0x0603
    AC_FREQ_MULTIPLIER = 0x0400
    AC_FREQ_DIVISOR = 0x0401

    _CONSTANT_ATTRIBUTES = {
        AC_VOLTAGE_MULTIPLIER: 1,
        AC_VOLTAGE_DIVISOR: 10,
        AC_CURRENT_MULTIPLIER: 1,
        AC_CURRENT_DIVISOR: 1000,
        AC_FREQ_MULTIPLIER: 1,
        AC_FREQ_DIVISOR: 100,
    }

    phase_attributes = [
        {
            "voltage": ElectricalMeasurement.AttributeDefs.rms_voltage.id,
            "current": ElectricalMeasurement.AttributeDefs.rms_current.id,
            "power": ElectricalMeasurement.AttributeDefs.active_power.id,
        },
        {
            "voltage": ElectricalMeasurement.AttributeDefs.rms_voltage_ph_b.id,
            "current": ElectricalMeasurement.AttributeDefs.rms_current_ph_b.id,
            "power": ElectricalMeasurement.AttributeDefs.active_power_ph_b.id,
        },
        {
            "voltage": ElectricalMeasurement.AttributeDefs.rms_voltage_ph_c.id,
            "current": ElectricalMeasurement.AttributeDefs.rms_current_ph_c.id,
            "power": ElectricalMeasurement.AttributeDefs.active_power_ph_c.id,
        },
    ]

    def vcp_reported(self, voltage, current, power, phase=0):
        """Voltage, current, power reported."""
        try:
            if phase < 0 or phase > 2:
                _LOGGER.error(f"Invalid phase index: {phase}")
                return
    
            _LOGGER.debug(f"Received data for phase {phase+1}: Voltage={voltage}, Current={current}, Power={power}")
    
            # Update the corresponding attributes
            self._update_attribute(self.phase_attributes[phase]["voltage"], voltage)
            self._update_attribute(self.phase_attributes[phase]["current"], current)
            self._update_attribute(self.phase_attributes[phase]["power"], power)
    
            _LOGGER.debug(
                f"Updated attributes for phase {phase+1} - Voltage: {voltage}V, Current: {current}A, Power: {power}W"
            )
        except Exception as e:
            _LOGGER.error(f"Error updating attributes for phase {phase+1}: {str(e)}")




class TuyaElectricalMeasurement(LocalDataCluster, Metering):
    """Custom class for total energy measurement."""

    cluster_id = Metering.cluster_id
    CURRENT_ID = 0x0000
    POWER_WATT = 0x0000

    """Setting unit of measurement."""
    _CONSTANT_ATTRIBUTES = {0x0300: POWER_WATT}

    def energy_reported(self, value):
        """Summation Energy reported."""
        self._update_attribute(self.CURRENT_ID, value)


class RTXC63PowerMeter3Phase(TuyaSwitch):
    """RTXC63 Power Meter Device"""

    def __init__(self, *args, **kwargs):
        """Init device."""
        self.switch_bus = Bus()
        super().__init__(*args, **kwargs)

    signature = {
        MODELS_INFO: [
            ("_TZE204_wbhaespm", "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,
                    TuyaManufClusterAttributes.cluster_id,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 0xA1E0,
                DEVICE_TYPE: 0x0061,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaManufClusterDinPower,
                    TuyaPowerMeasurement,
                    TuyaElectricalMeasurement,
                    TuyaOnOff,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 0xA1E0,
                DEVICE_TYPE: 0x0061,
                INPUT_CLUSTERS: [
                    TuyaOnOff,
                ],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        }
    }

Additional information

All suggestions on how to bind the data in ZHA would be greatly appreciated.

@l4red0
Copy link
Author

l4red0 commented Aug 4, 2024

I've made some progress. I tinkered a bit and checked how the implementations of other three-phase meters look. I think I'm now mapping the entire data from the three phases + summation delivered. Unfortunately, the sensors in the ZHA device panel only show data from the first phase (even though the data is transmitted and read by ZHA for all three phases; rms_voltage - as a sensor, only shows phase 1, rms_voltage_ph_b - hidden but reads data, similarly with current and power for the other phases). Additionally, the sum of W for the respective phases goes to total_active_power (it's calculated correctly but doesn't display as a sensor).

There are still some other mysteries to solve like switch control or these attributes:

[zhaquirks.tuya] [0xf438:1:0xef00] Received value [0, 0, 0, 0] for attribute 0x020d (command 0x0002)
[zhaquirks.tuya] [0xf438:1:0xef00] Received value [0] for attribute 0x0509 (command 0x0002)
[zhaquirks.tuya] [0xf438:1:0xef00] Received value [0, 0, 2, 126] for attribute 0x0201 (command 0x0002) #summation deliverd
[zhaquirks.tuya] [0xf438:1:0xef00] Received value [48, 48, 48, 48, 48, 48, 48, 48, 48, 48] for attribute 0x0313 (command 0x0002)

This is far from an ideal solution but it allows for further development. I would appreciate any guidance. Especially how to make sensors out of recivied data.

Screenshot from 2024-08-04 16-40-43

Additional screenshots

Screenshot from 2024-08-04 16-40-27

Screenshot from 2024-08-04 16-46-14

Screenshot from 2024-08-04 16-58-50

Custom quirk (very messy!)
"""Tuya Din Power Meter."""
from zigpy.profiles import zha
import zigpy.types as t
import logging
from zigpy.zcl.clusters.general import Basic, Groups, Ota, Scenes, Time, GreenPowerProxy
from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement
from zigpy.zcl.clusters.smartenergy import Metering

from zhaquirks import Bus, LocalDataCluster
from zhaquirks.const import (
  DEVICE_TYPE,
  ENDPOINTS,
  INPUT_CLUSTERS,
  MODELS_INFO,
  OUTPUT_CLUSTERS,
  PROFILE_ID,
)
from zhaquirks.tuya import TuyaManufClusterAttributes, TuyaOnOff
from zigpy.quirks import CustomDevice

_LOGGER = logging.getLogger(__name__)

# Tuya attribute IDs
TUYA_TOTAL_ENERGY_ATTR = 0x0201
#TUYA_CURRENT_ATTR = 0x0212
#TUYA_POWER_ATTR = 0x0267
#TUYA_VOLTAGE_ATTR = 0x0214

RTX_TOTAL_ACTIVE_POWER_ATTR = 0x0304
RTX_TOTAL_CURRENT = 0x0212
RTX_AVG_VOLTAGE = 0x0214
RTX_VCP_P1 = 0x0006
RTX_VCP_P2 = 0x0007
RTX_VCP_P3 = 0x0008

#SWITCH_EVENT = "switch_event"

class TuyaManufClusterDinPower(TuyaManufClusterAttributes):
  """Manufacturer Specific Cluster of the Tuya Power Meter device."""

  attributes = {
      TUYA_TOTAL_ENERGY_ATTR: ("energy", t.uint32_t, True),
      RTX_TOTAL_CURRENT: ("current", t.uint32_t, True),
      RTX_AVG_VOLTAGE: ("voltage", t.uint32_t, True),
      RTX_TOTAL_ACTIVE_POWER_ATTR: ("power", t.uint32_t, True),
      RTX_VCP_P1: ("vcp_ph_1", t.data64, True),
      RTX_VCP_P2: ("vcp_ph_2", t.data64, True),
      RTX_VCP_P3: ("vcp_ph_3", t.data64, True),
  }

  def _update_attribute(self, attrid, value):
      super()._update_attribute(attrid, value)
      if attrid == TUYA_TOTAL_ENERGY_ATTR:
          self.endpoint.smartenergy_metering.energy_reported(value / 100)
      elif attrid in [RTX_VCP_P1, RTX_VCP_P2, RTX_VCP_P3]:
          _LOGGER.debug(f"Received phase data: attrid={attrid}, value={value}")
          # Handling voltage and current for each phase
          power = int.from_bytes(value[0:3], byteorder="little")
          current = int.from_bytes(value[3:6], byteorder="little")
          voltage = int.from_bytes(value[6:8], byteorder="little")
          phase_index = attrid - RTX_VCP_P1  # Determine phase index (0, 1, or 2)

          # Update individual phase values
          self.endpoint.electrical_measurement.vcp_reported(voltage, current, power, phase_index)

      #elif attrid == TUYA_DIN_SWITCH_ATTR:
      #    self.endpoint.device.switch_bus.listener_event(
      #        SWITCH_EVENT, self.endpoint.endpoint_id, value
      #    )


class TuyaPowerMeasurement(LocalDataCluster, ElectricalMeasurement):
  """Custom class for power, voltage, and current measurement."""

  cluster_id = ElectricalMeasurement.cluster_id

  AC_VOLTAGE_MULTIPLIER = 0x0600
  AC_VOLTAGE_DIVISOR = 0x0601
  AC_CURRENT_MULTIPLIER = 0x0602
  AC_CURRENT_DIVISOR = 0x0603
  AC_FREQ_MULTIPLIER = 0x0400
  AC_FREQ_DIVISOR = 0x0401

  _CONSTANT_ATTRIBUTES = {
      AC_VOLTAGE_MULTIPLIER: 1,
      AC_VOLTAGE_DIVISOR: 10,
      AC_CURRENT_MULTIPLIER: 1,
      AC_CURRENT_DIVISOR: 1000,
      AC_FREQ_MULTIPLIER: 1,
      AC_FREQ_DIVISOR: 100,
  }

  phase_attributes = [
      {
          "voltage": ElectricalMeasurement.AttributeDefs.rms_voltage.id,
          "current": ElectricalMeasurement.AttributeDefs.rms_current.id,
          "power": ElectricalMeasurement.AttributeDefs.active_power.id,
      },
      {
          "voltage": ElectricalMeasurement.AttributeDefs.rms_voltage_ph_b.id,
          "current": ElectricalMeasurement.AttributeDefs.rms_current_ph_b.id,
          "power": ElectricalMeasurement.AttributeDefs.active_power_ph_b.id,
      },
      {
          "voltage": ElectricalMeasurement.AttributeDefs.rms_voltage_ph_c.id,
          "current": ElectricalMeasurement.AttributeDefs.rms_current_ph_c.id,
          "power": ElectricalMeasurement.AttributeDefs.active_power_ph_c.id,
      },
  ]

  def vcp_reported(self, voltage, current, power, phase=0):
      """Voltage, current, power reported."""
      try:
          if phase < 0 or phase > 2:
              _LOGGER.error(f"Invalid phase index: {phase}")
              return
  
          _LOGGER.debug(f"Received data for phase {phase+1}: Voltage={voltage}, Current={current}, Power={power}")
  
          # Update the corresponding attributes
          self._update_attribute(self.phase_attributes[phase]["voltage"], voltage)
          self._update_attribute(self.phase_attributes[phase]["current"], current)
          self._update_attribute(self.phase_attributes[phase]["power"], power)
              
          # Calculate the total active power across all phases
          self.update_total_active_power()

      except Exception as e:
          _LOGGER.error(f"Error updating attributes for phase {phase+1}: {str(e)}")

  def update_total_active_power(self):
      """Calculate and update the total active power across all phases."""
      try:
          total_power = 0
          for phase in range(3):
              power_attr = self.phase_attributes[phase]["power"]
              phase_power = self.get(power_attr, 0)
              total_power += phase_power
          
          _LOGGER.debug(f"Total active power across all phases: {total_power}W")
          # Update the total power attribute
          self._update_attribute(RTX_TOTAL_ACTIVE_POWER_ATTR, total_power)

      except Exception as e:
          _LOGGER.error(f"Error calculating total active power: {str(e)}")


class TuyaElectricalMeasurement(LocalDataCluster, Metering):
  """Custom class for total energy measurement."""

  cluster_id = Metering.cluster_id
  CURRENT_ID = 0x0000
  POWER_WATT = 0x0000

  """Setting unit of measurement."""
  _CONSTANT_ATTRIBUTES = {0x0300: POWER_WATT}

  def energy_reported(self, value):
      """Summation Energy reported."""
      self._update_attribute(self.CURRENT_ID, value)


class RTXC63PowerMeter3Phase(CustomDevice):
  """RTXC63 Power Meter Device"""

  def __init__(self, *args, **kwargs):
      """Init device."""
      self.switch_bus = Bus()
      super().__init__(*args, **kwargs)

  signature = {
      MODELS_INFO: [
          ("_TZE204_wbhaespm", "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,
                  TuyaManufClusterAttributes.cluster_id,
              ],
              OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
          },
          242: {
              PROFILE_ID: 0xA1E0,
              DEVICE_TYPE: 0x0061,
              INPUT_CLUSTERS: [],
              OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
          },
      },
  }

  replacement = {
      ENDPOINTS: {
          1: {
              PROFILE_ID: zha.PROFILE_ID,
              DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
              INPUT_CLUSTERS: [
                  Basic.cluster_id,
                  Groups.cluster_id,
                  Scenes.cluster_id,
                  TuyaManufClusterDinPower,
                  TuyaPowerMeasurement,
                  TuyaElectricalMeasurement,
              ],
              OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
          },
          242: {
              PROFILE_ID: 0xA1E0,
              DEVICE_TYPE: 0x0061,
              INPUT_CLUSTERS: [
                  TuyaOnOff,
              ],
              OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
          },
      }
  }

@TheJulianJES TheJulianJES added the Tuya Request/PR regarding a Tuya device label Aug 10, 2024
@l4red0
Copy link
Author

l4red0 commented Aug 15, 2024

I was able to build the basic functionality I was aiming for, but I am pausing work on this quirk. However, there are still many features and data from the device that remain unhandled, and the ones I managed to implement could be done better.

In this quirk, I've combined power, current, and voltage readings across all three phases to provide a single entity for each metric. The power values from all phases are summed and reflected in the main active_power attribute, while the current values are summed and stored in rms_current. For voltage, there is calculated the average across all three phases and assigned it to rms_voltage.

The original data for phase A has been overwritten with these combined values, so entities now represent the overall 3-phase performance rather than just phase A. This gives more accurate and holistic readings for power, current, and voltage while keeping the energy summation unchanged. Worth to mention separate VCP data for all three phases is accessible by 'manage device' option but not as entities.

Quirk provides total power (sum), total current (sum), and average voltage, all while preserving the original energy summation behavior. This implementation improves monitoring of the 3-phase system by giving a complete view rather than per-phase / phase A data.

Any further development (like on/off switch or mystery attributes mentioned in previous comment) will be very welcome. I.e I'm positive that over-current threshold can be set by configuration.

Zrzut ekranu 2024-08-15 220740
Clipboard_08-15-2024_01

Custom quirk (read comment above!)
from zigpy.profiles import zha
import zigpy.types as t
from zigpy.quirks import CustomDevice
from zigpy.zcl.clusters.general import Basic, Groups, Ota, Scenes, Time, GreenPowerProxy
from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement
from zigpy.zcl.clusters.smartenergy import Metering
import logging

from zhaquirks import Bus, LocalDataCluster
from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)
from zhaquirks.tuya import TuyaManufClusterAttributes

_LOGGER = logging.getLogger(__name__)

TUYA_TOTAL_ENERGY_ATTR = 0x0201
RTX_VCP_P1 = 0x0006
RTX_VCP_P2 = 0x0007
RTX_VCP_P3 = 0x0008

class TuyaManufClusterDinPower(TuyaManufClusterAttributes):

    attributes = {
        TUYA_TOTAL_ENERGY_ATTR: ("energy", t.uint32_t, True),
        RTX_VCP_P1: ("vcp_ph_1", t.data64, True),
        RTX_VCP_P2: ("vcp_ph_2", t.data64, True),
        RTX_VCP_P3: ("vcp_ph_3", t.data64, True),
    }

    def _update_attribute(self, attrid, value):
        super()._update_attribute(attrid, value)
        if attrid == TUYA_TOTAL_ENERGY_ATTR:
            self.endpoint.smartenergy_metering.energy_reported(value / 100)
        elif attrid in [RTX_VCP_P1, RTX_VCP_P2, RTX_VCP_P3]:
            _LOGGER.debug(f"Received phase data: attrid={attrid}, value={value}")
            power = int.from_bytes(value[0:3], byteorder="little")
            current = int.from_bytes(value[3:6], byteorder="little")
            voltage = int.from_bytes(value[6:8], byteorder="little")
            phase_index = attrid - RTX_VCP_P1  
            self.endpoint.electrical_measurement.vcp_reported(voltage, current, power, phase_index)

class TuyaPowerMeasurement(LocalDataCluster, ElectricalMeasurement):

    cluster_id = ElectricalMeasurement.cluster_id

    phase_attributes = [
        {  # Phase 1
            "voltage": ElectricalMeasurement.AttributeDefs.rms_voltage.id,
            "current": ElectricalMeasurement.AttributeDefs.rms_current.id,
            "power": ElectricalMeasurement.AttributeDefs.active_power.id,
        },
        {  # Phase 2
            "voltage": ElectricalMeasurement.AttributeDefs.rms_voltage_ph_b.id,
            "current": ElectricalMeasurement.AttributeDefs.rms_current_ph_b.id,
            "power": ElectricalMeasurement.AttributeDefs.active_power_ph_b.id,
        },
        {  # Phase 3
            "voltage": ElectricalMeasurement.AttributeDefs.rms_voltage_ph_c.id,
            "current": ElectricalMeasurement.AttributeDefs.rms_current_ph_c.id,
            "power": ElectricalMeasurement.AttributeDefs.active_power_ph_c.id,
        },
    ]

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.phase_power = [0, 0, 0]
        self.phase_current = [0, 0, 0]
        self.phase_voltage = [0, 0, 0]

    def vcp_reported(self, voltage, current, power, phase=0):
        try:
            if phase < 0 or phase > 2:
                _LOGGER.error(f"Invalid phase index: {phase}")
                return

            self._update_attribute(self.phase_attributes[phase]["voltage"], voltage)
            self._update_attribute(self.phase_attributes[phase]["current"], current)
            self._update_attribute(self.phase_attributes[phase]["power"], power)

            self.phase_power[phase] = power
            self.phase_current[phase] = current
            self.phase_voltage[phase] = voltage

            total_power = sum(self.phase_power)
            _LOGGER.debug(f"Total active power across all phases: {total_power}W")
            self._update_attribute(ElectricalMeasurement.AttributeDefs.active_power.id, total_power)

            total_current = sum(self.phase_current)
            _LOGGER.debug(f"Total current across all phases: {total_current}mA")
            self._update_attribute(ElectricalMeasurement.AttributeDefs.rms_current.id, total_current)

            average_voltage = sum(self.phase_voltage) / 3.0
            _LOGGER.debug(f"Average voltage across all phases: {average_voltage}V")
            self._update_attribute(ElectricalMeasurement.AttributeDefs.rms_voltage.id, round(average_voltage))

        except Exception as e:
            _LOGGER.error(f"Error updating attributes for phase {phase+1}: {str(e)}")

class TuyaElectricalMeasurement(LocalDataCluster, Metering):

    cluster_id = Metering.cluster_id

    def energy_reported(self, value):
        self._update_attribute(Metering.AttributeDefs.current_summ_delivered.id, value)

class TuyaPowerMeter3Phase(CustomDevice):

    signature = {
          #"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.FullFunctionDevice|MainsPowered|RxOnWhenIdle|AllocateAddress: 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": "0x0104",
          #    "device_type": "0x0051",
          #    "input_clusters": [
          #      "0x0000",
          #      "0x0004",
          #      "0x0005",
          #      "0x0702",
          #      "0x0b04",
          #      "0xef00"
          #    ],
          #    "output_clusters": [
          #      "0x000a",
          #      "0x0019"
          #    ]
          #  },
          #  "242": {
          #    "profile_id": "0xa1e0",
          #    "device_type": "0x0061",
          #    "input_clusters": [
          #      "0x0006"
          #    ],
          #    "output_clusters": [
          #      "0x0021"
          #    ]
          #  }
      
        MODELS_INFO: [("_TZE204_wbhaespm", "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,
                    TuyaManufClusterAttributes.cluster_id,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: { 
                PROFILE_ID: 0xA1E0,
                DEVICE_TYPE: 0x0061,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaManufClusterDinPower,
                    TuyaPowerMeasurement,
                    TuyaElectricalMeasurement,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 0xA1E0,
                DEVICE_TYPE: 0x0061,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        }
    }

@Noamibr
Copy link

Noamibr commented Sep 5, 2024

Thank you for this, highly appreciated.
I have the exact model and have the same issue.
When applying the quirk, it now shows the sensor's information but combined (for example - instead of 240V it shows 720V).
I still don't see any controls though...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Tuya Request/PR regarding a Tuya device
Projects
None yet
Development

No branches or pull requests

3 participants