Popular Moes thermostat not showing entities after ZHA pairing

Hi everyone,

I bought what I’m confident is a popular Moes Thermostat (Zigbee variant, for gas boiler heating).

When I add it using ZHA though, it doesn’t pair properly and no proper entities are exposed. Please see screenshot.

I’ve looked through this post and it looks very similar to the problem I’m seeing, but while the proposed solution is to submit a device support request in github, I can’t get my mind around this particular device not already having ZHA support, not least because a friend of mine has an almost identical one, and this device is almost the top result when searching smart thermostat on Amazon.

Does anyone have any ideas? I’m using Home Assistant Yellow which is fully updated, to the best of my knowledge (no updates in the Settings section).

Device info: TS0601

by _TZE204_aoclfnxz

Thank you very much in advance for any help!

2 Likes

Would be interested to see how you get on…
I’ve got mine working using the link but I see massive spikes every now and again on the target temperature (which I haven’t figure out yet…)

1 Like

Thanks! Yeah a lot of the reading I’ve done seems to suggest ways of coding around this, none of which are apparently perfect. I’m not a coder so am very wary of going down that route unless it’s absolutely necessary, especially for a device I’m almost sure there must already be support for. Device prevalence/popularity is hard to be 100% positive about though (I don’t have device sales figures, so my assertions may be totally wrong).

Side question - the amazon listing said battery needed. Did you use a battery? I didn’t, mainly as I didn’t understand why something that was hooked into the mains would need a battery, nor could I see where to put one. Just trying to rule out anything simple.

I’m no coder either but I muddled through adding a custom quirk to home assistant and it came to life after that. Wasn’t too dificult.

I didn’t see a battery option on mine but i haven’t powered it down yet either.
To be honest, I’m not that bothered about time or schedule as HA handles all of that rather than the controller.

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.