diff --git a/Classes/PluginConf.py b/Classes/PluginConf.py index 743c7bb19..08c8a892f 100644 --- a/Classes/PluginConf.py +++ b/Classes/PluginConf.py @@ -36,7 +36,7 @@ "internetAccess": { "type": "bool", "default": 1, "current": None, "restart": 1, "hidden": False, "Advanced": False, }, "CheckSSLCertificateValidity": { "type": "bool", "default": 0, "current": None, "restart": 1, "hidden": False, "Advanced": False, }, "allowOTA": { "type": "bool", "default": 1, "current": None, "restart": 1, "hidden": True, "Advanced": False, }, - "pingDevices": { "type": "bool", "default": 1, "current": None, "restart": 1, "hidden": False, "Advanced": True, }, + "CheckDeviceHealth": { "type": "bool", "default": 1, "current": None, "restart": 1, "hidden": False, "Advanced": True, }, "PluginAnalytics": { "type": "bool", "default": -1, "current": None, "restart": 0, "hidden": False, "Advanced": False, }, "DomoticzCustomMenu": { "type": "bool", "default": 1, "current": None, "restart": 1, "hidden": False, "Advanced": False, }, "NightShift": { "type": "bool", "default": 0, "current": None, "restart": 0, "hidden": False, "Advanced": False, } @@ -127,6 +127,7 @@ "Order": 8, "param": { "deviceOffWhenTimeOut": { "type": "bool", "default": 0, "current": None, "restart": 0, "hidden": False, "Advanced": True, }, + "pingDevicesFeq": { "type": "int", "default": 3600, "current": None, "restart": 0, "hidden": False, "Advanced": True, }, "forcePollingAfterAction": { "type": "bool", "default": 1, "current": None, "restart": 0, "hidden": False, "Advanced": True, }, "forcePassiveWidget": { "type": "bool", "default": 0, "current": None, "restart": 0, "hidden": False, "Advanced": True, }, "allowForceCreationDomoDevice": { "type": "bool", "default": 0, "current": None, "restart": 0, "hidden": True, "Advanced": True, }, @@ -146,7 +147,6 @@ "Order": 9, "param": { "blueLedOnOff": { "type": "bool", "default": 1, "current": None, "restart": 0, "hidden": False, "Advanced": False, }, - "pingDevicesFeq": { "type": "int", "default": 3600, "current": None, "restart": 0, "hidden": False, "Advanced": True, }, "resetPermit2Join": { "type": "bool", "default": 1, "current": None, "restart": 0, "hidden": False, "Advanced": True, }, "Ping": {"type": "bool", "default": 1, "current": None, "restart": 0, "hidden": False, "Advanced": True}, "allowRemoveZigateDevice": { "type": "bool", "default": 1, "current": None, "restart": 0, "hidden": True, "Advanced": True, "ZigpyRadio": "" }, @@ -266,6 +266,7 @@ "PDM": { "type": "bool", "default": 0, "current": None, "restart": 0, "hidden": False, "Advanced": True }, "Pairing": { "type": "bool", "default": 0, "current": None, "restart": 0, "hidden": False, "Advanced": True }, "Philips": { "type": "bool", "default": 0, "current": None, "restart": 0, "hidden": False, "Advanced": True }, + "PingDevices": { "type": "bool", "default": 0, "current": None, "restart": 0, "hidden": False, "Advanced": True }, "PiZigate": { "type": "bool", "default": 0, "current": None, "restart": 0, "hidden": False, "Advanced": True }, "Plugin": { "type": "bool", "default": 0, "current": None, "restart": 0, "hidden": False, "Advanced": True }, "PluginTools": { "type": "bool", "default": 0, "current": None, "restart": 0, "hidden": False, "Advanced": True }, diff --git a/Classes/ZigpyTransport/plugin_encoders.py b/Classes/ZigpyTransport/plugin_encoders.py index ef22fea41..ddfe43da1 100644 --- a/Classes/ZigpyTransport/plugin_encoders.py +++ b/Classes/ZigpyTransport/plugin_encoders.py @@ -119,7 +119,8 @@ def build_plugin_8011_frame_content(self, nwkid, status, lqi): # MsgSEQ = MsgData[12:14] if MsgLen > 12 else None lqi = lqi or 0x00 - frame_payload = "%02x" % status + nwkid + frame_payload = "%02x" % status + frame_payload += nwkid return encapsulate_plugin_frame("8011", frame_payload, "%02x" % lqi) def build_plugin_8014_frame_content(self, nwkid, payload): diff --git a/Classes/ZigpyTransport/zigpyThread.py b/Classes/ZigpyTransport/zigpyThread.py index 52531b8aa..49efba4b7 100644 --- a/Classes/ZigpyTransport/zigpyThread.py +++ b/Classes/ZigpyTransport/zigpyThread.py @@ -827,9 +827,8 @@ async def _send_and_retry(self, Function, destination, Profile, Cluster, _nwkid, result, _ = await zigpy_request(self, destination, Profile, Cluster, sEp, dEp, sequence, payload, ack_is_disable=ack_is_disable, use_ieee=use_ieee, extended_timeout=extended_timeout) except (asyncio.exceptions.TimeoutError, asyncio.exceptions.CancelledError, AttributeError, DeliveryError) as e: - error_log_message = f"Warning while submitting - {Function} {_ieee}/0x{_nwkid} 0x{Profile:X} 0x{Cluster:X} payload: {payload} AckIsDisable: {ack_is_disable} Retry: {attempt}/{max_retry} with exception ({e})" + error_log_message = f"Warning while submitting - {Function} {_ieee}/0x{_nwkid} 0x{Profile:X} 0x{Cluster:X} payload: {payload} AckIsDisable: {ack_is_disable} Retry: {attempt}/{max_retry} with exception: '{e}' ({type(e)}))" self.log.logging("TransportZigpy", "Log", error_log_message) - if await _retry_or_not(self, attempt, max_retry, Function, sequence, ack_is_disable, _ieee, _nwkid, destination, e): self.statistics._reTx += 1 if isinstance(e, asyncio.exceptions.TimeoutError): @@ -837,6 +836,7 @@ async def _send_and_retry(self, Function, destination, Profile, Cluster, _nwkid, continue else: self.statistics._ackKO += 1 + result = 0xB6 break except Exception as error: @@ -848,7 +848,7 @@ async def _send_and_retry(self, Function, destination, Profile, Cluster, _nwkid, else: # Success if delay_after_command_sent: - self.log.logging("TransportZigpy", "Log", f"sleeping {delay_after_command_sent} as per configured!!") + self.log.logging("TransportZigpy", "Debug", f"sleeping {delay_after_command_sent} as per configured!!") await asyncio.sleep( delay_after_command_sent ) handle_transport_result(self, Function, sequence, result, ack_is_disable, _ieee, _nwkid, destination.lqi) @@ -953,14 +953,14 @@ async def _retry_or_not(self, attempt, max_retry, Function, sequence,ack_is_disa return True # Stop here as we have exceed the max retrys - result = int(e.status) if hasattr(e, 'status') else 0xB6 + self.log.logging("TransportZigpy", "Log", f"_retry_or_not: result: {e} ({(type(e))})") + result = min(int(e.status) if hasattr(e, 'status') else 0xB6, 0xB6) handle_transport_result(self, Function, sequence, result, ack_is_disable, _ieee, _nwkid, destination.lqi) return False def handle_transport_result(self, Function, sequence, result, ack_is_disable, _ieee, _nwkid, lqi): - self.log.logging("TransportZigpy", "Debug", f"handle_transport_result - {Function} - {_nwkid} - AckIsDisable: {ack_is_disable} Result: {result}") if ack_is_disable: # As Ack is disable, we cannot conclude that the target device is in trouble. # this could be the coordinator itself, or the next hop. @@ -970,11 +970,9 @@ def handle_transport_result(self, Function, sequence, result, ack_is_disable, _i if result == 0x00 and _ieee in self._currently_not_reachable: self._currently_not_reachable.remove(_ieee) - self.log.logging("TransportZigpy", "Debug", f"handle_transport_result -removing {_ieee} to not_reachable queue") elif result != 0x00 and _ieee not in self._currently_not_reachable: # Mark the ieee has not reachable. - self.log.logging("TransportZigpy", "Debug", f"handle_transport_result -adding {_ieee} to not_reachable queue") self._currently_not_reachable.append(_ieee) diff --git a/Modules/heartbeat.py b/Modules/heartbeat.py index 065a3ef80..11d42abe8 100755 --- a/Modules/heartbeat.py +++ b/Modules/heartbeat.py @@ -87,66 +87,9 @@ PING_DEVICE_VIA_GROUPID = 3567 // HEARTBEAT # Secondes ( 59minutes et 45 secondes ) FIRST_PING_VIA_GROUP = 127 // HEARTBEAT +# Retry intervals: Retry #1 (30s), Retry #2 (120s), Retry #3 (300s) +PING_RETRY_INTERVALS = [25, 115, 295] -#def attributeDiscovery(self, NwkId): -# -# rescheduleAction = False -# # If Attributes not yet discovered, let's do it -# -# if "ConfigSource" not in self.ListOfDevices[NwkId]: -# return False -# -# if self.ListOfDevices[NwkId]["ConfigSource"] == "DeviceConf": -# return False -# -# if "Attributes List" in self.ListOfDevices[NwkId] and len(self.ListOfDevices[NwkId]["Attributes List"]) > 0: -# return False -# -# if "Attributes List" not in self.ListOfDevices[NwkId]: -# self.ListOfDevices[NwkId]["Attributes List"] = {'Ep': {}} -# if "Request" not in self.ListOfDevices[NwkId]["Attributes List"]: -# self.ListOfDevices[NwkId]["Attributes List"]["Request"] = {} -# -# for iterEp in list(self.ListOfDevices[NwkId]["Ep"]): -# if iterEp == "ClusterType": -# continue -# if iterEp not in self.ListOfDevices[NwkId]["Attributes List"]["Request"]: -# self.ListOfDevices[NwkId]["Attributes List"]["Request"][iterEp] = {} -# -# for iterCluster in list(self.ListOfDevices[NwkId]["Ep"][iterEp]): -# if iterCluster in ("Type", "ClusterType", "ColorMode"): -# continue -# if iterCluster not in self.ListOfDevices[NwkId]["Attributes List"]["Request"][iterEp]: -# self.ListOfDevices[NwkId]["Attributes List"]["Request"][iterEp][iterCluster] = 0 -# -# if self.ListOfDevices[NwkId]["Attributes List"]["Request"][iterEp][iterCluster] != 0: -# continue -# -# if not self.busy and self.ControllerLink.loadTransmit() <= MAX_LOAD_ZIGATE: -# if int(iterCluster, 16) < 0x0FFF: -# getListofAttribute(self, NwkId, iterEp, iterCluster) -# # getListofAttributeExtendedInfos(self, NwkId, EpOut, cluster, start_attribute=None, manuf_specific=None, manuf_code=None) -# elif ( -# "Manufacturer" in self.ListOfDevices[NwkId] -# and len(self.ListOfDevices[NwkId]["Manufacturer"]) == 4 -# and is_hex(self.ListOfDevices[NwkId]["Manufacturer"]) -# ): -# getListofAttribute( -# self, -# NwkId, -# iterEp, -# iterCluster, -# manuf_specific="01", -# manuf_code=self.ListOfDevices[NwkId]["Manufacturer"], -# ) -# # getListofAttributeExtendedInfos(self, NwkId, EpOut, cluster, start_attribute=None, manuf_specific=None, manuf_code=None) -# -# self.ListOfDevices[NwkId]["Attributes List"]["Request"][iterEp][iterCluster] = time.time() -# -# else: -# rescheduleAction = True -# -# return rescheduleAction def attributeDiscovery(self, NwkId): # If Attributes not yet discovered, let's do it @@ -421,199 +364,255 @@ def pollingDeviceStatus(self, NwkId): return False -def checkHealth(self, NwkId): +def is_check_device_health_not_needed(self, NwkId): + """ + Check and update the health status of a device in the network. - # Checking current state of the this Nwk - if "Health" not in self.ListOfDevices[NwkId]: - self.ListOfDevices[NwkId]["Health"] = "" - - if self.ListOfDevices[NwkId]["Health"] == "Disabled": + Args: + NwkId (str): The network ID of the device to check. + + Returns: + bool: False if a check is required, True otherwise. + + This method: + - Ensures the "Health" and "Stamp" fields are initialized for the device. + - Updates the device's health to "unknown" if it lacks a valid health status. + - Checks if a "Live" device has been inactive for more than 21,200 seconds and flags it as "Not seen last 24hours". + - Logs a message when a "Live" device appears to be out of the network. + """ + # Retrieve the device or initialize as an empty dictionary + device = self.ListOfDevices.get(NwkId, {}) + health = device.setdefault("Health", "") + + # Initialize Health and Stamp fields if missing + if "Stamp" not in device or "LastSeen" not in device["Stamp"]: + device["Health"] = "unknown" + + stamp = device.setdefault("Stamp", {"LastPing": 0, "LastSeen": 0}) + stamp.setdefault("LastSeen", 0) + + if health not in {"Disabled", "Live", "Not Reachable", "Not seen last 24hours"}: return False - - if "Stamp" not in self.ListOfDevices[NwkId]: - self.ListOfDevices[NwkId]["Stamp"] = {'LastPing': 0, 'LastSeen': 0} - self.ListOfDevices[NwkId]["Health"] = "unknown" - if "LastSeen" not in self.ListOfDevices[NwkId]["Stamp"]: - self.ListOfDevices[NwkId]["Stamp"]["LastSeen"] = 0 - self.ListOfDevices[NwkId]["Health"] = "unknown" + # Check if device is live but hasn't been seen recently + current_time = int(time.time()) + if device["Health"] == "Live" and current_time > ( stamp["LastSeen"] + 3600 * 12): + # That is 12 hours we didn't receive any message from the device which was Live! + z_device_name = device.get("ZDeviceName", f"NwkId: {NwkId}") + self.log.logging( + "PingDevices", + "Debug", + f"Device Health - {z_device_name}, IEEE: {device.get('IEEE')}, Model: {device.get('Model')} seems to be out of the network" + ) + device["Health"] = "Not seen last 24hours" - if ( - int(time.time()) > (self.ListOfDevices[NwkId]["Stamp"]["LastSeen"] + 21200) - and self.ListOfDevices[NwkId]["Health"] == "Live" - ): - if "ZDeviceName" in self.ListOfDevices[NwkId]: - self.log.logging("Heartbeat", "Debug", "Device Health - %s NwkId: %s,Ieee: %s , Model: %s seems to be out of the network" % ( - self.ListOfDevices[NwkId]["ZDeviceName"], NwkId, self.ListOfDevices[NwkId]["IEEE"], self.ListOfDevices[NwkId]["Model"],)) - else: - self.log.logging("Heartbeat", "Debug", "Device Health - NwkId: %s,Ieee: %s , Model: %s seems to be out of the network" % ( - NwkId, self.ListOfDevices[NwkId]["IEEE"], self.ListOfDevices[NwkId]["Model"]) ) - self.ListOfDevices[NwkId]["Health"] = "Not seen last 24hours" + # Return whether the device is not flagged as Not Reachable + return device["Health"] != "Not Reachable" - # If device flag as Not Reachable, don't do anything - return ( "Health" not in self.ListOfDevices[NwkId] or self.ListOfDevices[NwkId]["Health"] != "Not Reachable") +def retry_ping_device_in_bad_health(self, NwkId): + """ + Handle retry logic for pinging a device with bad health status. -def pingRetryDueToBadHealth(self, NwkId): + Args: + NwkId (str): The network ID of the device to check. + This method: + - Initializes the "pingDeviceRetry" structure if missing. + - Resets retry logic for devices with outdated or missing timestamp data. + - Performs up to three retries (at increasing intervals) if the device is in a bad health state. + - Logs details of each retry attempt and calls ping-related methods as needed. + """ now = int(time.time()) - # device is on Non Reachable state - self.log.logging("Heartbeat", "Debug", "--------> ping Retry Check %s" % NwkId, NwkId) - if "pingDeviceRetry" not in self.ListOfDevices[NwkId]: - self.ListOfDevices[NwkId]["pingDeviceRetry"] = {"Retry": 0, "TimeStamp": now} - if self.ListOfDevices[NwkId]["pingDeviceRetry"]["Retry"] == 0: - return + self.log.logging("Heartbeat", "Debug", f"--------> retry_ping_device_in_bad_health - ping Retry Check {NwkId}", NwkId) + + # Initialize or reset "pingDeviceRetry" if missing + retry_info = self.ListOfDevices.setdefault(NwkId, {}).setdefault( "pingDeviceRetry", {"Retry": 0, "TimeStamp": now} ) + retry = retry_info["Retry"] + last_timestamp = retry_info.get("TimeStamp", now) + + # Reset retry info for devices missing a timestamp (legacy data handling) + if retry > 0 and "TimeStamp" not in retry_info: + retry_info["Retry"] = 0 + retry_info["TimeStamp"] = now + + # Log current retry details + self.log.logging( "PingDevices", "Debug", f"--------> retry_ping_device_in_bad_health - ping Retry Check {NwkId} Retry: {retry} Gap: {now - last_timestamp}", NwkId, ) + + if retry < len(PING_RETRY_INTERVALS): + interval = PING_RETRY_INTERVALS[retry] + if (self.ControllerLink.loadTransmit() == 0) and now > (last_timestamp + interval): + ping_while_in_bad_health(self, retry, NwkId, retry_info, now) + - if "Retry" in self.ListOfDevices[NwkId]["pingDeviceRetry"] and "TimeStamp" not in self.ListOfDevices[NwkId]["pingDeviceRetry"]: - # This could be due to a previous version without TimeStamp - self.ListOfDevices[NwkId]["pingDeviceRetry"]["Retry"] = 0 - self.ListOfDevices[NwkId]["pingDeviceRetry"]["TimeStamp"] = now +def ping_while_in_bad_health(self, retry, NwkId, retry_info, now): + """ + Handle device ping attempts when the device is in bad health. - lastTimeStamp = self.ListOfDevices[NwkId]["pingDeviceRetry"]["TimeStamp"] - retry = self.ListOfDevices[NwkId]["pingDeviceRetry"]["Retry"] + Args: + retry (int): The current retry count. + NwkId (str): The network ID of the device. + retry_info (dict): A dictionary containing retry-related information. + now (int): The current timestamp. + This function logs the retry attempt, updates the retry information, + and sends a ping to the device after attempting a network address request. + """ + # Log the retry attempt self.log.logging( - "Heartbeat", + "PingDevices", "Debug", - "--------> ping Retry Check %s Retry: %s Gap: %s" % (NwkId, retry, now - lastTimeStamp), - NwkId, + f"--------> ping_while_in_bad_health - Ping Retry {retry + 1} for Device {NwkId}", + NwkId ) - # Retry #1 - if ( - retry == 0 - and self.ControllerLink.loadTransmit() == 0 - and now > (lastTimeStamp + 30) - ): # 30s - self.log.logging("Heartbeat", "Debug", "--------> ping Retry 1 Check %s" % NwkId, NwkId) - self.ListOfDevices[NwkId]["pingDeviceRetry"]["Retry"] += 1 - self.ListOfDevices[NwkId]["pingDeviceRetry"]["TimeStamp"] = now - lookup_ieee = self.ListOfDevices[ NwkId ]['IEEE'] - zdp_NWK_address_request(self, "0000", lookup_ieee) - submitPing(self, NwkId) - return - # Retry #2 - if ( - retry == 1 - and self.ControllerLink.loadTransmit() == 0 - and now > (lastTimeStamp + 120) - ): # 30 + 120s - # Let's retry - self.log.logging("Heartbeat", "Debug", "--------> ping Retry 2 Check %s" % NwkId, NwkId) - self.ListOfDevices[NwkId]["pingDeviceRetry"]["Retry"] += 1 - self.ListOfDevices[NwkId]["pingDeviceRetry"]["TimeStamp"] = now - lookup_ieee = self.ListOfDevices[ NwkId ]['IEEE'] - zdp_NWK_address_request(self, "fffd", lookup_ieee) - submitPing(self, NwkId) - return + # Update retry information + retry_info["Retry"] += 1 + retry_info["TimeStamp"] = now - # Retry #3 - if ( - retry == 2 - and self.ControllerLink.loadTransmit() == 0 - and now > (lastTimeStamp + 300) - ): # 30 + 120 + 300 - # Let's retry - self.log.logging("Heartbeat", "Debug", "--------> ping Retry 3 (last) Check %s" % NwkId, NwkId) - self.ListOfDevices[NwkId]["pingDeviceRetry"]["Retry"] += 1 - self.ListOfDevices[NwkId]["pingDeviceRetry"]["TimeStamp"] = now - lookup_ieee = self.ListOfDevices[ NwkId ]['IEEE'] - zdp_NWK_address_request(self, "FFFD", lookup_ieee) - submitPing(self, NwkId) + # Get the IEEE address of the device + lookup_ieee = self.ListOfDevices[NwkId]["IEEE"] + + # Perform a network address request, using a broadcast or direct address + target_address = "FFFD" if retry > 0 else "0000" + zdp_NWK_address_request(self, target_address, lookup_ieee) + + # Send a ping to the device + send_ping_to_device(self, NwkId) -def pingDevices(self, NwkId, health, checkHealthFlag, mainPowerFlag): +def ping_devices(self, NwkId, health, checkHealthFlag, mainPowerFlag): + """ + Manage the pinging of devices based on various conditions and configurations. + Args: + NwkId (str): The network ID of the device to ping. + health (bool): The health status of the device. + checkHealthFlag (bool): Flag indicating whether to perform a health check before pinging. + mainPowerFlag (bool): Flag indicating whether the device has main power. + + This method: + - Skips pinging if group ping is enabled. + - Logs device and retry information. + - Skips pinging based on main power, TuyaPing, or blacklist configurations. + - Avoids unnecessary pings for recently active devices. + - Handles retry logic for unhealthy devices. + - Pings devices if all conditions are met. + """ + # Check for group ping configuration if self.pluginconf.pluginConf["pingViaGroup"]: - self.log.logging( "Heartbeat", "Debug", "No direct pinDevices as Group ping is enabled" , NwkId, ) + self.log.logging("PingDevices", "Debug", "No direct ping_devices as Group ping is enabled", NwkId) return - - if "pingDeviceRetry" in self.ListOfDevices[NwkId]: - self.log.logging( "Heartbeat", "Debug", "------> pinDevices %s health: %s, checkHealth: %s, mainPower: %s, retry: %s" % ( - NwkId, health, checkHealthFlag, mainPowerFlag, self.ListOfDevices[NwkId]["pingDeviceRetry"]["Retry"]), NwkId, ) - else: - self.log.logging( "Heartbeat", "Debug", "------> pinDevices %s health: %s, checkHealth: %s, mainPower: %s" % ( - NwkId, health, checkHealthFlag, mainPowerFlag), NwkId, ) + # Log device and retry information + retry_info = self.ListOfDevices.get(NwkId, {}).get("pingDeviceRetry", {}) + retry = retry_info.get("Retry", "N/A") + self.log.logging( + "PingDevices", + "Debug", + f"------> ping_devices {NwkId} health: {health}, is_check_device_health_not_needed: {checkHealthFlag}, " + f"mainPower: {mainPowerFlag}, retry: {retry}", + NwkId, + ) + + # Skip if the device lacks main power if not mainPowerFlag: return - if ( - "Param" in self.ListOfDevices[NwkId] - and "TuyaPing" in self.ListOfDevices[NwkId]["Param"] - and int(self.ListOfDevices[NwkId]["Param"]["TuyaPing"]) == 1 - ): + # Skip based on TuyaPing configuration + params = self.ListOfDevices[NwkId].get("Param", {}) + if int(params.get("TuyaPing", 0)) == 1: self.log.logging( - "Heartbeat", + "PingDevices", "Debug", - "------> pingDevice disabled for %s as TuyaPing enabled %s" - % ( - NwkId, - self.ListOfDevices[NwkId]["Param"]["TuyaPing"], - ), + f"------> ping_devices disabled for {NwkId} as TuyaPing is enabled", NwkId, ) return - if ( - "Param" in self.ListOfDevices[NwkId] - and "pingBlackListed" in self.ListOfDevices[NwkId]["Param"] - and int(self.ListOfDevices[NwkId]["Param"]["pingBlackListed"]) == 1 - ): + # Skip based on ping blacklist configuration + if int(params.get("pingBlackListed", 0)) == 1: self.log.logging( - "Heartbeat", + "PingDevices", "Debug", - "------> pingDevice disabled for %s as pingBlackListed enabled %s" - % ( - NwkId, - self.ListOfDevices[NwkId]["Param"]["pingBlackListed"], - ), + f"------> ping_devices disabled for {NwkId} as pingBlackListed is enabled", NwkId, ) return now = int(time.time()) + stamp = self.ListOfDevices[NwkId].get("Stamp", {}) + plugin_ping_frequency = self.pluginconf.pluginConf["pingDevicesFeq"] + last_message_time = stamp.get("time", 0) - if ( - "time" in self.ListOfDevices[NwkId]["Stamp"] - and now < self.ListOfDevices[NwkId]["Stamp"]["time"] + self.pluginconf.pluginConf["pingDevicesFeq"] - ): - # If we have received a message since less than 1 hours, then no ping to be done ! - self.log.logging("Heartbeat", "Debug", "------> %s no need to ping as we received a message recently " % (NwkId,), NwkId) + # Skip if a message was received recently + if now < (last_message_time + plugin_ping_frequency): + self.log.logging( + "PingDevices", + "Debug", + f"------> ping_devices {NwkId} no need to ping as we received a message recently", + NwkId, + ) return + # Handle retry logic for unhealthy devices if not health: - pingRetryDueToBadHealth(self, NwkId) + retry_ping_device_in_bad_health(self, NwkId) return - if "LastPing" not in self.ListOfDevices[NwkId]["Stamp"]: - self.ListOfDevices[NwkId]["Stamp"]["LastPing"] = 0 - lastPing = self.ListOfDevices[NwkId]["Stamp"]["LastPing"] - lastSeen = self.ListOfDevices[NwkId]["Stamp"]["LastSeen"] - if checkHealthFlag and now > (lastPing + 60) and self.ControllerLink.loadTransmit() == 0: - submitPing(self, NwkId) + # Initialize LastPing if missing + last_ping = stamp.setdefault("LastPing", 0) + last_seen = stamp.get("LastSeen", 0) + + # Check and perform health ping if applicable + if checkHealthFlag and now > (last_ping + 60) and self.ControllerLink.loadTransmit() == 0: + send_ping_to_device(self, NwkId) return - self.log.logging( "Heartbeat", "Debug", "------> pinDevice %s time: %s LastPing: %s LastSeen: %s Freq: %s" % ( - NwkId, now, lastPing, lastSeen, self.pluginconf.pluginConf["pingDevicesFeq"]), NwkId, ) - if ( - (now > (lastPing + self.pluginconf.pluginConf["pingDevicesFeq"])) - and (now > (lastSeen + self.pluginconf.pluginConf["pingDevicesFeq"])) - and self.ControllerLink.loadTransmit() == 0 - ): + # Log details before the final ping decision + self.log.logging( + "PingDevices", + "Debug", + f"------> ping_devices {NwkId} time: {now}, LastPing: {last_ping}, " + f"LastSeen: {last_seen}, Freq: {plugin_ping_frequency}", + NwkId, + ) + + # Perform ping based on time and frequency + perform_ping = ( + (now > (last_ping + plugin_ping_frequency)) + and (now > (last_seen + plugin_ping_frequency)) + and (self.ControllerLink.loadTransmit() == 0) + ) - self.log.logging( "Heartbeat", "Debug", "------> pinDevice %s time: %s LastPing: %s LastSeen: %s Freq: %s" % ( - NwkId, now, lastPing, lastSeen, self.pluginconf.pluginConf["pingDevicesFeq"]), NwkId, ) + if perform_ping: + self.log.logging( + "PingDevices", + "Debug", + f"------> ping_devices {NwkId} time: {now}, LastPing: {last_ping}, " + f"LastSeen: {last_seen}, Freq: {plugin_ping_frequency}", + NwkId, + ) + send_ping_to_device(self, NwkId) - submitPing(self, NwkId) +def send_ping_to_device(self, NwkId): + """ + Send a ping to a device to verify its connectivity and update the last ping timestamp. -def submitPing(self, NwkId): - # Pinging devices to check they are still Alive - self.log.logging("Heartbeat", "Debug", "------------> call readAttributeRequest %s" % NwkId, NwkId) + Args: + NwkId (str): The network ID of the device to ping. + + This method: + - Logs the initiation of a ping operation. + - Updates the "LastPing" timestamp in the device's record. + - Calls the `ping_device_with_read_attribute` method to perform the actual ping operation. + """ + self.log.logging("PingDevices", "Debug", f"------------> send_ping_to_device - call readAttributeRequest {NwkId}", NwkId) self.ListOfDevices[NwkId]["Stamp"]["LastPing"] = int(time.time()) ping_device_with_read_attribute(self, NwkId) + def hr_process_device(self, Devices, NwkId): # Begin # Normalize Hearbeat value if needed @@ -627,11 +626,11 @@ def hr_process_device(self, Devices, NwkId): # Check if this is a Main powered device or Not. Source of information are: MacCapa and PowerSource _mainPowered = mainPoweredDevice(self, NwkId) _checkHealth = self.ListOfDevices[NwkId]["Health"] == "" - health = checkHealth(self, NwkId) + health = is_check_device_health_not_needed(self, NwkId) # Pinging devices to check they are still Alive - if self.pluginconf.pluginConf["pingDevices"]: - pingDevices(self, NwkId, health, _checkHealth, _mainPowered) + if self.pluginconf.pluginConf["CheckDeviceHealth"]: + ping_devices(self, NwkId, health, _checkHealth, _mainPowered) # Check if we are in the process of provisioning a new device. If so, just stop if self.CommiSSionning: @@ -776,54 +775,6 @@ def clear_last_polling_data(self, NwkId): self.ListOfDevices[NwkId].pop(key, None) -#def process_read_attributes(self, NwkId, model): -# self.log.logging( "Heartbeat", "Debug", f"process_read_attributes - for {NwkId} {model}") -# process_next_ep_later = False -# now = int(time.time()) # Will be used to trigger ReadAttributes -# -# device_infos = self.ListOfDevices[NwkId] -# for ep in device_infos["Ep"]: -# if ep == "ClusterType": -# continue -# -# if model == "lumi.ctrl_neutral1" and ep != "02" : # All Eps other than '02' are blacklisted -# continue -# -# if model == "lumi.ctrl_neutral2" and ep not in ("02", "03"): -# continue -# -# for Cluster in READ_ATTRIBUTES_REQUEST: -# # We process ALL available clusters for a particular EndPoint -# -# if ( Cluster not in READ_ATTRIBUTES_REQUEST or Cluster not in device_infos["Ep"][ep] ): -# continue -# -# if self.busy or self.ControllerLink.loadTransmit() > MAX_LOAD_ZIGATE: -# self.log.logging( "Heartbeat", "Debug", "process_read_attributes - %s skip ReadAttribute for now ... system too busy (%s/%s)" % ( -# NwkId, self.busy, self.ControllerLink.loadTransmit()), NwkId, ) -# process_next_ep_later = True -# -# if READ_ATTRIBUTES_REQUEST[Cluster][1] in self.pluginconf.pluginConf: -# timing = self.pluginconf.pluginConf[READ_ATTRIBUTES_REQUEST[Cluster][1]] -# else: -# self.log.logging( "Heartbeat", "Error", "proprocess_read_attributescessKnownDevices - missing timing attribute for Cluster: %s - %s" % ( -# Cluster, READ_ATTRIBUTES_REQUEST[Cluster][1]), NwkId ) -# continue -# -# # Let's check the timing -# if not is_time_to_perform_work(self, "ReadAttributes", NwkId, ep, Cluster, now, timing): -# continue -# -# self.log.logging( "Heartbeat", "Debug", "process_read_attributes - %s/%s and time to request ReadAttribute for %s" % ( -# NwkId, ep, Cluster), NwkId, ) -# -# func = READ_ATTRIBUTES_REQUEST[Cluster][0] -# func(self, NwkId) -# -# if process_next_ep_later: -# return True -# return False - def process_read_attributes(self, NwkId, model): self.log.logging("Heartbeat", "Debug", f"process_read_attributes - for {NwkId} {model}") now = int(time.time()) diff --git a/Modules/tools.py b/Modules/tools.py index aad9e3846..eadd8753e 100644 --- a/Modules/tools.py +++ b/Modules/tools.py @@ -421,15 +421,31 @@ def initDeviceInList(self, Nwkid): def timeStamped(self, key, Type): + """ + Updates the timestamp information for a device. + + Args: + key (str): The unique identifier (key) for the device in `ListOfDevices`. + Type (int): The message type or event type to be logged. + + This method: + - Ensures the device exists in `ListOfDevices` before updating. + - Initializes the "Stamp" field if it is missing. + - Updates the current time in both UNIX timestamp and formatted string formats. + - Logs the message type in hexadecimal format. + """ + # Ensure the device exists in ListOfDevices if key not in self.ListOfDevices: return - if "Stamp" not in self.ListOfDevices[key]: - self.ListOfDevices[key]["Stamp"] = {"LasteSeen": {}, "Time": {}, "MsgType": {}} - self.ListOfDevices[key]["Stamp"]["time"] = time.time() - self.ListOfDevices[key]["Stamp"]["Time"] = datetime.datetime.fromtimestamp(time.time()).strftime( - "%Y-%m-%d %H:%M:%S" - ) - self.ListOfDevices[key]["Stamp"]["MsgType"] = "%4x" % (Type) + + # Initialize the "Stamp" dictionary if not present + stamp = self.ListOfDevices[key].setdefault("Stamp", {"LastSeen": {}, "Time": {}, "MsgType": {}}) + + # Update the timestamp and message type + current_time = time.time() + stamp["time"] = current_time + stamp["Time"] = datetime.datetime.fromtimestamp(current_time).strftime("%Y-%m-%d %H:%M:%S") + stamp["MsgType"] = f"{Type:04x}" # Used by zcl/zdpRawCommands @@ -467,28 +483,38 @@ def updSQN(self, key, newSQN): def updLQI(self, key, LQI): + """ + Update the Link Quality Indicator (LQI) for a device. + + Args: + key (str): The unique identifier of the device. + LQI (str): The LQI value in hexadecimal format. + This function ensures the LQI value is updated and maintains a rolling history + of the last 10 LQI values for the device. + """ + # Ensure the device exists in the list if key not in self.ListOfDevices: return - if "LQI" not in self.ListOfDevices[key]: - self.ListOfDevices[key]["LQI"] = {} + # Initialize LQI fields if not present + device_info = self.ListOfDevices[key] + device_info.setdefault("LQI", {}) + device_info.setdefault("RollingLQI", []) - if LQI == "00": + # Skip invalid LQI values + if LQI == "00" or not is_hex(LQI): return - if is_hex(LQI): # Check if the LQI is Correct - - self.ListOfDevices[key]["LQI"] = int(LQI, 16) - - if "RollingLQI" not in self.ListOfDevices[key]: - self.ListOfDevices[key]["RollingLQI"] = [] - - if len(self.ListOfDevices[key]["RollingLQI"]) > 10: - del self.ListOfDevices[key]["RollingLQI"][0] - self.ListOfDevices[key]["RollingLQI"].append(int(LQI, 16)) + # Convert the LQI to an integer and update + lqi_value = int(LQI, 16) + device_info["LQI"] = lqi_value - return + # Maintain a rolling history of up to 10 LQI values + rolling_lqi = device_info["RollingLQI"] + if len(rolling_lqi) >= 10: + rolling_lqi.pop(0) # Remove the oldest value + rolling_lqi.append(lqi_value) def upd_RSSI(self, nwkid, rssi_value): diff --git a/Z4D_decoders/z4d_decoder_IEEE_Addr_Rsp.py b/Z4D_decoders/z4d_decoder_IEEE_Addr_Rsp.py index 18d7bf711..2902afe68 100644 --- a/Z4D_decoders/z4d_decoder_IEEE_Addr_Rsp.py +++ b/Z4D_decoders/z4d_decoder_IEEE_Addr_Rsp.py @@ -1,57 +1,105 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Implementation of Zigbee for Domoticz plugin. +# +# This file is part of Zigbee for Domoticz plugin. https://github.com/zigbeefordomoticz/Domoticz-Zigbee +# (C) 2015-2024 +# +# Initial authors: zaraki673 & pipiche38 +# +# SPDX-License-Identifier: GPL-3.0 license + from Modules.basicOutputs import handle_unknow_device -from Modules.domoTools import lastSeenUpdate from Modules.errorCodes import DisplayStatusCode -from Modules.tools import (DeviceExist, loggingMessages, timeStamped, +from Modules.tools import (DeviceExist, loggingMessages, zigpy_plugin_sanity_check) def Decode8041(self, Devices, MsgData, MsgLQI): + """ + Decode an 8041 IEEE Address response and process the received information. + + Args: + Devices (dict): Dictionary of devices managed by the system. + MsgData (str): The raw message data received from the network. + MsgLQI (str): Link Quality Indicator (LQI) of the received message. + + This method: + - Extracts and parses the IEEE address response. + - Logs the details of the response. + - Validates the response data against known devices. + - Updates timestamps and device records or handles unknown devices. + """ MsgSequenceNumber = MsgData[:2] MsgDataStatus = MsgData[2:4] MsgIEEE = MsgData[4:20] + + # Log and exit on invalid status if MsgDataStatus != '00': - self.log.logging('Input', 'Debug', 'Decode8041 - Reception of IEEE Address response for %s with status %s' % (MsgIEEE, MsgDataStatus)) + self.log.logging( 'Input', 'Debug', f"Decode8041 - Reception of IEEE Address response for {MsgIEEE} with status {MsgDataStatus}" ) return - + MsgShortAddress = MsgData[20:24] - extendedResponse = False - - if len(MsgData) > 24: - extendedResponse = True + extendedResponse = len(MsgData) > 24 + + if extendedResponse: MsgNumAssocDevices = MsgData[24:26] MsgStartIndex = MsgData[26:28] MsgDeviceList = MsgData[28:] - - if extendedResponse: - self.log.logging('Input', 'Debug', 'Decode8041 - IEEE Address response, Sequence number: %s Status: %s IEEE: %s NwkId: %s nbAssociated Devices: %s StartIdx: %s DeviceList: %s' % (MsgSequenceNumber, DisplayStatusCode(MsgDataStatus), MsgIEEE, MsgShortAddress, MsgNumAssocDevices, MsgStartIndex, MsgDeviceList)) + self.log.logging( + 'Input', 'Debug', + f"Decode8041 - IEEE Address response, Sequence number: {MsgSequenceNumber}, " + f"Status: {DisplayStatusCode(MsgDataStatus)}, IEEE: {MsgIEEE}, NwkId: {MsgShortAddress}, " + f"nbAssociated Devices: {MsgNumAssocDevices}, StartIdx: {MsgStartIndex}, DeviceList: {MsgDeviceList}" + ) - if MsgShortAddress == '0000' and self.ControllerIEEE and (MsgIEEE != self.ControllerIEEE): - self.log.logging('Input', 'Error', 'Decode8041 - Receive an IEEE: %s with a NwkId: %s something wrong !!!' % (MsgIEEE, MsgShortAddress)) + # Validate addresses and log inconsistencies + if MsgShortAddress == '0000' and self.ControllerIEEE and MsgIEEE != self.ControllerIEEE: + self.log.logging( + 'Input', 'Error', + f"Decode8041 - Received an IEEE: {MsgIEEE} with NwkId: {MsgShortAddress} - something is wrong!" + ) return - - elif self.ControllerIEEE and MsgIEEE == self.ControllerIEEE and (MsgShortAddress != '0000'): - self.log.logging('Input', 'Log', 'Decode8041 - Receive an IEEE: %s with a NwkId: %s something wrong !!!' % (MsgIEEE, MsgShortAddress)) + elif self.ControllerIEEE and MsgIEEE == self.ControllerIEEE and MsgShortAddress != '0000': + self.log.logging( + 'Input', 'Log', + f"Decode8041 - Received an IEEE: {MsgIEEE} with NwkId: {MsgShortAddress} - something is wrong!" + ) return - if MsgShortAddress in self.ListOfDevices and 'IEEE' in self.ListOfDevices[MsgShortAddress] and (self.ListOfDevices[MsgShortAddress]['IEEE'] == MsgShortAddress): - self.log.logging('Input', 'Debug', 'Decode8041 - Receive an IEEE: %s with a NwkId: %s' % (MsgIEEE, MsgShortAddress)) - timeStamped(self, MsgShortAddress, 32833) + # Handle known devices + if (MsgShortAddress in self.ListOfDevices + and 'IEEE' in self.ListOfDevices[MsgShortAddress] + and self.ListOfDevices[MsgShortAddress]['IEEE'] == MsgIEEE + ): + self.log.logging( + 'Input', 'Debug', + f"Decode8041 - Received an IEEE: {MsgIEEE} with NwkId: {MsgShortAddress}" + ) loggingMessages(self, '8041', MsgShortAddress, MsgIEEE, MsgLQI, MsgSequenceNumber) - lastSeenUpdate(self, Devices, NwkId=MsgShortAddress) return + # Handle reconnection for devices known by IEEE if MsgIEEE in self.IEEE2NWK: - self.log.logging('Input', 'Debug', 'Decode8041 - Receive an IEEE: %s with a NwkId: %s, will try to reconnect' % (MsgIEEE, MsgShortAddress)) - + self.log.logging( + 'Input', 'Debug', + f"Decode8041 - Received an IEEE: {MsgIEEE} with NwkId: {MsgShortAddress}, will try to reconnect" + ) if not DeviceExist(self, Devices, MsgShortAddress, MsgIEEE): if not zigpy_plugin_sanity_check(self, MsgShortAddress): handle_unknow_device(self, MsgShortAddress) - self.log.logging('Input', 'Log', 'Decode8041 - Not able to reconnect (unknown device) %s %s' % (MsgIEEE, MsgShortAddress)) + self.log.logging( + 'Input', 'Log', + f"Decode8041 - Unable to reconnect (unknown device) {MsgIEEE} {MsgShortAddress}" + ) return - timeStamped(self, MsgShortAddress, 32833) loggingMessages(self, '8041', MsgShortAddress, MsgIEEE, MsgLQI, MsgSequenceNumber) - lastSeenUpdate(self, Devices, NwkId=MsgShortAddress) return - self.log.logging('Input', 'Log', 'WARNING - Decode8041 - Receive an IEEE: %s with a NwkId: %s, not known by the plugin' % (MsgIEEE, MsgShortAddress)) \ No newline at end of file + # Handle unknown devices + self.log.logging( + 'Input', 'Log', + f"WARNING - Decode8041 - Received an IEEE: {MsgIEEE} with NwkId: {MsgShortAddress}, not known by the plugin" + ) diff --git a/Z4D_decoders/z4d_decoder_NWK_addr_req.py b/Z4D_decoders/z4d_decoder_NWK_addr_req.py index a26bc7636..14e815094 100644 --- a/Z4D_decoders/z4d_decoder_NWK_addr_req.py +++ b/Z4D_decoders/z4d_decoder_NWK_addr_req.py @@ -1,33 +1,67 @@ -import struct +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Implementation of Zigbee for Domoticz plugin. +# +# This file is part of Zigbee for Domoticz plugin. https://github.com/zigbeefordomoticz/Domoticz-Zigbee +# (C) 2015-2024 +# +# Initial authors: zaraki673 & pipiche38 +# +# SPDX-License-Identifier: GPL-3.0 license from Modules.sendZigateCommand import raw_APS_request def Decode0040(self, Devices, MsgData, MsgLQI): + """ + Decode a NWK address request (0040) and prepare a response. + + Args: + Devices (dict): The list of devices. + MsgData (str): The received message data. + MsgLQI (str): The Link Quality Indicator for the message. + + This function processes a NWK address request message, prepares the appropriate + response, and sends the response back. + """ + # Log incoming message details self.log.logging('Input', 'Debug', 'Decode0040 - NWK_addr_req: %s' % MsgData) + + # Extract relevant fields from MsgData sqn = MsgData[:2] srcNwkId = MsgData[2:6] srcEp = MsgData[6:8] ieee = MsgData[8:24] reqType = MsgData[24:26] startIndex = MsgData[26:28] - self.log.logging('Input', 'Debug', ' source req nwkid: %s' % srcNwkId) - self.log.logging('Input', 'Debug', ' request IEEE : %s' % ieee) - self.log.logging('Input', 'Debug', ' request Type : %s' % reqType) - self.log.logging('Input', 'Debug', ' request Idx : %s' % startIndex) + + # Log the extracted fields + self.log.logging('Input', 'Debug', f" source req nwkid: {srcNwkId}") + self.log.logging('Input', 'Debug', f" request IEEE : {ieee}") + self.log.logging('Input', 'Debug', f" request Type : {reqType}") + self.log.logging('Input', 'Debug', f" request Idx : {startIndex}") + + # Define the cluster ID for the request Cluster = '8000' + + # Prepare the payload based on the IEEE address if ieee == self.ControllerIEEE: - controller_ieee = '%016x' % struct.unpack('Q', struct.pack('>Q', int(self.ControllerIEEE, 16)))[0] - controller_nwkid = '%04x' % struct.unpack('H', struct.pack('>H', int(self.ControllerNWKID, 16)))[0] + controller_ieee = f'{int(self.ControllerIEEE, 16):016x}' + controller_nwkid = f'{int(self.ControllerNWKID, 16):04x}' status = '00' payload = sqn + status + controller_ieee + controller_nwkid + '00' elif ieee in self.IEEE2NWK: - device_ieee = '%016x' % struct.unpack('Q', struct.pack('>Q', int(ieee, 16)))[0] - device_nwkid = '%04x' % struct.unpack('H', struct.pack('>H', int(self.IEEE2NWK[ieee], 16)))[0] + device_ieee = f'{int(ieee, 16):016x}' + device_nwkid = f'{int(self.IEEE2NWK[ieee], 16):04x}' status = '00' payload = sqn + status + device_ieee + device_nwkid + '00' else: status = '81' payload = sqn + status + ieee - self.log.logging('Input', 'Debug', 'Decode0040 - response payload: %s' % payload) - raw_APS_request(self, srcNwkId, '00', Cluster, '0000', payload, zigpyzqn=sqn, zigate_ep='00') \ No newline at end of file + + # Log the response payload + self.log.logging('Input', 'Debug', f'Decode0040 - response payload: {payload}') + + # Send the response back using raw APS request + raw_APS_request(self, srcNwkId, '00', Cluster, '0000', payload, zigpyzqn=sqn, zigate_ep='00') diff --git a/Z4D_decoders/z4d_decoder_Node_Desc_Rsp.py b/Z4D_decoders/z4d_decoder_Node_Desc_Rsp.py index ea2f79506..fd6107fd8 100644 --- a/Z4D_decoders/z4d_decoder_Node_Desc_Rsp.py +++ b/Z4D_decoders/z4d_decoder_Node_Desc_Rsp.py @@ -1,94 +1,116 @@ -from Modules.tools import (ReArrangeMacCapaBasedOnModel, decodeMacCapa, updLQI) +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Implementation of Zigbee for Domoticz plugin. +# +# This file is part of Zigbee for Domoticz plugin. https://github.com/zigbeefordomoticz/Domoticz-Zigbee +# (C) 2015-2024 +# +# Initial authors: zaraki673 & pipiche38 +# +# SPDX-License-Identifier: GPL-3.0 license + +from Modules.tools import ReArrangeMacCapaBasedOnModel, decodeMacCapa, updLQI + def Decode8042(self, Devices, MsgData, MsgLQI): + """ + Decode an 8042 Node Descriptor response and update device information. + + Args: + Devices (dict): Dictionary of devices managed by the system. + MsgData (str): The raw message data received from the network. + MsgLQI (str): Link Quality Indicator (LQI) of the received message. + + This method parses the node descriptor data and updates the device records with + relevant information such as manufacturer, capabilities, and logical type. + """ sequence = MsgData[:2] status = MsgData[2:4] addr = MsgData[4:8] - if status == '00': - manufacturer = MsgData[8:12] - max_rx = MsgData[12:16] - max_tx = MsgData[16:20] - server_mask = MsgData[20:24] - descriptor_capability = MsgData[24:26] - mac_capability = MsgData[26:28] - max_buffer = MsgData[28:30] - bit_field = MsgData[30:34] + + # Handle invalid status codes if status != '00': - self.log.logging('Input', 'Debug', 'Decode8042 - Reception of Node Descriptor for %s with status %s' % (addr, status)) + self.log.logging( + 'Input', 'Debug', + f"Decode8042 - Reception of Node Descriptor for {addr} with status {status}" + ) return - - self.log.logging('Input', 'Debug', 'Decode8042 - Reception Node Descriptor for: ' + addr + ' SEQ: ' + sequence + ' Status: ' + status + ' manufacturer:' + manufacturer + ' mac_capability: ' + str(mac_capability) + ' bit_field: ' + str(bit_field), addr) + + # Extract descriptor details + manufacturer = MsgData[8:12] + max_rx = MsgData[12:16] + max_tx = MsgData[16:20] + server_mask = MsgData[20:24] + descriptor_capability = MsgData[24:26] + mac_capability = MsgData[26:28] + max_buffer = MsgData[28:30] + bit_field = MsgData[30:34] + + self.log.logging( + 'Input', 'Debug', + f"Decode8042 - Reception Node Descriptor for: {addr}, SEQ: {sequence}, " + f"Status: {status}, Manufacturer: {manufacturer}, MAC Capability: {mac_capability}, Bit Field: {bit_field}", + addr + ) + + # Initialize device record if not present if addr == '0000' and addr not in self.ListOfDevices: - self.ListOfDevices[addr] = {} - self.ListOfDevices[addr]['Ep'] = {} + self.ListOfDevices[addr] = {'Ep': {}} if addr not in self.ListOfDevices: - self.log.logging('Input', 'Log', 'Decode8042 receives a message from a non existing device %s' % addr) + self.log.logging( + 'Input', 'Log', + f"Decode8042 received a message from a non-existing device {addr}" + ) return - + + # Update device details updLQI(self, addr, MsgLQI) - - self.ListOfDevices[addr]['_rawNodeDescriptor'] = MsgData[8:] - self.ListOfDevices[addr]['Max Buffer Size'] = max_buffer - self.ListOfDevices[addr]['Max Rx'] = max_rx - self.ListOfDevices[addr]['Max Tx'] = max_tx - self.ListOfDevices[addr]['macapa'] = mac_capability - self.ListOfDevices[addr]['bitfield'] = bit_field - self.ListOfDevices[addr]['server_mask'] = server_mask - self.ListOfDevices[addr]['descriptor_capability'] = descriptor_capability + self.ListOfDevices[addr].update({ + '_rawNodeDescriptor': MsgData[8:], + 'Max Buffer Size': max_buffer, + 'Max Rx': max_rx, + 'Max Tx': max_tx, + 'macapa': mac_capability, + 'bitfield': bit_field, + 'server_mask': server_mask, + 'descriptor_capability': descriptor_capability, + }) + + # Rearrange MAC capability and decode capabilities mac_capability = ReArrangeMacCapaBasedOnModel(self, addr, mac_capability) capabilities = decodeMacCapa(mac_capability) - - if 'Able to act Coordinator' in capabilities: - AltPAN = 1 - - else: - AltPAN = 0 - - if 'Main Powered' in capabilities: - PowerSource = 'Main' - - else: - PowerSource = 'Battery' - - if 'Full-Function Device' in capabilities: - DeviceType = 'FFD' - - else: - DeviceType = 'RFD' - - if 'Receiver during Idle' in capabilities: - ReceiveonIdle = 'On' - - else: - ReceiveonIdle = 'Off' - - self.log.logging('Input', 'Debug', 'Decode8042 - Alternate PAN Coordinator = ' + str(AltPAN), addr) - self.log.logging('Input', 'Debug', 'Decode8042 - Receiver on Idle = ' + str(ReceiveonIdle), addr) - self.log.logging('Input', 'Debug', 'Decode8042 - Power Source = ' + str(PowerSource), addr) - self.log.logging('Input', 'Debug', 'Decode8042 - Device type = ' + str(DeviceType), addr) + + # Determine device properties + AltPAN = 'Able to act Coordinator' in capabilities + PowerSource = 'Main' if 'Main Powered' in capabilities else 'Battery' + DeviceType = 'FFD' if 'Full-Function Device' in capabilities else 'RFD' + ReceiveOnIdle = 'On' if 'Receiver during Idle' in capabilities else 'Off' + + # Log device properties + self.log.logging('Input', 'Debug', f"Decode8042 - Alternate PAN Coordinator = {AltPAN}", addr) + self.log.logging('Input', 'Debug', f"Decode8042 - Receiver on Idle = {ReceiveOnIdle}", addr) + self.log.logging('Input', 'Debug', f"Decode8042 - Power Source = {PowerSource}", addr) + self.log.logging('Input', 'Debug', f"Decode8042 - Device Type = {DeviceType}", addr) + + # Parse bit fields to determine logical type bit_fieldL = int(bit_field[2:4], 16) bit_fieldH = int(bit_field[:2], 16) - self.log.logging('Input', 'Debug', 'Decode8042 - bit_fieldL = %s bit_fieldH = %s' % (bit_fieldL, bit_fieldH)) - LogicalType = bit_fieldL & 15 - - if LogicalType == 0: - LogicalType = 'Coordinator' - - elif LogicalType == 1: - LogicalType = 'Router' - - elif LogicalType == 2: - LogicalType = 'End Device' - - self.log.logging('Input', 'Debug', 'Decode8042 - bit_field = ' + str(bit_fieldL) + ': ' + str(bit_fieldH), addr) - self.log.logging('Input', 'Debug', 'Decode8042 - Logical Type = ' + str(LogicalType), addr) - - if 'Manufacturer' not in self.ListOfDevices[addr] or self.ListOfDevices[addr]['Manufacturer'] in ('', {}): - self.ListOfDevices[addr]['Manufacturer'] = manufacturer - - if 'Status' not in self.ListOfDevices[addr] or self.ListOfDevices[addr]['Status'] != 'inDB': - self.ListOfDevices[addr]['Manufacturer'] = manufacturer - self.ListOfDevices[addr]['DeviceType'] = str(DeviceType) - self.ListOfDevices[addr]['LogicalType'] = str(LogicalType) - self.ListOfDevices[addr]['PowerSource'] = str(PowerSource) - self.ListOfDevices[addr]['ReceiveOnIdle'] = str(ReceiveonIdle) \ No newline at end of file + LogicalType = ['Coordinator', 'Router', 'End Device'][bit_fieldL & 15] if (bit_fieldL & 15) < 3 else 'Unknown' + + self.log.logging('Input', 'Debug', f"Decode8042 - bit_field = {bit_fieldL}:{bit_fieldH}", addr) + self.log.logging('Input', 'Debug', f"Decode8042 - Logical Type = {LogicalType}", addr) + + # Update or initialize device attributes + device_record = self.ListOfDevices[addr] + device_record.setdefault('Manufacturer', manufacturer) + if device_record.get('Status') != 'inDB': + device_record.update( + { + 'Manufacturer': manufacturer, + 'DeviceType': DeviceType, + 'LogicalType': str(LogicalType), + 'PowerSource': PowerSource, + 'ReceiveOnIdle': ReceiveOnIdle, + } + ) diff --git a/Z4D_decoders/z4d_decoder_Nwk_Addr_Rsp.py b/Z4D_decoders/z4d_decoder_Nwk_Addr_Rsp.py index fa761f4ee..5f7afd734 100644 --- a/Z4D_decoders/z4d_decoder_Nwk_Addr_Rsp.py +++ b/Z4D_decoders/z4d_decoder_Nwk_Addr_Rsp.py @@ -1,7 +1,20 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Implementation of Zigbee for Domoticz plugin. +# +# This file is part of Zigbee for Domoticz plugin. https://github.com/zigbeefordomoticz/Domoticz-Zigbee +# (C) 2015-2024 +# +# Initial authors: zaraki673 & pipiche38 +# +# SPDX-License-Identifier: GPL-3.0 license + + from Modules.basicOutputs import handle_unknow_device from Modules.domoTools import lastSeenUpdate from Modules.errorCodes import DisplayStatusCode -from Modules.tools import (DeviceExist, loggingMessages, timeStamped, +from Modules.tools import (DeviceExist, loggingMessages, zigpy_plugin_sanity_check) from Modules.zb_tables_management import store_NwkAddr_Associated_Devices from Z4D_decoders.z4d_decoder_helpers import \ @@ -40,7 +53,6 @@ def Decode8040(self, Devices, MsgData, MsgLQI): if extendedResponse: store_NwkAddr_Associated_Devices(self, MsgShortAddress, MsgStartIndex, MsgDeviceList) - timeStamped(self, MsgShortAddress, 32833) loggingMessages(self, '8040', MsgShortAddress, MsgIEEE, MsgLQI, MsgSequenceNumber) lastSeenUpdate(self, Devices, NwkId=MsgShortAddress) return @@ -58,7 +70,6 @@ def Decode8040(self, Devices, MsgData, MsgLQI): if extendedResponse: store_NwkAddr_Associated_Devices(self, MsgShortAddress, MsgStartIndex, MsgDeviceList) - timeStamped(self, MsgShortAddress, 32833) loggingMessages(self, '8040', MsgShortAddress, MsgIEEE, MsgLQI, MsgSequenceNumber) lastSeenUpdate(self, Devices, NwkId=MsgShortAddress) diff --git a/Z4D_decoders/z4d_decoder_Zigate_Cmd_Rsp.py b/Z4D_decoders/z4d_decoder_Zigate_Cmd_Rsp.py index f3a92075e..05707ee68 100644 --- a/Z4D_decoders/z4d_decoder_Zigate_Cmd_Rsp.py +++ b/Z4D_decoders/z4d_decoder_Zigate_Cmd_Rsp.py @@ -63,9 +63,11 @@ def Decode8000_v2(self, Devices, MsgData, MsgLQI): def Decode8011(self, Devices, MsgData, MsgLQI, TransportInfos=None): self.log.logging('Input', 'Debug', 'Decode8011 - APS ACK: %s' % MsgData) MsgLen = len(MsgData) + if MsgLen > 6: + self.log.logging('Input', 'Error', f"Decode8011 - unexpected payload received {MsgData}") MsgStatus = MsgData[:2] MsgSrcAddr = MsgData[2:6] - + if MsgSrcAddr not in self.ListOfDevices: if not zigpy_plugin_sanity_check(self, MsgSrcAddr): self.log.logging('Input', 'Debug', f"Decode8011 - not zigpy_plugin_sanity_check {MsgSrcAddr} {MsgStatus}") @@ -97,11 +99,10 @@ def Decode8011(self, Devices, MsgData, MsgLQI, TransportInfos=None): if not _powered: return - + self.log.logging('Input', 'Debug', f"Decode8011 - Timedout {MsgSrcAddr}") timedOutDevice(self, Devices, NwkId=MsgSrcAddr) - set_health_state(self, MsgSrcAddr, MsgData[8:12], MsgStatus)