Formatting cleanups for UPS and Prometheus functionality

This commit is contained in:
2026-05-02 18:18:34 +02:00
parent deb6b2bdcc
commit 86f2c548b3
2 changed files with 229 additions and 106 deletions
+31 -19
View File
@@ -1,11 +1,15 @@
import time
import prometheus_client import prometheus_client
from bmspy.utilities import debugger from bmspy.utilities import debugger
from bmspy.server import collect_data
def prometheus_export(daemonize=True, filename=None): def prometheus_export(daemonize=True, filename=None):
global debug global debug
if not can_export_prometheus: if not can_export_prometheus:
raise ModuleNotFoundError("Unable to export to Prometheus. Is prometheus-client installed?") raise ModuleNotFoundError(
"Unable to export to Prometheus. Is prometheus-client installed?"
)
data = dict() data = dict()
# Initialize data structure, to fill in help values # Initialize data structure, to fill in help values
@@ -34,44 +38,50 @@ def prometheus_export(daemonize=True, filename=None):
prometheus_client.generate_latest(registry) prometheus_client.generate_latest(registry)
else: else:
if filename is None: if filename is None:
debugger("Invalid filename supplied"); debugger("Invalid filename supplied")
return False return False
prometheus_client.write_to_textfile(filename, registry=registry) prometheus_client.write_to_textfile(filename, registry=registry)
return True return True
def prometheus_create_metric(registry, data): def prometheus_create_metric(registry, data):
metric = dict() metric = dict()
for name, contains in data.items(): for name, contains in data.items():
helpmsg = '' helpmsg = ""
if contains.get('help') is not None: if contains.get("help") is not None:
helpmsg = contains.get('help') helpmsg = contains.get("help")
if contains.get('units'): if contains.get("units"):
helpmsg += ' (' + contains.get('units') + ')' helpmsg += " (" + contains.get("units") + ")"
if contains.get('value') is not None: if contains.get("value") is not None:
metric[name] = prometheus_client.Gauge(name, helpmsg, registry=registry) metric[name] = prometheus_client.Gauge(name, helpmsg, registry=registry)
# Has multiple values, each a different label # Has multiple values, each a different label
elif contains.get('values') is not None: elif contains.get("values") is not None:
if contains.get('label') is None: if contains.get("label") is None:
debugger("ERROR: no label for {0} specified".format(name)) debugger("ERROR: no label for {0} specified".format(name))
label = contains.get('label') label = contains.get("label")
metric[name] = prometheus_client.Gauge(name, helpmsg, [label], registry=registry) metric[name] = prometheus_client.Gauge(
elif contains.get('info') is not None: name, helpmsg, [label], registry=registry
)
elif contains.get("info") is not None:
metric[name] = prometheus_client.Info(name, helpmsg, registry=registry) metric[name] = prometheus_client.Info(name, helpmsg, registry=registry)
else: else:
pass pass
return metric return metric
def prometheus_populate_metric(metric, data): def prometheus_populate_metric(metric, data):
for name, contains in data.items(): for name, contains in data.items():
if contains.get('value') is not None: if contains.get("value") is not None:
value = contains.get('value') value = contains.get("value")
metric[name].set(value) metric[name].set(value)
# doesn't have a value, but has [1-4]: # doesn't have a value, but has [1-4]:
if contains.get('values') is not None and isinstance(contains.get('values'), dict): if contains.get("values") is not None and isinstance(
for idx, label_value in contains.get('values').items(): contains.get("values"), dict
):
for idx, label_value in contains.get("values").items():
metric[name].labels(idx).set(label_value) metric[name].labels(idx).set(label_value)
if contains.get('info'): if contains.get("info"):
value = contains.get('info') value = contains.get("info")
metric[name].info({name: value}) metric[name].info({name: value})
else: else:
pass pass
@@ -79,9 +89,11 @@ def prometheus_populate_metric(metric, data):
# TODO fork bms daemon if need be? # TODO fork bms daemon if need be?
def main(): def main():
debugger("TODO. At present, run from bmspy directly.") debugger("TODO. At present, run from bmspy directly.")
# influxdb_export(bucket=args.influx_bucket, \ # influxdb_export(bucket=args.influx_bucket, \
# url=args.influx_url, \ # url=args.influx_url, \
# org=args.influx_org, \ # org=args.influx_org, \
+175 -64
View File
@@ -1,8 +1,13 @@
from collections import deque
import argparse import argparse
import atexit, datetime, os, re, sys, time import atexit
import smtplib, ssl, socket import os
from typing import Any import re
import time
import smtplib
import ssl
import socket
from collections import deque
from bmspy import client from bmspy import client
DAEMON_UPDATE_PERIOD = 30 DAEMON_UPDATE_PERIOD = 30
@@ -12,21 +17,31 @@ critical_sent = False
warning_sent = False warning_sent = False
alert_sent = False alert_sent = False
def handle_shutdown(action: str = 'cancel', delay: int = 0, debug: int = 0) -> None:
def handle_shutdown(action: str = "cancel", delay: int = 0, debug: int = 0) -> None:
global scheduled_shutdown global scheduled_shutdown
if action == 'shutdown': if action == "shutdown":
if scheduled_shutdown is False: if scheduled_shutdown is False:
scheduled_shutdown = time.time() + delay * 60 * 1000 scheduled_shutdown = time.time() + delay * 60 * 1000
os.system("/sbin/shutdown {}".format(delay)) os.system("/sbin/shutdown {}".format(delay))
elif action == 'cancel': elif action == "cancel":
os.system("/sbin/shutdown -c") os.system("/sbin/shutdown -c")
return return
def handle_email(text: str, level: str | None, recipient: str = "root", mailserver: str = "localhost", port: int = 25, mailuser: str | None = None, mailpass: str | None = None, debug: int = 0) -> None: def handle_email(
text: str,
level: str | None,
recipient: str = "root",
mailserver: str = "localhost",
port: int = 25,
mailuser: str | None = None,
mailpass: str | None = None,
debug: int = 0,
) -> None:
isSSL = False isSSL = False
hostname = socket.gethostname() hostname = socket.gethostname()
@@ -38,7 +53,9 @@ def handle_email(text: str, level: str | None, recipient: str = "root", mailserv
isSSL = True isSSL = True
if level is not None: if level is not None:
msg = "From: {}\r\nTo: {}\r\nSubject: {} from BMSPY UPS on {}\r\n\r\n{}\r\n".format(sender, recipient, level, hostname, text) msg = "From: {}\r\nTo: {}\r\nSubject: {} from BMSPY UPS on {}\r\n\r\n{}\r\n".format(
sender, recipient, level, hostname, text
)
if isSSL: if isSSL:
context = ssl.create_default_context() context = ssl.create_default_context()
@@ -58,43 +75,105 @@ def main() -> None:
global alert_sent, warning_sent, critical_sent global alert_sent, warning_sent, critical_sent
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description='Query JBD BMS and alert or shutdown when certain thresholds are reached', description="Query JBD BMS and alert or shutdown when certain thresholds are reached",
add_help=True, add_help=True,
) )
parser.add_argument('--alert', '-a', dest='alert', action='store_true', parser.add_argument(
default=True, help='Email an alert when UPS detects A/C loss (default: true)') "--alert",
parser.add_argument('--warning', '-w', dest='warning_threshold', action='store', type=int, "-a",
default=75, help='Email an alert when remaining capacity percentage drops below this figure (default: 75)') dest="alert",
parser.add_argument('--critical', '-c', dest='critical_threshold', action='store', type=int, action="store_true",
default=30, help='Shut system down when remaining capacity percentage drops below this figure (default: 30)') default=True,
parser.add_argument('--delay', '-d', dest='shutdown_delay', action='store', type=int, help="Email an alert when UPS detects A/C loss (default: true)",
default=5, help='Delay system shutdown (default: 5 minutes)') )
parser.add_argument('--mailserver', '-m', dest='mailserver', action='store', parser.add_argument(
default="localhost", help='Mail server (default: localhost)') "--warning",
parser.add_argument('--port', '-p', dest='port', action='store', "-w",
default=25, help='Mail server port (default: 25)') dest="warning_threshold",
parser.add_argument('--user', dest='mailuser', action='store', action="store",
default=None, help='Mail server user') type=int,
parser.add_argument('--pass', dest='mailpass', action='store', default=75,
default=None, help='Mail server password') help="Email an alert when remaining capacity percentage drops below this figure (default: 75)",
parser.add_argument('--to', '-t', dest='recipient', action='store', )
default="root", help='Email recipient (default: root)') parser.add_argument(
parser.add_argument('--socket', '-s', dest='socket', action='store', "--critical",
default='/run/bmspy/bms', help='Socket to communicate with daemon') "-c",
parser.add_argument('--verbose', '-v', action='count', dest="critical_threshold",
default=0, help='Print more verbose information (can be specified multiple times)') action="store",
type=int,
default=30,
help="Shut system down when remaining capacity percentage drops below this figure (default: 30)",
)
parser.add_argument(
"--delay",
"-d",
dest="shutdown_delay",
action="store",
type=int,
default=5,
help="Delay system shutdown (default: 5 minutes)",
)
parser.add_argument(
"--mailserver",
"-m",
dest="mailserver",
action="store",
default="localhost",
help="Mail server (default: localhost)",
)
parser.add_argument(
"--port",
"-p",
dest="port",
action="store",
default=25,
help="Mail server port (default: 25)",
)
parser.add_argument(
"--user", dest="mailuser", action="store", default=None, help="Mail server user"
)
parser.add_argument(
"--pass",
dest="mailpass",
action="store",
default=None,
help="Mail server password",
)
parser.add_argument(
"--to",
"-t",
dest="recipient",
action="store",
default="root",
help="Email recipient (default: root)",
)
parser.add_argument(
"--socket",
"-s",
dest="socket",
action="store",
default="/run/bmspy/bms",
help="Socket to communicate with daemon",
)
parser.add_argument(
"--verbose",
"-v",
action="count",
default=0,
help="Print more verbose information (can be specified multiple times)",
)
args = parser.parse_args() args = parser.parse_args()
debug = args.verbose debug = args.verbose
print("Running BMS UPS daemon on socket {}".format(args.socket)) print("Running BMS UPS daemon on socket {}".format(args.socket))
client.handle_registration(args.socket, 'ups', debug) client.handle_registration(args.socket, "ups", debug)
atexit.register(client.handle_registration, args.socket, 'ups', debug) atexit.register(client.handle_registration, args.socket, "ups", debug)
history = deque() history = deque()
while True: while True:
data = client.read_data(args.socket, 'ups') data = client.read_data(args.socket, "ups")
history.append(data) history.append(data)
# Remove the oldest data from the history # Remove the oldest data from the history
@@ -111,79 +190,111 @@ def main() -> None:
time.sleep(DAEMON_UPDATE_PERIOD) time.sleep(DAEMON_UPDATE_PERIOD)
continue continue
current_amps = float(data['bms_current_amps']['raw_value']) current_amps = float(data["bms_current_amps"]["raw_value"])
charge_ratio = float(data['bms_capacity_charge_ratio']['raw_value']) * 100 charge_ratio = float(data["bms_capacity_charge_ratio"]["raw_value"]) * 100
comparison_1_current_amps = float(comparison_1['bms_current_amps']['raw_value']) comparison_1_current_amps = float(comparison_1["bms_current_amps"]["raw_value"])
comparison_1_charge_ratio = float(comparison_1['bms_capacity_charge_ratio']['raw_value']) * 100 comparison_1_charge_ratio = (
comparison_2_current_amps = float(comparison_2['bms_current_amps']['raw_value']) float(comparison_1["bms_capacity_charge_ratio"]["raw_value"]) * 100
comparison_2_charge_ratio = float(comparison_2['bms_capacity_charge_ratio']['raw_value']) * 100 )
comparison_3_current_amps = float(comparison_3['bms_current_amps']['raw_value']) comparison_2_current_amps = float(comparison_2["bms_current_amps"]["raw_value"])
comparison_3_charge_ratio = float(comparison_3['bms_capacity_charge_ratio']['raw_value']) * 100 comparison_2_charge_ratio = (
float(comparison_2["bms_capacity_charge_ratio"]["raw_value"]) * 100
)
comparison_3_current_amps = float(comparison_3["bms_current_amps"]["raw_value"])
comparison_3_charge_ratio = (
float(comparison_3["bms_capacity_charge_ratio"]["raw_value"]) * 100
)
if debug > 1: if debug > 1:
print("current: {:>3.2f}A\ncapacity remaining: {:>4.0f}%".format(current_amps, charge_ratio)) print(
"current: {:>3.2f}A\ncapacity remaining: {:>4.0f}%".format(
current_amps, charge_ratio
)
)
if charge_ratio <= args.critical_threshold and \ if (
comparison_1_charge_ratio <= args.critical_threshold: charge_ratio <= args.critical_threshold
and comparison_1_charge_ratio <= args.critical_threshold
):
if debug > 0: if debug > 0:
print("Below critical threshold, shutting down") print("Below critical threshold, shutting down")
handle_shutdown(action = 'shutdown', delay = args.shutdown_delay, debug = debug) handle_shutdown(action="shutdown", delay=args.shutdown_delay, debug=debug)
if critical_sent is False: if critical_sent is False:
handle_email(text = "remaining capacity below {}%, shutting down".format(args.critical_threshold), handle_email(
text="remaining capacity below {}%, shutting down".format(
args.critical_threshold
),
level="Critical alert", level="Critical alert",
recipient=args.recipient, recipient=args.recipient,
mailserver=args.mailserver, mailserver=args.mailserver,
port=args.port, port=args.port,
mailuser=args.mailuser, mailuser=args.mailuser,
mailpass=args.mailpass, mailpass=args.mailpass,
debug = debug) debug=debug,
)
critical_sent = True critical_sent = True
elif charge_ratio <= args.warning_threshold and \ elif (
comparison_1_charge_ratio <= args.warning_threshold: charge_ratio <= args.warning_threshold
and comparison_1_charge_ratio <= args.warning_threshold
):
if debug > 0: if debug > 0:
print("Below warning threshold") print("Below warning threshold")
if warning_sent is False: if warning_sent is False:
handle_email(text = "remaining capacity below {}%".format(args.warning_threshold), handle_email(
text="remaining capacity below {}%".format(args.warning_threshold),
level="Warning", level="Warning",
recipient=args.recipient, recipient=args.recipient,
mailserver=args.mailserver, mailserver=args.mailserver,
port=args.port, port=args.port,
mailuser=args.mailuser, mailuser=args.mailuser,
mailpass=args.mailpass, mailpass=args.mailpass,
debug = debug) debug=debug,
)
warning_sent = True warning_sent = True
# Current needs to be negative for two consecutive reads # Current needs to be negative for two consecutive reads
elif args.alert and current_amps < 0 and \ elif (
comparison_1_current_amps < 0 and \ args.alert
comparison_2_current_amps >= 0: and current_amps < 0
and comparison_1_current_amps < 0
and comparison_2_current_amps >= 0
):
if debug > 0: if debug > 0:
print("Alert: discharging!") print("Alert: discharging!")
if alert_sent is False: if alert_sent is False:
handle_email(text = "power lost", level = "Power loss alert", handle_email(
text="power lost",
level="Power loss alert",
recipient=args.recipient, recipient=args.recipient,
mailserver=args.mailserver, mailserver=args.mailserver,
port=args.port, port=args.port,
mailuser=args.mailuser, mailuser=args.mailuser,
mailpass=args.mailpass, mailpass=args.mailpass,
debug = debug) debug=debug,
)
alert_sent = True alert_sent = True
# Current needs to be zero or positive for two consecutive reads # Current needs to be zero or positive for two consecutive reads
elif args.alert and current_amps >= 0 and \ elif (
comparison_1_current_amps >= 0 and \ args.alert
comparison_2_current_amps < 0: and current_amps >= 0
and comparison_1_current_amps >= 0
and comparison_2_current_amps < 0
):
if debug > 0: if debug > 0:
print("Alert: power regained!") print("Alert: power regained!")
handle_shutdown(action = 'cancel', debug = debug) handle_shutdown(action="cancel", debug=debug)
handle_email(text = "power regained", level = "Recovery alert", handle_email(
text="power regained",
level="Recovery alert",
recipient=args.recipient, recipient=args.recipient,
mailserver=args.mailserver, mailserver=args.mailserver,
port=args.port, port=args.port,
mailuser=args.mailuser, mailuser=args.mailuser,
mailpass=args.mailpass, mailpass=args.mailpass,
debug = debug) debug=debug,
)
critical_sent = False critical_sent = False
warning_sent = False warning_sent = False
alert_sent = False alert_sent = False