Show energy cost on dashboard
Build and Deploy / build-and-deploy (push) Successful in 25s

This commit is contained in:
Ketan Patel committed 2026-06-20 10:49:06 -04:00
1 parent c7a0ed93ff
commit 94f7ed8411
10 files changed
+72 -9

No files matched your search

+6 -1
View File
@@ -121,7 +121,12 @@ class UIConfig(BaseModel):
allow_resize: bool = True
enable_transfer_burst_alert: bool = False
enable_voltage_deviation_alert: bool = False
energy_cost_per_kwh: float = 0.0
energy_cost_per_kwh: float = Field(0.0, ge=0)
@field_validator("energy_cost_per_kwh", mode="after")
@classmethod
def round_energy_cost_per_kwh(cls, value: float) -> float:
return round(float(value), 4)
class AppConfig(BaseModel):
+4
View File
@@ -1,6 +1,7 @@
"""Configuration CRUD endpoints (UPS, SMTP, UI). All require session+CSRF for writes."""
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import ValidationError
from ..auth import require_session, require_session_and_csrf
from ..config import SMTPConfig, UIConfig, UPSConfig
@@ -142,6 +143,9 @@ async def update_ui_config(
for k, v in payload.items():
if k in ui_dict:
ui_dict[k] = v
try:
new_ui = UIConfig(**ui_dict)
except ValidationError as e:
raise HTTPException(status_code=422, detail=e.errors())
await config_manager.update_ui_config(new_ui)
return {"message": "UI config updated", "ui": new_ui.model_dump()}
+3
View File
@@ -138,6 +138,9 @@
.tile.hidden { display: none; }
.tile.dragging { opacity: 0.5; border: 1px dashed var(--accent); }
.tile.selected { outline: 2px solid var(--accent); }
.tile .energy-summary { flex-direction: column; gap: 4px; text-align: center; }
.tile .energy-summary .big-value { font-size: 18px; line-height: 1.2; }
.tile .energy-cost { color: var(--success); font-size: 13px; font-weight: 700; }
.tile-controls { position: absolute; top: 4px; right: 4px; display: flex; gap: 4px; }
.tile-controls select {
background: var(--surface-3);
+11 -3
View File
@@ -529,7 +529,7 @@ function initTilesFor(name, grid) {
energyTile.dataset.tile = 'energy';
energyTile.style.left = '12px';
energyTile.style.top = '12px';
energyTile.innerHTML = `<h4>Energy Today</h4><div class="tile-body"><span class="big-value" data-energy-val>-- kWh</span></div>`;
energyTile.innerHTML = `<h4>Energy Today</h4><div class="tile-body energy-summary"><span class="big-value" data-energy-val>-- kWh</span><span class="energy-cost" data-energy-cost>--</span></div>`;
const resizeHandle = document.createElement('div'); resizeHandle.className='tile-resize'; energyTile.appendChild(resizeHandle);
grid.appendChild(energyTile);
}
@@ -1194,11 +1194,19 @@ evtSource.onmessage = (e) => {
}).catch(()=>{});
if (uiCfg.show_energy) {
fetch(apiUpsPath(meta.name, '/energy')).then(r => r.json()).then(data => {
if (!data || data.kwh_today == null) return;
const card = getCard(meta.name);
if (!card) return;
const energyVal = card.querySelector('[data-energy-val]');
if (energyVal) energyVal.textContent = data.kwh_today.toFixed(2) + ' kWh';
const energyCost = card.querySelector('[data-energy-cost]');
if (!data || data.kwh_today == null) {
if (energyVal) energyVal.textContent = '-- kWh';
if (energyCost) energyCost.textContent = '--';
return;
}
if (energyVal) energyVal.textContent = `${Number(data.kwh_today).toFixed(4)} kWh`;
if (energyCost) {
energyCost.textContent = data.cost_today == null ? '--' : `$${Number(data.cost_today).toFixed(4)}`;
}
}).catch(()=>{});
}
});
+8 -1
View File
@@ -1,5 +1,11 @@
// Settings page
(async function () {
function normalizeEnergyRate(value) {
const parsed = Number.parseFloat(value);
if (!Number.isFinite(parsed) || parsed < 0) return '0.0000';
return parsed.toFixed(4);
}
async function loadUi() {
const r = await window.apiFetch('/api/config/ui');
if (!r.ok) return;
@@ -9,6 +15,7 @@
const el = form.elements[k];
if (!el) return;
if (el.type === 'checkbox') el.checked = !!v;
else if (k === 'energy_cost_per_kwh') el.value = normalizeEnergyRate(v);
else el.value = v;
});
}
@@ -37,7 +44,7 @@
color_badges: f.color_badges.checked,
enable_transfer_burst_alert: f.enable_transfer_burst_alert.checked,
enable_voltage_deviation_alert: f.enable_voltage_deviation_alert.checked,
energy_cost_per_kwh: parseFloat(f.energy_cost_per_kwh.value) || 0,
energy_cost_per_kwh: Number.parseFloat(normalizeEnergyRate(f.energy_cost_per_kwh.value)),
};
const r = await window.apiFetch('/api/config/ui', {
method: 'PUT', body: JSON.stringify(payload),
+1 -1
View File
@@ -6,7 +6,7 @@
<title>{% block title %}UPS Dashboard{% endblock %}</title>
<meta name="csrf-token" content="{{ request.cookies.get('csrf_token', '') }}" />
<link rel="stylesheet" href="/static/css/base.css?v=5" />
<link rel="stylesheet" href="/static/css/style.css?v=5" />
<link rel="stylesheet" href="/static/css/style.css?v=6" />
{% block head_extra %}{% endblock %}
</head>
<body>
+1 -1
View File
@@ -61,5 +61,5 @@
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/dashboard.js?v=6"></script>
<script src="/static/js/dashboard.js?v=7"></script>
{% endblock %}
+2 -2
View File
@@ -47,7 +47,7 @@
</div>
<div class="field-row">
<label class="field-label" for="energy-cost">Energy cost per kWh ($)</label>
<input class="field-input" id="energy-cost" type="number" step="0.001" min="0" name="energy_cost_per_kwh">
<input class="field-input" id="energy-cost" type="number" step="0.0001" min="0" name="energy_cost_per_kwh" inputmode="decimal" placeholder="0.1365">
</div>
<div class="form-bar">
<button type="submit" class="btn btn-primary">Save UI Settings</button>
@@ -116,5 +116,5 @@
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/settings.js?v=4"></script>
<script src="/static/js/settings.js?v=5"></script>
{% endblock %}
+17
View File
@@ -120,6 +120,23 @@ def test_ups_energy_with_data(authed_client, fake_redis):
assert body["kwh_today"] == 1.0
def test_ups_energy_uses_four_decimal_cost_rate(authed_client, fake_redis):
_add_ups(authed_client)
authed_client.put(
"/api/config/ui",
json={"show_energy": True, "energy_cost_per_kwh": 0.1365},
)
day_str = time.strftime("%Y%m%d")
fake_redis.set(f"ups:energy:u1:{day_str}", "7200000") # 2 kWh
r = authed_client.get("/api/ups/u1/energy")
assert r.status_code == 200
body = r.json()
assert body["kwh_today"] == 2.0
assert body["cost_today"] == 0.273
def test_ups_health(authed_client, fake_redis):
_add_ups(authed_client)
fake_redis.set("ups:health:last_ok:u1", "12345")
+19
View File
@@ -133,3 +133,22 @@ def test_ui_config_update(authed_client):
body = r.json()
assert body["ui"]["show_energy"] is True
assert body["ui"]["energy_cost_per_kwh"] == 0.15
def test_ui_config_energy_cost_keeps_four_decimal_rate(authed_client):
r = authed_client.put(
"/api/config/ui",
json={"show_energy": True, "energy_cost_per_kwh": 0.13654},
)
assert r.status_code == 200
assert r.json()["ui"]["energy_cost_per_kwh"] == 0.1365
def test_ui_config_rejects_negative_energy_cost(authed_client):
r = authed_client.put(
"/api/config/ui",
json={"energy_cost_per_kwh": -0.1},
)
assert r.status_code == 422