Improve installer readability and validation feedback
This commit is contained in:
@@ -1,3 +1,8 @@
|
|||||||
|
# 0.1.1
|
||||||
|
|
||||||
|
- Duidelijke foutmelding bij een ongeldige naam, te veel tegels of dubbele entiteiten.
|
||||||
|
- Updatepad met behoud van bestaande schermindelingen.
|
||||||
|
|
||||||
# 0.1.0
|
# 0.1.0
|
||||||
|
|
||||||
- Eerste ESP Screen Manager met Home Assistant Ingress.
|
- Eerste ESP Screen Manager met Home Assistant Ingress.
|
||||||
|
|||||||
@@ -206,7 +206,9 @@ def create_app(manager, development=False):
|
|||||||
raise web.HTTPForbidden(text='Vernieuw deze pagina en probeer opnieuw.')
|
raise web.HTTPForbidden(text='Vernieuw deze pagina en probeer opnieuw.')
|
||||||
try:
|
try:
|
||||||
response = await handler(request)
|
response = await handler(request)
|
||||||
except (ValueError, TypeError, KeyError):
|
except ValueError as error:
|
||||||
|
return web.json_response({'error': str(error)}, status=400)
|
||||||
|
except (TypeError, KeyError):
|
||||||
return web.json_response({'error': 'Ongeldige invoer. Controleer naam, bord en gekozen tegels.'}, status=400)
|
return web.json_response({'error': 'Ongeldige invoer. Controleer naam, bord en gekozen tegels.'}, status=400)
|
||||||
response.headers['Cache-Control'] = 'no-store'
|
response.headers['Cache-Control'] = 'no-store'
|
||||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||||
|
|||||||
@@ -1,64 +1,335 @@
|
|||||||
'use strict';
|
"use strict";
|
||||||
const $ = s => document.querySelector(s);
|
const $ = (s) => document.querySelector(s);
|
||||||
let inventory = {screens: [], entities: []}, selected = null, layout = null, dirty = false, filter = '', dragIndex = -1, busy = false;
|
let inventory = { screens: [], entities: [] },
|
||||||
const node = (tag, text, cls) => { const n = document.createElement(tag); if (text !== undefined) n.textContent = text; if (cls) n.className = cls; return n; };
|
selected = null,
|
||||||
function toast(message) { $('#toast').textContent = message; $('#toast').hidden = false; clearTimeout(toast.timer); toast.timer = setTimeout(() => $('#toast').hidden = true, 5000); }
|
layout = null,
|
||||||
|
dirty = false,
|
||||||
|
filter = "",
|
||||||
|
dragIndex = -1,
|
||||||
|
busy = false;
|
||||||
|
const node = (tag, text, cls) => {
|
||||||
|
const n = document.createElement(tag);
|
||||||
|
if (text !== undefined) n.textContent = text;
|
||||||
|
if (cls) n.className = cls;
|
||||||
|
return n;
|
||||||
|
};
|
||||||
|
function toast(message) {
|
||||||
|
$("#toast").textContent = message;
|
||||||
|
$("#toast").hidden = false;
|
||||||
|
clearTimeout(toast.timer);
|
||||||
|
toast.timer = setTimeout(() => ($("#toast").hidden = true), 5000);
|
||||||
|
}
|
||||||
async function api(path, options = {}) {
|
async function api(path, options = {}) {
|
||||||
const response = await fetch(`api/${path}`, {...options, headers: {'Content-Type': 'application/json', 'X-Screen-CSRF': inventory.csrf || '', ...options.headers}});
|
const response = await fetch(`api/${path}`, {
|
||||||
if (!response.ok) { let message = 'Dit lukte niet. Vernieuw de pagina en probeer opnieuw.'; try { message = (await response.json()).error || message; } catch {} throw new Error(message); }
|
...options,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Screen-CSRF": inventory.csrf || "",
|
||||||
|
...options.headers,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
let message = "Dit lukte niet. Vernieuw de pagina en probeer opnieuw.";
|
||||||
|
try {
|
||||||
|
message = (await response.json()).error || message;
|
||||||
|
} catch {}
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
function markDirty() { dirty = true; $('#dirty').textContent = 'Niet-opgeslagen wijzigingen'; $('#save-detail').textContent = 'Klik op opslaan om je scherm bij te werken.'; }
|
function markDirty() {
|
||||||
|
dirty = true;
|
||||||
|
$("#dirty").textContent = "Niet-opgeslagen wijzigingen";
|
||||||
|
$("#save-detail").textContent = "Klik op opslaan om je scherm bij te werken.";
|
||||||
|
}
|
||||||
function select(id) {
|
function select(id) {
|
||||||
if (id !== selected && dirty && !confirm('Je hebt niet-opgeslagen wijzigingen. Toch een ander scherm openen?')) return;
|
if (
|
||||||
selected = id; const screen = inventory.screens.find(s => s.id === id); if (!screen) return;
|
id !== selected &&
|
||||||
layout = structuredClone(screen.layout); dirty = false; $('#title').value = layout.title; $('#screen-name').textContent = screen.name;
|
dirty &&
|
||||||
$('#editor').hidden = false; $('#empty').hidden = true; $('#dirty').textContent = 'Alles opgeslagen'; $('#save-detail').textContent = 'Aanpassen kan zonder opnieuw flashen.';
|
!confirm(
|
||||||
renderScreens(); renderTiles(); renderResults();
|
"Je hebt niet-opgeslagen wijzigingen. Toch een ander scherm openen?",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
selected = id;
|
||||||
|
const screen = inventory.screens.find((s) => s.id === id);
|
||||||
|
if (!screen) return;
|
||||||
|
layout = structuredClone(screen.layout);
|
||||||
|
dirty = false;
|
||||||
|
$("#title").value = layout.title;
|
||||||
|
$("#screen-name").textContent = screen.name;
|
||||||
|
$("#editor").hidden = false;
|
||||||
|
$("#empty").hidden = true;
|
||||||
|
$("#dirty").textContent = "Alles opgeslagen";
|
||||||
|
$("#save-detail").textContent = "Aanpassen kan zonder opnieuw flashen.";
|
||||||
|
renderScreens();
|
||||||
|
renderTiles();
|
||||||
|
renderResults();
|
||||||
}
|
}
|
||||||
function renderScreens() {
|
function renderScreens() {
|
||||||
$('#screens').replaceChildren();
|
$("#screens").replaceChildren();
|
||||||
for (const screen of inventory.screens) { const b = node('button', undefined, `screen ${screen.id === selected ? 'selected' : ''}`); b.append(node('strong', screen.name), node('small', `${screen.online ? '● Online' : '○ Offline'}${screen.area ? ' · '+screen.area : ''} · firmware ${screen.firmware || 'onbekend'}`)); b.onclick = () => select(screen.id); $('#screens').append(b); }
|
for (const screen of inventory.screens) {
|
||||||
const screen = inventory.screens.find(s => s.id === selected);
|
const b = node(
|
||||||
if (screen) { $('#delivery').textContent = screen.online ? `${screen.delivery} · ${screen.status}` : 'Offline · wijzigingen worden bewaard'; $('#delivery').classList.toggle('online', screen.online); }
|
"button",
|
||||||
|
undefined,
|
||||||
|
`screen ${screen.id === selected ? "selected" : ""}`,
|
||||||
|
);
|
||||||
|
b.append(
|
||||||
|
node("strong", screen.name),
|
||||||
|
node(
|
||||||
|
"small",
|
||||||
|
`${screen.online ? "● Online" : "○ Offline"}${screen.area ? " · " + screen.area : ""} · firmware ${screen.firmware || "onbekend"}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
b.onclick = () => select(screen.id);
|
||||||
|
$("#screens").append(b);
|
||||||
|
}
|
||||||
|
const screen = inventory.screens.find((s) => s.id === selected);
|
||||||
|
if (screen) {
|
||||||
|
$("#delivery").textContent = screen.online
|
||||||
|
? `${screen.delivery} · ${screen.status}`
|
||||||
|
: "Offline · wijzigingen worden bewaard";
|
||||||
|
$("#delivery").classList.toggle("online", screen.online);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function move(from, to) {
|
||||||
|
if (to < 0 || to >= layout.tiles.length) return;
|
||||||
|
const [tile] = layout.tiles.splice(from, 1);
|
||||||
|
layout.tiles.splice(to, 0, tile);
|
||||||
|
markDirty();
|
||||||
|
renderTiles();
|
||||||
}
|
}
|
||||||
function move(from, to) { if (to < 0 || to >= layout.tiles.length) return; const [tile] = layout.tiles.splice(from, 1); layout.tiles.splice(to, 0, tile); markDirty(); renderTiles(); }
|
|
||||||
function renderTiles() {
|
function renderTiles() {
|
||||||
$('#tiles').replaceChildren(); $('#count').textContent = `${layout.tiles.length} / 10`; $('#no-tiles').hidden = layout.tiles.length > 0;
|
$("#tiles").replaceChildren();
|
||||||
|
$("#count").textContent = `${layout.tiles.length} / 10`;
|
||||||
|
$("#no-tiles").hidden = layout.tiles.length > 0;
|
||||||
layout.tiles.forEach((tile, i) => {
|
layout.tiles.forEach((tile, i) => {
|
||||||
if (i === 6) $('#tiles').append(node('li', 'Pagina 2', 'page-break'));
|
if (i === 6) $("#tiles").append(node("li", "Pagina 2", "page-break"));
|
||||||
const li = node('li', undefined, 'tile'); li.draggable = true;
|
const li = node("li", undefined, "tile");
|
||||||
li.ondragstart = e => { if (e.target instanceof HTMLInputElement) { e.preventDefault(); return; } dragIndex = i; e.dataTransfer.setData('text/plain', String(i)); li.classList.add('dragging'); };
|
li.draggable = true;
|
||||||
li.ondragend = () => { dragIndex = -1; li.classList.remove('dragging'); };
|
li.ondragstart = (e) => {
|
||||||
li.ondragover = e => { if (dragIndex >= 0) e.preventDefault(); };
|
if (e.target instanceof HTMLInputElement) {
|
||||||
li.ondrop = e => { e.preventDefault(); if (dragIndex >= 0) move(dragIndex, i); };
|
e.preventDefault();
|
||||||
const content = node('div'), input = node('input'); input.value = tile.name; input.placeholder = inventory.entities.find(e => e.id === tile.entity)?.name || tile.entity; input.maxLength = 60; input.setAttribute('aria-label', `Naam voor tegel ${i+1}`); input.oninput = () => { tile.name = input.value; markDirty(); }; content.append(input, node('small', tile.entity));
|
return;
|
||||||
const actions = node('div', undefined, 'tile-actions'); for (const [label, title, action, disabled] of [['↑','Omhoog',()=>move(i,i-1),i===0],['↓','Omlaag',()=>move(i,i+1),i===layout.tiles.length-1],['✕','Verwijderen',()=>{layout.tiles.splice(i,1);markDirty();renderTiles();renderResults();},false]]) { const b = node('button',label); b.title = title; b.setAttribute('aria-label',title); b.disabled = disabled; b.onclick = action; actions.append(b); }
|
}
|
||||||
li.append(node('span','⠿','grip'), content, actions); $('#tiles').append(li);
|
dragIndex = i;
|
||||||
|
e.dataTransfer.setData("text/plain", String(i));
|
||||||
|
li.classList.add("dragging");
|
||||||
|
};
|
||||||
|
li.ondragend = () => {
|
||||||
|
dragIndex = -1;
|
||||||
|
li.classList.remove("dragging");
|
||||||
|
};
|
||||||
|
li.ondragover = (e) => {
|
||||||
|
if (dragIndex >= 0) e.preventDefault();
|
||||||
|
};
|
||||||
|
li.ondrop = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (dragIndex >= 0) move(dragIndex, i);
|
||||||
|
};
|
||||||
|
const content = node("div"),
|
||||||
|
input = node("input");
|
||||||
|
input.value = tile.name;
|
||||||
|
input.placeholder =
|
||||||
|
inventory.entities.find((e) => e.id === tile.entity)?.name || tile.entity;
|
||||||
|
input.maxLength = 60;
|
||||||
|
input.setAttribute("aria-label", `Naam voor tegel ${i + 1}`);
|
||||||
|
input.oninput = () => {
|
||||||
|
tile.name = input.value;
|
||||||
|
markDirty();
|
||||||
|
};
|
||||||
|
content.append(input, node("small", tile.entity));
|
||||||
|
const actions = node("div", undefined, "tile-actions");
|
||||||
|
for (const [label, title, action, disabled] of [
|
||||||
|
["↑", "Omhoog", () => move(i, i - 1), i === 0],
|
||||||
|
["↓", "Omlaag", () => move(i, i + 1), i === layout.tiles.length - 1],
|
||||||
|
[
|
||||||
|
"✕",
|
||||||
|
"Verwijderen",
|
||||||
|
() => {
|
||||||
|
layout.tiles.splice(i, 1);
|
||||||
|
markDirty();
|
||||||
|
renderTiles();
|
||||||
|
renderResults();
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
],
|
||||||
|
]) {
|
||||||
|
const b = node("button", label);
|
||||||
|
b.title = title;
|
||||||
|
b.setAttribute("aria-label", title);
|
||||||
|
b.disabled = disabled;
|
||||||
|
b.onclick = action;
|
||||||
|
actions.append(b);
|
||||||
|
}
|
||||||
|
li.append(node("span", "⠿", "grip"), content, actions);
|
||||||
|
$("#tiles").append(li);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function renderResults() {
|
function renderResults() {
|
||||||
if (!layout) return;
|
if (!layout) return;
|
||||||
const query = $('#search').value.toLocaleLowerCase(); const chosen = new Set(layout.tiles.map(t => t.entity));
|
const query = $("#search").value.toLocaleLowerCase();
|
||||||
const matches = inventory.entities.filter(e => (!filter || e.id.startsWith(filter+'.')) && `${e.name} ${e.id} ${e.device} ${e.area}`.toLocaleLowerCase().includes(query));
|
const chosen = new Set(layout.tiles.map((t) => t.entity));
|
||||||
$('#results').replaceChildren();
|
const matches = inventory.entities.filter(
|
||||||
|
(e) =>
|
||||||
|
(!filter || e.id.startsWith(filter + ".")) &&
|
||||||
|
`${e.name} ${e.id} ${e.device} ${e.area}`
|
||||||
|
.toLocaleLowerCase()
|
||||||
|
.includes(query),
|
||||||
|
);
|
||||||
|
$("#results").replaceChildren();
|
||||||
for (const entity of matches.slice(0, 80)) {
|
for (const entity of matches.slice(0, 80)) {
|
||||||
const b = node('button', undefined, 'result'), description = node('span'); description.append(node('span',entity.name), node('small',[entity.area,entity.device].filter(Boolean).join(' · ')), node('small',entity.id)); b.append(description,node('span',chosen.has(entity.id)?'✓':'+','plus')); b.disabled = chosen.has(entity.id) || layout.tiles.length >= 10; b.onclick = () => { layout.tiles.push({entity:entity.id,name:''}); markDirty();renderTiles();renderResults(); }; $('#results').append(b);
|
const b = node("button", undefined, "result"),
|
||||||
|
description = node("span");
|
||||||
|
description.append(
|
||||||
|
node("span", entity.name),
|
||||||
|
node("small", [entity.area, entity.device].filter(Boolean).join(" · ")),
|
||||||
|
node("small", entity.id),
|
||||||
|
);
|
||||||
|
b.append(
|
||||||
|
description,
|
||||||
|
node("span", chosen.has(entity.id) ? "✓" : "+", "plus"),
|
||||||
|
);
|
||||||
|
b.disabled = chosen.has(entity.id) || layout.tiles.length >= 10;
|
||||||
|
b.onclick = () => {
|
||||||
|
layout.tiles.push({ entity: entity.id, name: "" });
|
||||||
|
markDirty();
|
||||||
|
renderTiles();
|
||||||
|
renderResults();
|
||||||
|
};
|
||||||
|
$("#results").append(b);
|
||||||
}
|
}
|
||||||
if (!matches.length) $('#results').append(node('p','Geen entiteiten gevonden. Probeer een andere naam.','hint'));
|
if (!matches.length)
|
||||||
if (matches.length > 80) $('#results').append(node('p',`${matches.length} resultaten. Typ verder om te verfijnen.`,'hint'));
|
$("#results").append(
|
||||||
|
node("p", "Geen entiteiten gevonden. Probeer een andere naam.", "hint"),
|
||||||
|
);
|
||||||
|
if (matches.length > 80)
|
||||||
|
$("#results").append(
|
||||||
|
node(
|
||||||
|
"p",
|
||||||
|
`${matches.length} resultaten. Typ verder om te verfijnen.`,
|
||||||
|
"hint",
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
async function refresh() {
|
async function refresh() {
|
||||||
try { inventory = await (await api('inventory')).json(); $('#connection').textContent = inventory.connected ? '● Home Assistant verbonden' : 'Verbinding met Home Assistant herstellen…'; $('#connection').classList.toggle('online',inventory.connected); renderScreens(); if (!selected && inventory.screens.length) select(inventory.screens[0].id); }
|
try {
|
||||||
catch { $('#connection').textContent = 'Beheerpagina niet bereikbaar · opnieuw proberen…'; }
|
inventory = await (await api("inventory")).json();
|
||||||
|
$("#connection").textContent = inventory.connected
|
||||||
|
? "● Home Assistant verbonden"
|
||||||
|
: "Verbinding met Home Assistant herstellen…";
|
||||||
|
$("#connection").classList.toggle("online", inventory.connected);
|
||||||
|
renderScreens();
|
||||||
|
if (!selected && inventory.screens.length) select(inventory.screens[0].id);
|
||||||
|
} catch {
|
||||||
|
$("#connection").textContent =
|
||||||
|
"Beheerpagina niet bereikbaar · opnieuw proberen…";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
$('#save').onclick = async () => { if (busy) return; busy = true; $('#save').disabled = true; try { layout.title = $('#title').value; await api(`screens/${encodeURIComponent(selected)}`, {method:'PUT',body:JSON.stringify(layout)}); dirty = false; $('#dirty').textContent = 'Opgeslagen'; $('#save-detail').textContent = 'Synchronisatie volgt automatisch, ook na opnieuw verbinden.'; toast('Opgeslagen. Je scherm wordt bijgewerkt.'); await refresh(); } catch(e) { toast(e.message); } finally { busy = false; $('#save').disabled = false; } };
|
$("#save").onclick = async () => {
|
||||||
$('#title').oninput = markDirty; $('#search').oninput = renderResults; $('#refresh').onclick = refresh;
|
if (busy) return;
|
||||||
for (const [value,label] of [['','Alles'],['light','Lampen'],['climate','Klimaat'],['scene','Scènes'],['vacuum','Vacuum'],['sensor','Sensoren']]) { const b = node('button',label,value===''?'active':''); b.onclick=()=>{filter=value;$('#filters').querySelectorAll('button').forEach(n=>n.classList.remove('active'));b.classList.add('active');renderResults();};$('#filters').append(b); }
|
busy = true;
|
||||||
$('#new-screen').onclick = $('#start').onclick = () => $('#installer').showModal(); $('#close-install').onclick = () => $('#installer').close();
|
$("#save").disabled = true;
|
||||||
let generatedName = '';
|
try {
|
||||||
$('#install-form').onsubmit = async e => { e.preventDefault(); $('#download').disabled = true; try { const data = Object.fromEntries(new FormData(e.target)); const response = await api('install',{method:'POST',body:JSON.stringify(data)}); $('#yaml-output').value = await response.text(); generatedName = data.name; $('#yaml-result').hidden = false; $('#install-status').textContent='Je YAML is klaar. Kopieer hem naar ESPHome en bewaar het bestand met sleutels.'; } catch(e) { $('#install-status').textContent=e.message; } finally { $('#download').disabled=false; } };
|
layout.title = $("#title").value;
|
||||||
$('#copy-yaml').onclick = async () => { try { if (navigator.clipboard && window.isSecureContext) await navigator.clipboard.writeText($('#yaml-output').value); else { $('#yaml-output').select(); if (!document.execCommand('copy')) throw new Error(); } $('#install-status').textContent='Gekopieerd. Plak dit bij Edit in ESPHome Device Builder.'; } catch { $('#yaml-output').select(); $('#install-status').textContent='De tekst is geselecteerd. Kopieer met Ctrl+C of Command+C.'; } };
|
await api(`screens/${encodeURIComponent(selected)}`, {
|
||||||
$('#download-yaml').onclick = () => { const url = URL.createObjectURL(new Blob([$('#yaml-output').value],{type:'text/plain'})); const a=node('a');a.href=url;a.download=`${generatedName}.yaml`;document.body.append(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(url),30000);$('#install-status').textContent='Download gestart. Blokkeert je browser deze? Gebruik Kopieer YAML.'; };
|
method: "PUT",
|
||||||
window.addEventListener('beforeunload',e=>{if(dirty){e.preventDefault();e.returnValue='';}});
|
body: JSON.stringify(layout),
|
||||||
refresh(); setInterval(refresh, 10000);
|
});
|
||||||
|
dirty = false;
|
||||||
|
$("#dirty").textContent = "Opgeslagen";
|
||||||
|
$("#save-detail").textContent =
|
||||||
|
"Synchronisatie volgt automatisch, ook na opnieuw verbinden.";
|
||||||
|
toast("Opgeslagen. Je scherm wordt bijgewerkt.");
|
||||||
|
await refresh();
|
||||||
|
} catch (e) {
|
||||||
|
toast(e.message);
|
||||||
|
} finally {
|
||||||
|
busy = false;
|
||||||
|
$("#save").disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$("#title").oninput = markDirty;
|
||||||
|
$("#search").oninput = renderResults;
|
||||||
|
$("#refresh").onclick = refresh;
|
||||||
|
for (const [value, label] of [
|
||||||
|
["", "Alles"],
|
||||||
|
["light", "Lampen"],
|
||||||
|
["climate", "Klimaat"],
|
||||||
|
["scene", "Scènes"],
|
||||||
|
["vacuum", "Vacuum"],
|
||||||
|
["sensor", "Sensoren"],
|
||||||
|
]) {
|
||||||
|
const b = node("button", label, value === "" ? "active" : "");
|
||||||
|
b.onclick = () => {
|
||||||
|
filter = value;
|
||||||
|
$("#filters")
|
||||||
|
.querySelectorAll("button")
|
||||||
|
.forEach((n) => n.classList.remove("active"));
|
||||||
|
b.classList.add("active");
|
||||||
|
renderResults();
|
||||||
|
};
|
||||||
|
$("#filters").append(b);
|
||||||
|
}
|
||||||
|
$("#new-screen").onclick = $("#start").onclick = () =>
|
||||||
|
$("#installer").showModal();
|
||||||
|
$("#close-install").onclick = () => $("#installer").close();
|
||||||
|
let generatedName = "";
|
||||||
|
$("#install-form").onsubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
$("#download").disabled = true;
|
||||||
|
try {
|
||||||
|
const data = Object.fromEntries(new FormData(e.target));
|
||||||
|
const response = await api("install", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(data),
|
||||||
|
});
|
||||||
|
$("#yaml-output").value = await response.text();
|
||||||
|
generatedName = data.name;
|
||||||
|
$("#yaml-result").hidden = false;
|
||||||
|
$("#install-status").textContent =
|
||||||
|
"Je YAML is klaar. Kopieer hem naar ESPHome en bewaar het bestand met sleutels.";
|
||||||
|
} catch (e) {
|
||||||
|
$("#install-status").textContent = e.message;
|
||||||
|
} finally {
|
||||||
|
$("#download").disabled = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$("#copy-yaml").onclick = async () => {
|
||||||
|
try {
|
||||||
|
if (navigator.clipboard && window.isSecureContext)
|
||||||
|
await navigator.clipboard.writeText($("#yaml-output").value);
|
||||||
|
else {
|
||||||
|
$("#yaml-output").select();
|
||||||
|
if (!document.execCommand("copy")) throw new Error();
|
||||||
|
}
|
||||||
|
$("#install-status").textContent =
|
||||||
|
"Gekopieerd. Plak dit bij Edit in ESPHome Device Builder.";
|
||||||
|
} catch {
|
||||||
|
$("#yaml-output").select();
|
||||||
|
$("#install-status").textContent =
|
||||||
|
"De tekst is geselecteerd. Kopieer met Ctrl+C of Command+C.";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
$("#download-yaml").onclick = () => {
|
||||||
|
const url = URL.createObjectURL(
|
||||||
|
new Blob([$("#yaml-output").value], { type: "text/plain" }),
|
||||||
|
);
|
||||||
|
const a = node("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${generatedName}.yaml`;
|
||||||
|
document.body.append(a);
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 30000);
|
||||||
|
$("#install-status").textContent =
|
||||||
|
"Download gestart. Blokkeert je browser deze? Gebruik Kopieer YAML.";
|
||||||
|
};
|
||||||
|
window.addEventListener("beforeunload", (e) => {
|
||||||
|
if (dirty) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.returnValue = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
refresh();
|
||||||
|
setInterval(refresh, 10000);
|
||||||
|
|||||||
@@ -1,21 +1,227 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="nl"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>ESP Screens</title><link rel="stylesheet" href="static/style.css"><script src="static/app.js" defer></script></head>
|
<html lang="nl">
|
||||||
<body>
|
<head>
|
||||||
<header><div class="brand"><span class="mark">▦</span><div><strong>ESP Screens</strong><small>Je huis. Jouw bediening.</small></div></div><button id="new-screen">+ Nieuw scherm</button></header>
|
<meta charset="utf-8" />
|
||||||
<main>
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<div class="intro"><div><p class="eyebrow">HOME ASSISTANT</p><h1>Een plek voor je favorieten.</h1><p>Kies wat je wilt bedienen. Je scherm verandert met je mee.</p></div><span id="connection" role="status" class="badge">Verbinden…</span></div>
|
<title>ESP Screens</title>
|
||||||
<div class="workspace">
|
<link rel="stylesheet" href="static/style.css" />
|
||||||
<aside><h2>Mijn schermen</h2><div id="screens"></div><p class="hint">Nieuw scherm gekoppeld? Het verschijnt hier automatisch binnen 30 seconden.</p><button class="quiet" id="refresh">↻ Vernieuwen</button></aside>
|
<script src="static/app.js" defer></script>
|
||||||
<section id="empty" class="empty"><span class="empty-icon">▦</span><h2>Begin met een scherm</h2><p>Sluit een CYD of Guition aan. We maken de installatie-YAML voor je en helpen je met de eerste flash.</p><button id="start">Nieuw scherm installeren</button></section>
|
</head>
|
||||||
<section id="editor" hidden>
|
<body>
|
||||||
<div class="editor-head"><div><p class="eyebrow" id="screen-name"></p><h2>Maak het jouw scherm</h2></div><span id="delivery" class="badge"></span></div>
|
<header>
|
||||||
<label>Naam bovenaan het scherm<input id="title" maxlength="60" placeholder="Bijvoorbeeld: Woonkamer"></label>
|
<div class="brand">
|
||||||
<div class="columns"><div class="tile-editor"><div class="section-title"><h3>Je tegels</h3><span id="count">0 / 10</span></div><p class="hint">Sleep om te ordenen of gebruik de pijltjes. Zes tegels per pagina.</p><ol id="tiles"></ol><p id="no-tiles" class="hint">Voeg rechts je eerste lamp, scène of apparaat toe.</p></div>
|
<span class="mark">▦</span>
|
||||||
<div class="picker"><label for="search">Voeg een entiteit toe</label><input id="search" type="search" placeholder="Zoek op naam, apparaat of ruimte…" autocomplete="off"><div class="filters" id="filters"></div><div id="results" aria-live="polite"></div></div></div>
|
<div>
|
||||||
<footer class="savebar"><div><strong id="dirty">Alles opgeslagen</strong><small id="save-detail">Aanpassen kan zonder opnieuw flashen.</small></div><button id="save">Opslaan & naar scherm</button></footer>
|
<strong>ESP Screens</strong><small>Je huis. Jouw bediening.</small>
|
||||||
</section></div>
|
</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></p><details class="help"><summary>Hoe werkt het dagelijks?</summary><p>Zoek een entiteit, voeg deze toe en klik op <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.</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.</p></details>
|
</div>
|
||||||
</main>
|
<button id="new-screen">+ Nieuw scherm</button>
|
||||||
<dialog id="installer"><form id="install-form"><div class="dialog-head"><div><p class="eyebrow">NIEUW SCHERM</p><h2>Van USB naar aan de muur.</h2></div><button type="button" class="quiet" id="close-install" aria-label="Sluiten">✕</button></div><p>Kies je scherm. Elk bestand krijgt eigen sleutels voor een veilige koppeling en draadloze updates.</p><fieldset><legend>Welk scherm heb je?</legend><label class="board"><input type="radio" name="board" value="cyd" checked><span><b>CYD · 2,8 inch</b><small>ESP32-2432S028 · 320 × 240</small></span></label><label class="board"><input type="radio" name="board" value="guition"><span><b>Guition · 4 inch</b><small>ESP32-S3-4848S040 · 480 × 480 · GT911</small></span></label></fieldset><label>Herkenbare naam<input name="friendly_name" required maxlength="60" placeholder="Scherm woonkamer"></label><label>Unieke apparaatnaam<input name="name" required pattern="[a-z][a-z0-9-]{0,29}" maxlength="30" placeholder="scherm-woonkamer"><small>Kleine letters, cijfers en streepjes. Elk scherm een andere naam.</small></label><button type="submit" id="download">Maak installatie-YAML</button><p id="install-status" role="status"></p><div id="yaml-result" hidden><label>Je eigen ESPHome-YAML<textarea id="yaml-output" readonly rows="9" spellcheck="false"></textarea></label><div class="yaml-actions"><button type="button" id="copy-yaml">Kopieer YAML</button><button type="button" class="quiet" id="download-yaml">Download bestand</button></div></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.</li><li><b>Vul wifi eenmalig in bij Secrets.</b> Gebruik <code>wifi_ssid</code> en <code>wifi_password</code>.</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.</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.</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.</li></ol><p class="hint">Bewaar je YAML met sleutels. Gebruik later Install → Wirelessly voor firmware-updates. Tegels wijzigen doe je hier.</p></form></dialog>
|
</header>
|
||||||
<div id="toast" role="status" hidden></div>
|
<main>
|
||||||
</body></html>
|
<div class="intro">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">HOME ASSISTANT</p>
|
||||||
|
<h1>Een plek voor je favorieten.</h1>
|
||||||
|
<p>Kies wat je wilt bedienen. Je scherm verandert met je mee.</p>
|
||||||
|
</div>
|
||||||
|
<span id="connection" role="status" class="badge">Verbinden…</span>
|
||||||
|
</div>
|
||||||
|
<div class="workspace">
|
||||||
|
<aside>
|
||||||
|
<h2>Mijn schermen</h2>
|
||||||
|
<div id="screens"></div>
|
||||||
|
<p class="hint">
|
||||||
|
Nieuw scherm gekoppeld? Het verschijnt hier automatisch binnen 30
|
||||||
|
seconden.
|
||||||
|
</p>
|
||||||
|
<button class="quiet" id="refresh">↻ Vernieuwen</button>
|
||||||
|
</aside>
|
||||||
|
<section id="empty" class="empty">
|
||||||
|
<span class="empty-icon">▦</span>
|
||||||
|
<h2>Begin met een scherm</h2>
|
||||||
|
<p>
|
||||||
|
Sluit een CYD of Guition aan. We maken de installatie-YAML voor je
|
||||||
|
en helpen je met de eerste flash.
|
||||||
|
</p>
|
||||||
|
<button id="start">Nieuw scherm installeren</button>
|
||||||
|
</section>
|
||||||
|
<section id="editor" hidden>
|
||||||
|
<div class="editor-head">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow" id="screen-name"></p>
|
||||||
|
<h2>Maak het jouw scherm</h2>
|
||||||
|
</div>
|
||||||
|
<span id="delivery" class="badge"></span>
|
||||||
|
</div>
|
||||||
|
<label
|
||||||
|
>Naam bovenaan het scherm<input
|
||||||
|
id="title"
|
||||||
|
maxlength="60"
|
||||||
|
placeholder="Bijvoorbeeld: Woonkamer"
|
||||||
|
/></label>
|
||||||
|
<div class="columns">
|
||||||
|
<div class="tile-editor">
|
||||||
|
<div class="section-title">
|
||||||
|
<h3>Je tegels</h3>
|
||||||
|
<span id="count">0 / 10</span>
|
||||||
|
</div>
|
||||||
|
<p class="hint">
|
||||||
|
Sleep om te ordenen of gebruik de pijltjes. Zes tegels per
|
||||||
|
pagina.
|
||||||
|
</p>
|
||||||
|
<ol id="tiles"></ol>
|
||||||
|
<p id="no-tiles" class="hint">
|
||||||
|
Voeg rechts je eerste lamp, scène of apparaat toe.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="picker">
|
||||||
|
<label for="search">Voeg een entiteit toe</label
|
||||||
|
><input
|
||||||
|
id="search"
|
||||||
|
type="search"
|
||||||
|
placeholder="Zoek op naam, apparaat of ruimte…"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<div class="filters" id="filters"></div>
|
||||||
|
<div id="results" aria-live="polite"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<footer class="savebar">
|
||||||
|
<div>
|
||||||
|
<strong id="dirty">Alles opgeslagen</strong
|
||||||
|
><small id="save-detail"
|
||||||
|
>Aanpassen kan zonder opnieuw flashen.</small
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<button id="save">Opslaan & naar scherm</button>
|
||||||
|
</footer>
|
||||||
|
</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
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<details class="help">
|
||||||
|
<summary>Hoe werkt het dagelijks?</summary>
|
||||||
|
<p>
|
||||||
|
Zoek een entiteit, voeg deze toe en klik op
|
||||||
|
<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.
|
||||||
|
</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.
|
||||||
|
</p>
|
||||||
|
</details>
|
||||||
|
</main>
|
||||||
|
<dialog id="installer">
|
||||||
|
<form id="install-form">
|
||||||
|
<div class="dialog-head">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">NIEUW SCHERM</p>
|
||||||
|
<h2>Van USB naar aan de muur.</h2>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="quiet"
|
||||||
|
id="close-install"
|
||||||
|
aria-label="Sluiten"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Kies je scherm. Elk bestand krijgt eigen sleutels voor een veilige
|
||||||
|
koppeling en draadloze updates.
|
||||||
|
</p>
|
||||||
|
<fieldset>
|
||||||
|
<legend>Welk scherm heb je?</legend>
|
||||||
|
<label class="board"
|
||||||
|
><input type="radio" name="board" value="cyd" checked /><span
|
||||||
|
><b>CYD · 2,8 inch</b
|
||||||
|
><small>ESP32-2432S028 · 320 × 240</small></span
|
||||||
|
></label
|
||||||
|
><label class="board"
|
||||||
|
><input type="radio" name="board" value="guition" /><span
|
||||||
|
><b>Guition · 4 inch</b
|
||||||
|
><small>ESP32-S3-4848S040 · 480 × 480 · GT911</small></span
|
||||||
|
></label
|
||||||
|
>
|
||||||
|
</fieldset>
|
||||||
|
<label
|
||||||
|
>Herkenbare naam<input
|
||||||
|
name="friendly_name"
|
||||||
|
required
|
||||||
|
maxlength="60"
|
||||||
|
placeholder="Scherm woonkamer" /></label
|
||||||
|
><label
|
||||||
|
>Unieke apparaatnaam<input
|
||||||
|
name="name"
|
||||||
|
required
|
||||||
|
pattern="[a-z][a-z0-9-]{0,29}"
|
||||||
|
maxlength="30"
|
||||||
|
placeholder="scherm-woonkamer"
|
||||||
|
/><small
|
||||||
|
>Kleine letters, cijfers en streepjes. Elk scherm een andere
|
||||||
|
naam.</small
|
||||||
|
></label
|
||||||
|
><button type="submit" id="download">Maak installatie-YAML</button>
|
||||||
|
<p id="install-status" role="status"></p>
|
||||||
|
<div id="yaml-result" hidden>
|
||||||
|
<label
|
||||||
|
>Je eigen ESPHome-YAML<textarea
|
||||||
|
id="yaml-output"
|
||||||
|
readonly
|
||||||
|
rows="9"
|
||||||
|
spellcheck="false"
|
||||||
|
></textarea>
|
||||||
|
</label>
|
||||||
|
<div class="yaml-actions">
|
||||||
|
<button type="button" id="copy-yaml">Kopieer YAML</button
|
||||||
|
><button type="button" class="quiet" id="download-yaml">
|
||||||
|
Download bestand
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</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.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<b>Vul wifi eenmalig in bij Secrets.</b> Gebruik
|
||||||
|
<code>wifi_ssid</code> en <code>wifi_password</code>.
|
||||||
|
</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.
|
||||||
|
</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.
|
||||||
|
</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.
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<p class="hint">
|
||||||
|
Bewaar je YAML met sleutels. Gebruik later Install → Wirelessly voor
|
||||||
|
firmware-updates. Tegels wijzigen doe je hier.
|
||||||
|
</p>
|
||||||
|
</form>
|
||||||
|
</dialog>
|
||||||
|
<div id="toast" role="status" hidden></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
|||||||
name: ESP Screen Manager
|
name: ESP Screen Manager
|
||||||
version: "0.1.0"
|
version: "0.1.1"
|
||||||
slug: esp_screen_manager
|
slug: esp_screen_manager
|
||||||
description: Kies tegels voor je CYD en Guition touchscreen, zonder opnieuw flashen.
|
description: Kies tegels voor je CYD en Guition touchscreen, zonder opnieuw flashen.
|
||||||
url: https://github.com/MaxGramser/homeassistant_espscreen
|
url: https://github.com/MaxGramser/homeassistant_espscreen
|
||||||
|
|||||||
Reference in New Issue
Block a user