From 055ca4c343a4010399175208b712867771de4c7f Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Sun, 15 Oct 2023 13:49:54 +0200 Subject: [PATCH 01/47] move to certified device (https://github.com/zigbeefordomoticz/z4d-certified-devices/commit/aa415ddaffa184a59d5949c0d5a4cc02cfed9392) --- Conf/Local-Devices/TS0601-Solar-Siren.json | 43 ---------------------- 1 file changed, 43 deletions(-) delete mode 100644 Conf/Local-Devices/TS0601-Solar-Siren.json diff --git a/Conf/Local-Devices/TS0601-Solar-Siren.json b/Conf/Local-Devices/TS0601-Solar-Siren.json deleted file mode 100644 index 5f95e11f8..000000000 --- a/Conf/Local-Devices/TS0601-Solar-Siren.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "_source": "https://github.com/zigbeefordomoticz/Domoticz-Zigbee/issues/1581", - "_comment": "NEO - Sirène intelligente extérieure Zigbee Tuya (alimentation 5V/1A ou batterie + panneau solaire)", - "_version": "1.0", - "Identifier": [ - [ "TS0601", "_TZE200_nlrfgpny"] - ], - "Ep": { - "01": { - "0000": "", - "0004": "", - "ef00": "", - "000a": "", - "0019": "", - "Type": "SwitchAlarm/TamperSwitch/Tamper/Notification" - } - }, - "Type": "", - "ClusterToBind": [], - "ConfigureReporting": {}, - "ReadAttributes": { - "0000": [ "0004", "0000", "0001", "0005", "0007", "fffe" ], - "0009": [] - }, - "TUYA_REGISTRATION": 19, - "TS0601_DP": { - "01": { "store_tuya_attribute": "AlarmState", "sensor_type": "tamper"}, - "06": { "store_tuya_attribute": "Charging", "sensor_type": "charging_mode"}, - "07": { "store_tuya_attribute": "TuyaAlarmDuration", "action_type": "TuyaAlarmDuration"}, - "0d": { "store_tuya_attribute": "TuyaAlarmSwitch", "action_type": "TuyaAlarmSwitch"}, - "0e": { "store_tuya_attribute": "Battery", "sensor_type": "battery"}, - "14": { "store_tuya_attribute": "TamperAlarmState", "sensor_type": "tamper"}, - "15": { "store_tuya_attribute": "TuyaAlarmMelody", "action_type": "TuyaAlarmMelody"}, - "65": { "store_tuya_attribute": "TuyaTamperSwitch", "action_type": "TuyaTamperSwitch"}, - "66": { "store_tuya_attribute": "TuyaAlarmMode", "action_type": "TuyaAlarmMode"} - }, - "BatteryDevice": 1, - "Param": { - "TuyaAlarmMelody": 0, - "TuyaAlarmMode": 2, - "TuyaAlarmDuration": 1 - } -} From 798b9cd5b92c295df5f0d2c797000480eac84f6a Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Sun, 15 Oct 2023 17:06:45 +0200 Subject: [PATCH 02/47] fix Heiman specific command handling --- Modules/heiman.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/Modules/heiman.py b/Modules/heiman.py index 89d9ab423..ef3541699 100644 --- a/Modules/heiman.py +++ b/Modules/heiman.py @@ -23,14 +23,14 @@ def heimanReadRawAPS(self, Devices, nwkId, srcEp, clusterId, targetNwkId, target command = payLoad[8:10] - self.log.logging( "Heiman", "Debug", "heimanReadRawAPS - Nwkid: %s Ep: %s, command found: %s" % ( - nwkId, srcEp, command), nwkId, ) - - if int(command, 16) in {0xF0, 0xF1, 0xF2, 0xF3,}: - self.log.logging( "Heiman", "Debug", "heimanReadRawAPS - Nwkid: %s Ep: %s, command found: %s UNKNOW" % ( - nwkId, srcEp, command), nwkId, ) - MajDomoDevice(self, Devices, nwkId, "01", "fc80", int(command,16)) - else: - self.log.logging( "Heiman", "Error", "heimanReadRawAPS - Nwkid: %s Ep: %s, command found: %s UNKNOW" % ( + self.log.logging( "Heiman", "Debug", "heimanReadRawAPS - Nwkid: %s Ep: %s, command found: %s" % ( nwkId, srcEp, command), nwkId, ) + + if int(command, 16) in {0xF0, 0xF1, 0xF2, 0xF3,}: + self.log.logging( "Heiman", "Debug", "heimanReadRawAPS - Nwkid: %s Ep: %s, command found: %s UNKNOW" % ( + nwkId, srcEp, command), nwkId, ) + MajDomoDevice(self, Devices, nwkId, "01", "fc80", int(command,16)) + else: + self.log.logging( "Heiman", "Error", "heimanReadRawAPS - Nwkid: %s Ep: %s, command found: %s UNKNOW" % ( + nwkId, srcEp, command), nwkId, ) \ No newline at end of file From d4eae166cadca053464076f0c07dc06b3d80d5db Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Sun, 15 Oct 2023 20:58:57 +0200 Subject: [PATCH 03/47] fix coding error --- Zigbee/encoder_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Zigbee/encoder_tools.py b/Zigbee/encoder_tools.py index bee7e93c4..4f5bfd595 100644 --- a/Zigbee/encoder_tools.py +++ b/Zigbee/encoder_tools.py @@ -54,7 +54,7 @@ def decode_endian_data(data, datatype, len_stream=None): # we expect 7 bytes lenght data = data[2:16] # 7 bytes - 56b - return "%016x" % ("%014x" % struct.unpack(">Q", struct.pack("Q", int("00" + data[:14], 16)))[0])[:14] + return ("%016x" % struct.unpack(">Q", struct.pack("Q", int("00" + data[:14], 16)))[0])[:14] if data_type_id in {0x0F, 0x1F, 0x27, 0x2F, 0x3A, 0xF0}: # 8 bytes - 64b From 3d23a921f82d46f5977736a052caa880d6248acd Mon Sep 17 00:00:00 2001 From: Pipiche <8291674+pipiche38@users.noreply.github.com> Date: Sun, 15 Oct 2023 21:25:24 +0200 Subject: [PATCH 04/47] Fix group cpr412 (#1653) * makes PR412 and CPR412 handling correctly the report attribute, and consequently get group working * implement LevelControl Stop --- Classes/GroupMgtv2/GrpDomoticz.py | 62 ++++++++++++++++++++----------- Modules/domoMaj.py | 41 ++++++++++---------- Modules/readZclClusters.py | 3 +- Zigbee/zclCommands.py | 11 +++++- 4 files changed, 74 insertions(+), 43 deletions(-) diff --git a/Classes/GroupMgtv2/GrpDomoticz.py b/Classes/GroupMgtv2/GrpDomoticz.py index 04592479b..f507131ac 100644 --- a/Classes/GroupMgtv2/GrpDomoticz.py +++ b/Classes/GroupMgtv2/GrpDomoticz.py @@ -10,7 +10,7 @@ from Classes.GroupMgtv2.GrpCommands import (set_hue_saturation, set_kelvin_color, set_rgb_color) from Classes.GroupMgtv2.GrpDatabase import update_due_to_nwk_id_change -from Modules.tools import Hex_Format +from Modules.tools import Hex_Format, is_hex from Modules.zigateConsts import ADDRESS_MODE, LEGRAND_REMOTES, ZIGATE_EP from Zigbee.zclCommands import (zcl_group_level_move_to_level, zcl_group_move_to_level_with_onoff, @@ -19,6 +19,7 @@ zcl_group_onoff_on, zcl_group_window_covering_off, zcl_group_window_covering_on, + zcl_group_move_to_level_stop, zcl_group_window_covering_stop) WIDGET_STYLE = { @@ -185,6 +186,7 @@ def best_group_widget(self, GroupId): # } GroupWidgetType = None + GroupWidgetStyle = None self.logging("Debug", "best_group_widget Device - %s" % str(self.ListOfGroups[GroupId]["Devices"])) for NwkId, devEp, iterIEEE in self.ListOfGroups[GroupId]["Devices"]: @@ -194,7 +196,7 @@ def best_group_widget(self, GroupId): if NwkId == "0000": continue - self.logging("debug", "bestGroupWidget - Group: %s processing %s" % (GroupId, NwkId)) + self.logging("Debug", "bestGroupWidget - Group: %s processing %s" % (GroupId, NwkId)) if NwkId not in self.ListOfDevices: # We have some inconsistency ! continue @@ -207,9 +209,13 @@ def best_group_widget(self, GroupId): WidgetType = self.ListOfDevices[NwkId]["Ep"][devEp]["ClusterType"][DomoDeviceUnit] self.logging("Debug", "------------ GroupWidget: %s WidgetType: %s" % (GroupWidgetType, WidgetType)) + if WidgetType == "LvlControl" and "Blind" in self.ListOfDevices[NwkId]["Type"]: + GroupWidgetStyle = "BlindPercentInverted" + if WidgetType in ("VenetianInverted", "VanneInverted", "CurtainInverted"): # Those widgets are commanded via cluster Level Control GroupWidgetType = "LvlControl" + GroupWidgetStyle = "VenetianInverted" continue if GroupWidgetType is None and WidgetType in WIDGET_STYLE: @@ -245,7 +251,6 @@ def best_group_widget(self, GroupId): GroupWidgetType = WidgetType continue - if WidgetType in ("Venetian", "WindowCovering", "BlindPercentInverted"): GroupWidgetType = WidgetType @@ -281,13 +286,13 @@ def best_group_widget(self, GroupId): else: self.ListOfGroups[GroupId]["Cluster"] = "" - self.logging( - "Debug", - "best_group_widget for GroupId: %s Found WidgetType: %s Widget: %s" - % (GroupId, GroupWidgetType, WIDGET_STYLE.get(GroupWidgetType, WIDGET_STYLE["ColorControlFull"])), - ) + self.logging( "Debug", "best_group_widget for GroupId: %s Found WidgetType: %s Widget: %s" % ( + GroupId, GroupWidgetType, WIDGET_STYLE.get(GroupWidgetType, WIDGET_STYLE["ColorControlFull"])), ) - return WIDGET_STYLE.get(GroupWidgetType, WIDGET_STYLE["ColorControlFull"]) + if GroupWidgetStyle is None: + GroupWidgetStyle = GroupWidgetType + + return WIDGET_STYLE.get(GroupWidgetStyle, WIDGET_STYLE["ColorControlFull"]) def update_domoticz_group_device(self, GroupId): @@ -317,7 +322,7 @@ def update_domoticz_group_device(self, GroupId): if "Cluster" in self.ListOfGroups[GroupId]: Cluster = self.ListOfGroups[GroupId]["Cluster"] - countOn = countOff = 0 + countStop = countOn = countOff = 0 nValue = 0 if self.pluginconf.pluginConf["OnIfOneOn"] else 1 sValue = level = None for NwkId, Ep, IEEE in self.ListOfGroups[GroupId]["Devices"]: @@ -347,11 +352,13 @@ def update_domoticz_group_device(self, GroupId): and Cluster in ("0006", "0008", "0300") and "0006" in self.ListOfDevices[NwkId]["Ep"][Ep] and "0000" in self.ListOfDevices[NwkId]["Ep"][Ep]["0006"] - and str(self.ListOfDevices[NwkId]["Ep"][Ep]["0006"]["0000"]).isdigit() + and is_hex( str(self.ListOfDevices[NwkId]["Ep"][Ep]["0006"]["0000"]) ) ): self.logging( "Debug", "update_domoticz_group_device - Cluster ON/OFF Group: %s NwkId: %s Ep: %s Value: %s" %( GroupId, NwkId, Ep, self.ListOfDevices[NwkId]["Ep"][Ep]["0006"]["0000"])) - if int(self.ListOfDevices[NwkId]["Ep"][Ep]["0006"]["0000"]) != 0: + if str(self.ListOfDevices[NwkId]["Ep"][Ep]["0006"]["0000"]) == "f0": + countStop += 1 + elif int(self.ListOfDevices[NwkId]["Ep"][Ep]["0006"]["0000"]) != 0: countOn += 1 else: countOff += 1 @@ -381,10 +388,12 @@ def update_domoticz_group_device(self, GroupId): level = lvl_value if level is None else (level + lvl_value) // 2 nValue, sValue = ValuesForVenetian(level) - self.logging( "Debug", "update_domoticz_group_device - Processing: Group: %s %s/%s On: %s, Off: %s level: %s" % ( - GroupId, NwkId, Ep, countOn, countOff, level), ) + self.logging( "Debug", "update_domoticz_group_device - Processing: Group: %s %s/%s On: %s, Off: %s Stop: %s, level: %s" % ( + GroupId, NwkId, Ep, countOn, countOff, countStop, level), ) - if self.pluginconf.pluginConf["OnIfOneOn"]: + if countStop > 0: + nValue = 17 + elif self.pluginconf.pluginConf["OnIfOneOn"]: if countOn > 0: nValue = 1 elif countOff > 0: @@ -392,12 +401,18 @@ def update_domoticz_group_device(self, GroupId): self.logging( "Debug", "update_domoticz_group_device - Processing: Group: %s == > nValue: %s, level: %s" % ( GroupId, nValue, level), ) + # At that stage # nValue == 0 if Off # nValue == 1 if Open/On + # nValue == 17 if Stop # level is None, so we use nValue/sValue # level is not None; so we have a LvlControl - if sValue is None and level: + if nValue == 17: + # Stop + sValue = "0" + + elif sValue is None and level: if self.Devices[unit].SwitchType not in (13, 14, 15, 16): # Not a Shutter/Blind analogValue = level @@ -439,11 +454,8 @@ def update_domoticz_group_device(self, GroupId): else: sValue = "On" - self.logging( - "Debug", - "update_domoticz_group_device - Processing: Group: %s == > from %s:%s to %s:%s" - % (GroupId, self.Devices[unit].nValue, self.Devices[unit].sValue, nValue, sValue), - ) + self.logging( "Debug", "update_domoticz_group_device - Processing: Group: %s == > from %s:%s to %s:%s" % ( + GroupId, self.Devices[unit].nValue, self.Devices[unit].sValue, nValue, sValue), ) if nValue != self.Devices[unit].nValue or sValue != self.Devices[unit].sValue: self.logging("Log", "UpdateGroup - (%15s) %s:%s" % (self.Devices[unit].Name, nValue, sValue)) self.Devices[unit].Update(nValue, sValue) @@ -680,6 +692,14 @@ def processCommand(self, unit, GrpId, Command, Level, Color_): sValue = "On" self.Devices[unit].Update(nValue=int(nValue), sValue=str(sValue)) + elif Command in ( "Stop",) and self.ListOfGroups[GrpId]["Cluster"] == "0102": + # Windowscovering Stop + zcl_group_window_covering_stop(self, GrpId, "01", EPout) + + elif Command in ( "Stop",) and self.ListOfGroups[GrpId]["Cluster"] == "0008": + # SetLevel Off + zcl_group_move_to_level_stop(self, GrpId, EPout) + elif Command == "Set Level": # Level: % value of move # Converted to value , raw value from 0 to 255 diff --git a/Modules/domoMaj.py b/Modules/domoMaj.py index bba0fc640..aa38a73aa 100644 --- a/Modules/domoMaj.py +++ b/Modules/domoMaj.py @@ -1184,14 +1184,27 @@ def MajDomoDevice(self, Devices, NWKID, Ep, clusterID, value, Attribute_="", Col # We do update only if this is a On/off UpdateDevice_v2(self, Devices, DeviceUnit, 1, "On", BatteryLevel, SignalLevel) + elif WidgetType == "VenetianInverted" and model_name in ( "PR412", "CPR412", "CPR412-E") and clusterID == "0006": + self.log.logging( "Widget", "Debug", "--++-> %s/%s ClusterType: %s Updating %s Value: %s" % (NWKID, Ep, ClusterType, WidgetType, value), NWKID, ) + # nValue will depends if we are on % or not + if value == '01': + nValue = 0 + sValue = "0" + + elif value == '00': + nValue = 1 + sValue = "100" + + elif value == 'f0': + nValue = 17 + sValue = "0" + + self.log.logging("Widget", "Debug", "------> %s %s/%s Value: %s:%s" % (WidgetType, NWKID, Ep, nValue, sValue), NWKID) + UpdateDevice_v2(self, Devices, DeviceUnit, nValue, sValue, BatteryLevel, SignalLevel) + elif WidgetType in ("VenetianInverted", "Venetian", "WindowCovering", "VanneInverted", "Vanne", "Curtain", "CurtainInverted"): _value = int(value, 16) - self.log.logging( - "Widget", - "Debug", - "------> %s/%s ClusterType: %s Updating %s Value: %s" % (NWKID, Ep, ClusterType, WidgetType, _value), - NWKID, - ) + self.log.logging( "Widget", "Debug", "------> %s/%s ClusterType: %s Updating %s Value: %s" % (NWKID, Ep, ClusterType, WidgetType, _value), NWKID, ) if WidgetType in ("VenetianInverted", "VanneInverted"): _value = 100 - _value self.log.logging("Widget", "Debug", "------> Patching %s/%s Value: %s" % (NWKID, Ep, _value), NWKID) @@ -1258,13 +1271,7 @@ def MajDomoDevice(self, Devices, NWKID, Ep, clusterID, value, Attribute_="", Col UpdateDevice_v2(self, Devices, DeviceUnit, nValue, str(_value), BatteryLevel, SignalLevel) if "LvlControl" in ClusterType: # LvlControl ( 0x0008) - if WidgetType == "LvlControl" or ( - WidgetType - in ( - "BSO-Volet", - "Blind", - ) - ): + if WidgetType == "LvlControl" or ( WidgetType in ( "BSO-Volet", "Blind", ) ): # We need to handle the case, where we get an update from a Read Attribute or a Reporting message # We might get a Level, but the device is still Off and we shouldn't make it On . nValue = None @@ -1320,13 +1327,7 @@ def MajDomoDevice(self, Devices, NWKID, Ep, clusterID, value, Attribute_="", Col nValue, normalized_value, Devices[DeviceUnit].SwitchType), NWKID, ) UpdateDevice_v2( self, Devices, DeviceUnit, Devices[DeviceUnit].nValue, str(normalized_value), BatteryLevel, SignalLevel, ) - elif WidgetType in ( - "ColorControlRGB", - "ColorControlWW", - "ColorControlRGBWW", - "ColorControlFull", - "ColorControl", - ): + elif WidgetType in ( "ColorControlRGB", "ColorControlWW", "ColorControlRGBWW", "ColorControlFull", "ColorControl", ): if Devices[DeviceUnit].nValue != 0 or Devices[DeviceUnit].sValue != "Off": nValue, sValue = getDimmerLevelOfColor(self, value) UpdateDevice_v2(self, Devices, DeviceUnit, nValue, str(sValue), BatteryLevel, SignalLevel, Color_) diff --git a/Modules/readZclClusters.py b/Modules/readZclClusters.py index 689fe0a8f..ca658e1f2 100644 --- a/Modules/readZclClusters.py +++ b/Modules/readZclClusters.py @@ -386,7 +386,8 @@ def majdomodevice_possiblevalues( self, MsgSrcEp, MsgClusterId, MsgAttrID, model return True eval_result = eval( _majdomodeviceValidValues ) - self.log.logging("ZclClusters", "Debug", " . majdomodevice_possiblevalues: %s -> %s" %( eval_result, _majdomodeviceValidValues)) + self.log.logging("ZclClusters", "Debug", " . majdomodevice_possiblevalues: >%s<(%s) %s -> %s" %( + value, type(value), eval_result, _majdomodeviceValidValues)) return eval_result diff --git a/Zigbee/zclCommands.py b/Zigbee/zclCommands.py index 2f58a6f42..28637ba78 100644 --- a/Zigbee/zclCommands.py +++ b/Zigbee/zclCommands.py @@ -447,6 +447,11 @@ def zcl_move_to_level_with_onoff(self, nwkid, EPout, OnOff, level, transition="0 return send_zigatecmd_zcl_noack(self, nwkid, "0081", data) return send_zigatecmd_zcl_ack(self, nwkid, "0081", data) +def zcl_move_to_level_stop(self, nwkid, EPout, ackIsDisabled=DEFAULT_ACK_MODE): + self.log.logging("zclCommand", "Debug", "zcl_move_to_level_stop %s %s" % (nwkid, EPout, )) + zcl_command_formated_logging( self, "zcl_move_to_level_stop", nwkid, EPout, "0008", "Stop", ackIsDisabled) + if "ControllerInRawMode" in self.pluginconf.pluginConf and self.pluginconf.pluginConf["ControllerInRawMode"]: + return zcl_raw_level_move_to_level(self, nwkid, ZIGATE_EP, EPout, "Stop") def zcl_group_move_to_level_with_onoff(self, nwkid, EPout, OnOff, level, transition="0000", ackIsDisabled=DEFAULT_ACK_MODE): self.log.logging("zclCommand", "Debug", "zcl_move_to_level_with_onoff %s %s %s %s %s" % (nwkid, EPout, OnOff, level, transition)) @@ -457,7 +462,11 @@ def zcl_group_move_to_level_with_onoff(self, nwkid, EPout, OnOff, level, transit data = "%02d" % ADDRESS_MODE["group"] + nwkid + ZIGATE_EP + EPout + OnOff + level + transition return send_zigatecmd_raw(self, "0081", data) - +def zcl_group_move_to_level_stop(self, nwkid, EPout, ackIsDisabled=DEFAULT_ACK_MODE): + self.log.logging("zclCommand", "Debug", "zcl_move_to_level_stop %s %s" % (nwkid, EPout, )) + zcl_command_formated_logging( self, "zcl_move_to_level_stop", nwkid, EPout, "0008", "Stop", ackIsDisabled) + if "ControllerInRawMode" in self.pluginconf.pluginConf and self.pluginconf.pluginConf["ControllerInRawMode"]: + return zcl_raw_level_move_to_level(self, nwkid, ZIGATE_EP, EPout, "Stop", groupaddrmode=True) # Cluster 0102 ( Window Covering ) ################################## From 94792a4160ffd4f41a0b99140fa25ab6c7867b9a Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Tue, 17 Oct 2023 11:08:19 +0200 Subject: [PATCH 05/47] sort import --- Modules/readZclClusters.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/readZclClusters.py b/Modules/readZclClusters.py index ca658e1f2..b8d5b28e4 100644 --- a/Modules/readZclClusters.py +++ b/Modules/readZclClusters.py @@ -1,17 +1,17 @@ import binascii import json - from os import listdir from os.path import isdir, isfile, join from pathlib import Path from DevicesModules import FUNCTION_MODULE, FUNCTION_WITH_ACTIONS_MODULE +from Modules.batterieManagement import UpdateBatteryAttribute from Modules.domoMaj import MajDomoDevice from Modules.paramDevice import get_device_config_param from Modules.tools import checkAndStoreAttributeValue, getAttributeValue -from Modules.zclClusterHelpers import handle_model_name, decoding_attribute_data -from Modules.batterieManagement import UpdateBatteryAttribute +from Modules.zclClusterHelpers import (decoding_attribute_data, + handle_model_name) # "ActionList": # check_store_value - check the value and store in the corresponding data strcuture entry From 2ec6cef63263c7d701a04fef8d0dfe79f7000db4 Mon Sep 17 00:00:00 2001 From: Pipiche <8291674+pipiche38@users.noreply.github.com> Date: Wed, 18 Oct 2023 19:40:35 +0200 Subject: [PATCH 06/47] Aotec zigbee button (#1658) * do not overwrite the Attribute when calling is_cluster_specific_config * conf file to Certified DB ( https://github.com/zigbeefordomoticz/z4d-certified-devices/commit/604a910702f312f5f67c40088b0ff7589ca4764b ) --- Modules/input.py | 11 ++++++++++- Modules/readZclClusters.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Modules/input.py b/Modules/input.py index b95dbb480..920f61fd7 100644 --- a/Modules/input.py +++ b/Modules/input.py @@ -3489,7 +3489,16 @@ def Decode8401(self, Devices, MsgData, MsgLQI): # Reception Zone status change motion_via_IAS_alarm = get_device_config_param( self, MsgSrcAddr, "MotionViaIASAlarm1") self.log.logging( "Input", "Debug", "MotionViaIASAlarm1 = %s" % (motion_via_IAS_alarm)) - if motion_via_IAS_alarm is not None and motion_via_IAS_alarm == 1: + ias_alarm1_2_merged = get_deviceconf_parameter_value( self, Model, "IASAlarmMerge", return_default=None ) + self.log.logging( "Input", "Debug", "IASAlarmMerge = %s" % (ias_alarm1_2_merged)) + + if ias_alarm1_2_merged: + self.log.logging( "Input", "Debug", "IASAlarmMerge alarm1 %s alarm2 %s" % (alarm1, alarm2)) + combined_alarm = ( alarm2 << 1 ) | alarm1 + self.log.logging( "Input", "Debug", "IASAlarmMerge combined value = %02d" % (combined_alarm)) + MajDomoDevice(self, Devices, MsgSrcAddr, MsgEp, "0006", "%02d" % combined_alarm) + + elif motion_via_IAS_alarm is not None and motion_via_IAS_alarm == 1: self.log.logging( "Input", "Debug", "Motion detected sending to MajDomo %s/%s %s" % ( MsgSrcAddr, MsgEp, (alarm1 or alarm2))) MajDomoDevice(self, Devices, MsgSrcAddr, MsgEp, "0406", "%02d" % (alarm1 or alarm2)) diff --git a/Modules/readZclClusters.py b/Modules/readZclClusters.py index b8d5b28e4..b77b01ef0 100644 --- a/Modules/readZclClusters.py +++ b/Modules/readZclClusters.py @@ -311,7 +311,7 @@ def is_cluster_zcl_config_available( self, nwkid, ep, cluster, attribute=None): if is_manufacturer_specific_cluster( self, cluster): return True - if is_cluster_specific_config(self, _get_model_name( self, nwkid), ep, cluster, attribute=None): + if is_cluster_specific_config(self, _get_model_name( self, nwkid), ep, cluster, attribute): return True return is_generic_zcl_cluster( self, cluster, attribute) From d9385fdb67c39a3d8e264186bb52f5f28409c106 Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Thu, 19 Oct 2023 19:50:16 +0200 Subject: [PATCH 07/47] handle correctly Alarm1 and Alarm2 on IAS Cluster 0500 --- Modules/readClusters.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/Modules/readClusters.py b/Modules/readClusters.py index 94f9f8837..af512a893 100644 --- a/Modules/readClusters.py +++ b/Modules/readClusters.py @@ -1614,22 +1614,24 @@ def Cluster0500(self, Devices, MsgSQN, MsgSrcAddr, MsgSrcEp, MsgClusterId, MsgAt self.ListOfDevices[MsgSrcAddr]["IAS"][MsgSrcEp]["ZoneStatus"]["doorbell"] = doorbell self.ListOfDevices[MsgSrcAddr]["IAS"][MsgSrcEp]["ZoneStatus"]["GlobalInfos"] = "%s;%s;%s;%s;%s;%s;%s;%s;%s;%s;%s" % ( - alarm1, - alarm2, - tamper, - batter, - srepor, - rrepor, - troubl, - acmain, - test, - batdef, - doorbell - ) + alarm1, alarm2, tamper, batter, srepor, rrepor, troubl, acmain, test, batdef, doorbell ) + self.ListOfDevices[MsgSrcAddr]["IAS"][MsgSrcEp]["ZoneStatus"]["TimeStamp"] = int(time()) - if "Model" in self.ListOfDevices[MsgSrcAddr] and self.ListOfDevices[MsgSrcAddr]["Model"] in ("RC-EF-3.0", "RC-EM"): # alarm1 or alarm2 not used on thoses devices + model = self.ListOfDevices[MsgSrcAddr].get("Model", "") + if model in ("RC-EF-3.0", "RC-EM"): # alarm1 or alarm2 not used on thoses devices return - MajDomoDevice(self, Devices, MsgSrcAddr, MsgSrcEp, MsgClusterId, "%02d" % (alarm1 or alarm2 or doorbell)) + + ias_alarm1_2_merged = get_deviceconf_parameter_value( self, model, "IASAlarmMerge", return_default=None ) + self.log.logging( "Input", "Debug", "IASAlarmMerge = %s" % (ias_alarm1_2_merged)) + + if ias_alarm1_2_merged: + self.log.logging( "Input", "Debug", "IASAlarmMerge alarm1 %s alarm2 %s" % (alarm1, alarm2)) + combined_alarm = ( alarm2 << 1 ) | alarm1 + self.log.logging( "Input", "Debug", "IASAlarmMerge combined value = %02d" % (combined_alarm)) + else: + combined_alarm = alarm1 or alarm2 or doorbell + + MajDomoDevice(self, Devices, MsgSrcAddr, MsgSrcEp, MsgClusterId, "%02d" % (combined_alarm)) if batter: # Battery Warning From 031d187759bba8b4867194fb3d7f7f682f3a7ee5 Mon Sep 17 00:00:00 2001 From: Pipiche <8291674+pipiche38@users.noreply.github.com> Date: Wed, 25 Oct 2023 11:49:48 +0200 Subject: [PATCH 08/47] improve check_duplicate_sqn --- Modules/input.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/Modules/input.py b/Modules/input.py index 920f61fd7..0f431a9de 100644 --- a/Modules/input.py +++ b/Modules/input.py @@ -4310,14 +4310,22 @@ def Decode7000(self, Devices, MsgData, MsgLQI): u8TransactionSequenceNumber, ) - def check_duplicate_sqn(self, Nwkid, Ep, Cluster, Sqn): - if "Ep" in self.ListOfDevices[Nwkid] and Ep in self.ListOfDevices[Nwkid]["Ep"]: - if Cluster not in self.ListOfDevices[Nwkid]["Ep"][Ep]: - self.ListOfDevices[Nwkid]["Ep"][Ep][Cluster] = {} - if not isinstance(self.ListOfDevices[Nwkid]["Ep"][Ep][Cluster], dict): - self.ListOfDevices[Nwkid]["Ep"][Ep][Cluster] = {} - if "0000" not in self.ListOfDevices[Nwkid]["Ep"][Ep][Cluster]: - self.ListOfDevices[Nwkid]["Ep"][Ep][Cluster]["0000"] = {} + """ + This function is useful for checking the uniqueness of sequence numbers associated with specific network devices, + ensuring data integrity and preventing duplicates in a network application. + """ + + if "Ep" not in self.ListOfDevices[Nwkid] or Ep not in self.ListOfDevices[Nwkid]["Ep"]: + return False + + EpCluster = self.ListOfDevices[Nwkid]["Ep"][Ep] + if Cluster not in EpCluster: + EpCluster[Cluster] = {} + elif not isinstance(EpCluster[Cluster], dict): + EpCluster[Cluster] = {} + + if "0000" not in EpCluster[Cluster]: + EpCluster[Cluster]["0000"] = {} return Sqn != "00" and "SQN" in self.ListOfDevices[Nwkid] and Sqn == self.ListOfDevices[Nwkid]["SQN"] From 7ef8761565bb6d5f456f0bc756c2a11e796d72b9 Mon Sep 17 00:00:00 2001 From: Pipiche <8291674+pipiche38@users.noreply.github.com> Date: Wed, 25 Oct 2023 15:08:10 +0200 Subject: [PATCH 09/47] add more log to error message --- Modules/tuyaTS0601.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Modules/tuyaTS0601.py b/Modules/tuyaTS0601.py index bb1588660..0f5a40de0 100644 --- a/Modules/tuyaTS0601.py +++ b/Modules/tuyaTS0601.py @@ -95,7 +95,8 @@ def ts0601_actuator( self, NwkId, command, value=None): dps_mapping = ts0601_extract_data_point_infos( self, model_name) if dps_mapping is None: - self.log.logging("Tuya0601", "Error", "ts0601_actuator - No DPS stanza in config file for %s %s" %(NwkId, command)) + self.log.logging("Tuya0601", "Error", "ts0601_actuator - No DPS stanza in config file for %s %s %s" %( + NwkId, model_name, command)) return False if command not in DP_ACTION_FUNCTION and command not in TS0601_COMMANDS: From d6f9f861e1412741d8c4bdf417f8fc5d70455640 Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Wed, 25 Oct 2023 18:59:43 +0200 Subject: [PATCH 10/47] prevent using the new tuyaTS0601 path, when the model do not provide any datapoint definition --- Modules/paramDevice.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/Modules/paramDevice.py b/Modules/paramDevice.py index d9ad3ba38..0ed5db5de 100644 --- a/Modules/paramDevice.py +++ b/Modules/paramDevice.py @@ -74,7 +74,8 @@ tuya_trv_set_opened_window_temp, tuya_trv_thermostat_sensor_mode, tuya_trv_window_detection) -from Modules.tuyaTS0601 import TS0601_COMMANDS, ts0601_actuator +from Modules.tuyaTS0601 import (TS0601_COMMANDS, ts0601_actuator, + ts0601_extract_data_point_infos) def Ballast_max_level(self, nwkid, max_level): @@ -308,20 +309,14 @@ def ias_sensitivity(self, nwkid, sensitivity): } def sanity_check_of_param(self, NwkId): - if "Param" not in self.ListOfDevices[NwkId]: - return - - for param in self.ListOfDevices[NwkId]["Param"]: - if param in TS0601_COMMANDS: - # We a Tuya TS0601 devices - value = self.ListOfDevices[NwkId]["Param"][param] - ts0601_actuator( self, NwkId, param, value) - continue - - if param in DEVICE_PARAMETERS: - func = DEVICE_PARAMETERS[param] - value = self.ListOfDevices[NwkId]["Param"][param] - func(self, NwkId, value) + param_data = self.ListOfDevices.get(NwkId, {}).get("Param", {}) + model_name = self.ListOfDevices.get(NwkId, {}).get("Model", "") + + for param, value in param_data.items(): + if ts0601_extract_data_point_infos(self, model_name) and param in TS0601_COMMANDS: + ts0601_actuator(self, NwkId, param, value) + elif param in DEVICE_PARAMETERS: + DEVICE_PARAMETERS[param](self, NwkId, value) def get_device_config_param( self, NwkId, config_parameter): From 35e1c9637ed38aee472716be0c376c810c7dfdfc Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Wed, 25 Oct 2023 19:03:03 +0200 Subject: [PATCH 11/47] address a codefactor multiple spaces before operator issue --- Zigbee/encoder_tools.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Zigbee/encoder_tools.py b/Zigbee/encoder_tools.py index 4f5bfd595..aba0033b7 100644 --- a/Zigbee/encoder_tools.py +++ b/Zigbee/encoder_tools.py @@ -54,7 +54,7 @@ def decode_endian_data(data, datatype, len_stream=None): # we expect 7 bytes lenght data = data[2:16] # 7 bytes - 56b - return ("%016x" % struct.unpack(">Q", struct.pack("Q", int("00" + data[:14], 16)))[0])[:14] + return ("%016x" % struct.unpack(">Q", struct.pack("Q", int("00" + data[:14], 16)))[0])[:14] if data_type_id in {0x0F, 0x1F, 0x27, 0x2F, 0x3A, 0xF0}: # 8 bytes - 64b From 2365062e0b32bb28cbb023ccee0571903fd13092 Mon Sep 17 00:00:00 2001 From: Pipiche <8291674+pipiche38@users.noreply.github.com> Date: Thu, 26 Oct 2023 19:56:39 +0200 Subject: [PATCH 12/47] Upgrade of zigpy radio libs to a more recent version (#1655) * Update zigpy radio libs to bellows 0.36.8, zigpy 0.58.1, zigpy_znp 0.11.6, zigpy_deconz 0.21.1 --- Classes/ZigpyTransport/zigpyThread.py | 7 +++---- plugin.py | 8 ++++---- requirements.txt | 10 +++++----- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Classes/ZigpyTransport/zigpyThread.py b/Classes/ZigpyTransport/zigpyThread.py index 0291fedf5..9462ad2ca 100644 --- a/Classes/ZigpyTransport/zigpyThread.py +++ b/Classes/ZigpyTransport/zigpyThread.py @@ -276,10 +276,9 @@ def post_coordinator_startup(self, radiomodule): self.log.logging( "TransportZigpy", "Debug", "Active Endpoint List: %s" % str(self.app.get_device(nwk=t.NWK(0x0000)).endpoints.keys()), ) for epid, ep in self.app.get_device(nwk=t.NWK(0x0000)).endpoints.items(): - if epid == 0: - continue - self.log.logging( "TransportZigpy", "Debug", "Simple Descriptor: %s" % ep) - self.forwarder_queue.put(build_plugin_8043_frame_list_node_descriptor(self, epid, ep)) + if epid != 0 and ep.status == 0x00: + self.log.logging( "TransportZigpy", "Debug", "Simple Descriptor: %s" % ep) + self.forwarder_queue.put(build_plugin_8043_frame_list_node_descriptor(self, epid, ep)) self.log.logging( "TransportZigpy", "Debug", "Controller Model %s" % self.app.get_device(nwk=t.NWK(0x0000)).model ) self.log.logging( "TransportZigpy", "Debug", "Controller Manufacturer %s" % self.app.get_device(nwk=t.NWK(0x0000)).manufacturer ) diff --git a/plugin.py b/plugin.py index b6014edbd..b8ea0a818 100644 --- a/plugin.py +++ b/plugin.py @@ -1502,10 +1502,10 @@ def update_DB_device_status_to_reinit( self ): def check_python_modules_version( self ): MODULES_VERSION = { - "zigpy": "0.56.1", - "zigpy_znp": "0.11.2", - "zigpy_deconz": "0.21.0", - "bellows": "0.35.8", + "zigpy": "0.58.1", + "zigpy_znp": "0.11.6", + "zigpy_deconz": "0.21.1", + "bellows": "0.36.8", } flag = True diff --git a/requirements.txt b/requirements.txt index 712f6b093..c265e1938 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ -bellows==0.35.8 +zigpy==0.58.1 +zigpy_deconz==0.21.1 +zigpy-cli==1.0.4 +zigpy_znp==0.11.6 +bellows==0.36.8 dnspython==2.3.0 pyserial>=3.5 z4d-certified-devices -zigpy==0.56.1 -zigpy_deconz==0.21.0 -zigpy_znp==0.11.2 charset-normalizer==2.0.11 distro -zigpy-cli==1.0.4 From ba1b7767a7c37907edf49a6848d204cd03aac7da Mon Sep 17 00:00:00 2001 From: Pipiche <8291674+pipiche38@users.noreply.github.com> Date: Sun, 29 Oct 2023 12:28:30 +0100 Subject: [PATCH 13/47] Lighting color control / Fixing Lidl TS0505A behaviour (#1657) * TS0505A exist with different Manufacturer Name and it seems that they are not controlled on the same way. --- Conf/Local-Devices/TS0505A-HueSaturation.json | 55 ++++++++ Conf/Local-Devices/TS0505A.json | 50 +++++++ Conf/Local-Devices/TS0601-Human-Presence.json | 43 ++++++ Modules/actuators.py | 128 ++++++++++++++---- Modules/basicOutputs.py | 7 +- Modules/command.py | 4 +- Modules/pairingProcess.py | 16 ++- Modules/readAttributes.py | 7 + Modules/tuya.py | 61 +++++++-- Zigbee/zclCommands.py | 6 + Zigbee/zclRawCommands.py | 28 ++++ 11 files changed, 357 insertions(+), 48 deletions(-) create mode 100644 Conf/Local-Devices/TS0505A-HueSaturation.json create mode 100644 Conf/Local-Devices/TS0505A.json create mode 100644 Conf/Local-Devices/TS0601-Human-Presence.json diff --git a/Conf/Local-Devices/TS0505A-HueSaturation.json b/Conf/Local-Devices/TS0505A-HueSaturation.json new file mode 100644 index 000000000..2ba4a64d8 --- /dev/null +++ b/Conf/Local-Devices/TS0505A-HueSaturation.json @@ -0,0 +1,55 @@ +{ + "_comment": "Tuya RR400ZB require Tuya HueandSaturation", + "_blakadder": "https://zigbee.blakadder.com/Lidl_HG06104A.html", + "_version": "1.0", + "Identifier" : [ + ["TS0505A", "_TZ3000_dbou1ap4"] + ], + "Ep": { + "01": { + "0000": "", + "0003": "", + "0004": "", + "0005": "", + "0006": "", + "0008": "", + "0300": { + "Attributes": { + "f000": {"Enabled": true, "Name": "ColorMode", "DataType": "20", "ActionList": [ "check_store_value"] }, + "f001": {"Enabled": true, "Name": "Brightness ", "DataType": "20", "ActionList": [ "check_store_value"] }, + "f003": {"Enabled": true, "Name": "SceneData", "DataType": "48", "ActionList": [ "check_store_value"] } + } + }, + "000a": "", + "0019": "", + "1000": "", + "ef00": "", + "Type": "ColorControlRGBWW" + }, + "f2": { + "0021": "", + "Type": "" + } + }, + "Type": "", + "bindEp": "01", + "ClusterToBind": [ ], + "ConfigureReporting": { + }, + "ReadAttributes": { + "0000": [ "0000", "0001", "0002", "0003", "0004", "0005", "0006", "0007" ], + "0006": [ "0000", "4001", "4002" ], + "0008": [ "0000" ], + "0019": [], + "0300": [ "f002", "f00d", "0000", "0001", "0003", "0004", "0007", "0008", "000f" ] + }, + "TUYA_REGISTRATION": 13, + "TuyaCommandF0": true, + "FORCE_COLOR_COMMAND": "TuyaMovetoHueandSaturation", + "TUYAColorControlRgbMode": true, + "Param": { + "moveToColourTemp": "0010", + "moveToColourRGB": "0010", + "moveToHueSatu": "0000" + } +} diff --git a/Conf/Local-Devices/TS0505A.json b/Conf/Local-Devices/TS0505A.json new file mode 100644 index 000000000..48f6b33c1 --- /dev/null +++ b/Conf/Local-Devices/TS0505A.json @@ -0,0 +1,50 @@ +{ + "_comment": "Tuya RR400ZB able to manage color via MoveToColour", + "_blakadder": "https://zigbee.blakadder.com/Lidl_HG06104A.html", + "_version": "1.0", + "Ep": { + "01": { + "0000": "", + "0003": "", + "0004": "", + "0005": "", + "0006": "", + "0008": "", + "0300": { + "Attributes": { + "f000": {"Enabled": true, "Name": "Tuya0300_f000", "DataType": "20", "ActionList": [ "check_store_value"] }, + "f003": {"Enabled": true, "Name": "Tuya0300_f003", "DataType": "48", "ActionList": [ "check_store_value"] } + } + }, + "000a": "", + "0019": "", + "1000": "", + "ef00": "", + "Type": "ColorControlRGBWW" + }, + "f2": { + "0021": "", + "Type": "" + } + }, + "Type": "", + "bindEp": "01", + "ClusterToBind": [ ], + "ConfigureReporting": { + }, + "ReadAttributes": { + "0000": [ "0000", "0001", "0002", "0003", "0004", "0005", "0006", "0007" ], + "0006": [ "0000", "4001", "4002" ], + "0008": [ "0000" ], + "0019": [], + "0300": [ "f002", "f00d", "0000", "0001", "0003", "0004", "0007", "0008", "000f" ] + }, + "TUYA_REGISTRATION": 13, + "TuyaCommandF0": true, + "TUYAColorControlRgbMode": true, + "Param": { + "moveToColourTemp": "0010", + "moveToColourRGB": "0010", + "moveToHueSatu": "0000" + } +} diff --git a/Conf/Local-Devices/TS0601-Human-Presence.json b/Conf/Local-Devices/TS0601-Human-Presence.json new file mode 100644 index 000000000..13f120a1c --- /dev/null +++ b/Conf/Local-Devices/TS0601-Human-Presence.json @@ -0,0 +1,43 @@ +{ + "_source": "https://github.com/zigbeefordomoticz/z4d-certified-devices/issues/11", + "_blakadder": "https://zigbee.blakadder.com/Tuya_ZY-M100-24G.html", + "_description": "uya Human Presence Detector 5.8GHz Ceiling Mount ZY-M100-2", + "Ep": { + "01": { + "0000": "", + "0004": "", + "0005": "", + "0500": "", + "000a": "", + "0019": "", + "Type": "Motion/Lux" + } + }, + "Identifier": [ + [ "TS0601", "_TZE200_bh3n6gk8" ], + [ "TS0601", "_TZE200_3towulqd" ], + [ "TS0601", "_TZE200_1ibpyhdc" ] + ], + "Type": "", + "ClusterToBind": [ ], + "ConfigureReporting": {}, + "ReadAttributes": { + "0000": [ "0004", "0000", "0001", "0005", "0007", "fffe" ], + "0001": [], + "0019": [], + "0500": [ "0000", "0001", "0002", "0010", "0011"], + "ef00": [] + }, + "TS0601_DP": { + "68": { "sensor_type": "illuminance"}, + "69": { "sensor_type": "motion", "EvalExp": "int(value == 0)", "DomoDeviceFormat": "str"}, + "6a": { "store_tuya_attribute": "Sensitivity"}, + "6b": { "store_tuya_attribute": "max_range"}, + "6c": { "store_tuya_attribute": "min_range"}, + "6d": { "store_tuya_attribute": "target_distace"}, + "6e": { "store_tuya_attribute": "fading_time"}, + "6f": { "store_tuya_attribute": "detection_delay"} + }, + "Param": { + } +} diff --git a/Modules/actuators.py b/Modules/actuators.py index bcfbdecf5..9f7bba38c 100644 --- a/Modules/actuators.py +++ b/Modules/actuators.py @@ -13,9 +13,16 @@ import json from Modules.basicOutputs import set_poweron_afteroffon -from Modules.readAttributes import ReadAttributeRequest_0006_400x +from Modules.paramDevice import get_device_config_param +from Modules.readAttributes import ( + ReadAttributeRequest_0006_400x, + ReadAttributeRequest_0300_Color_Capabilities) from Modules.thermostats import thermostat_Setpoint -from Modules.tools import Hex_Format, rgb_to_hsl, rgb_to_xy +from Modules.tools import (Hex_Format, get_deviceconf_parameter_value, + getAttributeValue, rgb_to_hsl, rgb_to_xy) +from Modules.tuya import (tuya_color_control_rgbMode, + tuya_Move_To_Hue_Saturation, + tuya_Move_To_Hue_Saturation_Brightness) from Modules.zigateConsts import ZIGATE_EP from Zigbee.zclCommands import (zcl_identify_send, zcl_identify_trigger_effect, zcl_level_move_to_level, @@ -238,34 +245,34 @@ def actuator_setcolor(self, nwkid, EPout, value, Color): # First manage level # if Hue_List['m'] or Hue_List['m'] != 9998 or manage_level: transitionMoveLevel = "0000" - if ( - "Param" in self.ListOfDevices[nwkid] - and "moveToLevel" in self.ListOfDevices[nwkid]["Param"] - ): + if ( "Param" in self.ListOfDevices[nwkid] and "moveToLevel" in self.ListOfDevices[nwkid]["Param"] ): transitionMoveLevel = "%04x" % int(self.ListOfDevices[nwkid]["Param"]["moveToLevel"]) - - if Hue_List["m"] or Hue_List["m"] != 9998: - value = lightning_percentage_to_analog( value ) - self.log.logging("Command", "Debug", "---------- Set Level: %s" % (value), nwkid) - actuator_setlevel(self, nwkid, EPout, value, "Light", transitionMoveLevel) - # ColorModeTemp = 2 // White with color temperature. Valid fields: t + force_color_command = get_deviceconf_parameter_value(self, self.ListOfDevices[nwkid]["Model"], "FORCE_COLOR_COMMAND", return_default=None) + ColorCapabilitiesList = device_color_capabilities( self, nwkid, EPout) + self.log.logging("Command", "Debug", "actuator_setcolor force_color_command %s" % force_color_command, nwkid) + if Hue_List["m"] == 2: + # ColorModeTemp = 2 // White with color temperature. Valid fields: t handle_color_mode_2(self, nwkid, EPout, Hue_List) - # ColorModeRGB = 3 // Color. Valid fields: r, g, b. + elif Hue_List["m"] == 3 and force_color_command == "TuyaMovetoHueandSaturation": + handle_color_mode_tuya( self, nwkid, EPout, Hue_List, value) + elif Hue_List["m"] == 3: + # ColorModeRGB = 3 // Color. Valid fields: r, g, b. handle_color_mode_3(self, nwkid, EPout, Hue_List) - # ColorModeCustom = 4, // Custom (color + white). Valid fields: r, g, b, cw, ww, depending on device capabilities elif Hue_List["m"] == 4: + # ColorModeCustom = 4, // Custom (color + white). Valid fields: r, g, b, cw, ww, depending on device capabilities handle_color_mode_4(self, nwkid, EPout, Hue_List ) - - # With saturation and hue, not seen in domoticz but present on zigate, and some device need it + elif Hue_List["m"] == 9998: - handle_color_mode_9998( self, nwkid, EPout, Hue_List) + # With saturation and hue, not seen in domoticz but present on zigate, and some device need it + handle_color_mode_9998( self, nwkid, EPout, Hue_List, value) def handle_color_mode_2(self, nwkid, EPout, Hue_List): + # White with color temperature. Valid fields: t # Value is in mireds (not kelvin) # Correct values are from 153 (6500K) up to 588 (1700K) # t is 0 > 255 @@ -274,8 +281,10 @@ def handle_color_mode_2(self, nwkid, EPout, Hue_List): self.log.logging( "Command", "Debug", "handle_color_mode_2 Set Temp Kelvin: %s-%s" % (TempMired, Hex_Format(4, TempMired)), nwkid ) transitionMoveLevel , transitionRGB , transitionMoveLevel , transitionHue , transitionTemp = get_all_transition_mode( self, nwkid) zcl_move_to_colour_temperature( self, nwkid, EPout, Hex_Format(4, TempMired), transitionTemp) + def handle_color_mode_3(self, nwkid, EPout, Hue_List): + # Color. Valid fields: r, g, b. x, y = rgb_to_xy((int(Hue_List["r"]), int(Hue_List["g"]), int(Hue_List["b"]))) # Convert 0>1 to 0>FFFF x = int(x * 65536) @@ -283,6 +292,8 @@ def handle_color_mode_3(self, nwkid, EPout, Hue_List): #strxy = Hex_Format(4, x) + Hex_Format(4, y) self.log.logging("Command", "Debug", "handle_color_mode_3 Set Temp X: %s Y: %s" % (x, y), nwkid) transitionMoveLevel , transitionRGB , transitionMoveLevel , transitionHue , transitionTemp = get_all_transition_mode( self, nwkid) + if get_deviceconf_parameter_value(self, self.ListOfDevices[nwkid]["Model"], "TUYAColorControlRgbMode", return_default=None): + tuya_color_control_rgbMode( self, nwkid, "01") zcl_move_to_colour(self, nwkid, EPout, Hex_Format(4, x), Hex_Format(4, y), transitionRGB) def handle_color_mode_4(self, nwkid, EPout, Hue_List ): @@ -296,9 +307,10 @@ def handle_color_mode_4(self, nwkid, EPout, Hue_List ): if cw != 0 and ww != 0: TempKelvin = int((255 - ww) * (6500 - 1700) / 255 + 1700) TempMired = 1000000 // TempKelvin - self.log.logging( - "Command", "Log", "handle_color_mode_4 Set Temp Kelvin: %s-%s" % (TempMired, Hex_Format(4, TempMired)), nwkid - ) + self.log.logging( "Command", "Log", "handle_color_mode_4 Set Temp Kelvin: %s-%s" % ( + TempMired, Hex_Format(4, TempMired)), nwkid ) + if get_deviceconf_parameter_value(self, self.ListOfDevices[nwkid]["Model"], "TUYAColorControlRgbMode", return_default=None): + tuya_color_control_rgbMode( self, nwkid, "01") zcl_move_to_colour_temperature( self, nwkid, EPout, Hex_Format(4, TempMired), transitionTemp) # Process Colour @@ -308,10 +320,13 @@ def handle_color_mode_4(self, nwkid, EPout, Hue_List ): hue = _h * 360 # 0 > 360 hue = int(hue * 254 // 360) - self.log.logging("Command", "Log", "handle_color_mode_4 Set Hue X: %s Saturation: %s" % (hue, saturation), nwkid) + self.log.logging("Command", "Log", "handle_color_mode_4 Set Hue X: %s Saturation: %s" % ( + hue, saturation), nwkid) + if get_deviceconf_parameter_value(self, self.ListOfDevices[nwkid]["Model"], "TUYAColorControlRgbMode", return_default=None): + tuya_color_control_rgbMode( self, nwkid, "01") zcl_move_hue_and_saturation(self, nwkid, EPout, Hex_Format(2, hue), Hex_Format(2, saturation), transitionRGB) - -def handle_color_mode_9998( self, nwkid, EPout, Hue_List): + +def handle_color_mode_9998( self, nwkid, EPout, Hue_List, value): transitionMoveLevel , transitionRGB , transitionMoveLevel , transitionHue , transitionTemp = get_all_transition_mode( self, nwkid) _h, _s, _l = rgb_to_hsl((int(Hue_List["r"]), int(Hue_List["g"]), int(Hue_List["b"]))) saturation = _s * 100 # 0 > 100 @@ -320,12 +335,35 @@ def handle_color_mode_9998( self, nwkid, EPout, Hue_List): hue = int(hue * 254 // 360) self.log.logging("Command", "Debug", "handle_color_mode_9998 Set Hue X: %s Saturation: %s" % (hue, saturation), nwkid) - zcl_move_hue_and_saturation(self, nwkid, EPout, Hex_Format(2, hue), Hex_Format(2, saturation), transitionRGB) + if get_deviceconf_parameter_value(self, self.ListOfDevices[nwkid]["Model"], "TUYAColorControlRgbMode", return_default=None): + tuya_color_control_rgbMode( self, nwkid, "01") + + if get_deviceconf_parameter_value(self, self.ListOfDevices[nwkid]["Model"], "FORCE_COLOR_COMMAND", return_default=None) == "TuyaMovetoHueandSaturation": + tuya_Move_To_Hue_Saturation( self, nwkid, hue, saturation, value) + else: + zcl_move_hue_and_saturation(self, nwkid, EPout, Hex_Format(2, hue), Hex_Format(2, saturation), transitionRGB) + value = lightning_percentage_to_analog( value ) + self.log.logging( "Command", "Debug", "handle_color_mode_9998 Set Level: %s instead of Level: %s" % (value, value), nwkid) + actuator_setlevel(self, nwkid, EPout, value, "Light", transitionMoveLevel) + - value = int(_l * 254 // 100) - OnOff = "01" - self.log.logging( "Command", "Debug", "handle_color_mode_9998 Set Level: %s instead of Level: %s" % (value, value), nwkid) - actuator_setlevel(self, nwkid, EPout, value, "Light", transitionMoveLevel) +def handle_color_mode_tuya( self, nwkid, EPout, Hue_List, value): + + self.log.logging("Command", "Debug", "handle_color_mode_tuya Hue_list: %s Value: %s" % ( + Hue_List, value), nwkid) + + transitionMoveLevel , transitionRGB , transitionMoveLevel , transitionHue , transitionTemp = get_all_transition_mode( self, nwkid) + _h, _s, _ = rgb_to_hsl((int(Hue_List["r"]), int(Hue_List["g"]), int(Hue_List["b"]))) + saturation = _s * 100 # 0 > 100 + saturation = int(saturation * 254 // 100) + hue = _h * 360 # 0 > 360 + hue = int(hue * 254 // 360) + + self.log.logging("Command", "Log", "handle_color_mode_tuya Set Hue X: %s Saturation: %s Value: %s" % ( + hue, saturation, value), nwkid) + if get_deviceconf_parameter_value(self, self.ListOfDevices[nwkid]["Model"], "TUYAColorControlRgbMode", return_default=None): + tuya_color_control_rgbMode( self, nwkid, "01") + tuya_Move_To_Hue_Saturation( self, nwkid, EPout, hue, saturation, transitionHue, value ) def actuator_identify(self, nwkid, ep, value=None): @@ -351,4 +389,36 @@ def actuator_identify(self, nwkid, ep, value=None): value = 0x00 # Flashing color = 0x03 # Blue - zcl_identify_trigger_effect( self, nwkid, ep, "%02x" % value, "%02x" % color) \ No newline at end of file + zcl_identify_trigger_effect( self, nwkid, ep, "%02x" % value, "%02x" % color) + + +def decode_color_capabilities(capabilities_value): + capabilities = { + "Hue and Saturation": 0b00000000_00000001, + "Enhanced Hue": 0b00000000_00000010, + "Color Loop": 0b00000000_00000100, + "XY Attributes": 0b00000000_00001000, + "Color Temperature": 0b00000000_00010000 + } + + return [ + feature + for feature, bitmask in capabilities.items() + if capabilities_value & bitmask + ] + +def device_color_capabilities( self, nwkid, ep): + self.log.logging( "Command", "Debug", "device_color_capabilities %s %s" % (nwkid, ep), nwkid) + deviceHasNoColorCapabilities = get_deviceconf_parameter_value(self, self.ListOfDevices[nwkid]["Model"], "NoColorCapabilitie", return_default=None) + colorCapabilities = getAttributeValue( self, nwkid, ep, "0300", "400a") + + self.log.logging( "Command", "Debug", "+ deviceHasColorCapabilities %s" % (deviceHasNoColorCapabilities), nwkid) + self.log.logging( "Command", "Debug", "+ colorCapabilities %s" % (colorCapabilities), nwkid) + + if colorCapabilities is None and deviceHasNoColorCapabilities: + return [] + if colorCapabilities is None or isinstance( colorCapabilities, str): + ReadAttributeRequest_0300_Color_Capabilities(self, nwkid) + return [] + + return decode_color_capabilities(colorCapabilities) diff --git a/Modules/basicOutputs.py b/Modules/basicOutputs.py index 143240a3c..33799407b 100644 --- a/Modules/basicOutputs.py +++ b/Modules/basicOutputs.py @@ -34,7 +34,8 @@ from Zigbee.zclCommands import (zcl_attribute_discovery_request, zcl_get_list_attribute_extended_infos, zcl_identify_send, zcl_identify_trigger_effect, - zcl_read_attribute, zcl_write_attribute, + zcl_read_attribute, zcl_reset_device, + zcl_write_attribute, zcl_write_attributeNoResponse) from Zigbee.zdpCommands import (zdp_get_permit_joint_status, zdp_IEEE_address_request, @@ -42,7 +43,7 @@ zdp_management_network_update_request, zdp_many_to_one_route_request, zdp_permit_joining_request, - zdp_raw_nwk_update_request, zdp_reset_device) + zdp_raw_nwk_update_request) from Zigbee.zdpRawCommands import (zdp_management_binding_table_request, zdp_management_routing_table_request) @@ -417,7 +418,7 @@ def reset_device(self, nwkid, epout): self.log.logging("BasicOutput", "Debug", "reset_device - Send a Device Reset to %s/%s" % (nwkid, epout), nwkid) #return send_zigatecmd_raw(self, "0050", "02" + nwkid + ZIGATE_EP + epout) - return zdp_reset_device(self, nwkid, ZIGATE_EP, epout) + return zcl_reset_device(self, nwkid, ZIGATE_EP, epout) def leaveRequest(self, ShortAddr=None, IEEE=None, RemoveChild=0x00, Rejoin=0x00): diff --git a/Modules/command.py b/Modules/command.py index 08022294a..c3b7cbf14 100644 --- a/Modules/command.py +++ b/Modules/command.py @@ -28,8 +28,8 @@ schneider_set_contract, schneider_temp_Setcurrent) from Modules.switchSelectorWidgets import SWITCH_SELECTORS -from Modules.tools import get_deviceconf_parameter_value from Modules.thermostats import thermostat_Mode, thermostat_Setpoint +from Modules.tools import get_deviceconf_parameter_value from Modules.tuya import (tuya_curtain_lvl, tuya_curtain_openclose, tuya_dimmer_dimmer, tuya_dimmer_onoff, tuya_energy_onoff, tuya_garage_door_action, @@ -45,7 +45,6 @@ from Modules.zigateConsts import (THERMOSTAT_LEVEL_2_MODE, THERMOSTAT_LEVEL_3_MODE, ZIGATE_EP) - # Matrix between Domoticz Type, Subtype, SwitchType and Plugin DeviceType # Type, Subtype, Switchtype DEVICE_SWITCH_MATRIX = { @@ -1358,6 +1357,7 @@ def mgtCommand(self, Devices, Unit, Command, Level, Color): self.log.logging( "Command", "Debug", "mgtCommand : Set Color for Device: %s EPout: %s Unit: %s DeviceType: %s Level: %s Color: %s" % ( NWKID, EPout, Unit, DeviceType, Level, Color), NWKID, ) + actuator_setcolor(self, NWKID, EPout, Level, Color) request_read_device_status(self, NWKID) diff --git a/Modules/pairingProcess.py b/Modules/pairingProcess.py index 6cc2fe2ed..3ba8f5bbc 100644 --- a/Modules/pairingProcess.py +++ b/Modules/pairingProcess.py @@ -36,7 +36,8 @@ from Modules.thermostats import thermostat_Calibration from Modules.tools import (get_deviceconf_parameter_value, getListOfEpForCluster, is_fake_ep) -from Modules.tuya import tuya_cmd_ts004F, tuya_command_f0, tuya_registration +from Modules.tuya import (tuya_cmd_ts004F, tuya_command_f0, + tuya_lighting_color_control, tuya_registration) from Modules.tuyaConst import TUYA_eTRV_MODEL from Modules.tuyaSiren import tuya_sirene_registration from Modules.tuyaTools import tuya_TS0121_registration @@ -627,7 +628,7 @@ def handle_device_specific_needs(self, Devices, NWKID): if self.ListOfDevices[NWKID]["Model"] in ("Wiser2-Thermostat",): wiser_home_lockout_thermostat(self, NWKID, 0) - elif get_device_config_param( self, NWKID, "AqaraMultiClick"): + elif get_device_config_param( self, NWKID, "AqaraMultiClick"): enable_click_mode_aqara( self, NWKID) elif ( MsgIEEE[: PREFIX_MAC_LEN] in PREFIX_MACADDR_WIZER_LEGACY and WISER_LEGACY_MODEL_NAME_PREFIX in self.ListOfDevices[NWKID]["Model"] ): @@ -661,7 +662,8 @@ def handle_device_specific_needs(self, Devices, NWKID): elif self.ListOfDevices[NWKID]["Model"] in ( "TS0222", "TS0002_relay_switch", "TS0003_relay_switch", 'TS0601-motion'): tuya_command_f0( self, NWKID ) - + + elif self.ListOfDevices[NWKID]["Model"] in ( "TS0601-Energy", "TS0601-switch", @@ -691,7 +693,13 @@ def handle_device_specific_needs(self, Devices, NWKID): elif self.ListOfDevices[NWKID]["Model"] == "lumi.remote.b28ac1": enable_click_mode_aqara( self, NWKID ) enableOppleSwitch( self, NWKID ) - + + if get_deviceconf_parameter_value( self, self.ListOfDevices[NWKID]["Model"], "TuyaCommandF0"): + tuya_command_f0( self, NWKID ) + + if get_deviceconf_parameter_value(self, self.ListOfDevices[NWKID]["Model"], "LightingColorControl", return_default=None): + tuya_lighting_color_control(self, NWKID) + def scan_device_for_group_memebership(self, NWKID): for ep in self.ListOfDevices[NWKID]["Ep"]: if "0004" in self.ListOfDevices[NWKID]["Ep"][ep] and self.groupmgt: diff --git a/Modules/readAttributes.py b/Modules/readAttributes.py index 6e5a33195..5b67c4dc2 100644 --- a/Modules/readAttributes.py +++ b/Modules/readAttributes.py @@ -999,7 +999,14 @@ def ReadAttributeRequest_0300(self, key): ) ReadAttributeReq(self, key, ZIGATE_EP, EPout, "0300", listAttributes, ackIsDisabled=is_ack_tobe_disabled(self, key)) +def ReadAttributeRequest_0300_Color_Capabilities(self, key): + # Cluster 0x0300 - Color Control + self.log.logging("ReadAttributes", "Debug", "ReadAttributeRequest_0300_Color_Capabilities - Key: %s " % key, nwkid=key) + ListOfEp = getListOfEpForCluster(self, key, "0300") + for EPout in ListOfEp: + ReadAttributeReq(self, key, ZIGATE_EP, EPout, "0300", [ 0x400A], ackIsDisabled=is_ack_tobe_disabled(self, key)) + def ReadAttributeRequest_0400(self, key): self.log.logging("ReadAttributes", "Debug", "ReadAttributeRequest_0400 - Key: %s " % key, nwkid=key) diff --git a/Modules/tuya.py b/Modules/tuya.py index 90b1a585e..e799149fc 100644 --- a/Modules/tuya.py +++ b/Modules/tuya.py @@ -11,6 +11,7 @@ """ +import struct import time from datetime import datetime, timedelta @@ -115,16 +116,7 @@ def tuya_cmd_0x0000_0xf0(self, NwkId): # Seen at pairing of a WGH-JLCZ02 / TS011F and TS0201 and TS0601 (MOES BRT-100) payload = "11" + get_and_inc_ZCL_SQN(self, NwkId) + "fe" - raw_APS_request( - self, - NwkId, - '01', - "0000", - "0104", - payload, - zigate_ep=ZIGATE_EP, - ackIsDisabled=is_ack_tobe_disabled(self, NwkId), - ) + raw_APS_request( self, NwkId, '01', "0000", "0104", payload, zigate_ep=ZIGATE_EP, ackIsDisabled=is_ack_tobe_disabled(self, NwkId), ) self.log.logging("Tuya", "Debug", "tuya_cmd_0x0000_0xf0 - Nwkid: %s reset device Cmd: fe" % NwkId) @@ -1515,3 +1507,52 @@ def ts110e_switch_type( self, NwkId, EPout, mode): self.log.logging("Tuya", "Debug", "ts110e_switch_type - mode %s" % mode, NwkId) mode = "%02x" %mode write_attribute(self, NwkId, ZIGATE_EP, EPout, "0008", "0000", "00", "fc02", "20", mode, ackIsDisabled=False) + +def tuya_lighting_color_control( self, NwkId, ColorCapabilities=25): + # The ColorCapabilities attribute specifies the color capabilities of the device supporting the color control clus- + # ter, as illustrated in Table 5.8. If a bit is set to 1, the corresponding attributes and commands SHALL become + # mandatory. If a bit is set to 0, the corresponding attributes and commands need not be implemented. + + self.log.logging("Tuya", "Debug", "tuya_lighting_color_control - Color Capabilities %s" % ColorCapabilities, NwkId) + write_attribute( self, NwkId, ZIGATE_EP, "01", "0300", "0000", "00", "400a", "19", "%04x" %ColorCapabilities, ackIsDisabled=False ) + self.log.logging("Tuya", "Debug", "tuya_lighting_color_control - Color Capabilities %s completed" % ColorCapabilities, NwkId) + + +def tuya_color_control_rgbMode( self, NwkId, mode): + # Command 0xfe: Set mode (Tuya-specific command) + # Change the mode. + # 0: White light + # 1: Colored light + # 2: Scene + # 3: Music + # https://developer.tuya.com/en/docs/connect-subdevices-to-gateways/tuya-zigbee-lighting-access-standard?id=K9ik6zvod83fi#title-7-Color%20Control%20cluster + + self.log.logging("Tuya", "Debug", "tuya_color_control_rgbMode", NwkId) + sqn = get_and_inc_ZCL_SQN(self, NwkId) + payload = "11" + sqn + "f0" + mode + raw_APS_request(self, NwkId, "01", "0300", "0104", payload, zigpyzqn=sqn, zigate_ep=ZIGATE_EP, ackIsDisabled=False) + +def tuya_Move_To_Hue_Saturation( self, NwkId, EPout, hue, saturation, transition, level): + # Command 0x06 + self.log.logging("Tuya", "Debug", "tuya_Move_To_Hue_Saturation", NwkId) + + hue = "%02x" % hue + saturation = "%02x" % saturation + + sqn = get_and_inc_ZCL_SQN(self, NwkId) + payload = "11" + sqn + "06" + hue + saturation + "0000" + "%02x" %level + + raw_APS_request(self, NwkId, EPout, "0300", "0104", payload, zigpyzqn=sqn, zigate_ep=ZIGATE_EP, ackIsDisabled=False) + + +def tuya_Move_To_Hue_Saturation_Brightness( self, NwkId, epout, hue, saturation, brightness): + # Command 0xe1 + self.log.logging("Tuya", "Debug", "tuya_Move_To_Hue_Saturation_Brightness", NwkId) + + saturation = "%04x" % struct.unpack("H", struct.pack(">H", saturation))[0] + hue = "%04x" % struct.unpack("H", struct.pack(">H", hue, ))[0] + brightness = "%04x" % struct.unpack("H", struct.pack(">H", brightness))[0] + sqn = get_and_inc_ZCL_SQN(self, NwkId) + payload = "11" + sqn + "e1" + hue + saturation + brightness + + raw_APS_request(self, NwkId, epout, "0300", "0104", payload, zigpyzqn=sqn, zigate_ep=ZIGATE_EP, ackIsDisabled=False) diff --git a/Zigbee/zclCommands.py b/Zigbee/zclCommands.py index 28637ba78..d61f4eee0 100644 --- a/Zigbee/zclCommands.py +++ b/Zigbee/zclCommands.py @@ -36,6 +36,7 @@ zcl_raw_read_report_config_request, zcl_raw_remove_all_groups, zcl_raw_remove_group_member_ship, + zcl_raw_reset_device, zcl_raw_send_group_member_ship_identify, zcl_raw_window_covering, zcl_raw_write_attributeNoResponse) @@ -44,6 +45,11 @@ # Standard commands +def zcl_reset_device(self, nwkid, epin, epout): + self.log.logging("zdpCommand", "Debug", "zcl_reset_device %s %s %s" % (nwkid, epin, epout)) + if "ControllerInRawMode" in self.pluginconf.pluginConf and self.pluginconf.pluginConf["ControllerInRawMode"]: + return zcl_raw_reset_device(self, nwkid, epin, epout) + return send_zigatecmd_raw(self, "0050", "02" + nwkid + epin + epout) def zcl_read_attribute(self, nwkid, EpIn, EpOut, Cluster, direction, manufacturer_spec, manufacturer, lenAttr, Attr, ackIsDisabled=DEFAULT_ACK_MODE): self.log.logging("zclCommand", "Debug", "read_attribute %s %s %s %s %s %s %s %s %s" % (nwkid, EpIn, EpOut, Cluster, direction, manufacturer_spec, manufacturer, lenAttr, Attr)) diff --git a/Zigbee/zclRawCommands.py b/Zigbee/zclRawCommands.py index eec66c217..93d52486d 100644 --- a/Zigbee/zclRawCommands.py +++ b/Zigbee/zclRawCommands.py @@ -15,6 +15,34 @@ # General Command Frame +def zcl_raw_reset_device(self, nwkid, epin, epout): + """ + Sends a raw ZCL reset device command to a Zigbee device to reset it. + + The raw command contains the frame control, sequence number, command, + and payload required for the ZCL reset device command. It is sent directly + without additional ZCL framing. + + Args: + nwkid: The network ID of the device to reset. + epin: The endpoint ID to send the command from. + epout: The endpoint ID to send the command to. + + Returns: + The sequence number used in the command. + """ + + self.log.logging("zclCommand", "Debug", "zcl_raw_default_response %s" % nwkid) + frame_control_field = "%02x" %0b0001_0001 + cmd = "00" + cluster = "0000" + sqn = get_and_inc_ZCL_SQN(self, nwkid) + payload = frame_control_field + sqn + cmd + zcl_command_formated_logging( self, "Reset Device (Raw)", nwkid, epout, sqn, cluster) + raw_APS_request(self, nwkid, epout, cluster, "0104", payload, zigpyzqn=sqn, zigate_ep=epin, ackIsDisabled=False) + return sqn + + # Read Attributes Command def rawaps_read_attribute_req(self, nwkid, EpIn, EpOut, Cluster, direction, manufacturer_spec, manufacturer, Attr, ackIsDisabled=DEFAULT_ACK_MODE, groupaddrmode=False): self.log.logging("zclCommand", "Debug", "rawaps_read_attribute_req %s %s %s %s %s %s %s %s" % (nwkid, EpIn, EpOut, Cluster, direction, manufacturer_spec, manufacturer, Attr)) From 39410dbb8d6d14ce8834de6d9c488b0c13d8d083 Mon Sep 17 00:00:00 2001 From: Pipiche <8291674+pipiche38@users.noreply.github.com> Date: Sun, 29 Oct 2023 17:17:30 +0100 Subject: [PATCH 14/47] upgrade to use zigpy packet_receive instead of handle_message (#1662) zigpy handle_message is deprecated, moving to packet_received --- Classes/ZigpyTransport/AppBellows.py | 21 ++---- Classes/ZigpyTransport/AppDeconz.py | 14 +--- Classes/ZigpyTransport/AppGeneric.py | 106 ++++++++++++--------------- Classes/ZigpyTransport/AppZnp.py | 16 +--- plugin.py | 2 +- requirements.txt | 2 +- 6 files changed, 61 insertions(+), 100 deletions(-) diff --git a/Classes/ZigpyTransport/AppBellows.py b/Classes/ZigpyTransport/AppBellows.py index e889497c4..c0d6c5b7f 100644 --- a/Classes/ZigpyTransport/AppBellows.py +++ b/Classes/ZigpyTransport/AppBellows.py @@ -7,7 +7,8 @@ import logging import bellows.config as bellows_conf -import bellows.types as t +import bellows.types as bt +import zigpy.types as t import bellows.zigbee.application import zigpy.config as zigpy_conf import zigpy.device @@ -136,7 +137,7 @@ async def register_endpoints(self, endpoint=1): def get_device(self, ieee=None, nwk=None): return Classes.ZigpyTransport.AppGeneric.get_device(self, ieee, nwk) - def handle_join(self, nwk: t.EmberNodeId, ieee: t.EmberEUI64, parent_nwk: t.EmberNodeId, *, handle_rejoin: bool = True,) -> None: + def handle_join(self, nwk: bt.EmberNodeId, ieee: bt.EmberEUI64, parent_nwk: bt.EmberNodeId, *, handle_rejoin: bool = True,) -> None: return Classes.ZigpyTransport.AppGeneric.handle_join(self, nwk, ieee, parent_nwk) def get_device_ieee(self, nwk): @@ -148,19 +149,9 @@ def handle_leave(self, nwk, ieee): def get_zigpy_version(self): return Classes.ZigpyTransport.AppGeneric.get_zigpy_version(self) - def handle_message( - self, - sender: zigpy.device.Device, - profile: int, - cluster: int, - src_ep: int, - dst_ep: int, - message: bytes, - * , - dst_addressing=None, - )->None: - return Classes.ZigpyTransport.AppGeneric.handle_message(self,sender,profile,cluster,src_ep,dst_ep,message, dst_addressing=dst_addressing) - + def packet_received(self, packet: t.ZigbeePacket) -> None: + return Classes.ZigpyTransport.AppGeneric.packet_received(self,packet) + async def set_zigpy_tx_power(self, power): # EmberConfigTxPowerMode - EZSP_CONFIG_TX_POWER_MODE in EzspConfigId # 0x00: Normal mode diff --git a/Classes/ZigpyTransport/AppDeconz.py b/Classes/ZigpyTransport/AppDeconz.py index 4b109fd8d..e7ace4f73 100644 --- a/Classes/ZigpyTransport/AppDeconz.py +++ b/Classes/ZigpyTransport/AppDeconz.py @@ -142,18 +142,8 @@ def get_device_ieee(self, nwk): def handle_leave(self, nwk, ieee): Classes.ZigpyTransport.AppGeneric.handle_leave(self, nwk, ieee) - def handle_message( - self, - sender: zigpy.device.Device, - profile: int, - cluster: int, - src_ep: int, - dst_ep: int, - message: bytes, - * , - dst_addressing=None, - ) -> None: - return Classes.ZigpyTransport.AppGeneric.handle_message(self,sender,profile,cluster,src_ep,dst_ep,message,dst_addressing=dst_addressing) + def packet_received(self, packet: t.ZigbeePacket) -> None: + return Classes.ZigpyTransport.AppGeneric.packet_received(self,packet) async def set_zigpy_tx_power(self, power): pass diff --git a/Classes/ZigpyTransport/AppGeneric.py b/Classes/ZigpyTransport/AppGeneric.py index 855f2daa2..a1d1911ba 100644 --- a/Classes/ZigpyTransport/AppGeneric.py +++ b/Classes/ZigpyTransport/AppGeneric.py @@ -34,7 +34,7 @@ async def initialize(self, *, auto_form: bool = False, force_form: bool = False) Starts the network on a connected radio, optionally forming one with random settings if necessary. """ - self.log.logging("TransportZigpy", "Debug", "AppGeneric:initialize auto_form: %s force_form: %s Class: %s" %( auto_form, force_form, type(self))) + self.log.logging("TransportZigpy", "Log", "AppGeneric:initialize auto_form: %s force_form: %s Class: %s" %( auto_form, force_form, type(self))) _retreived_backup = None if "autoRestore" in self.pluginconf.pluginConf and self.pluginconf.pluginConf["autoRestore"]: @@ -45,7 +45,7 @@ async def initialize(self, *, auto_form: bool = False, force_form: bool = False) if _retreived_backup: if self.pluginconf.pluginConf[ "OverWriteCoordinatorIEEEOnlyOnce"]: - LOGGER.debug("Allow eui64 overwrite only once !!!") + LOGGER.debug("Allow eui64 overwrite only once !!!") _retreived_backup.network_info.stack_specific.setdefault("ezsp", {})[ "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it"] = True LOGGER.debug("Last backup retreived: %s" % _retreived_backup ) @@ -161,7 +161,7 @@ def get_device_ieee(self, nwk): return None def handle_leave(self, nwk, ieee): - self.log.logging("TransportZigpy", "Debug","handle_leave (0x%04x %s)" %(nwk, ieee)) + self.log.logging("TransportZigpy", "Log","handle_leave (0x%04x %s)" %(nwk, ieee)) plugin_frame = build_plugin_8048_frame_content(self, ieee) self.callBackFunction(plugin_frame) super(type(self),self).handle_leave(nwk, ieee) @@ -171,86 +171,74 @@ def get_zigpy_version(self): LOGGER.debug("get_zigpy_version ake version number. !!") return self.version -def handle_message( - self, - sender: zigpy.device.Device, - profile: int, - cluster: int, - src_ep: int, - dst_ep: int, - message: bytes, - dst_addressing=None, -) -> None: +def packet_received(self, packet: t.ZigbeePacket) -> None: + """Notify zigpy of a received Zigbee packet.""" + try: + sender = self.get_device_with_address(packet.src) + self.log.logging("TransportZigpy", "Debug", "identified device - %s (%s)" %(str(sender), type(sender)) ) + + except KeyError: + self.log.logging("TransportZigpy", "Debug", "Unknown device %r", packet.src) + return + + self.log.logging("TransportZigpy", "Debug", "identified device - %s (%s)" % (str(sender), type(sender))) - write_capture_rx_frames( self, sender, profile, cluster, src_ep, dst_ep, message, binascii.hexlify(message).decode("utf-8"), dst_addressing) + profile, cluster, src_ep, dst_ep = packet.profile_id, packet.cluster_id, packet.src_ep, packet.dst_ep + message = packet.data.serialize() + hex_message = binascii.hexlify(message).decode("utf-8") + dst_addressing = packet.dst.addr_mode if packet.dst else None + write_capture_rx_frames( self, sender, profile, cluster, src_ep, dst_ep, message, hex_message, dst_addressing) + + self.log.logging("TransportZigpy", "Debug", "packet_received - %s %s %s %s %s %s %s %s" %( + sender, profile, cluster, src_ep, dst_ep, message, hex_message, dst_addressing)) + if sender.nwk == 0x0000: - self.log.logging("TransportZigpy", "Debug", "handle_message from Controller Sender: %s Profile: %04x Cluster: %04x srcEp: %02x dstEp: %02x message: %s" %( - str(sender.nwk), profile, cluster, src_ep, dst_ep, binascii.hexlify(message).decode("utf-8"))) - #self.super().handle_message(sender, profile, cluster, src_ep, dst_ep, message) - super(type(self),self).handle_message(sender, profile, cluster, src_ep, dst_ep, message) + # When coming from coordinator we have to send it back to zigpy + self.log.logging("TransportZigpy", "Debug", "packet_received from Controller Sender: %s Profile: %04x Cluster: %04x srcEp: %02x dstEp: %02x message: %s" %( + str(sender.nwk), profile, cluster, src_ep, dst_ep, hex_message)) + super(type(self),self).packet_received(packet) if cluster == 0x8036: # This has been handle via on_zdo_mgmt_permitjoin_rsp() - self.log.logging("TransportZigpy", "Debug", "handle_message 0x8036: %s Profile: %04x Cluster: %04x srcEp: %02x dstEp: %02x message: %s" %( - str(sender.nwk), profile, cluster, src_ep, dst_ep, binascii.hexlify(message).decode("utf-8"))) - self.callBackFunction( build_plugin_8014_frame_content(self, str(sender), binascii.hexlify(message).decode("utf-8") ) ) - super(type(self),self).handle_message(sender, profile, cluster, src_ep, dst_ep, message) + self.log.logging("TransportZigpy", "Debug", "packet_received 0x8036: %s Profile: %04x Cluster: %04x srcEp: %02x dstEp: %02x message: %s" %( + str(sender.nwk), profile, cluster, src_ep, dst_ep, hex_message)) + self.callBackFunction( build_plugin_8014_frame_content(self, str(sender), hex_message ) ) + super(type(self),self).packet_received(packet) return if cluster == 0x8034: # This has been handle via on_zdo_mgmt_leave_rsp() - self.log.logging("TransportZigpy", "Debug", "handle_message 0x8036: %s Profile: %04x Cluster: %04x srcEp: %02x dstEp: %02x message: %s" %( - str(sender.nwk), profile, cluster, src_ep, dst_ep, binascii.hexlify(message).decode("utf-8"))) - self.callBackFunction( build_plugin_8047_frame_content(self, str(sender), binascii.hexlify(message).decode("utf-8")) ) + self.log.logging("TransportZigpy", "Debug", "packet_received 0x8036: %s Profile: %04x Cluster: %04x srcEp: %02x dstEp: %02x message: %s" %( + str(sender.nwk), profile, cluster, src_ep, dst_ep, hex_message)) + self.callBackFunction( build_plugin_8047_frame_content(self, str(sender), hex_message) ) return addr = None if sender.nwk is not None: - addr_mode = 0x02 addr = sender.nwk.serialize()[::-1].hex() - if profile and cluster: - self.log.logging( - "TransportZigpy", - "Debug", - "handle_message device 1: %s Profile: %04x Cluster: %04x sEP: %s dEp: %s message: %s lqi: %s" % ( - str(sender), profile, cluster, src_ep, dst_ep, binascii.hexlify(message).decode("utf-8"), sender.lqi)), - elif sender.ieee is not None: addr = "%016x" % t.uint64_t.deserialize(sender.ieee.serialize())[0] - addr_mode = 0x03 - if profile and cluster: - self.log.logging( - "TransportZigpy", - "Debug", - "handle_message device 1: %s Profile: %04x Cluster: %04x sEP: %s dEp: %s message: %s lqi: %s" % ( - str(sender), profile, cluster, src_ep, dst_ep, binascii.hexlify(message).decode("utf-8"), sender.lqi)), - - if sender.lqi is None: - sender.lqi = 0x00 + + addr_mode = 0x02 if sender.nwk is not None else 0x03 + sender.lqi = sender.lqi or 0x00 + # Let's force profile to ZDP if eps == 0x00 if src_ep == dst_ep == 0x00: profile = 0x0000 - if profile and cluster: - self.log.logging( - "TransportZigpy", - "Debug", - "handle_message device 2: %s Profile: %04x Cluster: %04x sEP: %s dEp: %s message: %s lqi: %s" % ( - str(addr), profile, cluster, src_ep, dst_ep, binascii.hexlify(message).decode("utf-8"), sender.lqi), - ) + if profile is not None and cluster is not None: + self.log.logging( "TransportZigpy", "Debug", "packet_received device: %s Profile: %04x Cluster: %04x sEP: %s dEp: %s message: %s lqi: %s" % ( + str(addr), profile, cluster, src_ep, dst_ep, hex_message, sender.lqi), ) - if addr: + if addr is not None: plugin_frame = build_plugin_8002_frame_content(self, addr, profile, cluster, src_ep, dst_ep, message, sender.lqi, src_addrmode=addr_mode) - self.log.logging("TransportZigpy", "Debug", "handle_message Sender: %s frame for plugin: %s" % (addr, plugin_frame)) - self.callBackFunction(plugin_frame) - else: - self.log.logging( - "TransportZigpy", - "Error", - "handle_message - Issue with sender is %s %s" % (sender.nwk, sender.ieee), - ) + self.log.logging("TransportZigpy", "Debug", "packet_received Sender: %s frame for plugin: %s" % (addr, plugin_frame)) + return self.callBackFunction(plugin_frame) + + self.log.logging( "TransportZigpy", "Error", "packet_received - Issue with sender is %s %s" % ( + sender.nwk, sender.ieee), ) return diff --git a/Classes/ZigpyTransport/AppZnp.py b/Classes/ZigpyTransport/AppZnp.py index 609437e55..b6de747e8 100644 --- a/Classes/ZigpyTransport/AppZnp.py +++ b/Classes/ZigpyTransport/AppZnp.py @@ -13,7 +13,8 @@ import zigpy.zdo.types as zdo_types import zigpy_znp.commands.util import zigpy_znp.config as znp_conf -import zigpy_znp.types as t +#import zigpy_znp.types as znp_t +import zigpy.types as t import zigpy_znp.zigbee.application from Classes.ZigpyTransport.firmwareversionHelper import \ znp_extract_versioning_for_plugin @@ -116,17 +117,8 @@ def handle_leave(self, nwk, ieee): def get_zigpy_version(self): return Classes.ZigpyTransport.AppGeneric.get_zigpy_version(self) - def handle_message( - self, - sender: zigpy.device.Device, - profile: int, - cluster: int, - src_ep: int, - dst_ep: int, - message: bytes, - dst_addressing=None, - ) -> None: - return Classes.ZigpyTransport.AppGeneric.handle_message(self,sender,profile,cluster,src_ep,dst_ep,message, dst_addressing=dst_addressing) + def packet_received(self, packet: t.ZigbeePacket) -> None: + return Classes.ZigpyTransport.AppGeneric.packet_received(self,packet) async def set_zigpy_tx_power(self, power): self.log.logging("TransportZigpy", "Debug", "set_tx_power %s" %power) diff --git a/plugin.py b/plugin.py index b8ea0a818..58c09d6b1 100644 --- a/plugin.py +++ b/plugin.py @@ -1502,7 +1502,7 @@ def update_DB_device_status_to_reinit( self ): def check_python_modules_version( self ): MODULES_VERSION = { - "zigpy": "0.58.1", + "zigpy": "0.59.0", "zigpy_znp": "0.11.6", "zigpy_deconz": "0.21.1", "bellows": "0.36.8", diff --git a/requirements.txt b/requirements.txt index c265e1938..8c3ab80dc 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -zigpy==0.58.1 +zigpy==0.59.0 zigpy_deconz==0.21.1 zigpy-cli==1.0.4 zigpy_znp==0.11.6 From 33d3085fc5ad1bb2b1215d5e6f4b41ecd1b73975 Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Sun, 29 Oct 2023 17:29:17 +0100 Subject: [PATCH 15/47] update --- ReleaseNotes.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index 45fe1926f..ee5150b93 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -18,6 +18,17 @@ Release Numbering - Odd numbers --> Stable/6 - Even numbers --> Beta/6 (dev branch) +## November 2023 - stable7 7.1.005 ( 2023.11 ) + +- [Technical] - zigpy radio libraries upgrade +- [Technical] - fix issue on Groups +- [Technical] - Fix issue which was producing error message "No DPS stanza in config file for ....." +- [Technical] - Refactor how IAS Alarm 1 & 2 are managed. + +- [Hardware] - Fixing error on Heiman manufacturer command +- [Hardware] - Aotec zigbee button support +- [Hardware] - Tuya Hue and Saturation for Lidl "TS0505A", "_TZ3000_dbou1ap4" + ## October 2023 - stable7 - 7.1.004 ( 2023.10 ) - [Hardware] - Ronelabs SEM101 Meter From ce7ae5af20befbfc86e67ce100362d36dedc2c0c Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Sun, 29 Oct 2023 19:28:22 +0100 Subject: [PATCH 16/47] helpers to access nValue/sValue and SwitchType/SubType/Type --- Modules/domoticzAbstractLayer.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/Modules/domoticzAbstractLayer.py b/Modules/domoticzAbstractLayer.py index 47a39499c..dfe7eac06 100644 --- a/Modules/domoticzAbstractLayer.py +++ b/Modules/domoticzAbstractLayer.py @@ -282,6 +282,33 @@ def domo_update_api(self, Devices, DeviceID_, Unit_, nValue, sValue, SignalLevel else: Devices[Unit_].Update( nValue=int(nValue), sValue=str(sValue), SignalLevel=int(SignalLevel), BatteryLevel=int(BatteryLevel), TimedOut=0, ) +def domo_read_nValue_sValue(self, Devices, DeviceID, Unit): + """ + Read the nValue and sValue of a device unit. + + Args: + Devices: The dictionary of devices. + DeviceID: The ID of the device. + Unit: The unit number of the device. + + Returns: + Tuple: A tuple containing the nValue and sValue of the device unit. + """ + + if DOMOTICZ_EXTENDED_API: + _unit = Devices[DeviceID].Units[Unit] + else: + _unit = Devices[Unit] + + return _unit.nValue, _unit.sValue + +def domo_read_SwitchType_SubType_Type(self, Devices, DeviceID, Unit): + if DOMOTICZ_EXTENDED_API: + _unit = Devices[DeviceID].Units[Unit] + else: + _unit = Devices[Unit] + + return _unit.SwitchType, _unit.SubType, _unit.Type def _is_meter_widget( self, Devices,DeviceID_, Unit_): # self.log.logging("AbstractDz", "Debug", "_is_meter_widget: %s %s" %(DeviceID_, Unit_)) From e50460f02c1629cb22b9d78853ab6d45456e014a Mon Sep 17 00:00:00 2001 From: Pipiche <8291674+pipiche38@users.noreply.github.com> Date: Tue, 31 Oct 2023 08:14:32 +0100 Subject: [PATCH 17/47] Sonoff SNZB-02 gets connected as SONOFF Temp/Humi and is a Motion (#1661) --- Modules/pluginModels.py | 22 ++++++++++++++++++---- Modules/zclClusterHelpers.py | 14 ++++++++------ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/Modules/pluginModels.py b/Modules/pluginModels.py index d7401c3b9..c94b55a8c 100644 --- a/Modules/pluginModels.py +++ b/Modules/pluginModels.py @@ -34,12 +34,11 @@ def check_found_plugin_model( self, model, manufacturer_name=None, manufacturer_ if "Model" in x and model not in x["Model"]: continue if ( - "Manufacturer" in x and x["Manufacturer"] and manufacturer_name not in x["Manufacturer"] - or "ManufId" in x and x["ManufId"] and manufacturer_code not in x["ManufId"] + ( "Manufacturer" in x and x["Manufacturer"] and manufacturer_name not in x["Manufacturer"] ) + or ( "ManufId" in x and x["ManufId"] and manufacturer_code not in x["ManufId"]) + or ( "DeviceID" in x and x["DeviceID"] and device_id not in x["DeviceID"] ) ): continue - if "DeviceID" in x and x["DeviceID"] and device_id not in x["DeviceID"]: - continue self.log.logging( "Pairing", "Log", "check_found_plugin_model - Found %s" % x) @@ -240,5 +239,20 @@ def check_found_plugin_model( self, model, manufacturer_name=None, manufacturer_ "ManufId": [], "PluginModelName": "TS0601-_TZE200_dzuqwsyg",}, + # SONOFF 66666 'Temperature and humidity sensor',: + { + "Model": ["66666",], + "Manufacturer": "eWeLink", + "DeviceID": "0302", + "PluginModelName": "66666-temphumi.json" + }, + + # SONOFF 66666 'Motion' + { + "Model": ["66666",], + "Manufacturer": "eWeLink", + "DeviceID": "0402", + "PluginModelName": "66666-motion.json" + } ] diff --git a/Modules/zclClusterHelpers.py b/Modules/zclClusterHelpers.py index 8f057e931..4ea66372a 100644 --- a/Modules/zclClusterHelpers.py +++ b/Modules/zclClusterHelpers.py @@ -220,24 +220,26 @@ def _upd_data_strut_based_on_model(self, MsgSrcAddr, modelName, inital_ep): def _build_model_name( self, nwkid, modelName): - manufacturer_name = self.ListOfDevices[nwkid]["Manufacturer Name"] if "Manufacturer Name" in self.ListOfDevices[nwkid] else "" - manuf_code = self.ListOfDevices[nwkid]["Manufacturer"] if "Manufacturer" in self.ListOfDevices[nwkid] else "" + manufacturer_name = self.ListOfDevices[nwkid].get("Manufacturer Name", "") + manuf_code = self.ListOfDevices[nwkid].get("Manufacturer", "") + zdevice_id = self.ListOfDevices[nwkid].get("ZDeviceID", None) + if modelName in ( '66666', ): + # https://github.com/Koenkk/zigbee2mqtt/issues/4338 + return check_found_plugin_model( self, modelName, manufacturer_name=manufacturer_name, manufacturer_code=manuf_code, device_id=zdevice_id) # Try to check if the Model name is in the DeviceConf list ( optimised devices) if modelName + '-' + manufacturer_name in self.DeviceConf: return modelName + '-' + manufacturer_name - + if modelName + manufacturer_name in self.DeviceConf: return modelName + manufacturer_name - + # If not found, let see if the model name can be extracted from the (ModelName, ManufacturerName) tuple set in the Conf file as Identifier plugin_identifier = plugin_self_identifier( self, modelName, manufacturer_name) if plugin_identifier: return plugin_identifier - zdevice_id = self.ListOfDevices[nwkid]["ZDeviceID"] if "ZDeviceID" in self.ListOfDevices[nwkid] and self.ListOfDevices[nwkid]["ZDeviceID"] else None - return check_found_plugin_model( self, modelName, manufacturer_name=manufacturer_name, manufacturer_code=manuf_code, device_id=zdevice_id) From 7b4efd8fe806b8039f6ff81c6312c07b4495c145 Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Wed, 1 Nov 2023 10:02:31 +0100 Subject: [PATCH 18/47] Move level for WW color was not correctly handled --- Modules/actuators.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Modules/actuators.py b/Modules/actuators.py index 9f7bba38c..96aa7a215 100644 --- a/Modules/actuators.py +++ b/Modules/actuators.py @@ -255,6 +255,7 @@ def actuator_setcolor(self, nwkid, EPout, value, Color): if Hue_List["m"] == 2: # ColorModeTemp = 2 // White with color temperature. Valid fields: t handle_color_mode_2(self, nwkid, EPout, Hue_List) + actuator_setlevel(self, nwkid, EPout, value, "Light", transitionMoveLevel) elif Hue_List["m"] == 3 and force_color_command == "TuyaMovetoHueandSaturation": handle_color_mode_tuya( self, nwkid, EPout, Hue_List, value) @@ -281,6 +282,7 @@ def handle_color_mode_2(self, nwkid, EPout, Hue_List): self.log.logging( "Command", "Debug", "handle_color_mode_2 Set Temp Kelvin: %s-%s" % (TempMired, Hex_Format(4, TempMired)), nwkid ) transitionMoveLevel , transitionRGB , transitionMoveLevel , transitionHue , transitionTemp = get_all_transition_mode( self, nwkid) zcl_move_to_colour_temperature( self, nwkid, EPout, Hex_Format(4, TempMired), transitionTemp) + def handle_color_mode_3(self, nwkid, EPout, Hue_List): From 4d8198be292657b5056ba900bc7a02f7f10c9083 Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Wed, 1 Nov 2023 21:44:11 +0100 Subject: [PATCH 19/47] use a try/except to protect the binascii.unhexlify() --- DevicesModules/custom_zlinky.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/DevicesModules/custom_zlinky.py b/DevicesModules/custom_zlinky.py index 4913d8851..daab5f734 100644 --- a/DevicesModules/custom_zlinky.py +++ b/DevicesModules/custom_zlinky.py @@ -598,9 +598,14 @@ def zlinky_cluster_lixee_private(self, Devices, nwkid, ep, cluster, attribut, va elif attribut == "0217": # Standard : STGE self.log.logging( "ZLinky", "Log", "STGE raw Value: %s" % ( value )) - stge = binascii.unhexlify( value ).decode("utf-8") - self.log.logging( "ZLinky", "Log", "STGE unhexlify Value: %s" % ( stge )) - self.log.logging( "ZLinky", "Log", "STGE decoded : %s" % ( decode_STEG( stge ) )) + try: + stge = binascii.unhexlify( value ).decode("utf-8") + self.log.logging( "ZLinky", "Log", "STGE unhexlify Value: %s/%s" % ( value, stge )) + except Exception as e: + self.log.logging( "ZLinky", "Log", "STGE Value: %s" % ( value )) + stge = value + + self.log.logging( "ZLinky", "Log", "STGE decoded %s : %s" % ( stge, decode_STEG( stge ) )) store_ZLinky_infos( self, nwkid, "STGE", decode_STEG( stge )) checkAndStoreAttributeValue(self, nwkid, ep, cluster, attribut, stge) @@ -661,5 +666,4 @@ def zlinky_cluster_lixee_private(self, Devices, nwkid, ep, cluster, attribut, va elif attribut == "0300": # Linky Mode - update_zlinky_device_model_if_needed( self, nwkid ) - + update_zlinky_device_model_if_needed( self, nwkid ) \ No newline at end of file From 54cd0ee5dd229a725798498a99fdd7db0ff69655 Mon Sep 17 00:00:00 2001 From: Pipiche <8291674+pipiche38@users.noreply.github.com> Date: Thu, 2 Nov 2023 18:51:43 +0100 Subject: [PATCH 20/47] Add ep for tuyats0601 (#1663) * add possibility to force Ep when doing domo_update --- Modules/tuyaTS0601.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/Modules/tuyaTS0601.py b/Modules/tuyaTS0601.py index 0f5a40de0..fd627786b 100644 --- a/Modules/tuyaTS0601.py +++ b/Modules/tuyaTS0601.py @@ -38,9 +38,9 @@ def ts0601_response(self, Devices, model_name, NwkId, Ep, dp, datatype, data): value = int(data, 16) self.log.logging("Tuya0601", "Debug", " - value: %s" % (value), NwkId) - self.log.logging("Tuya0601", "Debug", " - dps_mapping[ %s ]: %s (%s)" % ( str_dp, dps_mapping[ str_dp ], type(dps_mapping[ str_dp ])), NwkId) + if not isinstance( dps_mapping[ str_dp ], list): # We complex data point which provide multiple value return process_dp_item( self, Devices, model_name, NwkId, Ep, dp, datatype, data, dps_mapping[ str_dp ], value) @@ -69,21 +69,21 @@ def sensor_type( self, Devices, NwkId, Ep, value, dp, datatype, data, dps_mappin store_tuya_attribute(self, NwkId, "UnknowDp_0x%02x_Dt_0x%02x" % (dp, datatype) , data) return True - divisor = dps_mapping_item["domo_divisor"] if "domo_divisor" in dps_mapping_item else 1 - value = value / divisor - rounding = dps_mapping_item["domo_round"] if "domo_round" in dps_mapping_item else 0 - value = round( value, rounding ) if rounding else int(value) + # we will overwrite the end point as, we have to force the domo update on a specific ep.add() + domo_ep = dps_mapping_item.get("domo_ep", Ep) + self.log.logging("Tuya0601", "Debug", " - Ep to be used for domo update %s" %domo_ep) + + divisor = dps_mapping_item.get("domo_divisor", 1) + value /= divisor + + rounding = dps_mapping_item.get("domo_round", 0) + value = round(value, rounding) if rounding else int(value) self.log.logging("Tuya0601", "Debug", " - after sensor_type() value: %s divisor: %s rounding: %s" % (value, divisor, rounding), NwkId) sensor_type = dps_mapping_item[ "sensor_type"] - if sensor_type in DP_SENSOR_FUNCTION: - value = check_domo_format_req( self, dps_mapping_item, value) - func = DP_SENSOR_FUNCTION[ sensor_type ] - func(self, Devices, NwkId, Ep, value ) - return True - - return False + return process_sensor_data(self, sensor_type, dps_mapping_item, value, Devices, NwkId, domo_ep) + def ts0601_actuator( self, NwkId, command, value=None): self.log.logging("Tuya0601", "Debug", "ts0601_actuator - requesting %s %s" %( @@ -128,7 +128,17 @@ def ts0601_actuator( self, NwkId, command, value=None): func(self, NwkId, "01", dp ) -# Helpers +# Helpers + +def process_sensor_data(self, sensor_type, dps_mapping_item, value, Devices, NwkId, domo_ep): + if sensor_type in DP_SENSOR_FUNCTION: + formatted_value = check_domo_format_req(self, dps_mapping_item, value) + sensor_function = DP_SENSOR_FUNCTION[sensor_type] + sensor_function(self, Devices, NwkId, domo_ep, formatted_value) + return True + return False + + def read_uint16_be(data, offset): # Use the format '>H' to specify big-endian (>) and 'H' for 16-bit unsigned integer. return struct.unpack_from('>H', data, offset)[0] From ae298dd1381ba06023299602b9bdd32921769d8f Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Thu, 2 Nov 2023 21:46:34 +0100 Subject: [PATCH 21/47] fix debug message --- Classes/ZigpyTransport/AppGeneric.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Classes/ZigpyTransport/AppGeneric.py b/Classes/ZigpyTransport/AppGeneric.py index a1d1911ba..04d1fdcc6 100644 --- a/Classes/ZigpyTransport/AppGeneric.py +++ b/Classes/ZigpyTransport/AppGeneric.py @@ -176,13 +176,13 @@ def packet_received(self, packet: t.ZigbeePacket) -> None: """Notify zigpy of a received Zigbee packet.""" try: sender = self.get_device_with_address(packet.src) - self.log.logging("TransportZigpy", "Debug", "identified device - %s (%s)" %(str(sender), type(sender)) ) + self.log.logging("TransportZigpy", "Debug", "packet_received identified device - %s (%s)" %(str(sender), type(sender)) ) except KeyError: - self.log.logging("TransportZigpy", "Debug", "Unknown device %r", packet.src) + self.log.logging("TransportZigpy", "Debug", "packet_received Unknown device %r" %packet.src) return - self.log.logging("TransportZigpy", "Debug", "identified device - %s (%s)" % (str(sender), type(sender))) + self.log.logging("TransportZigpy", "Debug", "packet_received identified device - %s (%s)" % (str(sender), type(sender))) profile, cluster, src_ep, dst_ep = packet.profile_id, packet.cluster_id, packet.src_ep, packet.dst_ep message = packet.data.serialize() From 0ec30eb6d9eec696bae4624a3b04cdf44c699472 Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Thu, 2 Nov 2023 22:29:49 +0100 Subject: [PATCH 22/47] better debug on packet_received --- Classes/ZigpyTransport/AppGeneric.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Classes/ZigpyTransport/AppGeneric.py b/Classes/ZigpyTransport/AppGeneric.py index 04d1fdcc6..fe3d7285d 100644 --- a/Classes/ZigpyTransport/AppGeneric.py +++ b/Classes/ZigpyTransport/AppGeneric.py @@ -182,7 +182,8 @@ def packet_received(self, packet: t.ZigbeePacket) -> None: self.log.logging("TransportZigpy", "Debug", "packet_received Unknown device %r" %packet.src) return - self.log.logging("TransportZigpy", "Debug", "packet_received identified device - %s (%s)" % (str(sender), type(sender))) + self.log.logging("TransportZigpy", "Debug", "packet_received identified device %r -> %r with source route %r" %( + packet.src.address, packet.dst.address, packet.source_route )) profile, cluster, src_ep, dst_ep = packet.profile_id, packet.cluster_id, packet.src_ep, packet.dst_ep message = packet.data.serialize() From 20324110def915f76825e9f2599aefbcf88cf817 Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Fri, 3 Nov 2023 15:10:07 +0100 Subject: [PATCH 23/47] Adding PowerFactor handling --- Modules/domoCreate.py | 1 + Modules/domoMaj.py | 16 +++++++++------- Modules/domoTools.py | 3 ++- Modules/tuyaTS0601.py | 17 +++++++++++------ 4 files changed, 23 insertions(+), 14 deletions(-) diff --git a/Modules/domoCreate.py b/Modules/domoCreate.py index 09ea64763..b99723a51 100644 --- a/Modules/domoCreate.py +++ b/Modules/domoCreate.py @@ -549,6 +549,7 @@ def set_default_value( self, Devices, unit, widget_record): "Alarm_ZL2": { "Type": 243, "Subtype": 22, "Switchtype": 0, }, "Alarm_ZL3": { "Type": 243, "Subtype": 22, "Switchtype": 0, }, "AirPurifierAlarm": { "Type": 243, "Subtype": 22, "Switchtype": 0, }, + "PowerFactor": { "Type": 243, "Subtype": 6, "Switchtype": 0, }, "Valve": { "Type": 243, "Subtype": 6, "Switchtype": 0, }, "FanSpeed": { "Type": 243, "Subtype": 6, "Switchtype": 0, }, "ThermoSetpoint": { "Type": 242, "Subtype": 1, }, diff --git a/Modules/domoMaj.py b/Modules/domoMaj.py index aa38a73aa..72079f932 100644 --- a/Modules/domoMaj.py +++ b/Modules/domoMaj.py @@ -581,13 +581,15 @@ def MajDomoDevice(self, Devices, NWKID, Ep, clusterID, value, Attribute_="", Col data = "09" UpdateDevice_v2(self, Devices, DeviceUnit, int(data), str(state), BatteryLevel, SignalLevel, ForceUpdate_=True) - if "Valve" in ClusterType: # Valve Position - if WidgetType == "Valve" and Attribute_ in ("026d", "4001", "0008"): - # value int is the % of the valve opening - # Percentage Widget - nValue = round(value, 1) - sValue = str(nValue) - UpdateDevice_v2(self, Devices, DeviceUnit, nValue, sValue, BatteryLevel, SignalLevel) + if "Valve" in ClusterType and (WidgetType == "Valve" and Attribute_ in ("026d", "4001", "0008")): + nValue = round(value, 1) + sValue = str(nValue) + UpdateDevice_v2(self, Devices, DeviceUnit, nValue, sValue, BatteryLevel, SignalLevel) + + if "PowerFactor" in ClusterType and WidgetType == "PowerFactor": + nValue = round(value, 1) + sValue = str(nValue) + UpdateDevice_v2(self, Devices, DeviceUnit, nValue, sValue, BatteryLevel, SignalLevel) if "ThermoMode" in ClusterType: # Thermostat Mode self.log.logging("Widget", "Debug", "ThermoMode %s WidgetType: %s Value: %s (%s) Attribute_: %s" % ( diff --git a/Modules/domoTools.py b/Modules/domoTools.py index 4c3b66640..25cfec594 100644 --- a/Modules/domoTools.py +++ b/Modules/domoTools.py @@ -568,7 +568,8 @@ def GetType(self, Addr, Ep): "fc80": "Heiman", "Distance": "Distance", "TamperSwitch": "TamperSwitch", - "Notification": "Notification" + "Notification": "Notification", + "PowerFactor": "PowerFactor" } def TypeFromCluster(self, cluster, create_=False, ProfileID_="", ZDeviceID_="", ModelName=""): diff --git a/Modules/tuyaTS0601.py b/Modules/tuyaTS0601.py index fd627786b..76b4a06d2 100644 --- a/Modules/tuyaTS0601.py +++ b/Modules/tuyaTS0601.py @@ -303,9 +303,13 @@ def ts0601_current(self, Devices, nwkid, ep, value): self.log.logging( "Tuya0601", "Debug", "ts0601_current - Current %s %s %s" % (nwkid, ep, value), nwkid, ) MajDomoDevice(self, Devices, nwkid, ep, "0b04", value, Attribute_="0508") checkAndStoreAttributeValue(self, nwkid, ep, "0b04", "0508", value) # Store int - store_tuya_attribute(self, nwkid, "Current", value) - + store_tuya_attribute(self, nwkid, "Current_%s" %ep, value) +def ts0601_power_factor(self, Devices, nwkid, ep, value): + self.log.logging( "Tuya0601", "Debug", "ts0601_current - Current %s %s %s" % (nwkid, ep, value), nwkid, ) + MajDomoDevice(self, Devices, nwkid, ep, "PowerFactor", value) + store_tuya_attribute(self, nwkid, "PowerFactor_%s" %ep, value) + def ts0601_summation_energy(self, Devices, nwkid, ep, value): self.log.logging( "Tuya0601", "Debug", "ts0601_summation_energy - Current Summation %s %s %s" % (nwkid, ep, value), nwkid, ) previous_summation = getAttributeValue(self, nwkid, ep, "0702", "0000") @@ -314,19 +318,19 @@ def ts0601_summation_energy(self, Devices, nwkid, ep, value): nwkid, ep, value, previous_summation, current_summation), nwkid, ) MajDomoDevice(self, Devices, nwkid, ep, "0702", current_summation, Attribute_="0000") checkAndStoreAttributeValue(self, nwkid, ep, "0702", "0000", current_summation) # Store int - store_tuya_attribute(self, nwkid, "Energy", value) + store_tuya_attribute(self, nwkid, "Energy_%s" %ep, value) def ts0601_summation_energy_raw(self, Devices, nwkid, ep, value): self.log.logging( "Tuya0601", "Debug", "ts0601_summation_energy - Current Summation %s %s %s" % (nwkid, ep, value), nwkid, ) MajDomoDevice(self, Devices, nwkid, ep, "0702", value, Attribute_="0000") checkAndStoreAttributeValue(self, nwkid, ep, "0702", "0000", value) # Store int - store_tuya_attribute(self, nwkid, "ConsumedEnergy", value) + store_tuya_attribute(self, nwkid, "ConsumedEnergy_%s" %ep, value) def ts0601_production_energy(self, Devices, nwkid, ep, value): self.log.logging( "Tuya0601", "Debug", "ts0601_production_energy - Production Energy %s %s %s" % (nwkid, ep, value), nwkid, ) MajDomoDevice(self, Devices, nwkid, ep, "0702", value, Attribute_="0001") checkAndStoreAttributeValue(self, nwkid, ep, "0702", "0001", value) # Store int - store_tuya_attribute(self, nwkid, "ProducedEnergy", value) + store_tuya_attribute(self, nwkid, "ProducedEnergy_%s" %ep, value) def ts0601_instant_power(self, Devices, nwkid, ep, value): @@ -338,7 +342,7 @@ def ts0601_instant_power(self, Devices, nwkid, ep, value): checkAndStoreAttributeValue(self, nwkid, ep, "0702", "0400", signed_int) MajDomoDevice(self, Devices, nwkid, ep, "0702", signed_int) - store_tuya_attribute(self, nwkid, "InstantPower", signed_int) # Store str + store_tuya_attribute(self, nwkid, "InstantPower_%s" %ep, signed_int) # Store str def ts0601_voltage(self, Devices, nwkid, ep, value): self.log.logging( "Tuya0601", "Debug", "ts0601_voltage - Voltage %s %s %s" % (nwkid, ep, value), nwkid, ) @@ -498,6 +502,7 @@ def ts0601_sensor_irrigation_mode(self, Devices, nwkid, ep, value): "smoke_state": ts0601_smoke_detection, "smoke_ppm": ts0601_smoke_concentration, "water_consumption": ts0601_water_consumption, + "power_factor": ts0601_power_factor, } def ts0601_tuya_cmd(self, NwkId, Ep, action, data): From 3831fd99bbda09d794ba4572d49c550d06948839 Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Sat, 4 Nov 2023 21:02:01 +0100 Subject: [PATCH 24/47] due to zigpy-znp issue on pairing, revert back to solid znp version ( https://github.com/zigpy/zigpy-znp/issues/231 ) --- Classes/ZigpyTransport/AppBellows.py | 21 ++++-- Classes/ZigpyTransport/AppDeconz.py | 14 +++- Classes/ZigpyTransport/AppGeneric.py | 107 +++++++++++++++------------ Classes/ZigpyTransport/AppZnp.py | 16 +++- plugin.py | 6 +- requirements.txt | 6 +- 6 files changed, 104 insertions(+), 66 deletions(-) diff --git a/Classes/ZigpyTransport/AppBellows.py b/Classes/ZigpyTransport/AppBellows.py index c0d6c5b7f..e889497c4 100644 --- a/Classes/ZigpyTransport/AppBellows.py +++ b/Classes/ZigpyTransport/AppBellows.py @@ -7,8 +7,7 @@ import logging import bellows.config as bellows_conf -import bellows.types as bt -import zigpy.types as t +import bellows.types as t import bellows.zigbee.application import zigpy.config as zigpy_conf import zigpy.device @@ -137,7 +136,7 @@ async def register_endpoints(self, endpoint=1): def get_device(self, ieee=None, nwk=None): return Classes.ZigpyTransport.AppGeneric.get_device(self, ieee, nwk) - def handle_join(self, nwk: bt.EmberNodeId, ieee: bt.EmberEUI64, parent_nwk: bt.EmberNodeId, *, handle_rejoin: bool = True,) -> None: + def handle_join(self, nwk: t.EmberNodeId, ieee: t.EmberEUI64, parent_nwk: t.EmberNodeId, *, handle_rejoin: bool = True,) -> None: return Classes.ZigpyTransport.AppGeneric.handle_join(self, nwk, ieee, parent_nwk) def get_device_ieee(self, nwk): @@ -149,9 +148,19 @@ def handle_leave(self, nwk, ieee): def get_zigpy_version(self): return Classes.ZigpyTransport.AppGeneric.get_zigpy_version(self) - def packet_received(self, packet: t.ZigbeePacket) -> None: - return Classes.ZigpyTransport.AppGeneric.packet_received(self,packet) - + def handle_message( + self, + sender: zigpy.device.Device, + profile: int, + cluster: int, + src_ep: int, + dst_ep: int, + message: bytes, + * , + dst_addressing=None, + )->None: + return Classes.ZigpyTransport.AppGeneric.handle_message(self,sender,profile,cluster,src_ep,dst_ep,message, dst_addressing=dst_addressing) + async def set_zigpy_tx_power(self, power): # EmberConfigTxPowerMode - EZSP_CONFIG_TX_POWER_MODE in EzspConfigId # 0x00: Normal mode diff --git a/Classes/ZigpyTransport/AppDeconz.py b/Classes/ZigpyTransport/AppDeconz.py index e7ace4f73..4b109fd8d 100644 --- a/Classes/ZigpyTransport/AppDeconz.py +++ b/Classes/ZigpyTransport/AppDeconz.py @@ -142,8 +142,18 @@ def get_device_ieee(self, nwk): def handle_leave(self, nwk, ieee): Classes.ZigpyTransport.AppGeneric.handle_leave(self, nwk, ieee) - def packet_received(self, packet: t.ZigbeePacket) -> None: - return Classes.ZigpyTransport.AppGeneric.packet_received(self,packet) + def handle_message( + self, + sender: zigpy.device.Device, + profile: int, + cluster: int, + src_ep: int, + dst_ep: int, + message: bytes, + * , + dst_addressing=None, + ) -> None: + return Classes.ZigpyTransport.AppGeneric.handle_message(self,sender,profile,cluster,src_ep,dst_ep,message,dst_addressing=dst_addressing) async def set_zigpy_tx_power(self, power): pass diff --git a/Classes/ZigpyTransport/AppGeneric.py b/Classes/ZigpyTransport/AppGeneric.py index fe3d7285d..855f2daa2 100644 --- a/Classes/ZigpyTransport/AppGeneric.py +++ b/Classes/ZigpyTransport/AppGeneric.py @@ -34,7 +34,7 @@ async def initialize(self, *, auto_form: bool = False, force_form: bool = False) Starts the network on a connected radio, optionally forming one with random settings if necessary. """ - self.log.logging("TransportZigpy", "Log", "AppGeneric:initialize auto_form: %s force_form: %s Class: %s" %( auto_form, force_form, type(self))) + self.log.logging("TransportZigpy", "Debug", "AppGeneric:initialize auto_form: %s force_form: %s Class: %s" %( auto_form, force_form, type(self))) _retreived_backup = None if "autoRestore" in self.pluginconf.pluginConf and self.pluginconf.pluginConf["autoRestore"]: @@ -45,7 +45,7 @@ async def initialize(self, *, auto_form: bool = False, force_form: bool = False) if _retreived_backup: if self.pluginconf.pluginConf[ "OverWriteCoordinatorIEEEOnlyOnce"]: - LOGGER.debug("Allow eui64 overwrite only once !!!") + LOGGER.debug("Allow eui64 overwrite only once !!!") _retreived_backup.network_info.stack_specific.setdefault("ezsp", {})[ "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it"] = True LOGGER.debug("Last backup retreived: %s" % _retreived_backup ) @@ -161,7 +161,7 @@ def get_device_ieee(self, nwk): return None def handle_leave(self, nwk, ieee): - self.log.logging("TransportZigpy", "Log","handle_leave (0x%04x %s)" %(nwk, ieee)) + self.log.logging("TransportZigpy", "Debug","handle_leave (0x%04x %s)" %(nwk, ieee)) plugin_frame = build_plugin_8048_frame_content(self, ieee) self.callBackFunction(plugin_frame) super(type(self),self).handle_leave(nwk, ieee) @@ -171,75 +171,86 @@ def get_zigpy_version(self): LOGGER.debug("get_zigpy_version ake version number. !!") return self.version +def handle_message( + self, + sender: zigpy.device.Device, + profile: int, + cluster: int, + src_ep: int, + dst_ep: int, + message: bytes, + dst_addressing=None, +) -> None: -def packet_received(self, packet: t.ZigbeePacket) -> None: - """Notify zigpy of a received Zigbee packet.""" - try: - sender = self.get_device_with_address(packet.src) - self.log.logging("TransportZigpy", "Debug", "packet_received identified device - %s (%s)" %(str(sender), type(sender)) ) - - except KeyError: - self.log.logging("TransportZigpy", "Debug", "packet_received Unknown device %r" %packet.src) - return - - self.log.logging("TransportZigpy", "Debug", "packet_received identified device %r -> %r with source route %r" %( - packet.src.address, packet.dst.address, packet.source_route )) - profile, cluster, src_ep, dst_ep = packet.profile_id, packet.cluster_id, packet.src_ep, packet.dst_ep - message = packet.data.serialize() - hex_message = binascii.hexlify(message).decode("utf-8") - dst_addressing = packet.dst.addr_mode if packet.dst else None + write_capture_rx_frames( self, sender, profile, cluster, src_ep, dst_ep, message, binascii.hexlify(message).decode("utf-8"), dst_addressing) - write_capture_rx_frames( self, sender, profile, cluster, src_ep, dst_ep, message, hex_message, dst_addressing) - - self.log.logging("TransportZigpy", "Debug", "packet_received - %s %s %s %s %s %s %s %s" %( - sender, profile, cluster, src_ep, dst_ep, message, hex_message, dst_addressing)) - if sender.nwk == 0x0000: - # When coming from coordinator we have to send it back to zigpy - self.log.logging("TransportZigpy", "Debug", "packet_received from Controller Sender: %s Profile: %04x Cluster: %04x srcEp: %02x dstEp: %02x message: %s" %( - str(sender.nwk), profile, cluster, src_ep, dst_ep, hex_message)) - super(type(self),self).packet_received(packet) + self.log.logging("TransportZigpy", "Debug", "handle_message from Controller Sender: %s Profile: %04x Cluster: %04x srcEp: %02x dstEp: %02x message: %s" %( + str(sender.nwk), profile, cluster, src_ep, dst_ep, binascii.hexlify(message).decode("utf-8"))) + #self.super().handle_message(sender, profile, cluster, src_ep, dst_ep, message) + super(type(self),self).handle_message(sender, profile, cluster, src_ep, dst_ep, message) if cluster == 0x8036: # This has been handle via on_zdo_mgmt_permitjoin_rsp() - self.log.logging("TransportZigpy", "Debug", "packet_received 0x8036: %s Profile: %04x Cluster: %04x srcEp: %02x dstEp: %02x message: %s" %( - str(sender.nwk), profile, cluster, src_ep, dst_ep, hex_message)) - self.callBackFunction( build_plugin_8014_frame_content(self, str(sender), hex_message ) ) - super(type(self),self).packet_received(packet) + self.log.logging("TransportZigpy", "Debug", "handle_message 0x8036: %s Profile: %04x Cluster: %04x srcEp: %02x dstEp: %02x message: %s" %( + str(sender.nwk), profile, cluster, src_ep, dst_ep, binascii.hexlify(message).decode("utf-8"))) + self.callBackFunction( build_plugin_8014_frame_content(self, str(sender), binascii.hexlify(message).decode("utf-8") ) ) + super(type(self),self).handle_message(sender, profile, cluster, src_ep, dst_ep, message) return if cluster == 0x8034: # This has been handle via on_zdo_mgmt_leave_rsp() - self.log.logging("TransportZigpy", "Debug", "packet_received 0x8036: %s Profile: %04x Cluster: %04x srcEp: %02x dstEp: %02x message: %s" %( - str(sender.nwk), profile, cluster, src_ep, dst_ep, hex_message)) - self.callBackFunction( build_plugin_8047_frame_content(self, str(sender), hex_message) ) + self.log.logging("TransportZigpy", "Debug", "handle_message 0x8036: %s Profile: %04x Cluster: %04x srcEp: %02x dstEp: %02x message: %s" %( + str(sender.nwk), profile, cluster, src_ep, dst_ep, binascii.hexlify(message).decode("utf-8"))) + self.callBackFunction( build_plugin_8047_frame_content(self, str(sender), binascii.hexlify(message).decode("utf-8")) ) return addr = None if sender.nwk is not None: + addr_mode = 0x02 addr = sender.nwk.serialize()[::-1].hex() + if profile and cluster: + self.log.logging( + "TransportZigpy", + "Debug", + "handle_message device 1: %s Profile: %04x Cluster: %04x sEP: %s dEp: %s message: %s lqi: %s" % ( + str(sender), profile, cluster, src_ep, dst_ep, binascii.hexlify(message).decode("utf-8"), sender.lqi)), + elif sender.ieee is not None: addr = "%016x" % t.uint64_t.deserialize(sender.ieee.serialize())[0] - - addr_mode = 0x02 if sender.nwk is not None else 0x03 - sender.lqi = sender.lqi or 0x00 + addr_mode = 0x03 + if profile and cluster: + self.log.logging( + "TransportZigpy", + "Debug", + "handle_message device 1: %s Profile: %04x Cluster: %04x sEP: %s dEp: %s message: %s lqi: %s" % ( + str(sender), profile, cluster, src_ep, dst_ep, binascii.hexlify(message).decode("utf-8"), sender.lqi)), + + if sender.lqi is None: + sender.lqi = 0x00 - # Let's force profile to ZDP if eps == 0x00 if src_ep == dst_ep == 0x00: profile = 0x0000 - if profile is not None and cluster is not None: - self.log.logging( "TransportZigpy", "Debug", "packet_received device: %s Profile: %04x Cluster: %04x sEP: %s dEp: %s message: %s lqi: %s" % ( - str(addr), profile, cluster, src_ep, dst_ep, hex_message, sender.lqi), ) + if profile and cluster: + self.log.logging( + "TransportZigpy", + "Debug", + "handle_message device 2: %s Profile: %04x Cluster: %04x sEP: %s dEp: %s message: %s lqi: %s" % ( + str(addr), profile, cluster, src_ep, dst_ep, binascii.hexlify(message).decode("utf-8"), sender.lqi), + ) - if addr is not None: + if addr: plugin_frame = build_plugin_8002_frame_content(self, addr, profile, cluster, src_ep, dst_ep, message, sender.lqi, src_addrmode=addr_mode) - self.log.logging("TransportZigpy", "Debug", "packet_received Sender: %s frame for plugin: %s" % (addr, plugin_frame)) - return self.callBackFunction(plugin_frame) - - self.log.logging( "TransportZigpy", "Error", "packet_received - Issue with sender is %s %s" % ( - sender.nwk, sender.ieee), ) + self.log.logging("TransportZigpy", "Debug", "handle_message Sender: %s frame for plugin: %s" % (addr, plugin_frame)) + self.callBackFunction(plugin_frame) + else: + self.log.logging( + "TransportZigpy", + "Error", + "handle_message - Issue with sender is %s %s" % (sender.nwk, sender.ieee), + ) return diff --git a/Classes/ZigpyTransport/AppZnp.py b/Classes/ZigpyTransport/AppZnp.py index b6de747e8..609437e55 100644 --- a/Classes/ZigpyTransport/AppZnp.py +++ b/Classes/ZigpyTransport/AppZnp.py @@ -13,8 +13,7 @@ import zigpy.zdo.types as zdo_types import zigpy_znp.commands.util import zigpy_znp.config as znp_conf -#import zigpy_znp.types as znp_t -import zigpy.types as t +import zigpy_znp.types as t import zigpy_znp.zigbee.application from Classes.ZigpyTransport.firmwareversionHelper import \ znp_extract_versioning_for_plugin @@ -117,8 +116,17 @@ def handle_leave(self, nwk, ieee): def get_zigpy_version(self): return Classes.ZigpyTransport.AppGeneric.get_zigpy_version(self) - def packet_received(self, packet: t.ZigbeePacket) -> None: - return Classes.ZigpyTransport.AppGeneric.packet_received(self,packet) + def handle_message( + self, + sender: zigpy.device.Device, + profile: int, + cluster: int, + src_ep: int, + dst_ep: int, + message: bytes, + dst_addressing=None, + ) -> None: + return Classes.ZigpyTransport.AppGeneric.handle_message(self,sender,profile,cluster,src_ep,dst_ep,message, dst_addressing=dst_addressing) async def set_zigpy_tx_power(self, power): self.log.logging("TransportZigpy", "Debug", "set_tx_power %s" %power) diff --git a/plugin.py b/plugin.py index 58c09d6b1..42920406a 100644 --- a/plugin.py +++ b/plugin.py @@ -1502,10 +1502,10 @@ def update_DB_device_status_to_reinit( self ): def check_python_modules_version( self ): MODULES_VERSION = { - "zigpy": "0.59.0", - "zigpy_znp": "0.11.6", + "zigpy": "0.56.1", + "zigpy_znp": "0.11.2", "zigpy_deconz": "0.21.1", - "bellows": "0.36.8", + "bellows": "0.35.8", } flag = True diff --git a/requirements.txt b/requirements.txt index 8c3ab80dc..468fdd798 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ -zigpy==0.59.0 +zigpy==0.56.1 zigpy_deconz==0.21.1 zigpy-cli==1.0.4 -zigpy_znp==0.11.6 -bellows==0.36.8 +zigpy_znp==0.11.2 +bellows==0.35.8 dnspython==2.3.0 pyserial>=3.5 z4d-certified-devices From 9e45b4b10a5befac1e3d5739485ca2f7a07b2f6d Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Sun, 5 Nov 2023 19:03:06 +0100 Subject: [PATCH 25/47] Fixing issue with PowerFactor wich overlap with Power --- Modules/domoMaj.py | 14 ++++++++------ Modules/domoTools.py | 2 +- Modules/tuyaTS0601.py | 5 ++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Modules/domoMaj.py b/Modules/domoMaj.py index 72079f932..db6fd86b8 100644 --- a/Modules/domoMaj.py +++ b/Modules/domoMaj.py @@ -271,6 +271,14 @@ def MajDomoDevice(self, Devices, NWKID, Ep, clusterID, value, Attribute_="", Col self.log.logging("Widget", "Debug", "------> Ampere3 : %s from Attribute: %s" % (sValue, Attribute_), NWKID) UpdateDevice_v2(self, Devices, DeviceUnit, 0, str(sValue), BatteryLevel, SignalLevel) + if "PWFactor" == ClusterType and WidgetType == "PowerFactor": + self.log.logging("Widget", "Debug", "PowerFactor %s WidgetType: %s Value: %s (%s)" % ( + NWKID, WidgetType, value, type(value)), NWKID) + + nValue = round(value, 1) + sValue = str(nValue) + UpdateDevice_v2(self, Devices, DeviceUnit, nValue, sValue, BatteryLevel, SignalLevel) + if "Power" in ClusterType: # Instant Power/Watts # Power and Meter usage are triggered only with the Instant Power usage. # it is assumed that if there is also summation provided by the device, that @@ -378,7 +386,6 @@ def MajDomoDevice(self, Devices, NWKID, Ep, clusterID, value, Attribute_="", Col self.log.logging("ZLinky", "Debug", "------> P1Meter_ZL (%s): %s" % (Ep, sValue), NWKID) UpdateDevice_v2(self, Devices, DeviceUnit, 0, str(sValue), BatteryLevel, SignalLevel) - if "Meter" in ClusterType: # Meter Usage. if WidgetType == "GazMeter" and Attribute_ == "0000": @@ -586,11 +593,6 @@ def MajDomoDevice(self, Devices, NWKID, Ep, clusterID, value, Attribute_="", Col sValue = str(nValue) UpdateDevice_v2(self, Devices, DeviceUnit, nValue, sValue, BatteryLevel, SignalLevel) - if "PowerFactor" in ClusterType and WidgetType == "PowerFactor": - nValue = round(value, 1) - sValue = str(nValue) - UpdateDevice_v2(self, Devices, DeviceUnit, nValue, sValue, BatteryLevel, SignalLevel) - if "ThermoMode" in ClusterType: # Thermostat Mode self.log.logging("Widget", "Debug", "ThermoMode %s WidgetType: %s Value: %s (%s) Attribute_: %s" % ( NWKID, WidgetType, value, type(value), Attribute_), NWKID) diff --git a/Modules/domoTools.py b/Modules/domoTools.py index 25cfec594..2ca35d816 100644 --- a/Modules/domoTools.py +++ b/Modules/domoTools.py @@ -569,7 +569,7 @@ def GetType(self, Addr, Ep): "Distance": "Distance", "TamperSwitch": "TamperSwitch", "Notification": "Notification", - "PowerFactor": "PowerFactor" + "PWFactor": "PWFactor" } def TypeFromCluster(self, cluster, create_=False, ProfileID_="", ZDeviceID_="", ModelName=""): diff --git a/Modules/tuyaTS0601.py b/Modules/tuyaTS0601.py index 76b4a06d2..d083c844b 100644 --- a/Modules/tuyaTS0601.py +++ b/Modules/tuyaTS0601.py @@ -306,8 +306,8 @@ def ts0601_current(self, Devices, nwkid, ep, value): store_tuya_attribute(self, nwkid, "Current_%s" %ep, value) def ts0601_power_factor(self, Devices, nwkid, ep, value): - self.log.logging( "Tuya0601", "Debug", "ts0601_current - Current %s %s %s" % (nwkid, ep, value), nwkid, ) - MajDomoDevice(self, Devices, nwkid, ep, "PowerFactor", value) + self.log.logging( "Tuya0601", "Debug", "ts0601_power_factor - Power Factor %s %s %s" % (nwkid, ep, value), nwkid, ) + MajDomoDevice(self, Devices, nwkid, ep, "PWFactor", value) store_tuya_attribute(self, nwkid, "PowerFactor_%s" %ep, value) def ts0601_summation_energy(self, Devices, nwkid, ep, value): @@ -332,7 +332,6 @@ def ts0601_production_energy(self, Devices, nwkid, ep, value): checkAndStoreAttributeValue(self, nwkid, ep, "0702", "0001", value) # Store int store_tuya_attribute(self, nwkid, "ProducedEnergy_%s" %ep, value) - def ts0601_instant_power(self, Devices, nwkid, ep, value): self.log.logging( "Tuya0601", "Debug", "ts0601_instant_power - Instant Power %s %s %s" % (nwkid, ep, value), nwkid, ) # Given Zigbee 24-bit integer and tuya store in two's complement form From c325386361fbd1dd028a8f9011f3372c4e2de761 Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Sun, 5 Nov 2023 20:04:29 +0100 Subject: [PATCH 26/47] Adding Adeo/Nodon Fil Pilote switch mode --- Modules/adeo.py | 25 +++++++++++++++++++++++++ Modules/command.py | 46 ++++++++++++++-------------------------------- 2 files changed, 39 insertions(+), 32 deletions(-) create mode 100644 Modules/adeo.py diff --git a/Modules/adeo.py b/Modules/adeo.py new file mode 100644 index 000000000..596036555 --- /dev/null +++ b/Modules/adeo.py @@ -0,0 +1,25 @@ + + +from Modules.sendZigateCommand import raw_APS_request +from Modules.tools import get_and_inc_ZCL_SQN + + +def adeo_fip(self, NwkId, EpOut, fip_level): + + ADEO_FIP_ONOFF_COMMAND = { + "Off": 0, + "Confort": 1, + "Eco": 2, + "Frost Protection": 3, + "Confort -1": 4, + "Confort -2": 5 + } + self.log.logging( "Command", "Log", "adeo_fip : Fil Pilote mode: %s - %s" % ( + fip_level, ADEO_FIP_ONOFF_COMMAND[fip_level]), NwkId, ) + + Cluster = "0006" + cluster_frame = 0b00010001 + sqn = get_and_inc_ZCL_SQN(self, NwkId) + payload = "%02x" % cluster_frame + sqn + "%02x" % ADEO_FIP_ONOFF_COMMAND[fip_level] + + raw_APS_request(self, NwkId, EpOut, Cluster, "0104", payload, zigpyzqn=sqn, zigate_ep="01", groupaddrmode=False, ackIsDisabled=False) \ No newline at end of file diff --git a/Modules/command.py b/Modules/command.py index c3b7cbf14..3ebe9869e 100644 --- a/Modules/command.py +++ b/Modules/command.py @@ -12,6 +12,7 @@ from Modules.actuators import (actuator_off, actuator_on, actuator_setcolor, actuator_setlevel, actuator_stop, actuators) +from Modules.adeo import adeo_fip from Modules.casaia import (casaia_ac201_fan_control, casaia_setpoint, casaia_swing_OnOff, casaia_system_mode) from Modules.cmdsDoorLock import cluster0101_lock_door, cluster0101_unlock_door @@ -902,43 +903,24 @@ def mgtCommand(self, Devices, Unit, Command, Level, Color): if ( Level in FIL_PILOT_MODE and "Model" in self.ListOfDevices[NWKID] ): if self.ListOfDevices[NWKID]["Model"] == "EH-ZB-HACT": - self.log.logging( - "Command", - "Debug", - "mgtCommand : -----> HACT -> Fil Pilote mode: %s - %s" % (Level, FIL_PILOT_MODE[Level]), - NWKID, - ) + self.log.logging( "Command", "Debug","mgtCommand : -----> HACT -> Fil Pilote mode: %s - %s" % ( + Level, FIL_PILOT_MODE[Level]),NWKID, ) self.ListOfDevices[NWKID]["Schneider Wiser"]["HACT FIP Mode"] = FIL_PILOT_MODE[Level] schneider_hact_fip_mode(self, NWKID, FIL_PILOT_MODE[Level]) - UpdateDevice_v2( - self, - Devices, - Unit, - int(Level) // 10, - Level, - BatteryLevel, - SignalLevel, - ForceUpdate_=forceUpdateDev, - ) + UpdateDevice_v2( self, Devices, Unit, int(Level) // 10, Level, BatteryLevel, SignalLevel, ForceUpdate_=forceUpdateDev, ) elif self.ListOfDevices[NWKID]["Model"] == "Cable outlet": - self.log.logging( - "Command", - "Debug", - "mgtCommand : -----> Fil Pilote mode: %s - %s" % (Level, FIL_PILOT_MODE[Level]), - NWKID, - ) + self.log.logging( "Command", "Debug", "mgtCommand : -----> Fil Pilote mode: %s - %s" % ( + Level, FIL_PILOT_MODE[Level]), NWKID, ) legrand_fc40(self, NWKID, FIL_PILOT_MODE[Level]) - UpdateDevice_v2( - self, - Devices, - Unit, - int(Level) // 10, - Level, - BatteryLevel, - SignalLevel, - ForceUpdate_=forceUpdateDev, - ) + UpdateDevice_v2( self, Devices, Unit, int(Level) // 10, Level, BatteryLevel, SignalLevel, ForceUpdate_=forceUpdateDev, ) + + elif self.ListOfDevices[NWKID]["Model"] == "SIN-4-FP-21_EQU": + self.log.logging( "Command", "Log", "mgtCommand : -----> Adeo/Nodon/Enky Fil Pilote mode: %s - %s" % ( + Level, FIL_PILOT_MODE[Level]), NWKID, ) + + adeo_fip(self, NWKID, EPout, FIL_PILOT_MODE[ Level ]) + UpdateDevice_v2( self, Devices, Unit, int(Level) // 10, Level, BatteryLevel, SignalLevel, ForceUpdate_=forceUpdateDev, ) # Let's force a refresh of Attribute in the next Heartbeat request_read_device_status(self, NWKID) From 94665c68de231b760fca548e9a9061303786e9eb Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Wed, 8 Nov 2023 08:34:06 +0100 Subject: [PATCH 27/47] make Fil Pilot using 0xfc00 --- Modules/adeo.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Modules/adeo.py b/Modules/adeo.py index 596036555..b07dc3aef 100644 --- a/Modules/adeo.py +++ b/Modules/adeo.py @@ -6,6 +6,7 @@ def adeo_fip(self, NwkId, EpOut, fip_level): + # https://github.com/Koenkk/zigbee2mqtt/issues/19169#issuecomment-1801181411 ADEO_FIP_ONOFF_COMMAND = { "Off": 0, "Confort": 1, @@ -17,9 +18,11 @@ def adeo_fip(self, NwkId, EpOut, fip_level): self.log.logging( "Command", "Log", "adeo_fip : Fil Pilote mode: %s - %s" % ( fip_level, ADEO_FIP_ONOFF_COMMAND[fip_level]), NwkId, ) - Cluster = "0006" - cluster_frame = 0b00010001 + Cluster = "fc00" + cmd = "00" + fcf = "15" + manufcode = "128b" sqn = get_and_inc_ZCL_SQN(self, NwkId) - payload = "%02x" % cluster_frame + sqn + "%02x" % ADEO_FIP_ONOFF_COMMAND[fip_level] + payload = fcf + manufcode[2:4] + manufcode[:2] + sqn + cmd + "%02x" % ADEO_FIP_ONOFF_COMMAND[fip_level] raw_APS_request(self, NwkId, EpOut, Cluster, "0104", payload, zigpyzqn=sqn, zigate_ep="01", groupaddrmode=False, ackIsDisabled=False) \ No newline at end of file From 9f74267f320da2ed752eb3a945af4075b05685ab Mon Sep 17 00:00:00 2001 From: Pipiche <8291674+pipiche38@users.noreply.github.com> Date: Wed, 8 Nov 2023 17:11:50 +0100 Subject: [PATCH 28/47] Update TS0601-Human-Presence.json --- Conf/Local-Devices/TS0601-Human-Presence.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Conf/Local-Devices/TS0601-Human-Presence.json b/Conf/Local-Devices/TS0601-Human-Presence.json index 13f120a1c..0500c7ce8 100644 --- a/Conf/Local-Devices/TS0601-Human-Presence.json +++ b/Conf/Local-Devices/TS0601-Human-Presence.json @@ -16,7 +16,8 @@ "Identifier": [ [ "TS0601", "_TZE200_bh3n6gk8" ], [ "TS0601", "_TZE200_3towulqd" ], - [ "TS0601", "_TZE200_1ibpyhdc" ] + [ "TS0601", "_TZE200_1ibpyhdc" ], + [ "TS0601", "_TZE204_e5m9c5hl" ], "Type": "", "ClusterToBind": [ ], From 9bae0dc06d95066a7a4f7f5d937a32af030402de Mon Sep 17 00:00:00 2001 From: Pipiche <8291674+pipiche38@users.noreply.github.com> Date: Wed, 8 Nov 2023 17:21:37 +0100 Subject: [PATCH 29/47] Update TS0601-Human-Presence.json --- Conf/Local-Devices/TS0601-Human-Presence.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Conf/Local-Devices/TS0601-Human-Presence.json b/Conf/Local-Devices/TS0601-Human-Presence.json index 0500c7ce8..3ee9f9c39 100644 --- a/Conf/Local-Devices/TS0601-Human-Presence.json +++ b/Conf/Local-Devices/TS0601-Human-Presence.json @@ -17,7 +17,7 @@ [ "TS0601", "_TZE200_bh3n6gk8" ], [ "TS0601", "_TZE200_3towulqd" ], [ "TS0601", "_TZE200_1ibpyhdc" ], - [ "TS0601", "_TZE204_e5m9c5hl" + [ "TS0601", "_TZE204_e5m9c5hl" ] ], "Type": "", "ClusterToBind": [ ], From 8015a680dcdc923da7d12d60ae6e37c80f67ebb0 Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Wed, 8 Nov 2023 18:24:30 +0100 Subject: [PATCH 30/47] updates to get PC311 working --- Modules/tuyaTS0601.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Modules/tuyaTS0601.py b/Modules/tuyaTS0601.py index d083c844b..2be1df9a4 100644 --- a/Modules/tuyaTS0601.py +++ b/Modules/tuyaTS0601.py @@ -15,7 +15,7 @@ from Modules.domoMaj import MajDomoDevice from Modules.domoTools import Update_Battery_Device from Modules.tools import (checkAndStoreAttributeValue, get_and_inc_ZCL_SQN, - getAttributeValue) + get_deviceconf_parameter_value, getAttributeValue) from Modules.tuyaTools import (get_tuya_attribute, store_tuya_attribute, tuya_cmd) @@ -321,7 +321,7 @@ def ts0601_summation_energy(self, Devices, nwkid, ep, value): store_tuya_attribute(self, nwkid, "Energy_%s" %ep, value) def ts0601_summation_energy_raw(self, Devices, nwkid, ep, value): - self.log.logging( "Tuya0601", "Debug", "ts0601_summation_energy - Current Summation %s %s %s" % (nwkid, ep, value), nwkid, ) + self.log.logging( "Tuya0601", "Log", "ts0601_summation_energy - Current Summation %s %s %s" % (nwkid, ep, value), nwkid, ) MajDomoDevice(self, Devices, nwkid, ep, "0702", value, Attribute_="0000") checkAndStoreAttributeValue(self, nwkid, ep, "0702", "0000", value) # Store int store_tuya_attribute(self, nwkid, "ConsumedEnergy_%s" %ep, value) @@ -335,10 +335,19 @@ def ts0601_production_energy(self, Devices, nwkid, ep, value): def ts0601_instant_power(self, Devices, nwkid, ep, value): self.log.logging( "Tuya0601", "Debug", "ts0601_instant_power - Instant Power %s %s %s" % (nwkid, ep, value), nwkid, ) # Given Zigbee 24-bit integer and tuya store in two's complement form + model_name = self.ListOfDevices[ nwkid ]["Model"] if "Model" in self.ListOfDevices[ nwkid ] else None + twocomplement_tst = int( get_deviceconf_parameter_value( self, model_name, "TWO_COMPLEMENT_TST", return_default=None ),16) + twocomplement_val = int( get_deviceconf_parameter_value( self, model_name, "TWO_COMPLEMENT_VAL", return_default=None ),16) + self.log.logging( "Tuya0601", "Debug", "ts0601_instant_power - Instant Power Two's Complement : %s %s" %( twocomplement_tst, twocomplement_val)) + signed_int = int( value ) - if (signed_int & 0x00800000) != 0: # Check the sign bit + if twocomplement_tst: + signed_int = signed_int - twocomplement_val if signed_int & twocomplement_tst else signed_int + elif (signed_int & 0x00800000) != 0: # Check the sign bit signed_int -= 0x01000000 # If negative, adjust to two's complement + self.log.logging( "Tuya0601", "Debug", "ts0601_instant_power - Instant Power Two's Complement : value: %s" %signed_int) + checkAndStoreAttributeValue(self, nwkid, ep, "0702", "0400", signed_int) MajDomoDevice(self, Devices, nwkid, ep, "0702", signed_int) store_tuya_attribute(self, nwkid, "InstantPower_%s" %ep, signed_int) # Store str From c4142ff7dbb40c435c211e5a57b8a989d604b1d8 Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Wed, 8 Nov 2023 18:24:51 +0100 Subject: [PATCH 31/47] Move some Logs to Debug level --- Modules/domoMaj.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/Modules/domoMaj.py b/Modules/domoMaj.py index db6fd86b8..248006fb7 100644 --- a/Modules/domoMaj.py +++ b/Modules/domoMaj.py @@ -139,13 +139,8 @@ def MajDomoDevice(self, Devices, NWKID, Ep, clusterID, value, Attribute_="", Col # Attribute_ : If used This is the Attribute from readCluster. Will help to route to the right action # Color_ : If used This is the color value to be set - self.log.logging( - "Widget", - "Debug", - "------> ClusterType: %s WidgetEp: %s WidgetId: %s WidgetType: %s Attribute_: %s" % ( - ClusterType, WidgetEp, WidgetId, WidgetType, Attribute_), - NWKID, - ) + self.log.logging( "Widget", "Debug", "------> ClusterType: %s WidgetEp: %s WidgetId: %s WidgetType: %s Attribute_: %s" % ( + ClusterType, WidgetEp, WidgetId, WidgetType, Attribute_), NWKID, ) SignalLevel, BatteryLevel = RetreiveSignalLvlBattery(self, NWKID) self.log.logging("Widget", "Debug", "------> SignalLevel: %s , BatteryLevel: %s" % (SignalLevel, BatteryLevel), NWKID) @@ -287,7 +282,7 @@ def MajDomoDevice(self, Devices, NWKID, Ep, clusterID, value, Attribute_="", Col if WidgetType == "Power" and (Attribute_ in ("", "050f") or clusterID == "000c"): # kWh if (( isinstance( value, (int, float)) and value < 0) or (float(value) < 0) ) and is_PowerNegative_widget( ClusterTypeList): - self.log.logging("Widget", "Log", "------>There is a PowerNegative widget and the value is negative. Skiping here", NWKID) + self.log.logging("Widget", "Debug", "------>There is a PowerNegative widget and the value is negative. Skiping here", NWKID) UpdateDevice_v2(self, Devices, DeviceUnit, 0, "0", BatteryLevel, SignalLevel) continue @@ -420,7 +415,7 @@ def MajDomoDevice(self, Devices, NWKID, Ep, clusterID, value, Attribute_="", Col or ( Attribute_ in ("0104", "0106") and Ep == "f2") or ( Attribute_ in ("0108", "010a") and Ep == "f3") ) - ): + ): check_set_meter_widget( Devices, DeviceUnit, 0) instant, _summation = retreive_data_from_current(self, Devices, DeviceUnit, "0;0") summation = round(float(zlinky_sum_all_indexes( self, NWKID )), 2) From 51ac2e589260860513bbfd9f2c03164f74b069db Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Wed, 8 Nov 2023 18:30:07 +0100 Subject: [PATCH 32/47] change the way to handle motion value for Tuya Human Presence Detection --- Conf/Local-Devices/TS0601-Human-Presence.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Conf/Local-Devices/TS0601-Human-Presence.json b/Conf/Local-Devices/TS0601-Human-Presence.json index 3ee9f9c39..db0ec5402 100644 --- a/Conf/Local-Devices/TS0601-Human-Presence.json +++ b/Conf/Local-Devices/TS0601-Human-Presence.json @@ -31,7 +31,7 @@ }, "TS0601_DP": { "68": { "sensor_type": "illuminance"}, - "69": { "sensor_type": "motion", "EvalExp": "int(value == 0)", "DomoDeviceFormat": "str"}, + "69": { "sensor_type": "motion", "EvalExp": "int(value != 0)", "DomoDeviceFormat": "str"}, "6a": { "store_tuya_attribute": "Sensitivity"}, "6b": { "store_tuya_attribute": "max_range"}, "6c": { "store_tuya_attribute": "min_range"}, From 94ba179749b8a63f012b77a9d0c05db2905d419e Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Wed, 8 Nov 2023 19:07:05 +0100 Subject: [PATCH 33/47] Move to Certified devices ( https://github.com/zigbeefordomoticz/z4d-certified-devices/commit/61426a50144793f11c6a78f4504e14566b2e73ac ) --- Conf/Local-Devices/TS0601-Human-Presence.json | 44 ------------------- 1 file changed, 44 deletions(-) delete mode 100644 Conf/Local-Devices/TS0601-Human-Presence.json diff --git a/Conf/Local-Devices/TS0601-Human-Presence.json b/Conf/Local-Devices/TS0601-Human-Presence.json deleted file mode 100644 index db0ec5402..000000000 --- a/Conf/Local-Devices/TS0601-Human-Presence.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "_source": "https://github.com/zigbeefordomoticz/z4d-certified-devices/issues/11", - "_blakadder": "https://zigbee.blakadder.com/Tuya_ZY-M100-24G.html", - "_description": "uya Human Presence Detector 5.8GHz Ceiling Mount ZY-M100-2", - "Ep": { - "01": { - "0000": "", - "0004": "", - "0005": "", - "0500": "", - "000a": "", - "0019": "", - "Type": "Motion/Lux" - } - }, - "Identifier": [ - [ "TS0601", "_TZE200_bh3n6gk8" ], - [ "TS0601", "_TZE200_3towulqd" ], - [ "TS0601", "_TZE200_1ibpyhdc" ], - [ "TS0601", "_TZE204_e5m9c5hl" ] - ], - "Type": "", - "ClusterToBind": [ ], - "ConfigureReporting": {}, - "ReadAttributes": { - "0000": [ "0004", "0000", "0001", "0005", "0007", "fffe" ], - "0001": [], - "0019": [], - "0500": [ "0000", "0001", "0002", "0010", "0011"], - "ef00": [] - }, - "TS0601_DP": { - "68": { "sensor_type": "illuminance"}, - "69": { "sensor_type": "motion", "EvalExp": "int(value != 0)", "DomoDeviceFormat": "str"}, - "6a": { "store_tuya_attribute": "Sensitivity"}, - "6b": { "store_tuya_attribute": "max_range"}, - "6c": { "store_tuya_attribute": "min_range"}, - "6d": { "store_tuya_attribute": "target_distace"}, - "6e": { "store_tuya_attribute": "fading_time"}, - "6f": { "store_tuya_attribute": "detection_delay"} - }, - "Param": { - } -} From 34510f4c1b73e78c5cfafd7763a772c9e97e6c64 Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Wed, 8 Nov 2023 19:25:59 +0100 Subject: [PATCH 34/47] Update Nodon FP command --- Modules/adeo.py | 19 ++++++------------- Modules/command.py | 14 +++++++++++--- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/Modules/adeo.py b/Modules/adeo.py index b07dc3aef..4ab500402 100644 --- a/Modules/adeo.py +++ b/Modules/adeo.py @@ -5,24 +5,17 @@ def adeo_fip(self, NwkId, EpOut, fip_level): - - # https://github.com/Koenkk/zigbee2mqtt/issues/19169#issuecomment-1801181411 - ADEO_FIP_ONOFF_COMMAND = { - "Off": 0, - "Confort": 1, - "Eco": 2, - "Frost Protection": 3, - "Confort -1": 4, - "Confort -2": 5 - } - self.log.logging( "Command", "Log", "adeo_fip : Fil Pilote mode: %s - %s" % ( - fip_level, ADEO_FIP_ONOFF_COMMAND[fip_level]), NwkId, ) + # Thanks to Nodon support + + if fip_level not in ( 0x00, 0x01, 0x02, 0x03, 0x04, 0x05 ): + self.log.logging( "Command", "Log", "adeo_fip : Fil Pilote mode error %s" %fip_level) + self.log.logging( "Command", "Log", "adeo_fip : Fil Pilote mode: %s " % ( fip_level, ), NwkId, ) Cluster = "fc00" cmd = "00" fcf = "15" manufcode = "128b" sqn = get_and_inc_ZCL_SQN(self, NwkId) - payload = fcf + manufcode[2:4] + manufcode[:2] + sqn + cmd + "%02x" % ADEO_FIP_ONOFF_COMMAND[fip_level] + payload = fcf + manufcode[2:4] + manufcode[:2] + sqn + cmd + "%02x" % fip_level raw_APS_request(self, NwkId, EpOut, Cluster, "0104", payload, zigpyzqn=sqn, zigate_ep="01", groupaddrmode=False, ackIsDisabled=False) \ No newline at end of file diff --git a/Modules/command.py b/Modules/command.py index 3ebe9869e..cc4a7a5ed 100644 --- a/Modules/command.py +++ b/Modules/command.py @@ -915,11 +915,19 @@ def mgtCommand(self, Devices, Unit, Command, Level, Color): legrand_fc40(self, NWKID, FIL_PILOT_MODE[Level]) UpdateDevice_v2( self, Devices, Unit, int(Level) // 10, Level, BatteryLevel, SignalLevel, ForceUpdate_=forceUpdateDev, ) - elif self.ListOfDevices[NWKID]["Model"] == "SIN-4-FP-21_EQU": + elif self.ListOfDevices[NWKID]["Model"] in ( "SIN-4-FP-21_EQU", "SIN-4-FP-21"): + ADEO_FIP_ONOFF_COMMAND = { + 60: 0, + 10: 1, + 40: 2, + 50: 3, + 20: 4, + 30: 5 + } self.log.logging( "Command", "Log", "mgtCommand : -----> Adeo/Nodon/Enky Fil Pilote mode: %s - %s" % ( - Level, FIL_PILOT_MODE[Level]), NWKID, ) + Level, ADEO_FIP_ONOFF_COMMAND[Level]), NWKID, ) - adeo_fip(self, NWKID, EPout, FIL_PILOT_MODE[ Level ]) + adeo_fip(self, NWKID, EPout, ADEO_FIP_ONOFF_COMMAND[ Level ]) UpdateDevice_v2( self, Devices, Unit, int(Level) // 10, Level, BatteryLevel, SignalLevel, ForceUpdate_=forceUpdateDev, ) # Let's force a refresh of Attribute in the next Heartbeat From 9381523e4452b10a1830d898e0798f2fcb1f25fa Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Wed, 8 Nov 2023 20:42:34 +0100 Subject: [PATCH 35/47] cosmetic --- Modules/command.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/command.py b/Modules/command.py index cc4a7a5ed..f2cfbd0f9 100644 --- a/Modules/command.py +++ b/Modules/command.py @@ -917,12 +917,12 @@ def mgtCommand(self, Devices, Unit, Command, Level, Color): elif self.ListOfDevices[NWKID]["Model"] in ( "SIN-4-FP-21_EQU", "SIN-4-FP-21"): ADEO_FIP_ONOFF_COMMAND = { - 60: 0, 10: 1, + 20: 4, + 30: 5, 40: 2, 50: 3, - 20: 4, - 30: 5 + 60: 0, } self.log.logging( "Command", "Log", "mgtCommand : -----> Adeo/Nodon/Enky Fil Pilote mode: %s - %s" % ( Level, ADEO_FIP_ONOFF_COMMAND[Level]), NWKID, ) From d7f5510da53810b3d4c53493124dfd37f5f6f314 Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Sat, 11 Nov 2023 11:21:55 +0100 Subject: [PATCH 36/47] check that the device entry exist in Domoticz Devices, before reading it --- Modules/domoMaj.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Modules/domoMaj.py b/Modules/domoMaj.py index 248006fb7..0a5e20399 100644 --- a/Modules/domoMaj.py +++ b/Modules/domoMaj.py @@ -124,7 +124,9 @@ def MajDomoDevice(self, Devices, NWKID, Ep, clusterID, value, Attribute_="", Col else: self.log.logging( "Widget", "Error", "WidgetID %s not found, unable to remove the entry from device" % WidgetId, NWKID) continue - + elif DeviceUnit not in Devices: + continue + Switchtype = Devices[DeviceUnit].SwitchType Subtype = Devices[DeviceUnit].SubType From 95cc5bcf9d9e899b18de7568e0121babf1626953 Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Sat, 11 Nov 2023 21:58:19 +0100 Subject: [PATCH 37/47] Manage Tuya Human Sensor and Lux --- Modules/switchSelectorWidgets.py | 14 +++++++++++++- Modules/tuyaTS0601.py | 6 ++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/Modules/switchSelectorWidgets.py b/Modules/switchSelectorWidgets.py index 86f02c6ea..ec72819af 100644 --- a/Modules/switchSelectorWidgets.py +++ b/Modules/switchSelectorWidgets.py @@ -269,7 +269,6 @@ "Language": { "fr-FR": {"LevelNames": "Off | Simple click|Double click|Triple click|Quadruple+ click"}}, }, - "SwitchAQ3WithOff": { "0": (0, "00"), "1": (1, "10"), @@ -806,4 +805,17 @@ "fr-FR": { "Off|Movie|At Home|Sleep|Go Out"} }, }, + "TuyaRadarSensor": { + 0: (0, "Off"), + 0x1: (1, "10"), + 0x2: (2, "20"), + "ForceUpdate": True, + "OffHidden": False, + "SelectorStyle": 1, + "LevelNames": "Off|Presence|Moving", + "Language": { + "fr-FR": { "Off|Presence|Deplacement"} + }, + }, + } \ No newline at end of file diff --git a/Modules/tuyaTS0601.py b/Modules/tuyaTS0601.py index 2be1df9a4..4619d3d68 100644 --- a/Modules/tuyaTS0601.py +++ b/Modules/tuyaTS0601.py @@ -198,6 +198,11 @@ def ts0601_motion(self, Devices, nwkid, ep, value): MajDomoDevice(self, Devices, nwkid, ep, "0406", value ) checkAndStoreAttributeValue(self, nwkid, "01", "0406", "0000", value) +def ts0601_tuya_presence_state(self, Devices, nwkid, ep, value): + # Presence State ( None, Present, Moving ) + self.log.logging("Tuya0601", "Debug", "ts0601_tuya_presence_state - state %s %s %s" % (nwkid, ep, value), nwkid) + store_tuya_attribute(self, nwkid, "presence_state", value) + MajDomoDevice(self, Devices, nwkid, ep, "0006", value ) def ts0601_illuminance(self, Devices, nwkid, ep, value): # Illuminance @@ -511,6 +516,7 @@ def ts0601_sensor_irrigation_mode(self, Devices, nwkid, ep, value): "smoke_ppm": ts0601_smoke_concentration, "water_consumption": ts0601_water_consumption, "power_factor": ts0601_power_factor, + "presence_state": ts0601_tuya_presence_state } def ts0601_tuya_cmd(self, NwkId, Ep, action, data): From 9d80a3bf5960867d700fa75430864fce79b503a0 Mon Sep 17 00:00:00 2001 From: Pipiche <8291674+pipiche38@users.noreply.github.com> Date: Sun, 12 Nov 2023 10:52:07 +0100 Subject: [PATCH 38/47] Zigpy packet receive (#1665) * upgrade to packet_receive, but not yet done * upgrade zigpy layers to the latest radio libs --- Classes/ZigpyTransport/AppBellows.py | 8 +- Classes/ZigpyTransport/AppDeconz.py | 3 + Classes/ZigpyTransport/AppGeneric.py | 153 +++++++++++++-------------- Classes/ZigpyTransport/AppZnp.py | 10 +- Modules/input.py | 93 ++++++++-------- Modules/pairingProcess.py | 18 ++-- Modules/pluginModels.py | 6 +- Modules/readClusters.py | 2 +- plugin.py | 6 +- requirements.txt | 6 +- 10 files changed, 154 insertions(+), 151 deletions(-) diff --git a/Classes/ZigpyTransport/AppBellows.py b/Classes/ZigpyTransport/AppBellows.py index e889497c4..70fc55b93 100644 --- a/Classes/ZigpyTransport/AppBellows.py +++ b/Classes/ZigpyTransport/AppBellows.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +#!/usr/bin/env python3btt. # coding: utf-8 -*- # # Author: deufo, badz & pipiche38 @@ -8,6 +8,7 @@ import bellows.config as bellows_conf import bellows.types as t +import zigpy.types as zigpy_t import bellows.zigbee.application import zigpy.config as zigpy_conf import zigpy.device @@ -148,6 +149,9 @@ def handle_leave(self, nwk, ieee): def get_zigpy_version(self): return Classes.ZigpyTransport.AppGeneric.get_zigpy_version(self) + def packet_received(self, packet: zigpy_t.ZigbeePacket) -> None: + return Classes.ZigpyTransport.AppGeneric.packet_received(self,packet) + def handle_message( self, sender: zigpy.device.Device, @@ -160,7 +164,7 @@ def handle_message( dst_addressing=None, )->None: return Classes.ZigpyTransport.AppGeneric.handle_message(self,sender,profile,cluster,src_ep,dst_ep,message, dst_addressing=dst_addressing) - + async def set_zigpy_tx_power(self, power): # EmberConfigTxPowerMode - EZSP_CONFIG_TX_POWER_MODE in EzspConfigId # 0x00: Normal mode diff --git a/Classes/ZigpyTransport/AppDeconz.py b/Classes/ZigpyTransport/AppDeconz.py index 4b109fd8d..1f4c6a39e 100644 --- a/Classes/ZigpyTransport/AppDeconz.py +++ b/Classes/ZigpyTransport/AppDeconz.py @@ -142,6 +142,9 @@ def get_device_ieee(self, nwk): def handle_leave(self, nwk, ieee): Classes.ZigpyTransport.AppGeneric.handle_leave(self, nwk, ieee) + def packet_received(self, packet: t.ZigbeePacket) -> None: + return Classes.ZigpyTransport.AppGeneric.packet_received(self,packet) + def handle_message( self, sender: zigpy.device.Device, diff --git a/Classes/ZigpyTransport/AppGeneric.py b/Classes/ZigpyTransport/AppGeneric.py index 855f2daa2..12cee00f2 100644 --- a/Classes/ZigpyTransport/AppGeneric.py +++ b/Classes/ZigpyTransport/AppGeneric.py @@ -26,6 +26,7 @@ LOGGER = logging.getLogger(__name__) + async def _load_db(self) -> None: pass @@ -34,7 +35,7 @@ async def initialize(self, *, auto_form: bool = False, force_form: bool = False) Starts the network on a connected radio, optionally forming one with random settings if necessary. """ - self.log.logging("TransportZigpy", "Debug", "AppGeneric:initialize auto_form: %s force_form: %s Class: %s" %( auto_form, force_form, type(self))) + self.log.logging("TransportZigpy", "Log", "AppGeneric:initialize auto_form: %s force_form: %s Class: %s" %( auto_form, force_form, type(self))) _retreived_backup = None if "autoRestore" in self.pluginconf.pluginConf and self.pluginconf.pluginConf["autoRestore"]: @@ -45,7 +46,7 @@ async def initialize(self, *, auto_form: bool = False, force_form: bool = False) if _retreived_backup: if self.pluginconf.pluginConf[ "OverWriteCoordinatorIEEEOnlyOnce"]: - LOGGER.debug("Allow eui64 overwrite only once !!!") + LOGGER.debug("Allow eui64 overwrite only once !!!") _retreived_backup.network_info.stack_specific.setdefault("ezsp", {})[ "i_understand_i_can_update_eui64_only_once_and_i_still_want_to_do_it"] = True LOGGER.debug("Last backup retreived: %s" % _retreived_backup ) @@ -122,30 +123,30 @@ def handle_join(self, nwk: t.NWK, ieee: t.EUI64, parent_nwk: t.NWK) -> None: """ Called when a device joins or announces itself on the network. """ + self.log.logging("TransportZigpy", "Debug","handle_join (0x%04x %s)" %(nwk, ieee)) + if str(ieee) in {"00:00:00:00:00:00:00:00", "ff:ff:ff:ff:ff:ff:ff:ff"}: # invalid ieee, drop - LOGGER.debug("ignoring invalid neighbor: %s", ieee) + self.log.logging("TransportZigpy", "Log", "ignoring invalid neighbor: %s", ieee) return ieee = t.EUI64(ieee) try: dev = self.get_device(ieee) - time.sleep(1.0) - LOGGER.info("Device 0x%04x (%s) joined the network", nwk, ieee) + #time.sleep(2.0) + self.log.logging("TransportZigpy", "Debug", "Device 0x%04x (%s) joined the network" %(nwk, ieee)) except KeyError: - time.sleep(1.0) dev = self.add_device(ieee, nwk) - LOGGER.debug("New device 0x%04x (%s) joined the network", nwk, ieee) + #time.sleep(2.0) + self.log.logging("TransportZigpy", "Debug", "New device 0x%04x (%s) joined the network" %(nwk, ieee)) if dev.nwk != nwk: dev.nwk = nwk _update_nkdids_if_needed(self, ieee, dev.nwk ) - LOGGER.debug("Device %s changed id (0x%04x => 0x%04x)", ieee, dev.nwk, nwk) + self.log.logging("TransportZigpy", "Debug", "Device %s changed id (0x%04x => 0x%04x)" %(ieee, dev.nwk, nwk)) + + super(type(self),self).handle_join(nwk, ieee, parent_nwk) -def _update_nkdids_if_needed( self, ieee, new_nwkid ): - _ieee = "%016x" % t.uint64_t.deserialize(ieee.serialize())[0] - _nwk = new_nwkid.serialize()[::-1].hex() - self.callBackUpdDevice(_ieee, _nwk) def get_device_ieee(self, nwk): # Call from the plugin to retreive the ieee @@ -153,9 +154,11 @@ def get_device_ieee(self, nwk): try: dev = super(type(self),self).get_device( nwk=int(nwk,16)) LOGGER.debug("AppZnp get_device nwk: %s returned %s" %( nwk, dev)) + except KeyError: LOGGER.debug("AppZnp get_device raise KeyError nwk: %s !!" %( nwk)) return None + if dev.ieee: return "%016x" % t.uint64_t.deserialize(dev.ieee.serialize())[0] return None @@ -166,94 +169,84 @@ def handle_leave(self, nwk, ieee): self.callBackFunction(plugin_frame) super(type(self),self).handle_leave(nwk, ieee) -def get_zigpy_version(self): - # This is a fake version number. This is just to inform the plugin that we are using ZNP over Zigpy - LOGGER.debug("get_zigpy_version ake version number. !!") - return self.version - -def handle_message( - self, - sender: zigpy.device.Device, - profile: int, - cluster: int, - src_ep: int, - dst_ep: int, - message: bytes, - dst_addressing=None, -) -> None: - +def packet_received( + self, + packet: t.ZigbeePacket + ) -> None: + + """Notify zigpy of a received Zigbee packet.""" + self.log.logging("TransportZigpy", "Debug", "packet_received %s" %(packet)) + + sender = packet.src.address.serialize()[::-1].hex() + addr_mode = int(packet.src.addr_mode) if packet.src.addr_mode is not None else None + profile = int(packet.profile_id) if packet.profile_id is not None else None + cluster = int(packet.cluster_id) if packet.cluster_id is not None else None + src_ep = int(packet.src_ep) if packet.src_ep is not None else None + dst_ep = int(packet.dst_ep) if packet.dst_ep is not None else None + + # self.log.logging("TransportZigpy", "Log", " Src : %s (%s)" %(sender,type(sender))) + # self.log.logging("TransportZigpy", "Log", " AddrMod : %02X" %(addr_mode)) + # self.log.logging("TransportZigpy", "Log", " src Ep : %02X" %(dst_ep)) + # self.log.logging("TransportZigpy", "Log", " dst Ep : %02x" %(dst_ep)) + # self.log.logging("TransportZigpy", "Log", " Profile : %04X" %(profile)) + # self.log.logging("TransportZigpy", "Log", " Cluster : %04X" %(cluster)) + + message = packet.data.serialize() + hex_message = binascii.hexlify(message).decode("utf-8") + dst_addressing = packet.dst.addr_mode if packet.dst else None + + self.log.logging("TransportZigpy", "Debug", "packet_received - %s %s %s %s %s %s %s %s" %( + packet.src, profile, cluster, src_ep, dst_ep, message, hex_message, dst_addressing)) - write_capture_rx_frames( self, sender, profile, cluster, src_ep, dst_ep, message, binascii.hexlify(message).decode("utf-8"), dst_addressing) + hex_message = binascii.hexlify(message).decode("utf-8") + write_capture_rx_frames( self, packet.src, profile, cluster, src_ep, dst_ep, message, hex_message, dst_addressing) - if sender.nwk == 0x0000: + if sender is None or profile is None or cluster is None: + super(type(self),self).packet_received(packet) + + if sender == 0x0000 or ( zigpy.zdo.ZDO_ENDPOINT in (packet.src_ep, packet.dst_ep)): self.log.logging("TransportZigpy", "Debug", "handle_message from Controller Sender: %s Profile: %04x Cluster: %04x srcEp: %02x dstEp: %02x message: %s" %( - str(sender.nwk), profile, cluster, src_ep, dst_ep, binascii.hexlify(message).decode("utf-8"))) - #self.super().handle_message(sender, profile, cluster, src_ep, dst_ep, message) - super(type(self),self).handle_message(sender, profile, cluster, src_ep, dst_ep, message) + sender, profile, cluster, src_ep, dst_ep, hex_message)) + super(type(self),self).packet_received(packet) if cluster == 0x8036: # This has been handle via on_zdo_mgmt_permitjoin_rsp() self.log.logging("TransportZigpy", "Debug", "handle_message 0x8036: %s Profile: %04x Cluster: %04x srcEp: %02x dstEp: %02x message: %s" %( - str(sender.nwk), profile, cluster, src_ep, dst_ep, binascii.hexlify(message).decode("utf-8"))) - self.callBackFunction( build_plugin_8014_frame_content(self, str(sender), binascii.hexlify(message).decode("utf-8") ) ) - super(type(self),self).handle_message(sender, profile, cluster, src_ep, dst_ep, message) + sender, profile, cluster, src_ep, dst_ep, hex_message)) + self.callBackFunction( build_plugin_8014_frame_content(self, sender, hex_message ) ) + super(type(self),self).packet_received(packet) return if cluster == 0x8034: # This has been handle via on_zdo_mgmt_leave_rsp() self.log.logging("TransportZigpy", "Debug", "handle_message 0x8036: %s Profile: %04x Cluster: %04x srcEp: %02x dstEp: %02x message: %s" %( - str(sender.nwk), profile, cluster, src_ep, dst_ep, binascii.hexlify(message).decode("utf-8"))) - self.callBackFunction( build_plugin_8047_frame_content(self, str(sender), binascii.hexlify(message).decode("utf-8")) ) + sender, profile, cluster, src_ep, dst_ep, hex_message)) + self.callBackFunction( build_plugin_8047_frame_content(self, sender, hex_message) ) return - addr = None - if sender.nwk is not None: - addr_mode = 0x02 - addr = sender.nwk.serialize()[::-1].hex() - if profile and cluster: - self.log.logging( - "TransportZigpy", - "Debug", - "handle_message device 1: %s Profile: %04x Cluster: %04x sEP: %s dEp: %s message: %s lqi: %s" % ( - str(sender), profile, cluster, src_ep, dst_ep, binascii.hexlify(message).decode("utf-8"), sender.lqi)), - - elif sender.ieee is not None: - addr = "%016x" % t.uint64_t.deserialize(sender.ieee.serialize())[0] - addr_mode = 0x03 - if profile and cluster: - self.log.logging( - "TransportZigpy", - "Debug", - "handle_message device 1: %s Profile: %04x Cluster: %04x sEP: %s dEp: %s message: %s lqi: %s" % ( - str(sender), profile, cluster, src_ep, dst_ep, binascii.hexlify(message).decode("utf-8"), sender.lqi)), - - if sender.lqi is None: - sender.lqi = 0x00 - - if src_ep == dst_ep == 0x00: - profile = 0x0000 + packet.lqi = 0x00 if packet.lqi is None else packet.lqi + profile = 0x0000 if src_ep == dst_ep == 0x00 else profile if profile and cluster: - self.log.logging( - "TransportZigpy", - "Debug", - "handle_message device 2: %s Profile: %04x Cluster: %04x sEP: %s dEp: %s message: %s lqi: %s" % ( - str(addr), profile, cluster, src_ep, dst_ep, binascii.hexlify(message).decode("utf-8"), sender.lqi), - ) + self.log.logging( "TransportZigpy", "Debug", "handle_message device 2: %s Profile: %04x Cluster: %04x sEP: %s dEp: %s message: %s lqi: %s" %( + sender, profile, cluster, src_ep, dst_ep, hex_message, packet.lqi), ) - if addr: - plugin_frame = build_plugin_8002_frame_content(self, addr, profile, cluster, src_ep, dst_ep, message, sender.lqi, src_addrmode=addr_mode) - self.log.logging("TransportZigpy", "Debug", "handle_message Sender: %s frame for plugin: %s" % (addr, plugin_frame)) - self.callBackFunction(plugin_frame) - else: - self.log.logging( - "TransportZigpy", - "Error", - "handle_message - Issue with sender is %s %s" % (sender.nwk, sender.ieee), - ) + plugin_frame = build_plugin_8002_frame_content(self, sender, profile, cluster, src_ep, dst_ep, message, packet.lqi, src_addrmode=addr_mode) + self.log.logging("TransportZigpy", "Debug", "handle_message Sender: %s frame for plugin: %s" % (sender, plugin_frame)) + self.callBackFunction(plugin_frame) return +def _update_nkdids_if_needed( self, ieee, new_nwkid ): + _ieee = "%016x" % t.uint64_t.deserialize(ieee.serialize())[0] + _nwk = new_nwkid.serialize()[::-1].hex() + self.callBackUpdDevice(_ieee, _nwk) + +def get_zigpy_version(self): + # This is a fake version number. This is just to inform the plugin that we are using ZNP over Zigpy + LOGGER.debug("get_zigpy_version ake version number. !!") + return self.version + async def register_specific_endpoints(self): """ Registers all necessary endpoints. diff --git a/Classes/ZigpyTransport/AppZnp.py b/Classes/ZigpyTransport/AppZnp.py index 609437e55..84d53fa20 100644 --- a/Classes/ZigpyTransport/AppZnp.py +++ b/Classes/ZigpyTransport/AppZnp.py @@ -14,6 +14,7 @@ import zigpy_znp.commands.util import zigpy_znp.config as znp_conf import zigpy_znp.types as t +import zigpy.types as zigpy_t import zigpy_znp.zigbee.application from Classes.ZigpyTransport.firmwareversionHelper import \ znp_extract_versioning_for_plugin @@ -96,9 +97,9 @@ async def register_endpoints(self): self.log.logging("TransportZigpy", "Status", "ZNP Radio register any additional/specific Ep") await Classes.ZigpyTransport.AppGeneric.register_specific_endpoints(self) - def device_initialized(self, device): - self.log.logging("TransportZigpy", "Log","device_initialized (0x%04x %s)" %(device.nwk, device.ieee)) - super().device_initialized(device) + #def device_initialized(self, device): + # self.log.logging("TransportZigpy", "Log","device_initialized (0x%04x %s)" %(device.nwk, device.ieee)) + # super().device_initialized(device) def get_device(self, ieee=None, nwk=None): @@ -116,6 +117,9 @@ def handle_leave(self, nwk, ieee): def get_zigpy_version(self): return Classes.ZigpyTransport.AppGeneric.get_zigpy_version(self) + def packet_received(self, packet: zigpy_t.ZigbeePacket) -> None: + return Classes.ZigpyTransport.AppGeneric.packet_received(self,packet) + def handle_message( self, sender: zigpy.device.Device, diff --git a/Modules/input.py b/Modules/input.py index 0f431a9de..e1939d229 100644 --- a/Modules/input.py +++ b/Modules/input.py @@ -2037,17 +2037,14 @@ def Decode8042(self, Devices, MsgData, MsgLQI): # Node Descriptor response def Decode8043(self, Devices, MsgData, MsgLQI): # Reception Simple descriptor response - # MsgLen = len(MsgData) + """Decode and process 0x8043 message.""" MsgDataSQN = MsgData[:2] MsgDataStatus = MsgData[2:4] MsgDataShAddr = MsgData[4:8] MsgDataLenght = MsgData[8:10] - self.log.logging( - "Input", - "Debug", - "Decode8043 - Received SQN: %s Addr: %s Len: %s Status: %s Data: %s" % (MsgDataSQN, MsgDataShAddr, MsgDataLenght, MsgDataStatus, MsgData), - ) + self.log.logging( "Input", "Debug", "Decode8043 - Received SQN: %s Addr: %s Len: %s Status: %s Data: %s" % ( + MsgDataSQN, MsgDataShAddr, MsgDataLenght, MsgDataStatus, MsgData), ) if MsgDataShAddr not in self.ListOfDevices: self.log.logging( "Input", "Log", "Decode8043 receives a message from a non existing device %s" % MsgDataShAddr, ) return @@ -2055,6 +2052,8 @@ def Decode8043(self, Devices, MsgData, MsgLQI): # Reception Simple descriptor r if "SQN" in self.ListOfDevices[MsgDataShAddr] and MsgDataSQN == self.ListOfDevices[MsgDataShAddr]["SQN"]: return + inDB_status = self.ListOfDevices[MsgDataShAddr].get("Status", None) == "inDB" + if int(MsgDataLenght, 16) == 0: return if MsgDataStatus != '00': @@ -2085,13 +2084,13 @@ def Decode8043(self, Devices, MsgData, MsgLQI): # Reception Simple descriptor r MsgDataProfile, MsgDataDeviceId), ) if MsgDataEp in self.ListOfDevices[MsgDataShAddr]["Ep"]: del self.ListOfDevices[MsgDataShAddr]["Ep"][MsgDataEp] - if "NbEp" in self.ListOfDevices[MsgDataShAddr]: - if self.ListOfDevices[MsgDataShAddr]["NbEp"] > "1": - self.ListOfDevices[MsgDataShAddr]["NbEp"] = int(self.ListOfDevices[MsgDataShAddr]["NbEp"]) - 1 + if "NbEp" in self.ListOfDevices[MsgDataShAddr] and self.ListOfDevices[MsgDataShAddr]["NbEp"] > "1": + self.ListOfDevices[MsgDataShAddr]["NbEp"] = int(self.ListOfDevices[MsgDataShAddr]["NbEp"]) - 1 return - self.log.logging( "Input", "Status", "[%s] NEW OBJECT: %s Simple Descriptor Response EP: 0x%s LQI: %s" % ( - "-", MsgDataShAddr, MsgDataEp, int(MsgLQI, 16)), ) + if not inDB_status: + self.log.logging( "Input", "Status", "[%s] NEW OBJECT: %s Simple Descriptor Response EP: 0x%s LQI: %s" % ( + "-", MsgDataShAddr, MsgDataEp, int(MsgLQI, 16)), ) # Endpoint V2 (ProfileID and ZDeviceID) if "Epv2" not in self.ListOfDevices[MsgDataShAddr]: @@ -2109,32 +2108,36 @@ def Decode8043(self, Devices, MsgData, MsgLQI): # Reception Simple descriptor r if self.ListOfDevices[MsgDataShAddr]["ProfileID"] != MsgDataProfile: pass self.ListOfDevices[MsgDataShAddr]["ProfileID"] = MsgDataProfile - self.log.logging( "Input", "Status", "[%s] NEW OBJECT: %s ProfileID %s" % ( - "-", MsgDataShAddr, MsgDataProfile), ) + if not inDB_status: + self.log.logging( "Input", "Status", "[%s] NEW OBJECT: %s ProfileID %s" % ( + "-", MsgDataShAddr, MsgDataProfile), ) if "ZDeviceID" in self.ListOfDevices[MsgDataShAddr]: if self.ListOfDevices[MsgDataShAddr]["ZDeviceID"] != MsgDataDeviceId: pass + self.ListOfDevices[MsgDataShAddr]["ZDeviceID"] = MsgDataDeviceId - self.log.logging( "Input", "Status", "[%s] NEW OBJECT: %s ZDeviceID %s" % ( - "-", MsgDataShAddr, MsgDataDeviceId), ) + if not inDB_status: + self.log.logging( "Input", "Status", "[%s] NEW OBJECT: %s ZDeviceID %s" % ( + "-", MsgDataShAddr, MsgDataDeviceId), ) # Decode Bit Field # Device version: 4 bits (bits 0-4) # eserved: 4 bits (bits4-7) DeviceVersion = int(MsgDataBField, 16) & 0x00001111 self.ListOfDevices[MsgDataShAddr]["ZDeviceVersion"] = "%04x" % DeviceVersion - self.log.logging( "Input", "Status", "[%s] NEW OBJECT: %s Application Version %s" % ( - "-", MsgDataShAddr, self.ListOfDevices[MsgDataShAddr]["ZDeviceVersion"]), ) + if not inDB_status: + self.log.logging( "Input", "Status", "[%s] NEW OBJECT: %s Application Version %s" % ( + "-", MsgDataShAddr, self.ListOfDevices[MsgDataShAddr]["ZDeviceVersion"]), ) configSourceAvailable = False - if "ConfigSource" in self.ListOfDevices[MsgDataShAddr]: - if self.ListOfDevices[MsgDataShAddr]["ConfigSource"] == "DeviceConf": - configSourceAvailable = True + if "ConfigSource" in self.ListOfDevices[MsgDataShAddr] and self.ListOfDevices[MsgDataShAddr]["ConfigSource"] == "DeviceConf": + configSourceAvailable = True # Decoding Cluster IN - self.log.logging( "Input", "Status", "[%s] NEW OBJECT: %s Cluster IN Count: %s" % ( - "-", MsgDataShAddr, MsgDataInClusterCount), ) + if not inDB_status: + self.log.logging( "Input", "Status", "[%s] NEW OBJECT: %s Cluster IN Count: %s" % ( + "-", MsgDataShAddr, MsgDataInClusterCount), ) idx = 24 i = 1 if int(MsgDataInClusterCount, 16) > 0: @@ -2161,23 +2164,25 @@ def Decode8043(self, Devices, MsgData, MsgLQI): # Reception Simple descriptor r if MsgDataCluster not in self.ListOfDevices[MsgDataShAddr]["Epv2"][MsgDataEp]["ClusterIn"]: self.ListOfDevices[MsgDataShAddr]["Epv2"][MsgDataEp]["ClusterIn"][MsgDataCluster] = {} - if MsgDataCluster in ZCL_CLUSTERS_LIST: - self.log.logging( "Input", "Status", "[%s] NEW OBJECT: %s Cluster In %s: %s (%s)" % ( - "-", MsgDataShAddr, i, MsgDataCluster, ZCL_CLUSTERS_LIST[MsgDataCluster], ), ) - else: - self.log.logging( "Input", "Status", "[%s] NEW OBJECT: %s Cluster In %s: %s" % ( - "-", MsgDataShAddr, i, MsgDataCluster), ) - i = i + 1 + if not inDB_status: + if MsgDataCluster in ZCL_CLUSTERS_LIST: + self.log.logging( "Input", "Status", "[%s] NEW OBJECT: %s Cluster In %s: %s (%s)" % ( + "-", MsgDataShAddr, i, MsgDataCluster, ZCL_CLUSTERS_LIST[MsgDataCluster], ), ) + else: + self.log.logging( "Input", "Status", "[%s] NEW OBJECT: %s Cluster In %s: %s" % ( + "-", MsgDataShAddr, i, MsgDataCluster), ) + i += 1 # Decoding Cluster Out idx = 24 + int(MsgDataInClusterCount, 16) * 4 MsgDataOutClusterCount = MsgData[idx : idx + 2] - self.log.logging( "Input", "Status", "[%s] NEW OBJECT: %s Cluster OUT Count: %s" % ( - "-", MsgDataShAddr, MsgDataOutClusterCount), ) + if not inDB_status: + self.log.logging( "Input", "Status", "[%s] NEW OBJECT: %s Cluster OUT Count: %s" % ( + "-", MsgDataShAddr, MsgDataOutClusterCount), ) + idx += 2 i = 1 - if int(MsgDataOutClusterCount, 16) > 0: while i <= int(MsgDataOutClusterCount, 16): MsgDataCluster = MsgData[idx + ((i - 1) * 4) : idx + (i * 4)] @@ -2198,21 +2203,21 @@ def Decode8043(self, Devices, MsgData, MsgLQI): # Reception Simple descriptor r if MsgDataCluster not in self.ListOfDevices[MsgDataShAddr]["Epv2"][MsgDataEp]["ClusterOut"]: self.ListOfDevices[MsgDataShAddr]["Epv2"][MsgDataEp]["ClusterOut"][MsgDataCluster] = {} - if MsgDataCluster in ZCL_CLUSTERS_LIST: - self.log.logging("Input","Status","[%s] NEW OBJECT: %s Cluster Out %s: %s (%s)"% ( - "-",MsgDataShAddr,i,MsgDataCluster,ZCL_CLUSTERS_LIST[MsgDataCluster],),) - else: - self.log.logging("Input","Status","[%s] NEW OBJECT: %s Cluster Out %s: %s" % ( - "-", MsgDataShAddr, i, MsgDataCluster),) + if not inDB_status: + if MsgDataCluster in ZCL_CLUSTERS_LIST: + self.log.logging("Input","Status","[%s] NEW OBJECT: %s Cluster Out %s: %s (%s)"% ( + "-",MsgDataShAddr,i,MsgDataCluster,ZCL_CLUSTERS_LIST[MsgDataCluster],),) + else: + self.log.logging("Input","Status","[%s] NEW OBJECT: %s Cluster Out %s: %s" % ( + "-", MsgDataShAddr, i, MsgDataCluster),) MsgDataCluster = "" - i = i + 1 + i += 1 # Let's check if there is any other Ep to be disxcovered - if request_next_Ep(self, MsgDataShAddr): - if self.ListOfDevices[MsgDataShAddr]["Status"] != "inDB": - self.ListOfDevices[MsgDataShAddr]["Status"] = "8043" - self.ListOfDevices[MsgDataShAddr]["Heartbeat"] = "0" + if request_next_Ep(self, MsgDataShAddr) and not inDB_status: + self.ListOfDevices[MsgDataShAddr]["Status"] = "8043" + self.ListOfDevices[MsgDataShAddr]["Heartbeat"] = "0" self.log.logging("Pairing","Debug","Decode8043 - Processed " + MsgDataShAddr + " end results is: " + str(self.ListOfDevices[MsgDataShAddr]),) @@ -2406,7 +2411,7 @@ def Decode8048(self, Devices, MsgData, MsgLQI): # Leave indication # Will set to Leave in order to protect Domoticz Widget, Just need to make sure that we can reconnect at a point of time self.ListOfDevices[sAddr]["Status"] = "Leave" self.ListOfDevices[sAddr]["Heartbeat"] = 0 - self.log.logging("Input", "Error", "Receiving a leave from %s/%s while device is %s status" % ( + self.log.logging("Input", "Error", "Receiving a leave from %s/%s while device is '%s' status." % ( sAddr, MsgExtAddress, self.ListOfDevices[sAddr]["Status"])) zdevname = "" diff --git a/Modules/pairingProcess.py b/Modules/pairingProcess.py index 3ba8f5bbc..5de8b4c07 100644 --- a/Modules/pairingProcess.py +++ b/Modules/pairingProcess.py @@ -210,19 +210,13 @@ def interview_state_8043(self, NWKID, RIA, knownModel, status): def request_node_descriptor(self, NWKID, RIA=None, status=None): + """ Trigger a Req Node Descriptor if Manufacturer not found""" - if "Manufacturer" in self.ListOfDevices[NWKID]: - if self.ListOfDevices[NWKID]["Manufacturer"] in ({}, ""): - self.log.logging("Pairing", "Status", "[%s] NEW OBJECT: %s Request Node Descriptor" % (RIA, NWKID)) - zdp_node_descriptor_request(self, NWKID) - return True - - self.log.logging( - "Pairing", - "Debug", - "[%s] NEW OBJECT: %s Manufacturer: %s" % (RIA, NWKID, self.ListOfDevices[NWKID]["Manufacturer"]), - NWKID, - ) + manufacturer = self.ListOfDevices[NWKID].get("Manufacturer", "") + model = self.ListOfDevices[NWKID].get("Model", "") + + if model in ( "lumi.sensor_switch",) or manufacturer not in ( "", {} ): + self.log.logging( "Pairing", "Debug", "[%s] NEW OBJECT: %s Manufacturer: %s Model: %s" % (RIA, NWKID, manufacturer, model), NWKID, ) return False self.log.logging("Pairing", "Status", "[%s] NEW OBJECT: %s Request Node Descriptor" % (RIA, NWKID)) diff --git a/Modules/pluginModels.py b/Modules/pluginModels.py index c94b55a8c..e454dcaee 100644 --- a/Modules/pluginModels.py +++ b/Modules/pluginModels.py @@ -26,7 +26,7 @@ def plugin_self_identifier( self, model, manufacturer): return None def check_found_plugin_model( self, model, manufacturer_name=None, manufacturer_code=None, device_id=None): - self.log.logging( "Pairing", "Log", "check_found_plugin_model - %s %s %s %s" % ( + self.log.logging( "Pairing", "Debug", "check_found_plugin_model - %s %s %s %s" % ( model, manufacturer_name, manufacturer_code, device_id)) # Let's check if @@ -40,10 +40,10 @@ def check_found_plugin_model( self, model, manufacturer_name=None, manufacturer_ ): continue - self.log.logging( "Pairing", "Log", "check_found_plugin_model - Found %s" % x) + self.log.logging( "Pairing", "Debug", "check_found_plugin_model - Found %s" % x) if "PluginModelName" in x: - self.log.logging( "Pairing", "Log", "check_found_plugin_model - return %s" % ( + self.log.logging( "Pairing", "Debug", "check_found_plugin_model - return %s" % ( x["PluginModelName"])) return x["PluginModelName"] diff --git a/Modules/readClusters.py b/Modules/readClusters.py index af512a893..310a27083 100644 --- a/Modules/readClusters.py +++ b/Modules/readClusters.py @@ -787,7 +787,7 @@ def Cluster0012(self, Devices, MsgSQN, MsgSrcAddr, MsgSrcEp, MsgClusterId, MsgAt checkAndStoreAttributeValue(self, MsgSrcAddr, MsgSrcEp, MsgClusterId, MsgAttrID, value) checkAndStoreAttributeValue(self, MsgSrcAddr, MsgSrcEp, "0006", "0000", value) - elif _modelName in ("lumi.sensor_switch.aq3", "lumi.sensor_switch.aq3"): + elif _modelName in ("lumi.sensor_switch.aq3", "lumi.sensor_switch.aq3t"): value = int(decodeAttribute(self, MsgAttType, MsgClusterData)) self.log.logging( "Cluster", diff --git a/plugin.py b/plugin.py index 42920406a..58c09d6b1 100644 --- a/plugin.py +++ b/plugin.py @@ -1502,10 +1502,10 @@ def update_DB_device_status_to_reinit( self ): def check_python_modules_version( self ): MODULES_VERSION = { - "zigpy": "0.56.1", - "zigpy_znp": "0.11.2", + "zigpy": "0.59.0", + "zigpy_znp": "0.11.6", "zigpy_deconz": "0.21.1", - "bellows": "0.35.8", + "bellows": "0.36.8", } flag = True diff --git a/requirements.txt b/requirements.txt index 468fdd798..ddf66b1c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ -zigpy==0.56.1 +zigpy==0.59.0 +zigpy_znp==0.11.6 zigpy_deconz==0.21.1 +bellows==0.36.8 zigpy-cli==1.0.4 -zigpy_znp==0.11.2 -bellows==0.35.8 dnspython==2.3.0 pyserial>=3.5 z4d-certified-devices From 3bda66e83cf341f3d64de606fcbeb1986bdfeb39 Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Sun, 12 Nov 2023 11:00:11 +0100 Subject: [PATCH 39/47] fixing when TWO_COMPLEMENT_TST do not exist --- Modules/tuyaTS0601.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/tuyaTS0601.py b/Modules/tuyaTS0601.py index 4619d3d68..308f54a73 100644 --- a/Modules/tuyaTS0601.py +++ b/Modules/tuyaTS0601.py @@ -341,8 +341,8 @@ def ts0601_instant_power(self, Devices, nwkid, ep, value): self.log.logging( "Tuya0601", "Debug", "ts0601_instant_power - Instant Power %s %s %s" % (nwkid, ep, value), nwkid, ) # Given Zigbee 24-bit integer and tuya store in two's complement form model_name = self.ListOfDevices[ nwkid ]["Model"] if "Model" in self.ListOfDevices[ nwkid ] else None - twocomplement_tst = int( get_deviceconf_parameter_value( self, model_name, "TWO_COMPLEMENT_TST", return_default=None ),16) - twocomplement_val = int( get_deviceconf_parameter_value( self, model_name, "TWO_COMPLEMENT_VAL", return_default=None ),16) + twocomplement_tst = int( get_deviceconf_parameter_value( self, model_name, "TWO_COMPLEMENT_TST", return_default=0x00 ),16) + twocomplement_val = int( get_deviceconf_parameter_value( self, model_name, "TWO_COMPLEMENT_VAL", return_default=0x00 ),16) self.log.logging( "Tuya0601", "Debug", "ts0601_instant_power - Instant Power Two's Complement : %s %s" %( twocomplement_tst, twocomplement_val)) signed_int = int( value ) From 7dc5a516d12a97e5b963f9bfe60de57dd22461e8 Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Sun, 12 Nov 2023 11:02:37 +0100 Subject: [PATCH 40/47] fixing when TWO_COMPLEMENT_TST do not exist --- Modules/tuyaTS0601.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/tuyaTS0601.py b/Modules/tuyaTS0601.py index 308f54a73..b7cf7abea 100644 --- a/Modules/tuyaTS0601.py +++ b/Modules/tuyaTS0601.py @@ -341,8 +341,8 @@ def ts0601_instant_power(self, Devices, nwkid, ep, value): self.log.logging( "Tuya0601", "Debug", "ts0601_instant_power - Instant Power %s %s %s" % (nwkid, ep, value), nwkid, ) # Given Zigbee 24-bit integer and tuya store in two's complement form model_name = self.ListOfDevices[ nwkid ]["Model"] if "Model" in self.ListOfDevices[ nwkid ] else None - twocomplement_tst = int( get_deviceconf_parameter_value( self, model_name, "TWO_COMPLEMENT_TST", return_default=0x00 ),16) - twocomplement_val = int( get_deviceconf_parameter_value( self, model_name, "TWO_COMPLEMENT_VAL", return_default=0x00 ),16) + twocomplement_tst = int( get_deviceconf_parameter_value( self, model_name, "TWO_COMPLEMENT_TST", return_default="0" ),16) + twocomplement_val = int( get_deviceconf_parameter_value( self, model_name, "TWO_COMPLEMENT_VAL", return_default="0" ),16) self.log.logging( "Tuya0601", "Debug", "ts0601_instant_power - Instant Power Two's Complement : %s %s" %( twocomplement_tst, twocomplement_val)) signed_int = int( value ) From f00633184786f6cb64c429673169ec2a446dbe70 Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Sun, 12 Nov 2023 18:05:20 +0100 Subject: [PATCH 41/47] update Release notes --- ReleaseNotes.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ReleaseNotes.md b/ReleaseNotes.md index ee5150b93..ae5503d37 100644 --- a/ReleaseNotes.md +++ b/ReleaseNotes.md @@ -21,13 +21,20 @@ Release Numbering ## November 2023 - stable7 7.1.005 ( 2023.11 ) - [Technical] - zigpy radio libraries upgrade +- [Technical] - Moving from handle_message to packet_received - [Technical] - fix issue on Groups - [Technical] - Fix issue which was producing error message "No DPS stanza in config file for ....." - [Technical] - Refactor how IAS Alarm 1 & 2 are managed. +- [Technical] - New widget as PowerFactor, ProdPower, ConsoPower +- [Technical] - Provide possibility to select which complement's 2 to be usd. - [Hardware] - Fixing error on Heiman manufacturer command - [Hardware] - Aotec zigbee button support - [Hardware] - Tuya Hue and Saturation for Lidl "TS0505A", "_TZ3000_dbou1ap4" +- [Hardware] - Enhancement of Lidl Hue and Saturation color control +- [Hardware] - Handle the 2 version of Sonoff SNZB-02 +- [Hardware] - Handle the command specific for Nodon Fil Pilot +- [Hardware] - Tuya PC311 Multimeter 2 ways, 2 flows ## October 2023 - stable7 - 7.1.004 ( 2023.10 ) From 0a87a02d45b426619e0fc5307dafec09b92ba013 Mon Sep 17 00:00:00 2001 From: Pipiche <8291674+pipiche38@users.noreply.github.com> Date: Mon, 13 Nov 2023 16:09:11 +0100 Subject: [PATCH 42/47] Update plugin.py --- plugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugin.py b/plugin.py index 58c09d6b1..f94f5b7db 100644 --- a/plugin.py +++ b/plugin.py @@ -1667,8 +1667,9 @@ def uninstall_Z4D_to_domoticz_custom_ui(): def do_python_garbage_collection( self ): # Garbage collector ( experimental for now) if self.internalHB % (3600 // HEARTBEAT) == 0: - self.log.logging("Plugin", "Debug", "Garbage Collection status: %s" % str(gc.get_count())) - self.log.logging("Plugin", "Debug", "Garbage Collection triggered: %s" % str(gc.collect())) + self.log.logging("Garbage", "Debug", "Garbage Collection status: %s" % str(gc.get_count()) ) + # self.log.logging("Garbage", "Debug", "Garbage Collection triggered: %s" % str(gc.collect()) ) + self.log.logging("Garbage", "Debug", "Garbage collection statistics: %s" % str( gc.get_stats()) ) def _check_if_busy(self): busy_ = self.ControllerLink.loadTransmit() >= MAX_FOR_ZIGATE_BUZY From f0da95321566fc181012e2e4f6884f5f0f411d1d Mon Sep 17 00:00:00 2001 From: Pipiche <8291674+pipiche38@users.noreply.github.com> Date: Mon, 13 Nov 2023 16:10:00 +0100 Subject: [PATCH 43/47] Update PluginConf.py --- Classes/PluginConf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Classes/PluginConf.py b/Classes/PluginConf.py index ce7449ccb..d34a86c45 100644 --- a/Classes/PluginConf.py +++ b/Classes/PluginConf.py @@ -318,6 +318,7 @@ "ThreadWriter": {"type": "bool","default": 0,"current": None,"restart": 0,"hidden": False,"Advanced": True,}, "ThreadCommunication": {"type": "bool","default": 0,"current": None,"restart": 0,"hidden": False,"Advanced": True,}, "ZLinky": {"type": "bool","default": 0,"current": None,"restart": 0,"hidden": False,"Advanced": True,}, + "Garbage": {"type": "bool","default": 0,"current": None,"restart": 0,"hidden": False,"Advanced": True,}, }, }, # Others From 3d9008104bf2f3a8fd573cd31820e5b3b770d259 Mon Sep 17 00:00:00 2001 From: Pipiche <8291674+pipiche38@users.noreply.github.com> Date: Mon, 13 Nov 2023 16:30:11 +0100 Subject: [PATCH 44/47] Update plugin.py --- plugin.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/plugin.py b/plugin.py index f94f5b7db..a912353c0 100644 --- a/plugin.py +++ b/plugin.py @@ -275,7 +275,9 @@ def __init__(self): def onStart(self): Domoticz.Status( "Zigbee for Domoticz plugin starting") - + # Enable the cycle detector + # gc.set_debug(gc.DEBUG_SAVEALL) + _current_python_version_major = sys.version_info.major _current_python_version_minor = sys.version_info.minor @@ -731,6 +733,15 @@ def onStop(self): # sourcery skip: class-extract-method if self.adminWidgets: self.adminWidgets.updateStatusWidget(Devices, "No Communication") + # objects = gc.get_objects() + # Domoticz.Log( "Garbage Collected objects: %s" %str(objects)) + + # Print detected cycles (garbage collectors) + # for item in gc.garbage: + # Domoticz.Log(item) + + + def onDeviceRemoved(self, Unit): if self.log: self.log.logging("Plugin", "Debug", "onDeviceRemoved called") From dca78c73b265cb8beaa19df0b1ec93f078ccfc8b Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Mon, 13 Nov 2023 19:21:14 +0100 Subject: [PATCH 45/47] instrument with GC --- Classes/PluginConf.py | 2 +- plugin.py | 29 ++++++++++++++++++----------- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/Classes/PluginConf.py b/Classes/PluginConf.py index d34a86c45..475cc574d 100644 --- a/Classes/PluginConf.py +++ b/Classes/PluginConf.py @@ -318,7 +318,7 @@ "ThreadWriter": {"type": "bool","default": 0,"current": None,"restart": 0,"hidden": False,"Advanced": True,}, "ThreadCommunication": {"type": "bool","default": 0,"current": None,"restart": 0,"hidden": False,"Advanced": True,}, "ZLinky": {"type": "bool","default": 0,"current": None,"restart": 0,"hidden": False,"Advanced": True,}, - "Garbage": {"type": "bool","default": 0,"current": None,"restart": 0,"hidden": False,"Advanced": True,}, + "Garbage": {"type": "bool","default": 0,"current": None,"restart": 0,"hidden": True,"Advanced": True,}, }, }, # Others diff --git a/plugin.py b/plugin.py index a912353c0..ad99d8c5f 100644 --- a/plugin.py +++ b/plugin.py @@ -275,9 +275,7 @@ def __init__(self): def onStart(self): Domoticz.Status( "Zigbee for Domoticz plugin starting") - # Enable the cycle detector - # gc.set_debug(gc.DEBUG_SAVEALL) - + _current_python_version_major = sys.version_info.major _current_python_version_minor = sys.version_info.minor @@ -367,6 +365,11 @@ def onStart(self): self.zigbee_communication, self.VersionNewFashion, self.DomoticzMajor, self.DomoticzMinor, Parameters["HomeFolder"], self.HardwareID ) + if self.pluginconf.pluginConf["Garbage"]: + # Enable the cycle detector + Domoticz.Log("Setup Garbage set_debug to %s" %gc.DEBUG_LEAK) + gc.set_debug(gc.DEBUG_LEAK) + # Create Domoticz Sub menu if "DomoticzCustomMenu" in self.pluginconf.pluginConf and self.pluginconf.pluginConf["DomoticzCustomMenu"] : install_Z4D_to_domoticz_custom_ui( ) @@ -431,7 +434,7 @@ def onStart(self): self.zigbee_communication and self.zigbee_communication == "zigpy" and ( self.pluginconf.pluginConf["forceZigpy_noasyncio"] or self.domoticzdb_Hardware.multiinstances_z4d_plugin_instance()) - ): + ): # https://github.com/python/cpython/issues/91375 self.log.logging("Plugin", "Status", "Multi instances plugin detected. Enable zigpy workaround") sys.modules["_asyncio"] = None @@ -512,7 +515,6 @@ def onStart(self): if "Model" in self.ListOfDevices[x] and self.ListOfDevices[x]["Model"] in ( "", {} ): profalux_fake_deviceModel(self, x) - self.log.logging("Plugin", "Debug", "ListOfDevices after checkListOfDevice2Devices: " + str(self.ListOfDevices)) self.log.logging("Plugin", "Debug", "IEEE2NWK after checkListOfDevice2Devices : " + str(self.IEEE2NWK)) @@ -733,12 +735,17 @@ def onStop(self): # sourcery skip: class-extract-method if self.adminWidgets: self.adminWidgets.updateStatusWidget(Devices, "No Communication") - # objects = gc.get_objects() - # Domoticz.Log( "Garbage Collected objects: %s" %str(objects)) + if self.pluginconf.pluginConf["Garbage"]: + + # Domoticz.Log( "Garbage Collected objects:") + # objects = gc.get_objects() + # for item in objects: + # Domoticz.Log( "- %s" %str(item)) - # Print detected cycles (garbage collectors) - # for item in gc.garbage: - # Domoticz.Log(item) + # Print detected cycles (garbage collectors) + Domoticz.Log( "Garbage Collected detected cycles:") + for item in gc.garbage: + Domoticz.Log("- %s" %str(item)) @@ -1679,8 +1686,8 @@ def do_python_garbage_collection( self ): # Garbage collector ( experimental for now) if self.internalHB % (3600 // HEARTBEAT) == 0: self.log.logging("Garbage", "Debug", "Garbage Collection status: %s" % str(gc.get_count()) ) - # self.log.logging("Garbage", "Debug", "Garbage Collection triggered: %s" % str(gc.collect()) ) self.log.logging("Garbage", "Debug", "Garbage collection statistics: %s" % str( gc.get_stats()) ) + # self.log.logging("Garbage", "Debug", "Garbage Collection triggered: %s" % str(gc.collect()) ) def _check_if_busy(self): busy_ = self.ControllerLink.loadTransmit() >= MAX_FOR_ZIGATE_BUZY From 038af64e2070944373789f82e64e0b9f4d5ec2a8 Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Tue, 14 Nov 2023 21:55:48 +0100 Subject: [PATCH 46/47] implement Tuya Data_Request wich must be sent by GW for some devices --- Modules/pairingProcess.py | 11 ++++++----- Modules/tuya.py | 7 ++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Modules/pairingProcess.py b/Modules/pairingProcess.py index 5de8b4c07..46d74f492 100644 --- a/Modules/pairingProcess.py +++ b/Modules/pairingProcess.py @@ -616,7 +616,8 @@ def handle_device_specific_needs(self, Devices, NWKID): return # Tuya_regitration ? - tuya_registration_parameter = get_deviceconf_parameter_value(self, self.ListOfDevices[NWKID]["Model"], "TUYA_REGISTRATION", return_default=None) + tuya_registration_parameter = get_deviceconf_parameter_value(self, self.ListOfDevices[NWKID]["Model"], "TUYA_REGISTRATION", return_default=None) + ty_data_request = get_deviceconf_parameter_value(self, self.ListOfDevices[NWKID]["Model"], "TUYA_RESET_CMD", return_default=False) # In case of Schneider Wiser, let's do the Registration Process MsgIEEE = self.ListOfDevices[NWKID]["IEEE"] if self.ListOfDevices[NWKID]["Model"] in ("Wiser2-Thermostat",): @@ -639,10 +640,10 @@ def handle_device_specific_needs(self, Devices, NWKID): elif self.ListOfDevices[NWKID]["Model"] in (TUYA_eTRV_MODEL): self.log.logging("Pairing", "Debug", "Tuya eTRV registration needed") - tuya_eTRV_registration(self, NWKID, device_reset=True) + tuya_eTRV_registration(self, NWKID, ty_data_request=True) elif tuya_registration_parameter: - tuya_registration(self, NWKID, device_reset=False, parkside=False, tuya_registration_value=tuya_registration_parameter) + tuya_registration(self, NWKID, ty_data_request=ty_data_request, parkside=False, tuya_registration_value=tuya_registration_parameter) elif self.ListOfDevices[NWKID]["Model"] in ("TS0121", "TS0002_relay_switch", "TS0002_relay_switch"): self.log.logging("Pairing", "Debug", "Tuya TS0121 registration needed") @@ -671,11 +672,11 @@ def handle_device_specific_needs(self, Devices, NWKID): "TS0601-temphumi", ): self.log.logging("Pairing", "Debug", "Tuya general registration needed") - tuya_registration(self, NWKID, device_reset=True) + tuya_registration(self, NWKID, ty_data_request=True) elif self.ListOfDevices[NWKID]["Model"] in ("TS0601-Parkside-Watering-Timer", "TS0601-_TZE200_nklqjk62"): self.log.logging("Pairing", "Debug", "Tuya Water Sensor Parkside registration needed") - tuya_registration(self, NWKID, device_reset=True, parkside=True) + tuya_registration(self, NWKID, ty_data_request=True, parkside=True) elif self.ListOfDevices[NWKID]["Model"] in ( "TS0216", "TY0A01", ): # Do just the registration diff --git a/Modules/tuya.py b/Modules/tuya.py index e799149fc..c3eb2985b 100644 --- a/Modules/tuya.py +++ b/Modules/tuya.py @@ -44,7 +44,7 @@ # 0x04: enum8 ( 0x00-0xff) # 0x05: bitmap ( 1,2, 4 bytes) as bits -def tuya_registration(self, nwkid, device_reset=False, parkside=False, tuya_registration_value=None): +def tuya_registration(self, nwkid, ty_data_request=False, parkside=False, tuya_registration_value=None): if "Model" not in self.ListOfDevices[nwkid]: return _ModelName = self.ListOfDevices[nwkid]["Model"] @@ -79,8 +79,9 @@ def tuya_registration(self, nwkid, device_reset=False, parkside=False, tuya_regi write_attribute(self, nwkid, ZIGATE_EP, EPout, "0000", "0000", "00", "ffde", "20", "13", ackIsDisabled=False) - # (3) Cmd 0x03 on Cluster 0xef00 (Cluster Specific) / Zigbee Device Reset - if device_reset: + # (3) Cmd 0x03 on Cluster 0xef00 (Cluster Specific) / Tuya TY_DATA_QUERY + # (https://developer.tuya.com/en/docs/iot/tuya-zigbee-universal-docking-access-standard?id=K9ik6zvofpzql#subtitle-6-Private%20cluster) + if ty_data_request: payload = "11" + get_and_inc_ZCL_SQN(self, nwkid) + "03" raw_APS_request( self, nwkid, EPout, "ef00", "0104", payload, zigate_ep=ZIGATE_EP, ackIsDisabled=is_ack_tobe_disabled(self, nwkid), ) self.log.logging("Tuya", "Debug", "tuya_registration - Nwkid: %s reset device Cmd: 03" % nwkid) From 991cd5e5cb6ad070eb4fa9b35a92c15078e360ce Mon Sep 17 00:00:00 2001 From: Patrick Pichon Date: Tue, 14 Nov 2023 21:56:17 +0100 Subject: [PATCH 47/47] disable gc code --- plugin.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/plugin.py b/plugin.py index ad99d8c5f..85ce9867c 100644 --- a/plugin.py +++ b/plugin.py @@ -365,10 +365,10 @@ def onStart(self): self.zigbee_communication, self.VersionNewFashion, self.DomoticzMajor, self.DomoticzMinor, Parameters["HomeFolder"], self.HardwareID ) - if self.pluginconf.pluginConf["Garbage"]: - # Enable the cycle detector - Domoticz.Log("Setup Garbage set_debug to %s" %gc.DEBUG_LEAK) - gc.set_debug(gc.DEBUG_LEAK) + #if self.pluginconf.pluginConf["Garbage"]: + # # Enable the cycle detector + # Domoticz.Log("Setup Garbage set_debug to %s" %gc.DEBUG_LEAK) + # gc.set_debug(gc.DEBUG_LEAK) # Create Domoticz Sub menu if "DomoticzCustomMenu" in self.pluginconf.pluginConf and self.pluginconf.pluginConf["DomoticzCustomMenu"] : @@ -735,17 +735,17 @@ def onStop(self): # sourcery skip: class-extract-method if self.adminWidgets: self.adminWidgets.updateStatusWidget(Devices, "No Communication") - if self.pluginconf.pluginConf["Garbage"]: - - # Domoticz.Log( "Garbage Collected objects:") - # objects = gc.get_objects() - # for item in objects: - # Domoticz.Log( "- %s" %str(item)) - - # Print detected cycles (garbage collectors) - Domoticz.Log( "Garbage Collected detected cycles:") - for item in gc.garbage: - Domoticz.Log("- %s" %str(item)) + #if self.pluginconf.pluginConf["Garbage"]: + # + # # Domoticz.Log( "Garbage Collected objects:") + # # objects = gc.get_objects() + # # for item in objects: + # # Domoticz.Log( "- %s" %str(item)) +# + # # Print detected cycles (garbage collectors) + # Domoticz.Log( "Garbage Collected detected cycles:") + # for item in gc.garbage: + # Domoticz.Log("- %s" %str(item)) @@ -1684,10 +1684,11 @@ def uninstall_Z4D_to_domoticz_custom_ui(): def do_python_garbage_collection( self ): # Garbage collector ( experimental for now) - if self.internalHB % (3600 // HEARTBEAT) == 0: - self.log.logging("Garbage", "Debug", "Garbage Collection status: %s" % str(gc.get_count()) ) - self.log.logging("Garbage", "Debug", "Garbage collection statistics: %s" % str( gc.get_stats()) ) - # self.log.logging("Garbage", "Debug", "Garbage Collection triggered: %s" % str(gc.collect()) ) + pass + #if self.internalHB % (3600 // HEARTBEAT) == 0: + # self.log.logging("Garbage", "Debug", "Garbage Collection status: %s" % str(gc.get_count()) ) + # self.log.logging("Garbage", "Debug", "Garbage collection statistics: %s" % str( gc.get_stats()) ) + # # self.log.logging("Garbage", "Debug", "Garbage Collection triggered: %s" % str(gc.collect()) ) def _check_if_busy(self): busy_ = self.ControllerLink.loadTransmit() >= MAX_FOR_ZIGATE_BUZY