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
719 stars 664 forks source link

[Device Support Request] Tuya TS0601 `_TZE204_5toc8efa` thermostat #2521

Closed LeFlairGoD closed 6 months ago

LeFlairGoD commented 1 year ago

Problem description

The device can be paired, but no entities are added. See screenshot image

https://de.aliexpress.com/item/1005004352027105.html?spm=a2g0o.order_list.order_list_main.29.5b125c5favtCSs&gatewayAdapt=glo2deu#nav-review

Solution description

Full thermostat support would be good. With temperature control, the switching options and definition of the programs.

Screenshots/Video

Screenshots/Video [Paste/upload your media here]

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=4417, maximum_buffer_size=66, maximum_incoming_transfer_size=66, server_mask=10752, maximum_outgoing_transfer_size=66, 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" ] }, "242": { "profile_id": "0xa1e0", "device_type": "0x0061", "input_clusters": [], "output_clusters": [ "0x0021" ] } }, "manufacturer": "_TZE204_5toc8efa", "model": "TS0601", "class": "zigpy.device.Device" } ```

Diagnostic information

Diagnostic information ```json { "home_assistant": { "installation_type": "Home Assistant OS", "version": "2023.8.1", "dev": false, "hassio": true, "virtualenv": false, "python_version": "3.11.4", "docker": true, "arch": "x86_64", "timezone": "Europe/Berlin", "os_name": "Linux", "os_version": "6.1.39", "supervisor": "2023.08.1", "host_os": "Home Assistant OS 10.4", "docker_version": "23.0.6", "chassis": "embedded", "run_as_root": true }, "custom_components": { "watchman": { "version": "0.5.1", "requirements": [ "prettytable==3.0.0" ] }, "hacs": { "version": "1.32.1", "requirements": [ "aiogithubapi>=22.10.1" ] } }, "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.35.8", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.102", "zigpy-deconz==0.21.0", "zigpy==0.56.4", "zigpy-xbee==0.18.1", "zigpy-zigate==0.11.0", "zigpy-znp==0.11.4" ], "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": 42206, "manufacturer": "_TZE204_5toc8efa", "model": "TS0601", "name": "_TZE204_5toc8efa TS0601", "quirk_applied": false, "quirk_class": "zigpy.device.Device", "manufacturer_code": 4417, "power_source": "Mains", "lqi": 255, "rssi": -70, "last_seen": "2023-08-10T20:42:46", "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=4417, maximum_buffer_size=66, maximum_incoming_transfer_size=66, server_mask=10752, maximum_outgoing_transfer_size=66, 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" ] }, "242": { "profile_id": "0xa1e0", "device_type": "0x0061", "input_clusters": [], "output_clusters": [ "0x0021" ] } }, "manufacturer": "_TZE204_5toc8efa", "model": "TS0601" }, "active_coordinator": false, "entities": [], "neighbors": [ { "device_type": "Coordinator", "rx_on_when_idle": "On", "relationship": "Parent", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0x0000", "permit_joining": "Unknown", "depth": "0", "lqi": "97" }, { "device_type": "EndDevice", "rx_on_when_idle": "Off", "relationship": "Child", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0x8B1F", "permit_joining": "Unknown", "depth": "2", "lqi": "18" }, { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "Sibling", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0x6183", "permit_joining": "Unknown", "depth": "1", "lqi": "97" }, { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "Sibling", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0x609D", "permit_joining": "Unknown", "depth": "1", "lqi": "97" }, { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "Sibling", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0xF1BC", "permit_joining": "Unknown", "depth": "1", "lqi": "48" }, { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "Sibling", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0xA917", "permit_joining": "Unknown", "depth": "1", "lqi": "63" }, { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "Sibling", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0x1034", "permit_joining": "Unknown", "depth": "1", "lqi": "63" }, { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "Sibling", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0x2CAF", "permit_joining": "Unknown", "depth": "1", "lqi": "30" }, { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "Sibling", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0x3CBA", "permit_joining": "Unknown", "depth": "1", "lqi": "21" }, { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "Sibling", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0x6676", "permit_joining": "Unknown", "depth": "1", "lqi": "24" }, { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "Sibling", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0x41F3", "permit_joining": "Unknown", "depth": "1", "lqi": "42" }, { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "Sibling", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0x265D", "permit_joining": "Unknown", "depth": "1", "lqi": "6" }, { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "Child", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0xEFBA", "permit_joining": "Unknown", "depth": "2", "lqi": "63" }, { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "Sibling", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0xA2DA", "permit_joining": "Unknown", "depth": "2", "lqi": "97" } ], "routes": [], "endpoint_names": [ { "name": "SMART_PLUG" }, { "name": "PROXY_BASIC" } ], "user_given_name": "Heizung", "device_reg_id": "9534645be46b406fcc464bb4522601fa", "area_id": "wohnzimmer", "cluster_details": { "1": { "device_type": { "name": "SMART_PLUG", "id": 81 }, "profile_id": 260, "in_clusters": { "0x0004": { "endpoint_attribute": "groups", "attributes": {}, "unsupported_attributes": {} }, "0x0005": { "endpoint_attribute": "scenes", "attributes": {}, "unsupported_attributes": {} }, "0xef00": { "endpoint_attribute": null, "attributes": {}, "unsupported_attributes": {} }, "0x0000": { "endpoint_attribute": "basic", "attributes": { "0x0001": { "attribute_name": "app_version", "value": 74 }, "0x0004": { "attribute_name": "manufacturer", "value": "_TZE204_5toc8efa" }, "0x0005": { "attribute_name": "model", "value": "TS0601" } }, "unsupported_attributes": {} } }, "out_clusters": { "0x0019": { "endpoint_attribute": "ota", "attributes": {}, "unsupported_attributes": {} }, "0x000a": { "endpoint_attribute": "time", "attributes": {}, "unsupported_attributes": {} } } }, "242": { "device_type": { "name": "PROXY_BASIC", "id": 97 }, "profile_id": 41440, "in_clusters": {}, "out_clusters": { "0x0021": { "endpoint_attribute": "green_power", "attributes": {}, "unsupported_attributes": {} } } } } } } ```

Logs

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

Custom quirk

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

Additional information

No response

javicalle commented 1 year ago

According to Z2M it seems to be a Moes BHT series Thermostat device: https://github.com/Koenkk/zigbee-herdsman-converters/blob/4d535effcbd6fbae0317ded493e116faf30e00ed/src/devices/moes.ts#L100-L109

Try to enable the local quirk and add your device to the MoesBHT signature:

derkorte commented 1 year ago

Same issue here. I've activated the quirks in config and added the .py to quirks folder. Then deleted the device and restart HA. After repair there where still no entities... I've tried to add the model I'd to the quirks, but this also not work Screenshot_2023-08-14-21-25-05-430_io homeassistant companion android Screenshot_2023-08-14-21-30-33-413_io homeassistant companion android

javicalle commented 1 year ago

Do you have enabled the custom quirk in the zha config? Put your local ts0601_electric_heating.py file in the configured folder (not in a subfolder).

TheJulianJES commented 1 year ago

Also, do not put your custom quirks folder in custom_components.

You need to create a custom_zha_quirks folder in /config and then add this to your configuration.yaml:

zha:
  custom_quirks_path: /config/custom_zha_quirks/
LeFlairGoD commented 1 year ago

Hi, I am relatively new to HomeAssistant, is there a rough guide somewhere on what exactly I need to do?

javicalle commented 1 year ago

is there a rough guide somewhere on what exactly I need to do?

https://github.com/zigpy/zha-device-handlers/discussions/693#discussioncomment-857274

derkorte commented 1 year ago

Screenshot_2023-08-15-14-00-22-745_io homeassistant companion android Screenshot_2023-08-15-13-58-30-552_io homeassistant companion android

Like this? Still not working

javicalle commented 1 year ago

Ummm, different signature... Can you add this class at the end of your file?

class MoesBHT(TuyaThermostat):
    """Tuya thermostat for devices like the Moes BHT-002GCLZB valve and BHT-003GBLZB Electric floor heating."""

    signature = {
        #  endpoint=1 profile=260 device_type=81 device_version=1 input_clusters=[0, 4, 5, 61184],
        #  output_clusters=[10, 25]
        MODELS_INFO: [
            ("_TZE200_aoclfnxz", "TS0601"),
            ("_TZE200_2ekuz3dz", "TS0601"),
            ("_TZE200_ye5jkfsb", "TS0601"),
            ("_TZE200_u9bfwha0", "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": ["0x0021"]
           }
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.THERMOSTAT,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    MoesBHTManufCluster,
                    MoesBHTThermostat,
                    MoesBHTUserInterface,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            "242": {
                "profile_id": "0xa1e0",
                "device_type": "0x0061",
                "input_clusters": [],
                "output_clusters": ["0x0021"]
            }
        }
    }

Please, review the code format and test. If working, we can fix the code.

derkorte commented 1 year ago

Do i have to add my device to the device list? it's _TZE204_5toc8efa

like this: ("_TZE200_aoclfnxz", "TS0601"), ("_TZE200_2ekuz3dz", "TS0601"), ("_TZE200_ye5jkfsb", "TS0601"), ("_TZE200_u9bfwha0", "TS0601"), ("_TZE204_5toc8efa", "TS0601"),

derkorte commented 1 year ago

this is my entry from logs:

Logger: zhaquirks Source: components/zha/init.py:126 First occurred: 18:48:19 (1 occurrences) Last logged: 18:48:19

Loaded custom quirks. Please contribute them to https://github.com/zigpy/zha-device-handlers

->so my config and folders seems to be correct. I also bought a smoke detector from Aliexpress, wich hasnt any entitys but by adding the device id to the quirk it works fine. So it seems like theres any issue with the converter. I've changed from z2m 2 month ago, there it was possible to detect as Moes BHT-002-GCLZB with Koenkk's converter with all entities. But with my change to ZHA all is more stable an seems to be faster.

Theres still no change when i change the lines you wrote above. But here is the full quirk, mabe ive build in some error:

Sorry i dont know how to edit a raw field or how ever it's called..

"""Map from manufacturer to standard clusters for electric heating thermostats.""" import logging

from zigpy.profiles import zha 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, TuyaThermostat, TuyaThermostatCluster, TuyaUserInterfaceCluster, )

info from https://github.com/zigpy/zha-device-handlers/pull/538#issuecomment-723334124

https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/converters/fromZigbee.js#L239

and https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/converters/common.js#L113

MOESBHT_TARGET_TEMP_ATTR = 0x0210 # [0,0,0,21] target room temp (degree) MOESBHT_TEMPERATURE_ATTR = 0x0218 # [0,0,0,200] current room temp (decidegree) MOESBHT_SCHEDULE_MODE_ATTR = 0x0403 # [1] false [0] true /!\ inverted MOESBHT_MANUAL_MODE_ATTR = 0x0402 # [1] false [0] true /!\ inverted MOESBHT_ENABLED_ATTR = 0x0101 # [0] off [1] on MOESBHT_RUNNING_MODE_ATTR = 0x0424 # [1] idle [0] heating /!\ inverted MOESBHT_CHILD_LOCK_ATTR = 0x0128 # [0] unlocked [1] child-locked

_LOGGER = logging.getLogger(name)

class MoesBHTManufCluster(TuyaManufClusterAttributes): """Manufacturer Specific Cluster of some electric heating thermostats."""

attributes = {
    MOESBHT_TARGET_TEMP_ATTR: ("target_temperature", t.uint32_t, True),
    MOESBHT_TEMPERATURE_ATTR: ("temperature", t.uint32_t, True),
    MOESBHT_SCHEDULE_MODE_ATTR: ("schedule_mode", t.uint8_t, True),
    MOESBHT_MANUAL_MODE_ATTR: ("manual_mode", t.uint8_t, True),
    MOESBHT_ENABLED_ATTR: ("enabled", t.uint8_t, True),
    MOESBHT_RUNNING_MODE_ATTR: ("running_mode", t.uint8_t, True),
    MOESBHT_CHILD_LOCK_ATTR: ("child_lock", t.uint8_t, True),
}

def _update_attribute(self, attrid, value):
    super()._update_attribute(attrid, value)
    if attrid == MOESBHT_TARGET_TEMP_ATTR:
        self.endpoint.device.thermostat_bus.listener_event(
            "temperature_change",
            "occupied_heating_setpoint",
            value * 100,  # degree to centidegree
        )
    elif attrid == MOESBHT_TEMPERATURE_ATTR:
        self.endpoint.device.thermostat_bus.listener_event(
            "temperature_change",
            "local_temperature",
            value * 10,  # decidegree to centidegree
        )
    elif attrid == MOESBHT_SCHEDULE_MODE_ATTR:
        if value == 0:  # value is inverted
            self.endpoint.device.thermostat_bus.listener_event(
                "program_change", "scheduled"
            )
    elif attrid == MOESBHT_MANUAL_MODE_ATTR:
        if value == 0:  # value is inverted
            self.endpoint.device.thermostat_bus.listener_event(
                "program_change", "manual"
            )
    elif attrid == MOESBHT_ENABLED_ATTR:
        self.endpoint.device.thermostat_bus.listener_event("enabled_change", value)
    elif attrid == MOESBHT_RUNNING_MODE_ATTR:
        # value is inverted
        self.endpoint.device.thermostat_bus.listener_event(
            "state_change", 1 - value
        )
    elif attrid == MOESBHT_CHILD_LOCK_ATTR:
        self.endpoint.device.ui_bus.listener_event("child_lock_change", value)

class MoesBHTThermostat(TuyaThermostatCluster): """Thermostat cluster for some electric heating controllers."""

def map_attribute(self, attribute, value):
    """Map standardized attribute value to dict of manufacturer values."""

    if attribute == "occupied_heating_setpoint":
        # centidegree to degree
        return {MOESBHT_TARGET_TEMP_ATTR: round(value / 100)}
    if attribute == "system_mode":
        if value == self.SystemMode.Off:
            return {MOESBHT_ENABLED_ATTR: 0}
        if value == self.SystemMode.Heat:
            return {MOESBHT_ENABLED_ATTR: 1}
        self.error("Unsupported value for SystemMode")
    elif attribute == "programing_oper_mode":
        # values are inverted
        if value == self.ProgrammingOperationMode.Simple:
            return {MOESBHT_MANUAL_MODE_ATTR: 0, MOESBHT_SCHEDULE_MODE_ATTR: 1}
        if value == self.ProgrammingOperationMode.Schedule_programming_mode:
            return {MOESBHT_MANUAL_MODE_ATTR: 1, MOESBHT_SCHEDULE_MODE_ATTR: 0}
        self.error("Unsupported value for ProgrammingOperationMode")

    return super().map_attribute(attribute, value)

def program_change(self, mode):
    """Programming mode change."""
    if mode == "manual":
        value = self.ProgrammingOperationMode.Simple
    else:
        value = self.ProgrammingOperationMode.Schedule_programming_mode

    self._update_attribute(
        self.attributes_by_name["programing_oper_mode"].id, value
    )

def enabled_change(self, value):
    """System mode change."""
    if value == 0:
        mode = self.SystemMode.Off
    else:
        mode = self.SystemMode.Heat
    self._update_attribute(self.attributes_by_name["system_mode"].id, mode)

class MoesBHTUserInterface(TuyaUserInterfaceCluster): """HVAC User interface cluster for tuya electric heating thermostats."""

_CHILD_LOCK_ATTR = MOESBHT_CHILD_LOCK_ATTR

class MoesBHT(TuyaThermostat): """Tuya thermostat for devices like the Moes BHT-002GCLZB valve and BHT-003GBLZB Electric floor heating."""

signature = {
    #  endpoint=1 profile=260 device_type=81 device_version=1 input_clusters=[0, 4, 5, 61184],
    #  output_clusters=[10, 25]
    MODELS_INFO: [
        ("_TZE200_aoclfnxz", "TS0601"),
        ("_TZE200_2ekuz3dz", "TS0601"),
        ("_TZE200_ye5jkfsb", "TS0601"),
        ("_TZE200_u9bfwha0", "TS0601"),
        ("_TZE204_5toc8efa", "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": ["0x0021"]
       }
    },
}

replacement = {
    ENDPOINTS: {
        1: {
            PROFILE_ID: zha.PROFILE_ID,
            DEVICE_TYPE: zha.DeviceType.THERMOSTAT,
            INPUT_CLUSTERS: [
                Basic.cluster_id,
                Groups.cluster_id,
                Scenes.cluster_id,
                MoesBHTManufCluster,
                MoesBHTThermostat,
                MoesBHTUserInterface,
            ],
            OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
        },
        "242": {
            "profile_id": "0xa1e0",
            "device_type": "0x0061",
            "input_clusters": [],
            "output_clusters": ["0x0021"]
        }
    }
}
derkorte commented 1 year ago

Anyone got this work?

bsfaxi commented 1 year ago

Trying the same here: [https://github.com/zigpy/zha-device-handlers/issues/2433]

LeFlairGoD commented 1 year ago

I don't get it, I have the code in _/config/custom_zhaquirks/ In my configuration.yaml added the following lines

zha:
custom_quirks_path: /config/custom_zha_quirks/

Removed the device and restarted HA. Nothing has changed.

My device is called: TS0601 from _TZE204_5toc8efa

And in the _/config/custom_zhaquirks/ folder is a file (thermostat_livingroom.py) with the following code:

"""Map from manufacturer to standard clusters for electric heating thermostats."""
import logging

from zigpy.profiles import zha
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,
    TuyaThermostat,
    TuyaThermostatCluster,
    TuyaUserInterfaceCluster,
)

# info from https://github.com/zigpy/zha-device-handlers/pull/538#issuecomment-723334124
# https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/converters/fromZigbee.js#L239
# and https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/converters/common.js#L113
MOESBHT_TARGET_TEMP_ATTR = 0x0210  # [0,0,0,21] target room temp (degree)
MOESBHT_TEMPERATURE_ATTR = 0x0218  # [0,0,0,200] current room temp (decidegree)
MOESBHT_SCHEDULE_MODE_ATTR = 0x0403  # [1] false [0] true   /!\ inverted
MOESBHT_MANUAL_MODE_ATTR = 0x0402  # [1] false [0] true /!\ inverted
MOESBHT_ENABLED_ATTR = 0x0101  # [0] off [1] on
MOESBHT_RUNNING_MODE_ATTR = 0x0424  # [1] idle [0] heating /!\ inverted
MOESBHT_CHILD_LOCK_ATTR = 0x0128  # [0] unlocked [1] child-locked

_LOGGER = logging.getLogger(__name__)

class MoesBHTManufCluster(TuyaManufClusterAttributes):
    """Manufacturer Specific Cluster of some electric heating thermostats."""

    attributes = {
        MOESBHT_TARGET_TEMP_ATTR: ("target_temperature", t.uint32_t, True),
        MOESBHT_TEMPERATURE_ATTR: ("temperature", t.uint32_t, True),
        MOESBHT_SCHEDULE_MODE_ATTR: ("schedule_mode", t.uint8_t, True),
        MOESBHT_MANUAL_MODE_ATTR: ("manual_mode", t.uint8_t, True),
        MOESBHT_ENABLED_ATTR: ("enabled", t.uint8_t, True),
        MOESBHT_RUNNING_MODE_ATTR: ("running_mode", t.uint8_t, True),
        MOESBHT_CHILD_LOCK_ATTR: ("child_lock", t.uint8_t, True),
    }

    def _update_attribute(self, attrid, value):
        super()._update_attribute(attrid, value)
        if attrid == MOESBHT_TARGET_TEMP_ATTR:
            self.endpoint.device.thermostat_bus.listener_event(
                "temperature_change",
                "occupied_heating_setpoint",
                value * 100,  # degree to centidegree
            )
        elif attrid == MOESBHT_TEMPERATURE_ATTR:
            self.endpoint.device.thermostat_bus.listener_event(
                "temperature_change",
                "local_temperature",
                value * 10,  # decidegree to centidegree
            )
        elif attrid == MOESBHT_SCHEDULE_MODE_ATTR:
            if value == 0:  # value is inverted
                self.endpoint.device.thermostat_bus.listener_event(
                    "program_change", "scheduled"
                )
        elif attrid == MOESBHT_MANUAL_MODE_ATTR:
            if value == 0:  # value is inverted
                self.endpoint.device.thermostat_bus.listener_event(
                    "program_change", "manual"
                )
        elif attrid == MOESBHT_ENABLED_ATTR:
            self.endpoint.device.thermostat_bus.listener_event("enabled_change", value)
        elif attrid == MOESBHT_RUNNING_MODE_ATTR:
            # value is inverted
            self.endpoint.device.thermostat_bus.listener_event(
                "state_change", 1 - value
            )
        elif attrid == MOESBHT_CHILD_LOCK_ATTR:
            self.endpoint.device.ui_bus.listener_event("child_lock_change", value)

class MoesBHTThermostat(TuyaThermostatCluster):
    """Thermostat cluster for some electric heating controllers."""

    def map_attribute(self, attribute, value):
        """Map standardized attribute value to dict of manufacturer values."""

        if attribute == "occupied_heating_setpoint":
            # centidegree to degree
            return {MOESBHT_TARGET_TEMP_ATTR: round(value / 100)}
        if attribute == "system_mode":
            if value == self.SystemMode.Off:
                return {MOESBHT_ENABLED_ATTR: 0}
            if value == self.SystemMode.Heat:
                return {MOESBHT_ENABLED_ATTR: 1}
            self.error("Unsupported value for SystemMode")
        elif attribute == "programing_oper_mode":
            # values are inverted
            if value == self.ProgrammingOperationMode.Simple:
                return {MOESBHT_MANUAL_MODE_ATTR: 0, MOESBHT_SCHEDULE_MODE_ATTR: 1}
            if value == self.ProgrammingOperationMode.Schedule_programming_mode:
                return {MOESBHT_MANUAL_MODE_ATTR: 1, MOESBHT_SCHEDULE_MODE_ATTR: 0}
            self.error("Unsupported value for ProgrammingOperationMode")

        return super().map_attribute(attribute, value)

    def program_change(self, mode):
        """Programming mode change."""
        if mode == "manual":
            value = self.ProgrammingOperationMode.Simple
        else:
            value = self.ProgrammingOperationMode.Schedule_programming_mode

        self._update_attribute(
            self.attributes_by_name["programing_oper_mode"].id, value
        )

    def enabled_change(self, value):
        """System mode change."""
        if value == 0:
            mode = self.SystemMode.Off
        else:
            mode = self.SystemMode.Heat
        self._update_attribute(self.attributes_by_name["system_mode"].id, mode)

class MoesBHTUserInterface(TuyaUserInterfaceCluster):
    """HVAC User interface cluster for tuya electric heating thermostats."""

    _CHILD_LOCK_ATTR = MOESBHT_CHILD_LOCK_ATTR

class MoesBHT(TuyaThermostat):
    """Tuya thermostat for devices like the Moes BHT-002GCLZB valve and BHT-003GBLZB Electric floor heating."""

    signature = {
        #  endpoint=1 profile=260 device_type=81 device_version=1 input_clusters=[0, 4, 5, 61184],
        #  output_clusters=[10, 25]
        MODELS_INFO: [
            ("_TZE200_aoclfnxz", "TS0601"),
            ("_TZE200_2ekuz3dz", "TS0601"),
            ("_TZE200_ye5jkfsb", "TS0601"),
            ("_TZE200_u9bfwha0", "TS0601"),
            ("_TZE204_5toc8efa", "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.THERMOSTAT,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    MoesBHTManufCluster,
                    MoesBHTThermostat,
                    MoesBHTUserInterface,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            }
        }
    }

Have I forgotten anything? This is the first time I've had to deal with Quirks.

After reconnecting, only the following was in the log file:

> Logger: zigpy.zcl
> Source: runner.py:179
> First occurred: 20:16:11 (50 occurrences)
> Last logged: 20:16:26
> 
> [0xCFF9:1:0xef00] Unknown cluster command 2 b'\x00\xdel\x00\x00\x11\x03\x06\x00\x00\xc8\x0b\x1e\x00\xc8\r\x1e\x00\xc8\x11\x1e\x00\xc8'
> [0xCFF9:1:0xef00] Unknown cluster command 2 b'\x00\xdfk\x00\x00\x11\x04\x06\x00\x00\xc8\x0b\x1e\x00\xc8\r\x1e\x00\xc8\x11\x1e\x00\xc8'
> [0xCFF9:1:0xef00] Unknown cluster command 2 b'\x00\xe0j\x00\x00\x11\x05\x06\x00\x00\xc8\x0b\x1e\x00\xc8\r\x1e\x00\xc8\x11\x1e\x00\xc8'
> [0xCFF9:1:0xef00] Unknown cluster command 2 b'\x00\xe1i\x00\x00\x11\x06\x06\x00\x00\xc8\x0b\x1e\x00\xc8\r\x1e\x00\xc8\x11\x1e\x00\xc8'
> [0xCFF9:1:0xef00] Unknown cluster command 2 b'\x00\xe2e\x00\x00\x11\x07\x06\x00\x00\xc8\x0b\x1e\x00\xc8\r\x1e\x00\xc8\x11\x1e\x00\xc8'
> 
bsfaxi commented 1 year ago

What about your HA log file? Any debug info regarding your ".py" file?

LeFlairGoD commented 1 year ago

Was ist mit Ihrer HA-Protokolldatei? Irgendwelche Debug-Informationen zu Ihrer „.py“-Datei?

The only thing in the log after reconnecting are these two entries

Logger: frontend.js.latest.202308021
Source: components/system_log/__init__.py:270
First occurred: 21:57:20 (2 occurrences)
Last logged: 21:57:20

http://homeassistant.local:8123/frontend_latest/62575-6mNV-72kqCE.js:4:8121 Uncaught TypeError: Cannot read properties of undefined (reading 'get')
Logger: zigpy.zcl
Source: runner.py:179
First occurred: 21:57:26 (51 occurrences)
Last logged: 21:57:42

[0xEAF3:1:0xef00] Unknown cluster command 2 b'\x00\xfbl\x00\x00\x11\x03\x06\x00\x00\xc8\x0b\x1e\x00\xc8\r\x1e\x00\xc8\x11\x1e\x00\xc8'
[0xEAF3:1:0xef00] Unknown cluster command 2 b'\x00\xfck\x00\x00\x11\x04\x06\x00\x00\xc8\x0b\x1e\x00\xc8\r\x1e\x00\xc8\x11\x1e\x00\xc8'
[0xEAF3:1:0xef00] Unknown cluster command 2 b'\x00\xfdj\x00\x00\x11\x05\x06\x00\x00\xc8\x0b\x1e\x00\xc8\r\x1e\x00\xc8\x11\x1e\x00\xc8'
[0xEAF3:1:0xef00] Unknown cluster command 2 b'\x00\xfei\x00\x00\x11\x06\x06\x00\x00\xc8\x0b\x1e\x00\xc8\r\x1e\x00\xc8\x11\x1e\x00\xc8'
[0xEAF3:1:0xef00] Unknown cluster command 2 b'\x00\xffe\x00\x00\x11\x07\x06\x00\x00\xc8\x0b\x1e\x00\xc8\r\x1e\x00\xc8\x11\x1e\x00\xc8'

and this in HA.log:

Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/components/zha/core/cluster_handlers/__init__.py", line 64, in wrapper
    return await RETRYABLE_REQUEST_DECORATOR(func)(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/zigpy/util.py", line 132, in retry
    return await func()
           ^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/zigpy/quirks/__init__.py", line 199, in command
    return await self.request(
           ^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/zigpy/zcl/__init__.py", line 375, in request
    return await self._endpoint.request(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/zigpy/endpoint.py", line 253, in request
    return await self.device.request(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/zigpy/device.py", line 309, in request
    async with asyncio_timeout(timeout):
  File "/usr/local/lib/python3.11/asyncio/timeouts.py", line 111, in __aexit__
    raise TimeoutError from exc_val
TimeoutError

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/components/websocket_api/commands.py", line 226, in handle_call_service
    await hass.services.async_call(
  File "/usr/src/homeassistant/homeassistant/core.py", line 1974, in async_call
    response_data = await coro
                    ^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/core.py", line 2011, in _execute_service
    return await target(service_call)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/entity_component.py", line 235, in handle_service
    return await service.entity_service_call(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/service.py", line 870, in entity_service_call
    response_data = await _handle_entity_call(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/helpers/service.py", line 942, in _handle_entity_call
    result = await task
             ^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/light/__init__.py", line 591, in async_handle_light_off_service
    await light.async_turn_off(**filter_turn_off_params(light, params))
  File "/usr/src/homeassistant/homeassistant/components/zha/light.py", line 471, in async_turn_off
    result = await self._on_off_cluster_handler.off()
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/homeassistant/homeassistant/components/zha/core/cluster_handlers/__init__.py", line 66, in wrapper
    raise HomeAssistantError(

I do not know where exactly to look. Searched the LogFiles for my .py, but there were no entries with the exact name of the file

bsfaxi commented 1 year ago

Please activate the debug logs to investigate. Your quirk is not yet taken into account...

logger:
  default: info
  logs:
    zigpy: debug
    zigpy.zcl: debug
    custom_zha_quirks: debug
    zhaquirks: debug
    homeassistant.components.zha: debug
LeFlairGoD commented 1 year ago

Bitte aktivieren Sie die Debug-Protokolle zur Untersuchung. Deine Eigenart ist noch nicht berücksichtigt...

logger:
  default: info
  logs:
    zigpy: debug
    zigpy.zcl: debug
    custom_zha_quirks: debug
    zhaquirks: debug
    homeassistant.components.zha: debug

I did that, but there is still nothing in the HA.log files. image image have searched for .py in the logs. But it finds only that again: image

derkorte commented 1 year ago

So I think there's an issue with the outputs of the devices. I've ordered an other thermostat from AliExpress for about 30€ witch is named moes and this works out of the box. The device from bsfaxi just looks like a moes thermostat. My _TZE204_5toc8efa is manufactured by bseed, so (I believe) there where other clusters. I don't know how to read out this device. Can someone explain how to, or give a link to get this done?

github-actions[bot] commented 6 months ago

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.

achikhv commented 6 months ago

Got it working with slightly modified quirk by @bsfaxi

I'll file PR in couple of days.

antonGritsenko commented 4 months ago

@achikhv did you already pushed the PR?

achikhv commented 4 months ago

Sorry, I didn't manage to get internet working on my RPI, so it's not easy to download quirk right now. I remember about this issue and will do my best to file PR as soon as possible.

Anyway, the quirk is taken here and the only difference is in multipliers of target temperature. As far as I remember target temperature is measured in 1/10th, not 1/100th degrees. So I had to change this multipliers. The rest of the quirk is intact.

antonGritsenko commented 4 months ago

@achikhv ok, thanks, yes, I'm using it now, but it a bit buggy: button doesn't work (and it shouldn't I think). Also, why not to move current temperature as readable value for HA?