From 2cd60fe8e467c477724032c91b3a1e2e1afec5cd Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 11 Mar 2024 13:11:16 +0100 Subject: [PATCH 01/26] Fix heat mode for legrand outlet --- zhaquirks/legrand/cable_outlet.py | 75 ++++++++++++++----------------- 1 file changed, 34 insertions(+), 41 deletions(-) diff --git a/zhaquirks/legrand/cable_outlet.py b/zhaquirks/legrand/cable_outlet.py index 2f18979696..4e61e9f5f7 100644 --- a/zhaquirks/legrand/cable_outlet.py +++ b/zhaquirks/legrand/cable_outlet.py @@ -1,5 +1,6 @@ """Module for Legrand Cable Outlet (with pilot wire functionality).""" + from zigpy.profiles import zgp, zha from zigpy.quirks import CustomCluster, CustomDevice import zigpy.types as t @@ -13,13 +14,7 @@ Scenes, ) from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement -from zigpy.zcl.foundation import ( - BaseAttributeDefs, - BaseCommandDefs, - Direction, - ZCLAttributeDef, - ZCLCommandDef, -) +from zigpy.zcl.foundation import Direction, ZCLCommandDef from zhaquirks.const import ( DEVICE_TYPE, @@ -33,10 +28,7 @@ MANUFACTURER_SPECIFIC_CLUSTER_ID_2 = 0xFC40 # 64576 - -class DeviceMode(t.enum16): - PILOT_OFF = 0x0100 - PILOT_ON = 0x0200 +HEAT_MODE_ATTR = 0x00 class LegrandCluster(CustomCluster): @@ -45,48 +37,49 @@ class LegrandCluster(CustomCluster): cluster_id = MANUFACTURER_SPECIFIC_CLUSTER_ID name = "LegrandCluster" ep_attribute = "legrand_cluster" - - class AttributeDefs(BaseAttributeDefs): - device_mode = ZCLAttributeDef( - id=0x0000, - type=t.data16, # DeviceMode - is_manufacturer_specific=True, - ) - led_dark = ZCLAttributeDef( - id=0x0001, - type=t.Bool, - is_manufacturer_specific=True, - ) - led_on = ZCLAttributeDef( - id=0x0002, - type=t.Bool, - is_manufacturer_specific=True, - ) - - -class PilotWireMode(t.enum8): - COMFORT = 0x00 - COMFORT_MINUS_1 = 0x01 - COMFORT_MINUS_2 = 0x02 - ECO = 0x03 - FROST_PROTECTION = 0x04 - OFF = 0x05 + attributes = { + 0x0000: ("device_mode", t.data16, True), + 0x0001: ("led_dark", t.Bool, True), + 0x0002: ("led_on", t.Bool, True), + } class LegrandCableOutletCluster(CustomCluster): """Legrand second manufacturer-specific cluster.""" + class HeatMode(t.enum8): + COMFORT = 0x00 + COMFORT_MINUS_1 = 0x01 + COMFORT_MINUS_2 = 0x02 + ECO = 0x03 + FROST_PROTECTION = 0x04 + OFF = 0x05 + cluster_id = MANUFACTURER_SPECIFIC_CLUSTER_ID_2 name = "CableOutlet" ep_attribute = "cable_outlet_cluster" - class ServerCommandDefs(BaseCommandDefs): - set_pilot_wire_mode = ZCLCommandDef( - id=0x00, - schema={"mode": PilotWireMode}, + attributes = {HEAT_MODE_ATTR: ("heat_mode", HeatMode, True)} + + server_commands = { + HEAT_MODE_ATTR: ZCLCommandDef( + "set_heat_mode", + schema={"mode": HeatMode}, direction=Direction.Client_to_Server, is_manufacturer_specific=True, ) + } + + async def write_attributes(self, attributes, manufacturer=None) -> list: + attrs = {} + for attr, value in attributes.items(): + attr_def = self.find_attribute(attr) + attr_id = attr_def.id + if attr_id == HEAT_MODE_ATTR: + await self.set_heat_mode(value, manufacturer=manufacturer) + else: + attrs[attr] = value + return await super().write_attributes(attrs, manufacturer) class Legrand064882CableOutlet(CustomDevice): From b83fbdd754a021e768716ab3720d4e55d682f6ff Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 11 Mar 2024 14:55:17 +0100 Subject: [PATCH 02/26] Use AttributeDefs and ServerCommandDefs --- zhaquirks/legrand/cable_outlet.py | 66 ++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 19 deletions(-) diff --git a/zhaquirks/legrand/cable_outlet.py b/zhaquirks/legrand/cable_outlet.py index 4e61e9f5f7..9f1ac8ac39 100644 --- a/zhaquirks/legrand/cable_outlet.py +++ b/zhaquirks/legrand/cable_outlet.py @@ -14,7 +14,13 @@ Scenes, ) from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement -from zigpy.zcl.foundation import Direction, ZCLCommandDef +from zigpy.zcl.foundation import ( + BaseAttributeDefs, + BaseCommandDefs, + Direction, + ZCLAttributeDef, + ZCLCommandDef, +) from zhaquirks.const import ( DEVICE_TYPE, @@ -31,44 +37,66 @@ HEAT_MODE_ATTR = 0x00 +class DeviceMode(t.enum16): + PILOT_OFF = 0x0100 + PILOT_ON = 0x0200 + + class LegrandCluster(CustomCluster): """LegrandCluster.""" cluster_id = MANUFACTURER_SPECIFIC_CLUSTER_ID name = "LegrandCluster" ep_attribute = "legrand_cluster" - attributes = { - 0x0000: ("device_mode", t.data16, True), - 0x0001: ("led_dark", t.Bool, True), - 0x0002: ("led_on", t.Bool, True), - } + + class AttributeDefs(BaseAttributeDefs): + device_mode = ZCLAttributeDef( + id=0x0000, + type=t.data16, # DeviceMode + is_manufacturer_specific=True, + ) + led_dark = ZCLAttributeDef( + id=0x0001, + type=t.Bool, + is_manufacturer_specific=True, + ) + led_on = ZCLAttributeDef( + id=0x0002, + type=t.Bool, + is_manufacturer_specific=True, + ) + + +class HeatMode(t.enum8): + COMFORT = 0x00 + COMFORT_MINUS_1 = 0x01 + COMFORT_MINUS_2 = 0x02 + ECO = 0x03 + FROST_PROTECTION = 0x04 + OFF = 0x05 class LegrandCableOutletCluster(CustomCluster): """Legrand second manufacturer-specific cluster.""" - class HeatMode(t.enum8): - COMFORT = 0x00 - COMFORT_MINUS_1 = 0x01 - COMFORT_MINUS_2 = 0x02 - ECO = 0x03 - FROST_PROTECTION = 0x04 - OFF = 0x05 - cluster_id = MANUFACTURER_SPECIFIC_CLUSTER_ID_2 name = "CableOutlet" ep_attribute = "cable_outlet_cluster" - attributes = {HEAT_MODE_ATTR: ("heat_mode", HeatMode, True)} + class AttributeDefs(BaseAttributeDefs): + device_mode = ZCLAttributeDef( + id=HEAT_MODE_ATTR, + type=HeatMode, + is_manufacturer_specific=True, + ) - server_commands = { - HEAT_MODE_ATTR: ZCLCommandDef( - "set_heat_mode", + class ServerCommandDefs(BaseCommandDefs): + set_heat_mode = ZCLCommandDef( + id=0x00, schema={"mode": HeatMode}, direction=Direction.Client_to_Server, is_manufacturer_specific=True, ) - } async def write_attributes(self, attributes, manufacturer=None) -> list: attrs = {} From 114f27c7b8a930396d6a1817cfc91937f1b49181 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 11 Mar 2024 15:22:06 +0100 Subject: [PATCH 03/26] Add tests --- tests/test_legrand.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/test_legrand.py b/tests/test_legrand.py index ab98c25f91..5535b0f45b 100644 --- a/tests/test_legrand.py +++ b/tests/test_legrand.py @@ -1,4 +1,6 @@ """Tests for Legrand.""" +from unittest import mock + import pytest import zhaquirks @@ -54,3 +56,25 @@ def test_light_switch_with_neutral_signature(assert_signature_matches_quirk): assert_signature_matches_quirk( zhaquirks.legrand.switch.LightSwitchWithNeutral, signature ) + + +async def test_cable_outlet_write_attrs(zigpy_device_from_quirk): + """Test Legrand cable outlet heat mode attr writing.""" + + device = zigpy_device_from_quirk( + zhaquirks.legrand.cable_outlet.Legrand064882CableOutlet + ) + cable_outlet_cluster = device.endpoints[1].cable_outlet_cluster + cable_outlet_cluster._write_attributes = mock.AsyncMock() + cable_outlet_cluster.set_heat_mode = mock.AsyncMock() + + await cable_outlet_cluster.write_attributes({0x00: 0x02}, manufacturer=0xFC40) + + cable_outlet_cluster.set_heat_mode.assert_awaited_with( + 0x02, + manufacturer=0xFC40, + ) + cable_outlet_cluster._write_attributes.assert_awaited_with( + [], + manufacturer=0xFC40, + ) From 1f7b88a30ac149eeef68dcf8c52cc2238cfb15b2 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 11 Mar 2024 15:33:12 +0100 Subject: [PATCH 04/26] Fix name --- zhaquirks/legrand/cable_outlet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zhaquirks/legrand/cable_outlet.py b/zhaquirks/legrand/cable_outlet.py index 9f1ac8ac39..d9786c0f46 100644 --- a/zhaquirks/legrand/cable_outlet.py +++ b/zhaquirks/legrand/cable_outlet.py @@ -84,7 +84,7 @@ class LegrandCableOutletCluster(CustomCluster): ep_attribute = "cable_outlet_cluster" class AttributeDefs(BaseAttributeDefs): - device_mode = ZCLAttributeDef( + heat_mode = ZCLAttributeDef( id=HEAT_MODE_ATTR, type=HeatMode, is_manufacturer_specific=True, @@ -92,7 +92,7 @@ class AttributeDefs(BaseAttributeDefs): class ServerCommandDefs(BaseCommandDefs): set_heat_mode = ZCLCommandDef( - id=0x00, + id=HEAT_MODE_ATTR, schema={"mode": HeatMode}, direction=Direction.Client_to_Server, is_manufacturer_specific=True, From 50ac382378fc3797d3c32ef60c8be80ab1cfd8a4 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 11 Mar 2024 15:59:01 +0100 Subject: [PATCH 05/26] Update case for enum --- zhaquirks/legrand/cable_outlet.py | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/zhaquirks/legrand/cable_outlet.py b/zhaquirks/legrand/cable_outlet.py index d9786c0f46..4857e4dc9c 100644 --- a/zhaquirks/legrand/cable_outlet.py +++ b/zhaquirks/legrand/cable_outlet.py @@ -37,11 +37,6 @@ HEAT_MODE_ATTR = 0x00 -class DeviceMode(t.enum16): - PILOT_OFF = 0x0100 - PILOT_ON = 0x0200 - - class LegrandCluster(CustomCluster): """LegrandCluster.""" @@ -52,7 +47,7 @@ class LegrandCluster(CustomCluster): class AttributeDefs(BaseAttributeDefs): device_mode = ZCLAttributeDef( id=0x0000, - type=t.data16, # DeviceMode + type=t.data16, is_manufacturer_specific=True, ) led_dark = ZCLAttributeDef( @@ -68,12 +63,12 @@ class AttributeDefs(BaseAttributeDefs): class HeatMode(t.enum8): - COMFORT = 0x00 - COMFORT_MINUS_1 = 0x01 - COMFORT_MINUS_2 = 0x02 - ECO = 0x03 - FROST_PROTECTION = 0x04 - OFF = 0x05 + Comfort = 0x00 + Comfort_minus_1 = 0x01 + Comfort_minus_2 = 0x02 + Eco = 0x03 + Frost_protection = 0x04 + Off = 0x05 class LegrandCableOutletCluster(CustomCluster): From 13b13cc92b6e90664f7e3d39ea7165c178ad5c67 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 11 Mar 2024 17:07:22 +0100 Subject: [PATCH 06/26] Use quirk v2 --- zhaquirks/legrand/cable_outlet.py | 112 ++++-------------------------- 1 file changed, 15 insertions(+), 97 deletions(-) diff --git a/zhaquirks/legrand/cable_outlet.py b/zhaquirks/legrand/cable_outlet.py index 4857e4dc9c..97ed8685b8 100644 --- a/zhaquirks/legrand/cable_outlet.py +++ b/zhaquirks/legrand/cable_outlet.py @@ -1,19 +1,10 @@ """Module for Legrand Cable Outlet (with pilot wire functionality).""" -from zigpy.profiles import zgp, zha -from zigpy.quirks import CustomCluster, CustomDevice +from zigpy.quirks import CustomCluster +from zigpy.quirks.v2 import add_to_registry_v2 import zigpy.types as t -from zigpy.zcl.clusters.general import ( - Basic, - GreenPowerProxy, - Groups, - Identify, - OnOff, - Ota, - Scenes, -) -from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement +from zigpy.zcl import ClusterType from zigpy.zcl.foundation import ( BaseAttributeDefs, BaseCommandDefs, @@ -22,14 +13,6 @@ ZCLCommandDef, ) -from zhaquirks.const import ( - DEVICE_TYPE, - ENDPOINTS, - INPUT_CLUSTERS, - MODELS_INFO, - OUTPUT_CLUSTERS, - PROFILE_ID, -) from zhaquirks.legrand import LEGRAND, MANUFACTURER_SPECIFIC_CLUSTER_ID MANUFACTURER_SPECIFIC_CLUSTER_ID_2 = 0xFC40 # 64576 @@ -63,12 +46,12 @@ class AttributeDefs(BaseAttributeDefs): class HeatMode(t.enum8): - Comfort = 0x00 - Comfort_minus_1 = 0x01 - Comfort_minus_2 = 0x02 - Eco = 0x03 - Frost_protection = 0x04 - Off = 0x05 + comfort = 0x00 + comfort_minus_1 = 0x01 + comfort_minus_2 = 0x02 + eco = 0x03 + frost_protection = 0x04 + off = 0x05 class LegrandCableOutletCluster(CustomCluster): @@ -105,74 +88,9 @@ async def write_attributes(self, attributes, manufacturer=None) -> list: return await super().write_attributes(attrs, manufacturer) -class Legrand064882CableOutlet(CustomDevice): - signature = { - # - MODELS_INFO: [(f" {LEGRAND}", " Cable outlet")], - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - Groups.cluster_id, - OnOff.cluster_id, - Scenes.cluster_id, - MANUFACTURER_SPECIFIC_CLUSTER_ID, - ElectricalMeasurement.cluster_id, - MANUFACTURER_SPECIFIC_CLUSTER_ID_2, - ], - OUTPUT_CLUSTERS: [ - OnOff.cluster_id, - Basic.cluster_id, - MANUFACTURER_SPECIFIC_CLUSTER_ID, - Scenes.cluster_id, - Ota.cluster_id, - ], - }, - # - 242: { - PROFILE_ID: zgp.PROFILE_ID, - DEVICE_TYPE: zgp.DeviceType.COMBO_BASIC, - INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], - OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], - }, - }, - } - - replacement = { - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.LEVEL_CONTROL_SWITCH, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - Groups.cluster_id, - OnOff.cluster_id, - Scenes.cluster_id, - LegrandCluster, - ElectricalMeasurement.cluster_id, - LegrandCableOutletCluster, - ], - OUTPUT_CLUSTERS: [ - OnOff.cluster_id, - Basic.cluster_id, - LegrandCluster, - Scenes.cluster_id, - Ota.cluster_id, - ], - }, - 242: { - PROFILE_ID: zgp.PROFILE_ID, - DEVICE_TYPE: zgp.DeviceType.COMBO_BASIC, - INPUT_CLUSTERS: [GreenPowerProxy.cluster_id], - OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id], - }, - }, - } +( + add_to_registry_v2(f" {LEGRAND}", " Cable outlet") + .replaces(LegrandCluster) + .replaces(LegrandCableOutletCluster) + .replaces(LegrandCluster, cluster_type=ClusterType.Client) +) From 6d82c18fe7dd5a09aa3d232a24956651aeed482c Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 11 Mar 2024 19:03:42 +0100 Subject: [PATCH 07/26] Add entity --- zhaquirks/legrand/cable_outlet.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/zhaquirks/legrand/cable_outlet.py b/zhaquirks/legrand/cable_outlet.py index 97ed8685b8..62232ffef6 100644 --- a/zhaquirks/legrand/cable_outlet.py +++ b/zhaquirks/legrand/cable_outlet.py @@ -3,6 +3,7 @@ from zigpy.quirks import CustomCluster from zigpy.quirks.v2 import add_to_registry_v2 +from zigpy.quirks.v2.homeassistant import EntityType import zigpy.types as t from zigpy.zcl import ClusterType from zigpy.zcl.foundation import ( @@ -93,4 +94,11 @@ async def write_attributes(self, attributes, manufacturer=None) -> list: .replaces(LegrandCluster) .replaces(LegrandCableOutletCluster) .replaces(LegrandCluster, cluster_type=ClusterType.Client) + .enum( + attribute_name=LegrandCableOutletCluster.AttributeDefs.heat_mode.name, + enum_class=HeatMode, + cluster_id=LegrandCableOutletCluster.cluster_id, + translation_key="heat_mode", + entity_type=EntityType.STANDARD, + ) ) From f2176fb9a3a1a4ae3ef1c2b249c19413dc82a932 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 11 Mar 2024 19:13:18 +0100 Subject: [PATCH 08/26] Update test to use v2 quirks --- tests/test_legrand.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/test_legrand.py b/tests/test_legrand.py index 5535b0f45b..35130a262f 100644 --- a/tests/test_legrand.py +++ b/tests/test_legrand.py @@ -4,6 +4,7 @@ import pytest import zhaquirks +from zhaquirks.legrand import LEGRAND zhaquirks.setup() @@ -58,12 +59,10 @@ def test_light_switch_with_neutral_signature(assert_signature_matches_quirk): ) -async def test_cable_outlet_write_attrs(zigpy_device_from_quirk): +async def test_cable_outlet_write_attrs(zigpy_device_from_v2_quirk): """Test Legrand cable outlet heat mode attr writing.""" - device = zigpy_device_from_quirk( - zhaquirks.legrand.cable_outlet.Legrand064882CableOutlet - ) + device = zigpy_device_from_v2_quirk(f" {LEGRAND}", " Cable outlet") cable_outlet_cluster = device.endpoints[1].cable_outlet_cluster cable_outlet_cluster._write_attributes = mock.AsyncMock() cable_outlet_cluster.set_heat_mode = mock.AsyncMock() From 261a4b129865f594677ee64339873ad16e803c7b Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 13 Mar 2024 12:27:02 +0100 Subject: [PATCH 09/26] Format code --- zhaquirks/legrand/cable_outlet.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/zhaquirks/legrand/cable_outlet.py b/zhaquirks/legrand/cable_outlet.py index 37cf977f81..0bec6969b3 100644 --- a/zhaquirks/legrand/cable_outlet.py +++ b/zhaquirks/legrand/cable_outlet.py @@ -1,6 +1,5 @@ """Module for Legrand Cable Outlet (with pilot wire functionality).""" - from zigpy.quirks import CustomCluster from zigpy.quirks.v2 import add_to_registry_v2 from zigpy.quirks.v2.homeassistant import EntityType @@ -67,6 +66,8 @@ class LegrandCableOutletCluster(CustomCluster): ep_attribute = "cable_outlet_cluster" class AttributeDefs(BaseAttributeDefs): + """Attribute definitions for LegrandCluster.""" + heat_mode = ZCLAttributeDef( id=HEAT_MODE_ATTR, type=HeatMode, @@ -75,7 +76,7 @@ class AttributeDefs(BaseAttributeDefs): class ServerCommandDefs(BaseCommandDefs): """Server command definitions.""" - + set_heat_mode = ZCLCommandDef( id=HEAT_MODE_ATTR, schema={"mode": HeatMode}, @@ -84,6 +85,8 @@ class ServerCommandDefs(BaseCommandDefs): ) async def write_attributes(self, attributes, manufacturer=None) -> list: + """Write attributes to the cluster.""" + attrs = {} for attr, value in attributes.items(): attr_def = self.find_attribute(attr) From 973a07287e006c31d336faeb005c40505e01928c Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 14 Mar 2024 11:16:24 +0100 Subject: [PATCH 10/26] Add climate entity --- zhaquirks/legrand/cable_outlet.py | 105 +++++++++++++++++++++++++----- 1 file changed, 89 insertions(+), 16 deletions(-) diff --git a/zhaquirks/legrand/cable_outlet.py b/zhaquirks/legrand/cable_outlet.py index 0bec6969b3..ad62e9f9a1 100644 --- a/zhaquirks/legrand/cable_outlet.py +++ b/zhaquirks/legrand/cable_outlet.py @@ -1,10 +1,10 @@ """Module for Legrand Cable Outlet (with pilot wire functionality).""" from zigpy.quirks import CustomCluster -from zigpy.quirks.v2 import add_to_registry_v2 -from zigpy.quirks.v2.homeassistant import EntityType +from zigpy.quirks.v2 import CustomDeviceV2, add_to_registry_v2 import zigpy.types as t -from zigpy.zcl import ClusterType +from zigpy.zcl import ClusterType, foundation +from zigpy.zcl.clusters.hvac import SystemMode, Thermostat from zigpy.zcl.foundation import ( BaseAttributeDefs, BaseCommandDefs, @@ -13,11 +13,15 @@ ZCLCommandDef, ) +from zhaquirks import Bus from zhaquirks.legrand import LEGRAND, MANUFACTURER_SPECIFIC_CLUSTER_ID MANUFACTURER_SPECIFIC_CLUSTER_ID_2 = 0xFC40 # 64576 +ZCL_SYSTEM_MODE = Thermostat.attributes_by_name["system_mode"].id + HEAT_MODE_ATTR = 0x00 +OPERATION_PRESET_ATTR = 0x4002 class LegrandCluster(CustomCluster): @@ -50,12 +54,70 @@ class AttributeDefs(BaseAttributeDefs): class HeatMode(t.enum8): """Heat mode.""" - comfort = 0x00 - comfort_minus_1 = 0x01 - comfort_minus_2 = 0x02 - eco = 0x03 - frost_protection = 0x04 - off = 0x05 + Comfort = 0x00 + Comfort_minus_1 = 0x01 + Comfort_minus_2 = 0x02 + Eco = 0x03 + Frost_protection = 0x04 + Off = 0x05 + + +class LegrandCableOutletThermostatCluster(CustomCluster, Thermostat): + """Thermostat cluster for Legrand Cable Outlet.""" + + _CONSTANT_ATTRIBUTES = { + 0x001B: Thermostat.ControlSequenceOfOperation.Heating_Only, + } + + attributes = Thermostat.attributes.copy() + attributes.update( + { + OPERATION_PRESET_ATTR: ("operation_preset", HeatMode), + } + ) + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self.endpoint.device.thermostat_bus.add_listener(self) + + def heat_mode_change(self, value): + """Handle the change in heat mode.""" + + system_mode = SystemMode.Off + if value in ( + HeatMode.Comfort, + HeatMode.Comfort_minus_1, + HeatMode.Comfort_minus_2, + HeatMode.Eco, + HeatMode.Frost_protection, + ): + system_mode = SystemMode.Heat + + """Heat mode changed reported.""" + self._update_attribute(ZCL_SYSTEM_MODE, system_mode) + self._update_attribute(OPERATION_PRESET_ATTR, value) + + async def write_attributes(self, attributes, manufacturer=None): + """Implement writeable attributes.""" + + attrs = {} + for attr, value in attributes.items(): + attr_def = self.find_attribute(attr) + attr_id = attr_def.id + if attr_id == OPERATION_PRESET_ATTR: + attrs[HEAT_MODE_ATTR] = value + elif attr_id == ZCL_SYSTEM_MODE: + current = self._attr_cache.get(ZCL_SYSTEM_MODE) + if current != value: + attrs[HEAT_MODE_ATTR] = ( + HeatMode.Comfort if value == SystemMode.Heat else HeatMode.Off + ) + else: + attrs[attr] = value + await self.endpoint.cable_outlet_cluster.write_attributes(attrs, manufacturer) + + return [[foundation.WriteAttributesStatusRecord(foundation.Status.SUCCESS)]] class LegrandCableOutletCluster(CustomCluster): @@ -97,17 +159,28 @@ async def write_attributes(self, attributes, manufacturer=None) -> list: attrs[attr] = value return await super().write_attributes(attrs, manufacturer) + def _update_attribute(self, attrid, value): + super()._update_attribute(attrid, value) + if attrid == HEAT_MODE_ATTR: + self.endpoint.device.thermostat_bus.listener_event( + "heat_mode_change", value + ) + + +class LegrandCableOutletThermostat(CustomDeviceV2): + """Legrand Cable Outlet Thermostat device.""" + + def __init__(self, *args, **kwargs): + """Init device.""" + self.thermostat_bus = Bus() + super().__init__(*args, **kwargs) + ( add_to_registry_v2(f" {LEGRAND}", " Cable outlet") + .device_class(LegrandCableOutletThermostat) .replaces(LegrandCluster) .replaces(LegrandCableOutletCluster) .replaces(LegrandCluster, cluster_type=ClusterType.Client) - .enum( - attribute_name=LegrandCableOutletCluster.AttributeDefs.heat_mode.name, - enum_class=HeatMode, - cluster_id=LegrandCableOutletCluster.cluster_id, - translation_key="heat_mode", - entity_type=EntityType.STANDARD, - ) + .adds(LegrandCableOutletThermostatCluster) ) From cfe94ba50211b3cd796f5bf68cc40aa9272b7208 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 14 Mar 2024 11:25:44 +0100 Subject: [PATCH 11/26] Add enum back --- zhaquirks/legrand/cable_outlet.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/zhaquirks/legrand/cable_outlet.py b/zhaquirks/legrand/cable_outlet.py index ad62e9f9a1..02a4a3ba7c 100644 --- a/zhaquirks/legrand/cable_outlet.py +++ b/zhaquirks/legrand/cable_outlet.py @@ -183,4 +183,9 @@ def __init__(self, *args, **kwargs): .replaces(LegrandCableOutletCluster) .replaces(LegrandCluster, cluster_type=ClusterType.Client) .adds(LegrandCableOutletThermostatCluster) + .enum( + attribute_name=LegrandCableOutletCluster.AttributeDefs.heat_mode.name, + enum_class=HeatMode, + cluster_id=LegrandCableOutletCluster.cluster_id, + ) ) From 77202129576c88310feceabc00dae95ed8a4fffa Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Thu, 14 Mar 2024 20:26:18 +0100 Subject: [PATCH 12/26] Use attributes def and don't use bus --- zhaquirks/legrand/cable_outlet.py | 34 ++++++++----------------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/zhaquirks/legrand/cable_outlet.py b/zhaquirks/legrand/cable_outlet.py index 02a4a3ba7c..a1d0c2a629 100644 --- a/zhaquirks/legrand/cable_outlet.py +++ b/zhaquirks/legrand/cable_outlet.py @@ -1,7 +1,7 @@ """Module for Legrand Cable Outlet (with pilot wire functionality).""" from zigpy.quirks import CustomCluster -from zigpy.quirks.v2 import CustomDeviceV2, add_to_registry_v2 +from zigpy.quirks.v2 import add_to_registry_v2 import zigpy.types as t from zigpy.zcl import ClusterType, foundation from zigpy.zcl.clusters.hvac import SystemMode, Thermostat @@ -13,7 +13,6 @@ ZCLCommandDef, ) -from zhaquirks import Bus from zhaquirks.legrand import LEGRAND, MANUFACTURER_SPECIFIC_CLUSTER_ID MANUFACTURER_SPECIFIC_CLUSTER_ID_2 = 0xFC40 # 64576 @@ -69,17 +68,14 @@ class LegrandCableOutletThermostatCluster(CustomCluster, Thermostat): 0x001B: Thermostat.ControlSequenceOfOperation.Heating_Only, } - attributes = Thermostat.attributes.copy() - attributes.update( - { - OPERATION_PRESET_ATTR: ("operation_preset", HeatMode), - } - ) + class AttributeDefs(Thermostat.AttributeDefs): + """Attribute definitions.""" - def __init__(self, *args, **kwargs): - """Init.""" - super().__init__(*args, **kwargs) - self.endpoint.device.thermostat_bus.add_listener(self) + operation_preset = ZCLAttributeDef( + id=OPERATION_PRESET_ATTR, + type=HeatMode, + is_manufacturer_specific=True, + ) def heat_mode_change(self, value): """Handle the change in heat mode.""" @@ -162,23 +158,11 @@ async def write_attributes(self, attributes, manufacturer=None) -> list: def _update_attribute(self, attrid, value): super()._update_attribute(attrid, value) if attrid == HEAT_MODE_ATTR: - self.endpoint.device.thermostat_bus.listener_event( - "heat_mode_change", value - ) - - -class LegrandCableOutletThermostat(CustomDeviceV2): - """Legrand Cable Outlet Thermostat device.""" - - def __init__(self, *args, **kwargs): - """Init device.""" - self.thermostat_bus = Bus() - super().__init__(*args, **kwargs) + self.endpoint.thermostat.heat_mode_change(value) ( add_to_registry_v2(f" {LEGRAND}", " Cable outlet") - .device_class(LegrandCableOutletThermostat) .replaces(LegrandCluster) .replaces(LegrandCableOutletCluster) .replaces(LegrandCluster, cluster_type=ClusterType.Client) From 25875ca325f8d7c88f4e77170d57a8b19ba02cde Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 15 Mar 2024 10:05:56 +0100 Subject: [PATCH 13/26] Remove thermostat --- zhaquirks/legrand/cable_outlet.py | 67 +------------------------------ 1 file changed, 1 insertion(+), 66 deletions(-) diff --git a/zhaquirks/legrand/cable_outlet.py b/zhaquirks/legrand/cable_outlet.py index a1d0c2a629..07fccc1817 100644 --- a/zhaquirks/legrand/cable_outlet.py +++ b/zhaquirks/legrand/cable_outlet.py @@ -3,8 +3,7 @@ from zigpy.quirks import CustomCluster from zigpy.quirks.v2 import add_to_registry_v2 import zigpy.types as t -from zigpy.zcl import ClusterType, foundation -from zigpy.zcl.clusters.hvac import SystemMode, Thermostat +from zigpy.zcl import ClusterType from zigpy.zcl.foundation import ( BaseAttributeDefs, BaseCommandDefs, @@ -17,10 +16,7 @@ MANUFACTURER_SPECIFIC_CLUSTER_ID_2 = 0xFC40 # 64576 -ZCL_SYSTEM_MODE = Thermostat.attributes_by_name["system_mode"].id - HEAT_MODE_ATTR = 0x00 -OPERATION_PRESET_ATTR = 0x4002 class LegrandCluster(CustomCluster): @@ -61,61 +57,6 @@ class HeatMode(t.enum8): Off = 0x05 -class LegrandCableOutletThermostatCluster(CustomCluster, Thermostat): - """Thermostat cluster for Legrand Cable Outlet.""" - - _CONSTANT_ATTRIBUTES = { - 0x001B: Thermostat.ControlSequenceOfOperation.Heating_Only, - } - - class AttributeDefs(Thermostat.AttributeDefs): - """Attribute definitions.""" - - operation_preset = ZCLAttributeDef( - id=OPERATION_PRESET_ATTR, - type=HeatMode, - is_manufacturer_specific=True, - ) - - def heat_mode_change(self, value): - """Handle the change in heat mode.""" - - system_mode = SystemMode.Off - if value in ( - HeatMode.Comfort, - HeatMode.Comfort_minus_1, - HeatMode.Comfort_minus_2, - HeatMode.Eco, - HeatMode.Frost_protection, - ): - system_mode = SystemMode.Heat - - """Heat mode changed reported.""" - self._update_attribute(ZCL_SYSTEM_MODE, system_mode) - self._update_attribute(OPERATION_PRESET_ATTR, value) - - async def write_attributes(self, attributes, manufacturer=None): - """Implement writeable attributes.""" - - attrs = {} - for attr, value in attributes.items(): - attr_def = self.find_attribute(attr) - attr_id = attr_def.id - if attr_id == OPERATION_PRESET_ATTR: - attrs[HEAT_MODE_ATTR] = value - elif attr_id == ZCL_SYSTEM_MODE: - current = self._attr_cache.get(ZCL_SYSTEM_MODE) - if current != value: - attrs[HEAT_MODE_ATTR] = ( - HeatMode.Comfort if value == SystemMode.Heat else HeatMode.Off - ) - else: - attrs[attr] = value - await self.endpoint.cable_outlet_cluster.write_attributes(attrs, manufacturer) - - return [[foundation.WriteAttributesStatusRecord(foundation.Status.SUCCESS)]] - - class LegrandCableOutletCluster(CustomCluster): """Legrand second manufacturer-specific cluster.""" @@ -155,18 +96,12 @@ async def write_attributes(self, attributes, manufacturer=None) -> list: attrs[attr] = value return await super().write_attributes(attrs, manufacturer) - def _update_attribute(self, attrid, value): - super()._update_attribute(attrid, value) - if attrid == HEAT_MODE_ATTR: - self.endpoint.thermostat.heat_mode_change(value) - ( add_to_registry_v2(f" {LEGRAND}", " Cable outlet") .replaces(LegrandCluster) .replaces(LegrandCableOutletCluster) .replaces(LegrandCluster, cluster_type=ClusterType.Client) - .adds(LegrandCableOutletThermostatCluster) .enum( attribute_name=LegrandCableOutletCluster.AttributeDefs.heat_mode.name, enum_class=HeatMode, From 5adb0d6f12f5770b0b3ef371ecc89f6c201ada74 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 15 Mar 2024 10:18:03 +0100 Subject: [PATCH 14/26] Remove enum --- zhaquirks/legrand/cable_outlet.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/zhaquirks/legrand/cable_outlet.py b/zhaquirks/legrand/cable_outlet.py index 07fccc1817..9d293a9f24 100644 --- a/zhaquirks/legrand/cable_outlet.py +++ b/zhaquirks/legrand/cable_outlet.py @@ -102,9 +102,4 @@ async def write_attributes(self, attributes, manufacturer=None) -> list: .replaces(LegrandCluster) .replaces(LegrandCableOutletCluster) .replaces(LegrandCluster, cluster_type=ClusterType.Client) - .enum( - attribute_name=LegrandCableOutletCluster.AttributeDefs.heat_mode.name, - enum_class=HeatMode, - cluster_id=LegrandCableOutletCluster.cluster_id, - ) ) From be6bbfcd2a6dde2cc6c80759f89c612bff84c993 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 15 Mar 2024 10:52:28 +0100 Subject: [PATCH 15/26] Add device mode --- zhaquirks/legrand/cable_outlet.py | 42 +++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/zhaquirks/legrand/cable_outlet.py b/zhaquirks/legrand/cable_outlet.py index 9d293a9f24..9e9dff01cc 100644 --- a/zhaquirks/legrand/cable_outlet.py +++ b/zhaquirks/legrand/cable_outlet.py @@ -17,6 +17,14 @@ MANUFACTURER_SPECIFIC_CLUSTER_ID_2 = 0xFC40 # 64576 HEAT_MODE_ATTR = 0x00 +ZCL_DEVICE_MODE = 0x4000 + + +class DeviceMode(t.enum16): + """Device mode.""" + + Standard = 0x01 + Wire_pilot = 0x02 class LegrandCluster(CustomCluster): @@ -44,6 +52,34 @@ class AttributeDefs(BaseAttributeDefs): type=t.Bool, is_manufacturer_specific=True, ) + device_mode_enum = ZCLAttributeDef( + id=ZCL_DEVICE_MODE, + type=DeviceMode, + ) + + def __init__(self, *args, **kwargs): + """Init.""" + super().__init__(*args, **kwargs) + self._send_sequence: int = None + self._attr_cache = {ZCL_DEVICE_MODE: 0x01} + + async def write_attributes(self, attributes, manufacturer=None) -> list: + """Write attributes to the cluster.""" + + attrs = {} + for attr, value in attributes.items(): + attr_def = self.find_attribute(attr) + attr_id = attr_def.id + if attr_id == ZCL_DEVICE_MODE: + attrs[0x0000] = [value, 0x00] + else: + attrs[attr] = value + return await super().write_attributes(attrs, manufacturer) + + def _update_attribute(self, attrid, value) -> None: + super()._update_attribute(attrid, value) + if attrid == 0x0000: + self._update_attribute(ZCL_DEVICE_MODE, value[0]) class HeatMode(t.enum8): @@ -102,4 +138,10 @@ async def write_attributes(self, attributes, manufacturer=None) -> list: .replaces(LegrandCluster) .replaces(LegrandCableOutletCluster) .replaces(LegrandCluster, cluster_type=ClusterType.Client) + .enum( + attribute_name=LegrandCluster.AttributeDefs.device_mode_enum.name, + cluster_id=LegrandCluster.cluster_id, + enum_class=DeviceMode, + translation_key="device_mode", + ) ) From 033a90c931801da57a3e28592f65d298e12c3838 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 15 Mar 2024 11:13:16 +0100 Subject: [PATCH 16/26] Remove init --- zhaquirks/legrand/cable_outlet.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/zhaquirks/legrand/cable_outlet.py b/zhaquirks/legrand/cable_outlet.py index 9e9dff01cc..d03fce962c 100644 --- a/zhaquirks/legrand/cable_outlet.py +++ b/zhaquirks/legrand/cable_outlet.py @@ -57,12 +57,6 @@ class AttributeDefs(BaseAttributeDefs): type=DeviceMode, ) - def __init__(self, *args, **kwargs): - """Init.""" - super().__init__(*args, **kwargs) - self._send_sequence: int = None - self._attr_cache = {ZCL_DEVICE_MODE: 0x01} - async def write_attributes(self, attributes, manufacturer=None) -> list: """Write attributes to the cluster.""" From 6f2f1e4a3973b006a12289c19d02fa77c6c4ab6f Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Fri, 15 Mar 2024 22:50:32 +0100 Subject: [PATCH 17/26] Add write device mode test --- tests/test_legrand.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_legrand.py b/tests/test_legrand.py index 319b3ac9c9..98c16a7b38 100644 --- a/tests/test_legrand.py +++ b/tests/test_legrand.py @@ -3,6 +3,7 @@ from unittest import mock import pytest +from zigpy.zcl import foundation import zhaquirks from zhaquirks.legrand import LEGRAND @@ -79,3 +80,31 @@ async def test_cable_outlet_write_attrs(zigpy_device_from_v2_quirk): [], manufacturer=0xFC40, ) + +@pytest.mark.parametrize( + "value, expected_value", + [ + (0x01, [1, 0]), + (0x02, [2, 0]), + ], +) +async def test_legrand_write_device_mode(zigpy_device_from_v2_quirk, value, expected_value): + """Test Legrand cable outlet heat mode attr writing.""" + + device = zigpy_device_from_v2_quirk(f" {LEGRAND}", " Cable outlet") + legrand_cluster = device.endpoints[1].legrand_cluster + legrand_cluster._write_attributes = mock.AsyncMock() + + await legrand_cluster.write_attributes({ 0x4000: value }, manufacturer=0xFC40) + + expected = foundation.Attribute(0x0000, foundation.TypeValue()) + expected_attr_def = legrand_cluster.find_attribute(0x0000) + expected.value.type = foundation.DATA_TYPES.pytype_to_datatype_id( + expected_attr_def.type + ) + expected.value.value = expected_attr_def.type(expected_value) + + legrand_cluster._write_attributes.assert_awaited_with( + [expected], + manufacturer=0xFC40, + ) From 7523f37646ec8bdc929764207dc1284e275f15b4 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Mon, 18 Mar 2024 17:21:56 +0100 Subject: [PATCH 18/26] Rename cable outlet to wire pilot --- tests/test_legrand.py | 21 +++++---- .../{cable_outlet.py => wire_pilot.py} | 46 ++++++++----------- 2 files changed, 30 insertions(+), 37 deletions(-) rename zhaquirks/legrand/{cable_outlet.py => wire_pilot.py} (76%) diff --git a/tests/test_legrand.py b/tests/test_legrand.py index 98c16a7b38..fb5005ef7c 100644 --- a/tests/test_legrand.py +++ b/tests/test_legrand.py @@ -62,21 +62,21 @@ def test_light_switch_with_neutral_signature(assert_signature_matches_quirk): ) -async def test_cable_outlet_write_attrs(zigpy_device_from_v2_quirk): +async def test_legrand_wire_pilot_cluster_write_attrs(zigpy_device_from_v2_quirk): """Test Legrand cable outlet heat mode attr writing.""" device = zigpy_device_from_v2_quirk(f" {LEGRAND}", " Cable outlet") - cable_outlet_cluster = device.endpoints[1].cable_outlet_cluster - cable_outlet_cluster._write_attributes = mock.AsyncMock() - cable_outlet_cluster.set_heat_mode = mock.AsyncMock() + legrand_wire_pilot_cluster = device.endpoints[1].legrand_wire_pilot_cluster + legrand_wire_pilot_cluster._write_attributes = mock.AsyncMock() + legrand_wire_pilot_cluster.set_heat_mode = mock.AsyncMock() - await cable_outlet_cluster.write_attributes({0x00: 0x02}, manufacturer=0xFC40) + await legrand_wire_pilot_cluster.write_attributes({0x00: 0x02}, manufacturer=0xFC40) - cable_outlet_cluster.set_heat_mode.assert_awaited_with( + legrand_wire_pilot_cluster.set_heat_mode.assert_awaited_with( 0x02, manufacturer=0xFC40, ) - cable_outlet_cluster._write_attributes.assert_awaited_with( + legrand_wire_pilot_cluster._write_attributes.assert_awaited_with( [], manufacturer=0xFC40, ) @@ -84,11 +84,11 @@ async def test_cable_outlet_write_attrs(zigpy_device_from_v2_quirk): @pytest.mark.parametrize( "value, expected_value", [ - (0x01, [1, 0]), - (0x02, [2, 0]), + (False, [1, 0]), + (True, [2, 0]), ], ) -async def test_legrand_write_device_mode(zigpy_device_from_v2_quirk, value, expected_value): +async def test_legrand_wire_pilot_mode_write_attrs(zigpy_device_from_v2_quirk, value, expected_value): """Test Legrand cable outlet heat mode attr writing.""" device = zigpy_device_from_v2_quirk(f" {LEGRAND}", " Cable outlet") @@ -108,3 +108,4 @@ async def test_legrand_write_device_mode(zigpy_device_from_v2_quirk, value, expe [expected], manufacturer=0xFC40, ) + diff --git a/zhaquirks/legrand/cable_outlet.py b/zhaquirks/legrand/wire_pilot.py similarity index 76% rename from zhaquirks/legrand/cable_outlet.py rename to zhaquirks/legrand/wire_pilot.py index d03fce962c..d8c60f80ab 100644 --- a/zhaquirks/legrand/cable_outlet.py +++ b/zhaquirks/legrand/wire_pilot.py @@ -1,4 +1,4 @@ -"""Module for Legrand Cable Outlet (with pilot wire functionality).""" +"""Module for Legrand Cable Outlet with pilot wire functionality.""" from zigpy.quirks import CustomCluster from zigpy.quirks.v2 import add_to_registry_v2 @@ -16,15 +16,9 @@ MANUFACTURER_SPECIFIC_CLUSTER_ID_2 = 0xFC40 # 64576 -HEAT_MODE_ATTR = 0x00 -ZCL_DEVICE_MODE = 0x4000 +WIRE_PILOT_HEAT_MODE_ATTR = 0x00 - -class DeviceMode(t.enum16): - """Device mode.""" - - Standard = 0x01 - Wire_pilot = 0x02 +ZCL_WIRE_PILOT_MODE = 0x4000 class LegrandCluster(CustomCluster): @@ -52,9 +46,8 @@ class AttributeDefs(BaseAttributeDefs): type=t.Bool, is_manufacturer_specific=True, ) - device_mode_enum = ZCLAttributeDef( - id=ZCL_DEVICE_MODE, - type=DeviceMode, + wire_pilot_mode = ZCLAttributeDef( + id=ZCL_WIRE_PILOT_MODE, type=t.Bool ) async def write_attributes(self, attributes, manufacturer=None) -> list: @@ -64,8 +57,8 @@ async def write_attributes(self, attributes, manufacturer=None) -> list: for attr, value in attributes.items(): attr_def = self.find_attribute(attr) attr_id = attr_def.id - if attr_id == ZCL_DEVICE_MODE: - attrs[0x0000] = [value, 0x00] + if attr_id == ZCL_WIRE_PILOT_MODE: + attrs[0x0000] = [0x02, 0x00] if value else [0x01, 0x00] else: attrs[attr] = value return await super().write_attributes(attrs, manufacturer) @@ -73,7 +66,7 @@ async def write_attributes(self, attributes, manufacturer=None) -> list: def _update_attribute(self, attrid, value) -> None: super()._update_attribute(attrid, value) if attrid == 0x0000: - self._update_attribute(ZCL_DEVICE_MODE, value[0]) + self._update_attribute(ZCL_WIRE_PILOT_MODE, value[0] == 0x02) class HeatMode(t.enum8): @@ -87,18 +80,18 @@ class HeatMode(t.enum8): Off = 0x05 -class LegrandCableOutletCluster(CustomCluster): - """Legrand second manufacturer-specific cluster.""" +class LegrandWirePilotCluster(CustomCluster): + """Legrand wire pilot manufacturer-specific cluster.""" cluster_id = MANUFACTURER_SPECIFIC_CLUSTER_ID_2 - name = "CableOutlet" - ep_attribute = "cable_outlet_cluster" + name = "LegrandWirePilotCluster" + ep_attribute = "legrand_wire_pilot_cluster" class AttributeDefs(BaseAttributeDefs): """Attribute definitions for LegrandCluster.""" heat_mode = ZCLAttributeDef( - id=HEAT_MODE_ATTR, + id=WIRE_PILOT_HEAT_MODE_ATTR, type=HeatMode, is_manufacturer_specific=True, ) @@ -107,7 +100,7 @@ class ServerCommandDefs(BaseCommandDefs): """Server command definitions.""" set_heat_mode = ZCLCommandDef( - id=HEAT_MODE_ATTR, + id=WIRE_PILOT_HEAT_MODE_ATTR, schema={"mode": HeatMode}, direction=Direction.Client_to_Server, is_manufacturer_specific=True, @@ -120,7 +113,7 @@ async def write_attributes(self, attributes, manufacturer=None) -> list: for attr, value in attributes.items(): attr_def = self.find_attribute(attr) attr_id = attr_def.id - if attr_id == HEAT_MODE_ATTR: + if attr_id == WIRE_PILOT_HEAT_MODE_ATTR: await self.set_heat_mode(value, manufacturer=manufacturer) else: attrs[attr] = value @@ -130,12 +123,11 @@ async def write_attributes(self, attributes, manufacturer=None) -> list: ( add_to_registry_v2(f" {LEGRAND}", " Cable outlet") .replaces(LegrandCluster) - .replaces(LegrandCableOutletCluster) + .replaces(LegrandWirePilotCluster) .replaces(LegrandCluster, cluster_type=ClusterType.Client) - .enum( - attribute_name=LegrandCluster.AttributeDefs.device_mode_enum.name, + .switch( + attribute_name=LegrandCluster.AttributeDefs.wire_pilot_mode.name, cluster_id=LegrandCluster.cluster_id, - enum_class=DeviceMode, - translation_key="device_mode", + translation_key="wire_pilot_mode", ) ) From 52584ad0b706f5d5683009d120d1b9330e276ae2 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 19 Mar 2024 09:49:18 +0100 Subject: [PATCH 19/26] Add thermostat with heat_mode attribute --- zhaquirks/legrand/wire_pilot.py | 62 +++++++++++++++++++++++++++------ 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/zhaquirks/legrand/wire_pilot.py b/zhaquirks/legrand/wire_pilot.py index d8c60f80ab..fdd881011d 100644 --- a/zhaquirks/legrand/wire_pilot.py +++ b/zhaquirks/legrand/wire_pilot.py @@ -4,6 +4,7 @@ from zigpy.quirks.v2 import add_to_registry_v2 import zigpy.types as t from zigpy.zcl import ClusterType +from zigpy.zcl.clusters.hvac import Thermostat from zigpy.zcl.foundation import ( BaseAttributeDefs, BaseCommandDefs, @@ -12,13 +13,15 @@ ZCLCommandDef, ) +from zhaquirks import LocalDataCluster from zhaquirks.legrand import LEGRAND, MANUFACTURER_SPECIFIC_CLUSTER_ID MANUFACTURER_SPECIFIC_CLUSTER_ID_2 = 0xFC40 # 64576 -WIRE_PILOT_HEAT_MODE_ATTR = 0x00 +WIRE_PILOT_MODE_ATTR = 0x4000 -ZCL_WIRE_PILOT_MODE = 0x4000 +LEGRAND_HEAT_MODE_ATTR = 0x00 +THERMOSTAT_HEAT_MODE_ATTR = 0x4000 class LegrandCluster(CustomCluster): @@ -46,9 +49,7 @@ class AttributeDefs(BaseAttributeDefs): type=t.Bool, is_manufacturer_specific=True, ) - wire_pilot_mode = ZCLAttributeDef( - id=ZCL_WIRE_PILOT_MODE, type=t.Bool - ) + wire_pilot_mode = ZCLAttributeDef(id=WIRE_PILOT_MODE_ATTR, type=t.Bool) async def write_attributes(self, attributes, manufacturer=None) -> list: """Write attributes to the cluster.""" @@ -57,7 +58,7 @@ async def write_attributes(self, attributes, manufacturer=None) -> list: for attr, value in attributes.items(): attr_def = self.find_attribute(attr) attr_id = attr_def.id - if attr_id == ZCL_WIRE_PILOT_MODE: + if attr_id == WIRE_PILOT_MODE_ATTR: attrs[0x0000] = [0x02, 0x00] if value else [0x01, 0x00] else: attrs[attr] = value @@ -66,7 +67,7 @@ async def write_attributes(self, attributes, manufacturer=None) -> list: def _update_attribute(self, attrid, value) -> None: super()._update_attribute(attrid, value) if attrid == 0x0000: - self._update_attribute(ZCL_WIRE_PILOT_MODE, value[0] == 0x02) + self._update_attribute(WIRE_PILOT_MODE_ATTR, value[0] == 0x02) class HeatMode(t.enum8): @@ -91,7 +92,7 @@ class AttributeDefs(BaseAttributeDefs): """Attribute definitions for LegrandCluster.""" heat_mode = ZCLAttributeDef( - id=WIRE_PILOT_HEAT_MODE_ATTR, + id=LEGRAND_HEAT_MODE_ATTR, type=HeatMode, is_manufacturer_specific=True, ) @@ -100,7 +101,7 @@ class ServerCommandDefs(BaseCommandDefs): """Server command definitions.""" set_heat_mode = ZCLCommandDef( - id=WIRE_PILOT_HEAT_MODE_ATTR, + id=LEGRAND_HEAT_MODE_ATTR, schema={"mode": HeatMode}, direction=Direction.Client_to_Server, is_manufacturer_specific=True, @@ -113,18 +114,59 @@ async def write_attributes(self, attributes, manufacturer=None) -> list: for attr, value in attributes.items(): attr_def = self.find_attribute(attr) attr_id = attr_def.id - if attr_id == WIRE_PILOT_HEAT_MODE_ATTR: + if attr_id == LEGRAND_HEAT_MODE_ATTR: await self.set_heat_mode(value, manufacturer=manufacturer) else: attrs[attr] = value return await super().write_attributes(attrs, manufacturer) + def _update_attribute(self, attrid, value): + super()._update_attribute(attrid, value) + if attrid == LEGRAND_HEAT_MODE_ATTR: + self.endpoint.thermostat.heat_mode_change(value) + + +class LegrandWirePilotThermostatCluster(LocalDataCluster, Thermostat): + """Thermostat cluster for Legrand wire pilot thermostats.""" + + _CONSTANT_ATTRIBUTES = {0x001B: Thermostat.ControlSequenceOfOperation.Heating_Only} + + class AttributeDefs(Thermostat.AttributeDefs): + """Attribute definitions.""" + + heat_mode = ZCLAttributeDef( + id=THERMOSTAT_HEAT_MODE_ATTR, + type=HeatMode, + is_manufacturer_specific=True, + ) + + async def write_attributes(self, attributes, manufacturer=None) -> list: + """Write attributes to the cluster.""" + + attrs = {} + for attr, value in attributes.items(): + attr_def = self.find_attribute(attr) + attr_id = attr_def.id + if attr_id == THERMOSTAT_HEAT_MODE_ATTR: + await self.endpoint.legrand_wire_pilot_cluster.set_heat_mode( + value, manufacturer=manufacturer + ) + else: + attrs[attr] = value + return await super().write_attributes(attrs, manufacturer) + + def heat_mode_change(self, value): + """Handle the change in heat mode.""" + + self._update_attribute(THERMOSTAT_HEAT_MODE_ATTR, value) + ( add_to_registry_v2(f" {LEGRAND}", " Cable outlet") .replaces(LegrandCluster) .replaces(LegrandWirePilotCluster) .replaces(LegrandCluster, cluster_type=ClusterType.Client) + .adds(LegrandWirePilotThermostatCluster) .switch( attribute_name=LegrandCluster.AttributeDefs.wire_pilot_mode.name, cluster_id=LegrandCluster.cluster_id, From 8003a1405fa05535e5ed703a071585d33bb4a9cc Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 19 Mar 2024 10:51:08 +0100 Subject: [PATCH 20/26] Add thermostat system mode --- zhaquirks/legrand/wire_pilot.py | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/zhaquirks/legrand/wire_pilot.py b/zhaquirks/legrand/wire_pilot.py index fdd881011d..6e83d9ac92 100644 --- a/zhaquirks/legrand/wire_pilot.py +++ b/zhaquirks/legrand/wire_pilot.py @@ -4,7 +4,7 @@ from zigpy.quirks.v2 import add_to_registry_v2 import zigpy.types as t from zigpy.zcl import ClusterType -from zigpy.zcl.clusters.hvac import Thermostat +from zigpy.zcl.clusters.hvac import SystemMode, Thermostat from zigpy.zcl.foundation import ( BaseAttributeDefs, BaseCommandDefs, @@ -18,10 +18,12 @@ MANUFACTURER_SPECIFIC_CLUSTER_ID_2 = 0xFC40 # 64576 + WIRE_PILOT_MODE_ATTR = 0x4000 LEGRAND_HEAT_MODE_ATTR = 0x00 THERMOSTAT_HEAT_MODE_ATTR = 0x4000 +THERMOSTAT_SYSTEM_MODE_ATTR = Thermostat.attributes_by_name["system_mode"].id class LegrandCluster(CustomCluster): @@ -137,27 +139,48 @@ class AttributeDefs(Thermostat.AttributeDefs): heat_mode = ZCLAttributeDef( id=THERMOSTAT_HEAT_MODE_ATTR, type=HeatMode, - is_manufacturer_specific=True, ) async def write_attributes(self, attributes, manufacturer=None) -> list: """Write attributes to the cluster.""" attrs = {} + mode: HeatMode = None for attr, value in attributes.items(): attr_def = self.find_attribute(attr) attr_id = attr_def.id if attr_id == THERMOSTAT_HEAT_MODE_ATTR: - await self.endpoint.legrand_wire_pilot_cluster.set_heat_mode( - value, manufacturer=manufacturer - ) + mode = value + elif attr_id == THERMOSTAT_SYSTEM_MODE_ATTR: + current = self._attr_cache.get(THERMOSTAT_SYSTEM_MODE_ATTR) + if current != value: + mode = ( + HeatMode.Comfort if value == SystemMode.Heat else HeatMode.Off + ) else: attrs[attr] = value + + if mode is not None: + await self.endpoint.legrand_wire_pilot_cluster.set_heat_mode( + mode, manufacturer=manufacturer + ) return await super().write_attributes(attrs, manufacturer) def heat_mode_change(self, value): """Handle the change in heat mode.""" + system_mode = SystemMode.Off + if value in ( + HeatMode.Comfort, + HeatMode.Comfort_minus_1, + HeatMode.Comfort_minus_2, + HeatMode.Eco, + HeatMode.Frost_protection, + ): + system_mode = SystemMode.Heat + + """Heat mode changed reported.""" + self._update_attribute(THERMOSTAT_SYSTEM_MODE_ATTR, system_mode) self._update_attribute(THERMOSTAT_HEAT_MODE_ATTR, value) From 5664eb7c4ddafd3abbb64adca9f58f417837902a Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 19 Mar 2024 18:07:48 +0100 Subject: [PATCH 21/26] Remove unreachable code --- zhaquirks/legrand/wire_pilot.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/zhaquirks/legrand/wire_pilot.py b/zhaquirks/legrand/wire_pilot.py index 6e83d9ac92..03df6d1494 100644 --- a/zhaquirks/legrand/wire_pilot.py +++ b/zhaquirks/legrand/wire_pilot.py @@ -118,8 +118,6 @@ async def write_attributes(self, attributes, manufacturer=None) -> list: attr_id = attr_def.id if attr_id == LEGRAND_HEAT_MODE_ATTR: await self.set_heat_mode(value, manufacturer=manufacturer) - else: - attrs[attr] = value return await super().write_attributes(attrs, manufacturer) def _update_attribute(self, attrid, value): From 9518cbdc06991ba8fff9fcf62277edaf993f914a Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 19 Mar 2024 18:34:11 +0100 Subject: [PATCH 22/26] Improve test coverage --- tests/test_legrand.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/tests/test_legrand.py b/tests/test_legrand.py index fb5005ef7c..3f0a2dd160 100644 --- a/tests/test_legrand.py +++ b/tests/test_legrand.py @@ -82,23 +82,25 @@ async def test_legrand_wire_pilot_cluster_write_attrs(zigpy_device_from_v2_quirk ) @pytest.mark.parametrize( - "value, expected_value", + "attr, value, expected_attr, expected_value", [ - (False, [1, 0]), - (True, [2, 0]), + (0x4000, False, 0x0000, [1, 0]), + (0x4000, True, 0x0000, [2, 0]), + (0x0001, False, 0x0001, False), + (0x0002, True, 0x0002, True), ], ) -async def test_legrand_wire_pilot_mode_write_attrs(zigpy_device_from_v2_quirk, value, expected_value): - """Test Legrand cable outlet heat mode attr writing.""" +async def test_legrand_wire_pilot_mode_write_attrs(zigpy_device_from_v2_quirk, attr, value, expected_attr, expected_value): + """Test Legrand cable outlet attr writing.""" device = zigpy_device_from_v2_quirk(f" {LEGRAND}", " Cable outlet") legrand_cluster = device.endpoints[1].legrand_cluster legrand_cluster._write_attributes = mock.AsyncMock() - await legrand_cluster.write_attributes({ 0x4000: value }, manufacturer=0xFC40) + await legrand_cluster.write_attributes({ attr: value }, manufacturer=0xFC40) - expected = foundation.Attribute(0x0000, foundation.TypeValue()) - expected_attr_def = legrand_cluster.find_attribute(0x0000) + expected = foundation.Attribute(expected_attr, foundation.TypeValue()) + expected_attr_def = legrand_cluster.find_attribute(expected_attr) expected.value.type = foundation.DATA_TYPES.pytype_to_datatype_id( expected_attr_def.type ) @@ -108,4 +110,3 @@ async def test_legrand_wire_pilot_mode_write_attrs(zigpy_device_from_v2_quirk, v [expected], manufacturer=0xFC40, ) - From c1d541b90b7de6a0f9f3cdaebbcf2f6c18ddcb86 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 19 Mar 2024 20:14:03 +0100 Subject: [PATCH 23/26] Add update attrs tests --- tests/test_legrand.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_legrand.py b/tests/test_legrand.py index 3f0a2dd160..d26faecbac 100644 --- a/tests/test_legrand.py +++ b/tests/test_legrand.py @@ -5,6 +5,7 @@ import pytest from zigpy.zcl import foundation +from tests.common import ClusterListener import zhaquirks from zhaquirks.legrand import LEGRAND @@ -84,8 +85,10 @@ async def test_legrand_wire_pilot_cluster_write_attrs(zigpy_device_from_v2_quirk @pytest.mark.parametrize( "attr, value, expected_attr, expected_value", [ + # Wire pilot mode attribute (0x4000, False, 0x0000, [1, 0]), (0x4000, True, 0x0000, [2, 0]), + # Other attributes (0x0001, False, 0x0001, False), (0x0002, True, 0x0002, True), ], @@ -110,3 +113,27 @@ async def test_legrand_wire_pilot_mode_write_attrs(zigpy_device_from_v2_quirk, a [expected], manufacturer=0xFC40, ) + +@pytest.mark.parametrize( + "attr, value, expected_attr, expected_value", + [ + # Device mode attribute + (0x0000, [1, 0], 0x4000, False), + (0x0000, [2, 0], 0x4000, True), + ], +) +async def test_legrand_wire_pilot_mode_update_attr(zigpy_device_from_v2_quirk, attr, value, expected_attr, expected_value): + """Test Legrand cable outlet attr update.""" + + device = zigpy_device_from_v2_quirk(f" {LEGRAND}", " Cable outlet") + legrand_cluster = device.endpoints[1].legrand_cluster + + legrand_cluster_listener = ClusterListener(legrand_cluster) + + legrand_cluster.update_attribute(attr, value) + + assert len(legrand_cluster_listener.attribute_updates) == 2 + assert legrand_cluster_listener.attribute_updates[0][0] == attr + assert legrand_cluster_listener.attribute_updates[0][1] == value + assert legrand_cluster_listener.attribute_updates[1][0] == expected_attr + assert legrand_cluster_listener.attribute_updates[1][1] == expected_value From f45249824715155fbcd2b1e19c1710ed764f2cda Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 19 Mar 2024 20:59:49 +0100 Subject: [PATCH 24/26] Remove thermostat cluster --- zhaquirks/legrand/wire_pilot.py | 66 --------------------------------- 1 file changed, 66 deletions(-) diff --git a/zhaquirks/legrand/wire_pilot.py b/zhaquirks/legrand/wire_pilot.py index 03df6d1494..3063427a2a 100644 --- a/zhaquirks/legrand/wire_pilot.py +++ b/zhaquirks/legrand/wire_pilot.py @@ -4,7 +4,6 @@ from zigpy.quirks.v2 import add_to_registry_v2 import zigpy.types as t from zigpy.zcl import ClusterType -from zigpy.zcl.clusters.hvac import SystemMode, Thermostat from zigpy.zcl.foundation import ( BaseAttributeDefs, BaseCommandDefs, @@ -13,7 +12,6 @@ ZCLCommandDef, ) -from zhaquirks import LocalDataCluster from zhaquirks.legrand import LEGRAND, MANUFACTURER_SPECIFIC_CLUSTER_ID MANUFACTURER_SPECIFIC_CLUSTER_ID_2 = 0xFC40 # 64576 @@ -22,8 +20,6 @@ WIRE_PILOT_MODE_ATTR = 0x4000 LEGRAND_HEAT_MODE_ATTR = 0x00 -THERMOSTAT_HEAT_MODE_ATTR = 0x4000 -THERMOSTAT_SYSTEM_MODE_ATTR = Thermostat.attributes_by_name["system_mode"].id class LegrandCluster(CustomCluster): @@ -120,74 +116,12 @@ async def write_attributes(self, attributes, manufacturer=None) -> list: await self.set_heat_mode(value, manufacturer=manufacturer) return await super().write_attributes(attrs, manufacturer) - def _update_attribute(self, attrid, value): - super()._update_attribute(attrid, value) - if attrid == LEGRAND_HEAT_MODE_ATTR: - self.endpoint.thermostat.heat_mode_change(value) - - -class LegrandWirePilotThermostatCluster(LocalDataCluster, Thermostat): - """Thermostat cluster for Legrand wire pilot thermostats.""" - - _CONSTANT_ATTRIBUTES = {0x001B: Thermostat.ControlSequenceOfOperation.Heating_Only} - - class AttributeDefs(Thermostat.AttributeDefs): - """Attribute definitions.""" - - heat_mode = ZCLAttributeDef( - id=THERMOSTAT_HEAT_MODE_ATTR, - type=HeatMode, - ) - - async def write_attributes(self, attributes, manufacturer=None) -> list: - """Write attributes to the cluster.""" - - attrs = {} - mode: HeatMode = None - for attr, value in attributes.items(): - attr_def = self.find_attribute(attr) - attr_id = attr_def.id - if attr_id == THERMOSTAT_HEAT_MODE_ATTR: - mode = value - elif attr_id == THERMOSTAT_SYSTEM_MODE_ATTR: - current = self._attr_cache.get(THERMOSTAT_SYSTEM_MODE_ATTR) - if current != value: - mode = ( - HeatMode.Comfort if value == SystemMode.Heat else HeatMode.Off - ) - else: - attrs[attr] = value - - if mode is not None: - await self.endpoint.legrand_wire_pilot_cluster.set_heat_mode( - mode, manufacturer=manufacturer - ) - return await super().write_attributes(attrs, manufacturer) - - def heat_mode_change(self, value): - """Handle the change in heat mode.""" - - system_mode = SystemMode.Off - if value in ( - HeatMode.Comfort, - HeatMode.Comfort_minus_1, - HeatMode.Comfort_minus_2, - HeatMode.Eco, - HeatMode.Frost_protection, - ): - system_mode = SystemMode.Heat - - """Heat mode changed reported.""" - self._update_attribute(THERMOSTAT_SYSTEM_MODE_ATTR, system_mode) - self._update_attribute(THERMOSTAT_HEAT_MODE_ATTR, value) - ( add_to_registry_v2(f" {LEGRAND}", " Cable outlet") .replaces(LegrandCluster) .replaces(LegrandWirePilotCluster) .replaces(LegrandCluster, cluster_type=ClusterType.Client) - .adds(LegrandWirePilotThermostatCluster) .switch( attribute_name=LegrandCluster.AttributeDefs.wire_pilot_mode.name, cluster_id=LegrandCluster.cluster_id, From 9fca2acf14efd6885355666649bd50f15fe90dc0 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Tue, 26 Mar 2024 17:46:02 +0100 Subject: [PATCH 25/26] Use attribute def --- zhaquirks/legrand/wire_pilot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/zhaquirks/legrand/wire_pilot.py b/zhaquirks/legrand/wire_pilot.py index 3063427a2a..f5fb132558 100644 --- a/zhaquirks/legrand/wire_pilot.py +++ b/zhaquirks/legrand/wire_pilot.py @@ -57,14 +57,14 @@ async def write_attributes(self, attributes, manufacturer=None) -> list: attr_def = self.find_attribute(attr) attr_id = attr_def.id if attr_id == WIRE_PILOT_MODE_ATTR: - attrs[0x0000] = [0x02, 0x00] if value else [0x01, 0x00] + attrs[LegrandCluster.AttributeDefs.device_mode.id] = [0x02, 0x00] if value else [0x01, 0x00] else: attrs[attr] = value return await super().write_attributes(attrs, manufacturer) def _update_attribute(self, attrid, value) -> None: super()._update_attribute(attrid, value) - if attrid == 0x0000: + if attrid == LegrandCluster.AttributeDefs.device_mode.id: self._update_attribute(WIRE_PILOT_MODE_ATTR, value[0] == 0x02) @@ -112,7 +112,7 @@ async def write_attributes(self, attributes, manufacturer=None) -> list: for attr, value in attributes.items(): attr_def = self.find_attribute(attr) attr_id = attr_def.id - if attr_id == LEGRAND_HEAT_MODE_ATTR: + if attr_id == LegrandWirePilotCluster.AttributeDefs.heat_mode.id: await self.set_heat_mode(value, manufacturer=manufacturer) return await super().write_attributes(attrs, manufacturer) From 144fe4ce2b33e760a5f847f1fae486df63e8e521 Mon Sep 17 00:00:00 2001 From: Paul Bottein Date: Wed, 27 Mar 2024 11:18:51 +0100 Subject: [PATCH 26/26] Use attribute def --- zhaquirks/legrand/wire_pilot.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/zhaquirks/legrand/wire_pilot.py b/zhaquirks/legrand/wire_pilot.py index f5fb132558..dadbf1eeef 100644 --- a/zhaquirks/legrand/wire_pilot.py +++ b/zhaquirks/legrand/wire_pilot.py @@ -17,11 +17,6 @@ MANUFACTURER_SPECIFIC_CLUSTER_ID_2 = 0xFC40 # 64576 -WIRE_PILOT_MODE_ATTR = 0x4000 - -LEGRAND_HEAT_MODE_ATTR = 0x00 - - class LegrandCluster(CustomCluster): """LegrandCluster.""" @@ -47,7 +42,7 @@ class AttributeDefs(BaseAttributeDefs): type=t.Bool, is_manufacturer_specific=True, ) - wire_pilot_mode = ZCLAttributeDef(id=WIRE_PILOT_MODE_ATTR, type=t.Bool) + wire_pilot_mode = ZCLAttributeDef(id=0x4000, type=t.Bool) async def write_attributes(self, attributes, manufacturer=None) -> list: """Write attributes to the cluster.""" @@ -56,8 +51,10 @@ async def write_attributes(self, attributes, manufacturer=None) -> list: for attr, value in attributes.items(): attr_def = self.find_attribute(attr) attr_id = attr_def.id - if attr_id == WIRE_PILOT_MODE_ATTR: - attrs[LegrandCluster.AttributeDefs.device_mode.id] = [0x02, 0x00] if value else [0x01, 0x00] + if attr_id == LegrandCluster.AttributeDefs.wire_pilot_mode.id: + attrs[LegrandCluster.AttributeDefs.device_mode.id] = ( + [0x02, 0x00] if value else [0x01, 0x00] + ) else: attrs[attr] = value return await super().write_attributes(attrs, manufacturer) @@ -65,7 +62,9 @@ async def write_attributes(self, attributes, manufacturer=None) -> list: def _update_attribute(self, attrid, value) -> None: super()._update_attribute(attrid, value) if attrid == LegrandCluster.AttributeDefs.device_mode.id: - self._update_attribute(WIRE_PILOT_MODE_ATTR, value[0] == 0x02) + self._update_attribute( + LegrandCluster.AttributeDefs.wire_pilot_mode.id, value[0] == 0x02 + ) class HeatMode(t.enum8): @@ -90,7 +89,7 @@ class AttributeDefs(BaseAttributeDefs): """Attribute definitions for LegrandCluster.""" heat_mode = ZCLAttributeDef( - id=LEGRAND_HEAT_MODE_ATTR, + id=0x00, type=HeatMode, is_manufacturer_specific=True, ) @@ -99,7 +98,7 @@ class ServerCommandDefs(BaseCommandDefs): """Server command definitions.""" set_heat_mode = ZCLCommandDef( - id=LEGRAND_HEAT_MODE_ATTR, + id=0x00, schema={"mode": HeatMode}, direction=Direction.Client_to_Server, is_manufacturer_specific=True,