diff --git a/src/cards.js b/src/cards.js
index 864804e..27bc813 100644
--- a/src/cards.js
+++ b/src/cards.js
@@ -121,6 +121,14 @@ export const hasActs = (p, id) => (cardCfg(p, id).actions || []).some(a => a &&
export const label = (p, e) => cardCfg(p, e.id).name || name(p.hass, e);
export const isHidden = (p, id) => !!cardCfg(p, id).hidden;
export const icoFor = (p, id, mdi, svg) => uico(cardCfg(p, id).icon, mdi, svg);
+/** how a plug may be switched, set per card ("plug"): "" = normally, "confirm" = after asking, "lock" = never (measure
+ only, e.g. a fridge). A device card and its switch entity each have settings; the stricter one counts */
+export const plugMode = (p, ...keys) => { const m = keys.filter(Boolean).map(k => cardCfg(p, k).plug); return m.includes("lock") ? "lock" : m.includes("confirm") ? "confirm" : ""; };
+/** `run` (switching a plug) guarded by plugMode: null when locked, a confirmation first when asked for */
+export function guardPlug(p, keys, title, on, run) {
+ const m = plugMode(p, ...keys); if (m === "lock") return null; if (m !== "confirm") return run;
+ return () => p.openPopup({ type: "confirm", title: t("Stekker"), big: title, sub: on ? t("Echt uitzetten?") : t("Echt aanzetten?"), okLabel: on ? t("Uitzetten") : t("Aanzetten"), ok: run });
+}
/** tap/hold wiring honouring the per-card "tap" override: toggle (default) or popup */
const toggleOrPopup = (p, id, toggle, popup) => pressFor(p, id, toggle, popup);
@@ -216,7 +224,7 @@ export function switchCard(p, e) {
const s = e.state, on = isOn(s), un = unavailable(s), d = e.id.split(".")[0];
const svc = d === "input_boolean" ? "input_boolean" : "switch";
return html`
p.call(svc, "toggle", { entity_id: e.id }), () => p.openPopup({ type: "switch", e })))}>
+ ${pressDir(p, toggleOrPopup(p, e.id, guardPlug(p, [e.id], label(p, e), on, () => p.call(svc, "toggle", { entity_id: e.id })), () => p.openPopup({ type: "switch", e })))}>
${un ? ua() : nothing}
${icoFor(p, e.id, "mdi:power-socket-eu", "plug")}
${ovr(p, e.id, "state") ?? (un ? t("Onbereikbaar") : on ? t("Aan") : t("Uit"))}
@@ -436,12 +444,13 @@ function plugCard(p, a, st, title, cfg, acts) {
const since = sw?.state?.last_changed ? sinceText(sw.state.last_changed) : "";
const tmr = p.plugTimers?.[a.id], left = tmr ? Math.max(0, Math.round((tmr.at - Date.now()) / 60000)) : null, plain = !!a.plain;
let bottom = nothing;
- if (mode === "timer") bottom = html`
ev.stopPropagation()}>${[15, 30, 60].map(m => html``)}
`;
+ const toggle = sw ? guardPlug(p, [a.id, sw.id], title, on, () => p.call("switch", "toggle", { entity_id: sw.id })) : null;
+ if (mode === "timer" && (!sw || toggle)) bottom = html`
ev.stopPropagation()}>${[15, 30, 60].map(m => html``)}
`;
else if (mode === "cost") bottom = price ? html`
${h?.today != null ? html`${t("Vandaag")} ${eur(h.today * price)}` : ""}${!on && W > 0.5 ? html`${h?.today != null ? dot() : nothing}${t("Sluipverbruik")} ${eur(W * 24 * 30 / 1000 * price)} / ${t("maand")}` : nothing}
` : html`
${t("Stel een kWh-prijs in bij Energie")}
`;
else if (mode === "bars") bottom = h?.rows ? html`
${h.rows.map(r => html` 0.01 })} style="height:${Math.max(9, r.v / (h.maxH || 1) * 100)}%">`)}
` : html`
…
`;
else if (mode === "spark" && h?.peak) bottom = html`
${t("piek {w} W om {t}", { w: h.peak.v, t: new Date(h.peak.t).toLocaleTimeString(locale(), { hour: "2-digit", minute: "2-digit" }) })}
`;
return html`
0, hasspk: mode === "spark" && !!h?.spark?.length, on, unavailable: st.st === "unknown", ...infoCls(cfg) })}
- ${pressDir(p, pressFor(p, a.id, sw ? () => p.call("switch", "toggle", { entity_id: sw.id }) : null, () => p.openPopup({ type: "appliance", a })))}>
+ ${pressDir(p, pressFor(p, a.id, toggle, () => p.openPopup({ type: "appliance", a })))}>
${st.st === "unknown" ? ua() : nothing}
${uico(cfg.icon, "mdi:power-plug-outline", "plug")}
${ovr(p, a.id, "state") ?? (left != null ? html`${t("uit over {n} min", { n: left })}` : st.st === "unknown" ? t("Geen meting") : plain ? (on ? t("Aan") : t("Uit")) : st.st === "running" ? t("Bezig") : st.st === "done" ? t("Klaar") : on ? (W > 0.5 ? t("Standby") : t("Aan")) : t("Uit"))}
@@ -463,7 +472,8 @@ export function applianceCard(p, a) {
const foot = [progName, st.progress != null ? `${Math.round(st.progress)}%` : "", st.power != null ? `${Math.round(st.power)} W` : "", a.door && p.hass.states[a.door.id]?.state === "open" ? t("Deur open") : ""].filter(Boolean);
const acts = [];
if (a.stop && !a.plain && ["running", "waiting"].includes(st.st)) acts.push({ icon: "mdi:stop", label: t("Stop"), run: () => p.call("button", "press", { entity_id: a.stop.id }) });
- if (a.switch && a.op && !unavailable(a.switch.state)) acts.push({ icon: "mdi:power", label: isOn(a.switch.state) ? t("Uit") : t("Aan"), on: isOn(a.switch.state), run: () => p.call("switch", "toggle", { entity_id: a.switch.id }) });
+ const pw = a.switch && a.op && !unavailable(a.switch.state) ? guardPlug(p, [a.id, a.switch.id], title, isOn(a.switch.state), () => p.call("switch", "toggle", { entity_id: a.switch.id })) : null;
+ if (pw) acts.push({ icon: "mdi:power", label: isOn(a.switch.state) ? t("Uit") : t("Aan"), on: isOn(a.switch.state), run: pw });
if (!a.op && a.power) return plugCard(p, a, st, title, cfg, acts);
return html`
0, on: APPLIANCE_ACTIVE.includes(st.st), running: st.st === "running", unavailable: st.st === "unknown" })}
${pressDir(p, { tap: () => p.openPopup({ type: "appliance", a }), hold: () => p.openPopup({ type: "appliance", a }) })}>
@@ -524,7 +534,7 @@ export function stripCard(p, st) {
${title}
${un ? t("Onbereikbaar") : on ? t("{n} van {m} aan", { n: on, m: st.switches.length }) : t("Alles uit")}
${st.switches.map((e, i) => html``)}
`;
}
diff --git a/src/lang/de.js b/src/lang/de.js
index a7f9884..4bc0f7a 100644
--- a/src/lang/de.js
+++ b/src/lang/de.js
@@ -350,4 +350,10 @@ export default {
"Geen onbereikbare apparaten, lege batterijen of updates": "Keine unerreichbaren Geräte, leeren Batterien oder Updates",
"In deze ruimte": "In diesem Raum",
"De entiteit hoort al bij deze ruimte; de kaart komt erbij.": "Die Entität gehört schon zu diesem Raum; die Karte kommt dazu.",
+ "Stekker": "Stecker",
+ "Echt uitzetten?": "Wirklich ausschalten?",
+ "Echt aanzetten?": "Wirklich einschalten?",
+ "met bevestiging": "mit Rückfrage",
+ "alleen meten": "nur messen",
+ "alleen meten: geen schakelknop en de kaart schakelt nooit, voor bijv. een koelkast": "nur messen: keine Schalttaste, und die Karte schaltet nie, z. B. für einen Kühlschrank",
};
diff --git a/src/lang/en.js b/src/lang/en.js
index d555b34..6b73b77 100644
--- a/src/lang/en.js
+++ b/src/lang/en.js
@@ -473,4 +473,10 @@ export default {
"Geen onbereikbare apparaten, lege batterijen of updates": "No unreachable devices, empty batteries or updates",
"In deze ruimte": "In this room",
"De entiteit hoort al bij deze ruimte; de kaart komt erbij.": "The entity already belongs to this room; the card is added.",
+ "Stekker": "Plug",
+ "Echt uitzetten?": "Really switch off?",
+ "Echt aanzetten?": "Really switch on?",
+ "met bevestiging": "ask first",
+ "alleen meten": "measure only",
+ "alleen meten: geen schakelknop en de kaart schakelt nooit, voor bijv. een koelkast": "measure only: no switch button and the card never switches, e.g. for a fridge",
};
diff --git a/src/popups.js b/src/popups.js
index 2c18553..abf0370 100644
--- a/src/popups.js
+++ b/src/popups.js
@@ -7,7 +7,7 @@ import { name, deviceName } from "./model.js";
import { PAGES } from "./pages.js";
import { keypad, bars } from "./extras.js";
import { eventWhen, timeline, applianceState } from "./extras2.js";
-import { wasteColor, wasteWhen, condText, condIcon, condSvg, label, camStream, attFilter, ATT_FILTERS, mediaView , lightCaps, batteryOf, groupMembers, groupKind, groupCaps, cardType, anyIco, cardCfg } from "./cards.js";
+import { wasteColor, wasteWhen, condText, condIcon, condSvg, label, camStream, attFilter, ATT_FILTERS, mediaView , lightCaps, batteryOf, groupMembers, groupKind, groupCaps, cardType, anyIco, cardCfg, guardPlug } from "./cards.js";
import { roomPresets, snapshot, applyPreset, presetMatches, installUpdate, pendingUpdates } from "./features.js";
import { modeList, toggleMode } from "./extras3.js";
import { field, text, select, check, condFields } from "./settings.js";
@@ -156,6 +156,7 @@ function cardForm(p, pop) {
${dom === "switch" ? row(t("Vermogenssensor"), select(p.hass, "sensor", draft.power, v => upd({ power: v || undefined }), { empty: t("automatisch (zelfde apparaat)"), filter: s => s.attributes.unit_of_measurement === "W" }), t("maakt er een stekkerkaart van (watt, verbruik, timer)")) : nothing}
${dom === "switch" && (applianceCapable(p, id) || draft.power) ? row(t("Apparaatfuncties"), html`
p.setApplianceMode(id, e.target.checked)}>`, t("bezig / klaar op basis van het vermogen")) : nothing}
${appliance ? row(t("Apparaatfuncties"), html`
p.setApplianceMode(pop.a.key, e.target.checked)}>`, pop.a.op ? t("bezig / klaar volgens het apparaat zelf") : t("bezig / klaar op basis van het vermogen")) : nothing}
+ ${dom === "switch" || (appliance && pop.a.switch) ? row(t("Stekker"), seg("plug", [{ v: "", l: t("schakelen") }, { v: "confirm", l: t("met bevestiging") }, { v: "lock", l: t("alleen meten") }], ""), t("alleen meten: geen schakelknop en de kaart schakelt nooit, voor bijv. een koelkast")) : nothing}
${appliance && pop.a.power ? row(t("Drempel (W)"), html`
upd({ threshold: e.target.value === "" || +e.target.value === 5 ? undefined : +e.target.value })}>`, t("boven = bezig")) : nothing}
${hasPress ? html`
${sh(t("Gedrag"))}
@@ -224,7 +225,7 @@ export function renderPopup(p, pop) {
case "switch": {
const s = hass.states[pop.e.id], on = isOn(s), d = pop.e.id.split(".")[0];
return wrap(roomName, label(p, pop.e), on ? t("Aan") : t("Uit"), html`
- ${modes([{ label: on ? t("Uitschakelen") : t("Inschakelen"), on, go: () => p.call(d === "input_boolean" ? "input_boolean" : d === "fan" ? "fan" : "switch", "toggle", { entity_id: pop.e.id }) }], "n2")}
+ ${(go => go ? modes([{ label: on ? t("Uitschakelen") : t("Inschakelen"), on, go }], "n2") : nothing)(guardPlug(p, [pop.e.id], label(p, pop.e), on, () => p.call(d === "input_boolean" ? "input_boolean" : d === "fan" ? "fan" : "switch", "toggle", { entity_id: pop.e.id })))}
${li("mdi:history", on ? t("Aan sinds") : t("Uit sinds"), relTime(s.last_changed))}${s.attributes.current_power_w ? li("mdi:flash-outline", t("Verbruik"), `${s.attributes.current_power_w} W`) : nothing}
`);
}
case "fan": {
@@ -299,7 +300,8 @@ export function renderPopup(p, pop) {
const fmt = e => { const s = e && hass.states[e.id]; if (!s || unavailable(s)) return null; const v = hass.formatEntityState?.(s) ?? s.state;
return v === s.state && /_program_/.test(v) ? v.split("_program_").pop().replace(/_/g, " ").replace(/^\w/, c => c.toUpperCase()) : v; }; // untranslated "dishcare_dishwasher_program_auto_2" → "Auto 2"
const prog = fmt(a.program), door = a.door ? hass.states[a.door.id]?.state : null, canStop = a.stop && ["running", "waiting"].includes(st.st) && hass.states[a.stop.id] && hass.states[a.stop.id].state !== "unavailable"; // a button's normal state is "unknown"
- const btns = [...(sw ? [{ label: sw.state === "on" ? t("Uitzetten") : t("Aanzetten"), on: sw.state === "on", go: () => p.call("switch", "toggle", { entity_id: a.switch.id }) }] : []), ...(canStop ? [{ label: t("Programma stoppen"), go: () => p.call("button", "press", { entity_id: a.stop.id }) }] : [])];
+ const swGo = sw ? guardPlug(p, [a.id, a.switch.id], title, sw.state === "on", () => p.call("switch", "toggle", { entity_id: a.switch.id })) : null;
+ const btns = [...(swGo ? [{ label: sw.state === "on" ? t("Uitzetten") : t("Aanzetten"), on: sw.state === "on", go: swGo }] : []), ...(canStop ? [{ label: t("Programma stoppen"), go: () => p.call("button", "press", { entity_id: a.stop.id }) }] : [])];
return wrap(roomName, title, st.label, html`
${btns.length ? modes(btns, "n2") : nothing}
${st.progress != null && ["running", "waiting"].includes(st.st) ? html`
` : nothing}
@@ -307,7 +309,7 @@ export function renderPopup(p, pop) {
}
const ps = hass.states[a.power.id];
return wrap(roomName, cfg.name || (hass.devices?.[a.device]?.name_by_user || hass.devices?.[a.device]?.name || t("Apparaat")), st.label, html`
- ${sw ? modes([{ label: sw.state === "on" ? t("Stekker uit") : t("Stekker aan"), on: sw.state === "on", go: () => p.call("switch", "toggle", { entity_id: a.switch.id }) }], "n2") : nothing}
+ ${(go => go ? modes([{ label: sw.state === "on" ? t("Stekker uit") : t("Stekker aan"), on: sw.state === "on", go }], "n2") : nothing)(sw ? guardPlug(p, [a.id, a.switch.id], cfg.name || (hass.devices?.[a.device]?.name_by_user || hass.devices?.[a.device]?.name || t("Apparaat")), sw.state === "on", () => p.call("switch", "toggle", { entity_id: a.switch.id })) : null)}
${li("mdi:flash-outline", t("Vermogen nu"), st.power != null ? `${Math.round(st.power)} W` : "–", { onclick: () => p.moreInfo(a.power.id) })}${li("mdi:tune", t("Drempel"), `${thr} W`, { sub: t("aanpassen via ⋯") })}${st.doneAt ? li("mdi:check", t("Klaar sinds"), relTime(st.doneAt.toISOString())) : nothing}${ps ? li("mdi:history", t("Laatste meting"), relTime(ps.last_updated)) : nothing}
${t("Bezig zolang het vermogen boven de drempel ligt; daarna \"Klaar\" tot het apparaat weer start.")}
`);
}