Skip to main content

🗓️ Abfuhrtermine (Abfall)

ioBroker Beispiel: Automatische VIS2-Visualisierung von iCal-Abfuhrtermine mit dynamischer Icon-Zuordnung und Echtzeit-Aktualisierung

VIS2-Anzeige (Beispiel): Abfuhrtermine.png

Verwende in VIS2 z.B. das "Basic"-Widget mit der Option "String (unescaped)", um die Termine anzuzeigen: Abfall_VIS2_Witget.png


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: Hellgrüner Hintergrund
  • Hervorhebung für morgen fällige Termine: Hellgelber 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
Icons Abfall-Symbole im Ordner /vis-2.0/Abfall/
JavaScript-Adapter Für das Verarbeitungs-Skript (z.B. javascript.0)

Funktionsweise

  1. Datenpunkt ical.0.data.table:

    • Der ical-Adapter erstellt diesen Datenpunkt automatisch, sobald er die Kalenderdaten erfolgreich abruft und verarbeitet.
  2. JavaScript-Skript:

    • Ein Skript registriert einen Event-Handler für Änderungen an ical.0.data.table.
    • Bei Änderungen generiert das Skript HTML-Code für die Anzeige der Abfalltermine.

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-01b

// =====================================
// 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 => ({
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#39;'
    }[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);
}

// ===== EVENT-HANDLER für Änderungen an 'ical.0.data.table' =====
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
        );
    }
});

🎨 Beispiel CSS für das VIS2

/* 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: 14px Arial, sans-serif;
    text-shadow: 1px 1px 2px #000;
}
/* Hervorhebung */
.AbfallText.today,
.AbfallText.tomorrow {
    font-weight: bold;
    color: #000;
    text-shadow: none;
    padding: 4px 6px;
    border-radius: 4px;
}
/* Spezifische Farben */
.AbfallText.today {
    background-color: #e6ffe6;
}
.AbfallText.tomorrow {
    background-color: #fff8e6;
}