This commit is contained in:
1 parent
c7a0ed93ff
commit
94f7ed8411
10 files changed
+73
-10
No files matched your search
+6
-1
@@ -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):
|
||||
|
||||
@@ -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
|
||||
new_ui = UIConfig(**ui_dict)
|
||||
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()}
|
||||
@@ -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);
|
||||
|
||||
@@ -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(()=>{});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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 %}
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user