From 438523a8114fc9692ec52330c0079106178731af Mon Sep 17 00:00:00 2001 From: Emanuele Trabattoni Date: Thu, 12 Jun 2025 00:55:50 +0200 Subject: [PATCH] dataclasses per upsmon --- upsmon/ups.py | 151 ++++++++++++++++++++++++++++++++---------------- upsmon/utils.py | 37 ++++++++++++ 2 files changed, 137 insertions(+), 51 deletions(-) create mode 100644 upsmon/utils.py diff --git a/upsmon/ups.py b/upsmon/ups.py index 4f98c5c..5012879 100644 --- a/upsmon/ups.py +++ b/upsmon/ups.py @@ -1,44 +1,89 @@ import os import sys import time -import string import serial import logging -import json -from pyutils.utils import * + +from utils import * +from copy import deepcopy +from dataclasses import dataclass from influxdb_client_3 import InfluxDBClient3 # Get environment variables env = dict(os.environ) LOGGER: logging.Logger -GIT_HASH: str = "HHHHHHHHH" -def send(port: serial.Serial, d: str): - port.write((d+'\r').encode('ascii')) - port.flush() +class UPScommand: + port: serial.Serial -def receive(port: serial.Serial, d: str) -> str: - # Expected response, 46chrs (231.5 230.8 229.2 012 50.0 2.27 27.0 00000001 - r = port.read_until(expected=b'\r').decode('ascii').rstrip() - LOGGER.debug(f"{d} : {r}") - return r + def __init__(self, p: serial.Serial): + self.port = p + return + + def send(self, cmd: str): + self.port.write((cmd+'\r').encode('ascii')) + self.port.flush() -def bruteforceCommands(port: serial.Serial): - # T and S cause unwanted shutdown - letters = string.ascii_uppercase.replace('T','').replace('S','') - LOGGER.debug(f"Test commands: {letters}") - for c in letters: - send(port, c) - receive(port, c) - for n in range(10): - d = c+f"{n:1d}" - send(port, d) - receive(port, d) - for n in range(100): - d = c+f"{n:02d}" - send(port, d) - receive(port, d) + def receive(self, cmd: str) -> str: + resp = self.port.read_until(expected=b'\r').decode('ascii').rstrip() + LOGGER.debug(f"{cmd} : {resp}") + return resp + + def request(self, cmd: str) -> list[str]: + self.send(cmd) + return self.receive(cmd).lstrip('(').rstrip().split() + + def asDict(self): + return deepcopy(self.__dict__) + +@dataclass +class UPSstatus(UPScommand): + inV: float + inF: float + outV: float + outF: float + current: float + loadPct: int + battV: float + temp: float + onLine: bool + onBatt: bool + ecoMode: bool + + def __init__(self, port: serial.Serial): + self.port = port + self.update() + + def update(self): + data = self.request(cmd="QGS") + self.inV = float(data[0]) + self.inF = float(data[1]) + self.outV = float(data[1]) + self.outF = float(data[3]) + self.current = float(data[4]) + self.loadPct = int(data[5]) + self.battV = float(data[8]) + self.temp = float(data[10]) + self.onLine = True if data[11].startswith('1') else False + self.onBatt = True if data[11].endswith('0') else False + self.ecoMode = True if data[11][4] == '1' else False + +@dataclass +class UPSbattery(UPScommand): + battV: float + battPct: int + timeLeft: float + + def __init__(self, port: serial.Serial): + self.port = port + self.update() + + def update(self): + data = self.request(cmd="QBV") + self.battV = float(data[0]) + self.battPct = int(data[3]) + self.timeLeft = int(data[4]) / 60.0 ################## ###### MAIN ###### @@ -58,6 +103,11 @@ def main() -> int: # Init Serial port port = serial.Serial(port=env['PORT'], baudrate=int(env['BAUD']), bytesize=8, parity='N', stopbits=1) port.flush() + + # Init Dataclasses + run: SignalHandler = SignalHandler(LOGGER) + status = UPSstatus(port=port) + battery = UPSbattery(port=port) except Exception as e: LOGGER.error(e) return 1 @@ -68,27 +118,26 @@ def main() -> int: ############################## ########## MAIN LOOP ######### ############################## - run: SignalHandler = SignalHandler(LOGGER) while run.running: try: - send(port, UPS_COMMAND) - raw_data = receive(port, UPS_COMMAND).lstrip('(').rstrip().split() - if len(raw_data) < 8: - LOGGER.error(f"Incomplete data: {raw_data}") - break - values = { - 'inV': float(raw_data[0]), - 'outV': float(raw_data[2]), - 'loadPercent': float(raw_data[3]), - 'lineFreq': float(raw_data[4]), - 'timeLeft': float(raw_data[5]), - 'onBatt': True if str(raw_data[7]).startswith('1') else False, - 'onLine': True if str(raw_data[7]).endswith('1') else False, - } - LOGGER.debug(f"UPS Status: \n{json.dumps(values, indent=2)}") - if values['onBatt']: - LOGGER.info(f"OnBattery: \n{json.dumps(values,indent=2)}") - write_client.write(record=dict2Point('ups', values)) + # Update data + status.update() + LOGGER.debug(f"{repr(status)}") + battery.update() + LOGGER.debug(f"{repr(battery)}") + + # Debug status Information when running on batteries + if status.onBatt: + LOGGER.info(f" Status:\n{repr(status)}") + LOGGER.info(f"Battery:\n{repr(battery)}") + + # Write datapoint to Influx merging measurements + datapoint = status.asDict() | battery.asDict() + datapoint.pop('port') + write_client.write(record=dict2Point(measurement='ups', + fields=datapoint + )) + # Sleep and repeat time.sleep(INTERVAL) except Exception as e: print(f"Unexpected exception: [{e}]") @@ -101,24 +150,24 @@ def main() -> int: return 0 if __name__ == "__main__": - # Logger Constants + # Logger Constants LOG_FORMAT = '%(asctime)s| %(levelname)-7s|%(funcName)-10s|%(lineno)-3d: %(message)-50s' - # Enabling Logger + # Enabling Logger LOGGER = logging.getLogger(__name__) LOGGER.setLevel(logging.DEBUG) LOGGER.propagate = False formatter = logging.Formatter(LOG_FORMAT, None) levels = logging.getLevelNamesMapping() - # File logging + # File logging log_name = os.path.abspath(env['LOG_FILE']) fh = logging.FileHandler(log_name) fh.setLevel(levels[env['LOG_FILE_LVL']]) fh.setFormatter(formatter) LOGGER.addHandler(fh) - # Console logging + # Console logging cl = logging.StreamHandler(sys.stdout) cl.setLevel(levels[env['LOG_CLI_LVL']]) cl.setFormatter(formatter) @@ -126,7 +175,7 @@ if __name__ == "__main__": LOGGER.warning(f"UPSmon started on: {time.asctime()}") LOGGER.info(f"UPSmon BUILD: {env.get("VER", "Test")}") - + while main(): LOGGER.error("Main thread exited unexpectedly") time.sleep(15) diff --git a/upsmon/utils.py b/upsmon/utils.py new file mode 100644 index 0000000..d90ec35 --- /dev/null +++ b/upsmon/utils.py @@ -0,0 +1,37 @@ +import signal +from logging import Logger +from influxdb_client_3 import Point + +class SignalHandler: + running: bool + logger: Logger + + def __init__(self, logger): + self.running: bool = True + self.logger: Logger = logger + signal.signal(signal.SIGINT, self._handle_sigint) + signal.signal(signal.SIGTERM, self._handle_sigint) + + def _handle_sigint(self, signum, frame): + self.logger.info(f"Received SIGNAL: {signal.strsignal(signum)}") + self.running = False + +def dict2Point(measurement: str, fields: dict, tags: dict | None = None) -> Point: + p = Point(measurement) + for k,v in fields.items(): + p.field(k,v) + if tags: + for k,v in tags.items(): + p.tag(k,v) + return p + +def convertInt(d: dict) -> dict: + for k,v in d.items(): + if str.isdecimal(v): + d[k] = int(v) + return d + +def convertIntList(l: list[dict]) -> list[dict]: + for n,d in enumerate(l): + l[n] = convertInt(d) + return l \ No newline at end of file