diff --git a/tdmgr/GUI/dialogs/main.py b/tdmgr/GUI/dialogs/main.py index f4b9309..312d524 100644 --- a/tdmgr/GUI/dialogs/main.py +++ b/tdmgr/GUI/dialogs/main.py @@ -1,7 +1,6 @@ import csv import logging import re -from logging.handlers import TimedRotatingFileHandler from PyQt5.QtCore import QDir, QFileInfo, QSettings, QSize, Qt, QTimer, QUrl, pyqtSlot from PyQt5.QtGui import QDesktopServices, QFont, QIcon @@ -34,6 +33,8 @@ from tdmgr.util.discovery import lwt_discovery_stage2 from tdmgr.util.mqtt import DEFAULT_PATTERNS, Message, MqttClient, expand_fulltopic +log = logging.getLogger(__name__) + class MainWindow(QMainWindow): def __init__( @@ -41,7 +42,6 @@ def __init__( version: str, settings: QSettings, devices: QSettings, - log_path: str, debug: bool, *args, **kwargs, @@ -69,17 +69,6 @@ def __init__( self.devices = devices self.setMinimumSize(QSize(1000, 600)) - # configure logging - logging.basicConfig( - level="DEBUG" if debug else "INFO", - datefmt="%Y-%m-%d %H:%M:%S", - format="%(asctime)s [%(levelname)s] %(message)s", - ) - logging.getLogger().addHandler( - TimedRotatingFileHandler(filename=log_path, when="d", interval=1) - ) - logging.info("### TDM START ###") - # load devices from the devices file, create TasmotaDevices and add the to the environment for mac in self.devices.childGroups(): self.devices.beginGroup(mac) @@ -129,6 +118,7 @@ def __init__( self.tele_docks = [] self.consoles = [] + log.info(f"### TDM {self._version} START ###") def setup_main_layout(self): self.mdi = QMdiArea() @@ -352,7 +342,7 @@ def mqtt_message(self, msg: Message): # try: # obj = DiscoverySchema.model_validate_json(msg.payload) # except ValueError: - # logging.error("Unable to parse Tasmota discovery message: %s", msg.payload) + # log.error("Unable to parse Tasmota discovery message: %s", msg.payload) # # if obj and not self.env.find_device(obj.t): # device = TasmotaDevice.from_discovery(obj) @@ -363,7 +353,7 @@ def mqtt_message(self, msg: Message): # # self.env.devices.append(device) # self.device_model.addDevice(device) - # logging.info( + # log.info( # "DISCOVERY(NATIVE): Discovered topic=%s with fulltopic=%s", # obj.t, # device.p["FullTopic"], @@ -375,7 +365,7 @@ def mqtt_message(self, msg: Message): if device := self.env.find_device(msg.topic): if msg.is_lwt: - logging.debug("MQTT: LWT message for %s: %s", device.p["Topic"], msg.payload) + log.debug("MQTT: LWT message for %s: %s", device.p["Topic"], msg.payload) device.update_property("LWT", msg.payload) if msg.payload == device.p["Online"]: @@ -396,7 +386,7 @@ def mqtt_message(self, msg: Message): # unknown device, start autodiscovery process if msg.is_lwt: self.env.lwts[msg.topic] = msg.payload - logging.info("DISCOVERY(LEGACY): LWT from an unknown device %s", msg.topic) + log.info("DISCOVERY(LEGACY): LWT from an unknown device %s", msg.topic) # STAGE 1 # load default and user-provided FullTopic patterns and for all the patterns, @@ -418,7 +408,7 @@ def mqtt_message(self, msg: Message): "/LWT", "/FullTopic" ) ) - logging.debug( + log.debug( "DISCOVERY(LEGACY): Asking an unknown device for FullTopic at %s", possible_topic_cmnd, ) @@ -429,7 +419,7 @@ def mqtt_message(self, msg: Message): if d := lwt_discovery_stage2(self.env, msg): self.env.devices.append(d) self.device_model.addDevice(d) - logging.debug("DISCOVERY: Sending initial query to topic %s", d.p["Topic"]) + log.debug("DISCOVERY: Sending initial query to topic %s", d.p["Topic"]) self.initial_query(d, True) tele_topic = d.tele_topic("LWT") self.env.lwts.pop(tele_topic, None) @@ -496,7 +486,7 @@ def clear_retained_topics(self): topic = itm.text() self.mqtt.publish(topic, retain=True) self.env.retained.remove(topic) - logging.info("MQTT: Cleared %s", topic) + log.info("MQTT: Cleared %s", topic) def prefs(self): dlg = PrefsDialog(self.settings) @@ -535,7 +525,7 @@ def open_config_file(self): @staticmethod def open_log_location(): - fi = QFileInfo(logging.getLogger().handlers[1].baseFilename) + fi = QFileInfo(log.getLogger().handlers[1].baseFilename) QDesktopServices.openUrl(QUrl.fromLocalFile(fi.absolutePath())) def auto_telemetry_period(self): diff --git a/tdmgr/run.py b/tdmgr/run.py index 22b0156..43e2194 100644 --- a/tdmgr/run.py +++ b/tdmgr/run.py @@ -4,6 +4,7 @@ import os import pathlib import sys +from logging.handlers import TimedRotatingFileHandler from PyQt5.QtCore import QDir, QSettings from PyQt5.QtWidgets import QApplication @@ -17,31 +18,33 @@ version = "" -def get_settings(args): - if args.local: - return QSettings("tdm.cfg", QSettings.IniFormat) - if args.config_location: - return QSettings(os.path.join(args.config_location, "tdm.cfg"), QSettings.IniFormat) - return QSettings(QSettings.IniFormat, QSettings.UserScope, "tdm", "tdm") +def configure_logging(args) -> None: + log_path = os.path.join(QDir.tempPath(), "tdm.log") - -def get_devices(args): if args.local: - return QSettings("devices.cfg", QSettings.IniFormat) - if args.config_location: - return QSettings(os.path.join(args.config_location, "devices.cfg"), QSettings.IniFormat) - return QSettings(QSettings.IniFormat, QSettings.UserScope, "tdm", "devices") + log_path = "tdm.log" + elif args.log_location: + log_path = os.path.join(args.log_location, "tdm.log") + + logging.basicConfig( + level="DEBUG" if args.debug else "INFO", + datefmt="%Y-%m-%d %H:%M:%S", + format="%(asctime)s [%(levelname)s] [%(filename)s] %(message)s", + ) + logging.getLogger(__name__).addHandler( + TimedRotatingFileHandler(filename=log_path, when="d", interval=1) + ) -def get_log_path(args): +def get_settings(args: argparse.Namespace, filename: str) -> QSettings: if args.local: - return "tdm.log" - if args.log_location: - return os.path.join(args.log_location, "tdm.log") - return os.path.join(QDir.tempPath(), "tdm.log") + return QSettings(f"{filename}.ini", QSettings.IniFormat) + if args.config_location: + return QSettings(os.path.join(args.config_location, f"{filename}.ini"), QSettings.IniFormat) + return QSettings(QSettings.IniFormat, QSettings.UserScope, "tdm", f"{filename}.ini") -def setup_parser(): +def setup_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(prog='Tasmota Device Manager') parser.add_argument('--debug', action='store_true', help='Enable debugging') parser.add_argument( @@ -54,17 +57,18 @@ def setup_parser(): return parser -def start(): +def start() -> None: parser = setup_parser() args = parser.parse_args() + configure_logging(args) try: app = QApplication(sys.argv) app.lastWindowClosed.connect(app.quit) app.setStyle("Fusion") - settings, devices, log_path = get_settings(args), get_devices(args), get_log_path(args) - MW = MainWindow(version, settings, devices, log_path, args.debug) + settings, devices = get_settings(args, "tdm"), get_settings(args, "devices") + MW = MainWindow(version, settings, devices, args.debug) MW.show() sys.exit(app.exec_()) diff --git a/tdmgr/schemas/common.py b/tdmgr/schemas/common.py index 83c0427..6c0d3a1 100644 --- a/tdmgr/schemas/common.py +++ b/tdmgr/schemas/common.py @@ -1,19 +1,6 @@ -import logging from enum import Enum -from pydantic import BaseModel, ConfigDict, model_validator - class OnOffEnum(str, Enum): ON = "ON" OFF = "OFF" - - -class TDMBaseModel(BaseModel): - model_config = ConfigDict(extra="allow") - - @model_validator(mode="after") - def log_extra_fields(cls, values): - if cls.__name__ != "StatusSNSSchema" and values.model_extra: - logging.warning("%s has extra fields: %s", cls.__name__, values.model_extra) - return values diff --git a/tdmgr/schemas/status.py b/tdmgr/schemas/status.py index 6bbffa4..741dbe6 100644 --- a/tdmgr/schemas/status.py +++ b/tdmgr/schemas/status.py @@ -1,11 +1,22 @@ +import logging from typing import List, Optional, Union -from pydantic import create_model +from pydantic import BaseModel, ConfigDict, create_model, model_validator -from tdmgr.schemas.common import TDMBaseModel +log = logging.getLogger(__name__) -class StatusSchema(TDMBaseModel): +class StatusBaseModel(BaseModel): + model_config = ConfigDict(extra="allow") + + @model_validator(mode="after") + def log_extra_fields(cls, values): + if cls.__name__ != "StatusSNSSchema" and values.model_extra: + log.warning("%s has extra fields: %s", cls.__name__, values.model_extra) + return values + + +class StatusSchema(StatusBaseModel): ButtonRetain: int ButtonTopic: str DeviceName: Optional[str] = None @@ -29,7 +40,7 @@ class StatusSchema(TDMBaseModel): Topic: str -class StatusPRMSchema(TDMBaseModel): +class StatusPRMSchema(StatusBaseModel): BCResetTime: Optional[str] = None Baudrate: int BootCount: int @@ -45,7 +56,7 @@ class StatusPRMSchema(TDMBaseModel): Uptime: str -class StatusFWRSchema(TDMBaseModel): +class StatusFWRSchema(StatusBaseModel): Boot: Optional[int] = None BuildDateTime: str CR: Optional[str] = None @@ -56,7 +67,7 @@ class StatusFWRSchema(TDMBaseModel): Version: str -class StatusLOGSchema(TDMBaseModel): +class StatusLOGSchema(StatusBaseModel): LogHost: str LogPort: int MqttLog: Optional[int] = None @@ -69,7 +80,7 @@ class StatusLOGSchema(TDMBaseModel): WebLog: int -class StatusMEMSchema(TDMBaseModel): +class StatusMEMSchema(StatusBaseModel): Drivers: Optional[str] = None Features: List[str] FlashChipId: str @@ -87,7 +98,7 @@ class StatusMEMSchema(TDMBaseModel): StackLowMark: Optional[int] = None -class BaseNETSchema(TDMBaseModel): +class StatusNETBaseSchema(StatusBaseModel): DNSServer1: Optional[str] = None DNSServer2: Optional[str] = None DNSServer: Optional[str] = None @@ -101,14 +112,14 @@ class BaseNETSchema(TDMBaseModel): Subnetmask: str -class StatusNETSchema(BaseNETSchema): - Ethernet: Optional[BaseNETSchema] = None +class StatusNETSchema(StatusNETBaseSchema): + Ethernet: Optional[StatusNETBaseSchema] = None Webserver: int WifiConfig: int WifiPower: Optional[int] = None -class StatusMQTSchema(TDMBaseModel): +class StatusMQTSchema(StatusBaseModel): KEEPALIVE: int MAX_PACKET_SIZE: int MqttClient: str @@ -121,7 +132,7 @@ class StatusMQTSchema(TDMBaseModel): SOCKET_TIMEOUT: Optional[int] = None -class StatusPTHSchema(TDMBaseModel): +class StatusPTHSchema(StatusBaseModel): CurrentHigh: Union[int, List[int]] CurrentLow: Union[int, List[int]] MaxEnergy: Optional[int] = None @@ -136,7 +147,7 @@ class StatusPTHSchema(TDMBaseModel): VoltageLow: Union[int, List[int]] -class StatusSTKSchema(TDMBaseModel): +class StatusSTKSchema(StatusBaseModel): CallChain: List[str] DEPC: str EPC: List[str] @@ -145,12 +156,12 @@ class StatusSTKSchema(TDMBaseModel): Reason: str -class StatusSNSSchema(TDMBaseModel): +class StatusSNSSchema(StatusBaseModel): TempUnit: Optional[str] = None Time: str -class StatusTIMSchema(TDMBaseModel): +class StatusTIMSchema(StatusBaseModel): EndDST: str Local: str StartDST: str @@ -160,7 +171,7 @@ class StatusTIMSchema(TDMBaseModel): UTC: str -class WifiSchema(TDMBaseModel): +class WifiSchema(StatusBaseModel): AP: int BSSId: str Channel: Union[int, List[int]] @@ -172,12 +183,12 @@ class WifiSchema(TDMBaseModel): Signal: Optional[int] = None -class BerrySchema(TDMBaseModel): +class BerrySchema(StatusBaseModel): HeapUsed: int Objects: int -class StateSTSBaseSchema(TDMBaseModel): +class StateSTSBaseSchema(StatusBaseModel): Berry: Optional[BerrySchema] = None Channel: Optional[List[int]] = None Color: Optional[str] = None @@ -220,51 +231,51 @@ class StateBaseSchema(StateSTSBaseSchema): ) -class StatusResponseSchema(TDMBaseModel): +class StatusResponseSchema(StatusBaseModel): Status: StatusSchema -class Status1ResponseSchema(TDMBaseModel): +class Status1ResponseSchema(StatusBaseModel): StatusPRM: StatusPRMSchema -class Status2ResponseSchema(TDMBaseModel): +class Status2ResponseSchema(StatusBaseModel): StatusFWR: StatusFWRSchema -class Status3ResponseSchema(TDMBaseModel): +class Status3ResponseSchema(StatusBaseModel): StatusLOG: StatusLOGSchema -class Status4ResponseSchema(TDMBaseModel): +class Status4ResponseSchema(StatusBaseModel): StatusMEM: StatusMEMSchema -class Status5ResponseSchema(TDMBaseModel): +class Status5ResponseSchema(StatusBaseModel): StatusNET: StatusNETSchema -class Status6ResponseSchema(TDMBaseModel): +class Status6ResponseSchema(StatusBaseModel): StatusMQT: StatusMQTSchema -class Status7ResponseSchema(TDMBaseModel): +class Status7ResponseSchema(StatusBaseModel): StatusTIM: StatusTIMSchema -class Status9ResponseSchema(TDMBaseModel): +class Status9ResponseSchema(StatusBaseModel): StatusPTH: StatusPTHSchema -class Status10ResponseSchema(TDMBaseModel): +class Status10ResponseSchema(StatusBaseModel): StatusSNS: StatusSNSSchema -class Status11ResponseSchema(TDMBaseModel): +class Status11ResponseSchema(StatusBaseModel): StatusSTS: StatusSTSSchema -class Status12ResponseSchema(TDMBaseModel): +class Status12ResponseSchema(StatusBaseModel): StatusSTK: StatusSTKSchema @@ -279,7 +290,7 @@ class Status0ResponseSchema(StatusResponseSchema): StatusSNS: Optional[Status10ResponseSchema] -STATUS_SCHEMA_MAP: [str, TDMBaseModel] = { +STATUS_SCHEMA_MAP: [str, StatusBaseModel] = { 'STATE': StateSchema, 'STATUS': StatusResponseSchema, 'STATUS0': Status0ResponseSchema, diff --git a/tdmgr/util/__init__.py b/tdmgr/util/__init__.py index d0a7119..5f69f0e 100644 --- a/tdmgr/util/__init__.py +++ b/tdmgr/util/__init__.py @@ -144,7 +144,6 @@ def __init__(self, topic: str, fulltopic: str = "%prefix%/%topic%/", devicename: @classmethod def from_discovery(cls, obj: DiscoverySchema): - logging.debug(obj) _ft = obj.ft.replace(obj.t, "%topic%").replace(obj.tp.tele, "%prefix%") device = TasmotaDevice(obj.t, _ft, obj.dn) device.topic_prefixes = obj.tp diff --git a/tdmgr/util/mqtt.py b/tdmgr/util/mqtt.py index 94541fe..115a594 100644 --- a/tdmgr/util/mqtt.py +++ b/tdmgr/util/mqtt.py @@ -11,6 +11,8 @@ import paho.mqtt.client as mqtt from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot +log = logging.getLogger(__name__) + MQTT_PATH_REGEX = "[^+#*>$/]+" # all symbols accepted except forbidden by MQTT topic spec DEFAULT_PREFIXES = ["tele", "stat"] @@ -66,7 +68,7 @@ def dict(self) -> dict: try: return json.loads(self.payload) except JSONDecodeError as e: - logging.critical("MQTT: Cannot parse %s: %s (%s)", self.endpoint, self.payload, e) + log.critical("Cannot parse %s: %s (%s)", self.endpoint, self.payload, e) return {} def match_fulltopic(self, pattern: str) -> Union[Match, None]: @@ -242,7 +244,7 @@ def disconnectFromHost(self): def subscribe(self, path): if self.state == MqttClient.Connected: self.m_client.subscribe(path) - logging.info("MQTT: Subscribed to %s", ", ".join([p[0] for p in path])) + log.info("Subscribed to %s", ", ".join([p[0] for p in path])) @pyqtSlot(str, str) def publish(self, topic, payload=None, qos=0, retain=False): @@ -256,9 +258,7 @@ def on_message(self, mqttc, obj, msg): message = Message(msg.topic, msg.payload, msg.retain) self.messageSignal.emit(message) except UnicodeDecodeError as e: - logging.error( - "MQTT MESSAGE DECODE ERROR: %s (%s=%s)", e, msg.topic, msg.payload.__repr__() - ) + log.error("MESSAGE DECODE ERROR: %s (%s=%s)", e, msg.topic, msg.payload.__repr__()) def on_connect(self, *args): rc = args[3]