Add rich runtime tiles and integrated ESPHome firmware management
This commit is contained in:
@@ -201,7 +201,7 @@ function renderTiles() {
|
||||
const li = node("li", undefined, "tile");
|
||||
li.draggable = true;
|
||||
li.ondragstart = (e) => {
|
||||
if (e.target instanceof HTMLInputElement) {
|
||||
if (e.target.closest("input, select, button")) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
@@ -255,6 +255,94 @@ function renderTiles() {
|
||||
b.onclick = action;
|
||||
actions.append(b);
|
||||
}
|
||||
const options = node("details", undefined, "tile-options");
|
||||
options.append(node("summary", "Tegelinstellingen"));
|
||||
const domain = tile.entity.split(".")[0];
|
||||
const fields = [
|
||||
[
|
||||
"tap",
|
||||
"Bij aantikken",
|
||||
[
|
||||
["auto", "Automatisch"],
|
||||
["detail", "Bediening openen"],
|
||||
["none", "Alleen bekijken"],
|
||||
],
|
||||
],
|
||||
[
|
||||
"display",
|
||||
"Weergave",
|
||||
[
|
||||
["standard", "Naam en status"],
|
||||
["watch", "Grote waarde"],
|
||||
],
|
||||
],
|
||||
];
|
||||
if (
|
||||
["light", "switch", "input_boolean", "fan", "media_player"].includes(
|
||||
domain,
|
||||
)
|
||||
)
|
||||
fields[0][2].push(["toggle", "Aan / uit"]);
|
||||
if (
|
||||
[
|
||||
"light",
|
||||
"fan",
|
||||
"cover",
|
||||
"number",
|
||||
"input_number",
|
||||
"media_player",
|
||||
].includes(domain)
|
||||
)
|
||||
fields.push([
|
||||
"inline",
|
||||
"Op de tegel",
|
||||
[
|
||||
["none", "Geen extra bediening"],
|
||||
["slider", "Mini-schuif"],
|
||||
],
|
||||
]);
|
||||
if (domain === "sensor")
|
||||
fields.push([
|
||||
"history_hours",
|
||||
"Geschiedenis",
|
||||
[
|
||||
[1, "1 uur"],
|
||||
[6, "6 uur"],
|
||||
[24, "24 uur"],
|
||||
],
|
||||
]);
|
||||
for (const [key, title, choices] of fields) {
|
||||
const label = node("label", title),
|
||||
input = node("select");
|
||||
for (const [value, text] of choices) {
|
||||
const option = node("option", text);
|
||||
option.value = value;
|
||||
input.append(option);
|
||||
}
|
||||
input.value =
|
||||
tile.options?.[key] ??
|
||||
(key === "tap"
|
||||
? "auto"
|
||||
: key === "display"
|
||||
? "standard"
|
||||
: key === "history_hours"
|
||||
? 24
|
||||
: "none");
|
||||
input.onchange = () => {
|
||||
tile.options = {
|
||||
...tile.options,
|
||||
[key]: key === "history_hours" ? Number(input.value) : input.value,
|
||||
};
|
||||
if (key === "display" && input.value === "watch")
|
||||
tile.options.inline = "none";
|
||||
if (key === "inline" && input.value === "slider")
|
||||
tile.options.display = "standard";
|
||||
markDirty();
|
||||
};
|
||||
label.append(input);
|
||||
options.append(label);
|
||||
}
|
||||
content.append(options);
|
||||
li.append(node("span", "⠿", "grip"), content, actions);
|
||||
$("#tiles").append(li);
|
||||
});
|
||||
@@ -356,6 +444,10 @@ for (const [value, label] of [
|
||||
["scene", "Scènes"],
|
||||
["vacuum", "Vacuum"],
|
||||
["sensor", "Sensoren"],
|
||||
["media_player", "Media"],
|
||||
["weather", "Weer"],
|
||||
["number", "Waarden"],
|
||||
["select", "Keuzelijsten"],
|
||||
]) {
|
||||
const b = node("button", label, value === "" ? "active" : "");
|
||||
b.onclick = () => {
|
||||
@@ -430,3 +522,120 @@ window.addEventListener("beforeunload", (e) => {
|
||||
});
|
||||
refresh();
|
||||
setInterval(refresh, 10000);
|
||||
|
||||
// Shared firmware workspace; always select a concrete profile and upload target.
|
||||
let firmwarePoll;
|
||||
async function firmwareRefresh(initial = false) {
|
||||
try {
|
||||
const data = await (await api("firmware")).json();
|
||||
if (initial) {
|
||||
$("#firmware-file").replaceChildren(
|
||||
...data.profiles.map((p) => {
|
||||
const o = node("option", p.file);
|
||||
o.value = p.file;
|
||||
return o;
|
||||
}),
|
||||
);
|
||||
$("#firmware-port").replaceChildren(
|
||||
...[["ota", "Wifi / OTA"], ...data.ports.map((p) => [p, p])].map(
|
||||
([value, text]) => {
|
||||
const o = node("option", text);
|
||||
o.value = value;
|
||||
return o;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
const running = data.job?.state === "running";
|
||||
for (const id of ["validate", "build", "install"])
|
||||
$("#firmware-" + id).disabled =
|
||||
running || !data.available || !data.profiles.length;
|
||||
$("#firmware-status").textContent = !data.available
|
||||
? "ESPHome CLI ontbreekt. Werk de app bij naar 0.2.0."
|
||||
: data.job
|
||||
? `${data.job.file} · ${data.job.action} · ${data.job.state}`
|
||||
: "Kies het bedoelde profiel en een USB-poort of IP-adres.";
|
||||
$("#firmware-log").textContent = data.logs.join("\n");
|
||||
} catch (e) {
|
||||
toast(e.message);
|
||||
}
|
||||
}
|
||||
$("#open-firmware").onclick = () => {
|
||||
$("#firmware-dialog").showModal();
|
||||
firmwareRefresh(true);
|
||||
clearInterval(firmwarePoll);
|
||||
firmwarePoll = setInterval(() => firmwareRefresh(), 3000);
|
||||
};
|
||||
$("#close-firmware").onclick = () => {
|
||||
$("#firmware-dialog").close();
|
||||
clearInterval(firmwarePoll);
|
||||
};
|
||||
$("#firmware-dialog").addEventListener("close", () =>
|
||||
clearInterval(firmwarePoll),
|
||||
);
|
||||
$("#firmware-port").onchange = () =>
|
||||
($("#firmware-host-label").hidden = $("#firmware-port").value !== "ota");
|
||||
$("#firmware-file").onchange = () => {
|
||||
$("#firmware-host").placeholder = $("#firmware-file").value.replace(
|
||||
/\.yaml$/,
|
||||
".local",
|
||||
);
|
||||
};
|
||||
for (const action of ["validate", "build", "install"])
|
||||
$("#firmware-" + action).onclick = async () => {
|
||||
try {
|
||||
await api("firmware/jobs", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
file: $("#firmware-file").value,
|
||||
action:
|
||||
action === "validate"
|
||||
? "validate"
|
||||
: action === "build"
|
||||
? "build"
|
||||
: "install",
|
||||
target:
|
||||
$("#firmware-port").value === "ota"
|
||||
? $("#firmware-host").value.trim()
|
||||
: $("#firmware-port").value,
|
||||
}),
|
||||
});
|
||||
await firmwareRefresh();
|
||||
} catch (e) {
|
||||
toast(e.message);
|
||||
}
|
||||
};
|
||||
$("#create-profile").onclick = async () => {
|
||||
const form = $("#install-form");
|
||||
if (!form.reportValidity()) return;
|
||||
try {
|
||||
const data = await (
|
||||
await api("firmware/profiles", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(Object.fromEntries(new FormData(form))),
|
||||
})
|
||||
).json();
|
||||
generatedName = data.file.replace(/\.yaml$/, " ").trim();
|
||||
$("#yaml-output").value = data.yaml;
|
||||
$("#yaml-result").hidden = false;
|
||||
$("#install-status").textContent =
|
||||
`${data.file} bewaard. Open Firmware & USB om te installeren.`;
|
||||
form.elements.wifi_password.value = "";
|
||||
} catch (e) {
|
||||
toast(e.message);
|
||||
}
|
||||
};
|
||||
$("#inspect").onclick = async () => {
|
||||
if (!selected) return;
|
||||
try {
|
||||
$("#inspection").textContent = JSON.stringify(
|
||||
await (
|
||||
await api(`screens/${encodeURIComponent(selected)}/inspect`)
|
||||
).json(),
|
||||
null,
|
||||
2,
|
||||
);
|
||||
} catch (e) {
|
||||
toast(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<strong>ESP Screens</strong><small>Je huis. Jouw bediening.</small>
|
||||
</div>
|
||||
</div>
|
||||
<button id="open-firmware" class="quiet">Firmware & USB</button>
|
||||
<button id="new-screen">+ Nieuw scherm</button>
|
||||
</header>
|
||||
<main>
|
||||
@@ -99,6 +100,16 @@
|
||||
Gelijke begin- en eindtijd schakelen het nachtvenster uit.
|
||||
</p>
|
||||
</details>
|
||||
<details class="screen-settings">
|
||||
<summary>
|
||||
Inspector
|
||||
<span class="hint"
|
||||
>Verbinding, status en ondersteunde eigenschappen</span
|
||||
>
|
||||
</summary>
|
||||
<button id="inspect" class="quiet">Lees actuele gegevens</button>
|
||||
<pre id="inspection">Selecteer een scherm en lees de gegevens.</pre>
|
||||
</details>
|
||||
<footer class="savebar">
|
||||
<div>
|
||||
<strong id="dirty">Alles opgeslagen</strong
|
||||
@@ -111,12 +122,9 @@
|
||||
</section>
|
||||
</div>
|
||||
<p class="firmware-link">
|
||||
<a href="/5c53de3b_esphome" target="_top"
|
||||
>Firmware bijwerken in ESPHome ↗</a
|
||||
><small
|
||||
>Open je bestaande apparaat → Install → Wirelessly. Je tegels en
|
||||
sleutels blijven behouden.</small
|
||||
>
|
||||
Firmware bouwen en via USB of wifi installeren kan via
|
||||
<b>Firmware & USB</b> bovenaan. Je bestaande YAML en secrets blijven
|
||||
bewaard.
|
||||
</p>
|
||||
<details class="help">
|
||||
<summary>Hoe werkt het dagelijks?</summary>
|
||||
@@ -125,12 +133,13 @@
|
||||
<b>Opslaan & naar scherm</b>. De app houdt de status bij. Voor een
|
||||
kleurenlamp: houd de tegel ingedrukt voor kleur, temperatuur en
|
||||
helderheid. Climate, vacuum en cover openen hun kaart direct. Sensoren
|
||||
tonen alleen hun waarde.
|
||||
openen hun HA-geschiedenis met één tik. Per tegel kies je een grote
|
||||
waarde, mini-schuif of klikgedrag.
|
||||
</p>
|
||||
<p>
|
||||
Alleen nieuwe firmware vraagt om een installatie via ESPHome Device
|
||||
Builder. Je tegels blijven in deze app bewaard. Een offline scherm
|
||||
krijgt de wijzigingen zodra het terug is.
|
||||
Nieuwe firmware bouw en installeer je hier met de ingebouwde ESPHome
|
||||
CLI. Je tegels blijven in deze app bewaard. Een offline scherm krijgt
|
||||
de wijzigingen zodra het terug is.
|
||||
</p>
|
||||
</details>
|
||||
</main>
|
||||
@@ -185,7 +194,21 @@
|
||||
>Kleine letters, cijfers en streepjes. Elk scherm een andere
|
||||
naam.</small
|
||||
></label
|
||||
><button type="submit" id="download">Maak installatie-YAML</button>
|
||||
><label
|
||||
>Wifi-naam (alleen bij eerste installatie)<input
|
||||
name="wifi_ssid"
|
||||
autocomplete="off"
|
||||
/></label>
|
||||
<label
|
||||
>Wifiwachtwoord<input
|
||||
name="wifi_password"
|
||||
type="password"
|
||||
autocomplete="new-password"
|
||||
/></label>
|
||||
<button type="button" id="create-profile">
|
||||
Bewaar profiel in ESP Screens
|
||||
</button>
|
||||
<button type="submit" id="download">Maak installatie-YAML</button>
|
||||
<p id="install-status" role="status"></p>
|
||||
<div id="yaml-result" hidden>
|
||||
<label
|
||||
@@ -205,36 +228,62 @@
|
||||
</div>
|
||||
<ol class="steps">
|
||||
<li>
|
||||
<b>Open ESPHome Device Builder.</b> Voeg een nieuw apparaat toe, sla
|
||||
de eerste installatie over en vervang de inhoud via Edit door het
|
||||
gedownloade bestand.
|
||||
<b>Bewaar je profiel.</b> Bestaande wifi-secrets worden behouden;
|
||||
bij een eerste installatie vul je wifi hierboven in.
|
||||
</li>
|
||||
<li>
|
||||
<b>Vul wifi eenmalig in bij Secrets.</b> Gebruik
|
||||
<code>wifi_ssid</code> en <code>wifi_password</code>.
|
||||
<b>Open Firmware & USB.</b> Kies dit profiel, sluit het scherm op je
|
||||
Raspberry aan en selecteer de juiste USB-poort. Kies Bouwen &
|
||||
installeren.
|
||||
</li>
|
||||
<li>
|
||||
<b>Sluit USB aan op je Home Assistant Raspberry.</b> Klik Install →
|
||||
Plug into the computer running ESPHome Device Builder en kies de
|
||||
USB-poort.
|
||||
<b>Koppel in HA → Apparaten & diensten → ESPHome.</b> Gebruik de
|
||||
API-sleutel uit je eigen YAML en sta HA-acties toe.
|
||||
</li>
|
||||
<li>
|
||||
<b>Koppel in Instellingen → Apparaten & diensten → ESPHome.</b>
|
||||
Gebruik de <code>api.encryption.key</code> uit je bestand en sta
|
||||
Home Assistant-acties toe bij Configureren.
|
||||
<b>Kies hier je tegels.</b> CYD vraagt bij de eerste start om
|
||||
kalibratie; Guition niet.
|
||||
</li>
|
||||
<li>
|
||||
<b>Open deze pagina en kies je tegels.</b> CYD toont bij de eerste
|
||||
start een kalibratie: tik ieder kruisje drie keer. Guition hoeft
|
||||
niet gekalibreerd.
|
||||
<b>Later updaten?</b> Kies hetzelfde profiel en het IP-adres van je
|
||||
scherm in Firmware & USB.
|
||||
</li>
|
||||
</ol>
|
||||
<p class="hint">
|
||||
Bewaar je YAML met sleutels. Gebruik later Install → Wirelessly voor
|
||||
firmware-updates. Tegels wijzigen doe je hier.
|
||||
Bewaar je YAML met sleutels. Gebruik later Firmware & USB met het
|
||||
IP-adres voor draadloze updates. Tegels wijzigen doe je hier.
|
||||
</p>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="firmware-dialog">
|
||||
<div class="dialog-head">
|
||||
<h2>Firmware & USB</h2>
|
||||
<button id="close-firmware" class="quiet">Sluiten</button>
|
||||
</div>
|
||||
<p>
|
||||
De ingebouwde ESPHome CLI gebruikt je eigen YAML en secrets. Tegels
|
||||
blijven in ESP Screens staan. Op de Raspberry kan de eerste build enkele
|
||||
minuten duren.
|
||||
</p>
|
||||
<label>ESPHome-profiel<select id="firmware-file"></select></label>
|
||||
<label
|
||||
>Installeren naar<select id="firmware-port">
|
||||
<option value="ota">Wifi / OTA</option>
|
||||
</select></label
|
||||
>
|
||||
<label id="firmware-host-label"
|
||||
>IP-adres of hostnaam<input
|
||||
id="firmware-host"
|
||||
placeholder="scherm-woonkamer.local"
|
||||
/></label>
|
||||
<div class="yaml-actions">
|
||||
<button id="firmware-validate" class="quiet">Controleren</button
|
||||
><button id="firmware-build" class="quiet">Alleen bouwen</button
|
||||
><button id="firmware-install">Bouwen & installeren</button>
|
||||
</div>
|
||||
<p id="firmware-status" role="status"></p>
|
||||
<pre id="firmware-log"></pre>
|
||||
</dialog>
|
||||
<div id="toast" role="status" hidden></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -616,3 +616,42 @@ textarea {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.tile-options {
|
||||
margin-top: 10px;
|
||||
font-size: 13px;
|
||||
}
|
||||
.tile-options label {
|
||||
display: block;
|
||||
margin: 8px 0;
|
||||
}
|
||||
select {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #d8dfe5;
|
||||
border-radius: 9px;
|
||||
background: white;
|
||||
color: #152331;
|
||||
font: inherit;
|
||||
}
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
max-height: 340px;
|
||||
overflow: auto;
|
||||
background: #f0f3f6;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
font-size: 12px;
|
||||
}
|
||||
#firmware-dialog {
|
||||
max-width: 780px;
|
||||
width: 90vw;
|
||||
}
|
||||
.tile-actions {
|
||||
align-self: start;
|
||||
}
|
||||
.tile .tile-options input,
|
||||
.tile .tile-options select {
|
||||
cursor: auto;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user