zha-device-handlers: [Device Support Request] TS0601 _TZE204_pcdmj88b TRV not showing any entities

Problem description

I bought some Zigbee TRVs, they show up in home assistant as TS0601_TZE204_pcdmj88b but although they are pairing, no entity for control or sensor reading is showing up.

Model link for reference: https://fr.aliexpress.com/item/1005006191259938.html?spm=a2g0o.productlist.main.3.2de8kzSokzSoTw&algo_pvid=fc119493-da4b-462c-86bd-2d78585444c8&algo_exp_id=fc119493-da4b-462c-86bd-2d78585444c8-1&pdp_npi=4%40dis!EUR!32.60!14.67!!!32.60!!%402103834816991401988737073e38b3!12000036203052461!sea!FR!769762047!&curPageLogUid=sWvhl7kLhrPV

I tried some custom quirks I found (for Moes or Zonnsmart TRVs) but obviously nothing good came out of it.

Solution description

I never used or debugged custom quirks before, but I’m willing to provide help if someone needs more information to create a custom quirks for this model. Thanks a lot!

Screenshots/Video

No response

Device signature

Device signature
{
  "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.EndDevice: 2>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress: 128>, 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=True, *is_full_function_device=False, *is_mains_powered=False, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False)",
  "endpoints": {
    "1": {
      "profile_id": "0x0104",
      "device_type": "0x0051",
      "input_clusters": [
        "0x0000",
        "0x0004",
        "0x0005",
        "0xef00"
      ],
      "output_clusters": [
        "0x000a",
        "0x0019"
      ]
    }
  },
  "manufacturer": "_TZE204_pcdmj88b",
  "model": "TS0601",
  "class": "zigpy.device.Device"
}

Diagnostic information

No response

Logs

No response

Custom quirk

No response

Additional information

No response

About this issue

  • Original URL
  • State: open
  • Created 8 months ago
  • Reactions: 5
  • Comments: 48

Most upvoted comments

Now reading is in better shape but i’m unable to find why i can’t change mode or temperature 😦

Code is available at : https://github.com/Teka101/zha-device-handlers/blob/support_tze204_pcdmj88b/zhaquirks/tuya/ts0601_trv_tze204.py

@Teka101 well it seems it did the trick, I have access to the calibration value now! it doesn’t appear as an entity yet though, it only works through zha interface “write attribute”, but it works! I tried to limit the range of value you cane use from -12 to +12 calibration = -12 if value < -12 else 12 if value > 12 else value but it doens’t seem to work, I don’t understand why.

Here is the converter I’m testing:

import logging
from typing import Optional, Tuple, Union

from zigpy.profiles import zha
import zigpy.types as t
from zigpy.zcl import foundation
from zigpy.zcl.clusters.general import (
    AnalogOutput,
    Basic,
    BinaryInput,
    Groups,
    OnOff,
    Ota,
    Scenes,
    Time,
)

from zhaquirks import Bus, LocalDataCluster
from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)
from zhaquirks.tuya import (
    TuyaManufClusterAttributes,
    TuyaPowerConfigurationCluster2AA,
    TuyaThermostat,
    TuyaThermostatCluster,
    TuyaUserInterfaceCluster,
)

_LOGGER = logging.getLogger(__name__)

# MQTT
# {
#   "2": "Mode",
#   "4": "Set temperature",
#   "5": "Current temperature",
#   "6": "Battery capacity",
#   "7": "Child lock",
#   "8": "Temperature scale",
#   "9": "Set temperature ceiling",
#   "10": "The lower limit of temperature",
#   "14": "Window check",
#   "16": "Window temp",
#   "17": "Window time",
#   "18": "Backlight brightness",
#   "19": "Factory data reset",
#   "21": "Holiday temperature",
#   "24": "Home temp", || comfort_temperature
#   "25": "Leave temp", || eco_temperature
#   "28": "Week program",
#   "29": "Week program Tuesday",
#   "30": "Week program Wednesday",
#   "31": "Week program Thursday",
#   "32": "Week program Friday",
#   "33": "Week program Saturday",
#   "34": "Week program Sunday",
#   "35": "Fault alarm",
#   "36": "Frost protection",
#   "37": "Rapid warming",
#   "38": "Rapid heating countdown",
#   "39": "Switch Scale",
#   "47": "Temperature correction",
#   "48": "Valve testing",
#   "49": "State of the valve",
#   "101": "111"
# }

#                                   010000 000000 = 0x400 | 1024
#                                   001000 000000 = 0x200 | 512
#                                   000000 111111 0x3F | 63
PCDM_PRESET = 1026 #                010000 000010 2
PCDM_TARGET_TEMP_ATTR = 516 #       001000 000100 4
PCDM_TEMPERATURE_ATTR = 517 #       001000 000101 5
PCDM_BATTERY_ATTR = 518 #           001000 000110 6
PCDM_CHILD_LOCK_ATTR = 263 #        000100 000111 7
PCDM_BATTERY_LOW_ATTR = 1315 #nop?  010100 100011 35
PCDM_SYSTEM_MODE_ATTR = 1073 #      010000 110001 49
PCDM_TEMPERATURE_CORRECTION_ATTR = 559  # 001000 101111 47
#
PCDM_TARGET_MANUAL_ATTR = 512+ 4
PCDM_TARGET_HOLIDAY_ATTR = 21
PCDM_TARGET_CONFORT_ATTR = 536#try 001000 011000 24
PCDM_TARGET_ECO_ATTR = 537#try   001000 011001 25 ## 35=NOP !
PCDM_BOOST_MODE = 293 #nop?         000100 100101 37

PcdmManuClusterSelf = None

class PcdmManufTrvCluster(TuyaManufClusterAttributes):
    """Manufacturer Specific Cluster of some thermostatic valves."""

    def __init__(self, *args, **kwargs):
        """Init."""
        super().__init__(*args, **kwargs)
        global PcdmManuClusterSelf
        PcdmManuClusterSelf = self

    set_time_offset = 1970

    attributes = TuyaManufClusterAttributes.attributes.copy()
    attributes.update(
        {
            PCDM_PRESET: ("operation_preset", t.uint8_t, True),
            PCDM_BATTERY_ATTR: ("battery", t.uint32_t, True),
            PCDM_BATTERY_LOW_ATTR: ("battery_low", t.uint8_t, True),
            PCDM_BOOST_MODE: ("boost_duration_seconds", t.uint32_t, True),
            PCDM_CHILD_LOCK_ATTR: ("child_lock", t.uint8_t, True),
            PCDM_SYSTEM_MODE_ATTR: ("system_mode", t.uint8_t, True),
            PCDM_TARGET_TEMP_ATTR: ("target_temperature", t.uint32_t, True),
            PCDM_TEMPERATURE_ATTR: ("temperature", t.uint32_t, True),
            PCDM_TEMPERATURE_CORRECTION_ATTR: ("temperature_correction", t.int32s, True),

            PCDM_TARGET_MANUAL_ATTR: ("occupied_heating_setpoint", t.uint32_t, True),
            PCDM_TARGET_CONFORT_ATTR: ("comfort_heating_setpoint", t.uint32_t, True),
            PCDM_TARGET_ECO_ATTR: ("eco_heating_setpoint", t.uint32_t, True),
        }
    )

    TEMPERATURE_ATTRS = {
        PCDM_TARGET_TEMP_ATTR: "occupied_heating_setpoint",
        PCDM_TARGET_CONFORT_ATTR: "comfort_heating_setpoint",
        PCDM_TARGET_ECO_ATTR: "eco_heating_setpoint",
        PCDM_TEMPERATURE_ATTR: "local_temperature",
    }
    
    def handle_cluster_request(
        self,
        hdr: foundation.ZCLHeader,
        args: Tuple,
        *,
        dst_addressing: Optional[
            Union[t.Addressing.Group, t.Addressing.IEEE, t.Addressing.NWK]
        ] = None,
    ) -> None:
        _LOGGER.debug(
            "handle_cluster_request: [0x%04x:%s:0x%04x] Received value (command 0x%04x)",
            self.endpoint.device.nwk,
            self.endpoint.endpoint_id,
            self.cluster_id,
            hdr.command_id,
        )
        _LOGGER.debug('%d # %s', len(args), str(args))
        return super().handle_cluster_request(hdr, args, dst_addressing=dst_addressing)
    
    async def write_attributes(self, attributes, manufacturer=None):
        return await super().write_attributes(attributes, manufacturer=foundation.ZCLHeader.NO_MANUFACTURER_ID)

    def _update_attribute(self, attrid, value):
        super()._update_attribute(attrid, value)
        if attrid in self.TEMPERATURE_ATTRS:
            self.endpoint.device.thermostat_bus.listener_event(
                "temperature_change",
                self.TEMPERATURE_ATTRS[attrid],
                value * 10,  # decidegree to centidegree
            )
        elif attrid == PCDM_BATTERY_ATTR:
            self.endpoint.device.battery_bus.listener_event("battery_change", value)
        elif attrid == PCDM_BATTERY_LOW_ATTR and value > 0:
            self.endpoint.device.battery_bus.listener_event("battery_change", 5)
        elif attrid == PCDM_BOOST_MODE:
            self.endpoint.device.boost_bus.listener_event("set_change", 1 if value > 0 else 0)
        elif attrid == PCDM_CHILD_LOCK_ATTR:
            self.endpoint.device.ui_bus.listener_event("child_lock_change", 1 if value > 0 else 0)
        elif attrid == PCDM_PRESET:
            self.endpoint.device.thermostat_bus.listener_event("program_change", value)
        elif attrid == PCDM_SYSTEM_MODE_ATTR:
            self.endpoint.device.thermostat_bus.listener_event("mode_change", value)
        elif attrid == PCDM_TEMPERATURE_CORRECTION_ATTR:
            self.endpoint.device.thermostat_bus.listener_event("temperature_correction_change", value)

class PcdmThermostat(TuyaThermostatCluster):
    """Thermostat cluster for some thermostatic valves."""

    class Preset(t.enum8):
        """Working modes of the thermostat."""

        Schedule = 0x00
        Away = 0x01
        Manual = 0x02
        Comfort = 0x03
        Eco = 0x04
    
    attributes = TuyaThermostatCluster.attributes.copy()
    attributes.update(
        {
            PCDM_PRESET: ("operation_preset", Preset, True),
        }
    )

    def map_attribute(self, attribute, value):
        _LOGGER.info(f'map_attribute: attribute={attribute} value={value}')
        if attribute == "occupied_heating_setpoint":
            active_preset = self._attr_cache.get(
                    self.attributes_by_name["operation_preset"].id,
                    self.ProgrammingOperationMode.Simple,
                )
            attrid = PCDM_TARGET_TEMP_ATTR
            # attrid = PCDM_TARGET_MANUAL_ATTR #TODO missing Preset.Schedule
            # if active_preset == self.Preset.Away:
            #     attrid = PCDM_TARGET_HOLIDAY_ATTR
            # elif active_preset == self.Preset.Manual:
            #     attrid = PCDM_TARGET_MANUAL_ATTR
            # elif active_preset == self.Preset.Comfort:
            #     attrid = PCDM_TARGET_CONFORT_ATTR
            # elif active_preset == self.Preset.Eco:
            #     attrid = PCDM_TARGET_ECO_ATTR
            _LOGGER.info(f'map_attribute: attribute={attribute} active_preset={active_preset} => {attrid}')
            # centidegree to decidegree
            return {attrid: round(value / 10)}
        if attribute == "local_temperature":
            # centidegree to decidegree
            return {PCDM_TEMPERATURE_ATTR: round(value / 10)}
        if attribute == "system_mode":#, "programing_oper_mode"):
            if attribute == "system_mode":
                system_mode = value
                # oper_mode = self._attr_cache.get(
                #     self.attributes_by_name["programing_oper_mode"].id,
                #     self.ProgrammingOperationMode.Simple,
                # )
            else:
                system_mode = self._attr_cache.get(
                    self.attributes_by_name["system_mode"].id, self.SystemMode.Heat
                )
                # oper_mode = value
            if system_mode == self.SystemMode.Off:
                return {PCDM_SYSTEM_MODE_ATTR: 0}
            if system_mode == self.SystemMode.Heat:
                return {PCDM_SYSTEM_MODE_ATTR: 1}
            else:
                self.error("Unsupported value for SystemMode")
        if attribute == "programing_oper_mode":
            if value == self.ProgrammingOperationMode.Schedule_programming_mode:
                return {PCDM_PRESET: self.Preset.Schedule.value}
            if value == self.ProgrammingOperationMode.Simple:
                return {PCDM_PRESET: self.Preset.Manual.value}
            if value == self.ProgrammingOperationMode.Economy_mode:
                return {PCDM_PRESET: self.Preset.Eco.value}
        if attribute == "operation_preset":
            return {PCDM_PRESET: value.value}
        if attribute == "temperature_correction":
            calibration = -12 if value < -12 else 12 if value > 12 else value
            return {PCDM_TEMPERATURE_CORRECTION_ATTR: calibration}

    def temperature_correction_change(self, value):
        calibration = -12 if value < -12 else 12 if value > 12 else value
        self._update_attribute(self.attributes_by_name["temperature_correction"].id, calibration)


    def mode_change(self, value):
        """System Mode change."""
        _LOGGER.error(f'mode_change value [{value}]')
        # mode = self.SystemMode.Off if value == 0 else self.SystemMode.Heat
        # self._update_attribute(self.attributes_by_name["system_mode"].id, mode)
        self._update_attribute(self.attributes_by_name["system_mode"].id, self.SystemMode.Heat)
        if value == 0:
            mode = self.RunningMode.Off
            state = self.RunningState.Idle
        else:
            mode = self.RunningMode.Heat
            state = self.RunningState.Heat_State_On
        self._update_attribute(self.attributes_by_name["running_mode"].id, mode)
        self._update_attribute(self.attributes_by_name["running_state"].id, state)
    
    def program_change(self, value):
        """Programming mode change."""
        operation_preset = None
        prog_mode = None
        if value == 0:
            prog_mode = self.ProgrammingOperationMode.Schedule_programming_mode
            operation_preset = self.Preset.Schedule
        elif value == 1:
            prog_mode = self.ProgrammingOperationMode.Simple
            operation_preset = self.Preset.Away
        elif value == 2:
            prog_mode = self.ProgrammingOperationMode.Simple
            operation_preset = self.Preset.Manual
        elif value == 3:
            prog_mode = self.ProgrammingOperationMode.Simple
            operation_preset = self.Preset.Comfort
        elif value == 4:
            prog_mode = self.ProgrammingOperationMode.Economy_mode
            operation_preset = self.Preset.Eco
        else:
            self.error("Unsupported value for Mode")
        _LOGGER.info(f'program_change PRESET value [{value}] {prog_mode} {operation_preset}')

        if operation_preset is not None:
            self._update_attribute(self.attributes_by_name["operation_preset"].id, operation_preset)
            self._update_attribute(self.attributes_by_name["programing_oper_mode"].id, prog_mode)
            self._update_attribute(self.attributes_by_name["system_mode"].id, self.SystemMode.Heat)


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

    _CHILD_LOCK_ATTR = PCDM_CHILD_LOCK_ATTR


class PcdmHelperOnOff(LocalDataCluster, OnOff):
    """Helper OnOff cluster for various functions controlled by switch."""

    def set_change(self, value):
        """Set new OnOff value."""
        self._update_attribute(self.attributes_by_name["on_off"].id, value)

    def get_attr_val_to_write(self, value):
        """Return dict with attribute and value for thermostat."""
        return None

    async def write_attributes(self, attributes, manufacturer=None):
        """Defer attributes writing to the set_data tuya command."""
        records = self._write_attr_records(attributes)
        if not records:
            return [[foundation.WriteAttributesStatusRecord(foundation.Status.SUCCESS)]]

        has_change = False
        for record in records:
            attr_name = self.attributes[record.attrid].name
            if attr_name == "on_off":
                value = record.value.value
                has_change = True

        if has_change:
            attr_val = self.get_attr_val_to_write(value)
            if attr_val is not None:
                # global self in case when different endpoint has to exist
                return await PcdmManuClusterSelf.endpoint.tuya_manufacturer.write_attributes(
                    attr_val, manufacturer=manufacturer
                )

        return [
            [
                foundation.WriteAttributesStatusRecord(
                    foundation.Status.FAILURE, r.attrid
                )
                for r in records
            ]
        ]

    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 command_id in (0x0000, 0x0001, 0x0002):
            if command_id == 0x0000:
                value = False
            elif command_id == 0x0001:
                value = True
            else:
                attrid = self.attributes_by_name["on_off"].id
                success, _ = await self.read_attributes(
                    (attrid,), manufacturer=manufacturer
                )
                try:
                    value = success[attrid]
                except KeyError:
                    return foundation.GENERAL_COMMANDS[
                        foundation.GeneralCommand.Default_Response
                    ].schema(command_id=command_id, status=foundation.Status.FAILURE)
                value = not value
            _LOGGER.debug("CALLING WRITE FROM COMMAND")
            (res,) = await self.write_attributes(
                {"on_off": value},
                manufacturer=manufacturer,
            )
            return foundation.GENERAL_COMMANDS[
                foundation.GeneralCommand.Default_Response
            ].schema(command_id=command_id, status=res[0].status)

        return foundation.GENERAL_COMMANDS[
            foundation.GeneralCommand.Default_Response
        ].schema(command_id=command_id, status=foundation.Status.UNSUP_CLUSTER_COMMAND)


class PcdmBoost(PcdmHelperOnOff):
    """On/Off cluster for the boost function of the heating thermostats."""

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

    def get_attr_val_to_write(self, value):
        """Return dict with attribute and value for boot mode."""
        return {PCDM_BOOST_MODE: 299 if value else 0}


class PcdmWindowDetection(PcdmHelperOnOff):
    """On/Off cluster for the window detection function of the electric heating thermostats."""

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

    def get_attr_val_to_write(self, value):
        """Return dict with attribute and value for boot mode."""
        return {PCDM_BOOST_MODE: value}


class PcdmTrv(TuyaThermostat):
    """PCDRM Thermostatic radiator valve"""

    def __init__(self, *args, **kwargs):
        """Init device."""
        self.boost_bus = Bus()
        self.window_detection_bus = Bus()
        super().__init__(*args, **kwargs)


    signature = {
        #  endpoint=1 profile=260 device_type=81 device_version=0 input_clusters=[0, 4, 5, 61184]
        #  output_clusters=[10, 25]>
        MODELS_INFO: [
            ("_TZE204_pcdmj88b", "TS0601"),
        ],
        ENDPOINTS: {
            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],
            }
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.THERMOSTAT,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    PcdmManufTrvCluster,
                    PcdmBoost,
                    PcdmThermostat,
                    PcdmUserInterface,
                    # PcdmWindowDetection,
                    TuyaPowerConfigurationCluster2AA,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            }
        }
    }

###

I’m still wondering how to make it show up as an entity, but there is progress!

@elf0heart yes there is more to do 😃

@T0ytoy preset can be change with group PcdmThermostat

At this time, you can only change:

  • target temperature
  • child mode lock (no visual integration in HASS but working with service call)
  • preset mode : eco / manual / away / schedule

And in read only, we have :

  • battery level
  • valve is open or closed

I’m still working on it

PS: je vois des français partout ^_^

@elf0heart yes it’s only work for reading… i’m working on it 💪

@royduin i’m trying to check if all features implemented works and after i will submit a PR 😃

@T0yto great ! i’ll check code asap (i only work 1 or 2 hours by week on this project, so sorry for delay in response) In term of Home-Assistant integration the new version 2023.12 try new things in order to improve integration of ZIGBEE devices… maybe in the futur, we don’t have to bring patch to have full features on HA.

Really good job for your blueprint !

@Teka101 I was finally able to test you update, it seems to work thank you! In the home assistant zha UI, boost mode and window detection mode are both switches without a name, so it isn’t clear which does what at first glance, but it’s not a big problem.

If I have some time this week-end, I’ll try to implement the right class so that temprature calibration gets it’s own numeric entity in home assistant, so that it can be used easily.

I use the calibration feature (it has an entity on the z2m integration) to correct the device internal temperature to the temperature of an external zigbee thermometer: that way when the radiator heats up, the TVR temperature does not increase just because it’s too close to the radiator. It is done home assistant side, that is why I’m deseperately trying to get that entity 😄

I’ll let you know if I’m getting anything done. Thank you!

@ed-wright Good news! It kinda make sense: the issue was probably on the zigbee association side, I’m glad there is no intricated technical issue with the python code, as I’m really not comfortable debugging advanced issues in this context 😄

To give credit to where it’s due: 890% of the work was done by @Teka101, I mostly just worked on the calibration feature. Many thanks to him 😃

As a side note, I’m currently experimenting with using an average value over ~5-10 minutes instead of the raw “0.5 °C resolution shit data” locale temperature data from the TVR to feed the blueprint I made, I think it might be working a bit better since the raw temperature is jumping ±1°C all the time and the automation blueprint reacts a lot to try to compensate. I’m hoping it will generate less spikes above and below target temperature.

@ed-wright I just checked, the quirk I have running on my HA and the one I linked above are exactly the same, and it’s working fine for me, so I don’t really know what to think. Could you maybe have a different version of the TRV? Mine came in a blue box, the user manuel front page says “Model: BAB-1413Pro-E”.

I guess since the quirk is loaded and would only do so with "_TZE204_pcdmj88b", "TS0601", the signature is right. Does it work if you change the calibration value in the “manage Zigbee device” menu?

EDIT: alternatively you can try and put some _LOGGER.error("line xxx : value is %s", value)at key lines of the code (%s or %d depending on the line I think, also ‘value’ or ‘intValue’): I’m thinking lines 456, 460, 464, 472 are good places to investigate. This way you would have information in home assistant logs on what is going on.

@T0ytoy ok thank you for your code.

I just publish a new version with: window detection mode and temperature calibration

Hello,

Very experimental patch (and very ugly), just add these lines at the end of file ts0601_trv.py:

from typing import Tuple
from zhaquirks.const import (
    SKIP_CONFIGURATION,
)

PCDM_PRESET = 1026 #OK
PCDM_TARGET_TEMP_ATTR = 516 #OK
PCDM_TEMPERATURE_ATTR = 517 #OK
PCDM_BATTERY_ATTR = 518 #OK
PCDM_CHILD_LOCK_ATTR = 1073 #nop?
PCDM_SYSTEM_MODE_ATTR = 293 #nop?
#1315 ?window_mode?

class PcdmManufTrvCluster(TuyaManufClusterAttributes):
    """Manufacturer Specific Cluster of some thermostatic valves."""

    class Preset(t.enum8):
        """Working modes of the thermostat."""

        Schedule = 0x00
        Away = 0x01
        Manual = 0x02
        Comfort = 0x03
        Eco = 0x04


    set_time_offset = 1970

    attributes = TuyaManufClusterAttributes.attributes.copy()
    attributes.update(
        {
            PCDM_PRESET: ("operation_preset", Preset, True),
            PCDM_TARGET_TEMP_ATTR: ("target_temperature", t.uint32_t, True),
            PCDM_TEMPERATURE_ATTR: ("temperature", t.uint32_t, True),
            PCDM_BATTERY_ATTR: ("battery", t.uint32_t, True),
            PCDM_CHILD_LOCK_ATTR: ("child_lock", t.uint8_t, True),
            PCDM_SYSTEM_MODE_ATTR: ("system_mode", t.uint8_t, True),
        }
    )

    TEMPERATURE_ATTRS = {
        PCDM_TARGET_TEMP_ATTR: "occupied_heating_setpoint",
        PCDM_TEMPERATURE_ATTR: "local_temperature",
    }
    
    def handle_cluster_request(
        self,
        hdr: foundation.ZCLHeader,
        args: Tuple,
        *,
        dst_addressing: Optional[
            Union[t.Addressing.Group, t.Addressing.IEEE, t.Addressing.NWK]
        ] = None,
    ) -> None:
        _LOGGER.debug(
            "handle_cluster_request: [0x%04x:%s:0x%04x] Received value (command 0x%04x)",
            self.endpoint.device.nwk,
            self.endpoint.endpoint_id,
            self.cluster_id,
            hdr.command_id,
        )
        _LOGGER.debug('%d # %s', len(args), str(args))
        return super().handle_cluster_request(hdr, args, dst_addressing=dst_addressing)
    
    async def write_attributes(self, attributes, manufacturer=None):
        _LOGGER.debug('write_attributes %s', str(attributes))
        return await super().write_attributes(attributes, manufacturer)

    def _update_attribute(self, attrid, value):
        super()._update_attribute(attrid, value)
        if attrid in self.TEMPERATURE_ATTRS:
            self.endpoint.device.thermostat_bus.listener_event(
                "temperature_change",
                self.TEMPERATURE_ATTRS[attrid],
                value * 10,  # decidegree to centidegree
            )
        elif attrid == PCDM_CHILD_LOCK_ATTR:
            mode = 1 if value else 0
            self.endpoint.device.ui_bus.listener_event("child_lock_change", mode)
        elif attrid == PCDM_BATTERY_ATTR:
            self.endpoint.device.battery_bus.listener_event("battery_change", value)
        elif attrid == PCDM_SYSTEM_MODE_ATTR:
            self.endpoint.device.thermostat_bus.listener_event("mode_change", value)

class PcdmThermostat(TuyaThermostatCluster):
    """Thermostat cluster for some thermostatic valves."""

    def map_attribute(self, attribute, value):
        _LOGGER.info(f'map_attribute: attribute={attribute} value={value}')
        if attribute == "occupied_heating_setpoint":
            # centidegree to decidegree
            return {PCDM_TARGET_TEMP_ATTR: round(value / 10)}
        if attribute == "local_temperature":
            # centidegree to decidegree
            return {PCDM_TEMPERATURE_ATTR: round(value / 10)}
        if attribute in ("system_mode", "programing_oper_mode"):
            if attribute == "system_mode":
                system_mode = value
                oper_mode = self._attr_cache.get(
                    self.attributes_by_name["programing_oper_mode"].id,
                    self.ProgrammingOperationMode.Simple,
                )
            else:
                system_mode = self._attr_cache.get(
                    self.attributes_by_name["system_mode"].id, self.SystemMode.Heat
                )
                oper_mode = value
            if system_mode == self.SystemMode.Off:
                return {PCDM_SYSTEM_MODE_ATTR: 0}
            if system_mode == self.SystemMode.Heat:
                return {PCDM_SYSTEM_MODE_ATTR: 1}
            else:
                self.error("Unsupported value for SystemMode")

    def mode_change(self, value):
        """System Mode change."""
        if value == 0:
            self._update_attribute(
                self.attributes_by_name["system_mode"].id, self.SystemMode.Off
            )
            return

        if value == 1:
            mode = self.ProgrammingOperationMode.Schedule_programming_mode
        else:
            mode = self.ProgrammingOperationMode.Simple

        self._update_attribute(
            self.attributes_by_name["system_mode"].id, self.SystemMode.Heat
        )
        self._update_attribute(self.attributes_by_name["programing_oper_mode"].id, mode)

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

    _CHILD_LOCK_ATTR = PCDM_CHILD_LOCK_ATTR


class PcdmTrv(TuyaThermostat):
    """PCDRM Thermostatic radiator valve"""

    def __init__(self, *args, **kwargs):
        """Init device."""
        # self.window_detection_bus = Bus()
        super().__init__(*args, **kwargs)


    signature = {
        #  endpoint=1 profile=260 device_type=81 device_version=0 input_clusters=[0, 4, 5, 61184]
        #  output_clusters=[10, 25]>
        MODELS_INFO: [
            ("_TZE204_pcdmj88b", "TS0601"),
        ],
        ENDPOINTS: {
            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],
            }
        },
    }

    replacement = {
#        SKIP_CONFIGURATION: True,
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.THERMOSTAT,
#                DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    PcdmManufTrvCluster,
                    PcdmThermostat,
                    #TODO PcdmUserInterface,
                    #TODO MoesWindowDetection
                    TuyaPowerConfigurationCluster2AA,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            }
        }
    }