Hope this is the right way to post this but here goes…
As mentioned by Stiltjack, I added this line into configuration.yaml:
zha:
custom_quirks_path: /config/custom_zha_quirks/
Then saved the code beow as ts0601.py in the folder
Then a restart and it seemed to work.
"""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],
}
}
}