Popular Moes thermostat not showing entities after ZHA pairing

Very fair, and thank you for the response.

Ok in light of the increasingly cold temperatures I’m going to give the solution you mentioned a go.

One last question beforehand though - I notice the file is called ‘electric_heating.py’. Can I just check that you used this for gas boiler control? I ask because I know there are 3 variants of this thermostat: Gas Boilers, underfloor heating, and water heating.

A thousand thanks in advance for your help!

First follow tips here → Guide for Zigbee interference avoidance and network range/coverage optimization

If still have issues pairing then submit a device support request or bug report issue, see → https://www.home-assistant.io/integrations/zha#how-to-add-support-for-new-and-unsupported-devices

Thanks very much Hedda!

I did so and have raised this request: [Device Support Request] · Issue #2755 · zigpy/zha-device-handlers · GitHub

The Device Info card in your screenshot suggests that no “quirk” has been applied. It would normally appear at the bottom just above the Zigbee logo:

This is the case even when the quirk has been built in to ZHA - I’m sure yours will be eventually, but there are quite a lot of them, and new ones being requested all the time. In the meantime you need to add it as a custom quirk. Looks like @slickers has found one, so there’s no need to request another.

To install a custom quirk:

  • Download it (there is a download button just above the GitHub code, top right).
  • Create a folder in the config directory of HA (you can call it anything you like)
  • Copy the downloaded file to this folder
  • In configuration.yaml add:
zha:
  custom_quirks_path: /config/your folder name/
  • Restart HA
  • If the device is already paired, you may need to reconfigure it

Bear in mind that these device handlers are needed because the manufacturers are not following the zigbee specification. The developers are doing their best.

1 Like

Hmm, I don’t see Quirk in any of my ZHA devices in the Zigbee Info section?

Is it a dev tools thing?

No. Presumably all your other devices work without one.

Here’s what mine looks like. Am I allowed to drop the file in here?

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],
			}
        }
    }
2 Likes

Thanks for all the responses everyone, I’m really grateful!

Because I’m up against it with the cold temperatures atm, I’ll give the custom quirk a try. I have no idea how long the official ZHA support will take, and I can’t easily see from git how long average device support requests take.

What happens if I use the custom quirk and then official support is rolled out in a month’s time, for example. Will the custom one be overwritten?

I work in QA, so please forgive the questions :sweat_smile:

No, it works the other way around, any custom quirks that you add will overwrite the ones that ships with ZHA for that device, if any does.

Suggest that you read the whole ZHA integration documentation. Most devices do not need quirks, a quirk is only needed if a device does either not exactly follow the standard Zigbee specifications (which manufacturers should not do) or if the manufacturer is using unique custom manufactuer clusters and attributes (which a manufacturer is allowed to do to add features not in the standard Zigbee specifications), read this → https://www.home-assistant.io/integrations/zha#knowing-which-devices-are-supported as well as more info here → https://www.home-assistant.io/integrations/zha#how-to-add-support-for-new-and-unsupported-devices

Is it fair to say, though, that if a device doesn’t have a quirk and entities appear to be missing, that’s probably the reason?

Yes, if missing entities that are expected to be there then you will usually need a quirk, and that is explained in ZHA docs → https://www.home-assistant.io/integrations/zha#how-to-add-support-for-new-and-unsupported-devices but good to know that in general most Zigbee devices that do not have complicated features should normally not need quirks, and that too is also explained in ZHA docs → https://www.home-assistant.io/integrations/zha#knowing-which-devices-are-supported

1 Like

I also seem to be having the same problem with the same brand of thermostat. I added the custom quirk and i think it has been read cause HA created a new dir in my custom_quirks folder, but the quirk doesn’t seem to be loaded/linked to the thermostat. What shoul i do?

image_2023-11-19_153949461

Hi everyone,

Thanks again for all the support with this.

I added the quirk that slickers posted above and it did indeed unlock the hvac and temperature entities (see screenshots). However, having recently added a custom quirk for some radiator valves which exposed something like 10 different entities, I’m pretty sure this custom quirk hasn’t exposed anywhere like the full list of things that should be supported.

I’m also pretty confident that the thermostat entity isn’t responding correctly to my very simple automation rules, but I haven’t had a good enough opportunity to thoroughly test them yet.

Nevertheless I think for the purposes of stopping me freezing in my house, this thread can be tentatively closed.

If I find a more comprehensive solution to this particular device, I’ll come back to this thread.


I thought I’d also just share that there’s clearly something wrong with the temperature setting too lol:

Or maybe it’s just the scale of the graph showing in HA?

I’m also seeing those spikes!
Haven’t figured that bit out yet. Deosn’t appear to make much difference to the target temperature but it does make my graphs look rubish.

I am seeing the spikes as well.

As a test i have set all schedules to 00:00 with the temp being 15C.
Have set the temp manually to 5C and still see the spikes at different timestamps other than between 23:50-00:10

I have integrated that Thermostate via Z2M and i also see these sudden changes in target temperature setting and it is quite annoying…in some irregular intervals the target is set to extremely high values (that result in setting the value to the max of 30°) or to 0°

However without any intervention the value stays at those extremes…
I setup an automation that resets the values if extremes are detected, but its really annoying…

Muchas gracias !!!

Por si ayuda a otras personas, comentar que su solución funciona OK en:

  • “Home Assistant Green” + “Home Assistant SkyConnect USB Stick” (integración ZHA)
  • Y con el termostato “MOES Zigbee Smart ZHT-006-GA-WH-MS” (que uso para regular la electroválvula de 3 vías que da paso al agua caliente a mis radiadores de calefacción. En un piso con calefacción central)

Gracias a todos por su tiempo !!!

Thanks in advance , I have possibly a similar issue, using ZHA i dont see many entities the quirk seems to be loaded correctly but i dont see any additional entities. maybe temperature and state are what i should only see.
no way to change dead zone or adjust clock.
Also is it possible to deactivate the temperature sensor and use this only as a switch as i want to use secondary temp sensors.