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}
+
Updated {generated_at} · This page refreshes every 60 seconds.
+