Repository: zigpy/zha-device-handlers

Issue:
hass.io installation
How would one install / add zha-device-handlers to HA on hass.io ?


Discussion:
It’s automatically installed when zha is configured 
I see that there is a device handler for the Sinopé thermostat, but when I pair it, my HA only gets the local room temp, and two switches that do nothing. No option or ability to set up thermostat.

But in Configuration -> ZHA -> Device Management, all clusters are available, and can be "manually" updated.

Trying to sort out why the quirk is not functioning as it appears it should.

Any assistance or insight would be greatly appreciated.

@Adminiuga any thoughts? I don’t have one of these. 

@michaeljameslewis do the simple descriptors that are printed after pairing match what’s in the quirk?
We don't have ZHA climate support at the moment ): If you want to set temperature via automation, you can use `zha.set_zigbee_cluster_attributes` service, eg:
```
    - service: zha.set_zigbee_cluster_attributes
      data:
        ieee: "50:0b:91:40:00:01:02:03"
        endpoint_id: 1
        cluster_id: 513
        attribute: 18
        value: 1800
```

The value is in 0.01Celsius. To control on/off state of the thermostat, write to attribute `28` value `4`--On, `0` -- off

There was ZHA Climate in the work which worked with Sinope, but other climate were a bit different and required a different approach. Although I wanna say I like Sinope thermostat implementation much better than of  Zen Thermostat. Maybe we should get it in, but it would require changes later anyway. 
Oops! I thought that was PR’d in at some point! My bad.
@Adminiuga

Wow! Thank you for that invaluable info on how to call the service! I had not been able to find anything that detailed so far.

I chose Sinopé for its Zigbee (less cloud reliance) and also because the tech is «fait au Québec» (where I live), and the physical footprint of the 'stat is identical to the dumb stats I replaced.

I don't need climate style thermostat in a card in Lovelace as much as I need automation. Too many stats to keep checking (6 in total).
![Screenshot_20191028-192641](https://user-images.githubusercontent.com/26877740/67726112-26671080-f9bb-11e9-88b3-a56bfb39d641.png)

Also, this is a screenshot of the three entities that are discovered when I pair the Sinopé with HA-ZHA (using deConz USB).
You also should see those devices if you go to Config Panel -> ZHA -> pick a device and you should also see the IEEE address for each device you pick there. 
With Sinope you can control on/off via writing the `system_mode` attribute and setpoint temperature via `occupied_heating_setpoint` attribute (attr id 18) and `unoccupied_heating_setpoint` attribute (attr id 20). To change thermostat into "away"/"unoccupied" mode you need to write into `set_occupancy` attribute on cluster `513` but for that one you also need to specify the `manufacturer` code which is `4508` decimal. I haven't tried this via service call, so confirm that it work.

You can also update the outdoor temperature display on the thermostat. I use this automation:
```
- id: thermostat_outdoor_temp
  alias: thermostat update outdoor temp display
  trigger:
    - platform: state
      entity_id: sensor.lumi_outside_temp
    - platform: state
      entity_id: sensor.outside_temperature
  action:
    - service: zha.set_zigbee_cluster_attribute
      data_template:
        ieee: "50:0b:91:40:00:01:02:03"
        endpoint_id: "1"
        cluster_id: "65281"
        attribute: "16"
        value: "{{trigger.to_state.state | int * 100}}"
```

You should also be able to write into cluster id `0xff01` attribute 32 the number of seconds since Jan 2000 and that should update the time on the display. No need to write every minute, just set it once after Thermostat is powercycled. I don't have an automation for it, cause I couldn't figure out jinja template for it. 
I have the (possibly) newer / newest Sinopé ZB stats with "Zigbee 3.0" (according to the box).

Cluster 65281 has now become 513.

OutdoorTemp attribute ID is now 1.

The default value is -32768 and nothing I commit to it, either through Config -> ZHA or DevTools will stick.

This is the only cluster+attribute that will not permit changes.

I did have it linked & sync'd with Smartthings v3 hub and the outdoor temp populated itself without intervention. There is a data_type value in the Sinopé SamsungST driver (groovy code), which I cannot find reference for in HA.

https://support.sinopetech.com/wp-content/uploads/2019/08/Sinope-Technologies-Zigbee-Thermostat-V.1.0.0-SVN-547-1.txt

There is also some strange math applied to the outdoor temp in an if/else with relation to being a + or - temp (in C). The positive temp being an average of the difference with a factor of 256. Not just a *100.

This is indeed puzzling!

However, looking at the zigbee.db file in SQLite Browser, I do confirm that all the attribute points referenced in the zha_quirks are being tracked.

All other useful attributes (setpoint, lock, etc.) are actioned when using DevTools or ZHA manual set_attribute in Config.
Hi
I'm new to HA and Python, I have the Sinope TH1400ZB and I've tried to change the sinope/thermostat.py file without any success:
MODELS_INFO: [(SINOPE, "TH1123ZB"), (SINOPE, "TH1124ZB")**, (SINOPE, "TH1400ZB")**],

My log output:
Checking quirks for Sinope Technologies TH1400ZB (50:0b:91:40:00:02:01:9e)
Considering <class 'zhaquirks.sinope.thermostat.SinopeTechnologiesThermostat'>
Fail because endpoint list mismatch: {1, 196} {1}

I think the TH1400ZB should be really similar to the TH1123ZB/TH1124ZB because they're all in the same linked file for the smartthings hub from the previous post.

I still can see the current temperature, but cannot see the power consomption nor change anything.

If you could point me in the right direction, I would really like to help and make this device work with HA.

Thank you.
Mathieu

> Fail because endpoint list mismatch: {1, 196} {1}

Hrm, your endpoint list is different. If you could remove it from the network, enable debug logging and re-pair it again capturing in the log simple descriptor request, that would provide more information. 
Alternatively, Try adding a copy of the Sinope quirks class, under different name and with endpoint 196 references removed eg

```
class SinopeTechnologiesThermostat(CustomDevice):
    """SinopeTechnologiesThermostat custom device."""

    class SinopeTechnologiesManufacturerCluster(CustomCluster):
        """SinopeTechnologiesManufacturerCluster manufacturer cluster."""

        cluster_id = SINOPE_MANUFACTURER_CLUSTER_ID
        name = "Sinopé Technologies Manufacturer specific"
        ep_attribute = "sinope_manufacturer_specific"
        attributes = {
            0x0010: ("outdoor_temp", t.int16s),
            0x0020: ("secs_since_2k", t.uint32_t),
        }
        client_commands = {}
        server_commands = {}

        def __init__(self, *args, **kwargs):
            """Init method."""
            super().__init__(*args, **kwargs)
            self._attridx = {
                attrname: attrid
                for attrid, (attrname, datatype) in self.attributes.items()
            }

    class SinopeTechnologiesThermostatCluster(CustomCluster, Thermostat):
        """SinopeTechnologiesThermostatCluster custom cluster."""

        attributes = Thermostat.attributes.copy()
        attributes[0x0400] = ("set_occupancy", t.enum8)

    signature = {
        # <SimpleDescriptor endpoint=1 profile=260 device_type=769
        # device_version=0 input_clusters=[0, 3, 4, 5, 513, 516, 1026, 2820,
        # 2821, 65281] output_clusters=[65281, 25]>
        MODELS_INFO: [(SINOPE, "TH1400ZB"),],
        ENDPOINTS: {
            1: {
                PROFILE_ID: 0x0104,
                DEVICE_TYPE: 0x0301,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Identify.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    Thermostat.cluster_id,
                    UserInterface.cluster_id,
                    TemperatureMeasurement.cluster_id,
                    ElectricalMeasurement.cluster_id,
                    Diagnostic.cluster_id,
                    SINOPE_MANUFACTURER_CLUSTER_ID,
                ],
                OUTPUT_CLUSTERS: [Ota.cluster_id, SINOPE_MANUFACTURER_CLUSTER_ID],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                INPUT_CLUSTERS: [
                    Basic,
                    Identify,
                    Groups,
                    Scenes,
                    UserInterface,
                    TemperatureMeasurement,
                    ElectricalMeasurement,
                    Diagnostic,
                    SinopeTechnologiesThermostatCluster,
                    SinopeTechnologiesManufacturerCluster,
                ],
                OUTPUT_CLUSTERS: [Ota, SINOPE_MANUFACTURER_CLUSTER_ID],
            },
           }
    }
```
@michaeljameslewis 
> There is also some strange math applied to the outdoor temp in an if/else with relation to being a + or - temp (in C). The positive temp being an average of the difference with a factor of 256. Not just a *100.

Hrm, maybe it's just a signed int16 vs an unsigned int16. If it is, then just send it as is and zigpy will take care of it

'outdoor_temp` as attr id 0x0001 is part of ZCL, but it is a read only attribute
![image](https://user-images.githubusercontent.com/5826160/71537689-a3e0cb80-28ed-11ea-8558-704ef5729f36.png)


and yeah, it is a signed int16, so anything in `0x0000` -- `0x7FFF` is a positive number and everything above it is a negative number. 

looked at ST device handler. They still use attr-id 0x0010 of the manufacturer specific cluster to update the outdoor temperature. Ignore the conversions they do for negative temps.
There's https://github.com/Adminiuga/home-assistant/tree/ac/new-zha-climate-1.0-ent-reg which has basic support for Sinope. No "time" or "outdoor" temperature update at the moment. 

Unless running in a docker, you could just copy the `homeassistant/components/zha` folder to your installation, just keep in mind it is going to update zigpy and zha-quirks versions.

this is the automation I use to update the outdoor temp:
```
- id: mbr_thermostat_outdoor_temp
  alias: Master Bedroom thermostat update outdoor temp display
  trigger:
    - platform: state
      entity_id: sensor.lumi_outside_temp
    - platform: state
      entity_id: sensor.outside_temperature
  action:
    - service: zha.set_zigbee_cluster_attribute
      data_template:
        ieee: "50:0b:91:40:00:01:01:01"
        endpoint_id: "1"
        cluster_id: "65281"
        attribute: "16"
        value: "{{trigger.to_state.state | int * 100}}"
```

[0x0000:zdo] ZDO request 0x0036: [60, <Bool.false: 0>]
[0x0000:zdo] ZDO request 0x0036: [60, <Bool.false: 0>]
Device 0x8c9f (50:0b:91:40:00:02:01:9e) joined the network
[0x8c9f] Requesting 'Node Descriptor'
[0x8c9f] Extending timeout for 0x49 request
Device 0x8c9f (50:0b:91:40:00:02:01:9e) joined the network
Skip initialization for existing device 50:0b:91:40:00:02:01:9e
[0x8c9f:zdo] ZDO request 0x0013: [0x8c9f, 50:0b:91:40:00:02:01:9e, 142]
[0x8c9f] Node Descriptor: <Optional byte1=17 byte2=64 mac_capability_flags=142 manufacturer_code=4508 maximum_buffer_size=71 maximum_incoming_transfer_size=43 server_mask=10752 maximum_outgoing_transfer_size=43 descriptor_capability_field=0>
[0x8c9f] Discovering endpoints
[0x8c9f] Discovered endpoints: [1]
[0x8c9f:1] Discovering endpoint information
[0x8c9f:zdo] ZDO request 0x0002: [0x0000]
[0x8c9f:zdo] Unsupported ZDO request:0x0002
[0x8c9f:zdo] ZDO request 0x0036: [180, <Bool.true: 1>]
[0x8c9f:1] Discovered endpoint information: <SimpleDescriptor endpoint=1 profile=260 device_type=769 device_version=1 input_clusters=[0, 3, 4, 5, 513, 516, 1026, 1794, 2821, 65281] output_clusters=[10, 65281, 25]>
[0x8c9f:1:0x0000] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=True disable_default_response=True> manufacturer=None tsn=79 command_id=Command.Read_Attributes_rsp>
[0x8c9f:1] Manufacturer: Sinope Technologies
[0x8c9f:1] Model: TH1400ZB
Checking quirks for Sinope Technologies TH1400ZB (50:0b:91:40:00:02:01:9e)
Considering <class 'zhaquirks.sinope.**thermostat_math**.SinopeTechnologiesThermostat'>
**Fail because input cluster mismatch on at least one endpoint**
Considering <class 'zigpy.quirks.smartthings.SmartthingsMultiPurposeSensor'>
Fail because device_type mismatch on at least one endpoint
Considering <class 'bellows.zigbee.application.EZSPCoordinator'>
Fail because device_type mismatch on at least one endpoint
Considering <class 'zhaquirks.gledopto.soposhgu10.SoposhGU10'>
Fail because endpoint list mismatch: {11, 13} {1}
Considering <class 'zhaquirks.lutron.lzl4bwhl01remote.LutronLZL4BWHL01Remote2'>
Fail because profile_id mismatch on at least one endpoint
Considering <class 'zhaquirks.netvox.z308e3ed.Z308E3ED'>
Fail because device_type mismatch on at least one endpoint
Considering <class 'zhaquirks.osram.a19twhite.A19TunableWhite'>
Fail because endpoint list mismatch: {3} {1}
Considering <class 'zhaquirks.philips.rwl021.PhilipsRWL021'>
Fail because endpoint list mismatch: {1, 2} {1}
Considering <class 'zhaquirks.smartthings.tag_v4.SmartThingsTagV4'>
Fail because device_type mismatch on at least one endpoint
Considering <class 'zhaquirks.xbee.xbee3_io.XBee3Sensor'>
Fail because endpoint list mismatch: {232, 230} {1}
Considering <class 'zhaquirks.xbee.xbee_io.XbeeSensor'>
Fail because endpoint list mismatch: {232, 230} {1}
Considering <class 'zhaquirks.xiaomi.mija.smoke.MijiaHoneywellSmokeDetectorSensor'>
Fail because device_type mismatch on at least one endpoint
device - 0x8c9f:50:0b:91:40:00:02:01:9e entering async_device_initialized - is_new_join: True
device - 0x8c9f:50:0b:91:40:00:02:01:9e has joined the ZHA zigbee network
[0x8c9f](TH1400ZB): started configuration
[0x8c9f:ZDO](TH1400ZB): channel: 'async_configure' stage succeeded
[0x8c9f:1:0x0000]: bound 'basic' cluster: Status.SUCCESS
[0x8c9f:1:0x0000]: finished channel configuration
[0x8c9f:1:0x0402]: bound 'temperature' cluster: Status.SUCCESS
[0x8c9f:1:0x0702] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=True disable_default_response=True> manufacturer=None tsn=85 command_id=Command.Read_Attributes_rsp>
Error calling listener.attribute_updated: unsupported operand type(s) for *: 'uint24_t' and 'NoneType'
[0x8c9f:1:0x0000] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=True disable_default_response=True> manufacturer=None tsn=87 command_id=Command.Read_Attributes_rsp>
[0x8c9f:1:0x0402] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=True disable_default_response=True> manufacturer=None tsn=89 command_id=Command.Configure_Reporting_rsp>
[0x8c9f:1:0x0000]: initializing channel: from_cache: False
[0x8c9f:1:0x0000]: channel: 'async_configure' stage succeeded
[0x8c9f:1:0x0402]: reporting 'measured_value' attr on 'temperature' cluster: 30/900/50: Result: '[[<ConfigureReportingResponseRecord status=Status.SUCCESS>]]'
[0x8c9f:1:0x0702] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=True disable_default_response=True> manufacturer=None tsn=91 command_id=Command.Read_Attributes_rsp>
Error calling listener.attribute_updated: unsupported operand type(s) for *: 'uint24_t' and 'NoneType'
[0x8c9f:1:0x0702] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=True disable_default_response=True> manufacturer=None tsn=93 command_id=Command.Read_Attributes_rsp>
[0x8c9f:1:0x0702] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=True disable_default_response=True> manufacturer=None tsn=95 command_id=Command.Read_Attributes_rsp>
[0x8c9f:1:0x0702]: bound 'smartenergy_metering' cluster: Status.SUCCESS
[0x8c9f:1:0x0702] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=True disable_default_response=True> manufacturer=None tsn=99 command_id=Command.Configure_Reporting_rsp>
[0x8c9f:1:0x0702]: reporting 'instantaneous_demand' attr on 'smartenergy_metering' cluster: 30/900/1: Result: '[[<ConfigureReportingResponseRecord status=Status.UNSUPPORTED_ATTRIBUTE direction=0 attrid=1024>]]'
[0x8c9f:1:0x0402]: finished channel configuration
[0x8c9f:1:0x0402]: channel: 'async_configure' stage succeeded
[0x8c9f:1:0x0702]: finished channel configuration
[0x8c9f:1:0x0702]: channel: 'async_configure' stage succeeded
[0x8c9f](TH1400ZB): completed configuration
[0x8c9f](TH1400ZB): stored in registry: ZhaDeviceEntry(name='Sinope Technologies TH1400ZB', ieee='50:0b:91:40:00:02:01:9e', last_seen=1577506567.7670414)
[0x8c9f](TH1400ZB): started initialization
[0x8c9f:ZDO](TH1400ZB): entry loaded from storage: ZhaDeviceEntry(name='Sinope Technologies TH1400ZB', ieee='50:0b:91:40:00:02:01:9e', last_seen=1577506567.7670414)
[0x8c9f:ZDO](TH1400ZB): channel: 'async_initialize' stage succeeded
[0x8c9f:1:0x0000] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=True disable_default_response=True> manufacturer=None tsn=101 command_id=Command.Read_Attributes_rsp>
[0x8c9f:1:0x0000]: initializing channel: from_cache: False
[0x8c9f:1:0x0000]: channel: 'async_initialize' stage succeeded
[0x8c9f:1:0x0402] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=True disable_default_response=True> manufacturer=None tsn=103 command_id=Command.Read_Attributes_rsp>
[0x8c9f:1:0x0702] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=True disable_default_response=True> manufacturer=None tsn=105 command_id=Command.Read_Attributes_rsp>
[0x8c9f:1:0x0402]: initializing channel: from_cache: False
[0x8c9f:1:0x0402]: channel: 'async_initialize' stage succeeded
[0x8c9f:1:0x0702]: initializing channel: from_cache: False
[0x8c9f:1:0x0702]: channel: 'async_initialize' stage succeeded
[0x8c9f](TH1400ZB): power source: Mains
[0x8c9f](TH1400ZB): completed initialization
[0x8c9f:zdo] ZDO request 0x0006: [0xfffd, 260, [10], []]
[0x8c9f:1:0x000a] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=False disable_default_response=True> manufacturer=None tsn=1 command_id=Command.Read_Attributes>
[0x8c9f:1:0x000a] ZCL request 0x0000: [[1]]
[0x8c9f:1:0x000a] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=False disable_default_response=True> manufacturer=None tsn=3 command_id=Command.Read_Attributes>
[0x8c9f:1:0x000a] ZCL request 0x0000: [[0]]
[0x8c9f:1:0x000a] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=False disable_default_response=True> manufacturer=None tsn=5 command_id=Command.Read_Attributes>
[0x8c9f:1:0x000a] ZCL request 0x0000: [[7]]
[0x8c9f:1:0x000a] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=False disable_default_response=True> manufacturer=None tsn=7 command_id=Command.Read_Attributes>
[0x8c9f:1:0x000a] ZCL request 0x0000: [[2]]
[0x8c9f:1:0x000a] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=False disable_default_response=True> manufacturer=None tsn=9 command_id=Command.Read_Attributes>
[0x8c9f:1:0x000a] ZCL request 0x0000: [[3]]
[0x8c9f:1:0x000a] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=False disable_default_response=True> manufacturer=None tsn=11 command_id=Command.Read_Attributes>
[0x8c9f:1:0x000a] ZCL request 0x0000: [[4]]
[0x8c9f:1:0x000a] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=False disable_default_response=True> manufacturer=None tsn=13 command_id=Command.Read_Attributes>
[0x8c9f:1:0x000a] ZCL request 0x0000: [[5]]
[0x8c9f:1:0x000a] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=False disable_default_response=True> manufacturer=None tsn=15 command_id=Command.Read_Attributes>
[0x8c9f:1:0x000a] ZCL request 0x0000: [[6]]
[0x8c9f:zdo] ZDO request 0x0006: [0xfffd, 260, [25], []]
[0x8c9f:1:0x0402] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=GLOBAL_COMMAND manufacturer_specific=False is_reply=True disable_default_response=True> manufacturer=None tsn=16 command_id=Command.Report_Attributes>
[0x8c9f:1:0x0402] ZCL request 0x000a: [[<Attribute attrid=0 value=<TypeValue type=int16s, value=2141>>]]
[0x8c9f:1:0x0402] Attribute report received: measured_value=2141
[0x8c9f:zdo] ZDO request 0x0001: [0x0000, 0, 1]
[0x8c9f:1:0x0019] ZCL deserialize: <ZCLHeader frame_control=<FrameControl frame_type=CLUSTER_COMMAND manufacturer_specific=False is_reply=False disable_default_response=False> manufacturer=None tsn=17 command_id=1>
[0x8c9f:1:0x0019] ZCL request 0x0001: [0, 4508, 26, 1539, None]
[0x8c9f:1:0x0019] OTA query_next_image handler for 'Sinope Technologies TH1400ZB': field_control=0, manufacture_id=4508, image_type=26, current_file_version=1539, hardware_version=None
[0x8c9f:1:0x0019] No OTA image is available


Resolving patch:
diff --git a/zhaquirks/sinope/thermostat.py b/zhaquirks/sinope/thermostat.py
--- a/zhaquirks/sinope/thermostat.py
+++ b/zhaquirks/sinope/thermostat.py
@@ -5,6 +5,7 @@
 of outdoor temperature, setting occupancy on/off and setting device time.
 """
 
+import zigpy.profiles.zha as zha_p
 from zigpy.quirks import CustomCluster, CustomDevice
 import zigpy.types as t
 from zigpy.zcl.clusters.general import (
@@ -14,10 +15,12 @@
     Ota,
     PowerConfiguration,
     Scenes,
+    Time,
 )
 from zigpy.zcl.clusters.homeautomation import Diagnostic, ElectricalMeasurement
 from zigpy.zcl.clusters.hvac import Thermostat, UserInterface
 from zigpy.zcl.clusters.measurement import TemperatureMeasurement
+from zigpy.zcl.clusters.smartenergy import Metering
 
 from . import SINOPE
 from ..const import (
@@ -32,35 +35,29 @@
 SINOPE_MANUFACTURER_CLUSTER_ID = 0xFF01
 
 
-class SinopeTechnologiesThermostat(CustomDevice):
-    """SinopeTechnologiesThermostat custom device."""
+class SinopeTechnologiesManufacturerCluster(CustomCluster):
+    """SinopeTechnologiesManufacturerCluster manufacturer cluster."""
 
-    class SinopeTechnologiesManufacturerCluster(CustomCluster):
-        """SinopeTechnologiesManufacturerCluster manufacturer cluster."""
+    cluster_id = SINOPE_MANUFACTURER_CLUSTER_ID
+    name = "Sinopé Technologies Manufacturer specific"
+    ep_attribute = "sinope_manufacturer_specific"
+    attributes = {
+        0x0010: ("outdoor_temp", t.int16s),
+        0x0020: ("secs_since_2k", t.uint32_t),
+    }
+    client_commands = {}
+    server_commands = {}
 
-        cluster_id = SINOPE_MANUFACTURER_CLUSTER_ID
-        name = "Sinopé Technologies Manufacturer specific"
-        ep_attribute = "sinope_manufacturer_specific"
-        attributes = {
-            0x0010: ("outdoor_temp", t.int16s),
-            0x0020: ("secs_since_2k", t.uint32_t),
-        }
-        client_commands = {}
-        server_commands = {}
-
-        def __init__(self, *args, **kwargs):
-            """Init method."""
-            super().__init__(*args, **kwargs)
-            self._attridx = {
-                attrname: attrid
-                for attrid, (attrname, datatype) in self.attributes.items()
-            }
 
-    class SinopeTechnologiesThermostatCluster(CustomCluster, Thermostat):
-        """SinopeTechnologiesThermostatCluster custom cluster."""
+class SinopeTechnologiesThermostatCluster(CustomCluster, Thermostat):
+    """SinopeTechnologiesThermostatCluster custom cluster."""
+
+    attributes = Thermostat.attributes.copy()
+    attributes[0x0400] = ("set_occupancy", t.enum8)
 
-        attributes = Thermostat.attributes.copy()
-        attributes[0x0400] = ("set_occupancy", t.enum8)
+
+class SinopeTechnologiesThermostat(CustomDevice):
+    """SinopeTechnologiesThermostat custom device."""
 
     signature = {
         # <SimpleDescriptor endpoint=1 profile=260 device_type=769
@@ -69,8 +66,8 @@ class SinopeTechnologiesThermostatCluster(CustomCluster, Thermostat):
         MODELS_INFO: [(SINOPE, "TH1123ZB"), (SINOPE, "TH1124ZB")],
         ENDPOINTS: {
             1: {
-                PROFILE_ID: 0x0104,
-                DEVICE_TYPE: 0x0301,
+                PROFILE_ID: zha_p.PROFILE_ID,
+                DEVICE_TYPE: zha_p.DeviceType.THERMOSTAT,
                 INPUT_CLUSTERS: [
                     Basic.cluster_id,
                     Identify.cluster_id,
@@ -89,7 +86,7 @@ class SinopeTechnologiesThermostatCluster(CustomCluster, Thermostat):
             # device_version=0 input_clusters=[1] output_clusters=[]>
             196: {
                 PROFILE_ID: 0xC25D,
-                DEVICE_TYPE: 0x0301,
+                DEVICE_TYPE: zha_p.DeviceType.THERMOSTAT,
                 INPUT_CLUSTERS: [PowerConfiguration.cluster_id],
                 OUTPUT_CLUSTERS: [],
             },
@@ -116,3 +113,57 @@ class SinopeTechnologiesThermostatCluster(CustomCluster, Thermostat):
             196: {INPUT_CLUSTERS: [PowerConfiguration]},
         }
     }
+
+
+class SinopeTH1400ZB(SinopeTechnologiesThermostat):
+    """TH1400ZB thermostat."""
+
+    signature = {
+        # <SimpleDescriptor endpoint=1 profile=260 device_type=769 device_version=1
+        # input_clusters=[0, 3, 4, 5, 513, 516, 1026, 1794, 2821, 65281]
+        # output_clusters=[10, 65281, 25]>
+        MODELS_INFO: [(SINOPE, "TH11400ZB")],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha_p.PROFILE_ID,
+                DEVICE_TYPE: zha_p.DeviceType.THERMOSTAT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    Thermostat.cluster_id,
+                    UserInterface.cluster_id,
+                    TemperatureMeasurement.cluster_id,
+                    Metering.cluster_id,
+                    Diagnostic.cluster_id,
+                    SINOPE_MANUFACTURER_CLUSTER_ID,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Time.cluster_id,
+                    Ota.cluster_id,
+                    SINOPE_MANUFACTURER_CLUSTER_ID,
+                ],
+            }
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                INPUT_CLUSTERS: [
+                    Basic,
+                    Identify,
+                    Groups,
+                    Scenes,
+                    UserInterface,
+                    TemperatureMeasurement,
+                    Metering,
+                    Diagnostic,
+                    SinopeTechnologiesThermostatCluster,
+                    SinopeTechnologiesManufacturerCluster,
+                ],
+                OUTPUT_CLUSTERS: [Time, Ota, SINOPE_MANUFACTURER_CLUSTER_ID],
+            }
+        }
+    }


Test patch: