Skip to content

Instantly share code, notes, and snippets.

@franortiz
Last active July 25, 2025 23:35
Show Gist options
  • Select an option

  • Save franortiz/f0253489098c2d5b1ccd89de661202d9 to your computer and use it in GitHub Desktop.

Select an option

Save franortiz/f0253489098c2d5b1ccd89de661202d9 to your computer and use it in GitHub Desktop.
zigpy quirk for TS0601 - Circuit Breaker with Current Leakage Detection and Electrical Measurement
"""Tuya 1 Phase Circuit Breaker with Current Leakage Detection and Electrical Measurement."""
"""WIP: Need multi attribute -> DP code for over/under settings"""
from zigpy.quirks.v2 import SensorDeviceClass, SensorStateClass, EntityPlatform, EntityType
from zigpy.quirks.v2.homeassistant import (
UnitOfElectricCurrent,
UnitOfEnergy,
UnitOfPower,
UnitOfTime
)
from typing import Any
import zigpy.types as t
from zigpy.zcl import foundation as f
from zigpy.zcl.clusters.general import LevelControl, OnOff, DeviceTemperature
from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement
from zhaquirks.tuya import DPToAttributeMapping, TuyaLocalCluster, PowerOnState
from zhaquirks.tuya.builder import TuyaQuirkBuilder
from zhaquirks.tuya.ts0601_rcbo import (
TuyaRCBOOnOff,
)
class FaultCode(t.enum8):
"""Fault Code enum."""
CLEAR = 0x00
OVERCURRENT = 0x01
UNDERVOLTAGE = 0x02
OVERVOLTAGE = 0x04
OVERLEAKAGECURRENT = 0x08
OVERTEMPERATURE = 0x10
class DeviceStatus(t.enum8):
"""Tuya Device Status"""
ONLINE = 0x00
ARMED = 0x01
def dp_to_power(data: bytes) -> int:
"""Convert DP data to power value."""
# From Koenkk/zigbee2mqtt#18603 (comment)
power = int(data)
if power > 0x0FFFFFFF:
power = (0x1999999C - power) * -1
return power
def multi_dp_to_power(data: bytes) -> int:
"""Convert DP data to power value."""
# Support negative power readings
# From Koenkk/zigbee2mqtt#18603 (comment)
power = data[7] | (data[6] << 8)
if power > 0x7FFF:
power = (0x999A - power) * -1
return power
def multi_dp_to_current(data: bytes) -> int:
"""Convert DP data to current value."""
return data[4] | (data[3] << 8)
def multi_dp_to_voltage(data: bytes) -> int:
"""Convert DP data to voltage value."""
return data[1] | (data[0] << 8)
class Tuya1PhaseRCBOEM(ElectricalMeasurement, TuyaLocalCluster):
"""Tuya 1 Phase Circuit Breaker with Current Leakage Detection and Electrical Measurement."""
_CONSTANT_ATTRIBUTES = {
ElectricalMeasurement.AttributeDefs.ac_current_multiplier.id: 1,
ElectricalMeasurement.AttributeDefs.ac_current_divisor.id: 1000,
ElectricalMeasurement.AttributeDefs.ac_voltage_multiplier.id: 1,
ElectricalMeasurement.AttributeDefs.ac_voltage_divisor.id: 10,
ElectricalMeasurement.AttributeDefs.ac_frequency.id: 50,
}
class AttributeDefs(ElectricalMeasurement.AttributeDefs):
"""Attribute definitions."""
def update_attribute(self, attr_name: str, value: Any) -> None:
"""Calculate active current and power factor."""
super().update_attribute(attr_name, value)
if attr_name == "rms_current":
rms_voltage = self.get("rms_voltage")
if rms_voltage:
apparent_power = value * rms_voltage / 1000 / 10
super().update_attribute("apparent_power", int(apparent_power))
if attr_name == "active_power":
apparent_power = self.get("apparent_power")
if apparent_power:
power_factor = value / apparent_power * 100
power_factor = min(power_factor, 100)
super().update_attribute("power_factor", round(power_factor))
class Tuya1PhaseRCBODT(DeviceTemperature, TuyaLocalCluster):
"""Tuya 1 Phase Circuit Breaker with Temperature Sensing Cluster."""
class AttributeDefs(DeviceTemperature.AttributeDefs):
high_temp_trip = f.ZCLAttributeDef(
id=0xff10, zcl_type=t.Bool, is_manufacturer_specific=True
)
(
TuyaQuirkBuilder("_TZE204_fhvdgeuh", "TS0601")
# dp 0x01 - Energy
.tuya_sensor(
dp_id=1,
attribute_name="energy_consumed",
type=t.int32s,
divisor=100,
state_class=SensorStateClass.TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
unit=UnitOfEnergy.KILO_WATT_HOUR,
fallback_name="Total energy",
)
# dp 0x06 - Instant Power / Voltage / Current
.tuya_dp_multi(
dp_id=6,
attribute_mapping=[
DPToAttributeMapping(
ep_attribute=Tuya1PhaseRCBOEM.ep_attribute,
attribute_name=Tuya1PhaseRCBOEM.AttributeDefs.active_power.name,
converter=multi_dp_to_power,
),
DPToAttributeMapping(
ep_attribute=Tuya1PhaseRCBOEM.ep_attribute,
attribute_name=Tuya1PhaseRCBOEM.AttributeDefs.rms_voltage.name,
converter=multi_dp_to_voltage,
),
DPToAttributeMapping(
ep_attribute=Tuya1PhaseRCBOEM.ep_attribute,
attribute_name=Tuya1PhaseRCBOEM.AttributeDefs.rms_current.name,
converter=multi_dp_to_current,
),
],
)
# dp 0x09 - Faults
.tuya_enum(
dp_id=9,
attribute_name="fault",
enum_class=FaultCode,
entity_type=EntityType.STANDARD,
entity_platform=EntityPlatform.SENSOR,
translation_key="fault",
fallback_name="Fault",
)
# dp 0x0b (11) => '?' value Bool.false report every 10m
# dp 0x0c (12) => '?' value Bool.false on boot
# dp 0x0d (13) => '?' value 0 report every 10m
# dp 0x0f - Leakage Current
.tuya_sensor(
dp_id=15,
attribute_name="leakage_current",
type=t.uint32_t,
divisor=1,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.CURRENT,
unit=UnitOfElectricCurrent.MILLIAMPERE,
translation_key='leakage_current',
fallback_name="Leakage Current",
)
# dp 0x10 - On / Off
.tuya_onoff(dp_id=16, onoff_cfg=TuyaRCBOOnOff)
# dp 0x11 (17) - overvoltage / undervoltage thresholds
# dp 0x12 (18) - overcurrent / leakage / temperature? thresholds
# dp 0x66 - Reclose Allowed Times
.tuya_number(
dp_id=102,
type=t.uint16_t,
attribute_name="reclosing_allowed_times",
unit=None,
min_value=1,
max_value=30,
translation_key="reclosing_allowed_times",
fallback_name="Reclose Allowed Times",
mode="box"
)
# dp 0x67 - Temperature
.tuya_dp(
dp_id=103,
ep_attribute=Tuya1PhaseRCBODT.ep_attribute,
attribute_name=Tuya1PhaseRCBODT.AttributeDefs.current_temperature.name,
converter=lambda x: x * 100
)
# dp 0x68 - Reclose Enable
.tuya_switch(
dp_id=104,
attribute_name="reclosing_enable",
translation_key="reclosing_enable",
fallback_name="Reclose Enable",
)
# dp 0x69 (105) - '?' value 0 report on boot
# dp 0x6a (106) - '?' value 'EwAAAAAAAAAAAA==' ('\x13\x00\x00\x00\x00\x00\x00\x00\x00\x00') report on boot
# dp 0x6b - Reclose Recover Time (s)
.tuya_number(
dp_id=107,
type=t.uint16_t,
attribute_name="reclose_recover_seconds",
unit=UnitOfTime.SECONDS,
min_value=10,
max_value=99,
translation_key="reclose_recover_seconds",
fallback_name="Reclose Recover",
mode="box"
)
# dp 0x6d (109) - '?' value 'AAAA' ('\x00\x00\x00') report on boot
# dp 0x77 - Power on delay / Power on time (no reporting)
# .tuya_number(
# dp_id=119,
# type=t.uint16_t,
# attribute_name="power_on_delay",
# unit=UnitOfTime.SECONDS,
# min_value=1,
# max_value=50,
# translation_key="power_on_delay",
# fallback_name="Power On Delay",
# mode="box"
# )
# dp 0x7f - Device Status # need more info on options
.tuya_enum(
dp_id=127,
attribute_name="status",
enum_class=DeviceStatus,
entity_type=EntityType.STANDARD,
entity_platform=EntityPlatform.SENSOR,
translation_key="status",
fallback_name="Status"
)
# dp 0x86 - Power On State
.tuya_enum(
dp_id=134,
attribute_name="relay_status_for_power_on",
enum_class=PowerOnState,
translation_key="power_on_state",
fallback_name="Power On State"
)
.adds(Tuya1PhaseRCBOEM)
.adds(Tuya1PhaseRCBODT)
.removes(LevelControl.cluster_id)
.skip_configuration()
.add_to_registry()
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment