From 52986840372a9e92ee9caa38006983558e9859dd Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 30 Nov 2025 22:49:32 +0100 Subject: [PATCH 1/3] Avoid adding switch if not supported Some weird devices have OnOff clusters with only partial attributes. Tuya remotes are one such device. --- .../data/devices/tz3000-gwkzibhs-ts004f.json | 12 +++++ .../data/devices/tz3000-kjfzuycl-ts004f.json | 12 +++++ .../data/devices/tze200-2aaelwxk-ts0225.json | 44 +------------------ zha/application/platforms/switch.py | 17 ++++++- 4 files changed, 41 insertions(+), 44 deletions(-) diff --git a/tests/data/devices/tz3000-gwkzibhs-ts004f.json b/tests/data/devices/tz3000-gwkzibhs-ts004f.json index a775d28d0..3da242073 100644 --- a/tests/data/devices/tz3000-gwkzibhs-ts004f.json +++ b/tests/data/devices/tz3000-gwkzibhs-ts004f.json @@ -123,6 +123,18 @@ "endpoint_attribute": "groups", "attributes": [] }, + { + "cluster_id": "0x0006", + "endpoint_attribute": "on_off", + "attributes": [ + { + "id": "0x0000", + "name": "on_off", + "zcl_type": "bool", + "unsupported": true + } + ] + }, { "cluster_id": "0x1000", "endpoint_attribute": "lightlink", diff --git a/tests/data/devices/tz3000-kjfzuycl-ts004f.json b/tests/data/devices/tz3000-kjfzuycl-ts004f.json index 6a82f0d53..0ca25020c 100644 --- a/tests/data/devices/tz3000-kjfzuycl-ts004f.json +++ b/tests/data/devices/tz3000-kjfzuycl-ts004f.json @@ -99,6 +99,18 @@ "endpoint_attribute": "groups", "attributes": [] }, + { + "cluster_id": "0x0006", + "endpoint_attribute": "on_off", + "attributes": [ + { + "id": "0x0000", + "name": "on_off", + "zcl_type": "bool", + "unsupported": true + } + ] + }, { "cluster_id": "0x1000", "endpoint_attribute": "lightlink", diff --git a/tests/data/devices/tze200-2aaelwxk-ts0225.json b/tests/data/devices/tze200-2aaelwxk-ts0225.json index 4c6863c3e..c82dfa914 100644 --- a/tests/data/devices/tze200-2aaelwxk-ts0225.json +++ b/tests/data/devices/tze200-2aaelwxk-ts0225.json @@ -501,7 +501,7 @@ "entity_category": null, "entity_registry_enabled_default": true, "enabled": true, - "primary": false, + "primary": true, "cluster_handlers": [ { "class_name": "OccupancySensingClusterHandler", @@ -1427,48 +1427,6 @@ } ], "switch": [ - { - "info_object": { - "fallback_name": null, - "unique_id": "ab:cd:ef:12:94:85:f7:5f-1-6", - "migrate_unique_ids": [], - "platform": "switch", - "class_name": "Switch", - "translation_key": "switch", - "translation_placeholders": null, - "device_class": null, - "state_class": null, - "entity_category": null, - "entity_registry_enabled_default": true, - "enabled": true, - "primary": true, - "cluster_handlers": [ - { - "class_name": "OnOffClusterHandler", - "generic_id": "cluster_handler_0x0006", - "endpoint_id": 1, - "cluster": { - "id": 6, - "name": "On/Off", - "type": "server" - }, - "id": "1:0x0006", - "unique_id": "ab:cd:ef:12:94:85:f7:5f:1:0x0006", - "status": "INITIALIZED", - "value_attribute": "on_off" - } - ], - "device_ieee": "ab:cd:ef:12:94:85:f7:5f", - "endpoint_id": 1, - "available": true, - "group_id": null - }, - "state": { - "class_name": "Switch", - "state": false, - "available": true - } - }, { "info_object": { "fallback_name": "LED indicator", diff --git a/zha/application/platforms/switch.py b/zha/application/platforms/switch.py index e1d98e8f7..9fab50d83 100644 --- a/zha/application/platforms/switch.py +++ b/zha/application/platforms/switch.py @@ -111,6 +111,7 @@ class Switch(PlatformEntity, BaseSwitch): _attr_translation_key = "switch" _attr_primary_weight = 10 + _attribute_name = OnOff.AttributeDefs.on_off.name def __init__( self, @@ -135,12 +136,26 @@ def on_add(self) -> None: ) ) + def _is_supported(self) -> bool: + if ( + self._attribute_name + in self._on_off_cluster_handler.cluster.unsupported_attributes + ): + _LOGGER.debug( + "%s is not supported - skipping %s entity creation", + self._attribute_name, + self.__class__.__name__, + ) + return False + + return super()._is_supported() + def handle_cluster_handler_attribute_updated( self, event: ClusterAttributeUpdatedEvent, # pylint: disable=unused-argument ) -> None: """Handle state update from cluster handler.""" - if event.attribute_name == OnOff.AttributeDefs.on_off.name: + if event.attribute_name == self._attribute_name: self.maybe_emit_state_changed_event() From b57adc1b5e1c53a70c2339a0170a9f5a7b17c061 Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 30 Nov 2025 23:33:45 +0100 Subject: [PATCH 2/3] Stay with current quirk version --- tests/data/devices/tz3000-gwkzibhs-ts004f.json | 12 ------------ tests/data/devices/tz3000-kjfzuycl-ts004f.json | 12 ------------ 2 files changed, 24 deletions(-) diff --git a/tests/data/devices/tz3000-gwkzibhs-ts004f.json b/tests/data/devices/tz3000-gwkzibhs-ts004f.json index 3da242073..a775d28d0 100644 --- a/tests/data/devices/tz3000-gwkzibhs-ts004f.json +++ b/tests/data/devices/tz3000-gwkzibhs-ts004f.json @@ -123,18 +123,6 @@ "endpoint_attribute": "groups", "attributes": [] }, - { - "cluster_id": "0x0006", - "endpoint_attribute": "on_off", - "attributes": [ - { - "id": "0x0000", - "name": "on_off", - "zcl_type": "bool", - "unsupported": true - } - ] - }, { "cluster_id": "0x1000", "endpoint_attribute": "lightlink", diff --git a/tests/data/devices/tz3000-kjfzuycl-ts004f.json b/tests/data/devices/tz3000-kjfzuycl-ts004f.json index 0ca25020c..6a82f0d53 100644 --- a/tests/data/devices/tz3000-kjfzuycl-ts004f.json +++ b/tests/data/devices/tz3000-kjfzuycl-ts004f.json @@ -99,18 +99,6 @@ "endpoint_attribute": "groups", "attributes": [] }, - { - "cluster_id": "0x0006", - "endpoint_attribute": "on_off", - "attributes": [ - { - "id": "0x0000", - "name": "on_off", - "zcl_type": "bool", - "unsupported": true - } - ] - }, { "cluster_id": "0x1000", "endpoint_attribute": "lightlink", From 57af64813605ed412d5d090f4ec12f12986b3bbf Mon Sep 17 00:00:00 2001 From: Joakim Plate Date: Sun, 30 Nov 2025 03:56:41 +0100 Subject: [PATCH 3/3] Add tuya switch_mode for rotary knobs --- .../data/devices/tz3000-gwkzibhs-ts004f.json | 80 ++++++++++++++++--- .../data/devices/tz3000-kjfzuycl-ts004f.json | 53 +++++++++++- zha/application/platforms/select.py | 13 +++ zha/zigbee/cluster_handlers/general.py | 1 + 4 files changed, 137 insertions(+), 10 deletions(-) diff --git a/tests/data/devices/tz3000-gwkzibhs-ts004f.json b/tests/data/devices/tz3000-gwkzibhs-ts004f.json index a775d28d0..59c9bfac9 100644 --- a/tests/data/devices/tz3000-gwkzibhs-ts004f.json +++ b/tests/data/devices/tz3000-gwkzibhs-ts004f.json @@ -9,7 +9,9 @@ "name": "_TZ3000_gwkzibhs TS004F", "quirk_applied": true, "quirk_class": "zhaquirks.tuya.ts004f.TuyaSmartRemote004FROK", - "exposes_features": [], + "exposes_features": [ + "tuya.plug_on_off_attributes" + ], "manufacturer_code": 4742, "power_source": "Battery or Unknown", "lqi": 182, @@ -123,6 +125,24 @@ "endpoint_attribute": "groups", "attributes": [] }, + { + "cluster_id": "0x0006", + "endpoint_attribute": "on_off", + "attributes": [ + { + "id": "0x0000", + "name": "on_off", + "zcl_type": "bool", + "unsupported": true + }, + { + "id": "0x8004", + "name": "switch_mode", + "zcl_type": "enum8", + "value": 0 + } + ] + }, { "cluster_id": "0x1000", "endpoint_attribute": "lightlink", @@ -148,14 +168,7 @@ { "cluster_id": "0x0006", "endpoint_attribute": "TS004X_cluster", - "attributes": [ - { - "id": "0x8004", - "name": "switch_mode", - "zcl_type": "enum8", - "value": 0 - } - ] + "attributes": [] }, { "cluster_id": "0x0008", @@ -297,6 +310,55 @@ } } ], + "select": [ + { + "info_object": { + "fallback_name": null, + "unique_id": "ab:cd:ef:12:e5:79:a6:00-1-6-switch_mode", + "migrate_unique_ids": [], + "platform": "select", + "class_name": "TuyaSwitchModeSelectEntity", + "translation_key": "switch_mode", + "translation_placeholders": null, + "device_class": null, + "state_class": null, + "entity_category": "config", + "entity_registry_enabled_default": true, + "enabled": true, + "primary": false, + "cluster_handlers": [ + { + "class_name": "OnOffClusterHandler", + "generic_id": "cluster_handler_0x0006", + "endpoint_id": 1, + "cluster": { + "id": 6, + "name": "On/Off", + "type": "server" + }, + "id": "1:0x0006", + "unique_id": "ab:cd:ef:12:e5:79:a6:00:1:0x0006", + "status": "INITIALIZED", + "value_attribute": "on_off" + } + ], + "device_ieee": "ab:cd:ef:12:e5:79:a6:00", + "endpoint_id": 1, + "available": true, + "group_id": null, + "enum": "SwitchMode", + "options": [ + "Command", + "Event" + ] + }, + "state": { + "class_name": "TuyaSwitchModeSelectEntity", + "available": true, + "state": "Command" + } + } + ], "sensor": [ { "info_object": { diff --git a/tests/data/devices/tz3000-kjfzuycl-ts004f.json b/tests/data/devices/tz3000-kjfzuycl-ts004f.json index 6a82f0d53..8b8106f1c 100644 --- a/tests/data/devices/tz3000-kjfzuycl-ts004f.json +++ b/tests/data/devices/tz3000-kjfzuycl-ts004f.json @@ -9,7 +9,9 @@ "name": "_TZ3000_kjfzuycl TS004F", "quirk_applied": true, "quirk_class": "zhaquirks.tuya.ts004f.TuyaSmartRemote004FSK", - "exposes_features": [], + "exposes_features": [ + "tuya.plug_on_off_attributes" + ], "manufacturer_code": 4098, "power_source": "Battery or Unknown", "lqi": null, @@ -99,6 +101,11 @@ "endpoint_attribute": "groups", "attributes": [] }, + { + "cluster_id": "0x0006", + "endpoint_attribute": "on_off", + "attributes": [] + }, { "cluster_id": "0x1000", "endpoint_attribute": "lightlink", @@ -386,6 +393,50 @@ ] } ], + "switch": [ + { + "info_object": { + "fallback_name": null, + "unique_id": "38:5b:44:ff:fe:36:af:0f-1-6", + "migrate_unique_ids": [], + "platform": "switch", + "class_name": "Switch", + "translation_key": "switch", + "translation_placeholders": null, + "device_class": null, + "state_class": null, + "entity_category": null, + "entity_registry_enabled_default": true, + "enabled": true, + "primary": true, + "cluster_handlers": [ + { + "class_name": "OnOffClusterHandler", + "generic_id": "cluster_handler_0x0006", + "endpoint_id": 1, + "cluster": { + "id": 6, + "name": "On/Off", + "type": "server" + }, + "id": "1:0x0006", + "unique_id": "38:5b:44:ff:fe:36:af:0f:1:0x0006", + "status": "INITIALIZED", + "value_attribute": "on_off" + } + ], + "device_ieee": "38:5b:44:ff:fe:36:af:0f", + "endpoint_id": 1, + "available": true, + "group_id": null + }, + "state": { + "class_name": "Switch", + "state": false, + "available": true + } + } + ], "update": [ { "info_object": { diff --git a/zha/application/platforms/select.py b/zha/application/platforms/select.py index a943d5f82..151289381 100644 --- a/zha/application/platforms/select.py +++ b/zha/application/platforms/select.py @@ -8,6 +8,7 @@ import logging from typing import TYPE_CHECKING, Any +from zhaquirks import tuya from zhaquirks.danfoss import thermostat as danfoss_thermostat from zhaquirks.quirk_ids import ( DANFOSS_ALLY_THERMOSTAT, @@ -317,6 +318,18 @@ class TuyaBacklightModeSelectEntity(ZCLEnumSelectEntity): _attr_translation_key: str = "backlight_mode" +@CONFIG_DIAGNOSTIC_MATCH( + cluster_handler_names=CLUSTER_HANDLER_ON_OFF, exposed_features=TUYA_PLUG_ONOFF +) +class TuyaSwitchModeSelectEntity(ZCLEnumSelectEntity): + """Representation of a ZHA backlight mode select entity.""" + + _unique_id_suffix = "switch_mode" + _attribute_name = "switch_mode" + _enum = tuya.SwitchMode + _attr_translation_key: str = "switch_mode" + + class MoesBacklightMode(types.enum8): """MOES switch backlight mode enum.""" diff --git a/zha/zigbee/cluster_handlers/general.py b/zha/zigbee/cluster_handlers/general.py index e47517495..95f1fbd59 100644 --- a/zha/zigbee/cluster_handlers/general.py +++ b/zha/zigbee/cluster_handlers/general.py @@ -550,6 +550,7 @@ def __init__(self, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> None: self.ZCL_INIT_ATTRS["backlight_mode"] = True self.ZCL_INIT_ATTRS["power_on_state"] = True self.ZCL_INIT_ATTRS["child_lock"] = True + self.ZCL_INIT_ATTRS["switch_mode"] = True @classmethod def matches(cls, cluster: zigpy.zcl.Cluster, endpoint: Endpoint) -> bool: