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 })))} `); } 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)}
${t("Bezig zolang het vermogen boven de drempel ligt; daarna \"Klaar\" tot het apparaat weer start.")}
`); }