updated to run on the pi5

This commit is contained in:
2026-08-10 19:07:26 +02:00
parent 14451d4d45
commit 0b92836367
2 changed files with 130 additions and 8 deletions
+31 -8
View File
@@ -76,10 +76,15 @@ sudo apt install nut nut-client nut-server
Also install RRDTool for the historical data collector: Also install RRDTool for the historical data collector:
```bash ```bash
sudo apt install librrd-dev python3-dev sudo apt install librrd-dev python3-dev python3-venv
python3 -m pip install -r requirements.txt 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: Check the NUT version:
```bash ```bash
@@ -538,7 +543,7 @@ The Python collector converts these into numeric values for RRDTool.
The Python program is: The Python program is:
```text ```text
/usr/local/bin/ups_rrd.py /opt/ups/ups2rrd.py
``` ```
Its job is deliberately simple: Its job is deliberately simple:
@@ -566,11 +571,12 @@ RRDTool
PNG graphs PNG graphs
``` ```
The script performs three main tasks: The script performs four main tasks:
1. Create the RRD database if it doesn't exist 1. Create the RRD database if it doesn't exist
2. Read the current UPS values 2. Read the current UPS values
3. Update the RRD and regenerate the graphs 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: The cron job runs the Python script every minute:
```cron ```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. Therefore, approximately one measurement is stored every minute.
@@ -767,6 +773,8 @@ are completely different things.
The script creates: The script creates:
```text ```text
/var/www/html/ups/index.html
/var/www/html/ups/status-3m.png /var/www/html/ups/status-3m.png
/var/www/html/ups/battery-1y.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: If `/var/www/html` is served by the web server, these can therefore be viewed as:
```text ```text
/ups/
/ups/status-3m.png /ups/status-3m.png
/ups/battery-1y.png /ups/battery-1y.png
/ups/battery-5y.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 /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 # 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: Run it manually:
```bash ```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: A successful run should produce something similar to:
@@ -826,12 +839,22 @@ rrdtool lastupdate /var/lib/rrd/bx950mi.rrd
# 20. Checking cron # 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 ```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: After a few minutes:
```bash ```bash
+99
View File
@@ -51,6 +51,7 @@ RRD_DIR = "/var/lib/rrd"
RRD_FILE = os.path.join(RRD_DIR, "bx950mi.rrd") RRD_FILE = os.path.join(RRD_DIR, "bx950mi.rrd")
GRAPH_DIR = "/var/www/html/ups" 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") GRAPH_STATUS = os.path.join(GRAPH_DIR, "status-3m.png")
@@ -839,6 +840,102 @@ def create_graphs():
graph_power_5y() 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"""<article class="card">
<h3>{label}</h3>
<a href="{os.path.basename(path)}">
<img src="{os.path.basename(path)}" alt="{title}{label}" loading="lazy">
</a>
</article>"""
for label, path in graphs
)
sections.append(
f"""<section>
<h2>{title}</h2>
<div class="grid">
{cards}
</div>
</section>"""
)
document = f"""<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="refresh" content="60">
<title>APC BX950MI UPS Dashboard</title>
<style>
:root {{ color-scheme: light dark; font-family: system-ui, sans-serif; }}
body {{ margin: 0; background: #10141c; color: #edf2f7; }}
header, main {{ width: min(1500px, calc(100% - 2rem)); margin: auto; }}
header {{ padding: 2rem 0 1rem; }}
h1, h2, h3, p {{ margin-top: 0; }}
h1 {{ margin-bottom: .35rem; }}
header p {{ color: #aab5c5; }}
section {{ margin: 1.5rem 0 2.5rem; }}
h2 {{ border-bottom: 1px solid #344054; padding-bottom: .5rem; }}
.grid {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 520px), 1fr)); gap: 1rem; }}
.card {{ overflow: hidden; border: 1px solid #344054; border-radius: 12px; background: #19202c; box-shadow: 0 8px 24px #0004; }}
.card h3 {{ padding: 1rem 1rem 0; color: #cbd5e1; font-size: 1rem; }}
.card a {{ display: block; }}
.card img {{ display: block; width: 100%; height: auto; background: white; }}
footer {{ padding: 0 0 2rem; color: #8c99aa; text-align: center; }}
</style>
</head>
<body>
<header>
<h1>APC BX950MI UPS Dashboard</h1>
<p>Updated {generated_at} · This page refreshes every 60 seconds.</p>
</header>
<main>
{''.join(sections)}
</main>
<footer>Powered by NUT, Python and RRDtool</footer>
</body>
</html>
"""
with open(HTML_FILE, "w", encoding="utf-8") as dashboard:
dashboard.write(document)
print(f"Created dashboard: {HTML_FILE}")
def main(): def main():
create_rrd() create_rrd()
@@ -846,6 +943,8 @@ def main():
create_graphs() create_graphs()
create_dashboard()
if __name__ == "__main__": if __name__ == "__main__":
main() main()