updated to run on the pi5
This commit is contained in:
@@ -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
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user