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
734 stars 673 forks source link

[Device Support Request] MOES Zigbee 006 Thermostat Model TS0601 manufacturer_TZE204_aoclfnxz #2433

Open bsfaxi opened 1 year ago

bsfaxi commented 1 year ago

Problem description

I bought this Moes Thermostat to control an electric underfloor heating system : https://fr.aliexpress.com/item/1005004996552864.html The device deosn't show ANY entity ! The existing quirk is only for TZE200 that has one Endpoint only. This one has 2.

Solution description

Is it possible to adapt the existing tuya quirk "ts0601_electric_heating.py" to include this model ?

Screenshots/Video

Screenshots/Video ![2023-06-15 22 37 18](https://github.com/zigpy/zha-device-handlers/assets/23717629/cffb5710-a2e5-4ec6-b817-471367060351) ![2023-06-15 22 37 43](https://github.com/zigpy/zha-device-handlers/assets/23717629/59e91bcb-75fb-4c00-9c8b-4fc0ca7e8f3f) ![2023-06-15 22 38 00](https://github.com/zigpy/zha-device-handlers/assets/23717629/5ba13ec8-61e4-492b-b961-726f6202c40f) ![2023-06-15 22 38 11-1](https://github.com/zigpy/zha-device-handlers/assets/23717629/31616887-0871-4f1e-8a04-c643b4af91a1)

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_aoclfnxz", "model": "TS0601", "class": "zigpy.device.Device" } ```

Diagnostic information

Diagnostic information ```json { "home_assistant": { "installation_type": "Home Assistant OS", "version": "2023.6.2", "dev": false, "hassio": true, "virtualenv": false, "python_version": "3.11.4", "docker": true, "arch": "aarch64", "timezone": "Europe/Paris", "os_name": "Linux", "os_version": "6.1.21-v8", "supervisor": "2023.06.2", "host_os": "Home Assistant OS 10.2", "docker_version": "23.0.6", "chassis": "embedded", "run_as_root": true }, "custom_components": {}, "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.5", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.100", "zigpy-deconz==0.21.0", "zigpy==0.55.0", "zigpy-xbee==0.18.0", "zigpy-zigate==0.11.0", "zigpy-znp==0.11.1" ], "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": 30655, "manufacturer": "_TZE204_aoclfnxz", "model": "TS0601", "name": "_TZE204_aoclfnxz TS0601", "quirk_applied": false, "quirk_class": "zigpy.device.Device", "manufacturer_code": 4417, "power_source": "Mains", "lqi": 255, "rssi": -77, "last_seen": "2023-06-15T22:43:39", "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_aoclfnxz", "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": "33" }, { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "Child", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0xAF5B", "permit_joining": "Unknown", "depth": "2", "lqi": "33" }, { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "Child", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0x6C17", "permit_joining": "Unknown", "depth": "2", "lqi": "51" }, { "device_type": "Router", "rx_on_when_idle": "On", "relationship": "Sibling", "extended_pan_id": "**REDACTED**", "ieee": "**REDACTED**", "nwk": "0x186B", "permit_joining": "Unknown", "depth": "3", "lqi": "51" } ], "routes": [], "endpoint_names": [ { "name": "SMART_PLUG" }, { "name": "unknown 97 device_type of 0xa1e0 profile id" } ], "user_given_name": "Thermostat Bureau", "device_reg_id": "8574b9d5efdb9c12867f38920eb40589", "area_id": "bureau", "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_aoclfnxz" }, "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": "unknown", "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 """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, GreenPowerProxy 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 = { MODELS_INFO: [ ("_TZE200_aoclfnxz", "TS0601"), ("_TZE200_2ekuz3dz", "TS0601"), ("_TZE200_ye5jkfsb", "TS0601"), ("_TZE200_u9bfwha0", "TS0601"), ("_TZE204_aoclfnxz", "TS0601"), ], ENDPOINTS: { # endpoint=1 profile=260 device_type=81 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], }, 242:{ PROFILE_ID: 41440, DEVICE_TYPE: 97, INPUT_CLUSTERS: [], OUTPUT_CLUSTERS: [GreenPowerProxy.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], } } } ```

Additional information

No response

bsfaxi commented 1 year ago

This model is now supported by zigbee2mqtt like stated here: https://www.zigbee2mqtt.io/devices/BHT-006GBZB.html. Anyone could help here please?

Yop1403 commented 1 year ago

I support this request - I have exactly the same device (and the same problem)

@bsfaxi if you change the signature in the Quirk of the TZE200 to: MODELS_INFO: [("_TZE204_aoclfnxz", "TS0601")],` --> are you getting any sensors at all or also nothing?

bsfaxi commented 1 year ago

Hi @Yop1403, The Thermostat entity appears, but unfortunately I can't control it ! image image

I have the following errors when I try to change the temperature :

2023-08-02 22:05:05.842 WARNING (MainThread) [homeassistant.helpers.entity] Updating state for sensor.thermostat_bureau_hvac_action (<class 'homeassistant.components.zha.sensor.ThermostatHVACAction'>) took 0.418 seconds. Please create a bug report at https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+zha%22
2023-08-04 00:15:49.871 WARNING (MainThread) [homeassistant.helpers.entity] Updating state for climate.thermostat_bureau (<class 'homeassistant.components.zha.climate.Thermostat'>) took 0.466 seconds. Please create a bug report at https://github.com/home-assistant/core/issues?q=is%3Aopen+is%3Aissue+label%3A%22integration%3A+zha%22
Yop1403 commented 1 year ago

Very interesting. You at least get further than I do... My HA instance doesn't want to see the Quirk:

Bildschirmfoto 2023-08-07 um 19 52 03

Would you mind sharing what this entry looks like in your Quirk file? signature = { MODELS_INFO: [("_TZE204_aoclfnxz", "TS0601")],

bsfaxi commented 1 year ago

The custom quirk, that I'm using, is in the description of this request in "Custom quirk" section. :)

Yop1403 commented 1 year ago

The custom quirk, that I'm using, is in the description of this request in "Custom quirk" section. :)

Ah sorry overlooked that, thanks. Let's see if anyone here has some mercy with us and helps us out getting this Moes Thermostat integrated into HA...

lotr commented 1 year ago

https://github.com/Koenkk/zigbee2mqtt/issues/18097 z2m should have it already

bsfaxi commented 1 year ago

Indeed, it's already supported there: https://www.zigbee2mqtt.io/devices/BHT-006GBZB.html. I need it in ZHA. ;)

alexlinno commented 1 year ago

Hi, the quirk in the the first comment did help in a sense that I see the temp and target temp, And the state, And I see the more useful clusters. I will be modifying the quirk to be able to control the device. Screenshot 2023-08-16 165543 Screenshot 2023-08-16 170106

douglascrc-git commented 1 year ago

I support this request too! I'm trying to make this thermostat work on ZHA, but no succes has been achieved so far ;C

bsfaxi commented 1 year ago

For those who speak Zigbee, I tried to generate logs during: 1- The pairing process TS0601_pairing_log.txt

2- The following scenario:

Anyone can help please?

bsfaxi commented 1 year ago

Here the 3 recurring commands that I see:

Command 2:

2023-08-20 19:30:06.305 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x9E07), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=None, profile_id=260, cluster_id=61184, data=Serialized[b'\t\xbf\x02\r\xf8\x18\x02\x00\x04\x00\x00\x00\xff'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=207, rssi=-68)
2023-08-20 19:30:06.305 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] Received ZCL frame: b'\t\xbf\x02\r\xf8\x18\x02\x00\x04\x00\x00\x00\xff'
2023-08-20 19:30:06.306 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False), tsn=191, command_id=2, *direction=<Direction.Client_to_Server: 1>)
2023-08-20 19:30:06.306 WARNING (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] Unknown cluster command 2 b'\r\xf8\x18\x02\x00\x04\x00\x00\x00\xff'
2023-08-20 19:30:06.308 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] Received command 0x02 (TSN 191): b'\r\xf8\x18\x02\x00\x04\x00\x00\x00\xff'
2023-08-20 19:30:06.309 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] No explicit handler for cluster command 0x02: b'\r\xf8\x18\x02\x00\x04\x00\x00\x00\xff'

2023-08-20 20:00:27.375 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x9E07), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=None, profile_id=260, cluster_id=61184, data=Serialized[b'\t\xba\x02\x16\xc1e\x00\x00$\x06\x02(\x0c\x1e*\r\x1f,\x12\x1f1\x07\x021\x0e\x01/\x0e\x1f-\x12\x1f*\x06\x00&\x0c\x1e(\x0e\x1e*\x12\x1e('], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=255, rssi=-58)
2023-08-20 20:00:27.375 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] Received ZCL frame: b'\t\xba\x02\x16\xc1e\x00\x00$\x06\x02(\x0c\x1e*\r\x1f,\x12\x1f1\x07\x021\x0e\x01/\x0e\x1f-\x12\x1f*\x06\x00&\x0c\x1e(\x0e\x1e*\x12\x1e('
2023-08-20 20:00:27.376 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False), tsn=186, command_id=2, *direction=<Direction.Client_to_Server: 1>)
2023-08-20 20:00:27.376 WARNING (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] Unknown cluster command 2 b'\x16\xc1e\x00\x00$\x06\x02(\x0c\x1e*\r\x1f,\x12\x1f1\x07\x021\x0e\x01/\x0e\x1f-\x12\x1f*\x06\x00&\x0c\x1e(\x0e\x1e*\x12\x1e('
2023-08-20 20:00:27.379 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] Received command 0x02 (TSN 186): b'\x16\xc1e\x00\x00$\x06\x02(\x0c\x1e*\r\x1f,\x12\x1f1\x07\x021\x0e\x01/\x0e\x1f-\x12\x1f*\x06\x00&\x0c\x1e(\x0e\x1e*\x12\x1e('
2023-08-20 20:00:27.379 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] No explicit handler for cluster command 0x02: b'\x16\xc1e\x00\x00$\x06\x02(\x0c\x1e*\r\x1f,\x12\x1f1\x07\x021\x0e\x01/\x0e\x1f-\x12\x1f*\x06\x00&\x0c\x1e(\x0e\x1e*\x12\x1e('

Command 10:

2023-08-20 19:36:38.523 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x9E07), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=None, profile_id=260, cluster_id=0, data=Serialized[b"\x08\x00\n\xdf\xffB<\xbb'u,i\xbb'u,i\xbc'u,i\xbc'u,i\xb7'u,i\xb8'u,i\xb8'u,i\xb9'u,i\xb9'u,i\xba'u,i\xba'u,i\xba'u,i"], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=239, rssi=-64)
2023-08-20 19:36:38.524 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0x0000] Received ZCL frame: b"\x08\x00\n\xdf\xffB<\xbb'u,i\xbb'u,i\xbc'u,i\xbc'u,i\xb7'u,i\xb8'u,i\xb8'u,i\xb9'u,i\xb9'u,i\xba'u,i\xba'u,i\xba'u,i"
2023-08-20 19:36:38.524 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.GLOBAL_COMMAND: 0>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=0, command_id=10, *direction=<Direction.Client_to_Server: 1>)
2023-08-20 19:36:38.525 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0x0000] Decoded ZCL frame: Basic:Report_Attributes(attribute_reports=[Attribute(attrid=0xFFDF, value=TypeValue(type=CharacterString, value="�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i"))])
2023-08-20 19:36:38.525 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0x0000] Received command 0x0A (TSN 0): Report_Attributes(attribute_reports=[Attribute(attrid=0xFFDF, value=TypeValue(type=CharacterString, value="�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i"))])
2023-08-20 19:36:38.526 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0x0000] Attribute report received: 0xFFDF="�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i"
2023-08-20 19:36:38.527 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0x0000] Sending reply header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.GLOBAL_COMMAND: 0>, is_manufacturer_specific=False, direction=<Direction.Client_to_Server: 1>, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=0, command_id=<GeneralCommand.Default_Response: 11>, *direction=<Direction.Client_to_Server: 1>)
2023-08-20 19:36:38.528 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0x0000] Sending reply: Default_Response(command_id=10, status=<Status.SUCCESS: 0>)

2023-08-20 19:36:45.970 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x9E07), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=None, profile_id=260, cluster_id=0, data=Serialized[b"\x08\x0c\n\xdf\xffB<\xc0'u,i\xc1'u,i\xc1'u,i\xc1'u,i\xc2'u,i\xc2'u,i\xc3'u,i\xc3'u,i\xc3'u,i\xc4'u,i\xc4'u,i\xc0'u,i"], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=239, rssi=-64)
2023-08-20 19:36:45.971 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0x0000] Received ZCL frame: b"\x08\x0c\n\xdf\xffB<\xc0'u,i\xc1'u,i\xc1'u,i\xc1'u,i\xc2'u,i\xc2'u,i\xc3'u,i\xc3'u,i\xc3'u,i\xc4'u,i\xc4'u,i\xc0'u,i"
2023-08-20 19:36:45.971 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0x0000] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.GLOBAL_COMMAND: 0>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=12, command_id=10, *direction=<Direction.Client_to_Server: 1>)
2023-08-20 19:36:45.972 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0x0000] Decoded ZCL frame: Basic:Report_Attributes(attribute_reports=[Attribute(attrid=0xFFDF, value=TypeValue(type=CharacterString, value="�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i"))])
2023-08-20 19:36:45.972 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0x0000] Received command 0x0A (TSN 12): Report_Attributes(attribute_reports=[Attribute(attrid=0xFFDF, value=TypeValue(type=CharacterString, value="�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i"))])
2023-08-20 19:36:45.972 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0x0000] Attribute report received: 0xFFDF="�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i�'u,i"
2023-08-20 19:36:45.973 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0x0000] Sending reply header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.GLOBAL_COMMAND: 0>, is_manufacturer_specific=False, direction=<Direction.Client_to_Server: 1>, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=12, command_id=<GeneralCommand.Default_Response: 11>, *direction=<Direction.Client_to_Server: 1>)
2023-08-20 19:36:45.974 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0x0000] Sending reply: Default_Response(command_id=10, status=<Status.SUCCESS: 0>)

Command 36:

2023-08-19 11:35:31.505 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x9E07), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=None, profile_id=260, cluster_id=61184, data=Serialized[b'\t\xe8$\xb3\t'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=95, rssi=-82)
2023-08-19 11:35:31.505 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] Received ZCL frame: b'\t\xe8$\xb3\t'
2023-08-19 11:35:31.506 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False), tsn=232, command_id=36, *direction=<Direction.Client_to_Server: 1>)
2023-08-19 11:35:31.507 WARNING (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] Unknown cluster command 36 b'\xb3\t'
2023-08-19 11:35:31.511 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] Received command 0x24 (TSN 232): b'\xb3\t'
2023-08-19 11:35:31.511 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] No explicit handler for cluster command 0x24: b'\xb3\t'

2023-08-20 19:30:05.902 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x9E07), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=None, profile_id=260, cluster_id=61184, data=Serialized[b'\t\xbe$\xf7\r'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=239, rssi=-64)
2023-08-20 19:30:05.902 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] Received ZCL frame: b'\t\xbe$\xf7\r'
2023-08-20 19:30:05.903 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False), tsn=190, command_id=36, *direction=<Direction.Client_to_Server: 1>)
2023-08-20 19:30:05.903 WARNING (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] Unknown cluster command 36 b'\xf7\r'
2023-08-20 19:30:05.905 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] Received command 0x24 (TSN 190): b'\xf7\r'
2023-08-20 19:30:05.905 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] No explicit handler for cluster command 0x24: b'\xf7\r'

2023-08-20 20:01:34.602 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x9E07), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=None, profile_id=260, cluster_id=61184, data=Serialized[b'\t\\$#\x17'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=215, rssi=-67)
2023-08-20 20:01:34.602 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] Received ZCL frame: b'\t\\$#\x17'
2023-08-20 20:01:34.603 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False), tsn=92, command_id=36, *direction=<Direction.Client_to_Server: 1>)
2023-08-20 20:01:34.603 WARNING (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] Unknown cluster command 36 b'#\x17'
2023-08-20 20:01:34.605 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] Received command 0x24 (TSN 92): b'#\x17'
2023-08-20 20:01:34.605 DEBUG (MainThread) [zigpy.zcl] [0x9E07:1:0xef00] No explicit handler for cluster command 0x24: b'#\x17'
mlouaze commented 1 year ago

Hi, I just got the same thermostat and I also struggle to make it work. The custom quirks also allowed me to see the thermostat, but I also can't control it. I am a beginner in HA and ZHA, but I will also try to investigate.

bsfaxi commented 1 year ago

When I applied the custom quirk provided in the description, I notice the following:

FOR COMMAND 0x02: Report room temperature 24° 2023-08-21 10:05:58.369 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 214): set_data_response(param=Command(status=9, tsn=122, command_id=536, function=0, data=[4, 0, 0, 0, 240])) Report room temperature 24.5° 2023-08-21 10:06:57.385 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 135): set_data_response(param=Command(status=9, tsn=180, command_id=536, function=0, data=[4, 0, 0, 0, 245])) You notice the command_id "536".

When nothing happens, I see a recurring message like the following: 2023-08-21 10:05:55.953 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 212): set_data_response(param=Command(status=9, tsn=122, command_id=1060, function=0, data=[1, 1])) You notice the command_id "1060".

When, I set the target temperature on the device to 20°, I see the following: 2023-08-21 10:07:00.185 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 162): set_data_response(param=Command(status=9, tsn=204, command_id=528, function=0, data=[4, 0, 0, 0, 20])) Even though I set the target temperature to 20.5°, the data still contains 20°: 2023-08-21 10:23:44.910 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 156): set_data_response(param=Command(status=13, tsn=115, command_id=528, function=0, data=[4, 0, 0, 0, 20])) Setting the target temperature to 21° shows 21 in the data: 2023-08-21 10:29:30.481 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 132): set_data_response(param=Command(status=14, tsn=67, command_id=528, function=0, data=[4, 0, 0, 0, 21])) You notice the command_id "528".

I see also a recurring command_id "536" every 1mn. But, I don't know to what it corresponds:

2023-08-20 21:05:28.659 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Decoded ZCL frame: MoesBHTManufCluster:set_data_response(param=Command(status=91, tsn=38, command_id=536, function=0, data=[4, 0, 0, 0, 225]))
2023-08-20 21:05:28.659 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 123): set_data_response(param=Command(status=91, tsn=38, command_id=536, function=0, data=[4, 0, 0, 0, 225]))
2023-08-20 21:06:28.685 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Decoded ZCL frame: MoesBHTManufCluster:set_data_response(param=Command(status=91, tsn=112, command_id=536, function=0, data=[4, 0, 0, 0, 225]))
2023-08-20 21:06:28.685 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 120): set_data_response(param=Command(status=91, tsn=112, command_id=536, function=0, data=[4, 0, 0, 0, 225]))
2023-08-20 21:07:28.720 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Decoded ZCL frame: MoesBHTManufCluster:set_data_response(param=Command(status=91, tsn=146, command_id=536, function=0, data=[4, 0, 0, 0, 230]))
2023-08-20 21:07:28.720 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 67): set_data_response(param=Command(status=91, tsn=146, command_id=536, function=0, data=[4, 0, 0, 0, 230]))
2023-08-20 21:08:28.758 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Decoded ZCL frame: MoesBHTManufCluster:set_data_response(param=Command(status=91, tsn=188, command_id=536, function=0, data=[4, 0, 0, 0, 230]))
2023-08-20 21:08:28.759 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 13): set_data_response(param=Command(status=91, tsn=188, command_id=536, function=0, data=[4, 0, 0, 0, 230]))
2023-08-20 21:09:28.791 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Decoded ZCL frame: MoesBHTManufCluster:set_data_response(param=Command(status=91, tsn=220, command_id=536, function=0, data=[4, 0, 0, 0, 230]))
2023-08-20 21:09:28.791 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 216): set_data_response(param=Command(status=91, tsn=220, command_id=536, function=0, data=[4, 0, 0, 0, 230]))
2023-08-20 21:10:28.823 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Decoded ZCL frame: MoesBHTManufCluster:set_data_response(param=Command(status=92, tsn=17, command_id=536, function=0, data=[4, 0, 0, 0, 235]))
2023-08-20 21:10:28.824 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 162): set_data_response(param=Command(status=92, tsn=17, command_id=536, function=0, data=[4, 0, 0, 0, 235]))
2023-08-20 21:11:29.363 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Decoded ZCL frame: MoesBHTManufCluster:set_data_response(param=Command(status=92, tsn=71, command_id=536, function=0, data=[4, 0, 0, 0, 235]))
2023-08-20 21:11:29.364 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 221): set_data_response(param=Command(status=92, tsn=71, command_id=536, function=0, data=[4, 0, 0, 0, 235]))
2023-08-20 21:12:29.398 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Decoded ZCL frame: MoesBHTManufCluster:set_data_response(param=Command(status=92, tsn=94, command_id=536, function=0, data=[4, 0, 0, 0, 240]))
2023-08-20 21:12:29.398 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 145): set_data_response(param=Command(status=92, tsn=94, command_id=536, function=0, data=[4, 0, 0, 0, 240]))
2023-08-20 21:13:29.442 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Decoded ZCL frame: MoesBHTManufCluster:set_data_response(param=Command(status=92, tsn=146, command_id=536, function=0, data=[4, 0, 0, 0, 240]))
2023-08-20 21:13:29.443 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 117): set_data_response(param=Command(status=92, tsn=146, command_id=536, function=0, data=[4, 0, 0, 0, 240]))
2023-08-20 21:14:29.487 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Decoded ZCL frame: MoesBHTManufCluster:set_data_response(param=Command(status=92, tsn=188, command_id=536, function=0, data=[4, 0, 0, 0, 240]))
2023-08-20 21:14:29.487 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 111): set_data_response(param=Command(status=92, tsn=188, command_id=536, function=0, data=[4, 0, 0, 0, 240]))

2023-08-21 11:25:57.173 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 88): set_data_response(param=Command(status=24, tsn=126, command_id=536, function=0, data=[4, 0, 0, 1, 14]))
2023-08-21 11:26:57.220 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Decoded ZCL frame: MoesBHTManufCluster:set_data_response(param=Command(status=24, tsn=157, command_id=536, function=0, data=[4, 0, 0, 1, 14]))
2023-08-21 11:26:57.221 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 10): set_data_response(param=Command(status=24, tsn=157, command_id=536, function=0, data=[4, 0, 0, 1, 14]))
2023-08-21 11:27:57.268 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Decoded ZCL frame: MoesBHTManufCluster:set_data_response(param=Command(status=24, tsn=239, command_id=536, function=0, data=[4, 0, 0, 1, 14]))
2023-08-21 11:27:57.268 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 237): set_data_response(param=Command(status=24, tsn=239, command_id=536, function=0, data=[4, 0, 0, 1, 14]))
2023-08-21 11:28:57.326 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Decoded ZCL frame: MoesBHTManufCluster:set_data_response(param=Command(status=25, tsn=9, command_id=536, function=0, data=[4, 0, 0, 1, 14]))
2023-08-21 11:28:57.326 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 157): set_data_response(param=Command(status=25, tsn=9, command_id=536, function=0, data=[4, 0, 0, 1, 14]))
2023-08-21 11:29:57.380 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Decoded ZCL frame: MoesBHTManufCluster:set_data_response(param=Command(status=25, tsn=86, command_id=536, function=0, data=[4, 0, 0, 1, 14]))
2023-08-21 11:29:57.380 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 130): set_data_response(param=Command(status=25, tsn=86, command_id=536, function=0, data=[4, 0, 0, 1, 14]))
2023-08-21 11:30:57.443 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Decoded ZCL frame: MoesBHTManufCluster:set_data_response(param=Command(status=25, tsn=125, command_id=536, function=0, data=[4, 0, 0, 1, 14]))
2023-08-21 11:30:57.443 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 51): set_data_response(param=Command(status=25, tsn=125, command_id=536, function=0, data=[4, 0, 0, 1, 14]))
2023-08-21 11:31:57.501 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Decoded ZCL frame: MoesBHTManufCluster:set_data_response(param=Command(status=25, tsn=187, command_id=536, function=0, data=[4, 0, 0, 1, 14]))
2023-08-21 11:31:57.501 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 22): set_data_response(param=Command(status=25, tsn=187, command_id=536, function=0, data=[4, 0, 0, 1, 14]))
2023-08-21 11:32:57.559 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Decoded ZCL frame: MoesBHTManufCluster:set_data_response(param=Command(status=25, tsn=209, command_id=536, function=0, data=[4, 0, 0, 1, 14]))
2023-08-21 11:32:57.559 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 200): set_data_response(param=Command(status=25, tsn=209, command_id=536, function=0, data=[4, 0, 0, 1, 14]))

FOR COMMAND 0x0A (10): I see these recurring messages, every 2mn or so:

2023-08-21 10:07:37.615 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0x0000] Received command 0x0A (TSN 21): Report_Attributes(attribute_reports=[Attribute(attrid=0x0001, value=TypeValue(type=uint8_t, value=74)), Attribute(attrid=0xFFE2, value=TypeValue(type=uint8_t, value=56)), Attribute(attrid=0xFFE4, value=TypeValue(type=uint8_t, value=0))])
2023-08-21 10:07:37.616 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0x0000] Attribute report received: app_version=74, 0xFFE2=56, 0xFFE4=0
2023-08-21 10:07:37.893 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0x0000] Received command 0x0A (TSN 22): Report_Attributes(attribute_reports=[Attribute(attrid=0xFFDF, value=TypeValue(type=CharacterString, value='��u,i��u,i��u,i��u,i��u,i��u,i��u,i��u,i��u,i��u,i��u,i��u,i'))])
2023-08-21 10:07:37.893 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0x0000] Attribute report received: 0xFFDF='��u,i��u,i��u,i��u,i��u,i��u,i��u,i��u,i��u,i��u,i��u,i��u,i'
2023-08-21 10:07:40.383 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0x0000] Received command 0x0A (TSN 33): Report_Attributes(attribute_reports=[Attribute(attrid=0xFFDF, value=TypeValue(type=CharacterString, value='��u,i��u,i��u,i��u,i��u,i��u,i��u,i��u,i'))])
2023-08-21 10:07:40.384 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0x0000] Attribute report received: 0xFFDF='��u,i��u,i��u,i��u,i��u,i��u,i��u,i��u,i'

2023-08-21 10:21:03.642 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0x0000] Received command 0x0A (TSN 98): Report_Attributes(attribute_reports=[Attribute(attrid=0x0001, value=TypeValue(type=uint8_t, value=74)), Attribute(attrid=0xFFE2, value=TypeValue(type=uint8_t, value=56)), Attribute(attrid=0xFFE4, value=TypeValue(type=uint8_t, value=0))])
2023-08-21 10:21:03.643 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0x0000] Attribute report received: app_version=74, 0xFFE2=56, 0xFFE4=0
2023-08-21 10:21:05.913 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0x0000] Received command 0x0A (TSN 112): Report_Attributes(attribute_reports=[Attribute(attrid=0xFFDF, value=TypeValue(type=CharacterString, value='��u,i��u,i��u,i��u,i��u,i��u,i\r�u,i��u,i��u,i��u,i��u,i��u,i'))])
2023-08-21 10:21:05.914 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0x0000] Attribute report received: 0xFFDF='��u,i��u,i��u,i��u,i��u,i��u,i\r�u,i��u,i��u,i��u,i��u,i��u,i'
2023-08-21 10:21:06.169 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0x0000] Received command 0x0A (TSN 113): Report_Attributes(attribute_reports=[Attribute(attrid=0xFFDF, value=TypeValue(type=CharacterString, value='\r�u,i\r�u,i\r�u,i\r�u,i��u,i��u,i��u,i��u,i'))])
2023-08-21 10:21:06.169 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0x0000] Attribute report received: 0xFFDF='\r�u,i\r�u,i\r�u,i\r�u,i��u,i��u,i��u,i��u,i'

FOR COMMAND 0x24 (36): I see these recurring messages, every 1mn or so:

2023-08-21 10:06:56.381 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 134): set_time_request(param=[179, 9])
2023-08-21 10:06:56.582 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 134): set_time_request(param=[179, 9])
2023-08-21 10:06:56.787 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 134): set_time_request(param=[179, 9])
2023-08-21 10:06:56.983 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 134): set_time_request(param=[179, 9])
2023-08-21 10:06:57.432 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 136): set_time_request(param=[181, 9])
2023-08-21 10:06:57.632 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 136): set_time_request(param=[181, 9])
2023-08-21 10:06:57.827 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 136): set_time_request(param=[181, 9])
2023-08-21 10:06:58.032 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 136): set_time_request(param=[181, 9])
2023-08-21 10:07:56.249 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 99): set_time_request(param=[8, 10])
2023-08-21 10:07:56.450 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 99): set_time_request(param=[8, 10])
2023-08-21 10:07:56.651 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 99): set_time_request(param=[8, 10])
2023-08-21 10:07:56.850 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 99): set_time_request(param=[8, 10])
2023-08-21 10:07:57.051 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 99): set_time_request(param=[8, 10])
2023-08-21 10:08:56.737 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 18): set_time_request(param=[41, 10])
2023-08-21 10:08:57.119 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 18): set_time_request(param=[41, 10])
2023-08-21 10:08:57.570 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 20): set_time_request(param=[43, 10])
2023-08-21 10:08:57.761 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 20): set_time_request(param=[43, 10])
2023-08-21 10:08:57.968 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 20): set_time_request(param=[43, 10])
2023-08-21 10:09:58.875 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 221): set_time_request(param=[98, 10])
2023-08-21 10:09:59.053 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 221): set_time_request(param=[98, 10])
2023-08-21 10:09:59.263 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 221): set_time_request(param=[98, 10])
2023-08-21 10:09:59.454 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 221): set_time_request(param=[98, 10])
2023-08-21 10:09:59.658 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 221): set_time_request(param=[98, 10])
2023-08-21 10:10:56.463 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 136): set_time_request(param=[161, 10])
2023-08-21 10:10:56.668 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 136): set_time_request(param=[161, 10])
2023-08-21 10:10:57.064 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 136): set_time_request(param=[161, 10])
2023-08-21 10:10:57.278 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 136): set_time_request(param=[161, 10])
2023-08-21 10:10:57.709 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 138): set_time_request(param=[163, 10])
2023-08-21 10:10:57.910 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 138): set_time_request(param=[163, 10])
2023-08-21 10:10:58.115 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 138): set_time_request(param=[163, 10])
2023-08-21 10:10:58.312 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 138): set_time_request(param=[163, 10])
2023-08-21 10:12:00.226 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 109): set_time_request(param=[208, 10])
2023-08-21 10:12:00.426 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 109): set_time_request(param=[208, 10])
2023-08-21 10:12:00.627 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 109): set_time_request(param=[208, 10])
2023-08-21 10:12:00.828 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 109): set_time_request(param=[208, 10])
2023-08-21 10:12:56.607 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 32): set_time_request(param=[244, 10])
2023-08-21 10:12:56.808 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 32): set_time_request(param=[244, 10])
2023-08-21 10:12:57.004 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 32): set_time_request(param=[244, 10])
2023-08-21 10:12:57.850 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 34): set_time_request(param=[246, 10])
2023-08-21 10:12:58.054 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 34): set_time_request(param=[246, 10])
2023-08-21 10:12:58.460 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 34): set_time_request(param=[246, 10])
2023-08-21 10:13:56.693 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 1): set_time_request(param=[42, 11])
2023-08-21 10:13:56.877 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 1): set_time_request(param=[42, 11])
2023-08-21 10:13:57.083 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 1): set_time_request(param=[42, 11])
2023-08-21 10:13:57.278 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 1): set_time_request(param=[42, 11])
2023-08-21 10:13:57.479 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 1): set_time_request(param=[42, 11])
2023-08-21 10:14:56.754 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 177): set_time_request(param=[64, 11])
2023-08-21 10:14:56.960 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 177): set_time_request(param=[64, 11])
2023-08-21 10:14:57.156 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 177): set_time_request(param=[64, 11])
2023-08-21 10:14:57.369 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 177): set_time_request(param=[64, 11])
2023-08-21 10:14:57.557 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 177): set_time_request(param=[64, 11])
2023-08-21 10:14:58.017 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 179): set_time_request(param=[66, 11])
2023-08-21 10:14:58.226 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 179): set_time_request(param=[66, 11])
2023-08-21 10:14:58.414 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 179): set_time_request(param=[66, 11])
2023-08-21 10:14:58.619 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 179): set_time_request(param=[66, 11])
2023-08-21 10:15:56.831 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 149): set_time_request(param=[113, 11])
2023-08-21 10:15:57.027 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 149): set_time_request(param=[113, 11])
2023-08-21 10:15:57.232 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 149): set_time_request(param=[113, 11])
2023-08-21 10:15:57.427 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 149): set_time_request(param=[113, 11])
2023-08-21 10:15:57.627 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 149): set_time_request(param=[113, 11])
2023-08-21 10:16:56.911 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 65): set_time_request(param=[162, 11])
2023-08-21 10:16:57.321 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 65): set_time_request(param=[162, 11])
2023-08-21 10:16:57.504 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 65): set_time_request(param=[162, 11])
2023-08-21 10:16:57.705 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 65): set_time_request(param=[162, 11])
2023-08-21 10:16:58.167 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 67): set_time_request(param=[164, 11])
2023-08-21 10:16:58.354 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 67): set_time_request(param=[164, 11])
2023-08-21 10:16:58.559 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 67): set_time_request(param=[164, 11])
2023-08-21 10:16:58.755 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 67): set_time_request(param=[164, 11])
2023-08-21 10:17:56.490 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 37): set_time_request(param=[221, 11])
2023-08-21 10:17:56.691 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 37): set_time_request(param=[221, 11])
2023-08-21 10:17:56.892 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 37): set_time_request(param=[221, 11])
2023-08-21 10:19:56.623 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 144): set_time_request(param=[132, 12])
2023-08-21 10:19:56.818 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 144): set_time_request(param=[132, 12])
2023-08-21 10:19:57.018 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 144): set_time_request(param=[132, 12])

2023-08-21 10:06:56.181 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] No explicit handler for cluster command 0x24: set_time_request(param=[179, 9])
mlouaze commented 1 year ago

I guess the 536 is just sent by the thermostat every minute to notify the current temperature. 536 is 218 in hexadecimal, and you can see it as MOESBHT_TEMPERATURE_ATTR = 0x0218 in the custom quirk. This seems to make sense since it is also the command_id you have when you report the room temperature. Following the same idea 1060 is 424 in hexadecimal so MOESBHT_RUNNING_MODE_ATTR = 0x0424 n the custom quirk. And 528 is 0x210 so MOESBHT_TARGET_TEMP_ATTR = 0x0210 which also seems to make sense.

But maybe you had already figured this out ...

bsfaxi commented 1 year ago

Another notice : The data sent by the COMMAND "0x24" every 1 minute contains the last TSN number and the Status from the last "command_id=1060".

2023-08-21 15:31:23.256 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 243): set_data_response(param=Command(status=88, tsn=215, command_id=1060, function=0, data=[1, 1]))
2023-08-21 15:31:23.318 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 244): set_data_response(param=Command(status=88, tsn=216, command_id=1060, function=0, data=[1, 1]))
2023-08-21 15:31:23.409 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 245): set_time_request(param=[216, 88])
2023-08-21 15:31:23.605 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x24 (TSN 245): set_time_request(param=[216, 88])
bsfaxi commented 1 year ago

When I set the actual time and schedule on the device, I see a new "command_id=101":

2023-08-21 14:56:05.303 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 139): set_data_response(param=Command(status=78, tsn=159, command_id=101, function=0, data=[36, 7, 0, 44, 11, 0, 44, 18, 0, 44, 23, 0, 44, 8, 0, 46, 12, 0, 46, 16, 0, 46, 23, 0, 46, 8, 0, 48, 12, 0, 48, 16, 0, 48, 23, 0, 48]))

I tried the following schedule: Monday to Friday:

Saturday:

Sunday:

So, the data sent is the following format: data=[36, 7, 0, 44, 11, 0, 44, 18, 0, 44, 23, 0, 44, 8, 0, 46, 12, 0, 46, 16, 0, 46, 23, 0, 46, 8, 0, 48, 12, 0, 48, 16, 0, 48, 23, 0, 48] data=[XX, H, M, Tx2, H, M, Tx2, H, M, Tx2, H, M, Tx2, H, M, Tx2, H, M, Tx2, H, M, Tx2, H, M, Tx2, H, M, Tx2, H, M, Tx2, H, M, Tx2, H, M, Tx2] Each triple has: "Hour, Minute, Desired_Temperature x 2". I don't know yet what XX means. In my installation, I encounter only 36 for the moment.

mlouaze commented 1 year ago

Pure Guess but 36 could be the number of information you just sent. 12 timeslot * 3 information per timeslot (hours, minutes and temperature).

EDIT: I didn't read your message correctly

bsfaxi commented 1 year ago

Pure Guess but 36 could be the number of information you just sent. 12 timeslot * 3 information per timeslot (hours, minutes and temperature).

I think so. :)

EDIT : In every data table of the commands, I noticed that the first number corresponds to the length of the rest of the table. ;)

bsfaxi commented 1 year ago

When I switched from "manual" to "schedule", I see the following:

2023-08-21 15:55:32.186 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 230): set_data_response(param=Command(status=94, tsn=221, command_id=1060, function=0, data=[1, 1]))
2023-08-21 15:55:32.256 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 231): set_data_response(param=Command(status=94, tsn=213, command_id=1026, function=0, data=[1, 1]))
2023-08-21 15:55:32.314 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 232): set_data_response(param=Command(status=94, tsn=216, command_id=1060, function=0, data=[1, 1]))

EDIT : There is 1 new "command_id=1026".

The new target temperature (22°) of the period has been taken into account: 2023-08-21 15:55:34.396 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 255): set_data_response(param=Command(status=94, tsn=219, command_id=528, function=0, data=[4, 0, 0, 0, 22]))

EDIT:

From manual to program: 2023-08-21 17:03:45.009 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 47): set_data_response(param=Command(status=108, tsn=215, command_id=1026, function=0, data=[1, 1]))

From program to manual: 2023-08-21 17:03:53.112 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 76): set_data_response(param=Command(status=108, tsn=228, command_id=1026, function=0, data=[1, 0]))

bsfaxi commented 1 year ago

There is a "command_id=257" when switching the state of the device on/off.

When I change the state of the thermostat from "off" to "on": 2023-08-21 16:53:41.237 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 165): set_data_response(param=Command(status=107, tsn=1, command_id=257, function=0, data=[1, 1]))

When I change the state of the thermostat from "on" to "off": 2023-08-21 16:52:52.827 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 244): set_data_response(param=Command(status=106, tsn=234, command_id=257, function=0, data=[1, 0]))

bsfaxi commented 1 year ago

Changing the status to heating: 2023-08-21 17:47:17.145 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 189): set_data_response(param=Command(status=14, tsn=150, command_id=1060, function=0, data=[1, 0]))

changing the status to idle: 2023-08-21 17:55:45.479 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 252): set_data_response(param=Command(status=16, tsn=193, command_id=1060, function=0, data=[1, 1]))

bsfaxi commented 1 year ago

Changing the status to locked: 2023-08-21 18:26:57.887 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 125): set_data_response(param=Command(status=24, tsn=62, command_id=296, function=0, data=[1, 1]))

Changing the status to unlocked: 2023-08-21 18:32:01.506 DEBUG (MainThread) [zigpy.zcl] [0x990F:1:0xef00] Received command 0x02 (TSN 51): set_data_response(param=Command(status=25, tsn=142, command_id=296, function=0, data=[1, 0]))

mlouaze commented 1 year ago

I checked all your values, it seems they all follow the ATTR in the custom quirk. image

So to recap we have : 528 (210 in hexadec) for the value of the target temperature (three 0 then value in degree). ex : [4, 0, 0, 0, 20] for 20° 536 (218 in hexadec) for the value of the actual ambiant temperature (three 0 then value in decidegree). ex : [4, 0, 0, 0, 205] for 20.5° 1026 (402 in hexadec) for the value manual mode (1=false and 0=true). ex : [1, 0] for manual mode enabled 257 (101 in hexadec) for the enabling of the device (0=off and1=on). ex : [1, 1] for device on 1060 (424 in hexadec) for the heating status (1=idle and 0=heating). ex: [1, 1] for device not heating (idling) 296 (128 in hexadec) for the lock//childlock (0=unlocked and 1=locked). ex [1, 1] for device locked

Those command come in arrays with a first number indicating how many values are in the array All those are already in the custom quirk and should then be working, I suppose.

The schedule command is not in the custom quirk already: 101 (65 in hexadec) for the temperature schedule. see https://github.com/zigpy/zha-device-handlers/issues/2433#issuecomment-1686336394

This explains why we can see what is happening on the device from homeassistant. But we still need to figure out why we can't send values to the thermostat correctly.

bsfaxi commented 1 year ago

Yes, all these are to GET the statuses. I don't know how to catch the SET commands.

mlouaze commented 1 year ago

image When sending a target_temperature using this inteface (sorry it is in french), I get the following message in the log : 2023-08-21 19:47:04.750 DEBUG (MainThread) [homeassistant.components.zha.websocket_api] Set attribute for: cluster_id: [61184] cluster_type: [in] endpoint_id: [1] attribute: [528] value: [10] manufacturer: [None] response: [[[WriteAttributesStatusRecord(status=<Status.SUCCESS: 0>)]]]

This seems good, but after writing 10 and then reading the value again I still see 18.

mlouaze commented 1 year ago

For info, when writing to another moes zigbee device (roller shutter controller) with the same method I get this : 2023-08-21 19:55:43.796 DEBUG (MainThread) [homeassistant.components.zha.websocket_api] Set attribute for: cluster_id: [258] cluster_type: [in] endpoint_id: [1] attribute: [61443] value: [307] manufacturer: [None] response: [Write_Attributes_rsp(status_records=[WriteAttributesStatusRecord(status=<Status.SUCCESS: 0>)])]

And it works (after writing 307 I do read 307)

bsfaxi commented 1 year ago

Each time, I try from HA to switch the status from "on" to "off", I have the following message: 2023-08-21 20:23:42.676 DEBUG (MainThread) [zhaquirks.tuya] [0x990f:1:0x0201] Mapping standard system_mode (0x001c) with value <SystemMode.Off: 0> to custom {257: 0}

And from "off" to "on": 2023-08-21 20:25:04.023 DEBUG (MainThread) [zhaquirks.tuya] [0x990f:1:0x0201] Mapping standard system_mode (0x001c) with value <SystemMode.Heat: 4> to custom {257: 1}

Of course, I set the opposite on the device before testing from HA.

When I set the target temperature from HA to 28, I have the following message: 2023-08-21 20:27:40.317 DEBUG (MainThread) [zhaquirks.tuya] [0x990f:1:0x0201] Mapping standard occupied_heating_setpoint (0x0012) with value 2800 to custom {528: 28}

Is this normal?

mlouaze commented 1 year ago

Just a question, do you also receive almost every 47ms a value for 0x0424 ? Something like this:

2023-08-22 12:03:48.426 DEBUG (MainThread) [zhaquirks.tuya] [0x1617:1:0xef00] Received value [1] for attribute 0x0424 (command 0x0002) 2023-08-22 12:03:48.427 DEBUG (MainThread) [homeassistant.components.zha.core.cluster_handlers] [0x1617:1:0x0201]: Attribute report 'MoesBHTThermostat'[running_mode] = RunningMode.Off 2023-08-22 12:03:48.428 DEBUG (MainThread) [homeassistant.components.zha.core.cluster_handlers] [0x1617:1:0x0201]: Attribute report 'MoesBHTThermostat'[running_state] = 0 2023-08-22 12:03:48.431 DEBUG (MainThread) [homeassistant.components.zha.entity] climate.tze204_aoclfnxz_ts0601_thermostat: Attribute 'running_mode' = RunningMode.Off update 2023-08-22 12:03:48.432 DEBUG (MainThread) [homeassistant.components.zha.entity] climate.tze204_aoclfnxz_ts0601_thermostat: Attribute 'running_state' = 0 update 2023-08-22 12:03:48.474 DEBUG (MainThread) [zhaquirks.tuya] [0x1617:1:0xef00] Received value [1] for attribute 0x0424 (command 0x0002) 2023-08-22 12:03:48.474 DEBUG (MainThread) [homeassistant.components.zha.core.cluster_handlers] [0x1617:1:0x0201]: Attribute report 'MoesBHTThermostat'[running_mode] = RunningMode.Off 2023-08-22 12:03:48.475 DEBUG (MainThread) [homeassistant.components.zha.core.cluster_handlers] [0x1617:1:0x0201]: Attribute report 'MoesBHTThermostat'[running_state] = 0 2023-08-22 12:03:48.477 DEBUG (MainThread) [homeassistant.components.zha.entity] climate.tze204_aoclfnxz_ts0601_thermostat: Attribute 'running_mode' = RunningMode.Off update 2023-08-22 12:03:48.478 DEBUG (MainThread) [homeassistant.components.zha.entity] climate.tze204_aoclfnxz_ts0601_thermostat: Attribute 'running_state' = 0 update

I find it really weird for the device to notify that often informations

bsfaxi commented 1 year ago

on my installation, it's almost every second... :)

mlouaze commented 1 year ago

That is surprising. I tried a lot of test to modify value but nothing seems to work. It seems that the value is sent correctly but the thermostat doesn't care at all. As if something else was mandatory to modify the value on it. I think the only way to figure this out would be to use the working z2m and reverse engineer what is sent.

bsfaxi commented 1 year ago

I tried a new dirty quirk to get rid off "MOESBHT_SCHEDULE_MODE_ATTR" and "MOESBHT_MANUAL_MODE_ATTR". Now, I have only MOESBHT6_MODE_ATTR = 0x0402 # [0] manual [1] scheduled I also changed some logic, like for example the inverted values, that are not relevant for this thermostat. Here is the new quirk:

"""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, GreenPowerProxy

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
MOESBHT6_TARGET_TEMP_ATTR = 0x0210  # [0,0,0,21] target room temp (degree)
MOESBHT6_TEMPERATURE_ATTR = 0x0218  # [0,0,0,200] current room temp (decidegree)
MOESBHT6_MODE_ATTR = 0x0402  # [0] manual [1] scheduled
MOESBHT6_ENABLED_ATTR = 0x0101  # [0] off [1] on
MOESBHT6_RUNNING_MODE_ATTR = 0x0424  # [1] idle [0] heating /!\ inverted
MOESBHT6_CHILD_LOCK_ATTR = 0x0128  # [0] unlocked [1] child-locked

_LOGGER = logging.getLogger(__name__)

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

    attributes = {
        MOESBHT6_TARGET_TEMP_ATTR: ("target_temperature", t.uint32_t, True),
        MOESBHT6_TEMPERATURE_ATTR: ("temperature", t.uint32_t, True),
        MOESBHT6_MODE_ATTR: ("mode", t.uint8_t, True),
        MOESBHT6_ENABLED_ATTR: ("enabled", t.uint8_t, True),
        MOESBHT6_RUNNING_MODE_ATTR: ("running_mode", t.uint8_t, True),
        MOESBHT6_CHILD_LOCK_ATTR: ("child_lock", t.uint8_t, True),
    }

    def _update_attribute(self, attrid, value):
        super()._update_attribute(attrid, value)
        if attrid == MOESBHT6_TARGET_TEMP_ATTR:
            self.endpoint.device.thermostat_bus.listener_event(
                "temperature_change",
                "occupied_heating_setpoint",
                value * 100,  # degree to centidegree
            )
        elif attrid == MOESBHT6_TEMPERATURE_ATTR:
            self.endpoint.device.thermostat_bus.listener_event(
                "temperature_change",
                "local_temperature",
                value * 10,  # decidegree to centidegree
            )
        elif attrid == MOESBHT6_MODE_ATTR:
            if value == 0:  # manual
                self.endpoint.device.thermostat_bus.listener_event(
                    "program_change", "manual"
                )
            elif value == 1:  # scheduled
                self.endpoint.device.thermostat_bus.listener_event(
                    "program_change", "scheduled"
                )
        elif attrid == MOESBHT6_ENABLED_ATTR:
            self.endpoint.device.thermostat_bus.listener_event("enabled_change", value)
        elif attrid == MOESBHT6_RUNNING_MODE_ATTR:
            self.endpoint.device.thermostat_bus.listener_event("state_change", value)
        elif attrid == MOESBHT6_CHILD_LOCK_ATTR:
            self.endpoint.device.ui_bus.listener_event("child_lock_change", value)

class MoesBHT6Thermostat(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 {MOESBHT6_TARGET_TEMP_ATTR: round(value / 100)}
        if attribute == "system_mode":
            if value == self.SystemMode.Off:
                return {MOESBHT6_ENABLED_ATTR: 0}
            if value == self.SystemMode.Heat:
                return {MOESBHT6_ENABLED_ATTR: 1}
            self.error("Unsupported value for SystemMode")
        elif attribute == "programing_oper_mode":
            if value == self.ProgrammingOperationMode.Simple:
                return {MOESBHT6_MODE_ATTR: 0}
            if value == self.ProgrammingOperationMode.Schedule_programming_mode:
                return {MOESBHT6_MODE_ATTR: 1}
            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 MoesBHT6UserInterface(TuyaUserInterfaceCluster):
    """HVAC User interface cluster for tuya electric heating thermostats."""

    _CHILD_LOCK_ATTR = MOESBHT6_CHILD_LOCK_ATTR

class MoesBHT6(TuyaThermostat):
    """Tuya thermostat for devices like the Moes BHT-006GBZB Electric floor heating."""

    signature = {
        MODELS_INFO: [
            ("_TZE204_aoclfnxz", "TS0601"),
        ],
        ENDPOINTS: {
            #  endpoint=1 profile=260 device_type=81 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],
            },
            242:{
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.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,
                    MoesBHT6ManufCluster,
                    MoesBHT6Thermostat,
                    MoesBHT6UserInterface,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            }
        }
    }
bsfaxi commented 1 year ago

Now, when I switch on/off from the device, I have data=[1,0] or data=[1,1]. But, when I try from HA, I have data=0 or data=1. See below:

2023-08-22 22:29:07.726 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Sending request: set_data(param=Command(status=0, tsn=207, command_id=257, function=0, data=0))
2023-08-22 22:34:02.048 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Decoded ZCL frame: MoesBHT6ManufCluster:set_data_response(param=Command(status=3, tsn=86, command_id=257, function=0, data=[1, 0]))
2023-08-22 22:34:02.049 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Received command 0x02 (TSN 233): set_data_response(param=Command(status=3, tsn=86, command_id=257, function=0, data=[1, 0]))
2023-08-22 22:36:15.600 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Sending request: set_data(param=Command(status=0, tsn=148, command_id=257, function=0, data=1))
2023-08-22 23:26:04.942 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Decoded ZCL frame: MoesBHT6ManufCluster:set_data_response(param=Command(status=13, tsn=158, command_id=257, function=0, data=[1, 1]))
2023-08-22 23:26:04.943 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Received command 0x02 (TSN 238): set_data_response(param=Command(status=13, tsn=158, command_id=257, function=0, data=[1, 1]))
2023-08-22 23:26:17.012 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Sending request: set_data(param=Command(status=0, tsn=194, command_id=257, function=0, data=0))

This is linked to this part of the quirk I think:

        if attribute == "system_mode":
            if value == self.SystemMode.Off:
                return {MOESBHT6_ENABLED_ATTR: 0}
            if value == self.SystemMode.Heat:
                return {MOESBHT6_ENABLED_ATTR: 1}
            self.error("Unsupported value for SystemMode")

Anyone could help here ?

bsfaxi commented 1 year ago

Same when setting target temperature. The data is not a table!

2023-08-22 23:39:58.158 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Decoded ZCL frame: MoesBHT6ManufCluster:set_data_response(param=Command(status=15, tsn=226, command_id=528, function=0, data=[4, 0, 0, 0, 29]))
2023-08-22 23:39:58.158 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Received command 0x02 (TSN 108): set_data_response(param=Command(status=15, tsn=226, command_id=528, function=0, data=[4, 0, 0, 0, 29]))
2023-08-22 23:40:19.081 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Sending request: set_data(param=Command(status=0, tsn=45, command_id=528, function=0, data=25))
mlouaze commented 1 year ago

I think it has to do with the fact that the value sent isn't a uint32 coded on 4 bytes but a uint8 coded in 1 byte. But you also have to add the 4 at the beginning of the array.

mlouaze commented 1 year ago

I will try to take a look at it tomorrow

bsfaxi commented 1 year ago

For the switch on/off command_id=257, it's 1 bloc. So, it's normal to have uint8... I found the below error:

2023-08-23 00:53:06.584 DEBUG (MainThread) [zhaquirks.tuya] [0x3728:1:0x0201] Mapping standard system_mode (0x001c) with value <SystemMode.Off: 0> to custom {257: 0}
2023-08-23 00:53:06.585 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Sending request header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=True, direction=<Direction.Server_to_Client: 0>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False), manufacturer=4417, tsn=58, command_id=0, *direction=<Direction.Server_to_Client: 0>)
2023-08-23 00:53:06.586 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Sending request: set_data(param=Command(status=0, tsn=58, command_id=257, function=0, data=0))
2023-08-23 00:53:06.618 DEBUG (MainThread) [homeassistant.components.zha.core.cluster_handlers] [0x3728:1:0x0201]: wrote {'system_mode': <SystemMode.Off: 0>} attrs, Status: [[WriteAttributesStatusRecord(status=<Status.SUCCESS: 0>)]]
2023-08-23 00:53:06.619 DEBUG (MainThread) [homeassistant.components.zha.core.cluster_handlers] [0x3728:1:0x0201]: set system to SystemMode.Off
2023-08-23 00:53:06.623 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x3728), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=None, profile_id=260, cluster_id=61184, data=Serialized[b'\x18:\x0b\x00\x83'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=135, rssi=-77)
2023-08-23 00:53:06.624 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Received ZCL frame: b'\x18:\x0b\x00\x83'
2023-08-23 00:53:06.624 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.GLOBAL_COMMAND: 0>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=58, command_id=11, *direction=<Direction.Client_to_Server: 1>)
2023-08-23 00:53:06.625 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Decoded ZCL frame: MoesBHT6ManufCluster:Default_Response(command_id=0, status=<Status.UNSUP_MANUF_CLUSTER_COMMAND: 131>)
2023-08-23 00:53:06.625 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Received command 0x0B (TSN 58): Default_Response(command_id=0, status=<Status.UNSUP_MANUF_CLUSTER_COMMAND: 131>)
2023-08-23 00:53:07.589 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x3728), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=None, profile_id=260, cluster_id=61184, data=Serialized[b'\t\x15\x02\x1d\xe9$\x04\x00\x01\x01'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=135, rssi=-77)
bsfaxi commented 1 year ago

Same error "UNSUP_MANUF_CLUSTER_COMMAND: 131" when trying to set target temperature. Example below: target temperature 26 and then 27.

2023-08-23 09:31:04.101 DEBUG (MainThread) [zhaquirks.tuya] [0x3728:1:0x0201] Mapping standard occupied_heating_setpoint (0x0012) with value 2600 to custom {528: 26}
2023-08-23 09:31:04.104 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Sending request header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=True, direction=<Direction.Server_to_Client: 0>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False), manufacturer=4417, tsn=91, command_id=0, *direction=<Direction.Server_to_Client: 0>)
2023-08-23 09:31:04.105 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Sending request: set_data(param=Command(status=0, tsn=91, command_id=528, function=0, data=26))
2023-08-23 09:31:04.566 DEBUG (MainThread) [homeassistant.components.zha.core.cluster_handlers] [0x3728:1:0x0201]: wrote {'occupied_heating_setpoint': 2600} attrs, Status: [[WriteAttributesStatusRecord(status=<Status.SUCCESS: 0>)]]
2023-08-23 09:31:04.573 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x3728), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=None, profile_id=260, cluster_id=61184, data=Serialized[b'\x18[\x0b\x00\x83'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=55, rssi=-87)
2023-08-23 09:31:04.574 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Received ZCL frame: b'\x18[\x0b\x00\x83'
2023-08-23 09:31:04.575 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.GLOBAL_COMMAND: 0>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=91, command_id=11, *direction=<Direction.Client_to_Server: 1>)
2023-08-23 09:31:04.577 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Decoded ZCL frame: MoesBHT6ManufCluster:Default_Response(command_id=0, status=<Status.UNSUP_MANUF_CLUSTER_COMMAND: 131>)
2023-08-23 09:31:04.578 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Received command 0x0B (TSN 91): Default_Response(command_id=0, status=<Status.UNSUP_MANUF_CLUSTER_COMMAND: 131>)

...
2023-08-23 09:40:39.766 DEBUG (MainThread) [zhaquirks.tuya] [0x3728:1:0x0201] Mapping standard occupied_heating_setpoint (0x0012) with value 2700 to custom {528: 27}
2023-08-23 09:40:39.767 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Sending request header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.CLUSTER_COMMAND: 1>, is_manufacturer_specific=True, direction=<Direction.Server_to_Client: 0>, disable_default_response=0, reserved=0, *is_cluster=True, *is_general=False), manufacturer=4417, tsn=236, command_id=0, *direction=<Direction.Server_to_Client: 0>)
2023-08-23 09:40:39.768 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Sending request: set_data(param=Command(status=0, tsn=236, command_id=528, function=0, data=27))
2023-08-23 09:40:39.793 DEBUG (MainThread) [homeassistant.components.zha.core.cluster_handlers] [0x3728:1:0x0201]: wrote {'occupied_heating_setpoint': 2700} attrs, Status: [[WriteAttributesStatusRecord(status=<Status.SUCCESS: 0>)]]
2023-08-23 09:40:39.801 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x3728), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=None, profile_id=260, cluster_id=61184, data=Serialized[b'\x18\xec\x0b\x00\x83'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=71, rssi=-85)
2023-08-23 09:40:39.801 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Received ZCL frame: b'\x18\xec\x0b\x00\x83'
2023-08-23 09:40:39.802 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=<FrameType.GLOBAL_COMMAND: 0>, is_manufacturer_specific=0, direction=<Direction.Client_to_Server: 1>, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=236, command_id=11, *direction=<Direction.Client_to_Server: 1>)
2023-08-23 09:40:39.802 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Decoded ZCL frame: MoesBHT6ManufCluster:Default_Response(command_id=0, status=<Status.UNSUP_MANUF_CLUSTER_COMMAND: 131>)
2023-08-23 09:40:39.803 DEBUG (MainThread) [zigpy.zcl] [0x3728:1:0xef00] Received command 0x0B (TSN 236): Default_Response(command_id=0, status=<Status.UNSUP_MANUF_CLUSTER_COMMAND: 131>)
bsfaxi commented 1 year ago

I saw that @javicalle has done some customization for another tuya window covering device here to handle this error. Any help is really appreciated.

BTW, here is my last quirk if someone wants to try:

"""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, GreenPowerProxy

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
MOESBHT6_TARGET_TEMP_ATTR = 0x0210  # [0,0,0,21] target room temp (degree)
MOESBHT6_TEMPERATURE_ATTR = 0x0218  # [0,0,0,200] current room temp (decidegree)
MOESBHT6_MODE_ATTR = 0x0402  # [0] manual [1] scheduled
MOESBHT6_ENABLED_ATTR = 0x0101  # [0] off [1] on
MOESBHT6_RUNNING_STATE_ATTR = 0x0424  # [1] idle [0] heating
MOESBHT6_CHILD_LOCK_ATTR = 0x0128  # [0] unlocked [1] child-locked

_LOGGER = logging.getLogger(__name__)

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

    attributes = {
        MOESBHT6_TARGET_TEMP_ATTR: ("target_temperature", t.uint32_t, True),
        MOESBHT6_TEMPERATURE_ATTR: ("temperature", t.uint32_t, True),
        MOESBHT6_MODE_ATTR: ("system_mode", t.uint8_t, True),
        MOESBHT6_ENABLED_ATTR: ("enabled", t.uint8_t, True),
        MOESBHT6_RUNNING_STATE_ATTR: ("running_state", t.uint8_t, True),
        MOESBHT6_CHILD_LOCK_ATTR: ("child_lock", t.uint8_t, True),
    }

    def _update_attribute(self, attrid, value):
        super()._update_attribute(attrid, value)
        if attrid == MOESBHT6_TARGET_TEMP_ATTR:
            self.endpoint.device.thermostat_bus.listener_event(
                "temperature_change",
                "occupied_heating_setpoint",
                value * 100,  # degree to centidegree
            )
        elif attrid == MOESBHT6_TEMPERATURE_ATTR:
            self.endpoint.device.thermostat_bus.listener_event(
                "temperature_change",
                "local_temperature",
                value * 10,  # decidegree to centidegree
            )
        elif attrid == MOESBHT6_MODE_ATTR:
            if value == 0:  # manual
                self.endpoint.device.thermostat_bus.listener_event(
                    "program_change", "manual"
                )
            elif value == 1:  # scheduled
                self.endpoint.device.thermostat_bus.listener_event(
                    "program_change", "scheduled"
                )
        elif attrid == MOESBHT6_ENABLED_ATTR:
            self.endpoint.device.thermostat_bus.listener_event("enabled_change", value)
        elif attrid == MOESBHT6_RUNNING_STATE_ATTR:
            self.endpoint.device.thermostat_bus.listener_event("state_change", value)
        elif attrid == MOESBHT6_CHILD_LOCK_ATTR:
            self.endpoint.device.ui_bus.listener_event("child_lock_change", value)

class MoesBHT6Thermostat(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 {MOESBHT6_TARGET_TEMP_ATTR: round(value / 100)}
        if attribute == "system_mode":
            if value == self.SystemMode.Off:
                return {MOESBHT6_ENABLED_ATTR: 0x00}
            if value == self.SystemMode.Heat:
                return {MOESBHT6_ENABLED_ATTR: 0x01}
            self.error("Unsupported value for SystemMode")
        elif attribute == "programing_oper_mode":
            if value == self.ProgrammingOperationMode.Simple:
                return {MOESBHT6_MODE_ATTR: 0}
            if value == self.ProgrammingOperationMode.Schedule_programming_mode:
                return {MOESBHT6_MODE_ATTR: 1}
            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 MoesBHT6UserInterface(TuyaUserInterfaceCluster):
    """HVAC User interface cluster for tuya electric heating thermostats."""

    _CHILD_LOCK_ATTR = MOESBHT6_CHILD_LOCK_ATTR

class MoesBHT6(TuyaThermostat):
    """Tuya thermostat for devices like the Moes BHT-006GBZB Electric floor heating."""

    signature = {
        MODELS_INFO: [
            ("_TZE204_aoclfnxz", "TS0601"),
        ],
        ENDPOINTS: {
            #  endpoint=1 profile=260 device_type=81 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],
            },
            242:{
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.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,
                    MoesBHT6ManufCluster,
                    MoesBHT6Thermostat,
                    MoesBHT6UserInterface,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            }
        }
    }
mlouaze commented 1 year ago

I was able to control the thermostat remotely using z2m. I will try to see how datas are exchanged to mimic it in ZHA.

bsfaxi commented 1 year ago

Below is another diiiiiiirty quirk. But, I can now control the on/off and the target temperature. 🥇 There is only one missing thing: when I set a target temperature higher than the actual temperature, the thermostat is heating. But, his "running_mode" is not reflected into HA as "Heat" ! It still "idle". :-( Could someone test it please? Ensure you delete the "pycache" subdirectory before restarting HA.

"""Map from manufacturer to standard clusters for electric heating thermostats."""
"""Tuya MCU based thermostat."""

import logging

from zigpy.profiles import zha
import zigpy.types as t
from zigpy.zcl.clusters.general import Basic, Groups, Ota, Scenes, Time, GreenPowerProxy

from typing import Dict, Optional, Union
from zigpy.zcl import foundation
from zigpy.zcl.clusters.hvac import Thermostat

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)
from zhaquirks.tuya import (
    TuyaManufClusterAttributes,
    TuyaThermostat,
    TuyaThermostatCluster,
    TuyaUserInterfaceCluster,
    NoManufacturerCluster,
    TUYA_MCU_COMMAND,
    TuyaLocalCluster,
)

from zhaquirks.tuya.mcu import (
    DPToAttributeMapping,
    TuyaClusterData,
    # TuyaDPType,
    TuyaMCUCluster,
)

class TuyaTC(t.enum8):
    """Tuya thermostat commands."""
    OFF = 0x00
    ON = 0x01

class ZclTC(t.enum8):
    """ZCL thermostat commands."""
    OFF = 0x00
    ON = 0x01

TUYA2ZB_COMMANDS = {
    ZclTC.OFF: TuyaTC.OFF,
    ZclTC.ON: TuyaTC.ON,
}

# 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
MOESBHT6_TARGET_TEMP_ATTR = 0x0210  # [0,0,0,21] target room temp (degree)
MOESBHT6_TEMPERATURE_ATTR = 0x0218  # [0,0,0,200] current room temp (decidegree)
MOESBHT6_MODE_ATTR = 0x0402  # [0] manual [1] scheduled
MOESBHT6_ENABLED_ATTR = 0x0101  # [0] off [1] on
MOESBHT6_RUNNING_MODE_ATTR = 0x0424  # [1] idle [0] heating
MOESBHT6_CHILD_LOCK_ATTR = 0x0128  # [0] unlocked [1] child-locked

_LOGGER = logging.getLogger(__name__)

class MoesBHT6ManufCluster(TuyaManufClusterAttributes, NoManufacturerCluster, TuyaLocalCluster):
    """Manufacturer Specific Cluster of some electric heating thermostats."""

    attributes = {
        MOESBHT6_TARGET_TEMP_ATTR: ("target_temperature", t.uint32_t, True),
        MOESBHT6_TEMPERATURE_ATTR: ("temperature", t.uint32_t, True),
        MOESBHT6_MODE_ATTR: ("system_mode", t.uint8_t, True),
        MOESBHT6_ENABLED_ATTR: ("enabled", t.uint8_t, True),
        MOESBHT6_RUNNING_MODE_ATTR: ("running_mode", t.uint8_t, True),
        MOESBHT6_CHILD_LOCK_ATTR: ("child_lock", t.uint8_t, True),
    }

    async def command(
        self,
        command_id: Union[foundation.GeneralCommand, int, t.uint8_t],
        *args,
        manufacturer: Optional[Union[int, t.uint16_t]] = None,
        expect_reply: bool = True,
        tsn: Optional[Union[int, t.uint8_t]] = None,
    ):
        """Override the default Cluster command."""

        # if manufacturer is None:
        #     manufacturer = self.endpoint.device.manufacturer

        self.debug(
            "Sending Tuya Cluster Command. Cluster Command is %x, Arguments are %s",
            command_id,
            args,
        )

        # (on, off)
        if command_id in (0x0000, 0x0001):
            cluster_data = TuyaClusterData(
                endpoint_id=self.endpoint.endpoint_id,
                cluster_name=self.ep_attribute,
                cluster_attr="enabled",
                attr_value=TUYA2ZB_COMMANDS[command_id],  # convert tuya2zigbee command
                expect_reply=expect_reply,
                manufacturer=-1,
            )
            self.endpoint.device.command_bus.listener_event(
                TUYA_MCU_COMMAND,
                cluster_data,
            )
            return foundation.GENERAL_COMMANDS[
                foundation.GeneralCommand.Default_Response
            ].schema(command_id=command_id, status=foundation.Status.SUCCESS)

    def _update_attribute(self, attrid, value):
        super()._update_attribute(attrid, value)
        if attrid == MOESBHT6_TARGET_TEMP_ATTR:
            self.endpoint.device.thermostat_bus.listener_event(
                "temperature_change",
                "occupied_heating_setpoint",
                value * 100,  # degree to centidegree
            )
        elif attrid == MOESBHT6_TEMPERATURE_ATTR:
            self.endpoint.device.thermostat_bus.listener_event(
                "temperature_change",
                "local_temperature",
                value * 10,  # decidegree to centidegree
            )
        elif attrid == MOESBHT6_MODE_ATTR:
            if value == 0:  # manual
                self.endpoint.device.thermostat_bus.listener_event(
                    "program_change", "manual"
                )
            elif value == 1:  # scheduled
                self.endpoint.device.thermostat_bus.listener_event(
                    "program_change", "scheduled"
                )
        elif attrid == MOESBHT6_ENABLED_ATTR:
            self.endpoint.device.thermostat_bus.listener_event("enabled_change", value)
        elif attrid == MOESBHT6_RUNNING_MODE_ATTR:
            self.endpoint.device.thermostat_bus.listener_event("running_change", value)
        elif attrid == MOESBHT6_CHILD_LOCK_ATTR:
            self.endpoint.device.ui_bus.listener_event("child_lock_change", value)

class MoesBHT6Thermostat(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 {MOESBHT6_TARGET_TEMP_ATTR: round(value / 100)}
        if attribute == "system_mode":
            if value == self.SystemMode.Off:
                return {MOESBHT6_ENABLED_ATTR: 0x00}
            if value == self.SystemMode.Heat:
                return {MOESBHT6_ENABLED_ATTR: 0x01}
            self.error("Unsupported value for SystemMode")
        elif attribute == "programing_oper_mode":
            if value == self.ProgrammingOperationMode.Simple:
                return {MOESBHT6_MODE_ATTR: 0}
            if value == self.ProgrammingOperationMode.Schedule_programming_mode:
                return {MOESBHT6_MODE_ATTR: 1}
            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)

    def running_change(self, value):
        """Running mode change."""
        if value == 0:
            mode = self.RunningMode.Off
        else:
            mode = self.RunningMode.Heat
        self._update_attribute(self.attributes_by_name["running_mode"].id, mode)

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

    _CHILD_LOCK_ATTR = MOESBHT6_CHILD_LOCK_ATTR

class MoesBHT6(TuyaThermostat):
    """Tuya thermostat for devices like the Moes BHT-006GBZB Electric floor heating."""

    signature = {
        MODELS_INFO: [
            ("_TZE204_aoclfnxz", "TS0601"),
        ],
        ENDPOINTS: {
            #  endpoint=1 profile=260 device_type=81 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],
            },
            242:{
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.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,
                    MoesBHT6ManufCluster,
                    MoesBHT6Thermostat,
                    MoesBHT6UserInterface,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            }
        }
    }
bsfaxi commented 1 year ago

And finally a dirty but fully working quirk. The Sensor is working from "Idle" to "Heating" and vice-versa. If someone could test it and confirm. ;)

"""Map from manufacturer to standard clusters for electric heating thermostats."""
"""Tuya MCU based thermostat."""

import logging

from zigpy.profiles import zha
import zigpy.types as t
from zigpy.zcl.clusters.general import Basic, Groups, Ota, Scenes, Time, GreenPowerProxy

from typing import Dict, Optional, Union
from zigpy.zcl import foundation
from zigpy.zcl.clusters.hvac import Thermostat

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)
from zhaquirks.tuya import (
    TuyaManufClusterAttributes,
    TuyaThermostat,
    TuyaThermostatCluster,
    TuyaUserInterfaceCluster,
    NoManufacturerCluster,
    TUYA_MCU_COMMAND,
    TuyaLocalCluster,
)

from zhaquirks.tuya.mcu import (
    DPToAttributeMapping,
    TuyaClusterData,
    TuyaMCUCluster,
)

class TuyaTC(t.enum8):
    """Tuya thermostat commands."""
    OFF = 0x00
    ON = 0x01

class ZclTC(t.enum8):
    """ZCL thermostat commands."""
    OFF = 0x00
    ON = 0x01

TUYA2ZB_COMMANDS = {
    ZclTC.OFF: TuyaTC.OFF,
    ZclTC.ON: TuyaTC.ON,
}

MOESBHT6_TARGET_TEMP_ATTR = 0x0210  # [0,0,0,21] target room temp (degree)
MOESBHT6_TEMPERATURE_ATTR = 0x0218  # [0,0,0,200] current room temp (decidegree)
MOESBHT6_MODE_ATTR = 0x0402  # [0] manual [1] scheduled
MOESBHT6_ENABLED_ATTR = 0x0101  # [0] off [1] on
MOESBHT6_RUNNING_MODE_ATTR = 0x0424  # [1] idle [0] heating
MOESBHT6_RUNNING_STATE_ATTR = 0x0424  # [1] idle [0] heating
MOESBHT6_CHILD_LOCK_ATTR = 0x0128  # [0] unlocked [1] child-locked

_LOGGER = logging.getLogger(__name__)

class MoesBHT6ManufCluster(TuyaManufClusterAttributes, NoManufacturerCluster, TuyaLocalCluster):
    """Manufacturer Specific Cluster of some electric heating thermostats."""

    attributes = {
        MOESBHT6_TARGET_TEMP_ATTR: ("target_temperature", t.uint32_t, True),
        MOESBHT6_TEMPERATURE_ATTR: ("temperature", t.uint32_t, True),
        MOESBHT6_MODE_ATTR: ("system_mode", t.uint8_t, True),
        MOESBHT6_ENABLED_ATTR: ("enabled", t.uint8_t, True),
        MOESBHT6_RUNNING_MODE_ATTR: ("running_mode", t.uint8_t, True),
        MOESBHT6_RUNNING_STATE_ATTR: ("running_state", t.uint8_t, True),
        MOESBHT6_CHILD_LOCK_ATTR: ("child_lock", t.uint8_t, True),
    }

    async def command(
        self,
        command_id: Union[foundation.GeneralCommand, int, t.uint8_t],
        *args,
        manufacturer: Optional[Union[int, t.uint16_t]] = None,
        expect_reply: bool = True,
        tsn: Optional[Union[int, t.uint8_t]] = None,
    ):
        """Override the default Cluster command."""

        # if manufacturer is None:
        #     manufacturer = self.endpoint.device.manufacturer

        self.debug(
            "Sending Tuya Cluster Command. Cluster Command is %x, Arguments are %s",
            command_id,
            args,
        )

        # (on, off)
        if command_id in (0x0000, 0x0001):
            cluster_data = TuyaClusterData(
                endpoint_id=self.endpoint.endpoint_id,
                cluster_name=self.ep_attribute,
                cluster_attr="enabled",
                attr_value=TUYA2ZB_COMMANDS[command_id],  # convert tuya2zigbee command
                expect_reply=expect_reply,
                manufacturer=-1,
            )
            self.endpoint.device.command_bus.listener_event(
                TUYA_MCU_COMMAND,
                cluster_data,
            )
            return foundation.GENERAL_COMMANDS[
                foundation.GeneralCommand.Default_Response
            ].schema(command_id=command_id, status=foundation.Status.SUCCESS)

    def _update_attribute(self, attrid, value):
        super()._update_attribute(attrid, value)
        if attrid == MOESBHT6_TARGET_TEMP_ATTR:
            self.endpoint.device.thermostat_bus.listener_event(
                "temperature_change",
                "occupied_heating_setpoint",
                value * 100,  # degree to centidegree
            )
        elif attrid == MOESBHT6_TEMPERATURE_ATTR:
            self.endpoint.device.thermostat_bus.listener_event(
                "temperature_change",
                "local_temperature",
                value * 10,  # decidegree to centidegree
            )
        elif attrid == MOESBHT6_MODE_ATTR:
            if value == 0:  # manual
                self.endpoint.device.thermostat_bus.listener_event(
                    "program_change", "manual"
                )
            elif value == 1:  # scheduled
                self.endpoint.device.thermostat_bus.listener_event(
                    "program_change", "scheduled"
                )
        elif attrid == MOESBHT6_ENABLED_ATTR:
            self.endpoint.device.thermostat_bus.listener_event("enabled_change", value)
        elif attrid == MOESBHT6_RUNNING_MODE_ATTR:
            self.endpoint.device.thermostat_bus.listener_event("running_change", value)
        elif attrid == MOESBHT6_RUNNING_STATE_ATTR:
            self.endpoint.device.thermostat_bus.listener_event("running_change", value)
        elif attrid == MOESBHT6_CHILD_LOCK_ATTR:
            self.endpoint.device.ui_bus.listener_event("child_lock_change", value)

class MoesBHT6Thermostat(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 {MOESBHT6_TARGET_TEMP_ATTR: round(value / 100)}
        if attribute == "system_mode":
            if value == self.SystemMode.Off:
                return {MOESBHT6_ENABLED_ATTR: 0}
            if value == self.SystemMode.Heat:
                return {MOESBHT6_ENABLED_ATTR: 1}
            self.error("Unsupported value for SystemMode")
        elif attribute == "programing_oper_mode":
            if value == self.ProgrammingOperationMode.Simple:
                return {MOESBHT6_MODE_ATTR: 0}
            if value == self.ProgrammingOperationMode.Schedule_programming_mode:
                return {MOESBHT6_MODE_ATTR: 1}
            self.error("Unsupported value for ProgrammingOperationMode")
        elif attribute == "running_state":
            if value == self.RunningState.Idle:
                return {MOESBHT6_RUNNING_STATE_ATTR: 1}
            if value == self.RunningState.Heat_State_On:
                return {MOESBHT6_RUNNING_STATE_ATTR: 0}
            self.error("Unsupported value for RunningState")
        elif attribute == "running_mode":
            if value == self.RunningMode.Off:
                return {MOESBHT6_RUNNING_MODE_ATTR: 1}
            if value == self.RunningMode.Heat:
                return {MOESBHT6_RUNNING_MODE_ATTR: 0}
            self.error("Unsupported value for RunningMode")

        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)

    def running_change(self, value):
        """Running state change."""
        if value == 0:
            mode = self.RunningMode.Heat
            state = self.RunningState.Heat_State_On
        else:
            mode = self.RunningMode.Off
            state = self.RunningState.Idle
        self._update_attribute(self.attributes_by_name["running_mode"].id, mode)
        self._update_attribute(self.attributes_by_name["running_state"].id, state)

class MoesBHT6UserInterface(TuyaUserInterfaceCluster):
    """HVAC User interface cluster for tuya electric heating thermostats."""
    _CHILD_LOCK_ATTR = MOESBHT6_CHILD_LOCK_ATTR

class MoesBHT6(TuyaThermostat):
    """Tuya thermostat for devices like the Moes BHT-006GBZB Electric floor heating."""

    signature = {
        MODELS_INFO: [
            ("_TZE204_aoclfnxz", "TS0601"),
        ],
        ENDPOINTS: {
            #  endpoint=1 profile=260 device_type=81 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],
            },
            242:{
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.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,
                    MoesBHT6ManufCluster,
                    MoesBHT6Thermostat,
                    MoesBHT6UserInterface,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            }
        }
    }
alexlinno commented 1 year ago

Thank you for all your efforts! Excellent work. I can now, switch it on and off, and set the target temp

mlouaze commented 1 year ago

Congrats, I think I will be staying with z2m since it was a nightmare to migrate and now everything is working pretty well, but thanks a lot for your work!

douglascrc-git commented 1 year ago

And finally a dirty but fully working quirk. The Sensor is working from "Idle" to "Heating" and vice-versa. If someone could test it and confirm. ;)

"""Map from manufacturer to standard clusters for electric heating thermostats."""
"""Tuya MCU based thermostat."""

import logging

from zigpy.profiles import zha
import zigpy.types as t
from zigpy.zcl.clusters.general import Basic, Groups, Ota, Scenes, Time, GreenPowerProxy

from typing import Dict, Optional, Union
from zigpy.zcl import foundation
from zigpy.zcl.clusters.hvac import Thermostat

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)
from zhaquirks.tuya import (
    TuyaManufClusterAttributes,
    TuyaThermostat,
    TuyaThermostatCluster,
    TuyaUserInterfaceCluster,
    NoManufacturerCluster,
    TUYA_MCU_COMMAND,
    TuyaLocalCluster,
)

from zhaquirks.tuya.mcu import (
    DPToAttributeMapping,
    TuyaClusterData,
    TuyaMCUCluster,
)

class TuyaTC(t.enum8):
    """Tuya thermostat commands."""
    OFF = 0x00
    ON = 0x01

class ZclTC(t.enum8):
    """ZCL thermostat commands."""
    OFF = 0x00
    ON = 0x01

TUYA2ZB_COMMANDS = {
    ZclTC.OFF: TuyaTC.OFF,
    ZclTC.ON: TuyaTC.ON,
}

MOESBHT6_TARGET_TEMP_ATTR = 0x0210  # [0,0,0,21] target room temp (degree)
MOESBHT6_TEMPERATURE_ATTR = 0x0218  # [0,0,0,200] current room temp (decidegree)
MOESBHT6_MODE_ATTR = 0x0402  # [0] manual [1] scheduled
MOESBHT6_ENABLED_ATTR = 0x0101  # [0] off [1] on
MOESBHT6_RUNNING_MODE_ATTR = 0x0424  # [1] idle [0] heating
MOESBHT6_RUNNING_STATE_ATTR = 0x0424  # [1] idle [0] heating
MOESBHT6_CHILD_LOCK_ATTR = 0x0128  # [0] unlocked [1] child-locked

_LOGGER = logging.getLogger(__name__)

class MoesBHT6ManufCluster(TuyaManufClusterAttributes, NoManufacturerCluster, TuyaLocalCluster):
    """Manufacturer Specific Cluster of some electric heating thermostats."""

    attributes = {
        MOESBHT6_TARGET_TEMP_ATTR: ("target_temperature", t.uint32_t, True),
        MOESBHT6_TEMPERATURE_ATTR: ("temperature", t.uint32_t, True),
        MOESBHT6_MODE_ATTR: ("system_mode", t.uint8_t, True),
        MOESBHT6_ENABLED_ATTR: ("enabled", t.uint8_t, True),
        MOESBHT6_RUNNING_MODE_ATTR: ("running_mode", t.uint8_t, True),
        MOESBHT6_RUNNING_STATE_ATTR: ("running_state", t.uint8_t, True),
        MOESBHT6_CHILD_LOCK_ATTR: ("child_lock", t.uint8_t, True),
    }

    async def command(
        self,
        command_id: Union[foundation.GeneralCommand, int, t.uint8_t],
        *args,
        manufacturer: Optional[Union[int, t.uint16_t]] = None,
        expect_reply: bool = True,
        tsn: Optional[Union[int, t.uint8_t]] = None,
    ):
        """Override the default Cluster command."""

        # if manufacturer is None:
        #     manufacturer = self.endpoint.device.manufacturer

        self.debug(
            "Sending Tuya Cluster Command. Cluster Command is %x, Arguments are %s",
            command_id,
            args,
        )

        # (on, off)
        if command_id in (0x0000, 0x0001):
            cluster_data = TuyaClusterData(
                endpoint_id=self.endpoint.endpoint_id,
                cluster_name=self.ep_attribute,
                cluster_attr="enabled",
                attr_value=TUYA2ZB_COMMANDS[command_id],  # convert tuya2zigbee command
                expect_reply=expect_reply,
                manufacturer=-1,
            )
            self.endpoint.device.command_bus.listener_event(
                TUYA_MCU_COMMAND,
                cluster_data,
            )
            return foundation.GENERAL_COMMANDS[
                foundation.GeneralCommand.Default_Response
            ].schema(command_id=command_id, status=foundation.Status.SUCCESS)

    def _update_attribute(self, attrid, value):
        super()._update_attribute(attrid, value)
        if attrid == MOESBHT6_TARGET_TEMP_ATTR:
            self.endpoint.device.thermostat_bus.listener_event(
                "temperature_change",
                "occupied_heating_setpoint",
                value * 100,  # degree to centidegree
            )
        elif attrid == MOESBHT6_TEMPERATURE_ATTR:
            self.endpoint.device.thermostat_bus.listener_event(
                "temperature_change",
                "local_temperature",
                value * 10,  # decidegree to centidegree
            )
        elif attrid == MOESBHT6_MODE_ATTR:
            if value == 0:  # manual
                self.endpoint.device.thermostat_bus.listener_event(
                    "program_change", "manual"
                )
            elif value == 1:  # scheduled
                self.endpoint.device.thermostat_bus.listener_event(
                    "program_change", "scheduled"
                )
        elif attrid == MOESBHT6_ENABLED_ATTR:
            self.endpoint.device.thermostat_bus.listener_event("enabled_change", value)
        elif attrid == MOESBHT6_RUNNING_MODE_ATTR:
            self.endpoint.device.thermostat_bus.listener_event("running_change", value)
        elif attrid == MOESBHT6_RUNNING_STATE_ATTR:
            self.endpoint.device.thermostat_bus.listener_event("running_change", value)
        elif attrid == MOESBHT6_CHILD_LOCK_ATTR:
            self.endpoint.device.ui_bus.listener_event("child_lock_change", value)

class MoesBHT6Thermostat(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 {MOESBHT6_TARGET_TEMP_ATTR: round(value / 100)}
        if attribute == "system_mode":
            if value == self.SystemMode.Off:
                return {MOESBHT6_ENABLED_ATTR: 0}
            if value == self.SystemMode.Heat:
                return {MOESBHT6_ENABLED_ATTR: 1}
            self.error("Unsupported value for SystemMode")
        elif attribute == "programing_oper_mode":
            if value == self.ProgrammingOperationMode.Simple:
                return {MOESBHT6_MODE_ATTR: 0}
            if value == self.ProgrammingOperationMode.Schedule_programming_mode:
                return {MOESBHT6_MODE_ATTR: 1}
            self.error("Unsupported value for ProgrammingOperationMode")
        elif attribute == "running_state":
            if value == self.RunningState.Idle:
                return {MOESBHT6_RUNNING_STATE_ATTR: 1}
            if value == self.RunningState.Heat_State_On:
                return {MOESBHT6_RUNNING_STATE_ATTR: 0}
            self.error("Unsupported value for RunningState")
        elif attribute == "running_mode":
            if value == self.RunningMode.Off:
                return {MOESBHT6_RUNNING_MODE_ATTR: 1}
            if value == self.RunningMode.Heat:
                return {MOESBHT6_RUNNING_MODE_ATTR: 0}
            self.error("Unsupported value for RunningMode")

        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)

    def running_change(self, value):
        """Running state change."""
        if value == 0:
            mode = self.RunningMode.Heat
            state = self.RunningState.Heat_State_On
        else:
            mode = self.RunningMode.Off
            state = self.RunningState.Idle
        self._update_attribute(self.attributes_by_name["running_mode"].id, mode)
        self._update_attribute(self.attributes_by_name["running_state"].id, state)

class MoesBHT6UserInterface(TuyaUserInterfaceCluster):
    """HVAC User interface cluster for tuya electric heating thermostats."""
    _CHILD_LOCK_ATTR = MOESBHT6_CHILD_LOCK_ATTR

class MoesBHT6(TuyaThermostat):
    """Tuya thermostat for devices like the Moes BHT-006GBZB Electric floor heating."""

    signature = {
        MODELS_INFO: [
            ("_TZE204_aoclfnxz", "TS0601"),
        ],
        ENDPOINTS: {
          #  endpoint=1 profile=260 device_type=81 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],
            },
          242:{
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.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,
                    MoesBHT6ManufCluster,
                    MoesBHT6Thermostat,
                    MoesBHT6UserInterface,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
          }
        }
    }

Do you know if it works for cooling?

douglascrc-git commented 1 year ago

Problem description

I bought this Moes Thermostat to control an electric underfloor heating system : https://fr.aliexpress.com/item/1005004996552864.html The device deosn't show ANY entity ! The existing quirk is only for TZE200 that has one Endpoint only. This one has 2.

Solution description

Is it possible to adapt the existing tuya quirk "ts0601_electric_heating.py" to include this model ?

Screenshots/Video

Screenshots/Video

Device signature

Device signature

Diagnostic information

Diagnostic information

Logs

Logs

Custom quirk

Custom quirk

Additional information

No response

I can't figure how to install the custom Quirk.

So far, I have created a folder /config/quirks and added this code in config.yaml:

zha:
  enable_quirks: true
  custom_quirks_path: /config/quirks            
s    

Nevertheless, I don't know which name the quirk has to have.

bsfaxi commented 1 year ago

Hello @douglascrc-git, The thermostat I bought has only "heating" mode, not "cooling". To try the custom quirk, you did the right configuration. Please add the signature of your device at the end if it's different from mine! Then restart your HA to take it into account. The name of the file can be whatever you want, and should only end with ".py". make sure to delete the "pycache" subdirectory under "quirks" before restarting.

douglascrc-git commented 1 year ago

Hello @douglascrc-git, The thermostat I bought has only "heating" mode, not "cooling". To try the custom quirk, you did the right configuration. Please add the signature of your device at the end if it's different from mine! Then restart your HA to take it into account. The name of the file can be whatever you want, and should only end with ".py". make sure to delete the "pycache" subdirectory under "quirks" before restarting.

Thankyou! Really appreaciate all the work you have done!

I just realice that your device is similar to my thermostat, however, mine's is dual (both heat, cool and fan). Also it can control a fan coil speed (Has 3 outputs for 3 speeds and two outpust for 2 valves [heat/coo]).

In the other hand, your quirk worked correctly for heat operation, iddle and off. But still need the cooling operation and the control of the fan coil speed.

I will try to modify the quirk your published in order to make it work for my device.

bsfaxi commented 1 year ago

I'm interested by your thermostat that does both cooling and heating. Could you please send me the link where you bought it? Thanks.

Deub31 commented 1 year ago

Thank you so much for your work! It works well. However, several functions are missing, such as weekly programming, manual or automatic mode, ... I saw that all these functions are available recently on zigbee2mqtt. How can they also be available on ZHA? Is there a way of recovering the work already done on zigbee2mqtt and integrating it into ZHA? Thanks for any help!