feat(web): add heat pump schematic and device status screens
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
649d2e9cdd
commit
58ffc2e056
3 files changed
+128
No files matched your search
@@ -0,0 +1,53 @@
|
||||
import { getJson } from "./api.js";
|
||||
import { celsius, number, hour } from "./format.js";
|
||||
import { schematic } from "./schematic.js";
|
||||
|
||||
const HEADLINES = {
|
||||
IDLE: "Pompa odpoczywa",
|
||||
HEATING_CWU: "Grzeje wodę (CWU)",
|
||||
HEATING_CO: "Grzeje dom (CO)",
|
||||
HEATING_BOTH: "Grzeje dom i wodę",
|
||||
};
|
||||
|
||||
const PUMP_NAMES = {
|
||||
ground: "Pompa gruntowa", buffer: "Pompa bufora", co: "Pompa CO",
|
||||
cwu: "Pompa CWU", circulation: "Pompa cyrkulacji",
|
||||
};
|
||||
|
||||
const row = (label, value) => `<div><span>${label}</span><b>${value}</b></div>`;
|
||||
|
||||
function pumpRows(pumps) {
|
||||
return Object.entries(PUMP_NAMES)
|
||||
.map(([key, name]) => row(name, pumps[key] ? "działa" : "stoi")).join("");
|
||||
}
|
||||
|
||||
function view(data) {
|
||||
const { targets, setpoints: s, temperatures: t } = data;
|
||||
const alarm = data.alarm
|
||||
? `<div class="banner bad" role="alert">ALARM pompy ciepła. Sprawdź sterownik w domu.</div>` : "";
|
||||
return `${alarm}
|
||||
<section class="card">
|
||||
<p class="headline">${HEADLINES[data.state]}</p>
|
||||
<p class="muted">Odczyt z ${hour(new Date(data.time))}</p>
|
||||
${schematic(data)}
|
||||
</section>
|
||||
<section class="card rows"><h2>Pompy</h2>${pumpRows(data.pumps)}</section>
|
||||
<section class="card rows"><h2>Temperatury</h2>
|
||||
${row("Na zewnątrz", celsius(t.outdoor))}
|
||||
${row("Średnia zewnętrzna", celsius(t.outdoorAvg))}
|
||||
</section>
|
||||
<section class="card rows"><h2>Cele teraz</h2>
|
||||
${row("Pokój", celsius(targets.room))}
|
||||
${row("Woda grzewcza CO", celsius(targets.co))}
|
||||
${row("Ciepła woda CWU", celsius(targets.cwu))}
|
||||
</section>
|
||||
<section class="card rows"><h2>Nastawy</h2>
|
||||
${row("CO oszczędnie", `${number(s.coEconomy)} °C`)}
|
||||
${row("CO komfort", `${number(s.coComfort)} °C`)}
|
||||
${row("CWU oszczędnie", `${number(s.cwuEconomy)} °C`)}
|
||||
${row("CWU komfort", `${number(s.cwuComfort)} °C`)}
|
||||
<p class="muted">Tylko podgląd. Zmiana nastaw będzie później.</p>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
export default { load: () => getJson("api/heat-pump"), view };
|
||||
@@ -0,0 +1,47 @@
|
||||
import { celsius, number } from "./format.js";
|
||||
|
||||
// The installation as one picture: ground loop → heat pump → CO buffer → house, and the CWU
|
||||
// tank. The solar collector has no sensor, so it is drawn without values.
|
||||
|
||||
const on = (flag) => (flag ? " on" : "");
|
||||
|
||||
function pump(x, y, running) {
|
||||
return `<g class="${running ? "on" : ""}" transform="translate(${x} ${y})">
|
||||
<circle class="pump" r="13"/>
|
||||
<path class="spokes" d="M-8 0H8M0-8V8"/></g>`;
|
||||
}
|
||||
|
||||
function box(x, y, width, height, lines) {
|
||||
const text = lines.map(([content, cls], i) =>
|
||||
`<text x="${x + 10}" y="${y + 22 + i * 20}" class="${cls ?? ""}" font-size="${cls ? 14.5 : 16.5}"
|
||||
font-weight="${cls ? 400 : 700}">${content}</text>`).join("");
|
||||
return `<rect class="box" x="${x}" y="${y}" width="${width}" height="${height}" rx="10"/>${text}`;
|
||||
}
|
||||
|
||||
export function schematic(view) {
|
||||
const { temperatures: t, pumps, state } = view;
|
||||
const heatsCo = state === "HEATING_CO" || state === "HEATING_BOTH";
|
||||
const heatsCwu = state === "HEATING_CWU" || state === "HEATING_BOTH";
|
||||
const compressor = view.compressorPercent == null ? "—" : `${number(view.compressorPercent)} %`;
|
||||
return `<svg class="schematic" viewBox="0 0 360 460" role="img"
|
||||
aria-label="Schemat instalacji: odwierty, pompa ciepła, bufor CO, dom i zbiornik CWU">
|
||||
<path class="pipe cold${on(pumps.ground)}" d="M150 78V130M210 78V130"/>
|
||||
<path class="pipe${on(heatsCo)}" d="M100 218V262M100 346V376"/>
|
||||
<path class="pipe${on(heatsCwu)}" d="M260 218V262"/>
|
||||
${box(15, 10, 330, 68, [["Odwierty (dolne źródło)"],
|
||||
[`Do pompy ${celsius(t.groundIn)}`, "sub"], [`Z pompy ${celsius(t.groundOut)}`, "sub"]])}
|
||||
${box(15, 130, 330, 90, [["Pompa ciepła"],
|
||||
[`Sprężarka ${compressor} · grzałka ${view.heaterOn ? "włączona" : "wyłączona"}`, "sub"],
|
||||
[`Wyjście pompy ${celsius(t.pumpOutlet)}`, "sub"]])}
|
||||
${box(15, 262, 165, 84, [["Bufor CO"], [celsius(t.coBuffer), "sub"], [`Cel ${celsius(view.targets.co)}`, "sub"]])}
|
||||
${box(190, 262, 155, 84, [["Zbiornik CWU"], [celsius(t.cwu), "sub"], [`Cel ${celsius(view.targets.cwu)}`, "sub"]])}
|
||||
${box(15, 376, 165, 70, [["Dom (grzejniki)"], [`Wewnątrz ${celsius(t.indoor)}`, "sub"], [`Cel ${celsius(view.targets.room)}`, "sub"]])}
|
||||
${box(190, 376, 155, 70, [["Kolektor słoneczny"], ["na dachu, bez pomiaru", "sub"]])}
|
||||
<path class="pipe" d="M270 346V376" stroke-dasharray="3 7"/>
|
||||
${pump(210, 104, pumps.ground)}
|
||||
${pump(100, 240, pumps.buffer)}
|
||||
${pump(100, 361, pumps.co)}
|
||||
${pump(260, 240, pumps.cwu)}
|
||||
${pump(145, 361, pumps.circulation)}
|
||||
</svg>`;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { getJson } from "./api.js";
|
||||
import { ago } from "./format.js";
|
||||
|
||||
const DEVICE_NAMES = {
|
||||
"heat-pump": "Pompa ciepła",
|
||||
"house-meter": "Licznik domu",
|
||||
bme280: "Czujnik pogody",
|
||||
deye: "Falownik Deye (bateria, PV)",
|
||||
fronius: "Falownik Fronius (PV)",
|
||||
};
|
||||
|
||||
const dot = (up) => `<span class="dot ${up ? "" : "down"}" aria-hidden="true"></span>`;
|
||||
|
||||
function device(d) {
|
||||
const last = d.lastSuccess ? `ostatni odczyt ${ago(d.lastSuccess)}` : "brak odczytu";
|
||||
const detail = d.up ? last : `nie odpowiada, ${last}`;
|
||||
return `<div><span>${dot(d.up)}${DEVICE_NAMES[d.name] ?? d.name}<br>
|
||||
<span class="muted">${detail}</span></span><b>${d.up ? "działa" : "brak"}</b></div>`;
|
||||
}
|
||||
|
||||
function view(data) {
|
||||
const gateway = data.gatewayReachable
|
||||
? `${dot(true)}Dom odpowiada` : `${dot(false)}Dom nie odpowiada`;
|
||||
return `<section class="card"><h2>Połączenie z domem</h2><p class="headline">${gateway}</p></section>
|
||||
<section class="card rows"><h2>Urządzenia</h2>${data.devices.map(device).join("")}</section>`;
|
||||
}
|
||||
|
||||
export default { load: () => getJson("api/health"), view };
|
||||
Reference in new issue
Block a user