a second pae with a week history
This commit is contained in:
@@ -774,17 +774,23 @@ The script creates:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
/var/www/html/ups/index.html
|
/var/www/html/ups/index.html
|
||||||
|
/var/www/html/ups/week.html
|
||||||
|
|
||||||
/var/www/html/ups/status-3m.png
|
/var/www/html/ups/status-3m.png
|
||||||
|
/var/www/html/ups/status-1w.png
|
||||||
|
|
||||||
|
/var/www/html/ups/battery-1w.png
|
||||||
/var/www/html/ups/battery-1y.png
|
/var/www/html/ups/battery-1y.png
|
||||||
/var/www/html/ups/battery-5y.png
|
/var/www/html/ups/battery-5y.png
|
||||||
|
|
||||||
|
/var/www/html/ups/runtime-1w.png
|
||||||
/var/www/html/ups/runtime-1y.png
|
/var/www/html/ups/runtime-1y.png
|
||||||
/var/www/html/ups/runtime-5y.png
|
/var/www/html/ups/runtime-5y.png
|
||||||
|
|
||||||
|
/var/www/html/ups/load-1w.png
|
||||||
/var/www/html/ups/load-3m.png
|
/var/www/html/ups/load-3m.png
|
||||||
|
|
||||||
|
/var/www/html/ups/power-1w.png
|
||||||
/var/www/html/ups/power-1y.png
|
/var/www/html/ups/power-1y.png
|
||||||
/var/www/html/ups/power-5y.png
|
/var/www/html/ups/power-5y.png
|
||||||
```
|
```
|
||||||
@@ -793,19 +799,27 @@ If `/var/www/html` is served by the web server, these can therefore be viewed as
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
/ups/
|
/ups/
|
||||||
|
/ups/week.html
|
||||||
|
|
||||||
/ups/status-3m.png
|
/ups/status-3m.png
|
||||||
|
/ups/status-1w.png
|
||||||
|
/ups/battery-1w.png
|
||||||
/ups/battery-1y.png
|
/ups/battery-1y.png
|
||||||
/ups/battery-5y.png
|
/ups/battery-5y.png
|
||||||
|
/ups/runtime-1w.png
|
||||||
/ups/runtime-1y.png
|
/ups/runtime-1y.png
|
||||||
/ups/runtime-5y.png
|
/ups/runtime-5y.png
|
||||||
|
/ups/load-1w.png
|
||||||
/ups/load-3m.png
|
/ups/load-3m.png
|
||||||
|
/ups/power-1w.png
|
||||||
/ups/power-1y.png
|
/ups/power-1y.png
|
||||||
/ups/power-5y.png
|
/ups/power-5y.png
|
||||||
```
|
```
|
||||||
|
|
||||||
The responsive dashboard groups all graphs on one page and refreshes every
|
The main responsive dashboard groups the long-term graphs on one page. The
|
||||||
60 seconds. Clicking a graph opens its full-size PNG.
|
weekly dashboard at `/ups/week.html` shows one-week history for every collected
|
||||||
|
value. The pages link to each other, refresh every 60 seconds, and open a
|
||||||
|
full-size PNG when a graph is clicked.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+202
-33
@@ -52,6 +52,13 @@ 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")
|
HTML_FILE = os.path.join(GRAPH_DIR, "index.html")
|
||||||
|
WEEKLY_HTML_FILE = os.path.join(GRAPH_DIR, "week.html")
|
||||||
|
|
||||||
|
GRAPH_STATUS_1W = os.path.join(GRAPH_DIR, "status-1w.png")
|
||||||
|
GRAPH_BATTERY_1W = os.path.join(GRAPH_DIR, "battery-1w.png")
|
||||||
|
GRAPH_RUNTIME_1W = os.path.join(GRAPH_DIR, "runtime-1w.png")
|
||||||
|
GRAPH_LOAD_1W = os.path.join(GRAPH_DIR, "load-1w.png")
|
||||||
|
GRAPH_POWER_1W = os.path.join(GRAPH_DIR, "power-1w.png")
|
||||||
|
|
||||||
GRAPH_STATUS = os.path.join(GRAPH_DIR, "status-3m.png")
|
GRAPH_STATUS = os.path.join(GRAPH_DIR, "status-3m.png")
|
||||||
|
|
||||||
@@ -817,6 +824,132 @@ def graph_power_5y():
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Graphs: one-week history
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def graph_weekly_status():
|
||||||
|
"""UPS status for the last week."""
|
||||||
|
|
||||||
|
now = int(time.time())
|
||||||
|
run_rrdtool(
|
||||||
|
[
|
||||||
|
"graph", GRAPH_STATUS_1W,
|
||||||
|
"--start", str(now - 7 * 86400),
|
||||||
|
"--end", str(now),
|
||||||
|
"--width", "1000",
|
||||||
|
"--height", "350",
|
||||||
|
"--title", "APC BX950MI - UPS Status (1 Week)",
|
||||||
|
"--vertical-label", "Status",
|
||||||
|
"--lower-limit", "0",
|
||||||
|
"--upper-limit", "4",
|
||||||
|
"--rigid",
|
||||||
|
f"DEF:status={RRD_FILE}:status:LAST",
|
||||||
|
"LINE2:status#0000FF:UPS status",
|
||||||
|
"COMMENT:\\n",
|
||||||
|
"COMMENT:0 = On Line (OL)\\n",
|
||||||
|
"COMMENT:1 = On Battery (OB)\\n",
|
||||||
|
"COMMENT:2 = Low Battery (LB)\\n",
|
||||||
|
"COMMENT:3 = Replace Battery (RB)\\n",
|
||||||
|
"COMMENT:4 = Unknown\\n",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def graph_weekly_metric(
|
||||||
|
output_file,
|
||||||
|
title,
|
||||||
|
vertical_label,
|
||||||
|
definition,
|
||||||
|
line,
|
||||||
|
value_name,
|
||||||
|
unit,
|
||||||
|
limits=None,
|
||||||
|
calculation=None,
|
||||||
|
):
|
||||||
|
"""Create a one-week graph for a numeric RRD data source."""
|
||||||
|
|
||||||
|
now = int(time.time())
|
||||||
|
command = [
|
||||||
|
"graph", output_file,
|
||||||
|
"--start", str(now - 7 * 86400),
|
||||||
|
"--end", str(now),
|
||||||
|
"--width", "1000",
|
||||||
|
"--height", "350",
|
||||||
|
"--title", f"APC BX950MI - {title} (1 Week)",
|
||||||
|
"--vertical-label", vertical_label,
|
||||||
|
]
|
||||||
|
|
||||||
|
if limits:
|
||||||
|
command.extend(
|
||||||
|
[
|
||||||
|
"--lower-limit", str(limits[0]),
|
||||||
|
"--upper-limit", str(limits[1]),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
command.append(definition)
|
||||||
|
|
||||||
|
if calculation:
|
||||||
|
command.append(calculation)
|
||||||
|
|
||||||
|
command.extend(
|
||||||
|
[
|
||||||
|
line,
|
||||||
|
f"GPRINT:{value_name}:MIN:Minimum %.1lf{unit}",
|
||||||
|
f"GPRINT:{value_name}:AVERAGE:Average %.1lf{unit}",
|
||||||
|
f"GPRINT:{value_name}:MAX:Maximum %.1lf{unit}",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
run_rrdtool(command)
|
||||||
|
|
||||||
|
|
||||||
|
def create_weekly_graphs():
|
||||||
|
"""Generate one-week graphs for every collected value."""
|
||||||
|
|
||||||
|
graph_weekly_status()
|
||||||
|
graph_weekly_metric(
|
||||||
|
GRAPH_BATTERY_1W,
|
||||||
|
"Battery Charge",
|
||||||
|
"Charge (%)",
|
||||||
|
f"DEF:charge={RRD_FILE}:battery_charge:AVERAGE",
|
||||||
|
"LINE2:charge#008000:Battery charge",
|
||||||
|
"charge",
|
||||||
|
"%%",
|
||||||
|
limits=(0, 100),
|
||||||
|
)
|
||||||
|
graph_weekly_metric(
|
||||||
|
GRAPH_RUNTIME_1W,
|
||||||
|
"Battery Runtime",
|
||||||
|
"Runtime (minutes)",
|
||||||
|
f"DEF:runtime={RRD_FILE}:battery_runtime:AVERAGE",
|
||||||
|
"LINE2:minutes#800080:Battery runtime",
|
||||||
|
"minutes",
|
||||||
|
" min",
|
||||||
|
calculation="CDEF:minutes=runtime,60,/",
|
||||||
|
)
|
||||||
|
graph_weekly_metric(
|
||||||
|
GRAPH_LOAD_1W,
|
||||||
|
"UPS Load",
|
||||||
|
"Load (%)",
|
||||||
|
f"DEF:load={RRD_FILE}:ups_load:AVERAGE",
|
||||||
|
"LINE2:load#FF8000:UPS load",
|
||||||
|
"load",
|
||||||
|
"%%",
|
||||||
|
limits=(0, 100),
|
||||||
|
)
|
||||||
|
graph_weekly_metric(
|
||||||
|
GRAPH_POWER_1W,
|
||||||
|
"Nominal Power",
|
||||||
|
"Power (W)",
|
||||||
|
f"DEF:power={RRD_FILE}:realpower_nominal:AVERAGE",
|
||||||
|
"LINE2:power#CC0000:Nominal power",
|
||||||
|
"power",
|
||||||
|
" W",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Main
|
# Main
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -840,37 +973,17 @@ def create_graphs():
|
|||||||
graph_power_5y()
|
graph_power_5y()
|
||||||
|
|
||||||
|
|
||||||
def create_dashboard():
|
def write_dashboard(
|
||||||
"""Create a responsive HTML dashboard for the generated graphs."""
|
html_file,
|
||||||
|
page_title,
|
||||||
|
graph_groups,
|
||||||
|
other_page,
|
||||||
|
other_page_label,
|
||||||
|
):
|
||||||
|
"""Write a responsive HTML dashboard for a set of graphs."""
|
||||||
|
|
||||||
generated_at = time.strftime("%Y-%m-%d %H:%M:%S %Z")
|
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 = []
|
sections = []
|
||||||
|
|
||||||
for title, graphs in graph_groups:
|
for title, graphs in graph_groups:
|
||||||
@@ -898,7 +1011,7 @@ def create_dashboard():
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<meta http-equiv="refresh" content="60">
|
<meta http-equiv="refresh" content="60">
|
||||||
<title>APC BX950MI UPS Dashboard</title>
|
<title>{page_title}</title>
|
||||||
<style>
|
<style>
|
||||||
:root {{ color-scheme: light dark; font-family: system-ui, sans-serif; }}
|
:root {{ color-scheme: light dark; font-family: system-ui, sans-serif; }}
|
||||||
body {{ margin: 0; background: #10141c; color: #edf2f7; }}
|
body {{ margin: 0; background: #10141c; color: #edf2f7; }}
|
||||||
@@ -907,6 +1020,9 @@ def create_dashboard():
|
|||||||
h1, h2, h3, p {{ margin-top: 0; }}
|
h1, h2, h3, p {{ margin-top: 0; }}
|
||||||
h1 {{ margin-bottom: .35rem; }}
|
h1 {{ margin-bottom: .35rem; }}
|
||||||
header p {{ color: #aab5c5; }}
|
header p {{ color: #aab5c5; }}
|
||||||
|
nav {{ margin-top: 1rem; }}
|
||||||
|
nav a {{ display: inline-block; padding: .6rem .9rem; border-radius: 8px; background: #263449; color: #edf2f7; text-decoration: none; }}
|
||||||
|
nav a:hover {{ background: #344763; }}
|
||||||
section {{ margin: 1.5rem 0 2.5rem; }}
|
section {{ margin: 1.5rem 0 2.5rem; }}
|
||||||
h2 {{ border-bottom: 1px solid #344054; padding-bottom: .5rem; }}
|
h2 {{ border-bottom: 1px solid #344054; padding-bottom: .5rem; }}
|
||||||
.grid {{ display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }}
|
.grid {{ display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem; }}
|
||||||
@@ -922,8 +1038,9 @@ def create_dashboard():
|
|||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h1>APC BX950MI UPS Dashboard</h1>
|
<h1>{page_title}</h1>
|
||||||
<p>Updated {generated_at} · This page refreshes every 60 seconds.</p>
|
<p>Updated {generated_at} · This page refreshes every 60 seconds.</p>
|
||||||
|
<nav><a href="{other_page}">{other_page_label}</a></nav>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
{''.join(sections)}
|
{''.join(sections)}
|
||||||
@@ -933,10 +1050,60 @@ def create_dashboard():
|
|||||||
</html>
|
</html>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
with open(HTML_FILE, "w", encoding="utf-8") as dashboard:
|
with open(html_file, "w", encoding="utf-8") as dashboard:
|
||||||
dashboard.write(document)
|
dashboard.write(document)
|
||||||
|
|
||||||
print(f"Created dashboard: {HTML_FILE}")
|
print(f"Created dashboard: {html_file}")
|
||||||
|
|
||||||
|
|
||||||
|
def create_dashboards():
|
||||||
|
"""Create the long-term and one-week HTML dashboards."""
|
||||||
|
|
||||||
|
write_dashboard(
|
||||||
|
HTML_FILE,
|
||||||
|
"APC BX950MI UPS Dashboard",
|
||||||
|
[
|
||||||
|
("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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
"week.html",
|
||||||
|
"View one-week history",
|
||||||
|
)
|
||||||
|
|
||||||
|
write_dashboard(
|
||||||
|
WEEKLY_HTML_FILE,
|
||||||
|
"APC BX950MI — One-Week History",
|
||||||
|
[
|
||||||
|
("UPS status", [("Last week", GRAPH_STATUS_1W)]),
|
||||||
|
("Battery charge", [("Last week", GRAPH_BATTERY_1W)]),
|
||||||
|
("Battery runtime", [("Last week", GRAPH_RUNTIME_1W)]),
|
||||||
|
("UPS load", [("Last week", GRAPH_LOAD_1W)]),
|
||||||
|
("Nominal power", [("Last week", GRAPH_POWER_1W)]),
|
||||||
|
],
|
||||||
|
"index.html",
|
||||||
|
"View long-term history",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
@@ -946,7 +1113,9 @@ def main():
|
|||||||
|
|
||||||
create_graphs()
|
create_graphs()
|
||||||
|
|
||||||
create_dashboard()
|
create_weekly_graphs()
|
||||||
|
|
||||||
|
create_dashboards()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user