🔔 Countdown-Widget (ioBroker VIS + MagicMirror)

Dieses Widget zeigt automatisch die zwei nächsten anstehenden Countdowns an (z. B. Weihnachten, Silvester, Urlaub, Touren usw.).

Abgelaufene Countdowns werden automatisch ausgeblendet.

⚠️ WICHTIGE HINWEISE (bitte lesen!)

JEDER Countdown muss im versteckten Subscribe-Block stehen

KEINE Einzeiler mit { … } im JavaScript

Kein Array / kein Objekt-Literal

Code-Formatierung NICHT verändern

Sonst erscheint:

SyntaxError: Unexpected token 'null'

🧩 1. Subscribe-Block (Pflicht!)

Hier meldet VIS alle benötigten Datenpunkte an.

{countdown.0.countdowns.Weihnachten.fullJson}
{countdown.0.countdowns.Silvester.fullJson}
{countdown.0.countdowns.SommerurlaubItalien.fullJson}

👉 Für jeden neuen Countdown MUSS hier eine Zeile ergänzt werden!

🖼️ 2. Anzeige-HTML (2 Slots)

<div id="cd_text_xmas" style="font-size:28px;font-weight:700;line-height:1.15;">
Lade...
</div>

<div style="height:18px;"></div>

<div id="cd_text_ny" style="font-size:28px;font-weight:700;line-height:1.15;">
Lade...
</div>

Die IDs bleiben absichtlich so – sie sind MagicMirror-erprobt.

🧠 3. JavaScript – Finale Version

⚠️ NICHT kürzen, NICHT umformatieren

<!-- ==========================================================
VIS Subscribe-Block (Pflicht!)
Jede OID, die du unten nutzt, MUSS hier stehen.
========================================================== -->
<div style="display:none;">
{countdown.0.countdowns.Weihnachten.fullJson}
{countdown.0.countdowns.Silvester.fullJson}
{countdown.0.countdowns.SommerurlaubItalien.fullJson}
</div>
<div style="text-align:center;">
<div id="cd_text_xmas" style="font-size:28px;font-weight:700;line-height:1.15;">
Lade...
</div>
<div style="height:18px;"></div>
<div id="cd_text_ny" style="font-size:28px;font-weight:700;line-height:1.15;">
Lade...
</div>
</div>
<script>
(function ()
{
/* ==========================================================
KONFIGURATION
========================================================== */
const EMPTY_TEXT = "…";
// Wenn TRUE: Stunden werden ausgeblendet, sobald days > 1
// (für days === 1 werden Stunden weiterhin angezeigt)
const HIDE_HOURS_IF_MORE_THAN_ONE_DAY = true;
// Wieviele Events sind AKTIV konfiguriert (max 8)
const EVENT_COUNT = 3;
/* ==========================================================
EVENT-DEFINITIONEN
TYPE:
1 = Weihnachten (Speziallogik)
2 = Silvester (Speziallogik)
3 = Generisch (Titel + Emoji)
========================================================== */
// ---------------- Event 1 ----------------
const E1_OID = "countdown.0.countdowns.Weihnachten.fullJson";
const E1_TYPE = 1;
const E1_TITLE = "";
const E1_EMOJI = "";
// ---------------- Event 2 ----------------
const E2_OID = "countdown.0.countdowns.Silvester.fullJson";
const E2_TYPE = 2;
const E2_TITLE = "";
const E2_EMOJI = "";
// ---------------- Event 3 ----------------
const E3_OID = "countdown.0.countdowns.SommerurlaubItalien.fullJson";
const E3_TYPE = 3;
const E3_TITLE = "Sommerurlaub Italien";
const E3_EMOJI = "☀️🇮🇹";
// ---------------- Event 4..8 (Vorlagen) ----------------
const E4_OID = "";
const E4_TYPE = 3;
const E4_TITLE = "";
const E4_EMOJI = "";
const E5_OID = "";
const E5_TYPE = 3;
const E5_TITLE = "";
const E5_EMOJI = "";
const E6_OID = "";
const E6_TYPE = 3;
const E6_TITLE = "";
const E6_EMOJI = "";
const E7_OID = "";
const E7_TYPE = 3;
const E7_TITLE = "";
const E7_EMOJI = "";
const E8_OID = "";
const E8_TYPE = 3;
const E8_TITLE = "";
const E8_EMOJI = "";
/* ==========================================================
HILFSFUNKTIONEN (VIS-safe formatiert)
========================================================== */
function safeNum(x, fallback)
{
const n = Number(x);
if (Number.isFinite(n))
{
return n;
}
return fallback;
}
function parseFullJson(val)
{
if (val === null || val === undefined || val === "")
{
return null;
}
try
{
let t = val;
if (typeof t === "string")
{
t = t.trim();
if (t.startsWith('"') && t.endsWith('"'))
{
t = JSON.parse(t);
}
return JSON.parse(t);
}
return t;
}
catch (e)
{
return null;
}
}
function subline(s)
{
return "<br><span style='font-size:18px;font-weight:400;opacity:.85'>(noch " + s + ")</span>";
}
function dayWord(n)
{
if (n === 1)
{
return "Tag";
}
return "Tagen";
}
function hourWord(n)
{
if (n === 1)
{
return "Stunde";
}
return "Stunden";
}
/* ==========================================================
EVENT-ZUGRIFF (ohne Arrays/Objekte)
========================================================== */
function getOid(i)
{
if (i === 1) return E1_OID;
if (i === 2) return E2_OID;
if (i === 3) return E3_OID;
if (i === 4) return E4_OID;
if (i === 5) return E5_OID;
if (i === 6) return E6_OID;
if (i === 7) return E7_OID;
if (i === 8) return E8_OID;
return "";
}
function getType(i)
{
if (i === 1) return E1_TYPE;
if (i === 2) return E2_TYPE;
if (i === 3) return E3_TYPE;
if (i === 4) return E4_TYPE;
if (i === 5) return E5_TYPE;
if (i === 6) return E6_TYPE;
if (i === 7) return E7_TYPE;
if (i === 8) return E8_TYPE;
return 0;
}
function getTitle(i)
{
if (i === 1) return E1_TITLE;
if (i === 2) return E2_TITLE;
if (i === 3) return E3_TITLE;
if (i === 4) return E4_TITLE;
if (i === 5) return E5_TITLE;
if (i === 6) return E6_TITLE;
if (i === 7) return E7_TITLE;
if (i === 8) return E8_TITLE;
return "";
}
function getEmoji(i)
{
if (i === 1) return E1_EMOJI;
if (i === 2) return E2_EMOJI;
if (i === 3) return E3_EMOJI;
if (i === 4) return E4_EMOJI;
if (i === 5) return E5_EMOJI;
if (i === 6) return E6_EMOJI;
if (i === 7) return E7_EMOJI;
if (i === 8) return E8_EMOJI;
return "";
}
/* ==========================================================
RESTZEIT / ABGELAUFEN
========================================================== */
function remainingSeconds(val)
{
const d = parseFullJson(val);
if (!d)
{
return 999999999;
}
if (d.total && d.total.seconds !== undefined)
{
return safeNum(d.total.seconds, 999999999);
}
const days = safeNum((d.total && d.total.days !== undefined) ? d.total.days : d.days, 0);
const hours = safeNum((d.total && d.total.hours !== undefined) ? d.total.hours : d.hours, 0);
const mins = safeNum((d.total && d.total.minutes !== undefined) ? d.total.minutes : d.minutes, 0);
const secs = safeNum((d.total && d.total.seconds !== undefined) ? d.total.seconds : d.seconds, 0);
return (days * 86400) + (hours * 3600) + (mins * 60) + secs;
}
function isExpired(val)
{
return (remainingSeconds(val) <= 0);
}
/* ==========================================================
FORMATTER: Zeittext mit optionalen Stunden
----------------------------------------------------------
Gibt nur den "Zeitanteil" zurück, z.B.:
- "3 Tagen" (wenn days>1 und hideHours aktiv)
- "1 Tag und 5 Stunden"
========================================================== */
function formatDaysHours(days, hours)
{
// Wenn konfiguriert: Stunden ausblenden sobald days > 1
if (HIDE_HOURS_IF_MORE_THAN_ONE_DAY)
{
if (days > 1)
{
return String(days) + " " + dayWord(days);
}
}
// Standard: Tage + Stunden
return String(days) + " " + dayWord(days) + " und " + String(hours) + " " + hourWord(hours);
}
/* ==========================================================
RENDERER
========================================================== */
function renderWeihnachten(val)
{
const data = parseFullJson(val);
const now = new Date();
if (now.getMonth() === 11 && now.getDate() >= 24 && now.getDate() <= 26)
{
return "🎄 Frohe Weihnachten 🎄<br><span style='font-size:26px;'>🎅🛷🎁</span>";
}
if (!data)
{
return EMPTY_TEXT;
}
const days = safeNum((data.total && data.total.days !== undefined) ? data.total.days : data.days, 0);
const hours = safeNum((data.hours !== undefined) ? data.hours : (data.total ? data.total.hours : undefined), 0);
const words = (data.inWords && data.inWords.long) ? data.inWords.long : "";
if (now.getMonth() === 11 && now.getDate() === 23)
{
return "🎅 Weihnachten morgen" + subline(words);
}
if (days >= 1)
{
return "🎁 Weihnachten in " + formatDaysHours(days, hours);
}
return "🎄 Frohe Weihnachten 🎄";
}
function renderSilvester(val)
{
const data = parseFullJson(val);
const now = new Date();
if (now.getMonth() === 11 && now.getDate() === 31)
{
return "🎇 Silvester heute";
}
if (now.getMonth() === 0 && now.getDate() === 1)
{
return "🥂🎉 Frohes neues Jahr! 🎉🥂";
}
if (!data)
{
return EMPTY_TEXT;
}
const days = safeNum((data.total && data.total.days !== undefined) ? data.total.days : data.days, 0);
const hours = safeNum((data.hours !== undefined) ? data.hours : (data.total ? data.total.hours : undefined), 0);
const words = (data.inWords && data.inWords.long) ? data.inWords.long : "";
if (now.getMonth() === 11 && now.getDate() === 30)
{
return "🎆 Silvester morgen" + subline(words);
}
if (days >= 1)
{
return "🕛 Silvester in " + formatDaysHours(days, hours);
}
return "🎇 Silvester heute" + subline(words);
}
function renderGeneric(val, title, emoji)
{
const data = parseFullJson(val);
if (!data)
{
return EMPTY_TEXT;
}
const days = safeNum((data.total && data.total.days !== undefined) ? data.total.days : data.days, 0);
const hours = safeNum((data.hours !== undefined) ? data.hours : (data.total ? data.total.hours : undefined), 0);
const words = (data.inWords && data.inWords.long) ? data.inWords.long : "";
if (days === 1)
{
return emoji + " " + title + " morgen" + subline(words);
}
if (days >= 1)
{
return emoji + " " + title + " in " + formatDaysHours(days, hours);
}
if (words)
{
return emoji + " " + title + subline(words);
}
return emoji + " " + title + " bald";
}
function renderByIndex(i, val)
{
const t = getType(i);
if (t === 1)
{
return renderWeihnachten(val);
}
if (t === 2)
{
return renderSilvester(val);
}
return renderGeneric(val, getTitle(i), getEmoji(i));
}
/* ==========================================================
AUSWAHL: 2 nächste Events
========================================================== */
function pickTwoUpcoming()
{
let best1_i = -1;
let best1_r = 999999999;
let best2_i = -1;
let best2_r = 999999999;
let i = 1;
while (i <= EVENT_COUNT)
{
const oid = getOid(i);
if (oid)
{
const val = vis.states.attr(oid + ".val");
if (!isExpired(val))
{
const r = remainingSeconds(val);
if (r < best1_r)
{
best2_i = best1_i;
best2_r = best1_r;
best1_i = i;
best1_r = r;
}
else
{
if (r < best2_r)
{
best2_i = i;
best2_r = r;
}
}
}
}
i = i + 1;
}
return String(best1_i) + "|" + String(best2_i);
}
function renderSlot(slotIndex, el)
{
const picks = pickTwoUpcoming().split("|");
let idx = -1;
if (slotIndex === 1)
{
idx = Number(picks[0]);
}
else
{
idx = Number(picks[1]);
}
if (idx < 1)
{
el.innerHTML = EMPTY_TEXT;
return;
}
const oid = getOid(idx);
const val = vis.states.attr(oid + ".val");
el.innerHTML = renderByIndex(idx, val);
}
/* ==========================================================
START & BINDING
========================================================== */
function start()
{
const el1 = document.getElementById("cd_text_xmas");
const el2 = document.getElementById("cd_text_ny");
if (typeof vis !== "undefined" && vis.states && el1 && el2)
{
function updateSlot1()
{
renderSlot(1, el1);
}
function updateSlot2()
{
renderSlot(2, el2);
}
if (E1_OID) vis.states.bind(E1_OID + ".val", updateSlot1);
if (E2_OID) vis.states.bind(E2_OID + ".val", updateSlot1);
if (E3_OID) vis.states.bind(E3_OID + ".val", updateSlot1);
if (E4_OID) vis.states.bind(E4_OID + ".val", updateSlot1);
if (E5_OID) vis.states.bind(E5_OID + ".val", updateSlot1);
if (E6_OID) vis.states.bind(E6_OID + ".val", updateSlot1);
if (E7_OID) vis.states.bind(E7_OID + ".val", updateSlot1);
if (E8_OID) vis.states.bind(E8_OID + ".val", updateSlot1);
if (E1_OID) vis.states.bind(E1_OID + ".val", updateSlot2);
if (E2_OID) vis.states.bind(E2_OID + ".val", updateSlot2);
if (E3_OID) vis.states.bind(E3_OID + ".val", updateSlot2);
if (E4_OID) vis.states.bind(E4_OID + ".val", updateSlot2);
if (E5_OID) vis.states.bind(E5_OID + ".val", updateSlot2);
if (E6_OID) vis.states.bind(E6_OID + ".val", updateSlot2);
if (E7_OID) vis.states.bind(E7_OID + ".val", updateSlot2);
if (E8_OID) vis.states.bind(E8_OID + ".val", updateSlot2);
updateSlot1();
updateSlot2();
}
else
{
setTimeout(start, 300);
}
}
if (document.readyState === "complete")
{
start();
}
else
{
window.addEventListener("load", start);
}
})();
</script>

➕ Neuen Countdown hinzufügen (Checkliste) Beispiel: countdown.0.countdowns.Herbsturlaub.fullJson ✅ Schritt 1 – Subscribe-Block {countdown.0.countdowns.Herbsturlaub.fullJson}

✅ Schritt 2 – OID definieren const OID_4 = „countdown.0.countdowns.Herbsturlaub.fullJson“;

✅ Schritt 3 – Renderfunktion ergänzen if (i === 4) return renderGeneric(val, „Herbsturlaub“, „🍂🧳“);

✅ Schritt 4 – remainingSeconds / pickTwo erweitern

(analog zu Event 3)