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
683 stars 633 forks source link

[Device Support Request] 4-pipe thermostat for fan coil (heat/cool) Tuya _TZE204_mpbki2zm #2529

Open douglascrc-git opened 10 months ago

douglascrc-git commented 10 months ago

Problem description

I have problems with the BAC-006 series zigbee thermostat "TZE204_mpbki2zm", wich is an 4 pipe thermostat for both cool & heat. Also it has fan coil speed control (auto, low, middle and high).

Once added via ZHA, Home Assistant does not recognize any entities and therefore I cannot use the device.

I have tried various ways but so far I have not been able to fix this compatibility issue. Also, there are a lot of user asking for the same request with the same result: failure. Any "quirk" barely work.

Thank you for your time! I appreciate any help.

https://vi.aliexpress.com/item/1005005650680212.html?spm=a2g0o.detail.1000013.4.1eeajGnJjGnJJ5&gps-id=pcDetailBottomMoreThisSeller&scm=1007.13339.291025.0&scm_id=1007.13339.291025.0&scm-url=1007.13339.291025.0&pvid=7a7fae38-688e-451f-99e8-43741f211647&_t=gps-id:pcDetailBottomMoreThisSeller,scm-url:1007.13339.291025.0,pvid:7a7fae38-688e-451f-99e8-43741f211647,tpp_buckets:668%232846%238110%231995&isseo=y&pdp_npi=4%40dis%21EUR%2168.41%2132.15%21%21%2172.10%21%21%40211b423c16929636189772973e53d3%2112000033886533029%21rec%21ES%21%21A

Solution description

I'd like to have the device working properly as it does from the Tuya app, reading/setting features as current temperature, set point,fan-coil speed, working mode (heat, cool, iddle).

Screenshots/Video

image

Screenshots/Video [Paste/upload your media here]

Device signature

{ "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.FullFunctionDevice|MainsPowered|RxOnWhenIdle|AllocateAddress: 142>, manufacturer_code=4417, maximum_buffer_size=66, maximum_incoming_transfer_size=66, server_mask=10752, maximum_outgoing_transfer_size=66, descriptor_capability_field=<DescriptorCapability.NONE: 0>, 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_mpbki2zm", "model": "TS0601", "class": "zigpy.device.Device" }

Diagnostic information

{ "home_assistant": { "installation_type": "Home Assistant OS", "version": "2023.7.3", "dev": false, "hassio": true, "virtualenv": false, "python_version": "3.11.4", "docker": true, "arch": "aarch64", "timezone": "Europe/Madrid", "os_name": "Linux", "os_version": "6.1.21-v8", "supervisor": "2023.08.1", "host_os": "Home Assistant OS 10.3", "docker_version": "23.0.6", "chassis": "embedded", "run_as_root": true }, "custom_components": { "dual_smart_thermostat": { "version": "0.5.5", "requirements": [] } }, "integration_manifest": { "domain": "zha", "name": "Zigbee Home Automation", "after_dependencies": [ "onboarding", "usb" ], "codeowners": [ "@dmulcahey", "@adminiuga", "@puddly" ], "config_flow": true, "dependencies": [ "file_upload" ], "documentation": "https://www.home-assistant.io/integrations/zha", "iot_class": "local_polling", "loggers": [ "aiosqlite", "bellows", "crccheck", "pure_pcapy3", "zhaquirks", "zigpy", "zigpy_deconz", "zigpy_xbee", "zigpy_zigate", "zigpy_znp" ], "requirements": [ "bellows==0.35.8", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.101", "zigpy-deconz==0.21.0", "zigpy==0.56.2", "zigpy-xbee==0.18.1", "zigpy-zigate==0.11.0", "zigpy-znp==0.11.3" ], "usb": [ { "vid": "10C4", "pid": "EA60", "description": "2652", "known_devices": [ "slae.sh cc2652rb stick" ] }, { "vid": "1A86", "pid": "55D4", "description": "sonoffplus", "known_devices": [ "sonoff zigbee dongle plus v2" ] }, { "vid": "10C4", "pid": "EA60", "description": "sonoffplus", "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": 13008, "manufacturer": "_TZE204_mpbki2zm", "model": "TS0601", "name": "_TZE204_mpbki2zm TS0601", "quirk_applied": false, "quirk_class": "zigpy.device.Device", "manufacturer_code": 4417, "power_source": "Mains", "lqi": 109, "rssi": null, "last_seen": "2023-08-17T12:17:25", "available": true, "device_type": "Router", "signature": { "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.FullFunctionDevice|MainsPowered|RxOnWhenIdle|AllocateAddress: 142>, manufacturer_code=4417, maximum_buffer_size=66, maximum_incoming_transfer_size=66, server_mask=10752, maximum_outgoing_transfer_size=66, descriptor_capability_field=<DescriptorCapability.NONE: 0>, 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_mpbki2zm", "model": "TS0601" }, "active_coordinator": false, "entities": [], "neighbors": [], "routes": [], "endpoint_names": [ { "name": "SMART_PLUG" }, { "name": "PROXY_BASIC" } ], "user_given_name": null, "device_reg_id": "24abbae2ce990ee22b7b6a9d2c6af369", "area_id": null, "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_mpbki2zm" }, "0x0005": { "attribute_name": "model", "value": "TS0601" } }, "unsupported_attributes": {} } }, "out_clusters": { "0x0019": { "endpoint_attribute": "ota", "attributes": {}, "unsupported_attributes": {} }, "0x000a": { "endpoint_attribute": "time", "attributes": {}, "unsupported_attributes": {} } } }, "242": { "device_type": { "name": "PROXY_BASIC", "id": 97 }, "profile_id": 41440, "in_clusters": {}, "out_clusters": { "0x0021": { "endpoint_attribute": "green_power", "attributes": {}, "unsupported_attributes": {} } } } } } }

Logs

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

Custom quirk

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

Additional information

image PXL_20230817_103441696 MP PXL_20230817_103429025 MP PXL_20230817_103414805 MP

douglascrc-git commented 10 months ago

I found a parcial solution, using the next quirk:

EDIT: this quirk works only for heating! It does not detect when the device is set in cooling or fan mod.

"""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_mpbki2zm", "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],
            }
        }
    }

Now, the device shows some entities as hvac_action and mode (heat and off) , but not all of them.

This is how it looks now:

image

I can't change the set point, fan speed or the mode to cool/fan.

douglascrc-git commented 10 months ago

I had some issues with the temperature set-point. While the device screen displays for instance 20ºC, Home assistant saved that value as 200ºC.

I did a little modification to the quirk in order to solve that issue.

This is the updated quirk:

"""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 * 10,  # 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 / 10)}
        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_mpbki2zm", "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],
            }
        }
    }

Nevertheless, still does not support de cool mode (only heating) 😢

douglascrc-git commented 10 months ago

I'v found in Z2MQTT a device that essentialy has the same behaviour: https://www.zigbee2mqtt.io/devices/PCT504.html

douglascrc-git commented 10 months ago

From the tuya iot platform, I found this information about the device standar instruction set

switch | Boolean | "{true,false}" -- | -- | -- mode | Enum | { "range": [ "cold", "hot", "wind" ] } eco | Boolean | "{true,false}" temp_set | Integer | { "unit": "℃", "min": 50, "max": 350, "scale": 1, "step": 5 } upper_temp_f | Integer | { "unit": "℃", "min": 150, "max": 350, "scale": 1, "step": 10 } upper_temp | Integer | { "unit": "℃", "min": 150, "max": 350, "scale": 1, "step": 10 } lower_temp_f | Integer | { "unit": "℃", "min": 50, "max": 150, "scale": 1, "step": 10 } lower_temp | Integer | { "unit": "℃", "min": 50, "max": 150, "scale": 1, "step": 10 } temp_correction | Integer | { "unit": "°C", "min": -9, "max": 9, "scale": 0, "step": 1 } level | Enum | { "range": [ "low", "middle", "high", "auto" ] } child_lock | Boolean | "{true,false}"
douglascrc-git commented 10 months ago

I installed the Tuya integration & added the thermostat through it: image

Now, I wonder, is it possible to see the code use by the integration and create a quirk?

prsveloso commented 10 months ago

I have the same thermostat. Wishing for a solution too.

douglascrc-git commented 10 months ago

I have the same thermostat. Wishing for a solution too.

Hello, I did this quirk.

It works at 95% but there is a little issue when you turn off the device. For any reason, the screen indeed turn off, but the device continues working in cool/heat ignoring the off instruction.

I hope it works for you too : )

The quirk:


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

import logging
import asyncio
from typing import Optional, Union

from zigpy.profiles import zha
import zigpy.types as t
from zigpy.zcl.clusters.general import Basic, Groups, Ota, Scenes, Time, GreenPowerProxy
from zigpy.zcl.clusters.hvac import Thermostat, Fan
from zigpy.quirks import CustomDevice
from typing import Dict, Optional, Union
from zigpy.zcl import foundation
from zigpy.zcl.clusters.hvac import Thermostat
from zhaquirks import LocalDataCluster, Bus

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_ENABLED_ATTR = 0x0101  # [0] off [1] on
# MOESBHT6_MODE_ATTR = 0x0402  # [0] manual [1] scheduled
# MOESBHT6_CHILD_LOCK_ATTR = 0x0128  # [0] unlocked [1] child-locked
# MOESBHT6_RUNNING_MODE_ATTR = 0x0424  # 1[] idle [0] heating
# MOESBHT6_RUNNING_STATE_ATTR = 0x0424  # [1] idle [0] heating

TUYA_FANCOIL_TARGET_TEMP_ATTR = 0x0210  # [0,0,0,21] target room temp (degree)
TUYA_FANCOIL_TEMPERATURE_ATTR = 0x0218  # [0,0,0,200] current room temp (decidegree)
TUYA_FANCOIL_ENABLED_ATTR = 0x0101  # [0] off [1] on
TUYA_FANCOIL_RUNNING_MODE_ATTR = 0x0402  # [0] cooling [1] heating [2] off
TUYA_FANCOIL_RUNNING_STATE_ATTR = 0x0402  # [0] cooling [1] heating [2] off
TUYA_FANCOIL_CHILD_LOCK_ATTR = 0x0128  # [0] unlocked [1] child-locked
TUYA_FANCOIL_FAN_MODE_ATTR = 0x041c# [0] manual [1] scheduled

_LOGGER = logging.getLogger(__name__)

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

    attributes = {
        TUYA_FANCOIL_TARGET_TEMP_ATTR: ("target_temperature", t.uint32_t, True),
        TUYA_FANCOIL_TEMPERATURE_ATTR: ("temperature", t.uint32_t, True),
        TUYA_FANCOIL_ENABLED_ATTR: ("enabled", t.uint8_t, True),
        TUYA_FANCOIL_RUNNING_MODE_ATTR: ("running_mode", t.uint8_t, True),
        TUYA_FANCOIL_RUNNING_STATE_ATTR: ("running_state", t.uint8_t, True),
        TUYA_FANCOIL_CHILD_LOCK_ATTR: ("child_lock", t.uint8_t, True),
        TUYA_FANCOIL_FAN_MODE_ATTR: ("fan_mode", t.uint8_t, True),
    }

    async def on_enabled_change(self, value):
        _LOGGER.debug("Reading running_mode from device")
        running_mode_id = self.attributes_by_name["running_mode"].id
        success, _ = await self.read_attributes((running_mode_id,), manufacturer=None)
        current_running_mode = success[running_mode_id]
        _LOGGER.debug("Current running_mode from device: " + str(current_running_mode))
        self.endpoint.device.thermostat_bus.listener_event("enabled_change", value, current_running_mode)

    def _update_attribute(self, attrid, value):
        super()._update_attribute(attrid, value)
        if attrid == TUYA_FANCOIL_TARGET_TEMP_ATTR:
            self.endpoint.device.thermostat_bus.listener_event(
                "temperature_change",
                "occupied_heating_setpoint",
                value * 10,  # degree to centidegree
            )
            self.endpoint.device.thermostat_bus.listener_event(
                "temperature_change",
                "occupied_cooling_setpoint",
                value * 10,  # degree to centidegree
            )
        elif attrid == TUYA_FANCOIL_TEMPERATURE_ATTR:
            self.endpoint.device.thermostat_bus.listener_event(
                "temperature_change",
                "local_temperature",
                value * 10,  # decidegree to centidegree
            )
        elif attrid == TUYA_FANCOIL_FAN_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 == TUYA_FANCOIL_ENABLED_ATTR:
            """Needs to get running_mode async in order to set the correct state when device is enabled""" 
            #asyncio.create_task(self.on_enabled_change(value))
            self.endpoint.device.thermostat_bus.listener_event("enabled_change", value)
        elif attrid == TUYA_FANCOIL_RUNNING_MODE_ATTR:
            self.endpoint.device.thermostat_bus.listener_event("state_change", value)
        elif attrid == TUYA_FANCOIL_RUNNING_STATE_ATTR:
            self.endpoint.device.thermostat_bus.listener_event("running_change", value)    
        elif attrid == TUYA_FANCOIL_CHILD_LOCK_ATTR:
            self.endpoint.device.ui_bus.listener_event("child_lock_change", value)
        elif attrid == TUYA_FANCOIL_FAN_MODE_ATTR:
            self.endpoint.device.fan_bus.listener_event("fan_mode_change", value)

class TuyaFancoilFanCluster(LocalDataCluster, Fan):
    _CONSTANT_ATTRIBUTES = {0x0001: Fan.FanModeSequence.Low_Med_High_Auto}

    def __init__(self, *args, **kwargs):
        """Init."""
        super().__init__(*args, **kwargs)
        self.endpoint.device.fan_bus.add_listener(self)

    def fan_mode_change(self, value):
        if value == 0:
            self._update_attribute(self.attributes_by_name["fan_mode"].id, self.FanMode.Low)
        elif value == 1:
            self._update_attribute(self.attributes_by_name["fan_mode"].id, self.FanMode.Medium)
        elif value == 2:
            self._update_attribute(self.attributes_by_name["fan_mode"].id, self.FanMode.High)
        elif value == 3:
            self._update_attribute(self.attributes_by_name["fan_mode"].id, self.FanMode.Auto)

class TuyaFancoilThermostatCluster(TuyaThermostatCluster):
    """Thermostat cluster for Tuya fancoil thermostats."""

    _CONSTANT_ATTRIBUTES = {
        0x001B: Thermostat.ControlSequenceOfOperation.Cooling_and_Heating,
    }

    def state_change(self, value):
        """State update from device."""
        if value == 0:
            mode = self.RunningMode.Cool
            state = self.RunningState.Cool_State_On
            system_mode = self.SystemMode.Cool
        elif value == 1:
            mode = self.RunningMode.Heat
            state = self.RunningState.Heat_State_On
            system_mode = self.SystemMode.Heat
        elif value == 2:
            mode = self.RunningMode.Off
            state = self.RunningState.Idle
            system_mode = self.SystemMode.Fan_only
        if mode is not None:
            self._update_attribute(self.attributes_by_name["running_mode"].id, mode)
        if state is not None:
            self._update_attribute(self.attributes_by_name["running_state"].id, state)
        if system_mode is not None:
            self._update_attribute(self.attributes_by_name["system_mode"].id, system_mode)

    # pylint: disable=W0236
    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,
    ):
        """Implement thermostat commands."""

        if command_id != 0x0000:
            return foundation.GENERAL_COMMANDS[
                foundation.GeneralCommand.Default_Response
            ].schema(
                command_id=command_id, status=foundation.Status.UNSUP_CLUSTER_COMMAND
            )

        mode, offset = args
        if mode not in (self.SetpointMode.Heat, self.SetpointMode.Cool, self.SetpointMode.Both):
            return foundation.GENERAL_COMMANDS[
                foundation.GeneralCommand.Default_Response
            ].schema(command_id=command_id, status=foundation.Status.INVALID_VALUE)

        heating_attrid = self.attributes_by_name["occupied_heating_setpoint"].id
        cooling_attrid = self.attributes_by_name["occupied_cooling_setpoint"].id

        success, _ = await self.read_attributes((heating_attrid, cooling_attrid), manufacturer=manufacturer)
        try:
            current_heat = success[heating_attrid]
            current_cool = success[cooling_attrid]
        except KeyError:
            return foundation.Status.FAILURE

        # offset is given in decidegrees, see Zigbee cluster specification
        (res,) = await self.write_attributes(
            {"occupied_heating_setpoint": current_heat + offset * 10,
             "occupied_cooling_setpoint": current_cool + offset * 10},
            manufacturer=manufacturer,
        )
        return foundation.GENERAL_COMMANDS[
            foundation.GeneralCommand.Default_Response
        ].schema(command_id=command_id, status=res[0].status)

class TuyaFancoilThermostat(TuyaFancoilThermostatCluster):
    """Thermostat cluster for Tuya fancoil."""

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

        if attribute == "occupied_heating_setpoint" or attribute == "occupied_cooling_setpoint":
            # centidegree to degree
            return {TUYA_FANCOIL_TARGET_TEMP_ATTR: round(value / 10)}
        if attribute == "system_mode":
            if value == self.SystemMode.Off:
                return {TUYA_FANCOIL_ENABLED_ATTR: 0}
            if value == self.SystemMode.Cool:
                return {TUYA_FANCOIL_ENABLED_ATTR: 1, TUYA_FANCOIL_RUNNING_MODE_ATTR: 0}
            if value == self.SystemMode.Heat:
                return {TUYA_FANCOIL_ENABLED_ATTR: 1, TUYA_FANCOIL_RUNNING_MODE_ATTR: 1}
            self.error("Unsupported value for SystemMode " + str(value))
        elif attribute == "programing_oper_mode":
            if value == self.ProgrammingOperationMode.Simple:
                return {TUYA_FANCOIL_FAN_MODE_ATTR: 0}
            if value == self.ProgrammingOperationMode.Schedule_programming_mode:
                return {TUYA_FANCOIL_FAN_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, running_mode):
        """System mode change."""
        if value == 0:
            self._update_attribute(self.attributes_by_name["system_mode"].id, self.SystemMode.Off)
            self._update_attribute(self.attributes_by_name["running_mode"].id, self.RunningMode.Off)
            self._update_attribute(self.attributes_by_name["running_state"].id, self.RunningState.Idle)
        else:
            if running_mode == 0:
                self._update_attribute(self.attributes_by_name["system_mode"].id, self.SystemMode.Cool)
                self._update_attribute(self.attributes_by_name["running_mode"].id, self.RunningMode.Cool)
                self._update_attribute(self.attributes_by_name["running_state"].id, self.RunningState.Cool_State_On)
            elif running_mode == 1:
                self._update_attribute(self.attributes_by_name["system_mode"].id, self.SystemMode.Heat)
                self._update_attribute(self.attributes_by_name["running_mode"].id, self.RunningMode.Heat)
                self._update_attribute(self.attributes_by_name["running_state"].id, self.RunningState.Heat_State_On)
            elif running_mode == 2:
                self._update_attribute(self.attributes_by_name["system_mode"].id, self.SystemMode.Fan_only)
                self._update_attribute(self.attributes_by_name["running_mode"].id, self.RunningMode.Off)
                self._update_attribute(self.attributes_by_name["running_state"].id, self.RunningState.Fan_State_On)

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

    _CHILD_LOCK_ATTR = TUYA_FANCOIL_CHILD_LOCK_ATTR

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

    """Generic Tuya thermostat device."""

    def __init__(self, *args, **kwargs):
        """Init device."""
        self.thermostat_bus = Bus()
        self.ui_bus = Bus()
        self.battery_bus = Bus()
        self.fan_bus = Bus()
        super().__init__(*args, **kwargs)

    signature = {
        MODELS_INFO: [
            ("_TZE204_mpbki2zm", "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,
                    TuyaFancoilThermostat,
                    TuyaFancoilUserInterface,
                    TuyaFancoilFanCluster
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            242: {
                PROFILE_ID: 41440,
                DEVICE_TYPE: 97,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            }
        }
    }
douglascrc-git commented 10 months ago

In case someone ask, I "created" the quirk posted before in base of those quirks:

  1. https://github.com/zigpy/zha-device-handlers/issues/1284#issuecomment-1684896622
  2. https://github.com/zigpy/zha-device-handlers/issues/2433#issuecomment-1689829732
prsveloso commented 10 months ago

Thank you, @douglascrc-git I'll try your quirk and try to learn something about this.

github-actions[bot] commented 4 months ago

There hasn't been any activity on this issue recently. Due to the high number of incoming GitHub notifications, we have to clean some of the old issues, as many of them have already been resolved with the latest updates. Please make sure to update to the latest version and check if that solves the issue. Let us know if that works for you by adding a comment 👍 This issue has now been marked as stale and will be closed if no further activity occurs. Thank you for your contributions.

mmatviichuk commented 1 month ago

Hello,

It looks like this device still needs to be supported. Are there any plans to add it to the standard?

Everyone is probably using either Quirk or connecting it via the Tuya Smart App.