From da24506b24a2613e8a3bf955d7163ccce1725164 Mon Sep 17 00:00:00 2001 From: caius-bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sun, 29 Jan 2023 16:48:50 +0100 Subject: [PATCH 01/79] Danfoss thermostat: Fixup attributes access, types, names and remove unsupported ones. split up proprietary clusters. add preheat_command (no usage yet). force read before bind of cluster. Add 2 thermostat models. Refactoring --- zhaquirks/danfoss/__init__.py | 3 +- zhaquirks/danfoss/thermostat.py | 258 ++++++++++++++++++++++++-------- 2 files changed, 199 insertions(+), 62 deletions(-) diff --git a/zhaquirks/danfoss/__init__.py b/zhaquirks/danfoss/__init__.py index 3c39ed787a..4159407e34 100644 --- a/zhaquirks/danfoss/__init__.py +++ b/zhaquirks/danfoss/__init__.py @@ -1,3 +1,4 @@ """Module for Danfoss quirks implementations.""" DANFOSS = "Danfoss" -D5X84YU = "D5X84YU" +HIVE = DANFOSS +POPP = "D5X84YU" diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 66684148a9..37ed9de34a 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -1,10 +1,12 @@ """Module to handle quirks of the Danfoss thermostat. - manufacturer specific attributes to control displaying and specific configuration. """ - import zigpy.profiles.zha as zha_p from zigpy.quirks import CustomCluster, CustomDevice +from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster + +from zhaquirks import Bus + import zigpy.types as t from zigpy.zcl import foundation from zigpy.zcl.clusters.general import ( @@ -26,50 +28,175 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) -from zhaquirks.danfoss import D5X84YU, DANFOSS +from . import POPP, HIVE, DANFOSS + + +MANUFACTURER = 0x1246 + +# 0x0201 +danfoss_thermostat_attr = { + 0x4000: ("open_window_detection", t.enum8, "rp"), + 0x4003: ("external_open_window_detected", t.Bool, "rpw"), + 0x4051: ("window_open_feature", t.Bool, "rpw"), + + 0x4010: ("exercise_day_of_week", t.enum8, "rpw"), + 0x4011: ("exercise_trigger_time", t.uint16_t, "rpw"), + + 0x4012: ("mounting_mode_active", t.Bool, "rp"), + 0x4013: ("mounting_mode_control", t.Bool, "rpw"), # undocumented + + 0x4014: ("orientation", t.enum8, "rpw"), + + 0x4015: ("external_measured_room_sensor", t.int16s, "rpw"), + 0x4016: ("radiator_covered", t.Bool, "rpw"), + + 0x4030: ("heat_available", t.Bool, "rpw"), # undocumented + 0x4031: ("heat_required", t.Bool, "rp"), # undocumented + + 0x4032: ("load_balancing_enable", t.Bool, "rpw"), + 0x4040: ("load_room_mean", t.int16s, "rpw"), + 0x404A: ("load_estimate", t.int16s, "rp"), + + 0x4020: ("control_algorithm_scale_factor", t.uint8_t, "rpw"), + 0x404B: ("regulation_setpoint_offset", t.int8s, "rpw"), + + 0x404C: ("adaptation_run_control", t.enum8, "rw"), + 0x404D: ("adaptation_run_status", t.bitmap8, "rp"), + 0x404E: ("adaptation_run_settings", t.bitmap8, "rw"), + + 0x404F: ("preheat_status", t.Bool, "rp"), + 0x4050: ("preheat_time", t.uint32_t, "rp"), +} +# ZCL Attributes Supported: pi_heating_demand (0x0008) + +# reading mandatory ZCL attribute 0xFFFD results in UNSUPPORTED_ATTRIBUTE +# ZCL Commands Supported: SetWeeklySchedule (0x01), GetWeeklySchedule (0x02), ClearWeeklySchedule (0x03) + +# Danfos says they support the following, but Popp eT093WRO responds with UNSUPPORTED_ATTRIBUTE +# 0x0003, 0x0004, 0x0015, 0x0016, 0x0025, 0x0030, 0x0020, 0x0021, 0x0022 + +# 0x0204 +danfoss_interface_attr = { + 0x4000: ("viewing_direction", t.enum8, "rpw"), +} + +# Writing to mandatory ZCL attribute 0x0000 doesn't seem to do anything +# ZCL Attributes Supported: keypad_lockout (0x0001) + +# 0x0b05 +danfoss_diagnostic_attr = { + 0x4000: ("sw_error_code", t.bitmap16, "rp"), + 0x4001: ("wake_time_avg", t.uint32_t, "rp"), # always 0? + 0x4002: ("wake_time_max_duration", t.uint32_t, "rp"), # always 0? + 0x4003: ("wake_time_min_duration", t.uint32_t, "rp"), # always 0? + 0x4004: ("sleep_postponed_count_avg", t.uint32_t, "rp"), # always 0? + 0x4005: ("sleep_postponed_count_max", t.uint32_t, "rp"), # always 0? + 0x4006: ("sleep_postponed_count_min", t.uint32_t, "rp"), # always 0? + 0x4010: ("motor_step_counter", t.uint32_t, "rp"), +} + + +async def read_attributes(dest, source, dictionary): + """ Automatically reads attributes from source cluster and stores them in the dest cluster """ + response = {} + step = 14 # The device doesn't respond to more than 14 per request it seems + + # read from source + for a in range(0, len(dictionary)+step, step): + subset = list(dictionary.keys())[a:a+step] + if subset: + response.update((await source.read_attributes(subset, manufacturer=MANUFACTURER))[0]) + + # store all of them in dest + for attrid, value in response.items(): + dest.update_attribute(attrid, value) + + +class DanfossTRVCluster(CustomCluster, ManufacturerSpecificCluster): + """Danfoss custom TRV cluster""" + + cluster_id = 0xFC03 + ep_attribute = "danfoss_trv_cluster" + + attributes = ManufacturerSpecificCluster.attributes.copy() + attributes.update(danfoss_thermostat_attr) + + async def write_attributes(self, attributes, manufacturer=None): + return await self.endpoint.thermostat.write_attributes(attributes, manufacturer) + + async def bind(self): + # read attributes before ZHA binds, this makes sure the entity is created + result = await read_attributes(self, self.endpoint.thermostat, danfoss_thermostat_attr) + + return await super().bind() + + +class DanfossTRVInterfaceCluster(CustomCluster, ManufacturerSpecificCluster): + """Danfoss custom interface cluster""" + + cluster_id = 0xFC04 + ep_attribute = "danfoss_trv_interface_cluster" + + attributes = ManufacturerSpecificCluster.attributes.copy() + attributes.update(danfoss_interface_attr) + + async def write_attributes(self, attributes, manufacturer=None): + return await self.endpoint.thermostat_ui.write_attributes(attributes, manufacturer) + + async def bind(self): + # read attributes before ZHA binds, this makes sure the entity is created + await read_attributes(self, self.endpoint.thermostat_ui, danfoss_interface_attr) + + return await super().bind() + + +class DanfossTRVDiagnosticCluster(CustomCluster, ManufacturerSpecificCluster): + """Danfoss custom diagnostic cluster""" + + cluster_id = 0xFC05 + ep_attribute = "danfoss_trv_diagnostic_cluster" + + attributes = ManufacturerSpecificCluster.attributes.copy() + attributes.update(danfoss_diagnostic_attr) + + async def write_attributes(self, attributes, manufacturer=None): + return await self.endpoint.diagnostic.write_attributes(attributes, manufacturer) + + async def bind(self): + # read attributes before ZHA binds, this makes sure the entity is created + await read_attributes(self, self.endpoint.diagnostic, danfoss_diagnostic_attr) + + return await super().bind() class DanfossThermostatCluster(CustomCluster, Thermostat): - """Danfoss custom cluster.""" + """Danfoss cluster for ZCL attributes and forwarding proprietary the attributes""" server_commands = Thermostat.server_commands.copy() - server_commands[0x40] = foundation.ZCLCommandDef( - "setpoint_command", - {"param1": t.enum8, "param2": t.int16s}, - is_manufacturer_specific=True, - ) + server_commands = { + 0x40: foundation.ZCLCommandDef( + "setpoint_command", + # Types + # 0: Schedule (relatively slow) + # 1: User Interaction (aggressive change) + # 2: Preheat (invisible to user) + {"type": t.enum8, "heating_setpoint": t.int16s}, + is_manufacturer_specific=True, + ), + # for synchronizing multiple TRVs preheating + 0x42: foundation.ZCLCommandDef( + "preheat_command", + # Force: 0 means force, other values for future needs + {"force": t.enum8, "timestamp": t.uint32_t}, + is_manufacturer_specific=True, + ) + } attributes = Thermostat.attributes.copy() - attributes.update( - { - 0x4000: ("etrv_open_windows_detection", t.enum8, True), - 0x4003: ("external_open_windows_detected", t.Bool, True), - 0x4010: ("exercise_day_of_week", t.enum8, True), - 0x4011: ("exercise_trigger_time", t.uint16_t, True), - 0x4012: ("mounting_mode_active", t.Bool, True), - 0x4013: ("mounting_mode_control", t.Bool, True), - 0x4014: ("orientation", t.Bool, True), - 0x4015: ("external_measured_room_sensor", t.int16s, True), - 0x4016: ("radiator_covered", t.Bool, True), - 0x4020: ("control_algorithm_scale_factor", t.uint8_t, True), - 0x4030: ("heat_available", t.Bool, True), - 0x4031: ("heat_supply_request", t.Bool, True), - 0x4032: ("load_balancing_enable", t.Bool, True), - 0x4040: ("load_radiator_room_mean", t.uint16_t, True), - 0x404A: ("load_estimate_radiator", t.uint16_t, True), - 0x404B: ("regulation_setPoint_offset", t.int8s, True), - 0x404C: ("adaptation_run_control", t.enum8, True), - 0x404D: ("adaptation_run_status", t.bitmap8, True), - 0x404E: ("adaptation_run_settings", t.bitmap8, True), - 0x404F: ("preheat_status", t.Bool, True), - 0x4050: ("preheat_time", t.uint32_t, True), - 0x4051: ("window_open_feature_on_off", t.Bool, True), - 0xFFFD: ("cluster_revision", t.uint16_t, True), - } - ) + attributes.update(danfoss_thermostat_attr) async def write_attributes(self, attributes, manufacturer=None): - """Send SETPOINT_COMMAND after setpoint change.""" + """Send SETPOINT_COMMAND after setpoint change""" write_res = await super().write_attributes( attributes, manufacturer=manufacturer @@ -84,50 +211,55 @@ async def write_attributes(self, attributes, manufacturer=None): ) return write_res + + def _update_attribute(self, attrid, value): + if attrid in {a for (a, *_) in danfoss_thermostat_attr.values()}: + self.endpoint.danfoss_trv_cluster.update_attribute(attrid, value) + + # update local either way + super()._update_attribute(attrid, value) class DanfossUserInterfaceCluster(CustomCluster, UserInterface): - """Danfoss custom cluster.""" + """Danfoss cluster for ZCL attributes and forwarding proprietary the attributes""" attributes = UserInterface.attributes.copy() - attributes.update( - { - 0x4000: ("viewing_direction", t.enum8, True), - } - ) + attributes.update(danfoss_interface_attr) + + def _update_attribute(self, attrid, value): + if attrid in {a for (a, *_) in danfoss_interface_attr.values()}: + self.endpoint.danfoss_trv_interface_cluster.update_attribute(attrid, value) + + # update local either way + super()._update_attribute(attrid, value) class DanfossDiagnosticCluster(CustomCluster, Diagnostic): - """Danfoss custom cluster.""" + """Danfoss cluster for ZCL attributes and forwarding proprietary the attributes""" attributes = Diagnostic.attributes.copy() - attributes.update( - { - 0x4000: ("sw_error_code", t.bitmap16, True), - 0x4001: ("wake_time_avg", t.uint32_t, True), - 0x4002: ("wake_time_max_duration", t.uint32_t, True), - 0x4003: ("wake_time_min_duration", t.uint32_t, True), - 0x4004: ("sleep_postponed_count_avg", t.uint32_t, True), - 0x4005: ("sleep_postponed_count_max", t.uint32_t, True), - 0x4006: ("sleep_postponed_count_min", t.uint32_t, True), - 0x4010: ("motor_step_counter", t.uint32_t, True), - } - ) + attributes.update(danfoss_diagnostic_attr) + + def _update_attribute(self, attrid, value): + if attrid in {a for (a, *_) in danfoss_diagnostic_attr.values()}: + self.endpoint.danfoss_trv_diagnostic_cluster.update_attribute(attrid, value) + + # update local either way + super()._update_attribute(attrid, value) class DanfossThermostat(CustomDevice): """DanfossThermostat custom device.""" signature = { - # MODELS_INFO: [ - (DANFOSS, "TRV001"), (DANFOSS, "eTRV0100"), (DANFOSS, "eTRV0101"), (DANFOSS, "eTRV0103"), - (D5X84YU, "eT093WRO"), + (POPP, "eT093WRO"), + (POPP, "eT093WRG"), + (HIVE, "TRV001"), + (HIVE, "TRV003"), ], ENDPOINTS: { 1: { @@ -160,6 +292,10 @@ class DanfossThermostat(CustomDevice): DanfossThermostatCluster, DanfossUserInterfaceCluster, DanfossDiagnosticCluster, + DanfossTRVCluster, + DanfossTRVInterfaceCluster, + DanfossTRVDiagnosticCluster, + ], OUTPUT_CLUSTERS: [Basic, Ota], } From 7d914a70648ab99649e134e285f08768b6801eed Mon Sep 17 00:00:00 2001 From: caius-bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sun, 29 Jan 2023 16:56:19 +0100 Subject: [PATCH 02/79] Danfoss thermostat: remove dead import --- zhaquirks/danfoss/thermostat.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 37ed9de34a..f96be5769e 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -5,8 +5,6 @@ from zigpy.quirks import CustomCluster, CustomDevice from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster -from zhaquirks import Bus - import zigpy.types as t from zigpy.zcl import foundation from zigpy.zcl.clusters.general import ( From b904108b95ec675928a7500ebfe4af33f9463143 Mon Sep 17 00:00:00 2001 From: caius-bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sun, 29 Jan 2023 19:09:18 +0100 Subject: [PATCH 03/79] formatted using black --- zhaquirks/danfoss/thermostat.py | 64 ++++++++++++++++----------------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index f96be5769e..8cdd2fde03 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -36,32 +36,23 @@ 0x4000: ("open_window_detection", t.enum8, "rp"), 0x4003: ("external_open_window_detected", t.Bool, "rpw"), 0x4051: ("window_open_feature", t.Bool, "rpw"), - 0x4010: ("exercise_day_of_week", t.enum8, "rpw"), 0x4011: ("exercise_trigger_time", t.uint16_t, "rpw"), - 0x4012: ("mounting_mode_active", t.Bool, "rp"), - 0x4013: ("mounting_mode_control", t.Bool, "rpw"), # undocumented - + 0x4013: ("mounting_mode_control", t.Bool, "rpw"), # undocumented 0x4014: ("orientation", t.enum8, "rpw"), - 0x4015: ("external_measured_room_sensor", t.int16s, "rpw"), 0x4016: ("radiator_covered", t.Bool, "rpw"), - - 0x4030: ("heat_available", t.Bool, "rpw"), # undocumented - 0x4031: ("heat_required", t.Bool, "rp"), # undocumented - + 0x4030: ("heat_available", t.Bool, "rpw"), # undocumented + 0x4031: ("heat_required", t.Bool, "rp"), # undocumented 0x4032: ("load_balancing_enable", t.Bool, "rpw"), 0x4040: ("load_room_mean", t.int16s, "rpw"), 0x404A: ("load_estimate", t.int16s, "rp"), - 0x4020: ("control_algorithm_scale_factor", t.uint8_t, "rpw"), 0x404B: ("regulation_setpoint_offset", t.int8s, "rpw"), - 0x404C: ("adaptation_run_control", t.enum8, "rw"), 0x404D: ("adaptation_run_status", t.bitmap8, "rp"), 0x404E: ("adaptation_run_settings", t.bitmap8, "rw"), - 0x404F: ("preheat_status", t.Bool, "rp"), 0x4050: ("preheat_time", t.uint32_t, "rp"), } @@ -84,27 +75,29 @@ # 0x0b05 danfoss_diagnostic_attr = { 0x4000: ("sw_error_code", t.bitmap16, "rp"), - 0x4001: ("wake_time_avg", t.uint32_t, "rp"), # always 0? - 0x4002: ("wake_time_max_duration", t.uint32_t, "rp"), # always 0? - 0x4003: ("wake_time_min_duration", t.uint32_t, "rp"), # always 0? - 0x4004: ("sleep_postponed_count_avg", t.uint32_t, "rp"), # always 0? - 0x4005: ("sleep_postponed_count_max", t.uint32_t, "rp"), # always 0? - 0x4006: ("sleep_postponed_count_min", t.uint32_t, "rp"), # always 0? + 0x4001: ("wake_time_avg", t.uint32_t, "rp"), # always 0? + 0x4002: ("wake_time_max_duration", t.uint32_t, "rp"), # always 0? + 0x4003: ("wake_time_min_duration", t.uint32_t, "rp"), # always 0? + 0x4004: ("sleep_postponed_count_avg", t.uint32_t, "rp"), # always 0? + 0x4005: ("sleep_postponed_count_max", t.uint32_t, "rp"), # always 0? + 0x4006: ("sleep_postponed_count_min", t.uint32_t, "rp"), # always 0? 0x4010: ("motor_step_counter", t.uint32_t, "rp"), } async def read_attributes(dest, source, dictionary): - """ Automatically reads attributes from source cluster and stores them in the dest cluster """ + """Automatically reads attributes from source cluster and stores them in the dest cluster""" response = {} - step = 14 # The device doesn't respond to more than 14 per request it seems + step = 14 # The device doesn't respond to more than 14 per request it seems # read from source - for a in range(0, len(dictionary)+step, step): - subset = list(dictionary.keys())[a:a+step] + for a in range(0, len(dictionary) + step, step): + subset = list(dictionary.keys())[a : a + step] if subset: - response.update((await source.read_attributes(subset, manufacturer=MANUFACTURER))[0]) - + response.update( + (await source.read_attributes(subset, manufacturer=MANUFACTURER))[0] + ) + # store all of them in dest for attrid, value in response.items(): dest.update_attribute(attrid, value) @@ -124,8 +117,10 @@ async def write_attributes(self, attributes, manufacturer=None): async def bind(self): # read attributes before ZHA binds, this makes sure the entity is created - result = await read_attributes(self, self.endpoint.thermostat, danfoss_thermostat_attr) - + result = await read_attributes( + self, self.endpoint.thermostat, danfoss_thermostat_attr + ) + return await super().bind() @@ -139,7 +134,9 @@ class DanfossTRVInterfaceCluster(CustomCluster, ManufacturerSpecificCluster): attributes.update(danfoss_interface_attr) async def write_attributes(self, attributes, manufacturer=None): - return await self.endpoint.thermostat_ui.write_attributes(attributes, manufacturer) + return await self.endpoint.thermostat_ui.write_attributes( + attributes, manufacturer + ) async def bind(self): # read attributes before ZHA binds, this makes sure the entity is created @@ -163,7 +160,7 @@ async def write_attributes(self, attributes, manufacturer=None): async def bind(self): # read attributes before ZHA binds, this makes sure the entity is created await read_attributes(self, self.endpoint.diagnostic, danfoss_diagnostic_attr) - + return await super().bind() @@ -187,7 +184,7 @@ class DanfossThermostatCluster(CustomCluster, Thermostat): # Force: 0 means force, other values for future needs {"force": t.enum8, "timestamp": t.uint32_t}, is_manufacturer_specific=True, - ) + ), } attributes = Thermostat.attributes.copy() @@ -209,11 +206,11 @@ async def write_attributes(self, attributes, manufacturer=None): ) return write_res - + def _update_attribute(self, attrid, value): if attrid in {a for (a, *_) in danfoss_thermostat_attr.values()}: self.endpoint.danfoss_trv_cluster.update_attribute(attrid, value) - + # update local either way super()._update_attribute(attrid, value) @@ -227,7 +224,7 @@ class DanfossUserInterfaceCluster(CustomCluster, UserInterface): def _update_attribute(self, attrid, value): if attrid in {a for (a, *_) in danfoss_interface_attr.values()}: self.endpoint.danfoss_trv_interface_cluster.update_attribute(attrid, value) - + # update local either way super()._update_attribute(attrid, value) @@ -241,7 +238,7 @@ class DanfossDiagnosticCluster(CustomCluster, Diagnostic): def _update_attribute(self, attrid, value): if attrid in {a for (a, *_) in danfoss_diagnostic_attr.values()}: self.endpoint.danfoss_trv_diagnostic_cluster.update_attribute(attrid, value) - + # update local either way super()._update_attribute(attrid, value) @@ -293,7 +290,6 @@ class DanfossThermostat(CustomDevice): DanfossTRVCluster, DanfossTRVInterfaceCluster, DanfossTRVDiagnosticCluster, - ], OUTPUT_CLUSTERS: [Basic, Ota], } From 3e745ad189db6ecc68c6893bdf0a063fd7a68deb Mon Sep 17 00:00:00 2001 From: caius-bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sun, 29 Jan 2023 19:31:28 +0100 Subject: [PATCH 04/79] danfoss thermostat: map System Mode (aka Operation Mode or HVAC mode) to change setpoint to 5 C --- zhaquirks/danfoss/thermostat.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 8cdd2fde03..65ea35b38f 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -204,6 +204,10 @@ async def write_attributes(self, attributes, manufacturer=None): await self.setpoint_command( 0x01, attributes["occupied_heating_setpoint"], manufacturer=manufacturer ) + elif "system_mode" in attributes and attributes["system_mode"] == 0: + # Thermostatic Radiator Valves often cannot be turned off to prevent damage during frost + # just turn setpoint down to minumum of 5 degrees Celcius + await self.setpoint_command(0x01, 500, manufacturer=manufacturer) return write_res From e9282b30153d542709a21e3159a8e8e8824bcdca Mon Sep 17 00:00:00 2001 From: caius-bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sun, 29 Jan 2023 19:33:56 +0100 Subject: [PATCH 05/79] fix unused assignment --- zhaquirks/danfoss/thermostat.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 65ea35b38f..1cc4c344a3 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -117,9 +117,7 @@ async def write_attributes(self, attributes, manufacturer=None): async def bind(self): # read attributes before ZHA binds, this makes sure the entity is created - result = await read_attributes( - self, self.endpoint.thermostat, danfoss_thermostat_attr - ) + await read_attributes(self, self.endpoint.thermostat, danfoss_thermostat_attr) return await super().bind() From f63b21f14af1e2a4a083e15be66b20452aae6e91 Mon Sep 17 00:00:00 2001 From: caius-bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sun, 29 Jan 2023 19:56:10 +0100 Subject: [PATCH 06/79] danfoss thermostat: fix flake8 issues --- zhaquirks/danfoss/thermostat.py | 37 ++++++++++++++++++++------------- zhaquirks/tuya/ts0601_dimmer.py | 2 -- zhaquirks/xbee/types.py | 2 -- 3 files changed, 22 insertions(+), 19 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 1cc4c344a3..519bd1286a 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -1,10 +1,9 @@ """Module to handle quirks of the Danfoss thermostat. + manufacturer specific attributes to control displaying and specific configuration. """ import zigpy.profiles.zha as zha_p from zigpy.quirks import CustomCluster, CustomDevice -from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster - import zigpy.types as t from zigpy.zcl import foundation from zigpy.zcl.clusters.general import ( @@ -17,6 +16,7 @@ ) from zigpy.zcl.clusters.homeautomation import Diagnostic from zigpy.zcl.clusters.hvac import Thermostat, UserInterface +from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster from zhaquirks.const import ( DEVICE_TYPE, @@ -26,8 +26,8 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) -from . import POPP, HIVE, DANFOSS +from . import DANFOSS, HIVE, POPP MANUFACTURER = 0x1246 @@ -86,7 +86,7 @@ async def read_attributes(dest, source, dictionary): - """Automatically reads attributes from source cluster and stores them in the dest cluster""" + """Automatically reads attributes from source cluster and stores them in the dest cluster.""" response = {} step = 14 # The device doesn't respond to more than 14 per request it seems @@ -104,7 +104,7 @@ async def read_attributes(dest, source, dictionary): class DanfossTRVCluster(CustomCluster, ManufacturerSpecificCluster): - """Danfoss custom TRV cluster""" + """Danfoss custom TRV cluster.""" cluster_id = 0xFC03 ep_attribute = "danfoss_trv_cluster" @@ -113,17 +113,18 @@ class DanfossTRVCluster(CustomCluster, ManufacturerSpecificCluster): attributes.update(danfoss_thermostat_attr) async def write_attributes(self, attributes, manufacturer=None): + """Write attributes to thermostat cluster.""" return await self.endpoint.thermostat.write_attributes(attributes, manufacturer) async def bind(self): - # read attributes before ZHA binds, this makes sure the entity is created + """Read attributes before ZHA binds, this makes sure the entity is created.""" await read_attributes(self, self.endpoint.thermostat, danfoss_thermostat_attr) return await super().bind() class DanfossTRVInterfaceCluster(CustomCluster, ManufacturerSpecificCluster): - """Danfoss custom interface cluster""" + """Danfoss custom interface cluster.""" cluster_id = 0xFC04 ep_attribute = "danfoss_trv_interface_cluster" @@ -132,19 +133,21 @@ class DanfossTRVInterfaceCluster(CustomCluster, ManufacturerSpecificCluster): attributes.update(danfoss_interface_attr) async def write_attributes(self, attributes, manufacturer=None): + """Write attributes to thermostat user interface cluster.""" + return await self.endpoint.thermostat_ui.write_attributes( attributes, manufacturer ) async def bind(self): - # read attributes before ZHA binds, this makes sure the entity is created + """Read attributes before ZHA binds, this makes sure the entity is created.""" await read_attributes(self, self.endpoint.thermostat_ui, danfoss_interface_attr) return await super().bind() class DanfossTRVDiagnosticCluster(CustomCluster, ManufacturerSpecificCluster): - """Danfoss custom diagnostic cluster""" + """Danfoss custom diagnostic cluster.""" cluster_id = 0xFC05 ep_attribute = "danfoss_trv_diagnostic_cluster" @@ -153,17 +156,18 @@ class DanfossTRVDiagnosticCluster(CustomCluster, ManufacturerSpecificCluster): attributes.update(danfoss_diagnostic_attr) async def write_attributes(self, attributes, manufacturer=None): + """Write attributes to diagnostic cluster.""" return await self.endpoint.diagnostic.write_attributes(attributes, manufacturer) async def bind(self): - # read attributes before ZHA binds, this makes sure the entity is created + """Read attributes before ZHA binds, this makes sure the entity is created.""" await read_attributes(self, self.endpoint.diagnostic, danfoss_diagnostic_attr) return await super().bind() class DanfossThermostatCluster(CustomCluster, Thermostat): - """Danfoss cluster for ZCL attributes and forwarding proprietary the attributes""" + """Danfoss cluster for ZCL attributes and forwarding proprietary the attributes.""" server_commands = Thermostat.server_commands.copy() server_commands = { @@ -189,7 +193,7 @@ class DanfossThermostatCluster(CustomCluster, Thermostat): attributes.update(danfoss_thermostat_attr) async def write_attributes(self, attributes, manufacturer=None): - """Send SETPOINT_COMMAND after setpoint change""" + """Send SETPOINT_COMMAND after setpoint change.""" write_res = await super().write_attributes( attributes, manufacturer=manufacturer @@ -204,12 +208,13 @@ async def write_attributes(self, attributes, manufacturer=None): ) elif "system_mode" in attributes and attributes["system_mode"] == 0: # Thermostatic Radiator Valves often cannot be turned off to prevent damage during frost - # just turn setpoint down to minumum of 5 degrees Celcius + # just turn setpoint down to minimum of 5 degrees celsius await self.setpoint_command(0x01, 500, manufacturer=manufacturer) return write_res def _update_attribute(self, attrid, value): + """Update attributes of TRV cluster.""" if attrid in {a for (a, *_) in danfoss_thermostat_attr.values()}: self.endpoint.danfoss_trv_cluster.update_attribute(attrid, value) @@ -218,12 +223,13 @@ def _update_attribute(self, attrid, value): class DanfossUserInterfaceCluster(CustomCluster, UserInterface): - """Danfoss cluster for ZCL attributes and forwarding proprietary the attributes""" + """Danfoss cluster for ZCL attributes and forwarding proprietary the attributes.""" attributes = UserInterface.attributes.copy() attributes.update(danfoss_interface_attr) def _update_attribute(self, attrid, value): + """Update attributes of TRV interface cluster.""" if attrid in {a for (a, *_) in danfoss_interface_attr.values()}: self.endpoint.danfoss_trv_interface_cluster.update_attribute(attrid, value) @@ -232,12 +238,13 @@ def _update_attribute(self, attrid, value): class DanfossDiagnosticCluster(CustomCluster, Diagnostic): - """Danfoss cluster for ZCL attributes and forwarding proprietary the attributes""" + """Danfoss cluster for ZCL attributes and forwarding proprietary the attributes.""" attributes = Diagnostic.attributes.copy() attributes.update(danfoss_diagnostic_attr) def _update_attribute(self, attrid, value): + """Update attributes or TRV diagnostic cluster.""" if attrid in {a for (a, *_) in danfoss_diagnostic_attr.values()}: self.endpoint.danfoss_trv_diagnostic_cluster.update_attribute(attrid, value) diff --git a/zhaquirks/tuya/ts0601_dimmer.py b/zhaquirks/tuya/ts0601_dimmer.py index 27af8248ac..8207f81e9f 100644 --- a/zhaquirks/tuya/ts0601_dimmer.py +++ b/zhaquirks/tuya/ts0601_dimmer.py @@ -22,8 +22,6 @@ class TuyaInWallLevelControlNM(NoManufacturerCluster, TuyaInWallLevelControl): """Tuya Level cluster for inwall dimmable device with NoManufacturerID.""" - pass - # --- DEVICE SUMMARY --- # TuyaSingleSwitchDimmer: 0x00, 0x04, 0x05, 0xEF00; 0x000A, 0x0019 diff --git a/zhaquirks/xbee/types.py b/zhaquirks/xbee/types.py index 80a1c5ab9c..953b3e4fef 100644 --- a/zhaquirks/xbee/types.py +++ b/zhaquirks/xbee/types.py @@ -109,8 +109,6 @@ def serialize(self): class FrameId(uint8_t): """Frame ID type.""" - pass - class NWK(int): """Network address serializable class.""" From bf7d91138a98b4f96168dd3800c70d7ba9e94575 Mon Sep 17 00:00:00 2001 From: caius-bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Wed, 1 Feb 2023 13:25:11 +0100 Subject: [PATCH 07/79] danfoss thermostat: use absolute import --- zhaquirks/danfoss/thermostat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 519bd1286a..aa029e55f7 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -27,7 +27,7 @@ PROFILE_ID, ) -from . import DANFOSS, HIVE, POPP +from zhaquirks.danfoss import DANFOSS, HIVE, POPP MANUFACTURER = 0x1246 From 40095645a208236ff90db42404c310c500f04fed Mon Sep 17 00:00:00 2001 From: caius-bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sat, 4 Feb 2023 22:16:52 +0100 Subject: [PATCH 08/79] Danfoss thermostat: add trv_operation_mode, occupied_heating_setpoint_scheduled, cluster_revision. Redirect reads from custom clusters. fix _update_attribute. Place SimpleDescriptor back. Move reads at bind to ZHA. --- zhaquirks/danfoss/thermostat.py | 196 +++++++++++++++++++++----------- 1 file changed, 128 insertions(+), 68 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index aa029e55f7..a3299b3c53 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -5,7 +5,6 @@ import zigpy.profiles.zha as zha_p from zigpy.quirks import CustomCluster, CustomDevice import zigpy.types as t -from zigpy.zcl import foundation from zigpy.zcl.clusters.general import ( Basic, Identify, @@ -17,6 +16,7 @@ from zigpy.zcl.clusters.homeautomation import Diagnostic from zigpy.zcl.clusters.hvac import Thermostat, UserInterface from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster +from zigpy.zcl.foundation import ZCLAttributeDef, ZCLCommandDef from zhaquirks.const import ( DEVICE_TYPE, @@ -26,10 +26,19 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) - from zhaquirks.danfoss import DANFOSS, HIVE, POPP -MANUFACTURER = 0x1246 + +class DanfossOperationModeEnum(t.bitmap8): + """Nonstandard and unnecessarily complicated implementation of Programming Operation Mode from Danfoss.""" + + Manual = 0b00000000 + Schedule = 0b00000001 + Manual_Preheat = 0b00000010 + Schedule_Preheat = 0b00000011 + + +setpoint_change_scheduled = 0x41FF # 0x0201 danfoss_thermostat_attr = { @@ -55,15 +64,33 @@ 0x404E: ("adaptation_run_settings", t.bitmap8, "rw"), 0x404F: ("preheat_status", t.Bool, "rp"), 0x4050: ("preheat_time", t.uint32_t, "rp"), + # Danfoss deviated heavily from the spec with this one + 0x0025: ZCLAttributeDef("trv_operation_mode", type=t.bitmap8, access="rpw"), + # We need a convenient way to access this, so we create our own attribute + 0x41FF: ZCLAttributeDef( + "occupied_heating_setpoint_scheduled", + type=DanfossOperationModeEnum, + access="rpw", + ), +} +# ZCL Attributes Supported: +# pi_heating_demand (0x0008), +# min_heat_setpoint_limit (0x0015) +# max_heat_setpoint_limit (0x0016) +# setpoint_change_source (0x0030) +# hardcoded: +# abs_min_heat_setpoint_limit (0x0003)=5 +# abs_max_heat_setpoint_limit (0x0004)=35 +# start_of_week (0x0020)=Monday +# number_of_weekly_transitions (0x0021)=42 +# number_of_daily_transitions (0x0022)=6 + +zcl_attr = { + 0xFFFD: ("cluster_revision", t.uint16_t, "r"), } -# ZCL Attributes Supported: pi_heating_demand (0x0008) -# reading mandatory ZCL attribute 0xFFFD results in UNSUPPORTED_ATTRIBUTE # ZCL Commands Supported: SetWeeklySchedule (0x01), GetWeeklySchedule (0x02), ClearWeeklySchedule (0x03) -# Danfos says they support the following, but Popp eT093WRO responds with UNSUPPORTED_ATTRIBUTE -# 0x0003, 0x0004, 0x0015, 0x0016, 0x0025, 0x0030, 0x0020, 0x0021, 0x0022 - # 0x0204 danfoss_interface_attr = { 0x4000: ("viewing_direction", t.enum8, "rpw"), @@ -85,24 +112,6 @@ } -async def read_attributes(dest, source, dictionary): - """Automatically reads attributes from source cluster and stores them in the dest cluster.""" - response = {} - step = 14 # The device doesn't respond to more than 14 per request it seems - - # read from source - for a in range(0, len(dictionary) + step, step): - subset = list(dictionary.keys())[a : a + step] - if subset: - response.update( - (await source.read_attributes(subset, manufacturer=MANUFACTURER))[0] - ) - - # store all of them in dest - for attrid, value in response.items(): - dest.update_attribute(attrid, value) - - class DanfossTRVCluster(CustomCluster, ManufacturerSpecificCluster): """Danfoss custom TRV cluster.""" @@ -114,13 +123,40 @@ class DanfossTRVCluster(CustomCluster, ManufacturerSpecificCluster): async def write_attributes(self, attributes, manufacturer=None): """Write attributes to thermostat cluster.""" - return await self.endpoint.thermostat.write_attributes(attributes, manufacturer) + return await self.endpoint.thermostat.write_attributes( + attributes, manufacturer=manufacturer + ) + + async def read_attributes_raw(self, attributes, manufacturer=None): + """Operation Mode is a ZCL attribute and needs to be requested without manufacturer code.""" + + # Setpoint_change_scheduled is not a real attribute + setpoint_change_scheduled = 0x41FF + if setpoint_change_scheduled in attributes: + attributes.remove(setpoint_change_scheduled) + + # Operation Mode is nonstandard and therefore manufacturer specific + operation_mode = 0x0025 + result_oper_mode = None + if operation_mode in attributes: + attributes.remove(operation_mode) + result_oper_mode = await self.endpoint.thermostat.read_attributes_raw( + [operation_mode], manufacturer=None + ) - async def bind(self): - """Read attributes before ZHA binds, this makes sure the entity is created.""" - await read_attributes(self, self.endpoint.thermostat, danfoss_thermostat_attr) + # Get normal result + result = None + if attributes: + result = await self.endpoint.thermostat.read_attributes_raw( + attributes, manufacturer=manufacturer + ) - return await super().bind() + # Combine results + if result_oper_mode and result: + result[0].append(result_oper_mode[0][0]) + return result + else: + return result if result else result_oper_mode class DanfossTRVInterfaceCluster(CustomCluster, ManufacturerSpecificCluster): @@ -134,16 +170,15 @@ class DanfossTRVInterfaceCluster(CustomCluster, ManufacturerSpecificCluster): async def write_attributes(self, attributes, manufacturer=None): """Write attributes to thermostat user interface cluster.""" - return await self.endpoint.thermostat_ui.write_attributes( - attributes, manufacturer + attributes, manufacturer=manufacturer ) - async def bind(self): - """Read attributes before ZHA binds, this makes sure the entity is created.""" - await read_attributes(self, self.endpoint.thermostat_ui, danfoss_interface_attr) - - return await super().bind() + def read_attributes_raw(self, attributes, manufacturer=None): + """Read in attributes from User Interface Cluster.""" + return self.endpoint.thermostat_ui.read_attributes_raw( + attributes, manufacturer=manufacturer + ) class DanfossTRVDiagnosticCluster(CustomCluster, ManufacturerSpecificCluster): @@ -157,13 +192,15 @@ class DanfossTRVDiagnosticCluster(CustomCluster, ManufacturerSpecificCluster): async def write_attributes(self, attributes, manufacturer=None): """Write attributes to diagnostic cluster.""" - return await self.endpoint.diagnostic.write_attributes(attributes, manufacturer) - - async def bind(self): - """Read attributes before ZHA binds, this makes sure the entity is created.""" - await read_attributes(self, self.endpoint.diagnostic, danfoss_diagnostic_attr) + return await self.endpoint.diagnostic.write_attributes( + attributes, manufacturer=manufacturer + ) - return await super().bind() + def read_attributes_raw(self, attributes, manufacturer=None): + """Read in attributes from Diagnostic Cluster.""" + return self.endpoint.diagnostic.read_attributes_raw( + attributes, manufacturer=manufacturer + ) class DanfossThermostatCluster(CustomCluster, Thermostat): @@ -171,7 +208,7 @@ class DanfossThermostatCluster(CustomCluster, Thermostat): server_commands = Thermostat.server_commands.copy() server_commands = { - 0x40: foundation.ZCLCommandDef( + 0x40: ZCLCommandDef( "setpoint_command", # Types # 0: Schedule (relatively slow) @@ -181,7 +218,7 @@ class DanfossThermostatCluster(CustomCluster, Thermostat): is_manufacturer_specific=True, ), # for synchronizing multiple TRVs preheating - 0x42: foundation.ZCLCommandDef( + 0x42: ZCLCommandDef( "preheat_command", # Force: 0 means force, other values for future needs {"force": t.enum8, "timestamp": t.uint32_t}, @@ -191,71 +228,94 @@ class DanfossThermostatCluster(CustomCluster, Thermostat): attributes = Thermostat.attributes.copy() attributes.update(danfoss_thermostat_attr) + attributes.update(zcl_attr) async def write_attributes(self, attributes, manufacturer=None): """Send SETPOINT_COMMAND after setpoint change.""" + off = 0x00 + scheduled = False + if "system_mode" in attributes and attributes["system_mode"] == off: + # Thermostatic Radiator Valves from Danfoss cannot be turned off to prevent damage during frost + # just turn setpoint down to minimum temperature + min_heat_setpoint_limit = 0x0015 + attributes["occupied_heating_setpoint"] = self._attr_cache[ + min_heat_setpoint_limit + ] + elif "occupied_heating_setpoint_scheduled" in attributes: + attributes["occupied_heating_setpoint"] = attributes.pop( + "occupied_heating_setpoint_scheduled" + ) + scheduled = True + write_res = await super().write_attributes( attributes, manufacturer=manufacturer ) - if "occupied_heating_setpoint" in attributes: - self.debug( - "sending setpoint command: %s", attributes["occupied_heating_setpoint"] - ) + if "occupied_heating_setpoint" in attributes and not scheduled: + aggressive_setpoint = 0x01 await self.setpoint_command( - 0x01, attributes["occupied_heating_setpoint"], manufacturer=manufacturer + aggressive_setpoint, + attributes["occupied_heating_setpoint"], + manufacturer=manufacturer, ) - elif "system_mode" in attributes and attributes["system_mode"] == 0: - # Thermostatic Radiator Valves often cannot be turned off to prevent damage during frost - # just turn setpoint down to minimum of 5 degrees celsius - await self.setpoint_command(0x01, 500, manufacturer=manufacturer) return write_res def _update_attribute(self, attrid, value): """Update attributes of TRV cluster.""" - if attrid in {a for (a, *_) in danfoss_thermostat_attr.values()}: - self.endpoint.danfoss_trv_cluster.update_attribute(attrid, value) - - # update local either way super()._update_attribute(attrid, value) + setpoint_change = 0x0012 + if attrid == setpoint_change: + self.endpoint.danfoss_trv_cluster._update_attribute( + setpoint_change_scheduled, value + ) + + if attrid in danfoss_thermostat_attr: + self.endpoint.danfoss_trv_cluster.update_attribute(attrid, value) + class DanfossUserInterfaceCluster(CustomCluster, UserInterface): """Danfoss cluster for ZCL attributes and forwarding proprietary the attributes.""" attributes = UserInterface.attributes.copy() attributes.update(danfoss_interface_attr) + attributes.update(zcl_attr) def _update_attribute(self, attrid, value): """Update attributes of TRV interface cluster.""" - if attrid in {a for (a, *_) in danfoss_interface_attr.values()}: - self.endpoint.danfoss_trv_interface_cluster.update_attribute(attrid, value) - - # update local either way super()._update_attribute(attrid, value) + if attrid in danfoss_interface_attr: + self.endpoint.danfoss_trv_interface_cluster.update_attribute(attrid, value) + class DanfossDiagnosticCluster(CustomCluster, Diagnostic): """Danfoss cluster for ZCL attributes and forwarding proprietary the attributes.""" attributes = Diagnostic.attributes.copy() attributes.update(danfoss_diagnostic_attr) + attributes.update(zcl_attr) def _update_attribute(self, attrid, value): """Update attributes or TRV diagnostic cluster.""" - if attrid in {a for (a, *_) in danfoss_diagnostic_attr.values()}: - self.endpoint.danfoss_trv_diagnostic_cluster.update_attribute(attrid, value) - - # update local either way super()._update_attribute(attrid, value) + if attrid in danfoss_diagnostic_attr: + self.endpoint.danfoss_trv_diagnostic_cluster.update_attribute(attrid, value) + class DanfossThermostat(CustomDevice): - """DanfossThermostat custom device.""" + """DanfossThermostat custom device. + + manufacturer_code = 0x1246 + """ signature = { + # MODELS_INFO: [ (DANFOSS, "eTRV0100"), (DANFOSS, "eTRV0101"), From 4b227d1f7858ff2c762b50a9c4c0efffdf29f567 Mon Sep 17 00:00:00 2001 From: caius-bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sat, 4 Feb 2023 23:26:08 +0100 Subject: [PATCH 09/79] reverse automatic edit in file I didn't want to touch --- zhaquirks/tuya/ts0601_dimmer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zhaquirks/tuya/ts0601_dimmer.py b/zhaquirks/tuya/ts0601_dimmer.py index 8207f81e9f..27af8248ac 100644 --- a/zhaquirks/tuya/ts0601_dimmer.py +++ b/zhaquirks/tuya/ts0601_dimmer.py @@ -22,6 +22,8 @@ class TuyaInWallLevelControlNM(NoManufacturerCluster, TuyaInWallLevelControl): """Tuya Level cluster for inwall dimmable device with NoManufacturerID.""" + pass + # --- DEVICE SUMMARY --- # TuyaSingleSwitchDimmer: 0x00, 0x04, 0x05, 0xEF00; 0x000A, 0x0019 From 78e90479c152c6d37f6148b69f92ad75c8681f7b Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Fri, 11 Aug 2023 17:02:00 +0200 Subject: [PATCH 10/79] fix errors --- zhaquirks/danfoss/thermostat.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index a3299b3c53..4082c2dd4c 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -65,12 +65,12 @@ class DanfossOperationModeEnum(t.bitmap8): 0x404F: ("preheat_status", t.Bool, "rp"), 0x4050: ("preheat_time", t.uint32_t, "rp"), # Danfoss deviated heavily from the spec with this one - 0x0025: ZCLAttributeDef("trv_operation_mode", type=t.bitmap8, access="rpw"), + 0x0025: ("programing_oper_mode", t.bitmap8, "rpw"), # We need a convenient way to access this, so we create our own attribute - 0x41FF: ZCLAttributeDef( + 0x41FF: ( "occupied_heating_setpoint_scheduled", - type=DanfossOperationModeEnum, - access="rpw", + DanfossOperationModeEnum, + "rpw", ), } # ZCL Attributes Supported: @@ -207,7 +207,7 @@ class DanfossThermostatCluster(CustomCluster, Thermostat): """Danfoss cluster for ZCL attributes and forwarding proprietary the attributes.""" server_commands = Thermostat.server_commands.copy() - server_commands = { + server_commands.update({ 0x40: ZCLCommandDef( "setpoint_command", # Types @@ -224,7 +224,7 @@ class DanfossThermostatCluster(CustomCluster, Thermostat): {"force": t.enum8, "timestamp": t.uint32_t}, is_manufacturer_specific=True, ), - } + }) attributes = Thermostat.attributes.copy() attributes.update(danfoss_thermostat_attr) From 28208e84133eca8bc45b91d12ac21f1b6a375bb4 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Fri, 11 Aug 2023 18:49:27 +0200 Subject: [PATCH 11/79] fix pre-commit --- zhaquirks/danfoss/thermostat.py | 40 +++++++++++++++++---------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 4082c2dd4c..600fee4e8d 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -16,7 +16,7 @@ from zigpy.zcl.clusters.homeautomation import Diagnostic from zigpy.zcl.clusters.hvac import Thermostat, UserInterface from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster -from zigpy.zcl.foundation import ZCLAttributeDef, ZCLCommandDef +from zigpy.zcl.foundation import ZCLCommandDef from zhaquirks.const import ( DEVICE_TYPE, @@ -207,24 +207,26 @@ class DanfossThermostatCluster(CustomCluster, Thermostat): """Danfoss cluster for ZCL attributes and forwarding proprietary the attributes.""" server_commands = Thermostat.server_commands.copy() - server_commands.update({ - 0x40: ZCLCommandDef( - "setpoint_command", - # Types - # 0: Schedule (relatively slow) - # 1: User Interaction (aggressive change) - # 2: Preheat (invisible to user) - {"type": t.enum8, "heating_setpoint": t.int16s}, - is_manufacturer_specific=True, - ), - # for synchronizing multiple TRVs preheating - 0x42: ZCLCommandDef( - "preheat_command", - # Force: 0 means force, other values for future needs - {"force": t.enum8, "timestamp": t.uint32_t}, - is_manufacturer_specific=True, - ), - }) + server_commands.update( + { + 0x40: ZCLCommandDef( + "setpoint_command", + # Types + # 0: Schedule (relatively slow) + # 1: User Interaction (aggressive change) + # 2: Preheat (invisible to user) + {"type": t.enum8, "heating_setpoint": t.int16s}, + is_manufacturer_specific=True, + ), + # for synchronizing multiple TRVs preheating + 0x42: ZCLCommandDef( + "preheat_command", + # Force: 0 means force, other values for future needs + {"force": t.enum8, "timestamp": t.uint32_t}, + is_manufacturer_specific=True, + ), + } + ) attributes = Thermostat.attributes.copy() attributes.update(danfoss_thermostat_attr) From dcee9bef41f26395659673687abc4786dc9d4f98 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sat, 12 Aug 2023 01:47:25 +0200 Subject: [PATCH 12/79] fix some accidental misuse of identifiers, remove magic strings and numbers, fix some type issues, add a few tests --- tests/test_danfoss.py | 64 ++++++++++++ zhaquirks/danfoss/thermostat.py | 179 ++++++++++++++++---------------- 2 files changed, 156 insertions(+), 87 deletions(-) create mode 100644 tests/test_danfoss.py diff --git a/tests/test_danfoss.py b/tests/test_danfoss.py new file mode 100644 index 0000000000..f08ab5bfd5 --- /dev/null +++ b/tests/test_danfoss.py @@ -0,0 +1,64 @@ +"""Tests the Danfoss quirk (all tests were written for the Popp eT093WRO)""" +from unittest import mock + +from zigpy.zcl import foundation +from zigpy.zcl.clusters.hvac import Thermostat + +import zhaquirks +from zhaquirks.danfoss.thermostat import DanfossTRVCluster + +zhaquirks.setup() + + +def test_popp_signature(assert_signature_matches_quirk): + signature = { + "node_descriptor": "NodeDescriptor(logical_type=, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=, mac_capability_flags=, manufacturer_code=4678, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=True, *is_full_function_device=False, *is_mains_powered=False, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False)", + # SizePrefixedSimpleDescriptor(endpoint=1, profile=260, device_type=769, device_version=1, input_clusters=[0, 1, 3, 10, 32, 513, 516, 2821], output_clusters=[0, 25]) + "endpoints": { + "1": { + "profile_id": 260, + "device_type": "0x0301", + "in_clusters": ["0x0000", "0x0001", "0x0003", "0x000a", "0x0020", "0x0201", "0x0204", "0x0b05"], + "out_clusters": ["0x0000", "0x0019"] + } + }, + "manufacturer": "D5X84YU", + "model": "eT093WRO", + "class": "danfoss.thermostat.DanfossThermostat" + } + + assert_signature_matches_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat, signature) + + +async def test_danfoss_trv_read_attributes(zigpy_device_from_quirk): + device = zigpy_device_from_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat) + + danfoss_thermostat_cluster = device.endpoints[1].in_clusters[Thermostat.cluster_id] + danfoss_trv_cluster = device.endpoints[1].in_clusters[DanfossTRVCluster.cluster_id] + + def mock_read(attributes, manufacturer=None): + records = [ + foundation.ReadAttributeRecord( + attr, foundation.Status.SUCCESS, foundation.TypeValue(None, 6) + ) + for attr in attributes + ] + return (records,) + + # data is served from danfoss_thermostat + patch_danfoss_thermostat_read = mock.patch.object( + danfoss_thermostat_cluster, "_read_attributes", mock.AsyncMock(side_effect=mock_read) + ) + + with patch_danfoss_thermostat_read: + # data should be received from danfoss_trv + success, fail = await danfoss_trv_cluster.read_attributes(["open_window_detection"]) + assert success + assert 6 in success.values() + assert not fail + + # this should return occupied_heating_setpoint_scheduled and occupied_heating_setpoint + success, fail = await danfoss_trv_cluster.read_attributes(["occupied_heating_setpoint_scheduled"]) + assert success + assert 6 in success.values() + assert not fail \ No newline at end of file diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 600fee4e8d..0824e831db 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -1,10 +1,13 @@ """Module to handle quirks of the Danfoss thermostat. manufacturer specific attributes to control displaying and specific configuration. + +manufacturer_code = 0x1246 """ import zigpy.profiles.zha as zha_p from zigpy.quirks import CustomCluster, CustomDevice import zigpy.types as t +from zigpy.types import uint16_t from zigpy.zcl.clusters.general import ( Basic, Identify, @@ -15,7 +18,6 @@ ) from zigpy.zcl.clusters.homeautomation import Diagnostic from zigpy.zcl.clusters.hvac import Thermostat, UserInterface -from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster from zigpy.zcl.foundation import ZCLCommandDef from zhaquirks.const import ( @@ -38,7 +40,19 @@ class DanfossOperationModeEnum(t.bitmap8): Schedule_Preheat = 0b00000011 -setpoint_change_scheduled = 0x41FF +OCCUPIED_HEATING_SETPOINT_TXT = "occupied_heating_setpoint" +OCCUPIED_HEATING_SETPOINT_SCHEDULED_TXT = "occupied_heating_setpoint_scheduled" +SYSTEM_MODE_TXT = "system_mode" + +OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ATTR = uint16_t(0x41FF) +OCCUPIED_HEATING_SETPOINT_THERM_ATTR = uint16_t(0x0012) +SETPOINT_CHANGE_THERM_ATTR = uint16_t(0x0012) +MIN_HEAT_SETPOINT_LIMIT_THERM_ATTR = uint16_t(0x0015) + +# This was set to: 0x01, but that is the schedule command not the setpoint command +AGGRESSIVE_SETPOINT_THERM_COMM = 0x40 + +SYSTEM_MODE_THERM_ATTR_OFF_VAL = 0x00 # 0x0201 danfoss_thermostat_attr = { @@ -65,11 +79,11 @@ class DanfossOperationModeEnum(t.bitmap8): 0x404F: ("preheat_status", t.Bool, "rp"), 0x4050: ("preheat_time", t.uint32_t, "rp"), # Danfoss deviated heavily from the spec with this one - 0x0025: ("programing_oper_mode", t.bitmap8, "rpw"), + 0x0025: ("programing_oper_mode", DanfossOperationModeEnum, "rpw"), # We need a convenient way to access this, so we create our own attribute - 0x41FF: ( - "occupied_heating_setpoint_scheduled", - DanfossOperationModeEnum, + OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ATTR: ( + OCCUPIED_HEATING_SETPOINT_SCHEDULED_TXT, + t.int16s, "rpw", ), } @@ -111,15 +125,33 @@ class DanfossOperationModeEnum(t.bitmap8): 0x4010: ("motor_step_counter", t.uint32_t, "rp"), } +danfoss_thermostat_comm = { + AGGRESSIVE_SETPOINT_THERM_COMM: ZCLCommandDef( + "setpoint_command", + # Types + # 0: Schedule (relatively slow) + # 1: User Interaction (aggressive change) + # 2: Preheat (invisible to user) + {"type": t.enum8, "heating_setpoint": t.int16s}, + is_manufacturer_specific=True, + ), + # for synchronizing multiple TRVs preheating + 0x42: ZCLCommandDef( + "preheat_command", + # Force: 0 means force, other values for future needs + {"force": t.enum8, "timestamp": t.uint32_t}, + is_manufacturer_specific=True, + ), +} -class DanfossTRVCluster(CustomCluster, ManufacturerSpecificCluster): + +class DanfossTRVCluster(CustomCluster): """Danfoss custom TRV cluster.""" cluster_id = 0xFC03 ep_attribute = "danfoss_trv_cluster" - attributes = ManufacturerSpecificCluster.attributes.copy() - attributes.update(danfoss_thermostat_attr) + attributes = danfoss_thermostat_attr async def write_attributes(self, attributes, manufacturer=None): """Write attributes to thermostat cluster.""" @@ -130,43 +162,46 @@ async def write_attributes(self, attributes, manufacturer=None): async def read_attributes_raw(self, attributes, manufacturer=None): """Operation Mode is a ZCL attribute and needs to be requested without manufacturer code.""" - # Setpoint_change_scheduled is not a real attribute - setpoint_change_scheduled = 0x41FF - if setpoint_change_scheduled in attributes: - attributes.remove(setpoint_change_scheduled) - - # Operation Mode is nonstandard and therefore manufacturer specific - operation_mode = 0x0025 - result_oper_mode = None - if operation_mode in attributes: - attributes.remove(operation_mode) - result_oper_mode = await self.endpoint.thermostat.read_attributes_raw( - [operation_mode], manufacturer=None - ) + # occupied_heating_setpoint_scheduled is not a real attribute, therefore: request occupied_heating_setpoint + occupied_heating_setpoint_in_attributes = OCCUPIED_HEATING_SETPOINT_THERM_ATTR in attributes + occupied_heating_setpoint_scheduled_in_attributes = OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ATTR in attributes + if occupied_heating_setpoint_scheduled_in_attributes: + attributes.remove(OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ATTR) + if not occupied_heating_setpoint_in_attributes: + attributes.append(OCCUPIED_HEATING_SETPOINT_THERM_ATTR) # Get normal result - result = None - if attributes: - result = await self.endpoint.thermostat.read_attributes_raw( - attributes, manufacturer=manufacturer - ) + result = await self.endpoint.thermostat.read_attributes_raw( + attributes, manufacturer=manufacturer + ) + + if occupied_heating_setpoint_scheduled_in_attributes: + # find record for occupied heating setpoint + occupied_heating_setpoint_index = None + for i in range(len(result[0])): + print(result[0][i].attrid) + if result[0][i].attrid == OCCUPIED_HEATING_SETPOINT_THERM_ATTR: + occupied_heating_setpoint_index = i - # Combine results - if result_oper_mode and result: - result[0].append(result_oper_mode[0][0]) - return result - else: - return result if result else result_oper_mode + if occupied_heating_setpoint_index is not None: + occupied_heating_setpoint_record = result[0][occupied_heating_setpoint_index] + occupied_heating_setpoint_record.attrid = OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ATTR + result[0].append(occupied_heating_setpoint_record) + # remove occupied_heating_setpoint if not requested + if not occupied_heating_setpoint_in_attributes: + del result[0][occupied_heating_setpoint_index] -class DanfossTRVInterfaceCluster(CustomCluster, ManufacturerSpecificCluster): + return result + + +class DanfossTRVInterfaceCluster(CustomCluster): """Danfoss custom interface cluster.""" cluster_id = 0xFC04 ep_attribute = "danfoss_trv_interface_cluster" - attributes = ManufacturerSpecificCluster.attributes.copy() - attributes.update(danfoss_interface_attr) + attributes = danfoss_interface_attr async def write_attributes(self, attributes, manufacturer=None): """Write attributes to thermostat user interface cluster.""" @@ -181,14 +216,13 @@ def read_attributes_raw(self, attributes, manufacturer=None): ) -class DanfossTRVDiagnosticCluster(CustomCluster, ManufacturerSpecificCluster): +class DanfossTRVDiagnosticCluster(CustomCluster): """Danfoss custom diagnostic cluster.""" cluster_id = 0xFC05 ep_attribute = "danfoss_trv_diagnostic_cluster" - attributes = ManufacturerSpecificCluster.attributes.copy() - attributes.update(danfoss_diagnostic_attr) + attributes = danfoss_diagnostic_attr async def write_attributes(self, attributes, manufacturer=None): """Write attributes to diagnostic cluster.""" @@ -204,49 +238,27 @@ def read_attributes_raw(self, attributes, manufacturer=None): class DanfossThermostatCluster(CustomCluster, Thermostat): - """Danfoss cluster for ZCL attributes and forwarding proprietary the attributes.""" + """Danfoss cluster for ZCL attributes and forwarding proprietary attributes.""" server_commands = Thermostat.server_commands.copy() - server_commands.update( - { - 0x40: ZCLCommandDef( - "setpoint_command", - # Types - # 0: Schedule (relatively slow) - # 1: User Interaction (aggressive change) - # 2: Preheat (invisible to user) - {"type": t.enum8, "heating_setpoint": t.int16s}, - is_manufacturer_specific=True, - ), - # for synchronizing multiple TRVs preheating - 0x42: ZCLCommandDef( - "preheat_command", - # Force: 0 means force, other values for future needs - {"force": t.enum8, "timestamp": t.uint32_t}, - is_manufacturer_specific=True, - ), - } - ) + server_commands.update(danfoss_thermostat_comm) attributes = Thermostat.attributes.copy() - attributes.update(danfoss_thermostat_attr) - attributes.update(zcl_attr) + attributes.update({**danfoss_thermostat_attr, **zcl_attr}) async def write_attributes(self, attributes, manufacturer=None): """Send SETPOINT_COMMAND after setpoint change.""" - off = 0x00 scheduled = False - if "system_mode" in attributes and attributes["system_mode"] == off: + if attributes.get(SYSTEM_MODE_TXT) == SYSTEM_MODE_THERM_ATTR_OFF_VAL: # Thermostatic Radiator Valves from Danfoss cannot be turned off to prevent damage during frost # just turn setpoint down to minimum temperature - min_heat_setpoint_limit = 0x0015 - attributes["occupied_heating_setpoint"] = self._attr_cache[ - min_heat_setpoint_limit + attributes[OCCUPIED_HEATING_SETPOINT_TXT] = self._attr_cache[ + MIN_HEAT_SETPOINT_LIMIT_THERM_ATTR ] - elif "occupied_heating_setpoint_scheduled" in attributes: - attributes["occupied_heating_setpoint"] = attributes.pop( - "occupied_heating_setpoint_scheduled" + elif OCCUPIED_HEATING_SETPOINT_SCHEDULED_TXT in attributes: + attributes[OCCUPIED_HEATING_SETPOINT_TXT] = attributes.pop( + OCCUPIED_HEATING_SETPOINT_SCHEDULED_TXT ) scheduled = True @@ -254,11 +266,10 @@ async def write_attributes(self, attributes, manufacturer=None): attributes, manufacturer=manufacturer ) - if "occupied_heating_setpoint" in attributes and not scheduled: - aggressive_setpoint = 0x01 + if OCCUPIED_HEATING_SETPOINT_TXT in attributes and not scheduled: await self.setpoint_command( - aggressive_setpoint, - attributes["occupied_heating_setpoint"], + AGGRESSIVE_SETPOINT_THERM_COMM, + attributes[OCCUPIED_HEATING_SETPOINT_TXT], manufacturer=manufacturer, ) @@ -268,10 +279,9 @@ def _update_attribute(self, attrid, value): """Update attributes of TRV cluster.""" super()._update_attribute(attrid, value) - setpoint_change = 0x0012 - if attrid == setpoint_change: + if attrid == SETPOINT_CHANGE_THERM_ATTR: self.endpoint.danfoss_trv_cluster._update_attribute( - setpoint_change_scheduled, value + OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ATTR, value ) if attrid in danfoss_thermostat_attr: @@ -279,11 +289,10 @@ def _update_attribute(self, attrid, value): class DanfossUserInterfaceCluster(CustomCluster, UserInterface): - """Danfoss cluster for ZCL attributes and forwarding proprietary the attributes.""" + """Danfoss cluster for ZCL attributes and forwarding proprietary attributes.""" attributes = UserInterface.attributes.copy() - attributes.update(danfoss_interface_attr) - attributes.update(zcl_attr) + attributes.update({**danfoss_interface_attr, **zcl_attr}) def _update_attribute(self, attrid, value): """Update attributes of TRV interface cluster.""" @@ -294,11 +303,10 @@ def _update_attribute(self, attrid, value): class DanfossDiagnosticCluster(CustomCluster, Diagnostic): - """Danfoss cluster for ZCL attributes and forwarding proprietary the attributes.""" + """Danfoss cluster for ZCL attributes and forwarding proprietary attributes.""" attributes = Diagnostic.attributes.copy() - attributes.update(danfoss_diagnostic_attr) - attributes.update(zcl_attr) + attributes.update({**danfoss_diagnostic_attr, **zcl_attr}) def _update_attribute(self, attrid, value): """Update attributes or TRV diagnostic cluster.""" @@ -309,10 +317,7 @@ def _update_attribute(self, attrid, value): class DanfossThermostat(CustomDevice): - """DanfossThermostat custom device. - - manufacturer_code = 0x1246 - """ + """DanfossThermostat custom device.""" signature = { # Date: Sat, 12 Aug 2023 03:35:56 +0200 Subject: [PATCH 13/79] write tests --- tests/test_danfoss.py | 118 +++++++++++++++++++++++++++++--- zhaquirks/danfoss/thermostat.py | 36 +++++++--- 2 files changed, 135 insertions(+), 19 deletions(-) diff --git a/tests/test_danfoss.py b/tests/test_danfoss.py index f08ab5bfd5..f78c052470 100644 --- a/tests/test_danfoss.py +++ b/tests/test_danfoss.py @@ -3,6 +3,7 @@ from zigpy.zcl import foundation from zigpy.zcl.clusters.hvac import Thermostat +from zigpy.zcl.foundation import WriteAttributesStatusRecord import zhaquirks from zhaquirks.danfoss.thermostat import DanfossTRVCluster @@ -18,16 +19,27 @@ def test_popp_signature(assert_signature_matches_quirk): "1": { "profile_id": 260, "device_type": "0x0301", - "in_clusters": ["0x0000", "0x0001", "0x0003", "0x000a", "0x0020", "0x0201", "0x0204", "0x0b05"], - "out_clusters": ["0x0000", "0x0019"] + "in_clusters": [ + "0x0000", + "0x0001", + "0x0003", + "0x000a", + "0x0020", + "0x0201", + "0x0204", + "0x0b05", + ], + "out_clusters": ["0x0000", "0x0019"], } }, "manufacturer": "D5X84YU", "model": "eT093WRO", - "class": "danfoss.thermostat.DanfossThermostat" + "class": "danfoss.thermostat.DanfossThermostat", } - assert_signature_matches_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat, signature) + assert_signature_matches_quirk( + zhaquirks.danfoss.thermostat.DanfossThermostat, signature + ) async def test_danfoss_trv_read_attributes(zigpy_device_from_quirk): @@ -47,18 +59,108 @@ def mock_read(attributes, manufacturer=None): # data is served from danfoss_thermostat patch_danfoss_thermostat_read = mock.patch.object( - danfoss_thermostat_cluster, "_read_attributes", mock.AsyncMock(side_effect=mock_read) + danfoss_thermostat_cluster, + "_read_attributes", + mock.AsyncMock(side_effect=mock_read), ) with patch_danfoss_thermostat_read: # data should be received from danfoss_trv - success, fail = await danfoss_trv_cluster.read_attributes(["open_window_detection"]) + success, fail = await danfoss_trv_cluster.read_attributes( + ["open_window_detection"] + ) assert success assert 6 in success.values() assert not fail # this should return occupied_heating_setpoint_scheduled and occupied_heating_setpoint - success, fail = await danfoss_trv_cluster.read_attributes(["occupied_heating_setpoint_scheduled"]) + success, fail = await danfoss_trv_cluster.read_attributes( + ["occupied_heating_setpoint_scheduled"] + ) assert success assert 6 in success.values() - assert not fail \ No newline at end of file + assert not fail + + +async def test_danfoss_thermostat_write_attributes(zigpy_device_from_quirk): + device = zigpy_device_from_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat) + + danfoss_thermostat_cluster = device.endpoints[1].in_clusters[Thermostat.cluster_id] + danfoss_trv_cluster = device.endpoints[1].in_clusters[DanfossTRVCluster.cluster_id] + + def mock_write(attributes, manufacturer=None): + records = [ + WriteAttributesStatusRecord(foundation.Status.SUCCESS) + for attr in attributes + ] + return [records, []] + + setting = -100 + operation = -0x01 + + def mock_setpoint(oper, sett, manufacturer=None): + nonlocal operation, setting + operation = oper + setting = sett + + # data is written to trv + patch_danfoss_trv_write = mock.patch.object( + danfoss_thermostat_cluster, + "_write_attributes", + mock.AsyncMock(side_effect=mock_write), + ) + patch_danfoss_setpoint = mock.patch.object( + danfoss_thermostat_cluster, + "setpoint_command", + mock.AsyncMock(side_effect=mock_setpoint), + ) + + with patch_danfoss_trv_write: + # data should be written to trv, but reach thermostat + success, fail = await danfoss_trv_cluster.write_attributes( + {"external_open_window_detected": False} + ) + assert success + assert not fail + assert not danfoss_thermostat_cluster._attr_cache[0x4003] + + with patch_danfoss_setpoint: + # data should be received from danfoss_trv + success, fail = await danfoss_thermostat_cluster.write_attributes( + {"occupied_heating_setpoint": 6} + ) + assert success + assert not fail + assert danfoss_thermostat_cluster._attr_cache[0x0012] == 6 + assert operation == 0x01 + assert setting == 6 + + danfoss_thermostat_cluster._attr_cache[ + 0x0015 + ] = 5 # min_limit is present normally + + success, fail = await danfoss_trv_cluster.write_attributes( + {"system_mode": 0x00} + ) + assert success + assert not fail + assert danfoss_thermostat_cluster._attr_cache[0x001C] == 0x00 + + # setpoint to min_limit, when system_mode to off + assert danfoss_thermostat_cluster._attr_cache[0x0012] == 5 + + assert operation == 0x01 + assert setting == 5 + + # scheduled should not send setpoint_command + operation = -0x01 + setting = -100 + success, fail = await danfoss_trv_cluster.write_attributes( + {"occupied_heating_setpoint_scheduled": 6} + ) + assert success + assert not fail + assert danfoss_trv_cluster._attr_cache[0x41FF] == 6 + + assert operation == -0x01 + assert setting == -100 diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 0824e831db..27aeb4c604 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -49,8 +49,7 @@ class DanfossOperationModeEnum(t.bitmap8): SETPOINT_CHANGE_THERM_ATTR = uint16_t(0x0012) MIN_HEAT_SETPOINT_LIMIT_THERM_ATTR = uint16_t(0x0015) -# This was set to: 0x01, but that is the schedule command not the setpoint command -AGGRESSIVE_SETPOINT_THERM_COMM = 0x40 +SETPOINT_COMM_AGGRESSIVE_VAL = 0x01 SYSTEM_MODE_THERM_ATTR_OFF_VAL = 0x00 @@ -126,7 +125,7 @@ class DanfossOperationModeEnum(t.bitmap8): } danfoss_thermostat_comm = { - AGGRESSIVE_SETPOINT_THERM_COMM: ZCLCommandDef( + 0x40: ZCLCommandDef( "setpoint_command", # Types # 0: Schedule (relatively slow) @@ -162,30 +161,45 @@ async def write_attributes(self, attributes, manufacturer=None): async def read_attributes_raw(self, attributes, manufacturer=None): """Operation Mode is a ZCL attribute and needs to be requested without manufacturer code.""" - # occupied_heating_setpoint_scheduled is not a real attribute, therefore: request occupied_heating_setpoint - occupied_heating_setpoint_in_attributes = OCCUPIED_HEATING_SETPOINT_THERM_ATTR in attributes - occupied_heating_setpoint_scheduled_in_attributes = OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ATTR in attributes + # store presence of requested attributes + occupied_heating_setpoint_in_attributes = ( + OCCUPIED_HEATING_SETPOINT_THERM_ATTR in attributes + ) + occupied_heating_setpoint_scheduled_in_attributes = ( + OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ATTR in attributes + ) + + # if occupied_heating_setpoint_scheduled is requested, + # remove it from attributes and request occupied_heating_setpoint if occupied_heating_setpoint_scheduled_in_attributes: attributes.remove(OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ATTR) if not occupied_heating_setpoint_in_attributes: attributes.append(OCCUPIED_HEATING_SETPOINT_THERM_ATTR) - # Get normal result + # Get result result = await self.endpoint.thermostat.read_attributes_raw( attributes, manufacturer=manufacturer ) + # if occupied_heating_setpoint_scheduled is requested, use occupied_heating_setpoint to deliver that if occupied_heating_setpoint_scheduled_in_attributes: # find record for occupied heating setpoint occupied_heating_setpoint_index = None for i in range(len(result[0])): - print(result[0][i].attrid) if result[0][i].attrid == OCCUPIED_HEATING_SETPOINT_THERM_ATTR: occupied_heating_setpoint_index = i + break + # if occupied_heating_setpoint is returned, + # copy occupied_heating_setpoint and change into occupied_heating_setpoint_scheduled and + # remove occupied_heating_setpoint from result if not requested if occupied_heating_setpoint_index is not None: - occupied_heating_setpoint_record = result[0][occupied_heating_setpoint_index] - occupied_heating_setpoint_record.attrid = OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ATTR + occupied_heating_setpoint_record = result[0][ + occupied_heating_setpoint_index + ] + occupied_heating_setpoint_record.attrid = ( + OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ATTR + ) result[0].append(occupied_heating_setpoint_record) # remove occupied_heating_setpoint if not requested @@ -268,7 +282,7 @@ async def write_attributes(self, attributes, manufacturer=None): if OCCUPIED_HEATING_SETPOINT_TXT in attributes and not scheduled: await self.setpoint_command( - AGGRESSIVE_SETPOINT_THERM_COMM, + SETPOINT_COMM_AGGRESSIVE_VAL, attributes[OCCUPIED_HEATING_SETPOINT_TXT], manufacturer=manufacturer, ) From 7c3017ab5e87c89a3a7fe6d980cadf78ac380768 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sat, 2 Sep 2023 19:33:13 +0200 Subject: [PATCH 14/79] refactoring --- zhaquirks/danfoss/thermostat.py | 198 +++++++++++++++++++------------- 1 file changed, 119 insertions(+), 79 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 27aeb4c604..e98119b27c 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -4,6 +4,8 @@ manufacturer_code = 0x1246 """ +from typing import Callable, Tuple, Union + import zigpy.profiles.zha as zha_p from zigpy.quirks import CustomCluster, CustomDevice import zigpy.types as t @@ -40,18 +42,18 @@ class DanfossOperationModeEnum(t.bitmap8): Schedule_Preheat = 0b00000011 -OCCUPIED_HEATING_SETPOINT_TXT = "occupied_heating_setpoint" -OCCUPIED_HEATING_SETPOINT_SCHEDULED_TXT = "occupied_heating_setpoint_scheduled" -SYSTEM_MODE_TXT = "system_mode" +OCCUPIED_HEATING_SETPOINT_NAME = "occupied_heating_setpoint" +OCCUPIED_HEATING_SETPOINT_SCHEDULED_NAME = "occupied_heating_setpoint_scheduled" +SYSTEM_MODE_NAME = "system_mode" -OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ATTR = uint16_t(0x41FF) -OCCUPIED_HEATING_SETPOINT_THERM_ATTR = uint16_t(0x0012) -SETPOINT_CHANGE_THERM_ATTR = uint16_t(0x0012) -MIN_HEAT_SETPOINT_LIMIT_THERM_ATTR = uint16_t(0x0015) +OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ID = uint16_t(0x41FF) +OCCUPIED_HEATING_SETPOINT_THERM_ID = uint16_t(0x0012) +SETPOINT_CHANGE_THERM_ID = uint16_t(0x0012) +MIN_HEAT_SETPOINT_LIMIT_THERM_ID = uint16_t(0x0015) -SETPOINT_COMM_AGGRESSIVE_VAL = 0x01 +SETPOINT_COMMAND_AGGRESSIVE_VAL = 0x01 -SYSTEM_MODE_THERM_ATTR_OFF_VAL = 0x00 +SYSTEM_MODE_THERM_OFF_VAL = 0x00 # 0x0201 danfoss_thermostat_attr = { @@ -80,8 +82,8 @@ class DanfossOperationModeEnum(t.bitmap8): # Danfoss deviated heavily from the spec with this one 0x0025: ("programing_oper_mode", DanfossOperationModeEnum, "rpw"), # We need a convenient way to access this, so we create our own attribute - OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ATTR: ( - OCCUPIED_HEATING_SETPOINT_SCHEDULED_TXT, + OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ID: ( + OCCUPIED_HEATING_SETPOINT_SCHEDULED_NAME, t.int16s, "rpw", ), @@ -144,6 +146,54 @@ class DanfossOperationModeEnum(t.bitmap8): } +def get_result_index(result: Tuple[list, list], attr_id: uint16_t) -> Union[int, None]: + index = None + for i in range(len(result[0])): + if result[0][i].attrid == attr_id: + index = i + break + return index + + +async def read_fakeattr(read_func: Callable, attributes, manufacturer, + attr_fake_id: uint16_t, attr_source_id: uint16_t): + """ + First remove fake from attributes + Then add source to attributes + Request result + Duplicate source in results and rename to fake + Remove source from results + """ + # store presence of requested attributes + source_requested = attr_source_id in attributes + fake_requested = attr_fake_id in attributes + + if fake_requested: + # fake should not be present in attributes + attributes.remove(attr_fake_id) + if not source_requested: + # if fake is requested, source should be requested + attributes.append(attr_source_id) + + # Get result + result = await read_func(attributes, manufacturer=manufacturer) + + # if fake is requested, use source to return that + if fake_requested: + index = get_result_index(result, attr_source_id) + + # if source is returned, copy source and change into fake and remove source from result if not requested + if index is not None: + attr_fake = result[0][index] + attr_fake.attrid = attr_fake_id + + result[0].append(attr_fake) + + # remove source if not requested + if not source_requested: + result[0].pop(index) + + class DanfossTRVCluster(CustomCluster): """Danfoss custom TRV cluster.""" @@ -153,60 +203,17 @@ class DanfossTRVCluster(CustomCluster): attributes = danfoss_thermostat_attr async def write_attributes(self, attributes, manufacturer=None): - """Write attributes to thermostat cluster.""" + """Write attributes to Thermostat cluster.""" return await self.endpoint.thermostat.write_attributes( attributes, manufacturer=manufacturer ) async def read_attributes_raw(self, attributes, manufacturer=None): - """Operation Mode is a ZCL attribute and needs to be requested without manufacturer code.""" - - # store presence of requested attributes - occupied_heating_setpoint_in_attributes = ( - OCCUPIED_HEATING_SETPOINT_THERM_ATTR in attributes - ) - occupied_heating_setpoint_scheduled_in_attributes = ( - OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ATTR in attributes - ) - - # if occupied_heating_setpoint_scheduled is requested, - # remove it from attributes and request occupied_heating_setpoint - if occupied_heating_setpoint_scheduled_in_attributes: - attributes.remove(OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ATTR) - if not occupied_heating_setpoint_in_attributes: - attributes.append(OCCUPIED_HEATING_SETPOINT_THERM_ATTR) + """Read attributes from Thermostat cluster""" - # Get result - result = await self.endpoint.thermostat.read_attributes_raw( - attributes, manufacturer=manufacturer - ) - - # if occupied_heating_setpoint_scheduled is requested, use occupied_heating_setpoint to deliver that - if occupied_heating_setpoint_scheduled_in_attributes: - # find record for occupied heating setpoint - occupied_heating_setpoint_index = None - for i in range(len(result[0])): - if result[0][i].attrid == OCCUPIED_HEATING_SETPOINT_THERM_ATTR: - occupied_heating_setpoint_index = i - break - - # if occupied_heating_setpoint is returned, - # copy occupied_heating_setpoint and change into occupied_heating_setpoint_scheduled and - # remove occupied_heating_setpoint from result if not requested - if occupied_heating_setpoint_index is not None: - occupied_heating_setpoint_record = result[0][ - occupied_heating_setpoint_index - ] - occupied_heating_setpoint_record.attrid = ( - OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ATTR - ) - result[0].append(occupied_heating_setpoint_record) - - # remove occupied_heating_setpoint if not requested - if not occupied_heating_setpoint_in_attributes: - del result[0][occupied_heating_setpoint_index] - - return result + return await self.endpoint.thermostat.read_attributes_raw( + attributes, + manufacturer=manufacturer) class DanfossTRVInterfaceCluster(CustomCluster): @@ -218,7 +225,7 @@ class DanfossTRVInterfaceCluster(CustomCluster): attributes = danfoss_interface_attr async def write_attributes(self, attributes, manufacturer=None): - """Write attributes to thermostat user interface cluster.""" + """Write attributes to User Interface cluster.""" return await self.endpoint.thermostat_ui.write_attributes( attributes, manufacturer=manufacturer ) @@ -239,7 +246,7 @@ class DanfossTRVDiagnosticCluster(CustomCluster): attributes = danfoss_diagnostic_attr async def write_attributes(self, attributes, manufacturer=None): - """Write attributes to diagnostic cluster.""" + """Write attributes to Diagnostic cluster.""" return await self.endpoint.diagnostic.write_attributes( attributes, manufacturer=manufacturer ) @@ -261,46 +268,79 @@ class DanfossThermostatCluster(CustomCluster, Thermostat): attributes.update({**danfoss_thermostat_attr, **zcl_attr}) async def write_attributes(self, attributes, manufacturer=None): - """Send SETPOINT_COMMAND after setpoint change.""" + """ + There are 2 types of setpoint changes: + Fast and Slow + Fast is used for immediate changes; this is done using a command (setpoint_command) + Slow is used for scheduled changes; this is done using an attribute (occupied_heating_setpoint) + + system mode=off is not implemented on Danfoss; this is emulated by setting setpoint to the minimum setpoint + + In case of a change on occupied_heating_setpoint or system mode=off, a fast setpoint change is done + In case of a schedules heating setpoint change, a slow setpoint change is done + """ + + fast_setpoint_change = None + + if OCCUPIED_HEATING_SETPOINT_NAME in attributes: + # On Danfoss an immediate setpoint change is done through a command + # store for later in fast_setpoint_change and remove from attributes + fast_setpoint_change = attributes[ + OCCUPIED_HEATING_SETPOINT_NAME + ] - scheduled = False - if attributes.get(SYSTEM_MODE_TXT) == SYSTEM_MODE_THERM_ATTR_OFF_VAL: + # if: system_mode = off + if attributes.get(SYSTEM_MODE_NAME) == SYSTEM_MODE_THERM_OFF_VAL: # Thermostatic Radiator Valves from Danfoss cannot be turned off to prevent damage during frost - # just turn setpoint down to minimum temperature - attributes[OCCUPIED_HEATING_SETPOINT_TXT] = self._attr_cache[ - MIN_HEAT_SETPOINT_LIMIT_THERM_ATTR + # just turn setpoint down to minimum temperature using fast_setpoint_change + fast_setpoint_change = self._attr_cache[ + MIN_HEAT_SETPOINT_LIMIT_THERM_ID ] - elif OCCUPIED_HEATING_SETPOINT_SCHEDULED_TXT in attributes: - attributes[OCCUPIED_HEATING_SETPOINT_TXT] = attributes.pop( - OCCUPIED_HEATING_SETPOINT_SCHEDULED_TXT + + if OCCUPIED_HEATING_SETPOINT_SCHEDULED_NAME in attributes: + # On Danfoss a normal setpoint change means a scheduled setpoint change (slow) + # remove setpoint change scheduled, because it is not a real attribute + attributes[OCCUPIED_HEATING_SETPOINT_NAME] = attributes.pop( + OCCUPIED_HEATING_SETPOINT_SCHEDULED_NAME ) - scheduled = True + # attributes cannot be empty, because write_res cannot be empty, but it can contain unrequested items write_res = await super().write_attributes( attributes, manufacturer=manufacturer ) - if OCCUPIED_HEATING_SETPOINT_TXT in attributes and not scheduled: + if fast_setpoint_change is not None: + # On Danfoss a fast setpoint change is done through a command await self.setpoint_command( - SETPOINT_COMM_AGGRESSIVE_VAL, - attributes[OCCUPIED_HEATING_SETPOINT_TXT], + SETPOINT_COMMAND_AGGRESSIVE_VAL, + fast_setpoint_change, manufacturer=manufacturer, ) return write_res + async def read_attributes_raw(self, attributes, manufacturer=None): + """ Handle Occupied heating Setpoint as fake attribute """ + return await read_fakeattr( + super().read_attributes_raw, + attributes, + manufacturer, + OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ID, + OCCUPIED_HEATING_SETPOINT_THERM_ID) + def _update_attribute(self, attrid, value): """Update attributes of TRV cluster.""" super()._update_attribute(attrid, value) - if attrid == SETPOINT_CHANGE_THERM_ATTR: - self.endpoint.danfoss_trv_cluster._update_attribute( - OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ATTR, value - ) - if attrid in danfoss_thermostat_attr: self.endpoint.danfoss_trv_cluster.update_attribute(attrid, value) + # also update scheduled heating setpoint + if attrid == SETPOINT_CHANGE_THERM_ID: + self._update_attribute( + OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ID, value + ) + class DanfossUserInterfaceCluster(CustomCluster, UserInterface): """Danfoss cluster for ZCL attributes and forwarding proprietary attributes.""" From bcf41d70945bd50687fb0301457b9178695c0338 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sat, 2 Sep 2023 19:44:48 +0200 Subject: [PATCH 15/79] remove scheduled setpoint --- zhaquirks/danfoss/thermostat.py | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index e98119b27c..429a4a820e 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -43,10 +43,8 @@ class DanfossOperationModeEnum(t.bitmap8): OCCUPIED_HEATING_SETPOINT_NAME = "occupied_heating_setpoint" -OCCUPIED_HEATING_SETPOINT_SCHEDULED_NAME = "occupied_heating_setpoint_scheduled" SYSTEM_MODE_NAME = "system_mode" -OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ID = uint16_t(0x41FF) OCCUPIED_HEATING_SETPOINT_THERM_ID = uint16_t(0x0012) SETPOINT_CHANGE_THERM_ID = uint16_t(0x0012) MIN_HEAT_SETPOINT_LIMIT_THERM_ID = uint16_t(0x0015) @@ -81,12 +79,6 @@ class DanfossOperationModeEnum(t.bitmap8): 0x4050: ("preheat_time", t.uint32_t, "rp"), # Danfoss deviated heavily from the spec with this one 0x0025: ("programing_oper_mode", DanfossOperationModeEnum, "rpw"), - # We need a convenient way to access this, so we create our own attribute - OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ID: ( - OCCUPIED_HEATING_SETPOINT_SCHEDULED_NAME, - t.int16s, - "rpw", - ), } # ZCL Attributes Supported: # pi_heating_demand (0x0008), @@ -277,7 +269,6 @@ async def write_attributes(self, attributes, manufacturer=None): system mode=off is not implemented on Danfoss; this is emulated by setting setpoint to the minimum setpoint In case of a change on occupied_heating_setpoint or system mode=off, a fast setpoint change is done - In case of a schedules heating setpoint change, a slow setpoint change is done """ fast_setpoint_change = None @@ -297,13 +288,6 @@ async def write_attributes(self, attributes, manufacturer=None): MIN_HEAT_SETPOINT_LIMIT_THERM_ID ] - if OCCUPIED_HEATING_SETPOINT_SCHEDULED_NAME in attributes: - # On Danfoss a normal setpoint change means a scheduled setpoint change (slow) - # remove setpoint change scheduled, because it is not a real attribute - attributes[OCCUPIED_HEATING_SETPOINT_NAME] = attributes.pop( - OCCUPIED_HEATING_SETPOINT_SCHEDULED_NAME - ) - # attributes cannot be empty, because write_res cannot be empty, but it can contain unrequested items write_res = await super().write_attributes( attributes, manufacturer=manufacturer @@ -319,15 +303,6 @@ async def write_attributes(self, attributes, manufacturer=None): return write_res - async def read_attributes_raw(self, attributes, manufacturer=None): - """ Handle Occupied heating Setpoint as fake attribute """ - return await read_fakeattr( - super().read_attributes_raw, - attributes, - manufacturer, - OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ID, - OCCUPIED_HEATING_SETPOINT_THERM_ID) - def _update_attribute(self, attrid, value): """Update attributes of TRV cluster.""" super()._update_attribute(attrid, value) @@ -335,12 +310,6 @@ def _update_attribute(self, attrid, value): if attrid in danfoss_thermostat_attr: self.endpoint.danfoss_trv_cluster.update_attribute(attrid, value) - # also update scheduled heating setpoint - if attrid == SETPOINT_CHANGE_THERM_ID: - self._update_attribute( - OCCUPIED_HEATING_SETPOINT_SCHEDULED_THERM_ID, value - ) - class DanfossUserInterfaceCluster(CustomCluster, UserInterface): """Danfoss cluster for ZCL attributes and forwarding proprietary attributes.""" From f70c7de5b02d1f6bbaa40727ad3f392d2db4bb5c Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sat, 2 Sep 2023 20:03:00 +0200 Subject: [PATCH 16/79] comment for programming operation mode --- zhaquirks/danfoss/thermostat.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 429a4a820e..8a0bb98e7f 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -34,7 +34,9 @@ class DanfossOperationModeEnum(t.bitmap8): - """Nonstandard and unnecessarily complicated implementation of Programming Operation Mode from Danfoss.""" + """Nonstandard implementation of Programming Operation Mode from Danfoss. + The official specification still works: 0x0 or 0x1, but Danfoss added a preheat bit + """ Manual = 0b00000000 Schedule = 0b00000001 @@ -77,8 +79,7 @@ class DanfossOperationModeEnum(t.bitmap8): 0x404E: ("adaptation_run_settings", t.bitmap8, "rw"), 0x404F: ("preheat_status", t.Bool, "rp"), 0x4050: ("preheat_time", t.uint32_t, "rp"), - # Danfoss deviated heavily from the spec with this one - 0x0025: ("programing_oper_mode", DanfossOperationModeEnum, "rpw"), + 0x0025: ("programing_oper_mode", DanfossOperationModeEnum, "rpw"), # Danfoss deviated from the spec } # ZCL Attributes Supported: # pi_heating_demand (0x0008), From a1dc598fe0b1ce8393aa352248d3d254a420c1db Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sat, 2 Sep 2023 20:03:40 +0200 Subject: [PATCH 17/79] remove read_fakeattr and get_result_index --- zhaquirks/danfoss/thermostat.py | 48 --------------------------------- 1 file changed, 48 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 8a0bb98e7f..cb1486893f 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -139,54 +139,6 @@ class DanfossOperationModeEnum(t.bitmap8): } -def get_result_index(result: Tuple[list, list], attr_id: uint16_t) -> Union[int, None]: - index = None - for i in range(len(result[0])): - if result[0][i].attrid == attr_id: - index = i - break - return index - - -async def read_fakeattr(read_func: Callable, attributes, manufacturer, - attr_fake_id: uint16_t, attr_source_id: uint16_t): - """ - First remove fake from attributes - Then add source to attributes - Request result - Duplicate source in results and rename to fake - Remove source from results - """ - # store presence of requested attributes - source_requested = attr_source_id in attributes - fake_requested = attr_fake_id in attributes - - if fake_requested: - # fake should not be present in attributes - attributes.remove(attr_fake_id) - if not source_requested: - # if fake is requested, source should be requested - attributes.append(attr_source_id) - - # Get result - result = await read_func(attributes, manufacturer=manufacturer) - - # if fake is requested, use source to return that - if fake_requested: - index = get_result_index(result, attr_source_id) - - # if source is returned, copy source and change into fake and remove source from result if not requested - if index is not None: - attr_fake = result[0][index] - attr_fake.attrid = attr_fake_id - - result[0].append(attr_fake) - - # remove source if not requested - if not source_requested: - result[0].pop(index) - - class DanfossTRVCluster(CustomCluster): """Danfoss custom TRV cluster.""" From 7faa8571ca18a837be0fda27907ef099324e69f2 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sat, 2 Sep 2023 20:18:37 +0200 Subject: [PATCH 18/79] fix tests --- tests/test_danfoss.py | 21 --------------------- zhaquirks/danfoss/thermostat.py | 23 +++++++++++------------ 2 files changed, 11 insertions(+), 33 deletions(-) diff --git a/tests/test_danfoss.py b/tests/test_danfoss.py index f78c052470..85b9267377 100644 --- a/tests/test_danfoss.py +++ b/tests/test_danfoss.py @@ -73,14 +73,6 @@ def mock_read(attributes, manufacturer=None): assert 6 in success.values() assert not fail - # this should return occupied_heating_setpoint_scheduled and occupied_heating_setpoint - success, fail = await danfoss_trv_cluster.read_attributes( - ["occupied_heating_setpoint_scheduled"] - ) - assert success - assert 6 in success.values() - assert not fail - async def test_danfoss_thermostat_write_attributes(zigpy_device_from_quirk): device = zigpy_device_from_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat) @@ -151,16 +143,3 @@ def mock_setpoint(oper, sett, manufacturer=None): assert operation == 0x01 assert setting == 5 - - # scheduled should not send setpoint_command - operation = -0x01 - setting = -100 - success, fail = await danfoss_trv_cluster.write_attributes( - {"occupied_heating_setpoint_scheduled": 6} - ) - assert success - assert not fail - assert danfoss_trv_cluster._attr_cache[0x41FF] == 6 - - assert operation == -0x01 - assert setting == -100 diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index cb1486893f..fb67ee1ad3 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -4,7 +4,6 @@ manufacturer_code = 0x1246 """ -from typing import Callable, Tuple, Union import zigpy.profiles.zha as zha_p from zigpy.quirks import CustomCluster, CustomDevice @@ -79,7 +78,11 @@ class DanfossOperationModeEnum(t.bitmap8): 0x404E: ("adaptation_run_settings", t.bitmap8, "rw"), 0x404F: ("preheat_status", t.Bool, "rp"), 0x4050: ("preheat_time", t.uint32_t, "rp"), - 0x0025: ("programing_oper_mode", DanfossOperationModeEnum, "rpw"), # Danfoss deviated from the spec + 0x0025: ( + "programing_oper_mode", + DanfossOperationModeEnum, + "rpw", + ), # Danfoss deviated from the spec } # ZCL Attributes Supported: # pi_heating_demand (0x0008), @@ -157,8 +160,8 @@ async def read_attributes_raw(self, attributes, manufacturer=None): """Read attributes from Thermostat cluster""" return await self.endpoint.thermostat.read_attributes_raw( - attributes, - manufacturer=manufacturer) + attributes, manufacturer=manufacturer + ) class DanfossTRVInterfaceCluster(CustomCluster): @@ -213,8 +216,7 @@ class DanfossThermostatCluster(CustomCluster, Thermostat): attributes.update({**danfoss_thermostat_attr, **zcl_attr}) async def write_attributes(self, attributes, manufacturer=None): - """ - There are 2 types of setpoint changes: + """There are 2 types of setpoint changes: Fast and Slow Fast is used for immediate changes; this is done using a command (setpoint_command) Slow is used for scheduled changes; this is done using an attribute (occupied_heating_setpoint) @@ -229,17 +231,14 @@ async def write_attributes(self, attributes, manufacturer=None): if OCCUPIED_HEATING_SETPOINT_NAME in attributes: # On Danfoss an immediate setpoint change is done through a command # store for later in fast_setpoint_change and remove from attributes - fast_setpoint_change = attributes[ - OCCUPIED_HEATING_SETPOINT_NAME - ] + fast_setpoint_change = attributes[OCCUPIED_HEATING_SETPOINT_NAME] # if: system_mode = off if attributes.get(SYSTEM_MODE_NAME) == SYSTEM_MODE_THERM_OFF_VAL: # Thermostatic Radiator Valves from Danfoss cannot be turned off to prevent damage during frost # just turn setpoint down to minimum temperature using fast_setpoint_change - fast_setpoint_change = self._attr_cache[ - MIN_HEAT_SETPOINT_LIMIT_THERM_ID - ] + fast_setpoint_change = self._attr_cache[MIN_HEAT_SETPOINT_LIMIT_THERM_ID] + attributes[OCCUPIED_HEATING_SETPOINT_NAME] = fast_setpoint_change # attributes cannot be empty, because write_res cannot be empty, but it can contain unrequested items write_res = await super().write_attributes( From 160592a2ad5a4f2aa6236864bfe2b73d42262468 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sat, 2 Sep 2023 20:25:05 +0200 Subject: [PATCH 19/79] comment fix --- zhaquirks/danfoss/thermostat.py | 1 - 1 file changed, 1 deletion(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index fb67ee1ad3..cf1b925069 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -222,7 +222,6 @@ async def write_attributes(self, attributes, manufacturer=None): Slow is used for scheduled changes; this is done using an attribute (occupied_heating_setpoint) system mode=off is not implemented on Danfoss; this is emulated by setting setpoint to the minimum setpoint - In case of a change on occupied_heating_setpoint or system mode=off, a fast setpoint change is done """ From b2c73b2a74a8f1d8c9b13692ef087a5e8140106e Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sat, 2 Sep 2023 20:49:09 +0200 Subject: [PATCH 20/79] increase coverage --- tests/test_danfoss.py | 71 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/tests/test_danfoss.py b/tests/test_danfoss.py index 85b9267377..13f40f40f6 100644 --- a/tests/test_danfoss.py +++ b/tests/test_danfoss.py @@ -2,11 +2,16 @@ from unittest import mock from zigpy.zcl import foundation -from zigpy.zcl.clusters.hvac import Thermostat +from zigpy.zcl.clusters.homeautomation import Diagnostic +from zigpy.zcl.clusters.hvac import Thermostat, UserInterface from zigpy.zcl.foundation import WriteAttributesStatusRecord import zhaquirks -from zhaquirks.danfoss.thermostat import DanfossTRVCluster +from zhaquirks.danfoss.thermostat import ( + DanfossTRVCluster, + DanfossTRVDiagnosticCluster, + DanfossTRVInterfaceCluster, +) zhaquirks.setup() @@ -143,3 +148,65 @@ def mock_setpoint(oper, sett, manufacturer=None): assert operation == 0x01 assert setting == 5 + + +async def test_danfoss_interface_update_attribute(zigpy_device_from_quirk): + device = zigpy_device_from_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat) + + danfoss_user_interface_cluster = device.endpoints[1].in_clusters[ + UserInterface.cluster_id + ] + danfoss_trv_interface_cluster = device.endpoints[1].in_clusters[ + DanfossTRVInterfaceCluster.cluster_id + ] + + attribute = -100 + value = -0x01 + + def mock_update(attr, val): + nonlocal attribute, value + + attribute = attr + value = val + + patch_danfoss_trv_interface_update_attribute = mock.patch.object( + danfoss_trv_interface_cluster, + "_update_attribute", + mock.Mock(side_effect=mock_update), + ) + + with patch_danfoss_trv_interface_update_attribute: + danfoss_user_interface_cluster.update_attribute(0x4000, True) + + assert attribute == 0x4000 + assert value + + +async def test_danfoss_diagnostic_update_attribute(zigpy_device_from_quirk): + device = zigpy_device_from_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat) + + danfoss_diagnostic_cluster = device.endpoints[1].in_clusters[Diagnostic.cluster_id] + danfoss_trv_diagnostic_cluster = device.endpoints[1].in_clusters[ + DanfossTRVDiagnosticCluster.cluster_id + ] + + attribute = -100 + value = -0x01 + + def mock_update(attr, val): + nonlocal attribute, value + + attribute = attr + value = val + + patch_danfoss_trv_diagnostic_update_attribute = mock.patch.object( + danfoss_trv_diagnostic_cluster, + "_update_attribute", + mock.Mock(side_effect=mock_update), + ) + + with patch_danfoss_trv_diagnostic_update_attribute: + danfoss_diagnostic_cluster.update_attribute(0x4010, 4546) + + assert attribute == 0x4010 + assert value == 4546 From a111a63cb8d366e435616594e0552b09f6a8a3a9 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Wed, 13 Sep 2023 12:52:31 +0200 Subject: [PATCH 21/79] change cluster ids --- zhaquirks/danfoss/thermostat.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index cf1b925069..5a2efce155 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -141,11 +141,16 @@ class DanfossOperationModeEnum(t.bitmap8): ), } +# clusters +DANFOSS_TRV_CLUSTER = 0xFC05 +DANFOSS_TRV_INTERFACE_CLUSTER = 0xFC06 +DANFOSS_TRV_DIAGNOSTIC_CLUSTER = 0xFC07 + class DanfossTRVCluster(CustomCluster): """Danfoss custom TRV cluster.""" - cluster_id = 0xFC03 + cluster_id = DANFOSS_TRV_CLUSTER ep_attribute = "danfoss_trv_cluster" attributes = danfoss_thermostat_attr @@ -167,7 +172,7 @@ async def read_attributes_raw(self, attributes, manufacturer=None): class DanfossTRVInterfaceCluster(CustomCluster): """Danfoss custom interface cluster.""" - cluster_id = 0xFC04 + cluster_id = DANFOSS_TRV_INTERFACE_CLUSTER ep_attribute = "danfoss_trv_interface_cluster" attributes = danfoss_interface_attr @@ -188,7 +193,7 @@ def read_attributes_raw(self, attributes, manufacturer=None): class DanfossTRVDiagnosticCluster(CustomCluster): """Danfoss custom diagnostic cluster.""" - cluster_id = 0xFC05 + cluster_id = DANFOSS_TRV_DIAGNOSTIC_CLUSTER ep_attribute = "danfoss_trv_diagnostic_cluster" attributes = danfoss_diagnostic_attr From 0a1e32cf580c6a5f2b1231a0f4dc2ee37e03763b Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Tue, 19 Sep 2023 12:08:18 +0200 Subject: [PATCH 22/79] remove custom clusters --- tests/test_danfoss.py | 104 +------------------------------- zhaquirks/danfoss/thermostat.py | 93 ---------------------------- 2 files changed, 2 insertions(+), 195 deletions(-) diff --git a/tests/test_danfoss.py b/tests/test_danfoss.py index 13f40f40f6..592ca97212 100644 --- a/tests/test_danfoss.py +++ b/tests/test_danfoss.py @@ -7,11 +7,6 @@ from zigpy.zcl.foundation import WriteAttributesStatusRecord import zhaquirks -from zhaquirks.danfoss.thermostat import ( - DanfossTRVCluster, - DanfossTRVDiagnosticCluster, - DanfossTRVInterfaceCluster, -) zhaquirks.setup() @@ -47,43 +42,10 @@ def test_popp_signature(assert_signature_matches_quirk): ) -async def test_danfoss_trv_read_attributes(zigpy_device_from_quirk): - device = zigpy_device_from_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat) - - danfoss_thermostat_cluster = device.endpoints[1].in_clusters[Thermostat.cluster_id] - danfoss_trv_cluster = device.endpoints[1].in_clusters[DanfossTRVCluster.cluster_id] - - def mock_read(attributes, manufacturer=None): - records = [ - foundation.ReadAttributeRecord( - attr, foundation.Status.SUCCESS, foundation.TypeValue(None, 6) - ) - for attr in attributes - ] - return (records,) - - # data is served from danfoss_thermostat - patch_danfoss_thermostat_read = mock.patch.object( - danfoss_thermostat_cluster, - "_read_attributes", - mock.AsyncMock(side_effect=mock_read), - ) - - with patch_danfoss_thermostat_read: - # data should be received from danfoss_trv - success, fail = await danfoss_trv_cluster.read_attributes( - ["open_window_detection"] - ) - assert success - assert 6 in success.values() - assert not fail - - async def test_danfoss_thermostat_write_attributes(zigpy_device_from_quirk): device = zigpy_device_from_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat) danfoss_thermostat_cluster = device.endpoints[1].in_clusters[Thermostat.cluster_id] - danfoss_trv_cluster = device.endpoints[1].in_clusters[DanfossTRVCluster.cluster_id] def mock_write(attributes, manufacturer=None): records = [ @@ -114,7 +76,7 @@ def mock_setpoint(oper, sett, manufacturer=None): with patch_danfoss_trv_write: # data should be written to trv, but reach thermostat - success, fail = await danfoss_trv_cluster.write_attributes( + success, fail = await danfoss_thermostat_cluster.write_attributes( {"external_open_window_detected": False} ) assert success @@ -136,7 +98,7 @@ def mock_setpoint(oper, sett, manufacturer=None): 0x0015 ] = 5 # min_limit is present normally - success, fail = await danfoss_trv_cluster.write_attributes( + success, fail = await danfoss_thermostat_cluster.write_attributes( {"system_mode": 0x00} ) assert success @@ -148,65 +110,3 @@ def mock_setpoint(oper, sett, manufacturer=None): assert operation == 0x01 assert setting == 5 - - -async def test_danfoss_interface_update_attribute(zigpy_device_from_quirk): - device = zigpy_device_from_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat) - - danfoss_user_interface_cluster = device.endpoints[1].in_clusters[ - UserInterface.cluster_id - ] - danfoss_trv_interface_cluster = device.endpoints[1].in_clusters[ - DanfossTRVInterfaceCluster.cluster_id - ] - - attribute = -100 - value = -0x01 - - def mock_update(attr, val): - nonlocal attribute, value - - attribute = attr - value = val - - patch_danfoss_trv_interface_update_attribute = mock.patch.object( - danfoss_trv_interface_cluster, - "_update_attribute", - mock.Mock(side_effect=mock_update), - ) - - with patch_danfoss_trv_interface_update_attribute: - danfoss_user_interface_cluster.update_attribute(0x4000, True) - - assert attribute == 0x4000 - assert value - - -async def test_danfoss_diagnostic_update_attribute(zigpy_device_from_quirk): - device = zigpy_device_from_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat) - - danfoss_diagnostic_cluster = device.endpoints[1].in_clusters[Diagnostic.cluster_id] - danfoss_trv_diagnostic_cluster = device.endpoints[1].in_clusters[ - DanfossTRVDiagnosticCluster.cluster_id - ] - - attribute = -100 - value = -0x01 - - def mock_update(attr, val): - nonlocal attribute, value - - attribute = attr - value = val - - patch_danfoss_trv_diagnostic_update_attribute = mock.patch.object( - danfoss_trv_diagnostic_cluster, - "_update_attribute", - mock.Mock(side_effect=mock_update), - ) - - with patch_danfoss_trv_diagnostic_update_attribute: - danfoss_diagnostic_cluster.update_attribute(0x4010, 4546) - - assert attribute == 0x4010 - assert value == 4546 diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 5a2efce155..486fb42bb4 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -141,75 +141,6 @@ class DanfossOperationModeEnum(t.bitmap8): ), } -# clusters -DANFOSS_TRV_CLUSTER = 0xFC05 -DANFOSS_TRV_INTERFACE_CLUSTER = 0xFC06 -DANFOSS_TRV_DIAGNOSTIC_CLUSTER = 0xFC07 - - -class DanfossTRVCluster(CustomCluster): - """Danfoss custom TRV cluster.""" - - cluster_id = DANFOSS_TRV_CLUSTER - ep_attribute = "danfoss_trv_cluster" - - attributes = danfoss_thermostat_attr - - async def write_attributes(self, attributes, manufacturer=None): - """Write attributes to Thermostat cluster.""" - return await self.endpoint.thermostat.write_attributes( - attributes, manufacturer=manufacturer - ) - - async def read_attributes_raw(self, attributes, manufacturer=None): - """Read attributes from Thermostat cluster""" - - return await self.endpoint.thermostat.read_attributes_raw( - attributes, manufacturer=manufacturer - ) - - -class DanfossTRVInterfaceCluster(CustomCluster): - """Danfoss custom interface cluster.""" - - cluster_id = DANFOSS_TRV_INTERFACE_CLUSTER - ep_attribute = "danfoss_trv_interface_cluster" - - attributes = danfoss_interface_attr - - async def write_attributes(self, attributes, manufacturer=None): - """Write attributes to User Interface cluster.""" - return await self.endpoint.thermostat_ui.write_attributes( - attributes, manufacturer=manufacturer - ) - - def read_attributes_raw(self, attributes, manufacturer=None): - """Read in attributes from User Interface Cluster.""" - return self.endpoint.thermostat_ui.read_attributes_raw( - attributes, manufacturer=manufacturer - ) - - -class DanfossTRVDiagnosticCluster(CustomCluster): - """Danfoss custom diagnostic cluster.""" - - cluster_id = DANFOSS_TRV_DIAGNOSTIC_CLUSTER - ep_attribute = "danfoss_trv_diagnostic_cluster" - - attributes = danfoss_diagnostic_attr - - async def write_attributes(self, attributes, manufacturer=None): - """Write attributes to Diagnostic cluster.""" - return await self.endpoint.diagnostic.write_attributes( - attributes, manufacturer=manufacturer - ) - - def read_attributes_raw(self, attributes, manufacturer=None): - """Read in attributes from Diagnostic Cluster.""" - return self.endpoint.diagnostic.read_attributes_raw( - attributes, manufacturer=manufacturer - ) - class DanfossThermostatCluster(CustomCluster, Thermostat): """Danfoss cluster for ZCL attributes and forwarding proprietary attributes.""" @@ -259,13 +190,6 @@ async def write_attributes(self, attributes, manufacturer=None): return write_res - def _update_attribute(self, attrid, value): - """Update attributes of TRV cluster.""" - super()._update_attribute(attrid, value) - - if attrid in danfoss_thermostat_attr: - self.endpoint.danfoss_trv_cluster.update_attribute(attrid, value) - class DanfossUserInterfaceCluster(CustomCluster, UserInterface): """Danfoss cluster for ZCL attributes and forwarding proprietary attributes.""" @@ -273,13 +197,6 @@ class DanfossUserInterfaceCluster(CustomCluster, UserInterface): attributes = UserInterface.attributes.copy() attributes.update({**danfoss_interface_attr, **zcl_attr}) - def _update_attribute(self, attrid, value): - """Update attributes of TRV interface cluster.""" - super()._update_attribute(attrid, value) - - if attrid in danfoss_interface_attr: - self.endpoint.danfoss_trv_interface_cluster.update_attribute(attrid, value) - class DanfossDiagnosticCluster(CustomCluster, Diagnostic): """Danfoss cluster for ZCL attributes and forwarding proprietary attributes.""" @@ -287,13 +204,6 @@ class DanfossDiagnosticCluster(CustomCluster, Diagnostic): attributes = Diagnostic.attributes.copy() attributes.update({**danfoss_diagnostic_attr, **zcl_attr}) - def _update_attribute(self, attrid, value): - """Update attributes or TRV diagnostic cluster.""" - super()._update_attribute(attrid, value) - - if attrid in danfoss_diagnostic_attr: - self.endpoint.danfoss_trv_diagnostic_cluster.update_attribute(attrid, value) - class DanfossThermostat(CustomDevice): """DanfossThermostat custom device.""" @@ -342,9 +252,6 @@ class DanfossThermostat(CustomDevice): DanfossThermostatCluster, DanfossUserInterfaceCluster, DanfossDiagnosticCluster, - DanfossTRVCluster, - DanfossTRVInterfaceCluster, - DanfossTRVDiagnosticCluster, ], OUTPUT_CLUSTERS: [Basic, Ota], } From e40da883e2bd9e62592b4d00aced669a70d10179 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sat, 30 Sep 2023 00:54:38 +0200 Subject: [PATCH 23/79] move constants out of init file --- zhaquirks/danfoss/__init__.py | 3 --- zhaquirks/danfoss/thermostat.py | 5 ++++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/zhaquirks/danfoss/__init__.py b/zhaquirks/danfoss/__init__.py index 4159407e34..8591f3f950 100644 --- a/zhaquirks/danfoss/__init__.py +++ b/zhaquirks/danfoss/__init__.py @@ -1,4 +1 @@ """Module for Danfoss quirks implementations.""" -DANFOSS = "Danfoss" -HIVE = DANFOSS -POPP = "D5X84YU" diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 486fb42bb4..1f270d2ab9 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -29,7 +29,6 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) -from zhaquirks.danfoss import DANFOSS, HIVE, POPP class DanfossOperationModeEnum(t.bitmap8): @@ -43,6 +42,10 @@ class DanfossOperationModeEnum(t.bitmap8): Schedule_Preheat = 0b00000011 +DANFOSS = "Danfoss" +HIVE = DANFOSS +POPP = "D5X84YU" + OCCUPIED_HEATING_SETPOINT_NAME = "occupied_heating_setpoint" SYSTEM_MODE_NAME = "system_mode" From 7dfccd8910ada0016e3ccf0dcdfadb081effb7b1 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sat, 30 Sep 2023 01:36:01 +0200 Subject: [PATCH 24/79] fix code quality --- tests/test_danfoss.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_danfoss.py b/tests/test_danfoss.py index 592ca97212..cd57dfd92d 100644 --- a/tests/test_danfoss.py +++ b/tests/test_danfoss.py @@ -2,8 +2,7 @@ from unittest import mock from zigpy.zcl import foundation -from zigpy.zcl.clusters.homeautomation import Diagnostic -from zigpy.zcl.clusters.hvac import Thermostat, UserInterface +from zigpy.zcl.clusters.hvac import Thermostat from zigpy.zcl.foundation import WriteAttributesStatusRecord import zhaquirks From b138169e68b01232be2879130596280bd8298f55 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Mon, 2 Oct 2023 05:43:16 +0200 Subject: [PATCH 25/79] added time sync --- zhaquirks/danfoss/thermostat.py | 130 +++++++++++++++++++++----------- 1 file changed, 88 insertions(+), 42 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 1f270d2ab9..b882e72296 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -1,10 +1,12 @@ """Module to handle quirks of the Danfoss thermostat. manufacturer specific attributes to control displaying and specific configuration. - -manufacturer_code = 0x1246 """ +import traceback + +from datetime import datetime + import zigpy.profiles.zha as zha_p from zigpy.quirks import CustomCluster, CustomDevice import zigpy.types as t @@ -59,34 +61,31 @@ class DanfossOperationModeEnum(t.bitmap8): # 0x0201 danfoss_thermostat_attr = { - 0x4000: ("open_window_detection", t.enum8, "rp"), - 0x4003: ("external_open_window_detected", t.Bool, "rpw"), - 0x4051: ("window_open_feature", t.Bool, "rpw"), - 0x4010: ("exercise_day_of_week", t.enum8, "rpw"), - 0x4011: ("exercise_trigger_time", t.uint16_t, "rpw"), - 0x4012: ("mounting_mode_active", t.Bool, "rp"), - 0x4013: ("mounting_mode_control", t.Bool, "rpw"), # undocumented - 0x4014: ("orientation", t.enum8, "rpw"), - 0x4015: ("external_measured_room_sensor", t.int16s, "rpw"), - 0x4016: ("radiator_covered", t.Bool, "rpw"), - 0x4030: ("heat_available", t.Bool, "rpw"), # undocumented - 0x4031: ("heat_required", t.Bool, "rp"), # undocumented - 0x4032: ("load_balancing_enable", t.Bool, "rpw"), - 0x4040: ("load_room_mean", t.int16s, "rpw"), - 0x404A: ("load_estimate", t.int16s, "rp"), - 0x4020: ("control_algorithm_scale_factor", t.uint8_t, "rpw"), - 0x404B: ("regulation_setpoint_offset", t.int8s, "rpw"), - 0x404C: ("adaptation_run_control", t.enum8, "rw"), - 0x404D: ("adaptation_run_status", t.bitmap8, "rp"), - 0x404E: ("adaptation_run_settings", t.bitmap8, "rw"), - 0x404F: ("preheat_status", t.Bool, "rp"), - 0x4050: ("preheat_time", t.uint32_t, "rp"), - 0x0025: ( - "programing_oper_mode", - DanfossOperationModeEnum, - "rpw", - ), # Danfoss deviated from the spec + 0x4000: ("open_window_detection", t.enum8, "rp", True), + 0x4003: ("external_open_window_detected", t.Bool, "rpw", True), + 0x4051: ("window_open_feature", t.Bool, "rpw", True), + 0x4010: ("exercise_day_of_week", t.enum8, "rpw", True), + 0x4011: ("exercise_trigger_time", t.uint16_t, "rpw", True), + 0x4012: ("mounting_mode_active", t.Bool, "rp", True), + 0x4013: ("mounting_mode_control", t.Bool, "rpw", True), # undocumented + 0x4014: ("orientation", t.enum8, "rpw", True), + 0x4015: ("external_measured_room_sensor", t.int16s, "rpw", True), + 0x4016: ("radiator_covered", t.Bool, "rpw", True), + 0x4030: ("heat_available", t.Bool, "rpw", True), # undocumented + 0x4031: ("heat_required", t.Bool, "rp", True), # undocumented + 0x4032: ("load_balancing_enable", t.Bool, "rpw", True), + 0x4040: ("load_room_mean", t.int16s, "rpw", True), + 0x404A: ("load_estimate", t.int16s, "rp", True), + 0x4020: ("control_algorithm_scale_factor", t.uint8_t, "rpw", True), + 0x404B: ("regulation_setpoint_offset", t.int8s, "rpw", True), + 0x404C: ("adaptation_run_control", t.enum8, "rw", True), + 0x404D: ("adaptation_run_status", t.bitmap8, "rp", True), + 0x404E: ("adaptation_run_settings", t.bitmap8, "rw", True), + 0x404F: ("preheat_status", t.Bool, "rp", True), + 0x4050: ("preheat_time", t.uint32_t, "rp", True), } +# 0x0025: ("programing_oper_mode", DanfossOperationModeEnum, "rpw",) # Danfoss deviated from the spec + # ZCL Attributes Supported: # pi_heating_demand (0x0008), # min_heat_setpoint_limit (0x0015) @@ -107,7 +106,7 @@ class DanfossOperationModeEnum(t.bitmap8): # 0x0204 danfoss_interface_attr = { - 0x4000: ("viewing_direction", t.enum8, "rpw"), + 0x4000: ("viewing_direction", t.enum8, "rpw", True), } # Writing to mandatory ZCL attribute 0x0000 doesn't seem to do anything @@ -115,14 +114,14 @@ class DanfossOperationModeEnum(t.bitmap8): # 0x0b05 danfoss_diagnostic_attr = { - 0x4000: ("sw_error_code", t.bitmap16, "rp"), - 0x4001: ("wake_time_avg", t.uint32_t, "rp"), # always 0? - 0x4002: ("wake_time_max_duration", t.uint32_t, "rp"), # always 0? - 0x4003: ("wake_time_min_duration", t.uint32_t, "rp"), # always 0? - 0x4004: ("sleep_postponed_count_avg", t.uint32_t, "rp"), # always 0? - 0x4005: ("sleep_postponed_count_max", t.uint32_t, "rp"), # always 0? - 0x4006: ("sleep_postponed_count_min", t.uint32_t, "rp"), # always 0? - 0x4010: ("motor_step_counter", t.uint32_t, "rp"), + 0x4000: ("sw_error_code", t.bitmap16, "rp", True), + 0x4001: ("wake_time_avg", t.uint32_t, "rp", True), # always 0? + 0x4002: ("wake_time_max_duration", t.uint32_t, "rp", True), # always 0? + 0x4003: ("wake_time_min_duration", t.uint32_t, "rp", True), # always 0? + 0x4004: ("sleep_postponed_count_avg", t.uint32_t, "rp", True), # always 0? + 0x4005: ("sleep_postponed_count_max", t.uint32_t, "rp", True), # always 0? + 0x4006: ("sleep_postponed_count_min", t.uint32_t, "rp", True), # always 0? + 0x4010: ("motor_step_counter", t.uint32_t, "rp", True), } danfoss_thermostat_comm = { @@ -145,7 +144,7 @@ class DanfossOperationModeEnum(t.bitmap8): } -class DanfossThermostatCluster(CustomCluster, Thermostat): +class DanfossThermostatCluster(Thermostat): """Danfoss cluster for ZCL attributes and forwarding proprietary attributes.""" server_commands = Thermostat.server_commands.copy() @@ -193,24 +192,71 @@ async def write_attributes(self, attributes, manufacturer=None): return write_res + def add_unsupported_attribute( + self, attr: int | str, inhibit_events: bool = False + ) -> None: + if attr in {8, "pi_heating_demand"}: + print("*!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\n\n\n\n") + print(f"Unsupported: {attr}") + traceback.print_stack() + print("^!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\n\n\n\n") + return super().add_unsupported_attribute(attr, inhibit_events=inhibit_events) + + async def bind(self): + """ + According to the documentation of Zigbee2MQTT there is a bug in the Danfoss firmware with the time. + It doesn't request it, so it has to be fed the correct time. + """ + await self.endpoint.time.write_time() + + return await super().bind() + -class DanfossUserInterfaceCluster(CustomCluster, UserInterface): +class DanfossUserInterfaceCluster(UserInterface): """Danfoss cluster for ZCL attributes and forwarding proprietary attributes.""" attributes = UserInterface.attributes.copy() attributes.update({**danfoss_interface_attr, **zcl_attr}) -class DanfossDiagnosticCluster(CustomCluster, Diagnostic): +class DanfossDiagnosticCluster(Diagnostic): """Danfoss cluster for ZCL attributes and forwarding proprietary attributes.""" attributes = Diagnostic.attributes.copy() attributes.update({**danfoss_diagnostic_attr, **zcl_attr}) +class DanfossTimeCluster(Time): + """Danfoss cluster for fixing the time.""" + + async def write_time(self): + epoch = datetime(2000, 1, 1, 0, 0, 0, 0) + current_time = (datetime.utcnow() - epoch).total_seconds() + + time_zone = (datetime.fromtimestamp(86400) - datetime.utcfromtimestamp(86400)).total_seconds() + + res = await self.write_attributes({"time": current_time, + "time_status": 0b00000010, # only bit 1 can be written + "time_zone": time_zone + }) + + async def bind(self): + """ + According to the documentation of Zigbee2MQTT there is a bug in the Danfoss firmware with the time. + It doesn't request it, so it has to be fed the correct time. + """ + result = await super().bind() + + await self.write_time() + + return result + + class DanfossThermostat(CustomDevice): """DanfossThermostat custom device.""" + manufacturer_code = 0x1246 + signature = { # Date: Mon, 2 Oct 2023 06:19:19 +0200 Subject: [PATCH 26/79] move attrs into new datastructure --- zhaquirks/danfoss/thermostat.py | 133 ++++++++++++++------------------ 1 file changed, 58 insertions(+), 75 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index b882e72296..d8cb59cdb6 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -21,7 +21,7 @@ ) from zigpy.zcl.clusters.homeautomation import Diagnostic from zigpy.zcl.clusters.hvac import Thermostat, UserInterface -from zigpy.zcl.foundation import ZCLCommandDef +from zigpy.zcl.foundation import ZCLCommandDef, ZCLAttributeDef from zhaquirks.const import ( DEVICE_TYPE, @@ -60,30 +60,6 @@ class DanfossOperationModeEnum(t.bitmap8): SYSTEM_MODE_THERM_OFF_VAL = 0x00 # 0x0201 -danfoss_thermostat_attr = { - 0x4000: ("open_window_detection", t.enum8, "rp", True), - 0x4003: ("external_open_window_detected", t.Bool, "rpw", True), - 0x4051: ("window_open_feature", t.Bool, "rpw", True), - 0x4010: ("exercise_day_of_week", t.enum8, "rpw", True), - 0x4011: ("exercise_trigger_time", t.uint16_t, "rpw", True), - 0x4012: ("mounting_mode_active", t.Bool, "rp", True), - 0x4013: ("mounting_mode_control", t.Bool, "rpw", True), # undocumented - 0x4014: ("orientation", t.enum8, "rpw", True), - 0x4015: ("external_measured_room_sensor", t.int16s, "rpw", True), - 0x4016: ("radiator_covered", t.Bool, "rpw", True), - 0x4030: ("heat_available", t.Bool, "rpw", True), # undocumented - 0x4031: ("heat_required", t.Bool, "rp", True), # undocumented - 0x4032: ("load_balancing_enable", t.Bool, "rpw", True), - 0x4040: ("load_room_mean", t.int16s, "rpw", True), - 0x404A: ("load_estimate", t.int16s, "rp", True), - 0x4020: ("control_algorithm_scale_factor", t.uint8_t, "rpw", True), - 0x404B: ("regulation_setpoint_offset", t.int8s, "rpw", True), - 0x404C: ("adaptation_run_control", t.enum8, "rw", True), - 0x404D: ("adaptation_run_status", t.bitmap8, "rp", True), - 0x404E: ("adaptation_run_settings", t.bitmap8, "rw", True), - 0x404F: ("preheat_status", t.Bool, "rp", True), - 0x4050: ("preheat_time", t.uint32_t, "rp", True), -} # 0x0025: ("programing_oper_mode", DanfossOperationModeEnum, "rpw",) # Danfoss deviated from the spec # ZCL Attributes Supported: @@ -98,60 +74,60 @@ class DanfossOperationModeEnum(t.bitmap8): # number_of_weekly_transitions (0x0021)=42 # number_of_daily_transitions (0x0022)=6 -zcl_attr = { - 0xFFFD: ("cluster_revision", t.uint16_t, "r"), -} +# 0xFFFD: ("cluster_revision", t.uint16_t, "r") # ZCL Commands Supported: SetWeeklySchedule (0x01), GetWeeklySchedule (0x02), ClearWeeklySchedule (0x03) # 0x0204 -danfoss_interface_attr = { - 0x4000: ("viewing_direction", t.enum8, "rpw", True), -} # Writing to mandatory ZCL attribute 0x0000 doesn't seem to do anything # ZCL Attributes Supported: keypad_lockout (0x0001) -# 0x0b05 -danfoss_diagnostic_attr = { - 0x4000: ("sw_error_code", t.bitmap16, "rp", True), - 0x4001: ("wake_time_avg", t.uint32_t, "rp", True), # always 0? - 0x4002: ("wake_time_max_duration", t.uint32_t, "rp", True), # always 0? - 0x4003: ("wake_time_min_duration", t.uint32_t, "rp", True), # always 0? - 0x4004: ("sleep_postponed_count_avg", t.uint32_t, "rp", True), # always 0? - 0x4005: ("sleep_postponed_count_max", t.uint32_t, "rp", True), # always 0? - 0x4006: ("sleep_postponed_count_min", t.uint32_t, "rp", True), # always 0? - 0x4010: ("motor_step_counter", t.uint32_t, "rp", True), -} - -danfoss_thermostat_comm = { - 0x40: ZCLCommandDef( - "setpoint_command", - # Types - # 0: Schedule (relatively slow) - # 1: User Interaction (aggressive change) - # 2: Preheat (invisible to user) - {"type": t.enum8, "heating_setpoint": t.int16s}, - is_manufacturer_specific=True, - ), - # for synchronizing multiple TRVs preheating - 0x42: ZCLCommandDef( - "preheat_command", - # Force: 0 means force, other values for future needs - {"force": t.enum8, "timestamp": t.uint32_t}, - is_manufacturer_specific=True, - ), -} - - -class DanfossThermostatCluster(Thermostat): + +class DanfossThermostatCluster(Thermostat, CustomCluster): """Danfoss cluster for ZCL attributes and forwarding proprietary attributes.""" - server_commands = Thermostat.server_commands.copy() - server_commands.update(danfoss_thermostat_comm) + class ServerCommandDefs(Thermostat.ServerCommandDefs): + setpoint_command = ZCLCommandDef(id=0x40, + # Types + # 0: Schedule (relatively slow) + # 1: User Interaction (aggressive change) + # 2: Preheat (invisible to user) + schema={"type": t.enum8, "heating_setpoint": t.int16s}, + is_manufacturer_specific=True, + ) + + # for synchronizing multiple TRVs preheating + preheat_command = ZCLCommandDef( + id=0x42, + # Force: 0 means force, other values for future needs + schema={"force": t.enum8, "timestamp": t.uint32_t}, + is_manufacturer_specific=True, + ) - attributes = Thermostat.attributes.copy() - attributes.update({**danfoss_thermostat_attr, **zcl_attr}) + class AttributeDefs(Thermostat.AttributeDefs): + open_window_detection = ZCLAttributeDef(id=0x4000, type=t.enum8, access="rp", is_manufacturer_specific=True) + external_open_window_detected = ZCLAttributeDef(id=0x4003, type=t.Bool, access="rpw", is_manufacturer_specific=True) + window_open_feature = ZCLAttributeDef(id=0x4051, type=t.Bool, access="rpw", is_manufacturer_specific=True) + exercise_day_of_week = ZCLAttributeDef(id=0x4010, type=t.enum8, access="rpw", is_manufacturer_specific=True) + exercise_trigger_time = ZCLAttributeDef(id=0x4011, type=t.uint16_t, access="rpw", is_manufacturer_specific=True) + mounting_mode_active = ZCLAttributeDef(id=0x4012, type=t.Bool, access="rp", is_manufacturer_specific=True) + mounting_mode_control = ZCLAttributeDef(id=0x4013, type=t.Bool, access="rpw", is_manufacturer_specific=True) + orientation = ZCLAttributeDef(id=0x4014, type=t.enum8, access="rpw", is_manufacturer_specific=True) + external_measured_room_sensor = ZCLAttributeDef(id=0x4015, type=t.int16s, access="rpw", is_manufacturer_specific=True) + radiator_covered = ZCLAttributeDef(id=0x4016, type=t.Bool, access="rpw", is_manufacturer_specific=True) + heat_available = ZCLAttributeDef(id=0x4030, type=t.Bool, access="rpw", is_manufacturer_specific=True) + heat_required = ZCLAttributeDef(id=0x4031, type=t.Bool, access="rp", is_manufacturer_specific=True) + load_balancing_enable = ZCLAttributeDef(id=0x4032, type=t.Bool, access="rpw", is_manufacturer_specific=True) + load_room_mean = ZCLAttributeDef(id=0x4040, type=t.int16s, access="rpw", is_manufacturer_specific=True) + load_estimate = ZCLAttributeDef(id=0x404A, type=t.int16s, access="rp", is_manufacturer_specific=True) + control_algorithm_scale_factor = ZCLAttributeDef(id=0x4020, type=t.uint8_t, access="rpw", is_manufacturer_specific=True) + regulation_setpoint_offset = ZCLAttributeDef(id=0x404B, type=t.int8s, access="rpw", is_manufacturer_specific=True) + adaptation_run_control = ZCLAttributeDef(id=0x404C, type=t.enum8, access="rw", is_manufacturer_specific=True) + adaptation_run_status = ZCLAttributeDef(id=0x404D, type=t.bitmap8, access="rp", is_manufacturer_specific=True) + adaptation_run_settings = ZCLAttributeDef(id=0x404E, type=t.bitmap8, access="rw", is_manufacturer_specific=True) + preheat_status = ZCLAttributeDef(id=0x404F, type=t.Bool, access="rp", is_manufacturer_specific=True) + preheat_time = ZCLAttributeDef(id=0x4050, type=t.uint32_t, access="rp", is_manufacturer_specific=True) async def write_attributes(self, attributes, manufacturer=None): """There are 2 types of setpoint changes: @@ -212,21 +188,28 @@ async def bind(self): return await super().bind() -class DanfossUserInterfaceCluster(UserInterface): +class DanfossUserInterfaceCluster(UserInterface, CustomCluster): """Danfoss cluster for ZCL attributes and forwarding proprietary attributes.""" - attributes = UserInterface.attributes.copy() - attributes.update({**danfoss_interface_attr, **zcl_attr}) + class AttributeDefs(UserInterface.AttributeDefs): + viewing_direction = ZCLAttributeDef(id=0x4000, type=t.enum8, access="rpw", is_manufacturer_specific=True) -class DanfossDiagnosticCluster(Diagnostic): +class DanfossDiagnosticCluster(Diagnostic, CustomCluster): """Danfoss cluster for ZCL attributes and forwarding proprietary attributes.""" - attributes = Diagnostic.attributes.copy() - attributes.update({**danfoss_diagnostic_attr, **zcl_attr}) + class AttributeDefs(Diagnostic.AttributeDefs): + sw_error_code = ZCLAttributeDef(id=0x4000, type=t.bitmap16, access="rp", is_manufacturer_specific=True) + wake_time_avg = ZCLAttributeDef(id=0x4001, type=t.uint32_t, access="rp", is_manufacturer_specific=True) + wake_time_max_duration = ZCLAttributeDef(id=0x4002, type=t.uint32_t, access="rp", is_manufacturer_specific=True) + wake_time_min_duration = ZCLAttributeDef(id=0x4003, type=t.uint32_t, access="rp", is_manufacturer_specific=True) + sleep_postponed_count_avg = ZCLAttributeDef(id=0x4004, type=t.uint32_t, access="rp", is_manufacturer_specific=True) + sleep_postponed_count_max = ZCLAttributeDef(id=0x4005, type=t.uint32_t, access="rp", is_manufacturer_specific=True) + sleep_postponed_count_min = ZCLAttributeDef(id=0x4006, type=t.uint32_t, access="rp", is_manufacturer_specific=True) + motor_step_counter = ZCLAttributeDef(id=0x4010, type=t.uint32_t, access="rp", is_manufacturer_specific=True) -class DanfossTimeCluster(Time): +class DanfossTimeCluster(Time, CustomCluster): """Danfoss cluster for fixing the time.""" async def write_time(self): From 54a3ef5f422a45fa5439722018dea5971398337c Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Mon, 2 Oct 2023 06:47:49 +0200 Subject: [PATCH 27/79] refactor comments --- zhaquirks/danfoss/thermostat.py | 72 ++++++++++++++------------------- 1 file changed, 30 insertions(+), 42 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index d8cb59cdb6..60c064fc97 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -1,6 +1,29 @@ """Module to handle quirks of the Danfoss thermostat. manufacturer specific attributes to control displaying and specific configuration. + +ZCL Attributes Supported: + 0x0201 - 0x0025: programing_oper_mode # Danfoss deviated from the spec + all - 0xFFFD: cluster_revision + + 0x0201 - pi_heating_demand (0x0008), + 0x0201 - min_heat_setpoint_limit (0x0015) + 0x0201 - max_heat_setpoint_limit (0x0016) + 0x0201 - setpoint_change_source (0x0030) + 0x0201 - abs_min_heat_setpoint_limit (0x0003)=5 + 0x0201 - abs_max_heat_setpoint_limit (0x0004)=35 + 0x0201 - start_of_week (0x0020)=Monday + 0x0201 - number_of_weekly_transitions (0x0021)=42 + 0x0201 - number_of_daily_transitions (0x0022)=6 + 0x0204: keypad_lockout (0x0001) + +ZCL Commands Supported: + 0x0201 - SetWeeklySchedule (0x01) + 0x0201 - GetWeeklySchedule (0x02) + 0x0201 - ClearWeeklySchedule (0x03) + +Broken ZCL Attributes: + 0x0204 - 0x0000: Writing doesn't seem to do anything """ import traceback @@ -33,17 +56,6 @@ ) -class DanfossOperationModeEnum(t.bitmap8): - """Nonstandard implementation of Programming Operation Mode from Danfoss. - The official specification still works: 0x0 or 0x1, but Danfoss added a preheat bit - """ - - Manual = 0b00000000 - Schedule = 0b00000001 - Manual_Preheat = 0b00000010 - Schedule_Preheat = 0b00000011 - - DANFOSS = "Danfoss" HIVE = DANFOSS POPP = "D5X84YU" @@ -59,33 +71,9 @@ class DanfossOperationModeEnum(t.bitmap8): SYSTEM_MODE_THERM_OFF_VAL = 0x00 -# 0x0201 -# 0x0025: ("programing_oper_mode", DanfossOperationModeEnum, "rpw",) # Danfoss deviated from the spec - -# ZCL Attributes Supported: -# pi_heating_demand (0x0008), -# min_heat_setpoint_limit (0x0015) -# max_heat_setpoint_limit (0x0016) -# setpoint_change_source (0x0030) -# hardcoded: -# abs_min_heat_setpoint_limit (0x0003)=5 -# abs_max_heat_setpoint_limit (0x0004)=35 -# start_of_week (0x0020)=Monday -# number_of_weekly_transitions (0x0021)=42 -# number_of_daily_transitions (0x0022)=6 - -# 0xFFFD: ("cluster_revision", t.uint16_t, "r") - -# ZCL Commands Supported: SetWeeklySchedule (0x01), GetWeeklySchedule (0x02), ClearWeeklySchedule (0x03) - -# 0x0204 - -# Writing to mandatory ZCL attribute 0x0000 doesn't seem to do anything -# ZCL Attributes Supported: keypad_lockout (0x0001) - -class DanfossThermostatCluster(Thermostat, CustomCluster): - """Danfoss cluster for ZCL attributes and forwarding proprietary attributes.""" +class DanfossThermostatCluster(CustomCluster, Thermostat): + """Danfoss cluster for standard and proprietary danfoss attributes""" class ServerCommandDefs(Thermostat.ServerCommandDefs): setpoint_command = ZCLCommandDef(id=0x40, @@ -188,15 +176,15 @@ async def bind(self): return await super().bind() -class DanfossUserInterfaceCluster(UserInterface, CustomCluster): - """Danfoss cluster for ZCL attributes and forwarding proprietary attributes.""" +class DanfossUserInterfaceCluster(CustomCluster, UserInterface): + """Danfoss cluster for standard and proprietary danfoss attributes""" class AttributeDefs(UserInterface.AttributeDefs): viewing_direction = ZCLAttributeDef(id=0x4000, type=t.enum8, access="rpw", is_manufacturer_specific=True) -class DanfossDiagnosticCluster(Diagnostic, CustomCluster): - """Danfoss cluster for ZCL attributes and forwarding proprietary attributes.""" +class DanfossDiagnosticCluster(CustomCluster, Diagnostic): + """Danfoss cluster for standard and proprietary danfoss attributes""" class AttributeDefs(Diagnostic.AttributeDefs): sw_error_code = ZCLAttributeDef(id=0x4000, type=t.bitmap16, access="rp", is_manufacturer_specific=True) @@ -209,7 +197,7 @@ class AttributeDefs(Diagnostic.AttributeDefs): motor_step_counter = ZCLAttributeDef(id=0x4010, type=t.uint32_t, access="rp", is_manufacturer_specific=True) -class DanfossTimeCluster(Time, CustomCluster): +class DanfossTimeCluster(CustomCluster, Time): """Danfoss cluster for fixing the time.""" async def write_time(self): From 69ce96dca626603d0884f665cfaf14f6ba62d0cb Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Mon, 2 Oct 2023 07:36:21 +0200 Subject: [PATCH 28/79] fix non-working standard clusters --- zhaquirks/danfoss/thermostat.py | 115 +++++++++++++++++++++++--------- 1 file changed, 82 insertions(+), 33 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 60c064fc97..8023a4bdf1 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -34,6 +34,7 @@ from zigpy.quirks import CustomCluster, CustomDevice import zigpy.types as t from zigpy.types import uint16_t +from zigpy.zcl import foundation from zigpy.zcl.clusters.general import ( Basic, Identify, @@ -55,7 +56,6 @@ PROFILE_ID, ) - DANFOSS = "Danfoss" HIVE = DANFOSS POPP = "D5X84YU" @@ -72,18 +72,70 @@ SYSTEM_MODE_THERM_OFF_VAL = 0x00 -class DanfossThermostatCluster(CustomCluster, Thermostat): +class CustomizedStandardCluster(CustomCluster): + """Danfoss customized standard clusters by adding custom attributes + Danfoss doesn't allow standard attributes when manufacturer specific is requested + + Therefore this subclass separates manufacturer specific and standard attributes before + Zigbee commands allowing manufacturer specific to be passed""" + @staticmethod + def combine_results(result_a, result_b): + return [[*result_a[0], *result_b[0]], [*result_a[1:], *result_b[1:]]] + + async def _configure_reporting( + self, + config_records, + *args, **kwargs, + ): + """Configure reporting ZCL foundation command.""" + config_records_manufacturer_specific = [e for e in config_records if self.attributes[e.attrid].is_manufacturer_specific] + config_records_standard = [e for e in config_records if not self.attributes[e.attrid].is_manufacturer_specific] + + result_a = await super()._configure_reporting( + config_records_manufacturer_specific, + *args, **kwargs, + ) + result_b = await super()._configure_reporting( + config_records_standard, + *args, **kwargs, + ) + + return self.combine_results(result_a, result_b) + + async def _read_attributes( # type:ignore[override] + self, + attribute_ids: list[t.uint16_t], + *args, + manufacturer: int | t.uint16_t | None = None, + **kwargs, + ): + """Read attributes ZCL foundation command.""" + + attribute_ids_manufacturer_specific = [e for e in attribute_ids if self.attributes[e].is_manufacturer_specific] + attribute_ids_standard = [e for e in attribute_ids if not self.attributes[e].is_manufacturer_specific] + + result_a = await super()._read_attributes( + attribute_ids_manufacturer_specific, *args, **kwargs + ) + + result_b = await super()._read_attributes( + attribute_ids_standard, *args, **kwargs + ) + return self.combine_results(result_a, result_b) + + +class DanfossThermostatCluster(CustomizedStandardCluster, Thermostat): """Danfoss cluster for standard and proprietary danfoss attributes""" class ServerCommandDefs(Thermostat.ServerCommandDefs): setpoint_command = ZCLCommandDef(id=0x40, - # Types - # 0: Schedule (relatively slow) - # 1: User Interaction (aggressive change) - # 2: Preheat (invisible to user) - schema={"type": t.enum8, "heating_setpoint": t.int16s}, - is_manufacturer_specific=True, - ) + # Types + # 0: Schedule (relatively slow) + # 1: User Interaction (aggressive change) + # 2: Preheat (invisible to user) + schema={"type": t.enum8, "heating_setpoint": t.int16s}, + is_manufacturer_specific=True, + ) # for synchronizing multiple TRVs preheating preheat_command = ZCLCommandDef( @@ -95,22 +147,26 @@ class ServerCommandDefs(Thermostat.ServerCommandDefs): class AttributeDefs(Thermostat.AttributeDefs): open_window_detection = ZCLAttributeDef(id=0x4000, type=t.enum8, access="rp", is_manufacturer_specific=True) - external_open_window_detected = ZCLAttributeDef(id=0x4003, type=t.Bool, access="rpw", is_manufacturer_specific=True) + external_open_window_detected = ZCLAttributeDef(id=0x4003, type=t.Bool, access="rpw", + is_manufacturer_specific=True) window_open_feature = ZCLAttributeDef(id=0x4051, type=t.Bool, access="rpw", is_manufacturer_specific=True) exercise_day_of_week = ZCLAttributeDef(id=0x4010, type=t.enum8, access="rpw", is_manufacturer_specific=True) exercise_trigger_time = ZCLAttributeDef(id=0x4011, type=t.uint16_t, access="rpw", is_manufacturer_specific=True) mounting_mode_active = ZCLAttributeDef(id=0x4012, type=t.Bool, access="rp", is_manufacturer_specific=True) mounting_mode_control = ZCLAttributeDef(id=0x4013, type=t.Bool, access="rpw", is_manufacturer_specific=True) orientation = ZCLAttributeDef(id=0x4014, type=t.enum8, access="rpw", is_manufacturer_specific=True) - external_measured_room_sensor = ZCLAttributeDef(id=0x4015, type=t.int16s, access="rpw", is_manufacturer_specific=True) + external_measured_room_sensor = ZCLAttributeDef(id=0x4015, type=t.int16s, access="rpw", + is_manufacturer_specific=True) radiator_covered = ZCLAttributeDef(id=0x4016, type=t.Bool, access="rpw", is_manufacturer_specific=True) heat_available = ZCLAttributeDef(id=0x4030, type=t.Bool, access="rpw", is_manufacturer_specific=True) heat_required = ZCLAttributeDef(id=0x4031, type=t.Bool, access="rp", is_manufacturer_specific=True) load_balancing_enable = ZCLAttributeDef(id=0x4032, type=t.Bool, access="rpw", is_manufacturer_specific=True) load_room_mean = ZCLAttributeDef(id=0x4040, type=t.int16s, access="rpw", is_manufacturer_specific=True) load_estimate = ZCLAttributeDef(id=0x404A, type=t.int16s, access="rp", is_manufacturer_specific=True) - control_algorithm_scale_factor = ZCLAttributeDef(id=0x4020, type=t.uint8_t, access="rpw", is_manufacturer_specific=True) - regulation_setpoint_offset = ZCLAttributeDef(id=0x404B, type=t.int8s, access="rpw", is_manufacturer_specific=True) + control_algorithm_scale_factor = ZCLAttributeDef(id=0x4020, type=t.uint8_t, access="rpw", + is_manufacturer_specific=True) + regulation_setpoint_offset = ZCLAttributeDef(id=0x404B, type=t.int8s, access="rpw", + is_manufacturer_specific=True) adaptation_run_control = ZCLAttributeDef(id=0x404C, type=t.enum8, access="rw", is_manufacturer_specific=True) adaptation_run_status = ZCLAttributeDef(id=0x404D, type=t.bitmap8, access="rp", is_manufacturer_specific=True) adaptation_run_settings = ZCLAttributeDef(id=0x404E, type=t.bitmap8, access="rw", is_manufacturer_specific=True) @@ -156,16 +212,6 @@ async def write_attributes(self, attributes, manufacturer=None): return write_res - def add_unsupported_attribute( - self, attr: int | str, inhibit_events: bool = False - ) -> None: - if attr in {8, "pi_heating_demand"}: - print("*!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\n\n\n\n") - print(f"Unsupported: {attr}") - traceback.print_stack() - print("^!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n\n\n\n\n") - return super().add_unsupported_attribute(attr, inhibit_events=inhibit_events) - async def bind(self): """ According to the documentation of Zigbee2MQTT there is a bug in the Danfoss firmware with the time. @@ -176,14 +222,14 @@ async def bind(self): return await super().bind() -class DanfossUserInterfaceCluster(CustomCluster, UserInterface): +class DanfossUserInterfaceCluster(CustomizedStandardCluster, UserInterface): """Danfoss cluster for standard and proprietary danfoss attributes""" class AttributeDefs(UserInterface.AttributeDefs): viewing_direction = ZCLAttributeDef(id=0x4000, type=t.enum8, access="rpw", is_manufacturer_specific=True) -class DanfossDiagnosticCluster(CustomCluster, Diagnostic): +class DanfossDiagnosticCluster(CustomizedStandardCluster, Diagnostic): """Danfoss cluster for standard and proprietary danfoss attributes""" class AttributeDefs(Diagnostic.AttributeDefs): @@ -191,13 +237,16 @@ class AttributeDefs(Diagnostic.AttributeDefs): wake_time_avg = ZCLAttributeDef(id=0x4001, type=t.uint32_t, access="rp", is_manufacturer_specific=True) wake_time_max_duration = ZCLAttributeDef(id=0x4002, type=t.uint32_t, access="rp", is_manufacturer_specific=True) wake_time_min_duration = ZCLAttributeDef(id=0x4003, type=t.uint32_t, access="rp", is_manufacturer_specific=True) - sleep_postponed_count_avg = ZCLAttributeDef(id=0x4004, type=t.uint32_t, access="rp", is_manufacturer_specific=True) - sleep_postponed_count_max = ZCLAttributeDef(id=0x4005, type=t.uint32_t, access="rp", is_manufacturer_specific=True) - sleep_postponed_count_min = ZCLAttributeDef(id=0x4006, type=t.uint32_t, access="rp", is_manufacturer_specific=True) + sleep_postponed_count_avg = ZCLAttributeDef(id=0x4004, type=t.uint32_t, access="rp", + is_manufacturer_specific=True) + sleep_postponed_count_max = ZCLAttributeDef(id=0x4005, type=t.uint32_t, access="rp", + is_manufacturer_specific=True) + sleep_postponed_count_min = ZCLAttributeDef(id=0x4006, type=t.uint32_t, access="rp", + is_manufacturer_specific=True) motor_step_counter = ZCLAttributeDef(id=0x4010, type=t.uint32_t, access="rp", is_manufacturer_specific=True) -class DanfossTimeCluster(CustomCluster, Time): +class DanfossTimeCluster(CustomizedStandardCluster, Time): """Danfoss cluster for fixing the time.""" async def write_time(self): @@ -206,10 +255,10 @@ async def write_time(self): time_zone = (datetime.fromtimestamp(86400) - datetime.utcfromtimestamp(86400)).total_seconds() - res = await self.write_attributes({"time": current_time, - "time_status": 0b00000010, # only bit 1 can be written - "time_zone": time_zone - }) + await self.write_attributes({"time": current_time, + "time_status": 0b00000010, # only bit 1 can be written + "time_zone": time_zone + }) async def bind(self): """ From 30897311408e09df54d9ee98ac357c181683f7e4 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Mon, 2 Oct 2023 07:38:57 +0200 Subject: [PATCH 29/79] style --- zhaquirks/danfoss/thermostat.py | 213 +++++++++++++++++++++----------- 1 file changed, 143 insertions(+), 70 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 8023a4bdf1..83f369bade 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -26,7 +26,6 @@ 0x0204 - 0x0000: Writing doesn't seem to do anything """ -import traceback from datetime import datetime @@ -34,7 +33,6 @@ from zigpy.quirks import CustomCluster, CustomDevice import zigpy.types as t from zigpy.types import uint16_t -from zigpy.zcl import foundation from zigpy.zcl.clusters.general import ( Basic, Identify, @@ -45,7 +43,7 @@ ) from zigpy.zcl.clusters.homeautomation import Diagnostic from zigpy.zcl.clusters.hvac import Thermostat, UserInterface -from zigpy.zcl.foundation import ZCLCommandDef, ZCLAttributeDef +from zigpy.zcl.foundation import ZCLAttributeDef, ZCLCommandDef from zhaquirks.const import ( DEVICE_TYPE, @@ -76,8 +74,10 @@ class CustomizedStandardCluster(CustomCluster): """Danfoss customized standard clusters by adding custom attributes Danfoss doesn't allow standard attributes when manufacturer specific is requested - Therefore this subclass separates manufacturer specific and standard attributes before - Zigbee commands allowing manufacturer specific to be passed""" + Therefore, this subclass separates manufacturer specific and standard attributes before + Zigbee commands allowing manufacturer specific to be passed + """ + @staticmethod def combine_results(result_a, result_b): return [[*result_a[0], *result_b[0]], [*result_a[1:], *result_b[1:]]] @@ -85,34 +85,48 @@ def combine_results(result_a, result_b): async def _configure_reporting( self, config_records, - *args, **kwargs, + *args, + **kwargs, ): """Configure reporting ZCL foundation command.""" - config_records_manufacturer_specific = [e for e in config_records if self.attributes[e.attrid].is_manufacturer_specific] - config_records_standard = [e for e in config_records if not self.attributes[e.attrid].is_manufacturer_specific] + config_records_manufacturer_specific = [ + e + for e in config_records + if self.attributes[e.attrid].is_manufacturer_specific + ] + config_records_standard = [ + e + for e in config_records + if not self.attributes[e.attrid].is_manufacturer_specific + ] result_a = await super()._configure_reporting( config_records_manufacturer_specific, - *args, **kwargs, + *args, + **kwargs, ) result_b = await super()._configure_reporting( config_records_standard, - *args, **kwargs, + *args, + **kwargs, ) return self.combine_results(result_a, result_b) - async def _read_attributes( # type:ignore[override] + async def _read_attributes( self, - attribute_ids: list[t.uint16_t], + attribute_ids, *args, - manufacturer: int | t.uint16_t | None = None, **kwargs, ): """Read attributes ZCL foundation command.""" - attribute_ids_manufacturer_specific = [e for e in attribute_ids if self.attributes[e].is_manufacturer_specific] - attribute_ids_standard = [e for e in attribute_ids if not self.attributes[e].is_manufacturer_specific] + attribute_ids_manufacturer_specific = [ + e for e in attribute_ids if self.attributes[e].is_manufacturer_specific + ] + attribute_ids_standard = [ + e for e in attribute_ids if not self.attributes[e].is_manufacturer_specific + ] result_a = await super()._read_attributes( attribute_ids_manufacturer_specific, *args, **kwargs @@ -128,14 +142,15 @@ class DanfossThermostatCluster(CustomizedStandardCluster, Thermostat): """Danfoss cluster for standard and proprietary danfoss attributes""" class ServerCommandDefs(Thermostat.ServerCommandDefs): - setpoint_command = ZCLCommandDef(id=0x40, - # Types - # 0: Schedule (relatively slow) - # 1: User Interaction (aggressive change) - # 2: Preheat (invisible to user) - schema={"type": t.enum8, "heating_setpoint": t.int16s}, - is_manufacturer_specific=True, - ) + setpoint_command = ZCLCommandDef( + id=0x40, + # Types + # 0: Schedule (relatively slow) + # 1: User Interaction (aggressive change) + # 2: Preheat (invisible to user) + schema={"type": t.enum8, "heating_setpoint": t.int16s}, + is_manufacturer_specific=True, + ) # for synchronizing multiple TRVs preheating preheat_command = ZCLCommandDef( @@ -146,32 +161,72 @@ class ServerCommandDefs(Thermostat.ServerCommandDefs): ) class AttributeDefs(Thermostat.AttributeDefs): - open_window_detection = ZCLAttributeDef(id=0x4000, type=t.enum8, access="rp", is_manufacturer_specific=True) - external_open_window_detected = ZCLAttributeDef(id=0x4003, type=t.Bool, access="rpw", - is_manufacturer_specific=True) - window_open_feature = ZCLAttributeDef(id=0x4051, type=t.Bool, access="rpw", is_manufacturer_specific=True) - exercise_day_of_week = ZCLAttributeDef(id=0x4010, type=t.enum8, access="rpw", is_manufacturer_specific=True) - exercise_trigger_time = ZCLAttributeDef(id=0x4011, type=t.uint16_t, access="rpw", is_manufacturer_specific=True) - mounting_mode_active = ZCLAttributeDef(id=0x4012, type=t.Bool, access="rp", is_manufacturer_specific=True) - mounting_mode_control = ZCLAttributeDef(id=0x4013, type=t.Bool, access="rpw", is_manufacturer_specific=True) - orientation = ZCLAttributeDef(id=0x4014, type=t.enum8, access="rpw", is_manufacturer_specific=True) - external_measured_room_sensor = ZCLAttributeDef(id=0x4015, type=t.int16s, access="rpw", - is_manufacturer_specific=True) - radiator_covered = ZCLAttributeDef(id=0x4016, type=t.Bool, access="rpw", is_manufacturer_specific=True) - heat_available = ZCLAttributeDef(id=0x4030, type=t.Bool, access="rpw", is_manufacturer_specific=True) - heat_required = ZCLAttributeDef(id=0x4031, type=t.Bool, access="rp", is_manufacturer_specific=True) - load_balancing_enable = ZCLAttributeDef(id=0x4032, type=t.Bool, access="rpw", is_manufacturer_specific=True) - load_room_mean = ZCLAttributeDef(id=0x4040, type=t.int16s, access="rpw", is_manufacturer_specific=True) - load_estimate = ZCLAttributeDef(id=0x404A, type=t.int16s, access="rp", is_manufacturer_specific=True) - control_algorithm_scale_factor = ZCLAttributeDef(id=0x4020, type=t.uint8_t, access="rpw", - is_manufacturer_specific=True) - regulation_setpoint_offset = ZCLAttributeDef(id=0x404B, type=t.int8s, access="rpw", - is_manufacturer_specific=True) - adaptation_run_control = ZCLAttributeDef(id=0x404C, type=t.enum8, access="rw", is_manufacturer_specific=True) - adaptation_run_status = ZCLAttributeDef(id=0x404D, type=t.bitmap8, access="rp", is_manufacturer_specific=True) - adaptation_run_settings = ZCLAttributeDef(id=0x404E, type=t.bitmap8, access="rw", is_manufacturer_specific=True) - preheat_status = ZCLAttributeDef(id=0x404F, type=t.Bool, access="rp", is_manufacturer_specific=True) - preheat_time = ZCLAttributeDef(id=0x4050, type=t.uint32_t, access="rp", is_manufacturer_specific=True) + open_window_detection = ZCLAttributeDef( + id=0x4000, type=t.enum8, access="rp", is_manufacturer_specific=True + ) + external_open_window_detected = ZCLAttributeDef( + id=0x4003, type=t.Bool, access="rpw", is_manufacturer_specific=True + ) + window_open_feature = ZCLAttributeDef( + id=0x4051, type=t.Bool, access="rpw", is_manufacturer_specific=True + ) + exercise_day_of_week = ZCLAttributeDef( + id=0x4010, type=t.enum8, access="rpw", is_manufacturer_specific=True + ) + exercise_trigger_time = ZCLAttributeDef( + id=0x4011, type=t.uint16_t, access="rpw", is_manufacturer_specific=True + ) + mounting_mode_active = ZCLAttributeDef( + id=0x4012, type=t.Bool, access="rp", is_manufacturer_specific=True + ) + mounting_mode_control = ZCLAttributeDef( + id=0x4013, type=t.Bool, access="rpw", is_manufacturer_specific=True + ) + orientation = ZCLAttributeDef( + id=0x4014, type=t.enum8, access="rpw", is_manufacturer_specific=True + ) + external_measured_room_sensor = ZCLAttributeDef( + id=0x4015, type=t.int16s, access="rpw", is_manufacturer_specific=True + ) + radiator_covered = ZCLAttributeDef( + id=0x4016, type=t.Bool, access="rpw", is_manufacturer_specific=True + ) + heat_available = ZCLAttributeDef( + id=0x4030, type=t.Bool, access="rpw", is_manufacturer_specific=True + ) + heat_required = ZCLAttributeDef( + id=0x4031, type=t.Bool, access="rp", is_manufacturer_specific=True + ) + load_balancing_enable = ZCLAttributeDef( + id=0x4032, type=t.Bool, access="rpw", is_manufacturer_specific=True + ) + load_room_mean = ZCLAttributeDef( + id=0x4040, type=t.int16s, access="rpw", is_manufacturer_specific=True + ) + load_estimate = ZCLAttributeDef( + id=0x404A, type=t.int16s, access="rp", is_manufacturer_specific=True + ) + control_algorithm_scale_factor = ZCLAttributeDef( + id=0x4020, type=t.uint8_t, access="rpw", is_manufacturer_specific=True + ) + regulation_setpoint_offset = ZCLAttributeDef( + id=0x404B, type=t.int8s, access="rpw", is_manufacturer_specific=True + ) + adaptation_run_control = ZCLAttributeDef( + id=0x404C, type=t.enum8, access="rw", is_manufacturer_specific=True + ) + adaptation_run_status = ZCLAttributeDef( + id=0x404D, type=t.bitmap8, access="rp", is_manufacturer_specific=True + ) + adaptation_run_settings = ZCLAttributeDef( + id=0x404E, type=t.bitmap8, access="rw", is_manufacturer_specific=True + ) + preheat_status = ZCLAttributeDef( + id=0x404F, type=t.Bool, access="rp", is_manufacturer_specific=True + ) + preheat_time = ZCLAttributeDef( + id=0x4050, type=t.uint32_t, access="rp", is_manufacturer_specific=True + ) async def write_attributes(self, attributes, manufacturer=None): """There are 2 types of setpoint changes: @@ -213,8 +268,7 @@ async def write_attributes(self, attributes, manufacturer=None): return write_res async def bind(self): - """ - According to the documentation of Zigbee2MQTT there is a bug in the Danfoss firmware with the time. + """According to the documentation of Zigbee2MQTT there is a bug in the Danfoss firmware with the time. It doesn't request it, so it has to be fed the correct time. """ await self.endpoint.time.write_time() @@ -226,24 +280,39 @@ class DanfossUserInterfaceCluster(CustomizedStandardCluster, UserInterface): """Danfoss cluster for standard and proprietary danfoss attributes""" class AttributeDefs(UserInterface.AttributeDefs): - viewing_direction = ZCLAttributeDef(id=0x4000, type=t.enum8, access="rpw", is_manufacturer_specific=True) + viewing_direction = ZCLAttributeDef( + id=0x4000, type=t.enum8, access="rpw", is_manufacturer_specific=True + ) class DanfossDiagnosticCluster(CustomizedStandardCluster, Diagnostic): """Danfoss cluster for standard and proprietary danfoss attributes""" class AttributeDefs(Diagnostic.AttributeDefs): - sw_error_code = ZCLAttributeDef(id=0x4000, type=t.bitmap16, access="rp", is_manufacturer_specific=True) - wake_time_avg = ZCLAttributeDef(id=0x4001, type=t.uint32_t, access="rp", is_manufacturer_specific=True) - wake_time_max_duration = ZCLAttributeDef(id=0x4002, type=t.uint32_t, access="rp", is_manufacturer_specific=True) - wake_time_min_duration = ZCLAttributeDef(id=0x4003, type=t.uint32_t, access="rp", is_manufacturer_specific=True) - sleep_postponed_count_avg = ZCLAttributeDef(id=0x4004, type=t.uint32_t, access="rp", - is_manufacturer_specific=True) - sleep_postponed_count_max = ZCLAttributeDef(id=0x4005, type=t.uint32_t, access="rp", - is_manufacturer_specific=True) - sleep_postponed_count_min = ZCLAttributeDef(id=0x4006, type=t.uint32_t, access="rp", - is_manufacturer_specific=True) - motor_step_counter = ZCLAttributeDef(id=0x4010, type=t.uint32_t, access="rp", is_manufacturer_specific=True) + sw_error_code = ZCLAttributeDef( + id=0x4000, type=t.bitmap16, access="rp", is_manufacturer_specific=True + ) + wake_time_avg = ZCLAttributeDef( + id=0x4001, type=t.uint32_t, access="rp", is_manufacturer_specific=True + ) + wake_time_max_duration = ZCLAttributeDef( + id=0x4002, type=t.uint32_t, access="rp", is_manufacturer_specific=True + ) + wake_time_min_duration = ZCLAttributeDef( + id=0x4003, type=t.uint32_t, access="rp", is_manufacturer_specific=True + ) + sleep_postponed_count_avg = ZCLAttributeDef( + id=0x4004, type=t.uint32_t, access="rp", is_manufacturer_specific=True + ) + sleep_postponed_count_max = ZCLAttributeDef( + id=0x4005, type=t.uint32_t, access="rp", is_manufacturer_specific=True + ) + sleep_postponed_count_min = ZCLAttributeDef( + id=0x4006, type=t.uint32_t, access="rp", is_manufacturer_specific=True + ) + motor_step_counter = ZCLAttributeDef( + id=0x4010, type=t.uint32_t, access="rp", is_manufacturer_specific=True + ) class DanfossTimeCluster(CustomizedStandardCluster, Time): @@ -253,16 +322,20 @@ async def write_time(self): epoch = datetime(2000, 1, 1, 0, 0, 0, 0) current_time = (datetime.utcnow() - epoch).total_seconds() - time_zone = (datetime.fromtimestamp(86400) - datetime.utcfromtimestamp(86400)).total_seconds() + time_zone = ( + datetime.fromtimestamp(86400) - datetime.utcfromtimestamp(86400) + ).total_seconds() - await self.write_attributes({"time": current_time, - "time_status": 0b00000010, # only bit 1 can be written - "time_zone": time_zone - }) + await self.write_attributes( + { + "time": current_time, + "time_status": 0b00000010, # only bit 1 can be written + "time_zone": time_zone, + } + ) async def bind(self): - """ - According to the documentation of Zigbee2MQTT there is a bug in the Danfoss firmware with the time. + """According to the documentation of Zigbee2MQTT there is a bug in the Danfoss firmware with the time. It doesn't request it, so it has to be fed the correct time. """ result = await super().bind() From b81c8f8c651de7efca9b6233a264957dcf27f032 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Mon, 2 Oct 2023 08:33:15 +0200 Subject: [PATCH 30/79] add tests --- tests/test_danfoss.py | 38 +++++++++++++++++++++++++++++++++ zhaquirks/danfoss/thermostat.py | 18 ++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/tests/test_danfoss.py b/tests/test_danfoss.py index cd57dfd92d..c136cf5ff5 100644 --- a/tests/test_danfoss.py +++ b/tests/test_danfoss.py @@ -1,7 +1,9 @@ """Tests the Danfoss quirk (all tests were written for the Popp eT093WRO)""" +from datetime import datetime from unittest import mock from zigpy.zcl import foundation +from zigpy.zcl.clusters.general import Time from zigpy.zcl.clusters.hvac import Thermostat from zigpy.zcl.foundation import WriteAttributesStatusRecord @@ -41,6 +43,42 @@ def test_popp_signature(assert_signature_matches_quirk): ) +async def test_danfoss_time_bind(zigpy_device_from_quirk): + device = zigpy_device_from_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat) + + danfoss_time_cluster = device.endpoints[1].in_clusters[Time.cluster_id] + + def mock_write(attributes, manufacturer=None): + records = [ + WriteAttributesStatusRecord(foundation.Status.SUCCESS) + for attr in attributes + ] + return [records, []] + + def mock_wildcard(*args, **kwargs): + return + + patch_danfoss_trv_write = mock.patch.object( + danfoss_time_cluster, + "_write_attributes", + mock.AsyncMock(side_effect=mock_write), + ) + + patch_danfoss_trv_bind = mock.patch.object( + Time, + "bind", + mock.AsyncMock(side_effect=mock_wildcard), + ) + + with patch_danfoss_trv_bind: + with patch_danfoss_trv_write: + await danfoss_time_cluster.bind() + + assert 0x0000 in danfoss_time_cluster._attr_cache + assert 0x0001 in danfoss_time_cluster._attr_cache + assert 0x0002 in danfoss_time_cluster._attr_cache + + async def test_danfoss_thermostat_write_attributes(zigpy_device_from_quirk): device = zigpy_device_from_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 83f369bade..42a5233b51 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -70,6 +70,17 @@ SYSTEM_MODE_THERM_OFF_VAL = 0x00 +class DanfossOperationModeEnum(t.bitmap8): + """Nonstandard implementation of Programming Operation Mode from Danfoss. + The official specification still works: 0x0 or 0x1, but Danfoss added a preheat bit + """ + + Manual = 0b00000000 + Schedule = 0b00000001 + Manual_Preheat = 0b00000010 + Schedule_Preheat = 0b00000011 + + class CustomizedStandardCluster(CustomCluster): """Danfoss customized standard clusters by adding custom attributes Danfoss doesn't allow standard attributes when manufacturer specific is requested @@ -228,6 +239,13 @@ class AttributeDefs(Thermostat.AttributeDefs): id=0x4050, type=t.uint32_t, access="rp", is_manufacturer_specific=True ) + programing_oper_mode = ZCLAttributeDef( + id=0x0025, + type=DanfossOperationModeEnum, + access="rpw", + is_manufacturer_specific=True, + ) # Danfoss deviated from the spec + async def write_attributes(self, attributes, manufacturer=None): """There are 2 types of setpoint changes: Fast and Slow From f19e73941c50d60e262e0e538d1b592652786e4f Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Mon, 2 Oct 2023 08:46:07 +0200 Subject: [PATCH 31/79] remove some lines --- tests/test_danfoss.py | 1 - zhaquirks/danfoss/thermostat.py | 14 ++------------ 2 files changed, 2 insertions(+), 13 deletions(-) diff --git a/tests/test_danfoss.py b/tests/test_danfoss.py index c136cf5ff5..e813f9969c 100644 --- a/tests/test_danfoss.py +++ b/tests/test_danfoss.py @@ -1,5 +1,4 @@ """Tests the Danfoss quirk (all tests were written for the Popp eT093WRO)""" -from datetime import datetime from unittest import mock from zigpy.zcl import foundation diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 42a5233b51..21f8235a13 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -93,12 +93,7 @@ class CustomizedStandardCluster(CustomCluster): def combine_results(result_a, result_b): return [[*result_a[0], *result_b[0]], [*result_a[1:], *result_b[1:]]] - async def _configure_reporting( - self, - config_records, - *args, - **kwargs, - ): + async def _configure_reporting(self, config_records, *args, **kwargs): """Configure reporting ZCL foundation command.""" config_records_manufacturer_specific = [ e @@ -124,12 +119,7 @@ async def _configure_reporting( return self.combine_results(result_a, result_b) - async def _read_attributes( - self, - attribute_ids, - *args, - **kwargs, - ): + async def _read_attributes(self, attribute_ids, *args, **kwargs): """Read attributes ZCL foundation command.""" attribute_ids_manufacturer_specific = [ From 618621679cf02758ee52522315617b680ebc0b9b Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Mon, 2 Oct 2023 10:08:40 +0200 Subject: [PATCH 32/79] add tests --- tests/test_danfoss.py | 68 ++++++++++++++++++++++++++++++++- zhaquirks/danfoss/thermostat.py | 63 +++++++++++++++--------------- 2 files changed, 98 insertions(+), 33 deletions(-) diff --git a/tests/test_danfoss.py b/tests/test_danfoss.py index e813f9969c..4e80dfb890 100644 --- a/tests/test_danfoss.py +++ b/tests/test_danfoss.py @@ -1,12 +1,14 @@ """Tests the Danfoss quirk (all tests were written for the Popp eT093WRO)""" from unittest import mock +from zigpy.quirks import CustomCluster from zigpy.zcl import foundation from zigpy.zcl.clusters.general import Time from zigpy.zcl.clusters.hvac import Thermostat -from zigpy.zcl.foundation import WriteAttributesStatusRecord +from zigpy.zcl.foundation import WriteAttributesStatusRecord, ZCLAttributeDef import zhaquirks +from zhaquirks.danfoss.thermostat import CustomizedStandardCluster zhaquirks.setup() @@ -146,3 +148,67 @@ def mock_setpoint(oper, sett, manufacturer=None): assert operation == 0x01 assert setting == 5 + + +async def test_customized_standardcluster(zigpy_device_from_quirk): + device = zigpy_device_from_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat) + + danfoss_thermostat_cluster = device.endpoints[1].in_clusters[Thermostat.cluster_id] + + assert CustomizedStandardCluster.combine_results([[4545], [5433]], [[345]]) == [[4545, 345], [5433]] + assert CustomizedStandardCluster.combine_results([[4545], [5433]], [[345], [45355]]) == [[4545, 345], [5433, 45355]] + + mock_attributes = {656: ZCLAttributeDef(is_manufacturer_specific=True), + 56454: ZCLAttributeDef(is_manufacturer_specific=False)} + + danfoss_thermostat_cluster.attributes = mock_attributes + + reports = None + + def mock_configure_reporting(reps, *args, **kwargs): + nonlocal reports + if mock_attributes[reps[0].attrid].is_manufacturer_specific: + reports = reps + + return [[545], [4545]] + + # data is written to trv + patch_danfoss_configure_reporting = mock.patch.object( + CustomCluster, + "_configure_reporting", + mock.AsyncMock(side_effect=mock_configure_reporting), + ) + + with patch_danfoss_configure_reporting: + one = foundation.AttributeReportingConfig() + one.direction = True + one.timeout = 4 + one.attrid = 56454 + + two = foundation.AttributeReportingConfig() + two.direction = True + two.timeout = 4 + two.attrid = 656 + await danfoss_thermostat_cluster._configure_reporting([one, two]) + assert reports == [two] + + + reports = None + + def mock_read_attributes(attrs, *args, **kwargs): + nonlocal reports + if mock_attributes[attrs[0]].is_manufacturer_specific: + reports = attrs + + return [[545], [4545]] + + # data is written to trv + patch_danfoss_read_attributes = mock.patch.object( + CustomCluster, + "_read_attributes", + mock.AsyncMock(side_effect=mock_read_attributes), + ) + + with patch_danfoss_read_attributes: + await danfoss_thermostat_cluster._read_attributes([56454, 656]) + assert reports == [656] \ No newline at end of file diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 21f8235a13..030db49677 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -90,53 +90,52 @@ class CustomizedStandardCluster(CustomCluster): """ @staticmethod - def combine_results(result_a, result_b): - return [[*result_a[0], *result_b[0]], [*result_a[1:], *result_b[1:]]] - - async def _configure_reporting(self, config_records, *args, **kwargs): + def combine_results(*result_lists): + result_global = [[], []] + for result in result_lists: + if len(result) == 1: + result_global[0].extend(result[0]) + elif len(result) == 2: + result_global[0].extend(result[0]) + result_global[1].extend(result[1]) + + return result_global + + async def _configure_reporting(self, records, *args, **kwargs): """Configure reporting ZCL foundation command.""" - config_records_manufacturer_specific = [ - e - for e in config_records - if self.attributes[e.attrid].is_manufacturer_specific + records_specific = [ + e for e in records if self.attributes[e.attrid].is_manufacturer_specific ] - config_records_standard = [ - e - for e in config_records - if not self.attributes[e.attrid].is_manufacturer_specific + records_standard = [ + e for e in records if not self.attributes[e.attrid].is_manufacturer_specific ] - result_a = await super()._configure_reporting( - config_records_manufacturer_specific, - *args, - **kwargs, + result_specific = await super()._configure_reporting( + records_specific, *args, **kwargs ) - result_b = await super()._configure_reporting( - config_records_standard, - *args, - **kwargs, + result_standard = await super()._configure_reporting( + records_standard, *args, **kwargs ) - return self.combine_results(result_a, result_b) + return self.combine_results(result_specific, result_standard) - async def _read_attributes(self, attribute_ids, *args, **kwargs): + async def _read_attributes(self, attr_ids, *args, **kwargs): """Read attributes ZCL foundation command.""" - attribute_ids_manufacturer_specific = [ - e for e in attribute_ids if self.attributes[e].is_manufacturer_specific + attr_ids_specific = [ + e for e in attr_ids if self.attributes[e].is_manufacturer_specific ] - attribute_ids_standard = [ - e for e in attribute_ids if not self.attributes[e].is_manufacturer_specific + attr_ids_standard = [ + e for e in attr_ids if not self.attributes[e].is_manufacturer_specific ] - result_a = await super()._read_attributes( - attribute_ids_manufacturer_specific, *args, **kwargs + result_specific = await super()._read_attributes( + attr_ids_specific, *args, **kwargs ) - - result_b = await super()._read_attributes( - attribute_ids_standard, *args, **kwargs + result_standard = await super()._read_attributes( + attr_ids_standard, *args, **kwargs ) - return self.combine_results(result_a, result_b) + return self.combine_results(result_specific, result_standard) class DanfossThermostatCluster(CustomizedStandardCluster, Thermostat): From 7e8c74f8a8f9c2716fb6b160b14f0e0a7d237cd7 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Mon, 2 Oct 2023 10:11:43 +0200 Subject: [PATCH 33/79] styl --- tests/test_danfoss.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/test_danfoss.py b/tests/test_danfoss.py index 4e80dfb890..3bebeac2b0 100644 --- a/tests/test_danfoss.py +++ b/tests/test_danfoss.py @@ -155,11 +155,18 @@ async def test_customized_standardcluster(zigpy_device_from_quirk): danfoss_thermostat_cluster = device.endpoints[1].in_clusters[Thermostat.cluster_id] - assert CustomizedStandardCluster.combine_results([[4545], [5433]], [[345]]) == [[4545, 345], [5433]] - assert CustomizedStandardCluster.combine_results([[4545], [5433]], [[345], [45355]]) == [[4545, 345], [5433, 45355]] - - mock_attributes = {656: ZCLAttributeDef(is_manufacturer_specific=True), - 56454: ZCLAttributeDef(is_manufacturer_specific=False)} + assert CustomizedStandardCluster.combine_results([[4545], [5433]], [[345]]) == [ + [4545, 345], + [5433], + ] + assert CustomizedStandardCluster.combine_results( + [[4545], [5433]], [[345], [45355]] + ) == [[4545, 345], [5433, 45355]] + + mock_attributes = { + 656: ZCLAttributeDef(is_manufacturer_specific=True), + 56454: ZCLAttributeDef(is_manufacturer_specific=False), + } danfoss_thermostat_cluster.attributes = mock_attributes @@ -192,7 +199,6 @@ def mock_configure_reporting(reps, *args, **kwargs): await danfoss_thermostat_cluster._configure_reporting([one, two]) assert reports == [two] - reports = None def mock_read_attributes(attrs, *args, **kwargs): @@ -211,4 +217,4 @@ def mock_read_attributes(attrs, *args, **kwargs): with patch_danfoss_read_attributes: await danfoss_thermostat_cluster._read_attributes([56454, 656]) - assert reports == [656] \ No newline at end of file + assert reports == [656] From 9470e404ed3692cd9f0a16e30a477d707e1341b2 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Mon, 2 Oct 2023 19:16:11 +0200 Subject: [PATCH 34/79] disallow systemmode=off --- zhaquirks/danfoss/thermostat.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 030db49677..d65a67dfe9 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -68,6 +68,7 @@ SETPOINT_COMMAND_AGGRESSIVE_VAL = 0x01 SYSTEM_MODE_THERM_OFF_VAL = 0x00 +SYSTEM_MODE_THERM_ON_VAL = 0x04 class DanfossOperationModeEnum(t.bitmap8): @@ -259,6 +260,9 @@ async def write_attributes(self, attributes, manufacturer=None): fast_setpoint_change = self._attr_cache[MIN_HEAT_SETPOINT_LIMIT_THERM_ID] attributes[OCCUPIED_HEATING_SETPOINT_NAME] = fast_setpoint_change + # Danfoss doesn't accept off, therefore set to On + attributes[SYSTEM_MODE_NAME] = SYSTEM_MODE_THERM_ON_VAL + # attributes cannot be empty, because write_res cannot be empty, but it can contain unrequested items write_res = await super().write_attributes( attributes, manufacturer=manufacturer From 188dc1747fba7994ce8716c40a8283fed6439441 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Mon, 2 Oct 2023 19:36:19 +0200 Subject: [PATCH 35/79] fix test --- tests/test_danfoss.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_danfoss.py b/tests/test_danfoss.py index 3bebeac2b0..bf571295cc 100644 --- a/tests/test_danfoss.py +++ b/tests/test_danfoss.py @@ -141,7 +141,7 @@ def mock_setpoint(oper, sett, manufacturer=None): ) assert success assert not fail - assert danfoss_thermostat_cluster._attr_cache[0x001C] == 0x00 + assert danfoss_thermostat_cluster._attr_cache[0x001C] == 0x04 # setpoint to min_limit, when system_mode to off assert danfoss_thermostat_cluster._attr_cache[0x0012] == 5 From 337ec1ed86c53256dbd05b2b1b0803cb80113610 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Thu, 19 Oct 2023 18:29:37 +0200 Subject: [PATCH 36/79] remove non-standard programing_oper_mode --- zhaquirks/danfoss/thermostat.py | 7 ------- zhaquirks/xiaomi/aqara/vibration_aq1.py | 2 ++ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index d65a67dfe9..79eedd104a 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -229,13 +229,6 @@ class AttributeDefs(Thermostat.AttributeDefs): id=0x4050, type=t.uint32_t, access="rp", is_manufacturer_specific=True ) - programing_oper_mode = ZCLAttributeDef( - id=0x0025, - type=DanfossOperationModeEnum, - access="rpw", - is_manufacturer_specific=True, - ) # Danfoss deviated from the spec - async def write_attributes(self, attributes, manufacturer=None): """There are 2 types of setpoint changes: Fast and Slow diff --git a/zhaquirks/xiaomi/aqara/vibration_aq1.py b/zhaquirks/xiaomi/aqara/vibration_aq1.py index b46a859ec4..044477e6b9 100644 --- a/zhaquirks/xiaomi/aqara/vibration_aq1.py +++ b/zhaquirks/xiaomi/aqara/vibration_aq1.py @@ -69,6 +69,8 @@ class VibrationAQ1(XiaomiQuickInitDevice): """Xiaomi aqara smart motion sensor device.""" + quirk_id = "XiaomiVibrationAQ1" + manufacturer_id_override = 0x115F def __init__(self, *args, **kwargs): From c48cd085959bbf7498d8f5817e97da5ae6cbc307 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Thu, 19 Oct 2023 19:15:28 +0200 Subject: [PATCH 37/79] align with better cluster documentation --- zhaquirks/danfoss/thermostat.py | 74 +++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 32 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 79eedd104a..588ab21ab4 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -166,56 +166,56 @@ class AttributeDefs(Thermostat.AttributeDefs): id=0x4000, type=t.enum8, access="rp", is_manufacturer_specific=True ) external_open_window_detected = ZCLAttributeDef( - id=0x4003, type=t.Bool, access="rpw", is_manufacturer_specific=True - ) + id=0x4003, type=t.Bool, access="rw", is_manufacturer_specific=True + ) # non-configurable reporting window_open_feature = ZCLAttributeDef( - id=0x4051, type=t.Bool, access="rpw", is_manufacturer_specific=True - ) + id=0x4051, type=t.Bool, access="rw", is_manufacturer_specific=True + ) # non-configurable reporting exercise_day_of_week = ZCLAttributeDef( - id=0x4010, type=t.enum8, access="rpw", is_manufacturer_specific=True + id=0x4010, type=t.enum8, access="rw", is_manufacturer_specific=True ) exercise_trigger_time = ZCLAttributeDef( - id=0x4011, type=t.uint16_t, access="rpw", is_manufacturer_specific=True + id=0x4011, type=t.uint16_t, access="rw", is_manufacturer_specific=True ) mounting_mode_active = ZCLAttributeDef( id=0x4012, type=t.Bool, access="rp", is_manufacturer_specific=True ) mounting_mode_control = ZCLAttributeDef( - id=0x4013, type=t.Bool, access="rpw", is_manufacturer_specific=True - ) + id=0x4013, type=t.Bool, access="rw", is_manufacturer_specific=True + ) # non-configurable reporting orientation = ZCLAttributeDef( - id=0x4014, type=t.enum8, access="rpw", is_manufacturer_specific=True - ) + id=0x4014, type=t.Bool, access="rw", is_manufacturer_specific=True + ) # non-configurable reporting external_measured_room_sensor = ZCLAttributeDef( - id=0x4015, type=t.int16s, access="rpw", is_manufacturer_specific=True + id=0x4015, type=t.int16s, access="rw", is_manufacturer_specific=True ) radiator_covered = ZCLAttributeDef( - id=0x4016, type=t.Bool, access="rpw", is_manufacturer_specific=True - ) + id=0x4016, type=t.Bool, access="rw", is_manufacturer_specific=True + ) # non-configurable reporting heat_available = ZCLAttributeDef( - id=0x4030, type=t.Bool, access="rpw", is_manufacturer_specific=True - ) + id=0x4030, type=t.Bool, access="rw", is_manufacturer_specific=True + ) # non-configurable reporting heat_required = ZCLAttributeDef( id=0x4031, type=t.Bool, access="rp", is_manufacturer_specific=True ) load_balancing_enable = ZCLAttributeDef( - id=0x4032, type=t.Bool, access="rpw", is_manufacturer_specific=True - ) + id=0x4032, type=t.Bool, access="rw", is_manufacturer_specific=True + ) # non-configurable reporting load_room_mean = ZCLAttributeDef( - id=0x4040, type=t.int16s, access="rpw", is_manufacturer_specific=True - ) + id=0x4040, type=t.int16s, access="w", is_manufacturer_specific=True + ) # non-configurable reporting load_estimate = ZCLAttributeDef( id=0x404A, type=t.int16s, access="rp", is_manufacturer_specific=True ) control_algorithm_scale_factor = ZCLAttributeDef( - id=0x4020, type=t.uint8_t, access="rpw", is_manufacturer_specific=True - ) + id=0x4020, type=t.uint8_t, access="rw", is_manufacturer_specific=True + ) # non-configurable reporting regulation_setpoint_offset = ZCLAttributeDef( - id=0x404B, type=t.int8s, access="rpw", is_manufacturer_specific=True + id=0x404B, type=t.int8s, access="rw", is_manufacturer_specific=True ) adaptation_run_control = ZCLAttributeDef( id=0x404C, type=t.enum8, access="rw", is_manufacturer_specific=True - ) + ) # non-configurable reporting adaptation_run_status = ZCLAttributeDef( id=0x404D, type=t.bitmap8, access="rp", is_manufacturer_specific=True ) @@ -285,8 +285,8 @@ class DanfossUserInterfaceCluster(CustomizedStandardCluster, UserInterface): class AttributeDefs(UserInterface.AttributeDefs): viewing_direction = ZCLAttributeDef( - id=0x4000, type=t.enum8, access="rpw", is_manufacturer_specific=True - ) + id=0x4000, type=t.enum8, access="rw", is_manufacturer_specific=True + ) # non-configurable reporting class DanfossDiagnosticCluster(CustomizedStandardCluster, Diagnostic): @@ -294,30 +294,40 @@ class DanfossDiagnosticCluster(CustomizedStandardCluster, Diagnostic): class AttributeDefs(Diagnostic.AttributeDefs): sw_error_code = ZCLAttributeDef( - id=0x4000, type=t.bitmap16, access="rp", is_manufacturer_specific=True + id=0x4000, type=t.bitmap16, access="rpw", is_manufacturer_specific=True ) wake_time_avg = ZCLAttributeDef( - id=0x4001, type=t.uint32_t, access="rp", is_manufacturer_specific=True + id=0x4001, type=t.uint32_t, access="r", is_manufacturer_specific=True ) wake_time_max_duration = ZCLAttributeDef( - id=0x4002, type=t.uint32_t, access="rp", is_manufacturer_specific=True + id=0x4002, type=t.uint32_t, access="r", is_manufacturer_specific=True ) wake_time_min_duration = ZCLAttributeDef( - id=0x4003, type=t.uint32_t, access="rp", is_manufacturer_specific=True + id=0x4003, type=t.uint32_t, access="r", is_manufacturer_specific=True ) sleep_postponed_count_avg = ZCLAttributeDef( - id=0x4004, type=t.uint32_t, access="rp", is_manufacturer_specific=True + id=0x4004, type=t.uint32_t, access="r", is_manufacturer_specific=True ) sleep_postponed_count_max = ZCLAttributeDef( - id=0x4005, type=t.uint32_t, access="rp", is_manufacturer_specific=True + id=0x4005, type=t.uint32_t, access="r", is_manufacturer_specific=True ) sleep_postponed_count_min = ZCLAttributeDef( - id=0x4006, type=t.uint32_t, access="rp", is_manufacturer_specific=True + id=0x4006, type=t.uint32_t, access="r", is_manufacturer_specific=True ) motor_step_counter = ZCLAttributeDef( id=0x4010, type=t.uint32_t, access="rp", is_manufacturer_specific=True ) + data_logger = ZCLAttributeDef( + id=0x4020, type=t.LimitedLVBytes(50), access="rpw", is_manufacturer_specific=True + ) + control_diagnostics = ZCLAttributeDef( + id=0x4021, type=t.LimitedLVBytes(30), access="rp", is_manufacturer_specific=True + ) + control_diagnostics_frequency = ZCLAttributeDef( + id=0x4022, type=t.uint16_t, access="rw", is_manufacturer_specific=True + ) # non-configurable reporting + class DanfossTimeCluster(CustomizedStandardCluster, Time): """Danfoss cluster for fixing the time.""" From b798074c7e6e8f42ad3c725a14f6bc93db1fdcdb Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Thu, 19 Oct 2023 19:23:36 +0200 Subject: [PATCH 38/79] deviate from documentation, because it works anyway --- zhaquirks/danfoss/thermostat.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 588ab21ab4..2468cf6360 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -202,8 +202,8 @@ class AttributeDefs(Thermostat.AttributeDefs): id=0x4032, type=t.Bool, access="rw", is_manufacturer_specific=True ) # non-configurable reporting load_room_mean = ZCLAttributeDef( - id=0x4040, type=t.int16s, access="w", is_manufacturer_specific=True - ) # non-configurable reporting + id=0x4040, type=t.int16s, access="rw", is_manufacturer_specific=True + ) # non-configurable reporting (according to the documentation, you cannot read it, but it works anyway) load_estimate = ZCLAttributeDef( id=0x404A, type=t.int16s, access="rp", is_manufacturer_specific=True ) @@ -319,10 +319,16 @@ class AttributeDefs(Diagnostic.AttributeDefs): ) data_logger = ZCLAttributeDef( - id=0x4020, type=t.LimitedLVBytes(50), access="rpw", is_manufacturer_specific=True + id=0x4020, + type=t.LimitedLVBytes(50), + access="rpw", + is_manufacturer_specific=True, ) control_diagnostics = ZCLAttributeDef( - id=0x4021, type=t.LimitedLVBytes(30), access="rp", is_manufacturer_specific=True + id=0x4021, + type=t.LimitedLVBytes(30), + access="rp", + is_manufacturer_specific=True, ) control_diagnostics_frequency = ZCLAttributeDef( id=0x4022, type=t.uint16_t, access="rw", is_manufacturer_specific=True From 7a8f8e05b761f7a51f8d9c68ea9d690a0d49665e Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Fri, 20 Oct 2023 00:23:55 +0200 Subject: [PATCH 39/79] remove accidentally comitted change --- zhaquirks/xiaomi/aqara/vibration_aq1.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/zhaquirks/xiaomi/aqara/vibration_aq1.py b/zhaquirks/xiaomi/aqara/vibration_aq1.py index 044477e6b9..b46a859ec4 100644 --- a/zhaquirks/xiaomi/aqara/vibration_aq1.py +++ b/zhaquirks/xiaomi/aqara/vibration_aq1.py @@ -69,8 +69,6 @@ class VibrationAQ1(XiaomiQuickInitDevice): """Xiaomi aqara smart motion sensor device.""" - quirk_id = "XiaomiVibrationAQ1" - manufacturer_id_override = 0x115F def __init__(self, *args, **kwargs): From e2d5aae3ba37512efd406981fd729cb010abde55 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Fri, 20 Oct 2023 00:29:58 +0200 Subject: [PATCH 40/79] remove unused enum --- zhaquirks/danfoss/thermostat.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 2468cf6360..832aad5769 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -71,17 +71,6 @@ SYSTEM_MODE_THERM_ON_VAL = 0x04 -class DanfossOperationModeEnum(t.bitmap8): - """Nonstandard implementation of Programming Operation Mode from Danfoss. - The official specification still works: 0x0 or 0x1, but Danfoss added a preheat bit - """ - - Manual = 0b00000000 - Schedule = 0b00000001 - Manual_Preheat = 0b00000010 - Schedule_Preheat = 0b00000011 - - class CustomizedStandardCluster(CustomCluster): """Danfoss customized standard clusters by adding custom attributes Danfoss doesn't allow standard attributes when manufacturer specific is requested From 0102f427ce5e08351b1bb4f7414d310444c81240 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sat, 21 Oct 2023 16:22:27 +0200 Subject: [PATCH 41/79] according to the documentation, this should be a new valid danfoss trv --- zhaquirks/danfoss/thermostat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 832aad5769..10ecffaf79 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -367,6 +367,7 @@ class DanfossThermostat(CustomDevice): (DANFOSS, "eTRV0100"), (DANFOSS, "eTRV0101"), (DANFOSS, "eTRV0103"), + (DANFOSS, "eTRV0120"), (POPP, "eT093WRO"), (POPP, "eT093WRG"), (HIVE, "TRV001"), From 2232b5555b8fba687eb77dcf77a8fd5a828a6fbb Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Tue, 24 Oct 2023 12:07:32 +0200 Subject: [PATCH 42/79] Revert "according to the documentation, this should be a new valid danfoss trv" This reverts commit 0102f427ce5e08351b1bb4f7414d310444c81240. Documentation Lied C C C It i --- zhaquirks/danfoss/thermostat.py | 1 - 1 file changed, 1 deletion(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 10ecffaf79..832aad5769 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -367,7 +367,6 @@ class DanfossThermostat(CustomDevice): (DANFOSS, "eTRV0100"), (DANFOSS, "eTRV0101"), (DANFOSS, "eTRV0103"), - (DANFOSS, "eTRV0120"), (POPP, "eT093WRO"), (POPP, "eT093WRG"), (HIVE, "TRV001"), From bc5568c8bb145cb49e1acf7075c0bbc56a2fc38e Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Thu, 26 Oct 2023 13:08:59 +0200 Subject: [PATCH 43/79] add quirk_id --- zhaquirks/danfoss/thermostat.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 832aad5769..85b9b5a015 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -357,6 +357,8 @@ async def bind(self): class DanfossThermostat(CustomDevice): """DanfossThermostat custom device.""" + quirk_id = "danfoss_thermostat" + manufacturer_code = 0x1246 signature = { From 24edf2171fb717f11679eb504131c606df138820 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Thu, 26 Oct 2023 22:25:25 +0200 Subject: [PATCH 44/79] move definition to quirk_ids.py --- zhaquirks/danfoss/thermostat.py | 3 ++- zhaquirks/quirk_ids.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 85b9b5a015..f3bbd95ad1 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -53,6 +53,7 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) +from zhaquirks.quirk_ids import DANFOSS_THERMOSTAT DANFOSS = "Danfoss" HIVE = DANFOSS @@ -357,7 +358,7 @@ async def bind(self): class DanfossThermostat(CustomDevice): """DanfossThermostat custom device.""" - quirk_id = "danfoss_thermostat" + quirk_id = DANFOSS_THERMOSTAT manufacturer_code = 0x1246 diff --git a/zhaquirks/quirk_ids.py b/zhaquirks/quirk_ids.py index 8306c91b5b..3b69f91921 100644 --- a/zhaquirks/quirk_ids.py +++ b/zhaquirks/quirk_ids.py @@ -3,3 +3,5 @@ # Tuya TUYA_PLUG_ONOFF = "tuya.plug_on_off_attributes" # plugs with configurable attributes on the OnOff cluster TUYA_PLUG_MANUFACTURER = "tuya.plug_manufacturer_attributes" # plugs with configurable attributes on a custom cluster + +DANFOSS_THERMOSTAT = "danfoss.thermostat" \ No newline at end of file From 216527abc27cbdc3753a234e9b1d9bdc6b415641 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Thu, 26 Oct 2023 22:29:37 +0200 Subject: [PATCH 45/79] black --- zhaquirks/quirk_ids.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhaquirks/quirk_ids.py b/zhaquirks/quirk_ids.py index 3b69f91921..a9719f6ccf 100644 --- a/zhaquirks/quirk_ids.py +++ b/zhaquirks/quirk_ids.py @@ -4,4 +4,4 @@ TUYA_PLUG_ONOFF = "tuya.plug_on_off_attributes" # plugs with configurable attributes on the OnOff cluster TUYA_PLUG_MANUFACTURER = "tuya.plug_manufacturer_attributes" # plugs with configurable attributes on a custom cluster -DANFOSS_THERMOSTAT = "danfoss.thermostat" \ No newline at end of file +DANFOSS_THERMOSTAT = "danfoss.thermostat" From 71cbea2ec6873dc45ebd179c5d61b5ceafdaab43 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Fri, 27 Oct 2023 00:57:07 +0200 Subject: [PATCH 46/79] comment --- zhaquirks/quirk_ids.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/zhaquirks/quirk_ids.py b/zhaquirks/quirk_ids.py index a9719f6ccf..98d90214f9 100644 --- a/zhaquirks/quirk_ids.py +++ b/zhaquirks/quirk_ids.py @@ -4,4 +4,5 @@ TUYA_PLUG_ONOFF = "tuya.plug_on_off_attributes" # plugs with configurable attributes on the OnOff cluster TUYA_PLUG_MANUFACTURER = "tuya.plug_manufacturer_attributes" # plugs with configurable attributes on a custom cluster -DANFOSS_THERMOSTAT = "danfoss.thermostat" +# Danfoss +DANFOSS_THERMOSTAT = "danfoss.thermostat" # Thermostatic Radiator Valves based on Danfoss Ally with custom clusters From fc79193988f2304d8c9fa02601b7ecbadd428656 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Fri, 27 Oct 2023 13:35:16 +0200 Subject: [PATCH 47/79] make combine_results more robust --- zhaquirks/danfoss/thermostat.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index f3bbd95ad1..b639e13f45 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -77,20 +77,28 @@ class CustomizedStandardCluster(CustomCluster): Danfoss doesn't allow standard attributes when manufacturer specific is requested Therefore, this subclass separates manufacturer specific and standard attributes before - Zigbee commands allowing manufacturer specific to be passed + Zigbee commands allowing manufacturer specific to be passed for specific attributes, but not for standard attributes """ @staticmethod def combine_results(*result_lists): - result_global = [[], []] + success_global = [] + failure_global = [] for result in result_lists: if len(result) == 1: - result_global[0].extend(result[0]) + success_global.extend(result[0]) elif len(result) == 2: - result_global[0].extend(result[0]) - result_global[1].extend(result[1]) + success_global.extend(result[0]) + failure_global.extend(result[1]) + else: + raise Exception(f"Unexpected result size: {len(result)}") - return result_global + if failure_global: + response = [success_global, failure_global] + else: + response = [success_global] + + return response async def _configure_reporting(self, records, *args, **kwargs): """Configure reporting ZCL foundation command.""" From 73111fc0c2f7485ce719bd80e9c67e23a5ef99d6 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Fri, 27 Oct 2023 14:19:44 +0200 Subject: [PATCH 48/79] reduce code duplication and don't send unnecessary requests --- zhaquirks/danfoss/thermostat.py | 46 +++++++++++++++------------------ 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index b639e13f45..91a8276919 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -28,6 +28,7 @@ from datetime import datetime +from typing import Callable, Any import zigpy.profiles.zha as zha_p from zigpy.quirks import CustomCluster, CustomDevice @@ -100,41 +101,36 @@ def combine_results(*result_lists): return response - async def _configure_reporting(self, records, *args, **kwargs): + async def split_command(self, records: list[Any], func: Callable[[list[Any], ...], Any], extract_attrid: Callable[[Any], int], *args, **kwargs): """Configure reporting ZCL foundation command.""" records_specific = [ - e for e in records if self.attributes[e.attrid].is_manufacturer_specific + e for e in records if self.attributes[extract_attrid(e)].is_manufacturer_specific ] records_standard = [ - e for e in records if not self.attributes[e.attrid].is_manufacturer_specific + e for e in records if not self.attributes[extract_attrid(e)].is_manufacturer_specific ] - result_specific = await super()._configure_reporting( - records_specific, *args, **kwargs - ) - result_standard = await super()._configure_reporting( - records_standard, *args, **kwargs - ) + result_specific = [] + result_standard = [] - return self.combine_results(result_specific, result_standard) + if records_specific: + result_specific = await func(records_specific, *args, **kwargs) - async def _read_attributes(self, attr_ids, *args, **kwargs): - """Read attributes ZCL foundation command.""" + if records_standard: + result_standard = await func(records_standard, *args, **kwargs) - attr_ids_specific = [ - e for e in attr_ids if self.attributes[e].is_manufacturer_specific - ] - attr_ids_standard = [ - e for e in attr_ids if not self.attributes[e].is_manufacturer_specific - ] + if result_specific and result_standard: + return self.combine_results(result_specific, result_standard) + else: + return result_standard if result_standard else result_specific - result_specific = await super()._read_attributes( - attr_ids_specific, *args, **kwargs - ) - result_standard = await super()._read_attributes( - attr_ids_standard, *args, **kwargs - ) - return self.combine_results(result_specific, result_standard) + async def _configure_reporting(self, records, *args, **kwargs): + """Configure reporting ZCL foundation command.""" + return await self.split_command(records, super()._configure_reporting, lambda x: x.attrid, *args, **kwargs) + + async def _read_attributes(self, attr_ids, *args, **kwargs): + """Read attributes ZCL foundation command.""" + return await self.split_command(attr_ids, super()._read_attributes, lambda x: x, *args, **kwargs) class DanfossThermostatCluster(CustomizedStandardCluster, Thermostat): From 2bdb1ddaba7f78aaed014e525d5b4d4463f7a4ff Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Fri, 27 Oct 2023 14:21:31 +0200 Subject: [PATCH 49/79] code style --- zhaquirks/danfoss/thermostat.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 91a8276919..745254bf31 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -28,7 +28,7 @@ from datetime import datetime -from typing import Callable, Any +from typing import Any, Callable import zigpy.profiles.zha as zha_p from zigpy.quirks import CustomCluster, CustomDevice @@ -101,13 +101,24 @@ def combine_results(*result_lists): return response - async def split_command(self, records: list[Any], func: Callable[[list[Any], ...], Any], extract_attrid: Callable[[Any], int], *args, **kwargs): + async def split_command( + self, + records: list[Any], + func: Callable[[list[Any], ...], Any], + extract_attrid: Callable[[Any], int], + *args, + **kwargs, + ): """Configure reporting ZCL foundation command.""" records_specific = [ - e for e in records if self.attributes[extract_attrid(e)].is_manufacturer_specific + e + for e in records + if self.attributes[extract_attrid(e)].is_manufacturer_specific ] records_standard = [ - e for e in records if not self.attributes[extract_attrid(e)].is_manufacturer_specific + e + for e in records + if not self.attributes[extract_attrid(e)].is_manufacturer_specific ] result_specific = [] @@ -126,11 +137,15 @@ async def split_command(self, records: list[Any], func: Callable[[list[Any], ... async def _configure_reporting(self, records, *args, **kwargs): """Configure reporting ZCL foundation command.""" - return await self.split_command(records, super()._configure_reporting, lambda x: x.attrid, *args, **kwargs) + return await self.split_command( + records, super()._configure_reporting, lambda x: x.attrid, *args, **kwargs + ) async def _read_attributes(self, attr_ids, *args, **kwargs): """Read attributes ZCL foundation command.""" - return await self.split_command(attr_ids, super()._read_attributes, lambda x: x, *args, **kwargs) + return await self.split_command( + attr_ids, super()._read_attributes, lambda x: x, *args, **kwargs + ) class DanfossThermostatCluster(CustomizedStandardCluster, Thermostat): From 4c2a789088f8537250559cad9daa0dfbffe12aa6 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Fri, 27 Oct 2023 14:23:27 +0200 Subject: [PATCH 50/79] comment --- zhaquirks/danfoss/thermostat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 745254bf31..dbaab9ce09 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -109,7 +109,7 @@ async def split_command( *args, **kwargs, ): - """Configure reporting ZCL foundation command.""" + """Split execution of command in one for manufacturer specific and one for standard attributes""" records_specific = [ e for e in records From cc35dab01593c6bdc88f9388f8cbae74d4e38a2b Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Fri, 27 Oct 2023 14:31:15 +0200 Subject: [PATCH 51/79] change list to List type for compatibility reasons --- zhaquirks/danfoss/thermostat.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index dbaab9ce09..5ad7270756 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -28,7 +28,7 @@ from datetime import datetime -from typing import Any, Callable +from typing import Any, Callable, List import zigpy.profiles.zha as zha_p from zigpy.quirks import CustomCluster, CustomDevice @@ -75,10 +75,10 @@ class CustomizedStandardCluster(CustomCluster): """Danfoss customized standard clusters by adding custom attributes - Danfoss doesn't allow standard attributes when manufacturer specific is requested + Danfoss doesn't allow all standard attributes when manufacturer specific is requested - Therefore, this subclass separates manufacturer specific and standard attributes before - Zigbee commands allowing manufacturer specific to be passed for specific attributes, but not for standard attributes + Therefore, this subclass separates manufacturer specific and standard attributes for Zigbee commands allowing + manufacturer specific to be passed for specific attributes, but not for standard attributes """ @staticmethod @@ -103,8 +103,8 @@ def combine_results(*result_lists): async def split_command( self, - records: list[Any], - func: Callable[[list[Any], ...], Any], + records: List[Any], + func: Callable[[List[Any], ...], Any], extract_attrid: Callable[[Any], int], *args, **kwargs, From 663c1355751b32cd5cbc3bb8d7ee236b324bc3b9 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Fri, 27 Oct 2023 14:34:04 +0200 Subject: [PATCH 52/79] remove variable arg Callable for compatibility reasons --- zhaquirks/danfoss/thermostat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 5ad7270756..1ec98dacab 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -104,7 +104,7 @@ def combine_results(*result_lists): async def split_command( self, records: List[Any], - func: Callable[[List[Any], ...], Any], + func: Callable, extract_attrid: Callable[[Any], int], *args, **kwargs, From 2d0862eb6ec3639e366c20dd90970739bfec53fd Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Tue, 31 Oct 2023 19:14:39 +0100 Subject: [PATCH 53/79] black --- zhaquirks/quirk_ids.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhaquirks/quirk_ids.py b/zhaquirks/quirk_ids.py index 235ebab6e5..bed6d37c1a 100644 --- a/zhaquirks/quirk_ids.py +++ b/zhaquirks/quirk_ids.py @@ -10,4 +10,4 @@ ) # Danfoss -DANFOSS_THERMOSTAT = "danfoss.thermostat" # Thermostatic Radiator Valves based on Danfoss Ally with custom clusters \ No newline at end of file +DANFOSS_THERMOSTAT = "danfoss.thermostat" # Thermostatic Radiator Valves based on Danfoss Ally with custom clusters From e8970b2a4d5b0beff9c4093068b7549aa96f4d0c Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Thu, 2 Nov 2023 13:14:07 +0100 Subject: [PATCH 54/79] improve quirk id to ward against future products --- zhaquirks/quirk_ids.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zhaquirks/quirk_ids.py b/zhaquirks/quirk_ids.py index bed6d37c1a..c79736e3b8 100644 --- a/zhaquirks/quirk_ids.py +++ b/zhaquirks/quirk_ids.py @@ -10,4 +10,4 @@ ) # Danfoss -DANFOSS_THERMOSTAT = "danfoss.thermostat" # Thermostatic Radiator Valves based on Danfoss Ally with custom clusters +DANFOSS_THERMOSTAT = "danfoss.ally_thermostat" # Thermostatic Radiator Valves based on Danfoss Ally with custom clusters From 169a56213f52becdda6a795ae39fdb9e6f71f07b Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Thu, 2 Nov 2023 13:35:32 +0100 Subject: [PATCH 55/79] fix variable name --- zhaquirks/danfoss/thermostat.py | 4 ++-- zhaquirks/quirk_ids.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 1ec98dacab..c5b47b7d06 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -54,7 +54,7 @@ OUTPUT_CLUSTERS, PROFILE_ID, ) -from zhaquirks.quirk_ids import DANFOSS_THERMOSTAT +from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT DANFOSS = "Danfoss" HIVE = DANFOSS @@ -377,7 +377,7 @@ async def bind(self): class DanfossThermostat(CustomDevice): """DanfossThermostat custom device.""" - quirk_id = DANFOSS_THERMOSTAT + quirk_id = DANFOSS_ALLY_THERMOSTAT manufacturer_code = 0x1246 diff --git a/zhaquirks/quirk_ids.py b/zhaquirks/quirk_ids.py index c79736e3b8..80ff96d20f 100644 --- a/zhaquirks/quirk_ids.py +++ b/zhaquirks/quirk_ids.py @@ -10,4 +10,4 @@ ) # Danfoss -DANFOSS_THERMOSTAT = "danfoss.ally_thermostat" # Thermostatic Radiator Valves based on Danfoss Ally with custom clusters +DANFOSS_ALLY_THERMOSTAT = "danfoss.ally_thermostat" # Thermostatic Radiator Valves based on Danfoss Ally with custom clusters From c54626d03dd1028ecab24a0118199a291f9fc0cd Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Thu, 18 Jan 2024 19:06:54 +0100 Subject: [PATCH 56/79] use more standard notation --- zhaquirks/danfoss/thermostat.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index c5b47b7d06..8f13b49386 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -30,7 +30,7 @@ from datetime import datetime from typing import Any, Callable, List -import zigpy.profiles.zha as zha_p +from zigpy.profiles import zha from zigpy.quirks import CustomCluster, CustomDevice import zigpy.types as t from zigpy.types import uint16_t @@ -396,8 +396,8 @@ class DanfossThermostat(CustomDevice): ], ENDPOINTS: { 1: { - PROFILE_ID: zha_p.PROFILE_ID, - DEVICE_TYPE: zha_p.DeviceType.THERMOSTAT, + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.THERMOSTAT, INPUT_CLUSTERS: [ Basic.cluster_id, PowerConfiguration.cluster_id, @@ -417,16 +417,16 @@ class DanfossThermostat(CustomDevice): ENDPOINTS: { 1: { INPUT_CLUSTERS: [ - Basic, - PowerConfiguration, - Identify, - DanfossTimeCluster, - PollControl, - DanfossThermostatCluster, - DanfossUserInterfaceCluster, - DanfossDiagnosticCluster, + Basic.cluster_id, + PowerConfiguration.cluster_id, + Identify.cluster_id, + DanfossTimeCluster.cluster_id, + PollControl.cluster_id, + DanfossThermostatCluster.cluster_id, + DanfossUserInterfaceCluster.cluster_id, + DanfossDiagnosticCluster.cluster_id, ], - OUTPUT_CLUSTERS: [Basic, Ota], + OUTPUT_CLUSTERS: [Basic.cluster_id, Ota.cluster_id], } } } From b5e4922cf46a5b0c88b911129b7a316d60518b05 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Thu, 18 Jan 2024 19:07:55 +0100 Subject: [PATCH 57/79] but do use custom clusters --- zhaquirks/danfoss/thermostat.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 8f13b49386..b90aa976bb 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -420,11 +420,11 @@ class DanfossThermostat(CustomDevice): Basic.cluster_id, PowerConfiguration.cluster_id, Identify.cluster_id, - DanfossTimeCluster.cluster_id, PollControl.cluster_id, - DanfossThermostatCluster.cluster_id, - DanfossUserInterfaceCluster.cluster_id, - DanfossDiagnosticCluster.cluster_id, + DanfossTimeCluster, + DanfossThermostatCluster, + DanfossUserInterfaceCluster, + DanfossDiagnosticCluster, ], OUTPUT_CLUSTERS: [Basic.cluster_id, Ota.cluster_id], } From d70cf1429cf3ad2e6fd1531d70cdf6153b279b8d Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Thu, 25 Jan 2024 16:21:34 +0100 Subject: [PATCH 58/79] use zcl constants from shared library --- zhaquirks/danfoss/thermostat.py | 34 ++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index b90aa976bb..1865e94ac4 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -33,7 +33,6 @@ from zigpy.profiles import zha from zigpy.quirks import CustomCluster, CustomDevice import zigpy.types as t -from zigpy.types import uint16_t from zigpy.zcl.clusters.general import ( Basic, Identify, @@ -60,18 +59,8 @@ HIVE = DANFOSS POPP = "D5X84YU" -OCCUPIED_HEATING_SETPOINT_NAME = "occupied_heating_setpoint" -SYSTEM_MODE_NAME = "system_mode" - -OCCUPIED_HEATING_SETPOINT_THERM_ID = uint16_t(0x0012) -SETPOINT_CHANGE_THERM_ID = uint16_t(0x0012) -MIN_HEAT_SETPOINT_LIMIT_THERM_ID = uint16_t(0x0015) - SETPOINT_COMMAND_AGGRESSIVE_VAL = 0x01 -SYSTEM_MODE_THERM_OFF_VAL = 0x00 -SYSTEM_MODE_THERM_ON_VAL = 0x04 - class CustomizedStandardCluster(CustomCluster): """Danfoss customized standard clusters by adding custom attributes @@ -250,20 +239,31 @@ async def write_attributes(self, attributes, manufacturer=None): fast_setpoint_change = None - if OCCUPIED_HEATING_SETPOINT_NAME in attributes: + if Thermostat.AttributeDefs.occupied_heating_setpoint.name in attributes: # On Danfoss an immediate setpoint change is done through a command # store for later in fast_setpoint_change and remove from attributes - fast_setpoint_change = attributes[OCCUPIED_HEATING_SETPOINT_NAME] + fast_setpoint_change = attributes[ + Thermostat.AttributeDefs.occupied_heating_setpoint.name + ] # if: system_mode = off - if attributes.get(SYSTEM_MODE_NAME) == SYSTEM_MODE_THERM_OFF_VAL: + if ( + attributes.get(Thermostat.AttributeDefs.system_mode.name) + == Thermostat.AttributeDefs.system_mode.type.Off + ): # Thermostatic Radiator Valves from Danfoss cannot be turned off to prevent damage during frost # just turn setpoint down to minimum temperature using fast_setpoint_change - fast_setpoint_change = self._attr_cache[MIN_HEAT_SETPOINT_LIMIT_THERM_ID] - attributes[OCCUPIED_HEATING_SETPOINT_NAME] = fast_setpoint_change + fast_setpoint_change = self._attr_cache[ + Thermostat.AttributeDefs.min_heat_setpoint_limit.id + ] + attributes[ + Thermostat.AttributeDefs.occupied_heating_setpoint.name + ] = fast_setpoint_change # Danfoss doesn't accept off, therefore set to On - attributes[SYSTEM_MODE_NAME] = SYSTEM_MODE_THERM_ON_VAL + attributes[ + Thermostat.AttributeDefs.system_mode.name + ] = Thermostat.AttributeDefs.system_mode.type.Heat # attributes cannot be empty, because write_res cannot be empty, but it can contain unrequested items write_res = await super().write_attributes( From e287e240fdd532ce988043d6ad727905b7119f6b Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Wed, 31 Jan 2024 00:29:09 +0100 Subject: [PATCH 59/79] docstring --- zhaquirks/danfoss/thermostat.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 1865e94ac4..c5ffbaff0e 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -3,19 +3,19 @@ manufacturer specific attributes to control displaying and specific configuration. ZCL Attributes Supported: - 0x0201 - 0x0025: programing_oper_mode # Danfoss deviated from the spec - all - 0xFFFD: cluster_revision - - 0x0201 - pi_heating_demand (0x0008), - 0x0201 - min_heat_setpoint_limit (0x0015) - 0x0201 - max_heat_setpoint_limit (0x0016) - 0x0201 - setpoint_change_source (0x0030) - 0x0201 - abs_min_heat_setpoint_limit (0x0003)=5 - 0x0201 - abs_max_heat_setpoint_limit (0x0004)=35 - 0x0201 - start_of_week (0x0020)=Monday - 0x0201 - number_of_weekly_transitions (0x0021)=42 - 0x0201 - number_of_daily_transitions (0x0022)=6 - 0x0204: keypad_lockout (0x0001) + 0x0201 - ThermostatProgrammingOperationMode (0x0025): Danfoss deviated from the spec + all - ClusterRevision (0xFFFD) + + 0x0201 - PIHeatingDemand (0x0008), + 0x0201 - MinHeatSetpointLimit (0x0015) + 0x0201 - MaxHeatSetpointLimit (0x0016) + 0x0201 - SetpointChangeSource (0x0030) + 0x0201 - AbsMinHeatSetpointLimit (0x0003)=5 + 0x0201 - AbsMaxHeatSetpointLimit (0x0004)=35 + 0x0201 - StartOfWeek (0x0020)=Monday + 0x0201 - NumberOfWeeklyTransitions (0x0021)=42 + 0x0201 - NumberOfDailyTransitions (0x0022)=6 + 0x0204 - KeypadLockout (0x0001) ZCL Commands Supported: 0x0201 - SetWeeklySchedule (0x01) @@ -23,7 +23,7 @@ 0x0201 - ClearWeeklySchedule (0x03) Broken ZCL Attributes: - 0x0204 - 0x0000: Writing doesn't seem to do anything + 0x0204 - TemperatureDisplayMode (0x0000): Writing doesn't seem to do anything """ From 08abd4b631ace9f842183ec8e6a435909ae4c71e Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sun, 4 Feb 2024 12:22:56 +0100 Subject: [PATCH 60/79] define enums in quirk and shorten AttributeDefs usage --- zhaquirks/danfoss/thermostat.py | 181 +++++++++++++++++++++----------- 1 file changed, 118 insertions(+), 63 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index c5ffbaff0e..597103668f 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -30,9 +30,9 @@ from datetime import datetime from typing import Any, Callable, List +from zigpy import types from zigpy.profiles import zha from zigpy.quirks import CustomCluster, CustomDevice -import zigpy.types as t from zigpy.zcl.clusters.general import ( Basic, Identify, @@ -55,11 +55,67 @@ ) from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT +occupied_heating_setpoint = Thermostat.AttributeDefs.occupied_heating_setpoint +system_mode = Thermostat.AttributeDefs.system_mode +min_heat_setpoint_limit = Thermostat.AttributeDefs.min_heat_setpoint_limit + DANFOSS = "Danfoss" HIVE = DANFOSS POPP = "D5X84YU" -SETPOINT_COMMAND_AGGRESSIVE_VAL = 0x01 + +class DanfossViewingDirectionEnum(types.enum8): + """Default (button above screen when looking at it) or Inverted (button below screen when looking at it).""" + + Default = 0x00 + Inverted = 0x01 + + +class DanfossAdaptationRunControlEnum(types.enum8): + """Initiate or Cancel Adaptation Run.""" + + Nothing = 0x00 + Initiate = 0x01 + Cancel = 0x02 + + +class DanfossExerciseDayOfTheWeekEnum(types.enum8): + """Day of the week.""" + + Sunday = 0 + Monday = 1 + Tuesday = 2 + Wednesday = 3 + Thursday = 4 + Friday = 5 + Saturday = 6 + + +class DanfossOpenWindowDetectionEnum(types.enum8): + """Danfoss open window detection judgments.""" + + Quarantine = 0x00 + Closed = 0x01 + Maybe = 0x02 + Open = 0x03 + External = 0x04 + + +class DanfossSetpointCommandEnum(types.enum8): + """Set behaviour to change the setpoint.""" + + Schedule = 0 # relatively slow + User_Interaction = 1 # aggressive change + Preheat = 2 # invisible to user + + +class DanfossPreheatCommandEnum(types.enum8): + """Set behaviour of preheat command. + + Only one option available, but other values are possible in the future. + """ + + Force = 0 class CustomizedStandardCluster(CustomCluster): @@ -143,88 +199,95 @@ class DanfossThermostatCluster(CustomizedStandardCluster, Thermostat): class ServerCommandDefs(Thermostat.ServerCommandDefs): setpoint_command = ZCLCommandDef( id=0x40, - # Types - # 0: Schedule (relatively slow) - # 1: User Interaction (aggressive change) - # 2: Preheat (invisible to user) - schema={"type": t.enum8, "heating_setpoint": t.int16s}, + schema={ + "type": DanfossSetpointCommandEnum, + "heating_setpoint": types.int16s, + }, is_manufacturer_specific=True, ) # for synchronizing multiple TRVs preheating preheat_command = ZCLCommandDef( id=0x42, - # Force: 0 means force, other values for future needs - schema={"force": t.enum8, "timestamp": t.uint32_t}, + schema={"force": DanfossPreheatCommandEnum, "timestamp": types.uint32_t}, is_manufacturer_specific=True, ) class AttributeDefs(Thermostat.AttributeDefs): open_window_detection = ZCLAttributeDef( - id=0x4000, type=t.enum8, access="rp", is_manufacturer_specific=True + id=0x4000, + type=DanfossOpenWindowDetectionEnum, + access="rp", + is_manufacturer_specific=True, ) external_open_window_detected = ZCLAttributeDef( - id=0x4003, type=t.Bool, access="rw", is_manufacturer_specific=True + id=0x4003, type=types.Bool, access="rw", is_manufacturer_specific=True ) # non-configurable reporting window_open_feature = ZCLAttributeDef( - id=0x4051, type=t.Bool, access="rw", is_manufacturer_specific=True + id=0x4051, type=types.Bool, access="rw", is_manufacturer_specific=True ) # non-configurable reporting exercise_day_of_week = ZCLAttributeDef( - id=0x4010, type=t.enum8, access="rw", is_manufacturer_specific=True + id=0x4010, + type=DanfossExerciseDayOfTheWeekEnum, + access="rw", + is_manufacturer_specific=True, ) exercise_trigger_time = ZCLAttributeDef( - id=0x4011, type=t.uint16_t, access="rw", is_manufacturer_specific=True + id=0x4011, type=types.uint16_t, access="rw", is_manufacturer_specific=True ) mounting_mode_active = ZCLAttributeDef( - id=0x4012, type=t.Bool, access="rp", is_manufacturer_specific=True + id=0x4012, type=types.Bool, access="rp", is_manufacturer_specific=True ) mounting_mode_control = ZCLAttributeDef( - id=0x4013, type=t.Bool, access="rw", is_manufacturer_specific=True + id=0x4013, type=types.Bool, access="rw", is_manufacturer_specific=True ) # non-configurable reporting - orientation = ZCLAttributeDef( - id=0x4014, type=t.Bool, access="rw", is_manufacturer_specific=True + orientation = ZCLAttributeDef( # Horizontal = False and Vertical = True + id=0x4014, type=types.Bool, access="rw", is_manufacturer_specific=True ) # non-configurable reporting external_measured_room_sensor = ZCLAttributeDef( - id=0x4015, type=t.int16s, access="rw", is_manufacturer_specific=True + id=0x4015, type=types.int16s, access="rw", is_manufacturer_specific=True ) radiator_covered = ZCLAttributeDef( - id=0x4016, type=t.Bool, access="rw", is_manufacturer_specific=True + id=0x4016, type=types.Bool, access="rw", is_manufacturer_specific=True ) # non-configurable reporting heat_available = ZCLAttributeDef( - id=0x4030, type=t.Bool, access="rw", is_manufacturer_specific=True + id=0x4030, type=types.Bool, access="rw", is_manufacturer_specific=True ) # non-configurable reporting heat_required = ZCLAttributeDef( - id=0x4031, type=t.Bool, access="rp", is_manufacturer_specific=True + id=0x4031, type=types.Bool, access="rp", is_manufacturer_specific=True ) load_balancing_enable = ZCLAttributeDef( - id=0x4032, type=t.Bool, access="rw", is_manufacturer_specific=True + id=0x4032, type=types.Bool, access="rw", is_manufacturer_specific=True ) # non-configurable reporting load_room_mean = ZCLAttributeDef( - id=0x4040, type=t.int16s, access="rw", is_manufacturer_specific=True + id=0x4040, type=types.int16s, access="rw", is_manufacturer_specific=True ) # non-configurable reporting (according to the documentation, you cannot read it, but it works anyway) load_estimate = ZCLAttributeDef( - id=0x404A, type=t.int16s, access="rp", is_manufacturer_specific=True + id=0x404A, type=types.int16s, access="rp", is_manufacturer_specific=True ) control_algorithm_scale_factor = ZCLAttributeDef( - id=0x4020, type=t.uint8_t, access="rw", is_manufacturer_specific=True + id=0x4020, type=types.uint8_t, access="rw", is_manufacturer_specific=True ) # non-configurable reporting regulation_setpoint_offset = ZCLAttributeDef( - id=0x404B, type=t.int8s, access="rw", is_manufacturer_specific=True + id=0x404B, type=types.int8s, access="rw", is_manufacturer_specific=True ) adaptation_run_control = ZCLAttributeDef( - id=0x404C, type=t.enum8, access="rw", is_manufacturer_specific=True + id=0x404C, + type=DanfossAdaptationRunControlEnum, + access="rw", + is_manufacturer_specific=True, ) # non-configurable reporting adaptation_run_status = ZCLAttributeDef( - id=0x404D, type=t.bitmap8, access="rp", is_manufacturer_specific=True + id=0x404D, type=types.bitmap8, access="rp", is_manufacturer_specific=True ) adaptation_run_settings = ZCLAttributeDef( - id=0x404E, type=t.bitmap8, access="rw", is_manufacturer_specific=True + id=0x404E, type=types.bitmap8, access="rw", is_manufacturer_specific=True ) preheat_status = ZCLAttributeDef( - id=0x404F, type=t.Bool, access="rp", is_manufacturer_specific=True + id=0x404F, type=types.Bool, access="rp", is_manufacturer_specific=True ) preheat_time = ZCLAttributeDef( - id=0x4050, type=t.uint32_t, access="rp", is_manufacturer_specific=True + id=0x4050, type=types.uint32_t, access="rp", is_manufacturer_specific=True ) async def write_attributes(self, attributes, manufacturer=None): @@ -239,31 +302,20 @@ async def write_attributes(self, attributes, manufacturer=None): fast_setpoint_change = None - if Thermostat.AttributeDefs.occupied_heating_setpoint.name in attributes: + if occupied_heating_setpoint.name in attributes: # On Danfoss an immediate setpoint change is done through a command # store for later in fast_setpoint_change and remove from attributes - fast_setpoint_change = attributes[ - Thermostat.AttributeDefs.occupied_heating_setpoint.name - ] + fast_setpoint_change = attributes[occupied_heating_setpoint.name] # if: system_mode = off - if ( - attributes.get(Thermostat.AttributeDefs.system_mode.name) - == Thermostat.AttributeDefs.system_mode.type.Off - ): + if attributes.get(system_mode.name) == system_mode.type.Off: # Thermostatic Radiator Valves from Danfoss cannot be turned off to prevent damage during frost # just turn setpoint down to minimum temperature using fast_setpoint_change - fast_setpoint_change = self._attr_cache[ - Thermostat.AttributeDefs.min_heat_setpoint_limit.id - ] - attributes[ - Thermostat.AttributeDefs.occupied_heating_setpoint.name - ] = fast_setpoint_change + fast_setpoint_change = self._attr_cache[min_heat_setpoint_limit.id] + attributes[occupied_heating_setpoint.name] = fast_setpoint_change # Danfoss doesn't accept off, therefore set to On - attributes[ - Thermostat.AttributeDefs.system_mode.name - ] = Thermostat.AttributeDefs.system_mode.type.Heat + attributes[system_mode.name] = system_mode.type.Heat # attributes cannot be empty, because write_res cannot be empty, but it can contain unrequested items write_res = await super().write_attributes( @@ -273,7 +325,7 @@ async def write_attributes(self, attributes, manufacturer=None): if fast_setpoint_change is not None: # On Danfoss a fast setpoint change is done through a command await self.setpoint_command( - SETPOINT_COMMAND_AGGRESSIVE_VAL, + DanfossSetpointCommandEnum.User_Interaction, fast_setpoint_change, manufacturer=manufacturer, ) @@ -294,7 +346,10 @@ class DanfossUserInterfaceCluster(CustomizedStandardCluster, UserInterface): class AttributeDefs(UserInterface.AttributeDefs): viewing_direction = ZCLAttributeDef( - id=0x4000, type=t.enum8, access="rw", is_manufacturer_specific=True + id=0x4000, + type=DanfossViewingDirectionEnum, + access="rw", + is_manufacturer_specific=True, ) # non-configurable reporting @@ -303,44 +358,44 @@ class DanfossDiagnosticCluster(CustomizedStandardCluster, Diagnostic): class AttributeDefs(Diagnostic.AttributeDefs): sw_error_code = ZCLAttributeDef( - id=0x4000, type=t.bitmap16, access="rpw", is_manufacturer_specific=True + id=0x4000, type=types.bitmap16, access="rpw", is_manufacturer_specific=True ) wake_time_avg = ZCLAttributeDef( - id=0x4001, type=t.uint32_t, access="r", is_manufacturer_specific=True + id=0x4001, type=types.uint32_t, access="r", is_manufacturer_specific=True ) wake_time_max_duration = ZCLAttributeDef( - id=0x4002, type=t.uint32_t, access="r", is_manufacturer_specific=True + id=0x4002, type=types.uint32_t, access="r", is_manufacturer_specific=True ) wake_time_min_duration = ZCLAttributeDef( - id=0x4003, type=t.uint32_t, access="r", is_manufacturer_specific=True + id=0x4003, type=types.uint32_t, access="r", is_manufacturer_specific=True ) sleep_postponed_count_avg = ZCLAttributeDef( - id=0x4004, type=t.uint32_t, access="r", is_manufacturer_specific=True + id=0x4004, type=types.uint32_t, access="r", is_manufacturer_specific=True ) sleep_postponed_count_max = ZCLAttributeDef( - id=0x4005, type=t.uint32_t, access="r", is_manufacturer_specific=True + id=0x4005, type=types.uint32_t, access="r", is_manufacturer_specific=True ) sleep_postponed_count_min = ZCLAttributeDef( - id=0x4006, type=t.uint32_t, access="r", is_manufacturer_specific=True + id=0x4006, type=types.uint32_t, access="r", is_manufacturer_specific=True ) motor_step_counter = ZCLAttributeDef( - id=0x4010, type=t.uint32_t, access="rp", is_manufacturer_specific=True + id=0x4010, type=types.uint32_t, access="rp", is_manufacturer_specific=True ) data_logger = ZCLAttributeDef( id=0x4020, - type=t.LimitedLVBytes(50), + type=types.LimitedLVBytes(50), access="rpw", is_manufacturer_specific=True, ) control_diagnostics = ZCLAttributeDef( id=0x4021, - type=t.LimitedLVBytes(30), + type=types.LimitedLVBytes(30), access="rp", is_manufacturer_specific=True, ) control_diagnostics_frequency = ZCLAttributeDef( - id=0x4022, type=t.uint16_t, access="rw", is_manufacturer_specific=True + id=0x4022, type=types.uint16_t, access="rw", is_manufacturer_specific=True ) # non-configurable reporting @@ -358,7 +413,7 @@ async def write_time(self): await self.write_attributes( { "time": current_time, - "time_status": 0b00000010, # only bit 1 can be written + "time_status": 0b00000010, # only bit 1 can be set "time_zone": time_zone, } ) From 5d04d098bfb83bf922bc4713f683663dab9ae827 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sun, 4 Feb 2024 12:28:10 +0100 Subject: [PATCH 61/79] capitals and dots in docstrings --- zhaquirks/danfoss/thermostat.py | 38 ++++++++++++++++----------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 597103668f..b8fd2508ec 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -1,8 +1,8 @@ """Module to handle quirks of the Danfoss thermostat. -manufacturer specific attributes to control displaying and specific configuration. +Manufacturer specific attributes to control displaying and specific configuration. -ZCL Attributes Supported: +ZCL attributes supported: 0x0201 - ThermostatProgrammingOperationMode (0x0025): Danfoss deviated from the spec all - ClusterRevision (0xFFFD) @@ -17,12 +17,12 @@ 0x0201 - NumberOfDailyTransitions (0x0022)=6 0x0204 - KeypadLockout (0x0001) -ZCL Commands Supported: +ZCL commands supported: 0x0201 - SetWeeklySchedule (0x01) 0x0201 - GetWeeklySchedule (0x02) 0x0201 - ClearWeeklySchedule (0x03) -Broken ZCL Attributes: +Broken ZCL attributes: 0x0204 - TemperatureDisplayMode (0x0000): Writing doesn't seem to do anything """ @@ -72,7 +72,7 @@ class DanfossViewingDirectionEnum(types.enum8): class DanfossAdaptationRunControlEnum(types.enum8): - """Initiate or Cancel Adaptation Run.""" + """Initiate or Cancel adaptation run.""" Nothing = 0x00 Initiate = 0x01 @@ -111,7 +111,6 @@ class DanfossSetpointCommandEnum(types.enum8): class DanfossPreheatCommandEnum(types.enum8): """Set behaviour of preheat command. - Only one option available, but other values are possible in the future. """ @@ -119,11 +118,11 @@ class DanfossPreheatCommandEnum(types.enum8): class CustomizedStandardCluster(CustomCluster): - """Danfoss customized standard clusters by adding custom attributes - Danfoss doesn't allow all standard attributes when manufacturer specific is requested + """Danfoss customized standard clusters by adding custom attributes. + Danfoss doesn't allow all standard attributes when manufacturer specific is requested. Therefore, this subclass separates manufacturer specific and standard attributes for Zigbee commands allowing - manufacturer specific to be passed for specific attributes, but not for standard attributes + manufacturer specific to be passed for specific attributes, but not for standard attributes. """ @staticmethod @@ -154,7 +153,7 @@ async def split_command( *args, **kwargs, ): - """Split execution of command in one for manufacturer specific and one for standard attributes""" + """Split execution of command in one for manufacturer specific and one for standard attributes.""" records_specific = [ e for e in records @@ -194,7 +193,7 @@ async def _read_attributes(self, attr_ids, *args, **kwargs): class DanfossThermostatCluster(CustomizedStandardCluster, Thermostat): - """Danfoss cluster for standard and proprietary danfoss attributes""" + """Danfoss cluster for standard and proprietary danfoss attributes.""" class ServerCommandDefs(Thermostat.ServerCommandDefs): setpoint_command = ZCLCommandDef( @@ -291,13 +290,12 @@ class AttributeDefs(Thermostat.AttributeDefs): ) async def write_attributes(self, attributes, manufacturer=None): - """There are 2 types of setpoint changes: - Fast and Slow - Fast is used for immediate changes; this is done using a command (setpoint_command) - Slow is used for scheduled changes; this is done using an attribute (occupied_heating_setpoint) + """There are 2 types of setpoint changes: Fast and Slow. + Fast is used for immediate changes; this is done using a command (setpoint_command). + Slow is used for scheduled changes; this is done using an attribute (occupied_heating_setpoint). - system mode=off is not implemented on Danfoss; this is emulated by setting setpoint to the minimum setpoint - In case of a change on occupied_heating_setpoint or system mode=off, a fast setpoint change is done + system mode=off is not implemented on Danfoss; this is emulated by setting setpoint to the minimum setpoint. + In case of a change on occupied_heating_setpoint or system mode=off, a fast setpoint change is done. """ fast_setpoint_change = None @@ -314,7 +312,7 @@ async def write_attributes(self, attributes, manufacturer=None): fast_setpoint_change = self._attr_cache[min_heat_setpoint_limit.id] attributes[occupied_heating_setpoint.name] = fast_setpoint_change - # Danfoss doesn't accept off, therefore set to On + # Danfoss doesn't accept off, therefore set to on attributes[system_mode.name] = system_mode.type.Heat # attributes cannot be empty, because write_res cannot be empty, but it can contain unrequested items @@ -342,7 +340,7 @@ async def bind(self): class DanfossUserInterfaceCluster(CustomizedStandardCluster, UserInterface): - """Danfoss cluster for standard and proprietary danfoss attributes""" + """Danfoss cluster for standard and proprietary danfoss attributes.""" class AttributeDefs(UserInterface.AttributeDefs): viewing_direction = ZCLAttributeDef( @@ -354,7 +352,7 @@ class AttributeDefs(UserInterface.AttributeDefs): class DanfossDiagnosticCluster(CustomizedStandardCluster, Diagnostic): - """Danfoss cluster for standard and proprietary danfoss attributes""" + """Danfoss cluster for standard and proprietary danfoss attributes.""" class AttributeDefs(Diagnostic.AttributeDefs): sw_error_code = ZCLAttributeDef( From e0180a1bdb02b004782a0b50483b2fbe8ca0347d Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sun, 11 Feb 2024 19:05:01 +0100 Subject: [PATCH 62/79] documentation --- zhaquirks/danfoss/thermostat.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index b8fd2508ec..fae6a42370 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -3,7 +3,9 @@ Manufacturer specific attributes to control displaying and specific configuration. ZCL attributes supported: - 0x0201 - ThermostatProgrammingOperationMode (0x0025): Danfoss deviated from the spec + 0x0201 - ThermostatProgrammingOperationMode (0x0025): + Danfoss writes in a presentation document that it implemented a preheat function with the second bit, + but this is contradicted by a detailed and up-to-date document. all - ClusterRevision (0xFFFD) 0x0201 - PIHeatingDemand (0x0008), From 6b4320302cb22695288818aee8e733f8ba07765f Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Tue, 13 Feb 2024 14:36:43 +0100 Subject: [PATCH 63/79] fix docstring --- zhaquirks/danfoss/thermostat.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index fae6a42370..e2f967e83e 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -295,29 +295,25 @@ async def write_attributes(self, attributes, manufacturer=None): """There are 2 types of setpoint changes: Fast and Slow. Fast is used for immediate changes; this is done using a command (setpoint_command). Slow is used for scheduled changes; this is done using an attribute (occupied_heating_setpoint). + In case of a change on occupied_heating_setpoint, a setpoint_command is used. - system mode=off is not implemented on Danfoss; this is emulated by setting setpoint to the minimum setpoint. - In case of a change on occupied_heating_setpoint or system mode=off, a fast setpoint change is done. + Thermostatic radiator valves from Danfoss cannot be turned off to prevent damage during frost. + This is emulated by setting setpoint to the minimum setpoint. """ fast_setpoint_change = None if occupied_heating_setpoint.name in attributes: - # On Danfoss an immediate setpoint change is done through a command - # store for later in fast_setpoint_change and remove from attributes + # Store setpoint for use in command fast_setpoint_change = attributes[occupied_heating_setpoint.name] - # if: system_mode = off if attributes.get(system_mode.name) == system_mode.type.Off: - # Thermostatic Radiator Valves from Danfoss cannot be turned off to prevent damage during frost - # just turn setpoint down to minimum temperature using fast_setpoint_change + # Just turn setpoint down to minimum temperature using fast_setpoint_change fast_setpoint_change = self._attr_cache[min_heat_setpoint_limit.id] attributes[occupied_heating_setpoint.name] = fast_setpoint_change - - # Danfoss doesn't accept off, therefore set to on attributes[system_mode.name] = system_mode.type.Heat - # attributes cannot be empty, because write_res cannot be empty, but it can contain unrequested items + # Attributes cannot be empty, because write_res cannot be empty, but it can contain unrequested items write_res = await super().write_attributes( attributes, manufacturer=manufacturer ) From fc1646eca9bcb6fdd99da86ccab53f57492a7269 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Tue, 13 Feb 2024 14:51:46 +0100 Subject: [PATCH 64/79] simnplify and add enum value to excersice day of the week --- zhaquirks/danfoss/thermostat.py | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index e2f967e83e..7815dcd8a7 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -91,6 +91,7 @@ class DanfossExerciseDayOfTheWeekEnum(types.enum8): Thursday = 4 Friday = 5 Saturday = 6 + Undefined = 7 class DanfossOpenWindowDetectionEnum(types.enum8): @@ -137,15 +138,11 @@ def combine_results(*result_lists): elif len(result) == 2: success_global.extend(result[0]) failure_global.extend(result[1]) - else: - raise Exception(f"Unexpected result size: {len(result)}") if failure_global: - response = [success_global, failure_global] + return [success_global, failure_global] else: - response = [success_global] - - return response + return [success_global] async def split_command( self, @@ -167,19 +164,15 @@ async def split_command( if not self.attributes[extract_attrid(e)].is_manufacturer_specific ] - result_specific = [] - result_standard = [] - - if records_specific: - result_specific = await func(records_specific, *args, **kwargs) + result_specific = ( + await func(records_specific, *args, **kwargs) if records_specific else [] + ) - if records_standard: - result_standard = await func(records_standard, *args, **kwargs) + result_standard = ( + await func(records_standard, *args, **kwargs) if records_standard else [] + ) - if result_specific and result_standard: - return self.combine_results(result_specific, result_standard) - else: - return result_standard if result_standard else result_specific + return self.combine_results(result_specific, result_standard) async def _configure_reporting(self, records, *args, **kwargs): """Configure reporting ZCL foundation command.""" From d0c640f3c75861809e431c858b377d7e485ad7e4 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Tue, 13 Feb 2024 16:42:36 +0100 Subject: [PATCH 65/79] add bitmaps and reorder attributes --- zhaquirks/danfoss/thermostat.py | 84 ++++++++++++++++++++++++++------- 1 file changed, 68 insertions(+), 16 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 7815dcd8a7..0c657d7629 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -76,7 +76,7 @@ class DanfossViewingDirectionEnum(types.enum8): class DanfossAdaptationRunControlEnum(types.enum8): """Initiate or Cancel adaptation run.""" - Nothing = 0x00 + Nothing = 0x00 # not documented everywhere Initiate = 0x01 Cancel = 0x02 @@ -104,6 +104,42 @@ class DanfossOpenWindowDetectionEnum(types.enum8): External = 0x04 +class DanfossSoftwareErrorCodeBitmap(types.bitmap16): + """Danfoss software error code bitmap.""" + + top_pcb_sensor_error = 0x0001 + side_pcb_sensor_error = 0x0002 + non_volatile_memory_error = 0x0004 + unknown_hw_error = 0x0008 + # 0x0010 = N/A + motor_error = 0x0020 + # 0x0040 = N/A + invalid_internal_communication = 0x0080 + # 0x0100 = N/A + invalid_clock_information = 0x0200 + # 0x0400 = N/A + radio_communication_error = 0x0800 + encoder_jammed = 0x1000 + low_battery = 0x2000 + critical_low_battery = 0x4000 + # 0x8000 = Reserved + + +class DanfossAdaptationRunStatusBitmap(types.bitmap8): + """Danfoss Adaptation run status bitmap.""" + + in_progress = 0x0001 + valve_characteristic_found = 0x0002 + valve_characteristic_lost = 0x0004 + + +class DanfossAdaptationRunSettingsBitmap(types.bitmap8): + """Danfoss Adaptation run settings bitmap.""" + + Disabled = 0x00 + Enabled = 0x01 + + class DanfossSetpointCommandEnum(types.enum8): """Set behaviour to change the setpoint.""" @@ -200,7 +236,6 @@ class ServerCommandDefs(Thermostat.ServerCommandDefs): is_manufacturer_specific=True, ) - # for synchronizing multiple TRVs preheating preheat_command = ZCLCommandDef( id=0x42, schema={"force": DanfossPreheatCommandEnum, "timestamp": types.uint32_t}, @@ -208,7 +243,7 @@ class ServerCommandDefs(Thermostat.ServerCommandDefs): ) class AttributeDefs(Thermostat.AttributeDefs): - open_window_detection = ZCLAttributeDef( + open_window_detection = ZCLAttributeDef( # etrv_open_window_detection id=0x4000, type=DanfossOpenWindowDetectionEnum, access="rp", @@ -217,9 +252,6 @@ class AttributeDefs(Thermostat.AttributeDefs): external_open_window_detected = ZCLAttributeDef( id=0x4003, type=types.Bool, access="rw", is_manufacturer_specific=True ) # non-configurable reporting - window_open_feature = ZCLAttributeDef( - id=0x4051, type=types.Bool, access="rw", is_manufacturer_specific=True - ) # non-configurable reporting exercise_day_of_week = ZCLAttributeDef( id=0x4010, type=DanfossExerciseDayOfTheWeekEnum, @@ -235,7 +267,7 @@ class AttributeDefs(Thermostat.AttributeDefs): mounting_mode_control = ZCLAttributeDef( id=0x4013, type=types.Bool, access="rw", is_manufacturer_specific=True ) # non-configurable reporting - orientation = ZCLAttributeDef( # Horizontal = False and Vertical = True + orientation = ZCLAttributeDef( # etrv_orientation (Horizontal = False and Vertical = True) id=0x4014, type=types.Bool, access="rw", is_manufacturer_specific=True ) # non-configurable reporting external_measured_room_sensor = ZCLAttributeDef( @@ -244,24 +276,29 @@ class AttributeDefs(Thermostat.AttributeDefs): radiator_covered = ZCLAttributeDef( id=0x4016, type=types.Bool, access="rw", is_manufacturer_specific=True ) # non-configurable reporting + control_algorithm_scale_factor = ( + ZCLAttributeDef( # values in [0x01, 0x0A] and disabled by 0x1X + id=0x4020, + type=types.uint8_t, + access="rw", + is_manufacturer_specific=True, + ) + ) # non-configurable reporting heat_available = ZCLAttributeDef( id=0x4030, type=types.Bool, access="rw", is_manufacturer_specific=True ) # non-configurable reporting - heat_required = ZCLAttributeDef( + heat_required = ZCLAttributeDef( # heat_supply_request id=0x4031, type=types.Bool, access="rp", is_manufacturer_specific=True ) load_balancing_enable = ZCLAttributeDef( id=0x4032, type=types.Bool, access="rw", is_manufacturer_specific=True ) # non-configurable reporting - load_room_mean = ZCLAttributeDef( + load_room_mean = ZCLAttributeDef( # load_radiator_room_mean id=0x4040, type=types.int16s, access="rw", is_manufacturer_specific=True ) # non-configurable reporting (according to the documentation, you cannot read it, but it works anyway) - load_estimate = ZCLAttributeDef( + load_estimate = ZCLAttributeDef( # load_estimate_on_this_radiator id=0x404A, type=types.int16s, access="rp", is_manufacturer_specific=True ) - control_algorithm_scale_factor = ZCLAttributeDef( - id=0x4020, type=types.uint8_t, access="rw", is_manufacturer_specific=True - ) # non-configurable reporting regulation_setpoint_offset = ZCLAttributeDef( id=0x404B, type=types.int8s, access="rw", is_manufacturer_specific=True ) @@ -272,10 +309,16 @@ class AttributeDefs(Thermostat.AttributeDefs): is_manufacturer_specific=True, ) # non-configurable reporting adaptation_run_status = ZCLAttributeDef( - id=0x404D, type=types.bitmap8, access="rp", is_manufacturer_specific=True + id=0x404D, + type=DanfossAdaptationRunStatusBitmap, + access="rp", + is_manufacturer_specific=True, ) adaptation_run_settings = ZCLAttributeDef( - id=0x404E, type=types.bitmap8, access="rw", is_manufacturer_specific=True + id=0x404E, + type=DanfossAdaptationRunSettingsBitmap, + access="rw", + is_manufacturer_specific=True, ) preheat_status = ZCLAttributeDef( id=0x404F, type=types.Bool, access="rp", is_manufacturer_specific=True @@ -283,6 +326,9 @@ class AttributeDefs(Thermostat.AttributeDefs): preheat_time = ZCLAttributeDef( id=0x4050, type=types.uint32_t, access="rp", is_manufacturer_specific=True ) + window_open_feature = ZCLAttributeDef( # window_open_feature_on_off + id=0x4051, type=types.Bool, access="rw", is_manufacturer_specific=True + ) # non-configurable reporting async def write_attributes(self, attributes, manufacturer=None): """There are 2 types of setpoint changes: Fast and Slow. @@ -347,7 +393,10 @@ class DanfossDiagnosticCluster(CustomizedStandardCluster, Diagnostic): class AttributeDefs(Diagnostic.AttributeDefs): sw_error_code = ZCLAttributeDef( - id=0x4000, type=types.bitmap16, access="rpw", is_manufacturer_specific=True + id=0x4000, + type=DanfossSoftwareErrorCodeBitmap, + access="rpw", + is_manufacturer_specific=True, ) wake_time_avg = ZCLAttributeDef( id=0x4001, type=types.uint32_t, access="r", is_manufacturer_specific=True @@ -474,3 +523,6 @@ class DanfossThermostat(CustomDevice): } } } + + +z = DanfossAdaptationRunStatusBitmap From bd56e0cf5012485dd6a3aa9cf0f274b641e718fc Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Tue, 13 Feb 2024 16:43:44 +0100 Subject: [PATCH 66/79] change captica, --- zhaquirks/danfoss/thermostat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 0c657d7629..bbaff54cef 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -144,7 +144,7 @@ class DanfossSetpointCommandEnum(types.enum8): """Set behaviour to change the setpoint.""" Schedule = 0 # relatively slow - User_Interaction = 1 # aggressive change + User_interaction = 1 # aggressive change Preheat = 2 # invisible to user @@ -360,7 +360,7 @@ async def write_attributes(self, attributes, manufacturer=None): if fast_setpoint_change is not None: # On Danfoss a fast setpoint change is done through a command await self.setpoint_command( - DanfossSetpointCommandEnum.User_Interaction, + DanfossSetpointCommandEnum.User_interaction, fast_setpoint_change, manufacturer=manufacturer, ) From 49a455693838f256ef7527cad5376e7237ae8efb Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:09:34 +0100 Subject: [PATCH 67/79] correct capitalization for enums and bitmaps --- zhaquirks/danfoss/thermostat.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index bbaff54cef..b3c0a24931 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -107,30 +107,30 @@ class DanfossOpenWindowDetectionEnum(types.enum8): class DanfossSoftwareErrorCodeBitmap(types.bitmap16): """Danfoss software error code bitmap.""" - top_pcb_sensor_error = 0x0001 - side_pcb_sensor_error = 0x0002 - non_volatile_memory_error = 0x0004 - unknown_hw_error = 0x0008 + Top_pcb_sensor_error = 0x0001 + Side_pcb_sensor_error = 0x0002 + Non_volatile_memory_error = 0x0004 + Unknown_hw_error = 0x0008 # 0x0010 = N/A - motor_error = 0x0020 + Motor_error = 0x0020 # 0x0040 = N/A - invalid_internal_communication = 0x0080 + Invalid_internal_communication = 0x0080 # 0x0100 = N/A - invalid_clock_information = 0x0200 + Invalid_clock_information = 0x0200 # 0x0400 = N/A - radio_communication_error = 0x0800 - encoder_jammed = 0x1000 - low_battery = 0x2000 - critical_low_battery = 0x4000 + Radio_communication_error = 0x0800 + Encoder_jammed = 0x1000 + Low_battery = 0x2000 + Critical_low_battery = 0x4000 # 0x8000 = Reserved class DanfossAdaptationRunStatusBitmap(types.bitmap8): """Danfoss Adaptation run status bitmap.""" - in_progress = 0x0001 - valve_characteristic_found = 0x0002 - valve_characteristic_lost = 0x0004 + In_progress = 0x0001 + Valve_characteristic_found = 0x0002 + Valve_characteristic_lost = 0x0004 class DanfossAdaptationRunSettingsBitmap(types.bitmap8): From a78b1022001e38428ef735106dce0b56f70ce8e1 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Tue, 13 Feb 2024 17:45:16 +0100 Subject: [PATCH 68/79] comments --- zhaquirks/danfoss/thermostat.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index b3c0a24931..bf3a132d1b 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -76,7 +76,7 @@ class DanfossViewingDirectionEnum(types.enum8): class DanfossAdaptationRunControlEnum(types.enum8): """Initiate or Cancel adaptation run.""" - Nothing = 0x00 # not documented everywhere + Nothing = 0x00 # not documented in all documentation, but in some places and seems to work Initiate = 0x01 Cancel = 0x02 @@ -136,7 +136,7 @@ class DanfossAdaptationRunStatusBitmap(types.bitmap8): class DanfossAdaptationRunSettingsBitmap(types.bitmap8): """Danfoss Adaptation run settings bitmap.""" - Disabled = 0x00 + Disabled = 0x00 # Undocumented, but seems to work Enabled = 0x01 From 7ab8a7f435517c11d97342f9bbb83aa6a86e7cc2 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Wed, 21 Feb 2024 14:05:02 +0100 Subject: [PATCH 69/79] test failure and success correctly --- tests/test_danfoss.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/test_danfoss.py b/tests/test_danfoss.py index bf571295cc..2d7795364b 100644 --- a/tests/test_danfoss.py +++ b/tests/test_danfoss.py @@ -206,7 +206,7 @@ def mock_read_attributes(attrs, *args, **kwargs): if mock_attributes[attrs[0]].is_manufacturer_specific: reports = attrs - return [[545], [4545]] + return [[545]] # data is written to trv patch_danfoss_read_attributes = mock.patch.object( @@ -216,5 +216,27 @@ def mock_read_attributes(attrs, *args, **kwargs): ) with patch_danfoss_read_attributes: - await danfoss_thermostat_cluster._read_attributes([56454, 656]) + result, fail = await danfoss_thermostat_cluster._read_attributes([56454, 656]) + assert result + assert not fail + assert reports == [656] + + def mock_read_attributes_fail(attrs, *args, **kwargs): + nonlocal reports + if mock_attributes[attrs[0]].is_manufacturer_specific: + reports = attrs + + return [[545], [4545]] + + # data is written to trv + patch_danfoss_read_attributes_fail = mock.patch.object( + CustomCluster, + "_read_attributes", + mock.AsyncMock(side_effect=mock_read_attributes_fail), + ) + + with patch_danfoss_read_attributes_fail: + result, fail = await danfoss_thermostat_cluster._read_attributes([56454, 656]) + assert result + assert fail assert reports == [656] From 1fcc763be926b361fe6d58064e74ea3bcdf0ffed Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Wed, 21 Feb 2024 14:06:15 +0100 Subject: [PATCH 70/79] now correclty --- tests/test_danfoss.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_danfoss.py b/tests/test_danfoss.py index 2d7795364b..e6c6ab1a5b 100644 --- a/tests/test_danfoss.py +++ b/tests/test_danfoss.py @@ -216,9 +216,8 @@ def mock_read_attributes(attrs, *args, **kwargs): ) with patch_danfoss_read_attributes: - result, fail = await danfoss_thermostat_cluster._read_attributes([56454, 656]) + result = await danfoss_thermostat_cluster._read_attributes([56454, 656]) assert result - assert not fail assert reports == [656] def mock_read_attributes_fail(attrs, *args, **kwargs): From 77020d4c8ecf47a2da0a24c09a9fc528f0a37b0c Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sat, 24 Feb 2024 00:24:06 +0100 Subject: [PATCH 71/79] remove accidentally added line --- zhaquirks/danfoss/thermostat.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index bf3a132d1b..5aa71cf152 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -523,6 +523,3 @@ class DanfossThermostat(CustomDevice): } } } - - -z = DanfossAdaptationRunStatusBitmap From f3b2f5805e8d897356f71d9d6aa31b8a6e04ded7 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Sat, 24 Feb 2024 00:48:56 +0100 Subject: [PATCH 72/79] swap --- zhaquirks/danfoss/thermostat.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 5aa71cf152..86325ce596 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -57,14 +57,14 @@ ) from zhaquirks.quirk_ids import DANFOSS_ALLY_THERMOSTAT -occupied_heating_setpoint = Thermostat.AttributeDefs.occupied_heating_setpoint -system_mode = Thermostat.AttributeDefs.system_mode -min_heat_setpoint_limit = Thermostat.AttributeDefs.min_heat_setpoint_limit - DANFOSS = "Danfoss" HIVE = DANFOSS POPP = "D5X84YU" +occupied_heating_setpoint = Thermostat.AttributeDefs.occupied_heating_setpoint +system_mode = Thermostat.AttributeDefs.system_mode +min_heat_setpoint_limit = Thermostat.AttributeDefs.min_heat_setpoint_limit + class DanfossViewingDirectionEnum(types.enum8): """Default (button above screen when looking at it) or Inverted (button below screen when looking at it).""" From c5cc05ca62afbcb4b08aeb23d5e48f32330f47a7 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:04:40 +0200 Subject: [PATCH 73/79] suggestions --- tests/test_danfoss.py | 21 ++++++--------------- zhaquirks/danfoss/thermostat.py | 11 ++++++----- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/tests/test_danfoss.py b/tests/test_danfoss.py index e6c6ab1a5b..a25c569b92 100644 --- a/tests/test_danfoss.py +++ b/tests/test_danfoss.py @@ -44,6 +44,7 @@ def test_popp_signature(assert_signature_matches_quirk): ) +@mock.patch("zigpy.zcl.Cluster.bind", mock.AsyncMock()) async def test_danfoss_time_bind(zigpy_device_from_quirk): device = zigpy_device_from_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat) @@ -56,28 +57,18 @@ def mock_write(attributes, manufacturer=None): ] return [records, []] - def mock_wildcard(*args, **kwargs): - return - patch_danfoss_trv_write = mock.patch.object( danfoss_time_cluster, "_write_attributes", mock.AsyncMock(side_effect=mock_write), ) - patch_danfoss_trv_bind = mock.patch.object( - Time, - "bind", - mock.AsyncMock(side_effect=mock_wildcard), - ) - - with patch_danfoss_trv_bind: - with patch_danfoss_trv_write: - await danfoss_time_cluster.bind() + with patch_danfoss_trv_write: + await danfoss_time_cluster.bind() - assert 0x0000 in danfoss_time_cluster._attr_cache - assert 0x0001 in danfoss_time_cluster._attr_cache - assert 0x0002 in danfoss_time_cluster._attr_cache + assert 0x0000 in danfoss_time_cluster._attr_cache + assert 0x0001 in danfoss_time_cluster._attr_cache + assert 0x0002 in danfoss_time_cluster._attr_cache async def test_danfoss_thermostat_write_attributes(zigpy_device_from_quirk): diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 86325ce596..6ba7f52712 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -29,8 +29,9 @@ """ -from datetime import datetime -from typing import Any, Callable, List +from collections.abc import Callable +from datetime import UTC, datetime +from typing import Any, List from zigpy import types from zigpy.profiles import zha @@ -441,11 +442,11 @@ class DanfossTimeCluster(CustomizedStandardCluster, Time): """Danfoss cluster for fixing the time.""" async def write_time(self): - epoch = datetime(2000, 1, 1, 0, 0, 0, 0) - current_time = (datetime.utcnow() - epoch).total_seconds() + epoch = datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=UTC) + current_time = (datetime.now(UTC) - epoch).total_seconds() time_zone = ( - datetime.fromtimestamp(86400) - datetime.utcfromtimestamp(86400) + datetime.fromtimestamp(86400) - datetime.fromtimestamp(86400, UTC) ).total_seconds() await self.write_attributes( From 2f84b33c750eb1ec50391242d73caeb31b18da05 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:15:13 +0200 Subject: [PATCH 74/79] ruffle --- tests/test_danfoss.py | 11 +++++++++-- zhaquirks/danfoss/thermostat.py | 18 ++++++++++++++++-- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/tests/test_danfoss.py b/tests/test_danfoss.py index a25c569b92..730bcc0026 100644 --- a/tests/test_danfoss.py +++ b/tests/test_danfoss.py @@ -1,4 +1,4 @@ -"""Tests the Danfoss quirk (all tests were written for the Popp eT093WRO)""" +"""Tests the Danfoss quirk (all tests were written for the Popp eT093WRO).""" from unittest import mock from zigpy.quirks import CustomCluster @@ -14,6 +14,7 @@ def test_popp_signature(assert_signature_matches_quirk): + """Test the signature matching the Device Class.""" signature = { "node_descriptor": "NodeDescriptor(logical_type=, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=, mac_capability_flags=, manufacturer_code=4678, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=True, *is_full_function_device=False, *is_mains_powered=False, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False)", # SizePrefixedSimpleDescriptor(endpoint=1, profile=260, device_type=769, device_version=1, input_clusters=[0, 1, 3, 10, 32, 513, 516, 2821], output_clusters=[0, 25]) @@ -46,6 +47,7 @@ def test_popp_signature(assert_signature_matches_quirk): @mock.patch("zigpy.zcl.Cluster.bind", mock.AsyncMock()) async def test_danfoss_time_bind(zigpy_device_from_quirk): + """Test the time being set when binding the Time cluster.""" device = zigpy_device_from_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat) danfoss_time_cluster = device.endpoints[1].in_clusters[Time.cluster_id] @@ -72,6 +74,7 @@ def mock_write(attributes, manufacturer=None): async def test_danfoss_thermostat_write_attributes(zigpy_device_from_quirk): + """Test the Thermostat writes behaving correctly, in particular regarding setpoint.""" device = zigpy_device_from_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat) danfoss_thermostat_cluster = device.endpoints[1].in_clusters[Thermostat.cluster_id] @@ -79,7 +82,7 @@ async def test_danfoss_thermostat_write_attributes(zigpy_device_from_quirk): def mock_write(attributes, manufacturer=None): records = [ WriteAttributesStatusRecord(foundation.Status.SUCCESS) - for attr in attributes + for _ in attributes ] return [records, []] @@ -142,6 +145,10 @@ def mock_setpoint(oper, sett, manufacturer=None): async def test_customized_standardcluster(zigpy_device_from_quirk): + """Test customized standard cluster class correctly separating zigbee operations. + + This is regarding manufacturer specific attributes. + """ device = zigpy_device_from_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat) danfoss_thermostat_cluster = device.endpoints[1].in_clusters[Thermostat.cluster_id] diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 6ba7f52712..3c3238db8f 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -31,7 +31,7 @@ from collections.abc import Callable from datetime import UTC, datetime -from typing import Any, List +from typing import Any from zigpy import types from zigpy.profiles import zha @@ -151,6 +151,7 @@ class DanfossSetpointCommandEnum(types.enum8): class DanfossPreheatCommandEnum(types.enum8): """Set behaviour of preheat command. + Only one option available, but other values are possible in the future. """ @@ -167,6 +168,7 @@ class CustomizedStandardCluster(CustomCluster): @staticmethod def combine_results(*result_lists): + """Combine results from 1 or more result lists from zigbee commands.""" success_global = [] failure_global = [] for result in result_lists: @@ -183,7 +185,7 @@ def combine_results(*result_lists): async def split_command( self, - records: List[Any], + records: list[Any], func: Callable, extract_attrid: Callable[[Any], int], *args, @@ -228,6 +230,8 @@ class DanfossThermostatCluster(CustomizedStandardCluster, Thermostat): """Danfoss cluster for standard and proprietary danfoss attributes.""" class ServerCommandDefs(Thermostat.ServerCommandDefs): + """Server Command Definitions.""" + setpoint_command = ZCLCommandDef( id=0x40, schema={ @@ -244,6 +248,8 @@ class ServerCommandDefs(Thermostat.ServerCommandDefs): ) class AttributeDefs(Thermostat.AttributeDefs): + """Attribute Definitions.""" + open_window_detection = ZCLAttributeDef( # etrv_open_window_detection id=0x4000, type=DanfossOpenWindowDetectionEnum, @@ -333,6 +339,7 @@ class AttributeDefs(Thermostat.AttributeDefs): async def write_attributes(self, attributes, manufacturer=None): """There are 2 types of setpoint changes: Fast and Slow. + Fast is used for immediate changes; this is done using a command (setpoint_command). Slow is used for scheduled changes; this is done using an attribute (occupied_heating_setpoint). In case of a change on occupied_heating_setpoint, a setpoint_command is used. @@ -370,6 +377,7 @@ async def write_attributes(self, attributes, manufacturer=None): async def bind(self): """According to the documentation of Zigbee2MQTT there is a bug in the Danfoss firmware with the time. + It doesn't request it, so it has to be fed the correct time. """ await self.endpoint.time.write_time() @@ -381,6 +389,8 @@ class DanfossUserInterfaceCluster(CustomizedStandardCluster, UserInterface): """Danfoss cluster for standard and proprietary danfoss attributes.""" class AttributeDefs(UserInterface.AttributeDefs): + """Attribute Definitions.""" + viewing_direction = ZCLAttributeDef( id=0x4000, type=DanfossViewingDirectionEnum, @@ -393,6 +403,8 @@ class DanfossDiagnosticCluster(CustomizedStandardCluster, Diagnostic): """Danfoss cluster for standard and proprietary danfoss attributes.""" class AttributeDefs(Diagnostic.AttributeDefs): + """Attribute Definitions.""" + sw_error_code = ZCLAttributeDef( id=0x4000, type=DanfossSoftwareErrorCodeBitmap, @@ -442,6 +454,7 @@ class DanfossTimeCluster(CustomizedStandardCluster, Time): """Danfoss cluster for fixing the time.""" async def write_time(self): + """Write time info to Time Cluster.""" epoch = datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=UTC) current_time = (datetime.now(UTC) - epoch).total_seconds() @@ -459,6 +472,7 @@ async def write_time(self): async def bind(self): """According to the documentation of Zigbee2MQTT there is a bug in the Danfoss firmware with the time. + It doesn't request it, so it has to be fed the correct time. """ result = await super().bind() From 6d8f204d88467be8d8241a552d4350390a80287e Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:15:38 +0200 Subject: [PATCH 75/79] Update tests/test_danfoss.py Co-authored-by: TheJulianJES --- tests/test_danfoss.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_danfoss.py b/tests/test_danfoss.py index 730bcc0026..df0be90160 100644 --- a/tests/test_danfoss.py +++ b/tests/test_danfoss.py @@ -50,7 +50,7 @@ async def test_danfoss_time_bind(zigpy_device_from_quirk): """Test the time being set when binding the Time cluster.""" device = zigpy_device_from_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat) - danfoss_time_cluster = device.endpoints[1].in_clusters[Time.cluster_id] + danfoss_time_cluster = device.endpoints[1].time def mock_write(attributes, manufacturer=None): records = [ From c29ae992e95214f3826de497b148fe438af9d02c Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:15:53 +0200 Subject: [PATCH 76/79] Update tests/test_danfoss.py Co-authored-by: TheJulianJES --- tests/test_danfoss.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_danfoss.py b/tests/test_danfoss.py index df0be90160..81320fbf52 100644 --- a/tests/test_danfoss.py +++ b/tests/test_danfoss.py @@ -77,7 +77,7 @@ async def test_danfoss_thermostat_write_attributes(zigpy_device_from_quirk): """Test the Thermostat writes behaving correctly, in particular regarding setpoint.""" device = zigpy_device_from_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat) - danfoss_thermostat_cluster = device.endpoints[1].in_clusters[Thermostat.cluster_id] + danfoss_thermostat_cluster = device.endpoints[1].thermostat def mock_write(attributes, manufacturer=None): records = [ From d37c214476d5d87eb0fdfb12ea3ff8cab8a986bf Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Tue, 9 Apr 2024 16:23:23 +0200 Subject: [PATCH 77/79] suggestions --- tests/test_danfoss.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_danfoss.py b/tests/test_danfoss.py index 81320fbf52..f43e8a7714 100644 --- a/tests/test_danfoss.py +++ b/tests/test_danfoss.py @@ -3,7 +3,6 @@ from zigpy.quirks import CustomCluster from zigpy.zcl import foundation -from zigpy.zcl.clusters.general import Time from zigpy.zcl.clusters.hvac import Thermostat from zigpy.zcl.foundation import WriteAttributesStatusRecord, ZCLAttributeDef @@ -51,6 +50,7 @@ async def test_danfoss_time_bind(zigpy_device_from_quirk): device = zigpy_device_from_quirk(zhaquirks.danfoss.thermostat.DanfossThermostat) danfoss_time_cluster = device.endpoints[1].time + danfoss_thermostat_cluster = device.endpoints[1].thermostat def mock_write(attributes, manufacturer=None): records = [ @@ -66,7 +66,7 @@ def mock_write(attributes, manufacturer=None): ) with patch_danfoss_trv_write: - await danfoss_time_cluster.bind() + await danfoss_thermostat_cluster.bind() assert 0x0000 in danfoss_time_cluster._attr_cache assert 0x0001 in danfoss_time_cluster._attr_cache From 94b61128b585c43154248d73e9ca69cd9f7b2349 Mon Sep 17 00:00:00 2001 From: Caius-Bonus <123886836+Caius-Bonus@users.noreply.github.com> Date: Tue, 9 Apr 2024 19:17:35 +0200 Subject: [PATCH 78/79] fix test --- tests/test_danfoss.py | 2 +- zhaquirks/danfoss/thermostat.py | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/tests/test_danfoss.py b/tests/test_danfoss.py index f43e8a7714..28a2c08b0a 100644 --- a/tests/test_danfoss.py +++ b/tests/test_danfoss.py @@ -55,7 +55,7 @@ async def test_danfoss_time_bind(zigpy_device_from_quirk): def mock_write(attributes, manufacturer=None): records = [ WriteAttributesStatusRecord(foundation.Status.SUCCESS) - for attr in attributes + for _ in attributes ] return [records, []] diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 3c3238db8f..1bae1fc37a 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -27,10 +27,9 @@ Broken ZCL attributes: 0x0204 - TemperatureDisplayMode (0x0000): Writing doesn't seem to do anything """ - - from collections.abc import Callable from datetime import UTC, datetime +import time from typing import Any from zigpy import types @@ -454,19 +453,19 @@ class DanfossTimeCluster(CustomizedStandardCluster, Time): """Danfoss cluster for fixing the time.""" async def write_time(self): - """Write time info to Time Cluster.""" + """Write time info to Time Cluster. + + It supports adjusting for daylight saving time, but this is not trivial to retrieve with the modules: + zoneinfo, datetime or time + """ epoch = datetime(2000, 1, 1, 0, 0, 0, 0, tzinfo=UTC) current_time = (datetime.now(UTC) - epoch).total_seconds() - time_zone = ( - datetime.fromtimestamp(86400) - datetime.fromtimestamp(86400, UTC) - ).total_seconds() - await self.write_attributes( { "time": current_time, "time_status": 0b00000010, # only bit 1 can be set - "time_zone": time_zone, + "time_zone": time.timezone } ) From 2684a356a8826470c51c89f2d247ed7f8cdd068a Mon Sep 17 00:00:00 2001 From: TheJulianJES Date: Tue, 9 Apr 2024 19:46:18 +0200 Subject: [PATCH 79/79] Remove empty lines --- zhaquirks/danfoss/thermostat.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/zhaquirks/danfoss/thermostat.py b/zhaquirks/danfoss/thermostat.py index 1bae1fc37a..eaa606a2c4 100644 --- a/zhaquirks/danfoss/thermostat.py +++ b/zhaquirks/danfoss/thermostat.py @@ -475,9 +475,7 @@ async def bind(self): It doesn't request it, so it has to be fed the correct time. """ result = await super().bind() - await self.write_time() - return result