From cff1380fcb942afa698b5999d96d540fa4c9c77f Mon Sep 17 00:00:00 2001 From: Jonny Date: Tue, 8 Mar 2022 18:33:03 -0800 Subject: [PATCH 1/7] Add device support for TS110E This device uses a non-default set level command. Tested in local docker environment. Thanks to @javicalle this was possible. Fix #1415 --- zhaquirks/tuya/ts110e.py | 142 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 zhaquirks/tuya/ts110e.py diff --git a/zhaquirks/tuya/ts110e.py b/zhaquirks/tuya/ts110e.py new file mode 100644 index 0000000000..1b339a098d --- /dev/null +++ b/zhaquirks/tuya/ts110e.py @@ -0,0 +1,142 @@ +"""Tuya Dimmer TS110E.""" +from zigpy.profiles import zha +import zigpy.types as t +from zigpy.zcl.clusters.general import ( + Basic, + GreenPowerProxy, + Groups, + LevelControl, + OnOff, + Ota, + Scenes, + Time, +) + +from zigpy.zcl import foundation +from typing import Optional, Union + +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) + +from zhaquirks.tuya import ( + TuyaDimmerSwitch, + TuyaZBExternalSwitchTypeCluster, +) + +class TuyaLevelPayload(t.Struct): + """Tuya Level payload.""" + + level: t.uint16_t + transtime: t.uint16_t + +TUYA_LEVEL_ATTRIBUTE = 0xF000 +TUYA_CUSTOM_LEVEL_COMMAND = 0x00F0 + +class F000LevelControlCluster(LevelControl): + """LevelControlCluster that reports to attrid 0xF000.""" + + manufacturer_attributes = {TUYA_LEVEL_ATTRIBUTE: ("manufacturer_current_level", t.uint16_t),} + + manufacturer_server_commands = { + TUYA_CUSTOM_LEVEL_COMMAND: ("moveToLevelTuya", (TuyaLevelPayload,), False), + } + + # 0xF000 reported values are 0-1000, convert to 0-255 + def _update_attribute(self, attrid, value): + if attrid == TUYA_LEVEL_ATTRIBUTE: + value = (value * 255) // 1000 + attrid = 0x0000 + + super()._update_attribute(attrid, value) + + async def command( + self, + command_id: Union[foundation.Command, 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.""" + self.debug( + "Sending Cluster Command. Cluster Command is %x, Arguments are %s", + command_id, + args, + ) + # move_to_level, move, move_to_level_with_on_off + if command_id in (0x0000, 0x0001, 0x0004): + # convert dim values to 0-1000 + brightness = (args[0] * 1000) // 255 + return await super().command( + TUYA_CUSTOM_LEVEL_COMMAND, + TuyaLevelPayload(level=brightness, transtime=0), + manufacturer=manufacturer, + expect_reply=expect_reply, + tsn=tsn + ) + + return super().command(command_id, *args, manufacturer, expect_reply, tsn) + + +class DimmerSwitchWithNeutral1Gang(TuyaDimmerSwitch): + """Tuya Dimmer Switch Module With Neutral 1 Gang""" + + signature = { + MODELS_INFO: [("_TZ3210_ngqk6jia", "TS110E")], + ENDPOINTS: { + # + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.DIMMABLE_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + TuyaZBExternalSwitchTypeCluster.cluster_id, + ], + OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], + }, + 242: { + # Date: Tue, 8 Mar 2022 18:40:57 -0800 Subject: [PATCH 2/7] Lint --- zhaquirks/tuya/ts110e.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/zhaquirks/tuya/ts110e.py b/zhaquirks/tuya/ts110e.py index 1b339a098d..7b631acb1c 100644 --- a/zhaquirks/tuya/ts110e.py +++ b/zhaquirks/tuya/ts110e.py @@ -29,19 +29,24 @@ TuyaZBExternalSwitchTypeCluster, ) + +TUYA_LEVEL_ATTRIBUTE = 0xF000 +TUYA_CUSTOM_LEVEL_COMMAND = 0x00F0 + + class TuyaLevelPayload(t.Struct): """Tuya Level payload.""" level: t.uint16_t transtime: t.uint16_t -TUYA_LEVEL_ATTRIBUTE = 0xF000 -TUYA_CUSTOM_LEVEL_COMMAND = 0x00F0 class F000LevelControlCluster(LevelControl): """LevelControlCluster that reports to attrid 0xF000.""" - manufacturer_attributes = {TUYA_LEVEL_ATTRIBUTE: ("manufacturer_current_level", t.uint16_t),} + manufacturer_attributes = { + TUYA_LEVEL_ATTRIBUTE: ("manufacturer_current_level", t.uint16_t), + } manufacturer_server_commands = { TUYA_CUSTOM_LEVEL_COMMAND: ("moveToLevelTuya", (TuyaLevelPayload,), False), @@ -74,11 +79,11 @@ async def command( # convert dim values to 0-1000 brightness = (args[0] * 1000) // 255 return await super().command( - TUYA_CUSTOM_LEVEL_COMMAND, - TuyaLevelPayload(level=brightness, transtime=0), - manufacturer=manufacturer, + TUYA_CUSTOM_LEVEL_COMMAND, + TuyaLevelPayload(level=brightness, transtime=0), + manufacturer=manufacturer, expect_reply=expect_reply, - tsn=tsn + tsn=tsn, ) return super().command(command_id, *args, manufacturer, expect_reply, tsn) From afd2b8a2af75e8598f5b9b17d949c9efa4b3da5f Mon Sep 17 00:00:00 2001 From: Jonny Date: Tue, 8 Mar 2022 18:47:07 -0800 Subject: [PATCH 3/7] Lint --- zhaquirks/tuya/ts110e.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/zhaquirks/tuya/ts110e.py b/zhaquirks/tuya/ts110e.py index 7b631acb1c..df92fa4564 100644 --- a/zhaquirks/tuya/ts110e.py +++ b/zhaquirks/tuya/ts110e.py @@ -1,6 +1,9 @@ """Tuya Dimmer TS110E.""" +from typing import Optional, Union + from zigpy.profiles import zha import zigpy.types as t +from zigpy.zcl import foundation from zigpy.zcl.clusters.general import ( Basic, GreenPowerProxy, @@ -12,9 +15,6 @@ Time, ) -from zigpy.zcl import foundation -from typing import Optional, Union - from zhaquirks.const import ( DEVICE_TYPE, ENDPOINTS, @@ -23,12 +23,7 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) - -from zhaquirks.tuya import ( - TuyaDimmerSwitch, - TuyaZBExternalSwitchTypeCluster, -) - +from zhaquirks.tuya import TuyaDimmerSwitch, TuyaZBExternalSwitchTypeCluster TUYA_LEVEL_ATTRIBUTE = 0xF000 TUYA_CUSTOM_LEVEL_COMMAND = 0x00F0 @@ -90,7 +85,7 @@ async def command( class DimmerSwitchWithNeutral1Gang(TuyaDimmerSwitch): - """Tuya Dimmer Switch Module With Neutral 1 Gang""" + """Tuya Dimmer Switch Module With Neutral 1 Gang.""" signature = { MODELS_INFO: [("_TZ3210_ngqk6jia", "TS110E")], From 92a2f41a202e5d5d56bc691f98f35473185e58b5 Mon Sep 17 00:00:00 2001 From: Jonny Date: Sat, 12 Mar 2022 15:34:47 -0800 Subject: [PATCH 4/7] Update ts110e.py --- zhaquirks/tuya/ts110e.py | 49 +++++++++++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 11 deletions(-) diff --git a/zhaquirks/tuya/ts110e.py b/zhaquirks/tuya/ts110e.py index df92fa4564..fd3fb94cea 100644 --- a/zhaquirks/tuya/ts110e.py +++ b/zhaquirks/tuya/ts110e.py @@ -1,9 +1,6 @@ """Tuya Dimmer TS110E.""" -from typing import Optional, Union - from zigpy.profiles import zha import zigpy.types as t -from zigpy.zcl import foundation from zigpy.zcl.clusters.general import ( Basic, GreenPowerProxy, @@ -15,6 +12,9 @@ Time, ) +from zigpy.zcl import foundation +from typing import Optional, Union + from zhaquirks.const import ( DEVICE_TYPE, ENDPOINTS, @@ -23,9 +23,17 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) -from zhaquirks.tuya import TuyaDimmerSwitch, TuyaZBExternalSwitchTypeCluster + +from zhaquirks.tuya import ( + TuyaDimmerSwitch, + TuyaZBExternalSwitchTypeCluster, +) + TUYA_LEVEL_ATTRIBUTE = 0xF000 +TUYA_BULB_TYPE_ATTRIBUTE = 0xFC02 +TUYA_MIN_LEVEL_ATTRIBUTE = 0xFC03 +TUYA_MAX_LEVEL_ATTRIBUTE = 0xFC04 TUYA_CUSTOM_LEVEL_COMMAND = 0x00F0 @@ -36,21 +44,36 @@ class TuyaLevelPayload(t.Struct): transtime: t.uint16_t +class TuyaBulbType(t.enum8): + """Tuya bulb type.""" + + LED = 0x00 + INCANDESCENT = 0x01 + HALOGEN = 0x02 + + class F000LevelControlCluster(LevelControl): """LevelControlCluster that reports to attrid 0xF000.""" manufacturer_attributes = { TUYA_LEVEL_ATTRIBUTE: ("manufacturer_current_level", t.uint16_t), + TUYA_BULB_TYPE_ATTRIBUTE: ("bulb_type", TuyaBulbType), # 0xFC02 + TUYA_MIN_LEVEL_ATTRIBUTE: ("manufacturer_min_level", t.uint16_t), # 0xFC03 + TUYA_MAX_LEVEL_ATTRIBUTE: ("manufacturer_max_level", t.uint16_t), # 0xFC04 } manufacturer_server_commands = { TUYA_CUSTOM_LEVEL_COMMAND: ("moveToLevelTuya", (TuyaLevelPayload,), False), } - # 0xF000 reported values are 0-1000, convert to 0-255 + # 0xF000 reported values are 10-1000, convert to 0-254 def _update_attribute(self, attrid, value): if attrid == TUYA_LEVEL_ATTRIBUTE: - value = (value * 255) // 1000 + self.debug( + "Getting brightness %s", + value, + ) + value = (value + 4 - 10) * 254 // (1000 - 10) attrid = 0x0000 super()._update_attribute(attrid, value) @@ -71,8 +94,12 @@ async def command( ) # move_to_level, move, move_to_level_with_on_off if command_id in (0x0000, 0x0001, 0x0004): - # convert dim values to 0-1000 - brightness = (args[0] * 1000) // 255 + # convert dim values to 10-1000 + brightness = args[0] * (1000 - 10) // 254 + 10 + self.debug( + "Setting brightness to %s", + brightness, + ) return await super().command( TUYA_CUSTOM_LEVEL_COMMAND, TuyaLevelPayload(level=brightness, transtime=0), @@ -85,7 +112,7 @@ async def command( class DimmerSwitchWithNeutral1Gang(TuyaDimmerSwitch): - """Tuya Dimmer Switch Module With Neutral 1 Gang.""" + """Tuya Dimmer Switch Module With Neutral 1 Gang""" signature = { MODELS_INFO: [("_TZ3210_ngqk6jia", "TS110E")], @@ -102,7 +129,7 @@ class DimmerSwitchWithNeutral1Gang(TuyaDimmerSwitch): Scenes.cluster_id, OnOff.cluster_id, LevelControl.cluster_id, - TuyaZBExternalSwitchTypeCluster.cluster_id, + TuyaZBExternalSwitchTypeCluster, ], OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], }, @@ -128,7 +155,7 @@ class DimmerSwitchWithNeutral1Gang(TuyaDimmerSwitch): Scenes.cluster_id, OnOff.cluster_id, F000LevelControlCluster, - TuyaZBExternalSwitchTypeCluster.cluster_id, + TuyaZBExternalSwitchTypeCluster, ], OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], }, From cdd118425f8740bdcfb435a09a3eeb8525431e81 Mon Sep 17 00:00:00 2001 From: Jonny Date: Sat, 12 Mar 2022 15:36:32 -0800 Subject: [PATCH 5/7] Update ts110e.py --- zhaquirks/tuya/ts110e.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/zhaquirks/tuya/ts110e.py b/zhaquirks/tuya/ts110e.py index fd3fb94cea..14e2b1bf64 100644 --- a/zhaquirks/tuya/ts110e.py +++ b/zhaquirks/tuya/ts110e.py @@ -1,6 +1,9 @@ """Tuya Dimmer TS110E.""" +from typing import Optional, Union + from zigpy.profiles import zha import zigpy.types as t +from zigpy.zcl import foundation from zigpy.zcl.clusters.general import ( Basic, GreenPowerProxy, @@ -12,9 +15,6 @@ Time, ) -from zigpy.zcl import foundation -from typing import Optional, Union - from zhaquirks.const import ( DEVICE_TYPE, ENDPOINTS, @@ -24,10 +24,7 @@ PROFILE_ID, ) -from zhaquirks.tuya import ( - TuyaDimmerSwitch, - TuyaZBExternalSwitchTypeCluster, -) +from zhaquirks.tuya import TuyaDimmerSwitch, TuyaZBExternalSwitchTypeCluster TUYA_LEVEL_ATTRIBUTE = 0xF000 From 25ab68997473f015b30d4bdedd382f8d9e870dca Mon Sep 17 00:00:00 2001 From: Jonny Date: Sun, 12 Jun 2022 18:52:31 -0700 Subject: [PATCH 6/7] Update ts110e.py --- zhaquirks/tuya/ts110e.py | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/zhaquirks/tuya/ts110e.py b/zhaquirks/tuya/ts110e.py index 14e2b1bf64..7e303565af 100644 --- a/zhaquirks/tuya/ts110e.py +++ b/zhaquirks/tuya/ts110e.py @@ -24,7 +24,11 @@ PROFILE_ID, ) -from zhaquirks.tuya import TuyaDimmerSwitch, TuyaZBExternalSwitchTypeCluster +from zhaquirks.tuya import ( + TuyaDimmerSwitch, + TuyaZBExternalSwitchTypeCluster, + NoManufacturerCluster, +) TUYA_LEVEL_ATTRIBUTE = 0xF000 @@ -49,19 +53,29 @@ class TuyaBulbType(t.enum8): HALOGEN = 0x02 -class F000LevelControlCluster(LevelControl): +class F000LevelControlCluster(NoManufacturerCluster, LevelControl): """LevelControlCluster that reports to attrid 0xF000.""" - manufacturer_attributes = { - TUYA_LEVEL_ATTRIBUTE: ("manufacturer_current_level", t.uint16_t), - TUYA_BULB_TYPE_ATTRIBUTE: ("bulb_type", TuyaBulbType), # 0xFC02 - TUYA_MIN_LEVEL_ATTRIBUTE: ("manufacturer_min_level", t.uint16_t), # 0xFC03 - TUYA_MAX_LEVEL_ATTRIBUTE: ("manufacturer_max_level", t.uint16_t), # 0xFC04 - } - - manufacturer_server_commands = { - TUYA_CUSTOM_LEVEL_COMMAND: ("moveToLevelTuya", (TuyaLevelPayload,), False), - } + server_commands = LevelControl.server_commands.copy() + server_commands[TUYA_CUSTOM_LEVEL_COMMAND] = foundation.ZCLCommandDef( + "moveToLevelTuya", + (TuyaLevelPayload,), + is_manufacturer_specific=False, + ) + + attributes = LevelControl.attributes.copy() + attributes.update( + { + # 0xF000 + TUYA_LEVEL_ATTRIBUTE: ("manufacturer_current_level", t.uint16_t), + # 0xFC02 + TUYA_BULB_TYPE_ATTRIBUTE: ("bulb_type", TuyaBulbType), + # 0xFC03 + TUYA_MIN_LEVEL_ATTRIBUTE: ("manufacturer_min_level", t.uint16_t), + # 0xFC04 + TUYA_MAX_LEVEL_ATTRIBUTE: ("manufacturer_max_level", t.uint16_t), + } + ) # 0xF000 reported values are 10-1000, convert to 0-254 def _update_attribute(self, attrid, value): @@ -126,7 +140,7 @@ class DimmerSwitchWithNeutral1Gang(TuyaDimmerSwitch): Scenes.cluster_id, OnOff.cluster_id, LevelControl.cluster_id, - TuyaZBExternalSwitchTypeCluster, + TuyaZBExternalSwitchTypeCluster.cluster_id, ], OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], }, From b4c49fd637b4607120426fd211311f65b617f4ed Mon Sep 17 00:00:00 2001 From: Jonny Date: Mon, 13 Jun 2022 08:10:13 -0700 Subject: [PATCH 7/7] Update ts110e.py --- zhaquirks/tuya/ts110e.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/zhaquirks/tuya/ts110e.py b/zhaquirks/tuya/ts110e.py index 7e303565af..21cbae5929 100644 --- a/zhaquirks/tuya/ts110e.py +++ b/zhaquirks/tuya/ts110e.py @@ -23,14 +23,12 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) - from zhaquirks.tuya import ( + NoManufacturerCluster, TuyaDimmerSwitch, TuyaZBExternalSwitchTypeCluster, - NoManufacturerCluster, ) - TUYA_LEVEL_ATTRIBUTE = 0xF000 TUYA_BULB_TYPE_ATTRIBUTE = 0xFC02 TUYA_MIN_LEVEL_ATTRIBUTE = 0xFC03 @@ -123,7 +121,7 @@ async def command( class DimmerSwitchWithNeutral1Gang(TuyaDimmerSwitch): - """Tuya Dimmer Switch Module With Neutral 1 Gang""" + """Tuya Dimmer Switch Module With Neutral 1 Gang.""" signature = { MODELS_INFO: [("_TZ3210_ngqk6jia", "TS110E")],