From 0b9283636766b2aa15e12911f43405f4795c877c Mon Sep 17 00:00:00 2001 From: Ignace Date: Mon, 10 Aug 2026 19:07:26 +0200 Subject: [PATCH] updated to run on the pi5 --- README.md | 39 ++++++++++++++++----- ups2rrd.py | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 78be4e6..266b4a5 100644 --- a/README.md +++ b/README.md @@ -76,10 +76,15 @@ sudo apt install nut nut-client nut-server Also install RRDTool for the historical data collector: ```bash -sudo apt install librrd-dev python3-dev -python3 -m pip install -r requirements.txt +sudo apt install librrd-dev python3-dev python3-venv +cd /opt/ups +python3 -m venv .venv +.venv/bin/python -m pip install -r requirements.txt ``` +The collector uses the virtual environment at `/opt/ups/.venv`. Calling its +Python executable directly means cron does not need to activate the environment. + Check the NUT version: ```bash @@ -538,7 +543,7 @@ The Python collector converts these into numeric values for RRDTool. The Python program is: ```text -/usr/local/bin/ups_rrd.py +/opt/ups/ups2rrd.py ``` Its job is deliberately simple: @@ -566,11 +571,12 @@ RRDTool PNG graphs ``` -The script performs three main tasks: +The script performs four main tasks: 1. Create the RRD database if it doesn't exist 2. Read the current UPS values 3. Update the RRD and regenerate the graphs +4. Generate the HTML dashboard --- @@ -614,7 +620,7 @@ The corresponding configuration is: The cron job runs the Python script every minute: ```cron -* * * * * /usr/local/bin/ups_rrd.py >> /var/log/ups_rrd.log 2>&1 +* * * * * /opt/ups/.venv/bin/python /opt/ups/ups2rrd.py >> /var/log/ups_rrd.log 2>&1 ``` Therefore, approximately one measurement is stored every minute. @@ -767,6 +773,8 @@ are completely different things. The script creates: ```text +/var/www/html/ups/index.html + /var/www/html/ups/status-3m.png /var/www/html/ups/battery-1y.png @@ -784,6 +792,8 @@ The script creates: If `/var/www/html` is served by the web server, these can therefore be viewed as: ```text +/ups/ + /ups/status-3m.png /ups/battery-1y.png /ups/battery-5y.png @@ -794,6 +804,9 @@ If `/var/www/html` is served by the web server, these can therefore be viewed as /ups/power-5y.png ``` +The responsive dashboard groups all graphs on one page and refreshes every +60 seconds. Clicking a graph opens its full-size PNG. + --- # 19. Testing the Python program @@ -801,7 +814,7 @@ If `/var/www/html` is served by the web server, these can therefore be viewed as Run it manually: ```bash -sudo /usr/local/bin/ups_rrd.py +sudo /opt/ups/.venv/bin/python /opt/ups/ups2rrd.py ``` A successful run should produce something similar to: @@ -826,12 +839,22 @@ rrdtool lastupdate /var/lib/rrd/bx950mi.rrd # 20. Checking cron -The cron entry is: +Install the cron entry in root's crontab because the collector writes to +`/var/lib/rrd` and `/var/www/html/ups`: + +```bash +sudo crontab -e +``` + +Add: ```cron -* * * * * /usr/local/bin/ups_rrd.py >> /var/log/ups_rrd.log 2>&1 +* * * * * /opt/ups/.venv/bin/python /opt/ups/ups2rrd.py >> /var/log/ups_rrd.log 2>&1 ``` +There is no need to run `source`, activate the virtual environment, or use a +wrapper script. + After a few minutes: ```bash diff --git a/ups2rrd.py b/ups2rrd.py index 6fc3d6a..49d2542 100644 --- a/ups2rrd.py +++ b/ups2rrd.py @@ -51,6 +51,7 @@ RRD_DIR = "/var/lib/rrd" RRD_FILE = os.path.join(RRD_DIR, "bx950mi.rrd") GRAPH_DIR = "/var/www/html/ups" +HTML_FILE = os.path.join(GRAPH_DIR, "index.html") GRAPH_STATUS = os.path.join(GRAPH_DIR, "status-3m.png") @@ -839,6 +840,102 @@ def create_graphs(): graph_power_5y() +def create_dashboard(): + """Create a responsive HTML dashboard for the generated graphs.""" + + generated_at = time.strftime("%Y-%m-%d %H:%M:%S %Z") + + graph_groups = [ + ("UPS status", [("Last 3 months", GRAPH_STATUS)]), + ( + "Battery charge", + [ + ("Last year", GRAPH_BATTERY_1Y), + ("Last 5 years", GRAPH_BATTERY_5Y), + ], + ), + ( + "Battery runtime", + [ + ("Last year", GRAPH_RUNTIME_1Y), + ("Last 5 years", GRAPH_RUNTIME_5Y), + ], + ), + ("UPS load", [("Last 3 months", GRAPH_LOAD)]), + ( + "Nominal power", + [ + ("Last year", GRAPH_POWER_1Y), + ("Last 5 years", GRAPH_POWER_5Y), + ], + ), + ] + + sections = [] + + for title, graphs in graph_groups: + cards = "\n".join( + f"""
+

{label}

+ + {title} — {label} + +
""" + for label, path in graphs + ) + sections.append( + f"""
+

{title}

+
+ {cards} +
+
""" + ) + + document = f""" + + + + + + APC BX950MI UPS Dashboard + + + +
+

APC BX950MI UPS Dashboard

+

Updated {generated_at} · This page refreshes every 60 seconds.

+
+
+ {''.join(sections)} +
+ + + +""" + + with open(HTML_FILE, "w", encoding="utf-8") as dashboard: + dashboard.write(document) + + print(f"Created dashboard: {HTML_FILE}") + + def main(): create_rrd() @@ -846,6 +943,8 @@ def main(): create_graphs() + create_dashboard() + if __name__ == "__main__": main()