zigpy / zha-device-handlers

ZHA device handlers bridge the functionality gap created when manufacturers deviate from the ZCL specification, handling deviations and exceptions by parsing custom messages to and from Zigbee devices.
Apache License 2.0
683 stars 634 forks source link

[Device Support Request] SPM01-D2TZ-ZM Zemismart Energy Sensor TS0601 _TZE200_qhlxve78 #3045

Open kneschi2 opened 3 months ago

kneschi2 commented 3 months ago

Problem description

Paring went smoothly but I don't see any sensors or entities

Solution description

Sensors and entities to display power in W and energy in kWh

Screenshots/Video

image

Device Information

Device signature

Device signature ```json { "node_descriptor": "NodeDescriptor(logical_type=, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=, mac_capability_flags=, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=, *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", "0xef00" ], "output_clusters": [ "0x000a", "0x0019" ] } }, "manufacturer": "_TZE200_qhlxve78", "model": "TS0601", "class": "zigpy.device.Device" } ```

Diagnostic information

Diagnostic information ```json { "home_assistant": { "installation_type": "Home Assistant OS", "version": "2024.3.1", "dev": false, "hassio": true, "virtualenv": false, "python_version": "3.12.2", "docker": true, "arch": "x86_64", "timezone": "Europe/Berlin", "os_name": "Linux", "os_version": "6.6.20-haos", "supervisor": "2024.03.0", "host_os": "Home Assistant OS 12.1", "docker_version": "24.0.7", "chassis": "embedded", "run_as_root": true }, "custom_components": { "alarmo": { "version": "v1.9.15", "requirements": [] }, "hoymiles_dtu": { "version": "0.6.1", "requirements": [ "plum-py>=0.8.6" ] }, "adaptive_lighting": { "version": "1.20.0", "requirements": [ "ulid-transform" ] }, "hacs": { "version": "1.34.0", "requirements": [ "aiogithubapi>=22.10.1" ] }, "frigate": { "version": "5.0.1", "requirements": [ "pytz==2022.7" ] } }, "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", "import_executor": true, "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.38.1", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.112", "zigpy-deconz==0.23.1", "zigpy==0.63.4", "zigpy-xbee==0.20.1", "zigpy-zigate==0.12.0", "zigpy-znp==0.12.1", "universal-silabs-flasher==0.0.18", "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*" } ], "is_built_in": true }, "data": { "ieee": "**REDACTED**", "nwk": 20669, "manufacturer": "_TZE200_qhlxve78", "model": "TS0601", "name": "_TZE200_qhlxve78 TS0601", "quirk_applied": false, "quirk_class": "zigpy.device.Device", "quirk_id": null, "manufacturer_code": 4098, "power_source": "Mains", "lqi": 61, "rssi": null, "last_seen": "2024-03-16T18:36:54", "available": true, "device_type": "Router", "signature": { "node_descriptor": "NodeDescriptor(logical_type=, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=, mac_capability_flags=, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=, *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", "0xef00" ], "output_clusters": [ "0x000a", "0x0019" ] } }, "manufacturer": "_TZE200_qhlxve78", "model": "TS0601" }, "active_coordinator": false, "entities": [ { "entity_id": "update.solarpumpe_firmware", "name": "_TZE200_qhlxve78 TS0601" } ], "neighbors": [ { "device_type": "Coordinator", "rx_on_when_idle": "On", "relationship": "Sibling", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0x0000", "permit_joining": "Unknown", "depth": "0", "lqi": "39" }, { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "Sibling", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0x12D9", "permit_joining": "Unknown", "depth": "15", "lqi": "48" }, { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "Sibling", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0xB72C", "permit_joining": "Unknown", "depth": "15", "lqi": "45" }, { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "Sibling", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0xB742", "permit_joining": "Unknown", "depth": "15", "lqi": "53" }, { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "Sibling", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0xB753", "permit_joining": "Unknown", "depth": "15", "lqi": "67" }, { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "Parent", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0xCA19", "permit_joining": "Unknown", "depth": "15", "lqi": "115" } ], "routes": [ { "dest_nwk": "0x0000", "route_status": "Active", "memory_constrained": false, "many_to_one": true, "route_record_required": false, "next_hop": "0xCA19" } ], "endpoint_names": [ { "name": "SMART_PLUG" } ], "user_given_name": "Solarpumpe", "device_reg_id": "10206b93ad345e03081e2c1075acc480", "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": 68 }, "0x0003": { "attribute_name": "hw_version", "value": 1 }, "0x0004": { "attribute_name": "manufacturer", "value": "_TZE200_qhlxve78" }, "0x0005": { "attribute_name": "model", "value": "TS0601" }, "0x0007": { "attribute_name": "power_source", "value": 1 } }, "unsupported_attributes": { "0x0011": { "attribute_name": "physical_env" }, "0x0013": { "attribute_name": "alarm_mask" }, "0x0012": { "attribute_name": "device_enabled" } } }, "0x0004": { "endpoint_attribute": "groups", "attributes": {}, "unsupported_attributes": {} }, "0x0005": { "endpoint_attribute": "scenes", "attributes": { "0xfffd": { "attribute_name": "cluster_revision", "value": 2 }, "0x0000": { "attribute_name": "count", "value": 0 }, "0x0001": { "attribute_name": "current_scene", "value": 0 } }, "unsupported_attributes": { "0x0005": { "attribute_name": "last_configured_by" } } }, "0xef00": { "endpoint_attribute": null, "attributes": {}, "unsupported_attributes": {} } }, "out_clusters": { "0x0019": { "endpoint_attribute": "ota", "attributes": { "0x0002": { "attribute_name": "current_file_version", "value": 68 } }, "unsupported_attributes": { "0x0002": { "attribute_name": "current_file_version" } } }, "0x000a": { "endpoint_attribute": "time", "attributes": {}, "unsupported_attributes": {} } } } } } } ```

Logs

Logs ```python [Paste the logs here] ```

Custom quirk

Custom quirk ```python [Paste your custom quirk here] ```

Additional information

No response

nazmang commented 3 months ago

This worked for me ts0601_din_power.py.txt

stast1 commented 2 months ago

ts0601_din_power.zip Corrected version

agomezfabian commented 1 week ago

Correcting @stast1 code, dividing ZEMISMART_VOLTAGE_ATTR by 10 since it was reporting the decimal point as unit.

"""Tuya Din Power Meter."""
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 zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)
from zhaquirks.tuya import TuyaManufClusterAttributes
from zhaquirks.tuya.ts0601_din_power import TuyaElectricalMeasurement, TuyaPowerMeasurement

"""Zemismart Power Meter Attributes"""
ZEMISMART_TOTAL_ENERGY_ATTR = 0x0201
ZEMISMART_TOTAL_REVERSE_ENERGY_ATTR = 0x0202
ZEMISMART_POWER_FACTOR_ATTR = 0x020F
ZEMISMART_FREQUENCY_ATTR = 0x0265
ZEMISMART_VOLTAGE_ATTR = 0x0266
ZEMISMART_CURRENT_ATTR = 0x0267
ZEMISMART_POWER_ATTR = 0x0268

class ZemismartManufCluster(TuyaManufClusterAttributes):
    """Manufacturer Specific Cluster of the Zemismart SPM01 Power Meter device."""

    attributes = {
        ZEMISMART_TOTAL_ENERGY_ATTR: ("energy", t.uint32_t, True),
        ZEMISMART_TOTAL_REVERSE_ENERGY_ATTR: ("reverse_energy", t.uint32_t, True),
        ZEMISMART_CURRENT_ATTR: ("current", t.int16s, True),
        ZEMISMART_POWER_ATTR: ("power", t.uint16_t, True),
        ZEMISMART_VOLTAGE_ATTR: ("voltage", t.uint16_t, True),
        ZEMISMART_FREQUENCY_ATTR: ("frequency", t.uint16_t, True),
        ZEMISMART_POWER_FACTOR_ATTR: ("power_factor", t.uint16_t, True),
    }

    def _update_attribute(self, attrid, value):
        super()._update_attribute(attrid, value)
        if attrid == ZEMISMART_TOTAL_ENERGY_ATTR:
            self.endpoint.smartenergy_metering.energy_deliver_reported(value * 10)
        elif attrid == ZEMISMART_TOTAL_REVERSE_ENERGY_ATTR:
            self.endpoint.smartenergy_metering.energy_receive_reported(value * 10)
        elif attrid == ZEMISMART_CURRENT_ATTR:
            self.endpoint.electrical_measurement.current_reported(value)
        elif attrid == ZEMISMART_POWER_ATTR:
            self.endpoint.electrical_measurement.power_reported(value)
        elif attrid == ZEMISMART_VOLTAGE_ATTR:
            self.endpoint.electrical_measurement.voltage_reported(value / 10)
        elif attrid == ZEMISMART_FREQUENCY_ATTR:
            self.endpoint.electrical_measurement.frequency_reported(value)
        elif attrid == ZEMISMART_POWER_FACTOR_ATTR:
            self.endpoint.electrical_measurement.power_factor_reported(value)

class TuyaZemismartPowerMeter(CustomDevice):
    """Tuya power meter device."""

    signature = {
        MODELS_INFO: [
            ("_TZE200_qhlxve78", "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],
            }
        },
    }

    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,
                    TuyaElectricalMeasurement,
                    TuyaPowerMeasurement,
                    ZemismartManufCluster,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            }
        }
    }
dpetrini commented 3 days ago

HI, the file above worked for me. However as I use in Energy Panel in HA, it was not working on it. So I used configuration from other similar device and now is working both as device and as energy meter. (similar to ("_TZE200_bcusnqt8", "TS0601") ) I used as below:

"""Tuya Din Power Meter."""
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.homeautomation import ElectricalMeasurement

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

"""Zemismart Power Meter Attributes"""
ZEMISMART_TOTAL_ENERGY_ATTR = 0x0201
ZEMISMART_TOTAL_REVERSE_ENERGY_ATTR = 0x0202
ZEMISMART_VCP_ATTR = 0x0006

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

    cluster_id = ElectricalMeasurement.cluster_id

    POWER_ID = 0x050B
    VOLTAGE_ID = 0x0505
    CURRENT_ID = 0x0508

    AC_VOLTAGE_MULTIPLIER = 0x0600
    AC_VOLTAGE_DIVISOR = 0x0601
    AC_CURRENT_MULTIPLIER = 0x0602
    AC_CURRENT_DIVISOR = 0x0603

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

    def voltage_reported(self, value):
        """Voltage reported."""
        self._update_attribute(self.VOLTAGE_ID, value)

    def power_reported(self, value):
        """Power reported."""
        self._update_attribute(self.POWER_ID, value)

    def current_reported(self, value):
        """Ampers reported."""
        self._update_attribute(self.CURRENT_ID, value)

class ZemismartElectricalMeasurement(TuyaElectricalMeasurement):
    """Custom class for total energy measurement."""

    POWER_UNIT = 0x300
    POWER_DIVISOR = 0x302
    POWER_WATT = 0x0000  # Actually this does not work. Data is interpreted as kWh.

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

class ZemismartManufCluster(TuyaManufClusterAttributes):
    """Manufacturer Specific Cluster of the Zemismart SPM01 Power Meter device."""

    attributes = {
        ZEMISMART_TOTAL_ENERGY_ATTR: ("energy", t.uint32_t, True),
        ZEMISMART_TOTAL_REVERSE_ENERGY_ATTR: ("reverse_energy", t.uint32_t, True),
        ZEMISMART_VCP_ATTR: ("vcp_raw", t.data64, True),
    }

    def _update_attribute(self, attrid, value):
        super()._update_attribute(attrid, value)
        if attrid == ZEMISMART_TOTAL_ENERGY_ATTR:
            self.endpoint.smartenergy_metering.energy_deliver_reported(value)
        elif attrid == ZEMISMART_TOTAL_REVERSE_ENERGY_ATTR:
            self.endpoint.smartenergy_metering.energy_receive_reported(value)
        elif attrid == ZEMISMART_VCP_ATTR:
            self.endpoint.electrical_measurement.voltage_reported(
                (value[7] * 256) + value[6]
            )
            self.endpoint.electrical_measurement.current_reported(
                (value[5] * 256 * 256) + (value[4] * 256) + value[3]
            )
            self.endpoint.electrical_measurement.power_reported(
                (value[2] * 256 * 256) + (value[1] * 256) + value[0]
            )

class TuyaZemismartPowerMeter(CustomDevice):
    """Tuya power meter device."""

    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=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264,
        # maximum_outgoing_transfer_size=82, 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)",
        # device_version=1
        # input_clusters=[0x0000, 0x0004, 0x0005, 0xef00]
        # output_clusters=[0x000a, 0x0019]
        MODELS_INFO: [
            ("_TZE200_qhlxve78" , "TS0601"),
        ],
        ENDPOINTS: {
            # <SimpleDescriptor endpoint=1 profile=260 device_type=51
            # device_version=1
            # input_clusters=[0, 4, 5, 61184]
            # output_clusters=[10, 25]>
            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],
            }
        },
    }

    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,
                    ZemismartManufCluster,
                    ZemismartElectricalMeasurement,
                    ZemismartPowerMeasurement,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            }
        }
    }