diff --git a/Classes/PluginConf.py b/Classes/PluginConf.py index fe94a38e2..743c7bb19 100644 --- a/Classes/PluginConf.py +++ b/Classes/PluginConf.py @@ -391,7 +391,6 @@ "nPDUaPDUThreshold": {"type": "bool","default": 0,"current": None,"restart": 0,"hidden": True,"Advanced": True,"ZigpyRadio": ""}, "forceAckOnZCL": {"type": "bool","default": 0,"current": None,"restart": 0,"hidden": True,"Advanced": True,}, "DropBadAnnoucement": {"type": "bool","default": 1,"current": None,"restart": 0,"hidden": True,"Advanced": True,}, - "expJsonDatabase": {"type": "bool","default": 1,"current": None,"restart": 0,"hidden": True,"Advanced": True,}, "TryFindingIeeeOfUnknownNwkid": {"type": "bool","default": 0,"current": None,"restart": 0,"hidden": True,"Advanced": True,}, "enableZigpyPersistentInFile": {"type": "bool","default": 0,"current": None,"restart": 1,"hidden": False,"Advanced": True,}, "enableZigpyPersistentInMemory": {"type": "bool","default": 1,"current": None,"restart": 1,"hidden": False,"Advanced": True,}, @@ -407,7 +406,6 @@ "allowAutoPairing": {"type": "bool","default": 0,"current": None,"restart": 0,"hidden": True,"Advanced": True,}, "disabledDefaultResponseFirmware": {"type": "bool","default": 0,"current": None,"restart": 1,"hidden": True,"Advanced": True,}, "logUnknownDeviceModel": {"type": "bool","default": 1,"current": None,"restart": 0,"hidden": True,"Advanced": True,}, - "expJsonDatabase": {"type": "bool","default": 0,"current": None,"restart": 0,"hidden": True,"Advanced": True,}, "forceAckOnZCL": {"type": "bool","default": 0,"current": None,"restart": 0,"hidden": True,"Advanced": True,}, "ControllerInHybridMode": {"type": "bool","default": 0,"current": None,"restart": 0,"hidden": True,"Advanced": True,}, "ControllerInRawMode": {"type": "bool","default": 0,"current": None,"restart": 0,"hidden": True,"Advanced": True,}, diff --git a/Classes/TransportStats.py b/Classes/TransportStats.py index 5b29c7f54..acbbf2f76 100644 --- a/Classes/TransportStats.py +++ b/Classes/TransportStats.py @@ -12,12 +12,10 @@ import json -from time import time - -from Modules.domoticzAbstractLayer import (domoticz_error_api, - domoticz_log_api, - domoticz_status_api) +import time +from pathlib import Path +from Modules.domoticzAbstractLayer import domoticz_log_api, domoticz_status_api class TransportStatistics: def __init__(self, pluginconf, log, zigbee_communication): @@ -49,7 +47,7 @@ def __init__(self, pluginconf, log, zigbee_communication): self._maxRxProcesses = self._cumulRxProcess = self._cntRxProcess = self._averageRxProcess = 0 self._max_reading_thread_timing = self._cumul_reading_thread_timing = self._cnt_reading_thread_timing = self._average_reading_thread_timing = 0 self._max_reading_zigpy_timing = self._cumul_reading_zigpy_timing = self._cnt_reading_zigpy_timing = self._average_reading_zigpy_timing = 0 - self._start = int(time()) + self._start = int(time.time()) self.TrendStats = [] self.pluginconf = pluginconf self.log = log @@ -59,12 +57,15 @@ def __init__(self, pluginconf, log, zigbee_communication): def starttime(self): return self._start + def pdm_loaded(self): self._pdmLoads += 1 + def get_pdm_loaded(self): return self._pdmLoads + def add_timing_zigpy(self, timing): self._cumul_reading_zigpy_timing += timing self._cnt_reading_zigpy_timing += 1 @@ -85,6 +86,7 @@ def add_timing_thread(self, timing): % (self._max_reading_thread_timing, self._average_reading_thread_timing) ) + def add_timing8000(self, timing): self._cumulTiming8000 += timing @@ -97,6 +99,7 @@ def add_timing8000(self, timing): % (self._maxTiming8000, self._averageTiming8000) ) + def add_timing8011(self, timing): self._cumulTiming8011 += timing @@ -109,6 +112,7 @@ def add_timing8011(self, timing): % (self._maxTiming8011, self._averageTiming8011) ) + def add_timing8012(self, timing): self._cumulTiming8012 += timing @@ -121,6 +125,7 @@ def add_timing8012(self, timing): % (self._maxTiming8012, self._averageTiming8012) ) + def add_rxTiming(self, timing): self._cumulRxProcess += timing @@ -133,151 +138,224 @@ def add_rxTiming(self, timing): % (self._maxRxProcesses, self._averageRxProcess) ) + def addPointforTrendStats(self, TimeStamp): + """ + Adds a point to the trend statistics table, tracking Rx, Tx, and Load metrics. + + Args: + TimeStamp (int): The timestamp for the data point. + Note: + The table is capped at MAX_TREND_STAT_TABLE entries, with the oldest entry removed when the limit is reached. + """ MAX_TREND_STAT_TABLE = 120 - uptime = int(time() - self._start) - Rxps = round(self._received / uptime, 2) - Txps = round(self._sent / uptime, 2) - if len(self.TrendStats) >= MAX_TREND_STAT_TABLE: - del self.TrendStats[0] - self.TrendStats.append({"_TS": TimeStamp, "Rxps": Rxps, "Txps": Txps, "Load": self._Load}) + try: + # Calculate uptime and transmission rates + uptime = int(time.time() - self._start) + if uptime <= 0: + self.log.logging("Stats", "Error", "Invalid uptime calculation: uptime must be greater than 0.") + return + + Rxps = round(self._received / uptime, 2) + Txps = round(self._sent / uptime, 2) + + # Maintain the size of the TrendStats table + if len(self.TrendStats) >= MAX_TREND_STAT_TABLE: + self.TrendStats.pop(0) + + # Append the new data point + self.TrendStats.append({ + "_TS": TimeStamp, + "Rxps": Rxps, + "Txps": Txps, + "Load": self._Load + }) + + except Exception as e: + self.log.logging("Stats", "Error", f"Failed to add point to trend stats: {e}") + def reTx(self): """ return the number of crc Errors """ return self._reTx + def crcErrors(self): " return the number of crc Errors " return self._crcErrors + def frameErrors(self): " return the number of frame errors" return self._frameErrors + def sent(self): " return he number of sent messages" return self._sent + def received(self): " return the number of received messages" return self._received + def ackReceived(self): return self._ack + def ackKOReceived(self): return self._ackKO + def dataReceived(self): return self._data + def TOstatus(self): return self._TOstatus + def TOdata(self): return self._TOdata + def clusterOK(self): return self._clusterOK + def clusterKO(self): return self._clusterKO + def APSFailure(self): return self._APSFailure + def APSAck(self): return self._APSAck + def APSNck(self): return self._APSNck + def printSummary(self): - if self.received() == 0: - return - if self.sent() == 0 or self.received() == 0: + """ + Prints a summary of plugin statistics, including transmission, + reception, and timing metrics. + """ + if self.received() == 0 or self.sent() == 0: return + + def print_with_percentage(label, value, total): + percentage = round((value / total) * 100, 2) + domoticz_status_api(f"{label}: {value} ({percentage}%)") + domoticz_status_api("Plugin statistics") domoticz_status_api(" Messages Sent:") - domoticz_status_api(" Max Load (Queue) : %s " % (self._MaxLoad)) - domoticz_status_api(" TX commands : %s" % (self.sent())) - domoticz_status_api(" TX failed : %s (%s" % (self.ackKOReceived(), round((self.ackKOReceived() / self.sent()) * 10, 2))+ "%)") + domoticz_status_api(f" Max Load (Queue) : {self._MaxLoad}") + domoticz_status_api(f" TX commands : {self.sent()}") + + print_with_percentage(" TX failed", self.ackKOReceived(), self.sent()) if self.zigbee_communication == "native": - domoticz_status_api(" TX timeout : %s (%s" % (self.TOstatus(), round((self.TOstatus() / self.sent()) * 100, 2)) + "%)") + print_with_percentage(" TX timeout", self.TOstatus(), self.sent()) - domoticz_status_api(" TX data timeout : %s (%s" % (self.TOdata(), round((self.TOdata() / self.sent()) * 100, 2)) + "%)") - domoticz_status_api(" TX reTransmit : %s (%s" % (self.reTx(), round((self.reTx() / self.sent()) * 100, 2)) + "%)") + print_with_percentage(" TX data timeout", self.TOdata(), self.sent()) + print_with_percentage(" TX reTransmit", self.reTx(), self.sent()) if self.zigbee_communication == "native": - domoticz_status_api(" TX APS Failure : %s (%s" % (self.APSFailure(), round((self.APSFailure() / self.sent()) * 100, 2))+ "%)") + print_with_percentage(" TX APS Failure", self.APSFailure(), self.sent()) - domoticz_status_api(" TX APS Ack : %s (%s" % (self.APSAck(), round((self.APSAck() / self.sent()) * 100, 2)) + "%)") - domoticz_status_api(" TX APS Nck : %s (%s" % (self.APSNck(), round((self.APSNck() / self.sent()) * 100, 2)) + "%)") + print_with_percentage(" TX APS Ack", self.APSAck(), self.sent()) + print_with_percentage(" TX APS Nck", self.APSNck(), self.sent()) domoticz_status_api(" Messages Received:") - domoticz_status_api(" RX frame : %s" % (self.received())) - domoticz_status_api(" RX clusters : %s" % (self.clusterOK())) - domoticz_status_api(" RX clusters KO : %s" % (self.clusterKO())) + domoticz_status_api(f" RX frame : {self.received()}") + domoticz_status_api(f" RX clusters : {self.clusterOK()}") + domoticz_status_api(f" RX clusters KO : {self.clusterKO()}") if self.zigbee_communication == "native": domoticz_status_api(" Coordinator reacting time on Tx (if ReactTime enabled)") - domoticz_status_api(" Max : %s sec" % (self._maxTiming8000)) - domoticz_status_api(" Average : %s sec" % (self._averageTiming8000)) - + domoticz_status_api(f" Max : {self._maxTiming8000} sec") + domoticz_status_api(f" Average : {self._averageTiming8000} sec") else: domoticz_status_api(" Plugin reacting time on Tx (if ReactTime enabled)") - domoticz_status_api(" Max : %s ms" % (self._max_reading_zigpy_timing)) - domoticz_status_api(" Average : %s ms" % (self._average_reading_zigpy_timing)) + domoticz_status_api(f" Max : {self._max_reading_zigpy_timing} ms") + domoticz_status_api(f" Average : {self._average_reading_zigpy_timing} ms") domoticz_status_api(" Plugin processing time on Rx (if ReactTime enabled)") - if self.zigbee_communication == "native": - domoticz_status_api(" Max : %s sec" % (self._maxRxProcesses)) - domoticz_status_api(" Average : %s sec" % (self._averageRxProcess)) - else: - domoticz_status_api(" Max : %s ms" % (self._maxRxProcesses)) - domoticz_status_api(" Average : %s ms" % (self._averageRxProcess)) - - t0 = self.starttime() - t1 = int(time()) - _days = 0 - _duration = t1 - t0 - _hours = _duration // 3600 - _duration = _duration % 3600 - if _hours >= 24: - _days = _hours // 24 - _hours = _hours % 24 - _min = _duration // 60 - _duration = _duration % 60 - _sec = _duration % 60 - domoticz_status_api(" Operating time : %s Hours %s Mins %s Secs" % (_hours, _min, _sec)) + timing_unit = "sec" if self.zigbee_communication == "native" else "ms" + domoticz_status_api(f" Max : {self._maxRxProcesses} {timing_unit}") + domoticz_status_api(f" Average : {self._averageRxProcess} {timing_unit}") + days, hours, mins, secs = _plugin_uptime(self.starttime()) + domoticz_status_api(" Operating time : %d Days %d Hours %d Mins %d Secs" % (days, hours, mins, secs)) - def writeReport(self): - timing = int(time()) - stats = {timing: {}} - stats[timing]["crcErrors"] = self._crcErrors - stats[timing]["frameErrors"] = self._frameErrors - stats[timing]["sent"] = self._sent - stats[timing]["received"] = self._received - stats[timing]["APS Ack"] = self._APSAck - stats[timing]["APS Nck"] = self._APSNck - stats[timing]["ack"] = self._ack - stats[timing]["ackKO"] = self._ackKO - stats[timing]["data"] = self._data - stats[timing]["TOstatus"] = self._TOstatus - stats[timing]["TOdata"] = self._TOdata - stats[timing]["clusterOK"] = self._clusterOK - stats[timing]["clusterKO"] = self._clusterKO - stats[timing]["reTx"] = self._reTx - stats[timing]["MaxLoad"] = self._MaxLoad - stats[timing]["start"] = self._start - stats[timing]["stop"] = timing - - json_filename = self.pluginconf.pluginConf["pluginReports"] + "Transport-stats.json" - with open(json_filename, "at") as json_file: - json_file.write("\n") - json.dump(stats, json_file) + def writeReport(self): + """ + Write transport statistics to a JSON file. + """ + # Collect the current timestamp + current_time = int(time.time()) + + # Prepare stats dictionary + stats = { + current_time: { + "crcErrors": self._crcErrors, + "frameErrors": self._frameErrors, + "sent": self._sent, + "received": self._received, + "APS Ack": self._APSAck, + "APS Nck": self._APSNck, + "ack": self._ack, + "ackKO": self._ackKO, + "data": self._data, + "TOstatus": self._TOstatus, + "TOdata": self._TOdata, + "clusterOK": self._clusterOK, + "clusterKO": self._clusterKO, + "reTx": self._reTx, + "MaxLoad": self._MaxLoad, + "start": self._start, + "stop": current_time, + } + } + + # Construct the JSON file path + json_filename = Path(self.pluginconf.pluginConf["pluginReports"]) / "Transport-stats.json" + + try: + # Append statistics to the JSON file + with open(json_filename, "a") as json_file: # Use 'a' for appending + json_file.write("\n") + json.dump(stats, json_file, indent=4) # Add indent for better readability + except Exception as e: + self.log.logging("Plugin", "Error", f"Failed to write transport stats: {e}") + + +def _plugin_uptime(starttime): + """ + Calculates the uptime since the given start time. + + Args: + starttime (int): The start time in seconds since the epoch. + + Returns: + tuple: Uptime in days, hours, minutes, and seconds. + """ + t1 = int(time.time()) + _duration = t1 - starttime + + _days = _duration // (24 * 3600) + _duration %= 24 * 3600 + _hours = _duration // 3600 + _duration %= 3600 + _mins = _duration // 60 + _secs = _duration % 60 + + return _days, _hours, _mins, _secs diff --git a/Tools/extract_configuration_record.py b/Tools/extract_configuration_record.py old mode 100644 new mode 100755 index c339d01fe..5b76de52d --- a/Tools/extract_configuration_record.py +++ b/Tools/extract_configuration_record.py @@ -1,3 +1,15 @@ +#!/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: badz & pipiche38 +# +# SPDX-License-Identifier: GPL-3.0 license + import sqlite3 import json import base64 @@ -22,6 +34,7 @@ def decode_base64_to_dict(b64_data): print("Warning: Could not decode base64 or parse JSON") return {} + def decode_configuration(config_str, main_attribut=None): """ Decodes the Configuration JSON string and processes its fields based on known Attribut values. @@ -71,6 +84,7 @@ def decode_configuration(config_str, main_attribut=None): print("Warning: Could not decode Configuration JSON") return {} + def fetch_hardware_records(database_path, main_attribut=None): """ Connects to the SQLite database, retrieves records from the Hardware table,