monitoring(power): implement six-panel UPS and climate layout

This commit is contained in:
Brad Stein 2026-04-03 20:45:40 -03:00
parent e199d20a3e
commit 758654c9df
5 changed files with 915 additions and 1452 deletions

View File

@ -464,15 +464,75 @@ HECATE_UPS_LOAD_DB = (
HECATE_UPS_LOAD_TETHYS = (
f'max(hecate_ups_load_percent{{{HECATE_SELECTOR},instance="titan-24"}}) or on() vector(0)'
)
HECATE_UPS_DRAW_WATTS_DB = (
f'max((hecate_ups_load_percent{{{HECATE_SELECTOR},instance="titan-db"}} '
f'* hecate_ups_power_nominal_watts{{{HECATE_SELECTOR},instance="titan-db"}}) / 100) or on() vector(0)'
)
HECATE_UPS_DRAW_WATTS_TETHYS = (
f'max((hecate_ups_load_percent{{{HECATE_SELECTOR},instance="titan-24"}} '
f'* hecate_ups_power_nominal_watts{{{HECATE_SELECTOR},instance="titan-24"}}) / 100) or on() vector(0)'
)
HECATE_UPS_DRAW_WATTS_TOTAL = (
f'sum((hecate_ups_load_percent{{{HECATE_SELECTOR}}} * hecate_ups_power_nominal_watts{{{HECATE_SELECTOR}}}) / 100) '
"or on() vector(0)"
)
HECATE_UPS_DRAW_WATTS_DB_SERIES = (
f'((hecate_ups_load_percent{{{HECATE_SELECTOR},instance="titan-db"}} '
f'* hecate_ups_power_nominal_watts{{{HECATE_SELECTOR},instance="titan-db"}}) / 100)'
)
HECATE_UPS_DRAW_WATTS_TETHYS_SERIES = (
f'((hecate_ups_load_percent{{{HECATE_SELECTOR},instance="titan-24"}} '
f'* hecate_ups_power_nominal_watts{{{HECATE_SELECTOR},instance="titan-24"}}) / 100)'
)
HECATE_UPS_DRAW_WATTS_TOTAL_SERIES = (
f'sum((hecate_ups_load_percent{{{HECATE_SELECTOR}}} * hecate_ups_power_nominal_watts{{{HECATE_SELECTOR}}}) / 100)'
)
HECATE_UPS_RUNTIME_BY_SOURCE = f"hecate_ups_runtime_seconds{{{HECATE_SELECTOR}}}"
HECATE_UPS_LOAD_BY_SOURCE = f"hecate_ups_load_percent{{{HECATE_SELECTOR}}}"
HECATE_UPS_CHARGE_BY_SOURCE = f"hecate_ups_battery_charge_percent{{{HECATE_SELECTOR}}}"
HECATE_UPS_TRIGGER_BY_SOURCE = f"hecate_ups_trigger_active{{{HECATE_SELECTOR}}}"
CLIMATE_SENSOR_COUNT = "count(atlas_climate_temperature_celsius) or on() vector(0)"
CLIMATE_TEMP_MAX = "max(atlas_climate_temperature_celsius) or on() vector(0)"
CLIMATE_TEMP_MAX = "max(atlas_climate_tent_temperature_celsius) or max(atlas_climate_temperature_celsius) or on() vector(0)"
CLIMATE_PRESSURE_CURRENT = "max(atlas_climate_tent_pressure_kpa) or max(atlas_climate_pressure_kpa) or on() vector(0)"
CLIMATE_HUMIDITY_MAX = "max(atlas_climate_humidity_percent) or on() vector(0)"
CLIMATE_TEMP_SERIES = "atlas_climate_temperature_celsius"
CLIMATE_FAN_SERIES = "atlas_climate_fan_rpm"
CLIMATE_TEMP_SERIES = "(atlas_climate_tent_temperature_celsius or atlas_climate_temperature_celsius)"
CLIMATE_PRESSURE_SERIES = "(atlas_climate_tent_pressure_kpa or atlas_climate_pressure_kpa)"
CLIMATE_FAN_OUTLET_CURRENT = (
'max(atlas_climate_fan_activity_level{fan_group="outlet"}) '
'or max(atlas_climate_fan_activity_level{position="outlet"}) '
"or on() vector(0)"
)
CLIMATE_FAN_INSIDE_INLET_CURRENT = (
'max(atlas_climate_fan_activity_level{fan_group="inside_inlet"}) '
'or max(atlas_climate_fan_activity_level{position="inside_inlet"}) '
"or on() vector(0)"
)
CLIMATE_FAN_OUTSIDE_INLET_CURRENT = (
'max(atlas_climate_fan_activity_level{fan_group="outside_inlet"}) '
'or max(atlas_climate_fan_activity_level{position="outside_inlet"}) '
"or on() vector(0)"
)
CLIMATE_FAN_INTERIOR_CURRENT = (
'max(atlas_climate_fan_activity_level{fan_group="interior"}) '
'or max(atlas_climate_fan_activity_level{position="interior"}) '
"or on() vector(0)"
)
CLIMATE_FAN_OUTLET_SERIES = (
'(atlas_climate_fan_activity_level{fan_group="outlet"} '
'or atlas_climate_fan_activity_level{position="outlet"})'
)
CLIMATE_FAN_INSIDE_INLET_SERIES = (
'(atlas_climate_fan_activity_level{fan_group="inside_inlet"} '
'or atlas_climate_fan_activity_level{position="inside_inlet"})'
)
CLIMATE_FAN_OUTSIDE_INLET_SERIES = (
'(atlas_climate_fan_activity_level{fan_group="outside_inlet"} '
'or atlas_climate_fan_activity_level{position="outside_inlet"})'
)
CLIMATE_FAN_INTERIOR_SERIES = (
'(atlas_climate_fan_activity_level{fan_group="interior"} '
'or atlas_climate_fan_activity_level{position="interior"})'
)
POSTGRES_CONN_USED = (
'label_replace(sum(pg_stat_activity_count), "conn", "used", "__name__", ".*") '
'or label_replace(max(pg_settings_max_connections), "conn", "max", "__name__", ".*")'
@ -572,6 +632,9 @@ def stat_panel(
instant=False,
value_suffix=None,
links=None,
targets=None,
field_overrides=None,
description=None,
):
"""Return a Grafana stat panel definition."""
defaults = {
@ -592,14 +655,16 @@ def stat_panel(
defaults["custom"]["valueSuffix"] = value_suffix
if decimals is not None:
defaults["decimals"] = decimals
target_list = targets if targets is not None else [{"expr": expr, "refId": "A"}]
panel = {
"id": panel_id,
"type": "stat",
"title": title,
"datasource": PROM_DS,
"gridPos": grid,
"targets": [{"expr": expr, "refId": "A"}],
"fieldConfig": {"defaults": defaults, "overrides": []},
"targets": target_list,
"fieldConfig": {"defaults": defaults, "overrides": field_overrides or []},
"options": {
"colorMode": "value",
"graphMode": "area",
@ -608,12 +673,15 @@ def stat_panel(
"textMode": text_mode,
},
}
if legend:
if legend and len(panel["targets"]) == 1:
panel["targets"][0]["legendFormat"] = legend
if instant:
panel["targets"][0]["instant"] = True
for t in panel["targets"]:
t.setdefault("instant", True)
if links:
panel["links"] = links
if description:
panel["description"] = description
return panel
@ -674,16 +742,20 @@ def timeseries_panel(
legend_calcs=None,
time_from=None,
links=None,
targets=None,
field_overrides=None,
description=None,
):
"""Return a Grafana time-series panel definition."""
target_list = targets if targets is not None else [{"expr": expr, "refId": "A"}]
panel = {
"id": panel_id,
"type": "timeseries",
"title": title,
"datasource": PROM_DS,
"gridPos": grid,
"targets": [{"expr": expr, "refId": "A"}],
"fieldConfig": {"defaults": {"unit": unit}, "overrides": []},
"targets": target_list,
"fieldConfig": {"defaults": {"unit": unit}, "overrides": field_overrides or []},
"options": {
"legend": {
"displayMode": legend_display,
@ -694,7 +766,7 @@ def timeseries_panel(
}
if max_value is not None:
panel["fieldConfig"]["defaults"]["max"] = max_value
if legend:
if legend and len(panel["targets"]) == 1:
panel["targets"][0]["legendFormat"] = legend
if legend_calcs:
panel["options"]["legend"]["calcs"] = legend_calcs
@ -702,6 +774,8 @@ def timeseries_panel(
panel["timeFrom"] = time_from
if links:
panel["links"] = links
if description:
panel["description"] = description
return panel
@ -2777,222 +2851,161 @@ def build_jobs_dashboard():
def build_power_dashboard():
panels = []
on_battery_thresholds = {
"mode": "absolute",
"steps": [
{"color": "green", "value": None},
{"color": "red", "value": 1},
],
}
runtime_thresholds = {
"mode": "absolute",
"steps": [
{"color": "red", "value": None},
{"color": "orange", "value": 600},
{"color": "yellow", "value": 1200},
{"color": "green", "value": 1800},
],
}
charge_thresholds = {
"mode": "absolute",
"steps": [
{"color": "red", "value": None},
{"color": "orange", "value": 25},
{"color": "yellow", "value": 45},
{"color": "green", "value": 65},
],
}
load_thresholds = {
"mode": "absolute",
"steps": [
{"color": "green", "value": None},
{"color": "yellow", "value": 55},
{"color": "orange", "value": 75},
{"color": "red", "value": 90},
],
}
status_mapping = [
{
"type": "value",
"options": {
"0": {"text": "⚡ Charging"},
"1": {"text": "🔋 Discharging"},
},
}
]
panels.append(
stat_panel(
1,
"titan-db UPS Status (On Battery)",
HECATE_UPS_ON_BATTERY_DB,
{"h": 4, "w": 6, "x": 0, "y": 0},
"UPS Current Load",
None,
{"h": 8, "w": 12, "x": 0, "y": 0},
unit="none",
instant=True,
thresholds=on_battery_thresholds,
decimals=1,
text_mode="name_and_value",
targets=[
{"refId": "A", "expr": HECATE_UPS_DRAW_WATTS_DB, "legendFormat": "titan-db Draw (W)", "instant": True},
{"refId": "B", "expr": HECATE_UPS_RUNTIME_DB, "legendFormat": "titan-db Discharge ETA", "instant": True},
{"refId": "C", "expr": HECATE_UPS_ON_BATTERY_DB, "legendFormat": "titan-db Status", "instant": True},
{"refId": "D", "expr": HECATE_UPS_DRAW_WATTS_TETHYS, "legendFormat": "tethys Draw (W)", "instant": True},
{"refId": "E", "expr": HECATE_UPS_RUNTIME_TETHYS, "legendFormat": "tethys Discharge ETA", "instant": True},
{"refId": "F", "expr": HECATE_UPS_ON_BATTERY_TETHYS, "legendFormat": "tethys Status", "instant": True},
],
field_overrides=[
{"matcher": {"id": "byName", "options": "titan-db Draw (W)"}, "properties": [{"id": "unit", "value": "watt"}]},
{"matcher": {"id": "byName", "options": "tethys Draw (W)"}, "properties": [{"id": "unit", "value": "watt"}]},
{"matcher": {"id": "byName", "options": "titan-db Discharge ETA"}, "properties": [{"id": "unit", "value": "s"}]},
{"matcher": {"id": "byName", "options": "tethys Discharge ETA"}, "properties": [{"id": "unit", "value": "s"}]},
{
"matcher": {"id": "byName", "options": "titan-db Status"},
"properties": [{"id": "mappings", "value": status_mapping}],
},
{
"matcher": {"id": "byName", "options": "tethys Status"},
"properties": [{"id": "mappings", "value": status_mapping}],
},
],
description=(
"Per-UPS live snapshot: current draw in watts, estimated battery runtime if discharge started now, and charging/discharging status."
),
)
)
panels.append(
stat_panel(
timeseries_panel(
2,
"tethys UPS Status (On Battery)",
HECATE_UPS_ON_BATTERY_TETHYS,
{"h": 4, "w": 6, "x": 6, "y": 0},
unit="none",
instant=True,
thresholds=on_battery_thresholds,
"UPS History (Power Draw)",
None,
{"h": 8, "w": 12, "x": 12, "y": 0},
unit="watt",
targets=[
{"refId": "A", "expr": HECATE_UPS_DRAW_WATTS_DB_SERIES, "legendFormat": "titan-db"},
{"refId": "B", "expr": HECATE_UPS_DRAW_WATTS_TETHYS_SERIES, "legendFormat": "tethys"},
{"refId": "C", "expr": HECATE_UPS_DRAW_WATTS_TOTAL_SERIES, "legendFormat": "combined"},
],
legend_display="table",
legend_placement="right",
description="Historical UPS power consumption in watts for titan-db, tethys, and combined load.",
)
)
panels.append(
stat_panel(
3,
"titan-db Runtime Remaining",
HECATE_UPS_RUNTIME_DB,
{"h": 4, "w": 6, "x": 12, "y": 0},
unit="s",
decimals=0,
instant=True,
thresholds=runtime_thresholds,
"Current Climate",
None,
{"h": 8, "w": 12, "x": 0, "y": 8},
unit="none",
decimals=2,
text_mode="name_and_value",
targets=[
{"refId": "A", "expr": CLIMATE_TEMP_MAX, "legendFormat": "Tent Temp (°C)", "instant": True},
{"refId": "B", "expr": CLIMATE_PRESSURE_CURRENT, "legendFormat": "Tent Pressure (kPa)", "instant": True},
],
field_overrides=[
{"matcher": {"id": "byName", "options": "Tent Temp (°C)"}, "properties": [{"id": "unit", "value": "celsius"}]},
{"matcher": {"id": "byName", "options": "Tent Pressure (kPa)"}, "properties": [{"id": "unit", "value": "none"}]},
],
description="Current tent temperature and air pressure. These render once climate telemetry is online.",
)
)
panels.append(
stat_panel(
timeseries_panel(
4,
"tethys Runtime Remaining",
HECATE_UPS_RUNTIME_TETHYS,
{"h": 4, "w": 6, "x": 18, "y": 0},
unit="s",
decimals=0,
instant=True,
thresholds=runtime_thresholds,
"Climate History",
None,
{"h": 8, "w": 12, "x": 12, "y": 8},
unit="celsius",
targets=[
{"refId": "A", "expr": CLIMATE_TEMP_SERIES, "legendFormat": "Temperature (°C)"},
{"refId": "B", "expr": CLIMATE_PRESSURE_SERIES, "legendFormat": "Pressure (kPa)"},
],
field_overrides=[
{
"matcher": {"id": "byName", "options": "Pressure (kPa)"},
"properties": [
{"id": "unit", "value": "none"},
{"id": "custom.axisPlacement", "value": "right"},
{"id": "custom.axisLabel", "value": "kPa"},
{"id": "decimals", "value": 2},
],
}
],
legend_display="table",
legend_placement="right",
description="Two-axis chart: tent temperature (left axis) and tent pressure in kPa (right axis).",
)
)
panels.append(
stat_panel(
5,
"titan-db Battery Charge",
HECATE_UPS_BATTERY_CHARGE_DB,
{"h": 4, "w": 6, "x": 0, "y": 4},
unit="percent",
decimals=1,
instant=True,
thresholds=charge_thresholds,
)
)
panels.append(
stat_panel(
6,
"tethys Battery Charge",
HECATE_UPS_BATTERY_CHARGE_TETHYS,
{"h": 4, "w": 6, "x": 6, "y": 4},
unit="percent",
decimals=1,
instant=True,
thresholds=charge_thresholds,
)
)
panels.append(
stat_panel(
7,
"titan-db UPS Load",
HECATE_UPS_LOAD_DB,
{"h": 4, "w": 6, "x": 12, "y": 4},
unit="percent",
decimals=1,
instant=True,
thresholds=load_thresholds,
)
)
panels.append(
stat_panel(
8,
"tethys UPS Load",
HECATE_UPS_LOAD_TETHYS,
{"h": 4, "w": 6, "x": 18, "y": 4},
unit="percent",
decimals=1,
instant=True,
thresholds=load_thresholds,
)
)
panels.append(
timeseries_panel(
9,
"UPS Runtime by Source",
HECATE_UPS_RUNTIME_BY_SOURCE,
{"h": 8, "w": 12, "x": 0, "y": 8},
unit="s",
legend="{{instance}}/{{source}}",
legend_display="table",
legend_placement="right",
)
)
panels.append(
timeseries_panel(
10,
"UPS Load % by Source",
HECATE_UPS_LOAD_BY_SOURCE,
{"h": 8, "w": 12, "x": 12, "y": 8},
unit="percent",
legend="{{instance}}/{{source}}",
legend_display="table",
legend_placement="right",
)
)
panels.append(
timeseries_panel(
11,
"UPS Battery Charge % by Source",
HECATE_UPS_CHARGE_BY_SOURCE,
"Fan Activity",
None,
{"h": 8, "w": 12, "x": 0, "y": 16},
unit="percent",
legend="{{instance}}/{{source}}",
legend_display="table",
legend_placement="right",
unit="none",
decimals=1,
text_mode="name_and_value",
targets=[
{"refId": "A", "expr": CLIMATE_FAN_OUTLET_CURRENT, "legendFormat": "Outlet", "instant": True},
{"refId": "B", "expr": CLIMATE_FAN_INSIDE_INLET_CURRENT, "legendFormat": "Inside Inlet", "instant": True},
{"refId": "C", "expr": CLIMATE_FAN_OUTSIDE_INLET_CURRENT, "legendFormat": "Outside Inlet", "instant": True},
{"refId": "D", "expr": CLIMATE_FAN_INTERIOR_CURRENT, "legendFormat": "Interior", "instant": True},
],
thresholds={
"mode": "absolute",
"steps": [
{"color": "green", "value": None},
{"color": "yellow", "value": 7},
{"color": "red", "value": 9},
],
},
description="Current fan activity levels (0-10): outlet, inside inlet, outside inlet, and interior.",
)
)
panels.append(
timeseries_panel(
12,
"UPS Trigger Activity by Source",
HECATE_UPS_TRIGGER_BY_SOURCE,
6,
"Fan History (0-10)",
None,
{"h": 8, "w": 12, "x": 12, "y": 16},
unit="none",
legend="{{instance}}/{{source}}",
max_value=10,
targets=[
{"refId": "A", "expr": CLIMATE_FAN_OUTLET_SERIES, "legendFormat": "Outlet"},
{"refId": "B", "expr": CLIMATE_FAN_INSIDE_INLET_SERIES, "legendFormat": "Inside Inlet"},
{"refId": "C", "expr": CLIMATE_FAN_OUTSIDE_INLET_SERIES, "legendFormat": "Outside Inlet"},
{"refId": "D", "expr": CLIMATE_FAN_INTERIOR_SERIES, "legendFormat": "Interior"},
],
legend_display="table",
legend_placement="right",
description="Historical fan activity for all four fan groups (0-10 scale).",
)
)
climate_temp_panel = timeseries_panel(
13,
"Climate Temperature Over Time",
CLIMATE_TEMP_SERIES,
{"h": 8, "w": 12, "x": 0, "y": 24},
unit="celsius",
legend="{{sensor}}",
legend_display="table",
legend_placement="right",
)
climate_temp_panel["description"] = (
"Future climate collector panel: temperature probes (inside inlet, outside inlet, outlet, interior)."
)
panels.append(climate_temp_panel)
climate_fan_panel = timeseries_panel(
14,
"Climate Fan Speed Over Time",
CLIMATE_FAN_SERIES,
{"h": 8, "w": 12, "x": 12, "y": 24},
unit="rpm",
legend="{{position}}",
legend_display="table",
legend_placement="right",
)
climate_fan_panel["description"] = (
"Future climate collector panel: fan RPM by position (inside inlet, outside inlet, outlet, interior)."
)
panels.append(climate_fan_panel)
status_panel = table_panel(
15,
"UPS Status Snapshot (OL/OB/LB)",
f"max by (instance, source, status, target) ({HECATE_UPS_RUNTIME_BY_SOURCE})",
{"h": 6, "w": 24, "x": 0, "y": 32},
unit="s",
transformations=[{"id": "labelsToFields", "options": {}}, {"id": "sortBy", "options": {"fields": ["instance"], "order": "asc"}}],
instant=True,
)
status_panel["description"] = "NUT status flags: OL=on line (utility), OB=on battery, LB=low battery."
panels.append(status_panel)
return {
"uid": "atlas-power",

View File

@ -1311,7 +1311,7 @@
},
"targets": [
{
"expr": "max(atlas_climate_temperature_celsius) or on() vector(0)",
"expr": "max(atlas_climate_tent_temperature_celsius) or max(atlas_climate_temperature_celsius) or on() vector(0)",
"refId": "A"
}
],

File diff suppressed because it is too large Load Diff

View File

@ -1320,7 +1320,7 @@ data:
},
"targets": [
{
"expr": "max(atlas_climate_temperature_celsius) or on() vector(0)",
"expr": "max(atlas_climate_tent_temperature_celsius) or max(atlas_climate_temperature_celsius) or on() vector(0)",
"refId": "A"
}
],

File diff suppressed because it is too large Load Diff