🗓️ Abfuhrtermine (Abfall)
ioBroker Beispiel: Automatische VIS-2 Visualisierung von iCal-Abfuhrtermine mit dynamischer Bilder-Zuordnung und Echtzeit-Aktualisierung
Verwende in VIS-2 z.B. das "Basic"-Widget mit der Option "String (unescaped)", um die Termine anzuzeigen:

Funktionen
- Zeigt Abfuhrtermine als übersichtliche HTML-Tabelle
- Unterstützt alle gängigen Abfallarten (Restmüll, Papier, Gelber Sack etc.)
- Wählbares Datumsformat:
Mo, 01.01.(weekday_first)01.01. (Mo)(short)01.01.2024 (Mo)(long)
- Anzahl der angezeigten Termine einstellbar (Standard: 4)
- Es werden nur heutige und zukünftige Termine angezeigt
- Termine werden automatisch nach Datum sortiert
- Hervorhebung für heutige Termine: Roter Hintergrund
- Hervorhebung für morgen fällige Termine: Oranger Hintergrund
- Automatische Anpassung der Textfarbe für optimale Lesbarkeit auf den Hervorhebungsfarben
- Textschatten für bessere Lesbarkeit auf dunklem Hintergrund (wird bei Hervorhebungen deaktiviert)
Voraussetzungen
| Was du brauchst | Details |
|---|---|
| ical-Adapter | Muss installiert und mit deinem Abfuhrtermin (Kalender URL) verbunden sein |
| ical-URL | Beispiel: https://art-trier.de/ics-feed/[PLZ]:[ORT]@.ics |
| Datenpunkt | ical.0.data.table wird automatisch vom ical-Adapter erstellt und mit den Kalenderdaten gefüllt |
| Bilder | Abfall-Symbole im Ordner /vis-2.0/Abfall/ |
| JavaScript-Adapter | Für das Verarbeitungs-Skript (z.B. javascript.0) |
Funktionsweise
-
Datenpunkt
ical.0.data.table:- Der ical-Adapter erstellt diesen Datenpunkt automatisch, sobald er die Kalenderdaten erfolgreich abruft und verarbeitet.
-
JavaScript-Skript:
- Das Skript reagiert auf drei Auslöser:
- Trigger 1: Änderung von
ical.0.data.table→ wenn ical neue Daten lädt - Trigger 2: Täglich um Mitternacht → damit vergangene Termine aus der Anzeige verschwinden
- Trigger 3: Einmalig beim Skript-Start → damit die Anzeige sofort befüllt ist
- Trigger 1: Änderung von
- Das Skript reagiert auf drei Auslöser:
-
Automatische Aktualisierung (ical-Adapter Schedule):
- Der ical-Adapter ruft die Kalenderdaten automatisch ab – bei mir jeden Sonntag um 02:00 Uhr (
0 2 * * 0). - Den Schedule findest du in den Instanz-Einstellungen von
ical.0(Schraubenschlüssel-Icon). - Bei jeder Aktualisierung wird
ical.0.data.tableneu befüllt und das JavaScript-Skript automatisch ausgelöst.
- Der ical-Adapter ruft die Kalenderdaten automatisch ab – bei mir jeden Sonntag um 02:00 Uhr (
Zusammenfassung
- Der ical-Adapter liest die Kalenderdaten und erstellt automatisch den Datenpunkt
ical.0.data.table. - Ein JavaScript-Skript reagiert auf Änderungen dieses Datenpunkts und erstellt eine benutzerfreundliche Anzeige der Abfalltermine.
⚡ JavaScript (Skriptname: "Abfall")
// ioBroker Abfuhrtermine (Abfall)
// V 2026-04-25
// =====================================
// Um manuell auszulösen, starte einfach den ical Adapter neu (reload)
// ===== EINSTELLUNGEN =====
const DATE_FORMAT = "weekday_first"; // "short", "long", "weekday_first"
const MAX_TERMINE = 4;
const TEST_ALL_TODAY = false; // TESTSCHALTER (true = alles wird "today")
const STATE_ID = 'javascript.0.Abfall';
// ===== KONSTANTEN =====
const ABFALL_TYPES = {
PAPIER_GELBER_SACK: {
name: ["Papier & Gelber Sack", "Papierabfall", "Gelber Sack", "Papier & Gelber Sack"],
image: "/vis-2.0/Abfall/gelber_sack_und_papierabfall.png"
},
PAPIER: {
name: "Papier",
image: "/vis-2.0/Abfall/papierabfall.png"
},
GELBER_SACK: {
name: "Gelber Sack",
image: "/vis-2.0/Abfall/gelber_sack.png"
},
RESTMUELL: {
name: ["Restabfall", "Restmüll"],
image: "/vis-2.0/Abfall/restmuell.png"
}
};
// ===== HILFSFUNKTIONEN =====
function escapeHTML(str) {
if (!str) return "";
return str.replace(/[&<>"']/g, m => ({
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}[m]));
}
function getAbfallType(eventText) {
if (!eventText) return ABFALL_TYPES.RESTMUELL;
const normalizedEventText = eventText.toLowerCase().trim();
for (const type of Object.values(ABFALL_TYPES)) {
const names = Array.isArray(type.name) ? type.name : [type.name];
for (const name of names) {
if (normalizedEventText.includes(name.toLowerCase().trim())) {
return type;
}
}
}
return ABFALL_TYPES.RESTMUELL;
}
function parseDate(dateStr) {
if (!dateStr) return null;
const match = dateStr.match(/^(\d{2})\.(\d{2})\.(\d{4})/);
if (!match) return null;
const day = Number(match[1]);
const month = Number(match[2]);
const year = Number(match[3]);
const date = new Date(year, month - 1, day);
return isNaN(date) ? null : date;
}
function formatDateFromDate(dateObj, format = DATE_FORMAT) {
if (!dateObj || isNaN(dateObj)) return "–";
const weekdays = ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"];
const weekday = weekdays[dateObj.getDay()];
const day = String(dateObj.getDate()).padStart(2, "0");
const month = String(dateObj.getMonth() + 1).padStart(2, "0");
const year = dateObj.getFullYear();
switch (format) {
case "long":
return `${day}.${month}.${year} (${weekday})`;
case "weekday_first":
return `${weekday}, ${day}.${month}.`;
case "short":
default:
return `${day}.${month}. (${weekday})`;
}
}
function normalizeDate(date) {
const d = new Date(date);
d.setHours(0, 0, 0, 0);
return d;
}
function isToday(date) {
const today = normalizeDate(new Date());
return normalizeDate(date).getTime() === today.getTime();
}
function isTomorrow(date) {
const tomorrow = normalizeDate(new Date());
tomorrow.setDate(tomorrow.getDate() + 1);
return normalizeDate(date).getTime() === tomorrow.getTime();
}
// ===== HAUPTFUNKTION =====
function MakeAbfallHTML(obj) {
if (!obj || !Array.isArray(obj)) {
setState(STATE_ID, "<div style='color:red;'>Fehler: Ungültige Kalenderdaten</div>", true);
return;
}
if (obj.length === 0) {
setState(STATE_ID, "<div>Keine Termine vorhanden</div>", true);
return;
}
const today = normalizeDate(new Date());
const cleaned = obj
.map(entry => {
const date = parseDate(entry?.date);
return { ...entry, parsedDate: date };
})
.filter(entry => entry.parsedDate && normalizeDate(entry.parsedDate) >= today)
.sort((a, b) => a.parsedDate - b.parsedDate);
if (cleaned.length === 0) {
setState(STATE_ID, "<div>Keine zukünftigen Termine</div>", true);
return;
}
const termineCount = Math.min(MAX_TERMINE, cleaned.length);
let html = "<table class='Abfall'><tr>";
// ===== BILDERZEILE =====
for (let i = 0; i < termineCount; i++) {
const entry = cleaned[i];
if (entry?.event) {
const abfallType = getAbfallType(entry.event);
const label = Array.isArray(abfallType.name) ? abfallType.name[0] : abfallType.name;
html += `
<td class='Abfallimage'>
<img
width='80'
height='80'
src='${abfallType.image}'
alt='${escapeHTML(label)}'
>
</td>`;
} else {
html += "<td class='Abfallimage'></td>";
}
}
// ===== DATUMSZEILE =====
html += "</tr><tr>";
for (let i = 0; i < termineCount; i++) {
const entry = cleaned[i];
const eventDate = entry.parsedDate;
let dateClass = 'AbfallText';
if (TEST_ALL_TODAY) {
dateClass += ' today';
} else if (eventDate) {
if (isToday(eventDate)) {
dateClass += ' today';
} else if (isTomorrow(eventDate)) {
dateClass += ' tomorrow';
}
}
const dateStr = formatDateFromDate(eventDate);
html += `<td class='${dateClass}'>${escapeHTML(dateStr)}</td>`;
}
html += "</tr></table>";
setState(STATE_ID, html, true);
}
// ===== TRIGGER 1: ical hat neue Daten =====
on('ical.0.data.table', function (theObj) {
try {
MakeAbfallHTML(theObj?.state?.val);
} catch (error) {
console.error("Fehler im Abfall-Script:", error.stack || error);
setState(STATE_ID, `<div style='color:red;'>Script-Fehler: ${escapeHTML(error.message)}</div>`, true);
}
});
// ===== TRIGGER 2: Täglich Mitternacht (vergangene Termine entfernen) =====
schedule("0 0 * * *", function () {
try {
MakeAbfallHTML(getState('ical.0.data.table')?.val);
} catch (error) {
console.error("Fehler im Schedule:", error.stack || error);
}
});
// ===== TRIGGER 3: Einmalig beim Skript-Start =====
try {
MakeAbfallHTML(getState('ical.0.data.table')?.val);
} catch (error) {
console.error("Fehler beim Start:", error.stack || error);
}
🎨 Beispiel CSS für das VIS-2
/* Abfuhrtermine-Stil */
.Abfall {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
}
.Abfall td {
padding: 2px;
text-align: center;
vertical-align: middle;
}
/* Bilder */
.Abfallimage img {
max-width: 80px;
max-height: 80px;
}
/* Basis-Text */
.AbfallText {
width: 100%;
color: #fff;
font-family: Arial, sans-serif;
font-size: 14px;
text-shadow: 1px 1px 2px #000;
}
/* Hervorhebung */
.AbfallText.today,
.AbfallText.tomorrow {
font-weight: bold;
text-shadow: none;
padding: 4px 6px;
border-radius: 4px;
}
/* Spezifische Farben */
.AbfallText.tomorrow {
background-color: #f57c00; /* Orange */
color: #fff;
}
.AbfallText.today {
background-color: #d32f2f; /* Rot */
color: #fff;
}
