<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Homepilot Rolladensteuerung per Telegram]]></title><description><![CDATA[<p dir="auto">Hallo zusammen,<br />
es ist richtig schön mit chat-gpt zu arbeiten habe ein Script entworfen. Habe folgende Hardware: Homepilot, Aqara Sensoren<br />
Iobroker folgende Adapter: Homepilot 2.0, Deconz, Telegram</p>
<p dir="auto">Das Skript hat im aktuellen Stand diese Eigenschaften:<br />
<strong>Grundfunktion</strong><br />
Es steuert drei Rolläden:<br />
Fenster<br />
Terrasse<br />
Küche<br />
und kombiniert dabei:<br />
feste Zeitfenster<br />
Zufallszeiten<br />
Astrozeiten (dawn, dusk)<br />
Fenster-/Türsensoren<br />
Telegram-Steuerung<br />
VIS-konfigurierbare Zeiten<br />
Urlaubsmodus<br />
<strong>Zeitlogik</strong><br />
Für jeden Tag werden Zufallszeiten neu berechnet für:<br />
<strong>Morgens</strong><br />
Fenster<br />
Terrasse<br />
Küche</p>
<p dir="auto"><strong>Abends</strong><br />
Küche<br />
Fenster<br />
Terrasse</p>
<p dir="auto"><strong>Dabei gilt:</strong></p>
<p dir="auto">morgens wird ab Dämmerung (dawn) gestartet, aber nie vor deinem konfigurierten Startfenster<br />
abends wird ab Dämmerung (dusk) geschlossen, aber nie vor deinem konfigurierten Abendfenster<br />
die berechneten Zeiten liegen immer innerhalb deiner konfigurierten Zeitfenster<br />
die Reihenfolge bleibt erhalten:<br />
morgens: Fenster → Terrasse, Küche separat<br />
abends: Küche → Fenster → Terrasse<br />
Unterschied Werktag / Frei</p>
<p dir="auto"><strong>Das Skript unterscheidet automatisch:</strong><br />
Werktag<br />
Frei = Samstag, Sonntag oder Feiertag</p>
<p dir="auto"><strong>Der Feiertag kommt über:</strong> feiertage.0.heute.boolean<br />
<strong>Urlaubsmodus</strong><br />
Es gibt einen State:<br />
0_userdata.0.Rolladen.Urlaubsmodus<br />
Wenn der aktiv ist:<br />
werden die Zeitabstände größer zufällig gestreut<br />
Telegram meldet zusätzlich 🏖️ Urlaubsmodus aktiv<br />
Sensorlogik<br />
<strong>Küche</strong><br />
Wenn das Küchenfenster offen ist und der Rolladen ganz zu ist:<br />
fährt der Küchenrolladen automatisch auf Spaltposition<br />
Standard: KUECHE_SPALT = 20<br />
<strong>Terrasse</strong><br />
Wenn Terrassentür offen oder gekippt ist und der Rolladen ganz zu ist:<br />
fährt der Terrassenrolladen automatisch auf Spaltposition<br />
Standard: TERRASSE_SPALT = 16<br />
Wenn der Terrassenrolladen abends schließen soll, die Tür aber noch offen ist:<br />
bleibt er erst offen<br />
sobald die Tür geschlossen wird, fährt er nachträglich runter<br />
Prozentanzeige invertiert<br />
<strong>Das Skript zeigt Prozentwerte in Meldungen andersherum an:</strong><br />
anzeigeProzent(val) = 100 - val<br />
Das heißt z. B.:<br />
interner Wert 100 → Anzeige 0%<br />
interner Wert 0 → Anzeige 100%<br />
interner Wert 16 → Anzeige 84%<br />
<strong>Das betrifft:</strong><br />
manuelle Telegram-Meldungen<br />
automatische Telegram-Meldungen<br />
Testfahrten<br />
<strong>Telegram-Funktionen</strong><br />
Hauptmenü</p>
<p dir="auto">Per Telegram gibt es ein Menü mit Buttons für:</p>
<ul>
<li>Morgen</li>
<li>Abend</li>
<li>Infozeiten</li>
<li>aktuelle Zeiten anzeigen</li>
<li>heutige Zufallszeiten anzeigen</li>
<li>Urlaubsmodus an/aus</li>
<li>Testfahrten</li>
<li>Hilfe<br />
<strong>Telegram-Zeitänderung</strong><br />
Du kannst Zeiten per Menü ändern:</li>
</ul>
<p dir="auto"><strong>Für Morgen / Abend:</strong></p>
<ul>
<li>Bereich wählen:<br />
Fenster/Terrasse<br />
Küche</li>
<li>Tagtyp wählen:<br />
Werktag<br />
Frei</li>
<li>Grenze wählen:<br />
Start<br />
Ende</li>
<li>dann nur noch Zeit schicken:<br />
z. B. 06:45<br />
<strong>Für Infozeiten:</strong></li>
<li>Erste Meldung</li>
<li>Zweite Meldung</li>
<li>dann Uhrzeit senden<br />
<strong>Nach dem Speichern bekommst du:</strong></li>
<li>das geänderte Feld</li>
<li>den neuen Wert</li>
<li>die komplette aktuelle Übersicht<br />
<strong>Telegram-Testfahrten</strong><br />
Es gibt ein Testfahrt-Menü für:</li>
<li>Fenster</li>
<li>Terrasse</li>
<li>Küche</li>
</ul>
<p dir="auto">Pro Bereich:</p>
<ul>
<li>Auf</li>
<li>Zu</li>
<li>Spalt<br />
Zusätzlich:</li>
<li>Zufallszeiten neu berechnen</li>
<li>Heutige Zeiten senden<br />
<strong>Telegram-Direktbefehle</strong><br />
Zusätzlich zum Menü gehen weiterhin Textbefehle wie:<br />
/zeit morgens ft werktag start 06:45<br />
/zeit abends kueche frei ende 21:00<br />
/zeit info 1 12:15<br />
/urlaub an<br />
/urlaub aus<br />
/zeiten<br />
/heute<br />
/menu<br />
<strong>VIS / States</strong><br />
Das Skript legt automatisch Config-States an unter: 0_userdata.0.Rolladen.Config<br />
Dort gibt es getrennt:</li>
<li>Morgens</li>
<li>Abends</li>
<li>FensterTerrasse</li>
<li>Kueche</li>
<li>Werktag</li>
<li>Frei</li>
<li>Start</li>
<li>Ende</li>
<li>Info</li>
<li>ErsteMeldung</li>
<li>ZweiteMeldung<br />
<strong>Außerdem Anzeige-States unter:</strong> 0_userdata.0.Rolladen.Heute.<br />
für:</li>
<li>heute Morgen Fenster</li>
<li>heute Morgen Terrasse</li>
<li>heute Morgen Küche</li>
<li>heute Abend Fenster</li>
<li>heute Abend Terrasse</li>
<li>heute Abend Küche</li>
<li><a href="http://Heute.Info" rel="nofollow ugc">Heute.Info</a><br />
<strong>Tägliche Meldungen</strong><br />
Es gibt zwei konfigurierbare Infozeiten:</li>
<li>ErsteMeldung</li>
<li>ZweiteMeldung<br />
Zu diesen Zeiten sendet das Skript per Telegram die heute berechneten Rolladenzeiten.<br />
<strong>Manuelle Bedienung</strong><br />
Wenn ein Rolladen <strong>manuell bewegt</strong> wird und es war <strong>keine automatische Fahrt</strong>:</li>
<li>sendet das Skript eine Telegram-Meldung<br />
Beispiel:</li>
<li>Terrassenrolladen manuell bewegt</li>
<li>Fensterrolladen manuell bewegt</li>
<li>Küchenrolladen manuell bewegt<br />
<strong>Schutz gegen Doppelmeldungen / Doppelaktionen</strong><br />
Das Skript arbeitet mit Tagesflags wie:</li>
<li>morgenHeuteFenster</li>
<li>abendHeuteTerrasse<br />
damit eine automatische Aktion pro Tag nur einmal ausgeführt wird.<br />
<strong>Robuste Punkte</strong><br />
Das Skript ist robuster gemacht durch:</li>
<li>saubere Groß-/Kleinschreibung der Config-States</li>
<li>tolerantes Einlesen von Telegram-Nachrichten</li>
<li>Entfernen von Präfixen</li>
<li>sicheres Parsen von Uhrzeiten</li>
<li>Begrenzung von Zufallszeiten auf erlaubte Fenster</li>
<li>Fallback auf Default-Zeiten, wenn Config fehlt<br />
<strong>Konfigurierbare Kernwerte oben im Skript</strong><br />
Direkt oben anpassbar sind u. a.:</li>
<li>Geräte-IDs</li>
<li>Sensor-IDs</li>
<li>Telegram-/Push-Instanz</li>
<li><strong>Spaltwerte:</strong></li>
<li>FENSTER_SPALT</li>
<li>TERRASSE_SPALT</li>
<li>KUECHE_SPALT</li>
<li>Zufallsstreuung normal</li>
<li>Zufallsstreuung Urlaub</li>
<li>Default-Zeitfenster</li>
</ul>
<pre><code>'use strict';

/* =========================
   Geräte / IDs
   ========================= */

const TERRASSE              = 'homepilot20.0.Actuator.3-10182345.Position_inverted';
const FENSTER               = 'homepilot20.0.Actuator.6-10182345.Position_inverted';
const KUECHE                = 'homepilot20.0.Actuator.7-10122345.Position_inverted';

const TERRASSEN_TUER_AUF    = 'deconz.0.Sensors.18.open';
const TERRASSEN_TUER_KIPP   = 'deconz.0.Sensors.17.open';
const KUECHE_FENSTER        = 'deconz.0.Sensors.16.open';
const FEIERTAG              = 'feiertage.0.heute.boolean';

const TELEGRAM_INSTANCE     = 'telegram.0';
const PUSH_INSTANCE         = 'pushover.0'; // leer lassen '' wenn kein Push genutzt wird

/* =========================
   Basis-Pfade
   ========================= */

const BASE_STATE            = '0_userdata.0.Rolladen';
const CONFIG_STATE          = BASE_STATE + '.Config';
const URLAUBSMODUS          = BASE_STATE + '.Urlaubsmodus';

const AUF                   = 100;
const ZU                    = 0;

const FENSTER_SPALT         = 16;
const TERRASSE_SPALT        = 16;
const KUECHE_SPALT          = 20;

// normale tägliche Streuung in Minuten
const NORMAL_MORGEN_MIN     = 1;
const NORMAL_MORGEN_MAX     = 5;
const NORMAL_ABEND_MIN      = 1;
const NORMAL_ABEND_MAX      = 5;

// größere Streuung im Urlaubsmodus
const URLAUB_MORGEN_MIN     = 2;
const URLAUB_MORGEN_MAX     = 10;
const URLAUB_ABEND_MIN      = 2;
const URLAUB_ABEND_MAX      = 10;

/* =========================
   Default-Zeiten für Config-States
   ========================= */

const DEFAULT_TIMES = {
    morgens: {
        fensterTerrasse: {
            werktag: { start: '06:40', ende: '07:00' },
            frei:    { start: '08:00', ende: '08:30' }
        },
        kueche: {
            werktag: { start: '06:30', ende: '07:00' },
            frei:    { start: '08:00', ende: '08:30' }
        }
    },
    abends: {
        fensterTerrasse: {
            werktag: { start: '20:25', ende: '21:03' },
            frei:    { start: '20:30', ende: '21:30' }
        },
        kueche: {
            werktag: { start: '19:30', ende: '20:00' },
            frei:    { start: '20:30', ende: '21:30' }
        }
    },
    info: {
        ersteMeldung:  '12:00',
        zweiteMeldung: '18:00'
    }
};

/* =========================
   Laufzeitvariablen
   ========================= */

let terrassePending         = false;

let morgenHeuteFenster      = false;
let morgenHeuteTerrasse     = false;
let morgenHeuteKueche       = false;

let abendHeuteFenster       = false;
let abendHeuteTerrasse      = false;
let abendHeuteKueche        = false;

let randomMorningFenster    = null;
let randomMorningTerrasse   = null;
let randomMorningKueche     = null;

let randomEveningFenster    = null;
let randomEveningTerrasse   = null;
let randomEveningKueche     = null;

let randomDateKey           = '';
let lastInfoSent            = {};

// Kennzeichen für automatische Fahrten
let autoMoveTerrasse        = false;
let autoMoveFenster         = false;
let autoMoveKueche          = false;

// Telegram Menü / Eingabe
let telegramEditTarget      = null;
let telegramMenuState       = null;

/* =========================
   Hilfsfunktionen
   ========================= */

function tg(msg) {
    try {
        sendTo(TELEGRAM_INSTANCE, 'send', { text: msg });
    } catch (e) {
        log('Telegram Fehler: ' + e, 'warn');
    }
}

function tgWithKeyboard(msg, keyboardRows) {
    try {
        sendTo(TELEGRAM_INSTANCE, 'send', {
            text: msg,
            reply_markup: {
                keyboard: keyboardRows,
                resize_keyboard: true,
                one_time_keyboard: false
            }
        });
    } catch (e) {
        log('Telegram Keyboard Fehler: ' + e, 'warn');
        tg(msg);
    }
}

function tgMainMenu(msg) {
    tgWithKeyboard(msg || '📋 Rolladen-Menü', [
        ['🌅 Morgen', '🌙 Abend'],
        ['🕒 Infozeiten', '🕒 /zeiten'],
        ['🧪 Testfahrten', '🔄 /heute'],
        ['🏖️ Urlaub an', '🏠 Urlaub aus'],
        ['ℹ️ Hilfe']
    ]);
}

function tgAreaMenuMorgen(msg) {
    tgWithKeyboard(msg || '🌅 Morgen: Bereich wählen', [
        ['🪟 Fenster/Terrasse', '🍽️ Küche'],
        ['⬅️ Zurück', '❌ Abbrechen']
    ]);
}

function tgAreaMenuAbend(msg) {
    tgWithKeyboard(msg || '🌙 Abend: Bereich wählen', [
        ['🪟 Fenster/Terrasse', '🍽️ Küche'],
        ['⬅️ Zurück', '❌ Abbrechen']
    ]);
}

function tgDaytypeMenu(msg) {
    tgWithKeyboard(msg || 'Bitte Tagtyp wählen', [
        ['💼 Werktag', '🎉 Frei'],
        ['⬅️ Zurück', '❌ Abbrechen']
    ]);
}

function tgBoundaryMenu(msg) {
    tgWithKeyboard(msg || 'Bitte Start oder Ende wählen', [
        ['▶️ Start', '⏹️ Ende'],
        ['⬅️ Zurück', '❌ Abbrechen']
    ]);
}

function tgInfoMenu(msg) {
    tgWithKeyboard(msg || '🕒 Infozeiten wählen', [
        ['1️⃣ Erste Meldung', '2️⃣ Zweite Meldung'],
        ['⬅️ Zurück', '❌ Abbrechen']
    ]);
}

function tgTestMenu(msg) {
    tgWithKeyboard(msg || '🧪 Testfahrten wählen', [
        ['🪟 Fenster', '🏡 Terrasse'],
        ['🍽️ Küche', '📅 Neu berechnen'],
        ['📤 Zeiten senden'],
        ['⬅️ Zurück', '❌ Abbrechen']
    ]);
}

function tgTestFensterMenu(msg) {
    tgWithKeyboard(msg || '🪟 Fenster Testfahrt', [
        ['⬆️ Auf', '⬇️ Zu'],
        ['↔️ Spalt'],
        ['⬅️ Zurück', '❌ Abbrechen']
    ]);
}

function tgTestTerrasseMenu(msg) {
    tgWithKeyboard(msg || '🏡 Terrasse Testfahrt', [
        ['⬆️ Auf', '⬇️ Zu'],
        ['↔️ Spalt'],
        ['⬅️ Zurück', '❌ Abbrechen']
    ]);
}

function tgTestKuecheMenu(msg) {
    tgWithKeyboard(msg || '🍽️ Küche Testfahrt', [
        ['⬆️ Auf', '⬇️ Zu'],
        ['↔️ Spalt'],
        ['⬅️ Zurück', '❌ Abbrechen']
    ]);
}

function tgCancelMenu(msg) {
    telegramMenuState = null;
    telegramEditTarget = null;
    tgMainMenu(msg || '❌ Abgebrochen');
}

function push(msg, title) {
    if (!PUSH_INSTANCE) return;
    try {
        sendTo(PUSH_INSTANCE, 'send', {
            message: msg,
            title: title || 'Rolladen'
        });
    } catch (e) {
        log('Push Fehler: ' + e, 'warn');
    }
}

function notifyAction(title, msg) {
    log(msg, 'info');
    tg(msg);
    push(msg, title);
}

function formatZeit(d) {
    return d ? d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' }) : '--:--';
}

function anzeigeProzent(val) {
    return 100 - Number(val);
}

function posText(val) {
    return anzeigeProzent(val) + '%';
}

function feiertag() {
    try {
        return !!getState(FEIERTAG).val;
    } catch (e) {
        return false;
    }
}

function urlaubsmodus() {
    try {
        return !!getState(URLAUBSMODUS).val;
    } catch (e) {
        return false;
    }
}

function wochenendeOderFeiertag() {
    const d = new Date().getDay();
    return d === 0 || d === 6 || feiertag();
}

function gleicheMinute(a, b) {
    return a &amp;&amp; b &amp;&amp;
        a.getHours() === b.getHours() &amp;&amp;
        a.getMinutes() === b.getMinutes() &amp;&amp;
        a.getDate() === b.getDate() &amp;&amp;
        a.getMonth() === b.getMonth() &amp;&amp;
        a.getFullYear() === b.getFullYear();
}

function getDateKey(d) {
    return d.getFullYear() + '-' +
        String(d.getMonth() + 1).padStart(2, '0') + '-' +
        String(d.getDate()).padStart(2, '0');
}

function terrassenTuerOffenOderGekippt() {
    return !!getState(TERRASSEN_TUER_AUF).val || !!getState(TERRASSEN_TUER_KIPP).val;
}

function randomInt(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

function addMinutes(date, minutes) {
    return new Date(date.getTime() + minutes * 60000);
}

function clampDate(date, min, max) {
    if (date &lt; min) return new Date(min);
    if (date &gt; max) return new Date(max);
    return new Date(date);
}

function normalizeWindow(start, end) {
    if (start &gt; end) {
        return {
            start: new Date(end),
            end: new Date(end)
        };
    }
    return {
        start: new Date(start),
        end: new Date(end)
    };
}

function safeRandomMinuteBetween(start, end) {
    const win = normalizeWindow(start, end);
    const s = Math.floor(win.start.getTime() / 60000);
    const e = Math.floor(win.end.getTime() / 60000);

    if (e &lt;= s) return new Date(win.end);

    const rnd = Math.floor(Math.random() * (e - s + 1)) + s;
    return new Date(rnd * 60000);
}

function ensureState(id, def, common) {
    if (!existsState(id)) {
        createState(id, def, common);
    }
}

function ensureConfigTimeState(id, def, name) {
    ensureState(id, def, {
        name: name,
        type: 'string',
        role: 'text',
        read: true,
        write: true,
        def: def
    });
}

function parseTimeString(str, fallback) {
    const val = (str || fallback || '').toString().trim();
    const m = /^(\d{1,2}):(\d{2})$/.exec(val);

    if (!m) {
        const fb = /^(\d{1,2}):(\d{2})$/.exec((fallback || '').toString().trim());
        if (!fb) return { h: 0, m: 0 };
        return {
            h: Math.min(23, Math.max(0, Number(fb[1]))),
            m: Math.min(59, Math.max(0, Number(fb[2])))
        };
    }

    return {
        h: Math.min(23, Math.max(0, Number(m[1]))),
        m: Math.min(59, Math.max(0, Number(m[2])))
    };
}

function setZeit(date, zeitObj) {
    const d = new Date(date);
    d.setHours(zeitObj.h, zeitObj.m, 0, 0);
    return d;
}

function getConfigTime(path, fallback) {
    try {
        if (!existsState(path)) return parseTimeString(fallback, fallback);
        const s = getState(path);
        if (!s) return parseTimeString(fallback, fallback);
        return parseTimeString(s.val, fallback);
    } catch (e) {
        return parseTimeString(fallback, fallback);
    }
}

function getConfigTimeString(path, fallback) {
    try {
        if (!existsState(path)) {
            const p = parseTimeString(fallback, fallback);
            return String(p.h).padStart(2, '0') + ':' + String(p.m).padStart(2, '0');
        }

        const s = getState(path);
        if (!s) {
            const p = parseTimeString(fallback, fallback);
            return String(p.h).padStart(2, '0') + ':' + String(p.m).padStart(2, '0');
        }

        const v = s.val;
        if (typeof v === 'string' &amp;&amp; /^\d{1,2}:\d{2}$/.test(v.trim())) {
            const p = parseTimeString(v.trim(), fallback);
            return String(p.h).padStart(2, '0') + ':' + String(p.m).padStart(2, '0');
        }
    } catch (e) {
        // ignore
    }

    const p = parseTimeString(fallback, fallback);
    return String(p.h).padStart(2, '0') + ':' + String(p.m).padStart(2, '0');
}

function getWindowTimes(group, area, isFreeDay) {
    const cfgGroup = group === 'morgens' ? 'Morgens' : 'Abends';
    const cfgArea = area === 'fensterTerrasse' ? 'FensterTerrasse' : 'Kueche';
    const cfgDay = isFreeDay ? 'Frei' : 'Werktag';

    const dayKey = isFreeDay ? 'frei' : 'werktag';

    const startPath = `${CONFIG_STATE}.${cfgGroup}.${cfgArea}.${cfgDay}.Start`;
    const endPath   = `${CONFIG_STATE}.${cfgGroup}.${cfgArea}.${cfgDay}.Ende`;

    const fallbackStart = DEFAULT_TIMES[group][area][dayKey].start;
    const fallbackEnd   = DEFAULT_TIMES[group][area][dayKey].ende;

    return {
        start: getConfigTime(startPath, fallbackStart),
        ende:  getConfigTime(endPath, fallbackEnd)
    };
}

function getInfoTime(which) {
    const map = {
        ersteMeldung: `${CONFIG_STATE}.Info.ErsteMeldung`,
        zweiteMeldung: `${CONFIG_STATE}.Info.ZweiteMeldung`
    };
    return getConfigTimeString(map[which], DEFAULT_TIMES.info[which]);
}

function isValidTimeText(txt) {
    return /^\d{1,2}:\d{2}$/.test((txt || '').trim());
}

function normalizeTimeText(txt) {
    const p = parseTimeString(txt, '00:00');
    return String(p.h).padStart(2, '0') + ':' + String(p.m).padStart(2, '0');
}

function setConfigTimeState(path, value) {
    const normalized = normalizeTimeText(value);
    setState(path, normalized);
    return normalized;
}

function getTodayOverviewText() {
    return (
        '📅 Heutige Zufallszeiten\n\n' +
        '🌅 Morgen:\n' +
        '- Fenster: ' + formatZeit(randomMorningFenster) + '\n' +
        '- Terrasse: ' + formatZeit(randomMorningTerrasse) + '\n' +
        '- Küche: ' + formatZeit(randomMorningKueche) + '\n\n' +
        '🌙 Abend:\n' +
        '- Küche: ' + formatZeit(randomEveningKueche) + '\n' +
        '- Fenster: ' + formatZeit(randomEveningFenster) + '\n' +
        '- Terrasse: ' + formatZeit(randomEveningTerrasse) +
        (urlaubsmodus() ? '\n\n🏖️ Urlaubsmodus aktiv' : '')
    );
}

function getConfigOverviewText() {
    return (
        '🕒 Aktuelle Rolladen-Zeiten\n\n' +

        '🌅 Morgens Werktag\n' +
        '- Fenster/Terrasse Start: ' + getConfigTimeString(`${CONFIG_STATE}.Morgens.FensterTerrasse.Werktag.Start`, DEFAULT_TIMES.morgens.fensterTerrasse.werktag.start) + '\n' +
        '- Fenster/Terrasse Ende: ' + getConfigTimeString(`${CONFIG_STATE}.Morgens.FensterTerrasse.Werktag.Ende`, DEFAULT_TIMES.morgens.fensterTerrasse.werktag.ende) + '\n' +
        '- Küche Start: ' + getConfigTimeString(`${CONFIG_STATE}.Morgens.Kueche.Werktag.Start`, DEFAULT_TIMES.morgens.kueche.werktag.start) + '\n' +
        '- Küche Ende: ' + getConfigTimeString(`${CONFIG_STATE}.Morgens.Kueche.Werktag.Ende`, DEFAULT_TIMES.morgens.kueche.werktag.ende) + '\n\n' +

        '🌅 Morgens Frei\n' +
        '- Fenster/Terrasse Start: ' + getConfigTimeString(`${CONFIG_STATE}.Morgens.FensterTerrasse.Frei.Start`, DEFAULT_TIMES.morgens.fensterTerrasse.frei.start) + '\n' +
        '- Fenster/Terrasse Ende: ' + getConfigTimeString(`${CONFIG_STATE}.Morgens.FensterTerrasse.Frei.Ende`, DEFAULT_TIMES.morgens.fensterTerrasse.frei.ende) + '\n' +
        '- Küche Start: ' + getConfigTimeString(`${CONFIG_STATE}.Morgens.Kueche.Frei.Start`, DEFAULT_TIMES.morgens.kueche.frei.start) + '\n' +
        '- Küche Ende: ' + getConfigTimeString(`${CONFIG_STATE}.Morgens.Kueche.Frei.Ende`, DEFAULT_TIMES.morgens.kueche.frei.ende) + '\n\n' +

        '🌙 Abends Werktag\n' +
        '- Fenster/Terrasse Start: ' + getConfigTimeString(`${CONFIG_STATE}.Abends.FensterTerrasse.Werktag.Start`, DEFAULT_TIMES.abends.fensterTerrasse.werktag.start) + '\n' +
        '- Fenster/Terrasse Ende: ' + getConfigTimeString(`${CONFIG_STATE}.Abends.FensterTerrasse.Werktag.Ende`, DEFAULT_TIMES.abends.fensterTerrasse.werktag.ende) + '\n' +
        '- Küche Start: ' + getConfigTimeString(`${CONFIG_STATE}.Abends.Kueche.Werktag.Start`, DEFAULT_TIMES.abends.kueche.werktag.start) + '\n' +
        '- Küche Ende: ' + getConfigTimeString(`${CONFIG_STATE}.Abends.Kueche.Werktag.Ende`, DEFAULT_TIMES.abends.kueche.werktag.ende) + '\n\n' +

        '🌙 Abends Frei\n' +
        '- Fenster/Terrasse Start: ' + getConfigTimeString(`${CONFIG_STATE}.Abends.FensterTerrasse.Frei.Start`, DEFAULT_TIMES.abends.fensterTerrasse.frei.start) + '\n' +
        '- Fenster/Terrasse Ende: ' + getConfigTimeString(`${CONFIG_STATE}.Abends.FensterTerrasse.Frei.Ende`, DEFAULT_TIMES.abends.fensterTerrasse.frei.ende) + '\n' +
        '- Küche Start: ' + getConfigTimeString(`${CONFIG_STATE}.Abends.Kueche.Frei.Start`, DEFAULT_TIMES.abends.kueche.frei.start) + '\n' +
        '- Küche Ende: ' + getConfigTimeString(`${CONFIG_STATE}.Abends.Kueche.Frei.Ende`, DEFAULT_TIMES.abends.kueche.frei.ende) + '\n\n' +

        'ℹ️ Info\n' +
        '- 1. Meldung: ' + getConfigTimeString(`${CONFIG_STATE}.Info.ErsteMeldung`, DEFAULT_TIMES.info.ersteMeldung) + '\n' +
        '- 2. Meldung: ' + getConfigTimeString(`${CONFIG_STATE}.Info.ZweiteMeldung`, DEFAULT_TIMES.info.zweiteMeldung)
    );
}

function resolveTelegramTimePath(tokens) {
    if (!tokens || !tokens.length) return null;

    const scope = (tokens[0] || '').toLowerCase();

    if (scope === 'info') {
        const which = (tokens[1] || '').toLowerCase();
        if (which === '1' || which === 'erste') return `${CONFIG_STATE}.Info.ErsteMeldung`;
        if (which === '2' || which === 'zweite') return `${CONFIG_STATE}.Info.ZweiteMeldung`;
        return null;
    }

    const tageszeit = scope;
    const bereich = (tokens[1] || '').toLowerCase();
    const tagtyp = (tokens[2] || '').toLowerCase();
    const grenze = (tokens[3] || '').toLowerCase();

    let part1 = null;
    let part2 = null;
    let part3 = null;
    let part4 = null;

    if (tageszeit === 'morgens') part1 = 'Morgens';
    if (tageszeit === 'abends')  part1 = 'Abends';

    if (bereich === 'ft' || bereich === 'fensterterrasse' || bereich === 'fenster/terrasse') part2 = 'FensterTerrasse';
    if (bereich === 'kueche' || bereich === 'küche') part2 = 'Kueche';

    if (tagtyp === 'werktag') part3 = 'Werktag';
    if (tagtyp === 'frei')    part3 = 'Frei';

    if (grenze === 'start') part4 = 'Start';
    if (grenze === 'ende')  part4 = 'Ende';

    if (!part1 || !part2 || !part3 || !part4) return null;

    return `${CONFIG_STATE}.${part1}.${part2}.${part3}.${part4}`;
}

function getMenuTargetPath(context) {
    if (!context) return null;

    if (context.mode === 'info') {
        if (context.which === 'erste') return `${CONFIG_STATE}.Info.ErsteMeldung`;
        if (context.which === 'zweite') return `${CONFIG_STATE}.Info.ZweiteMeldung`;
        return null;
    }

    const part1 = context.tageszeit === 'morgens' ? 'Morgens' : context.tageszeit === 'abends' ? 'Abends' : null;
    const part2 = context.bereich === 'ft' ? 'FensterTerrasse' : context.bereich === 'kueche' ? 'Kueche' : null;
    const part3 = context.tagtyp === 'werktag' ? 'Werktag' : context.tagtyp === 'frei' ? 'Frei' : null;
    const part4 = context.grenze === 'start' ? 'Start' : context.grenze === 'ende' ? 'Ende' : null;

    if (!part1 || !part2 || !part3 || !part4) return null;
    return `${CONFIG_STATE}.${part1}.${part2}.${part3}.${part4}`;
}

function getMenuTargetLabel(context) {
    if (!context) return '';

    if (context.mode === 'info') {
        return context.which === 'erste' ? 'Info / Erste Meldung' : 'Info / Zweite Meldung';
    }

    return (
        (context.tageszeit === 'morgens' ? 'Morgens' : 'Abends') + ' / ' +
        (context.bereich === 'ft' ? 'Fenster/Terrasse' : 'Küche') + ' / ' +
        (context.tagtyp === 'werktag' ? 'Werktag' : 'Frei') + ' / ' +
        (context.grenze === 'start' ? 'Start' : 'Ende')
    );
}

function extractTelegramText(val) {
    if (val === null || val === undefined) return '';

    let text = '';

    if (typeof val === 'string') {
        const trimmed = val.trim();

        if (trimmed.startsWith('{') &amp;&amp; trimmed.endsWith('}')) {
            try {
                const obj = JSON.parse(trimmed);
                if (obj &amp;&amp; typeof obj.text === 'string') {
                    text = obj.text.trim();
                } else if (obj &amp;&amp; typeof obj.data === 'string') {
                    text = obj.data.trim();
                } else {
                    text = trimmed;
                }
            } catch (e) {
                text = trimmed;
            }
        } else {
            text = trimmed;
        }
    } else if (typeof val === 'object') {
        if (typeof val.text === 'string') {
            text = val.text.trim();
        } else if (typeof val.data === 'string') {
            text = val.data.trim();
        } else {
            text = String(val).trim();
        }
    } else {
        text = String(val).trim();
    }

    text = text.replace(/^\[[^\]]+\]\s*/, '').trim();

    return text;
}

function telegramSavedMessage(label, saved) {
    return (
        '✅ Gespeichert:\n' +
        label + ' = ' + saved + '\n\n' +
        getConfigOverviewText()
    );
}

/* =========================
   States anlegen
   ========================= */

function ensureStates() {
    ensureState(URLAUBSMODUS, false, {
        name: 'Urlaubsmodus Rolladen',
        type: 'boolean',
        role: 'switch',
        read: true,
        write: true,
        def: false
    });

    ensureState(BASE_STATE + '.Heute.Morgen.Fenster', '--:--', {
        name: 'Heute Morgen Fenster',
        type: 'string',
        role: 'text',
        read: true,
        write: true,
        def: '--:--'
    });

    ensureState(BASE_STATE + '.Heute.Morgen.Terrasse', '--:--', {
        name: 'Heute Morgen Terrasse',
        type: 'string',
        role: 'text',
        read: true,
        write: true,
        def: '--:--'
    });

    ensureState(BASE_STATE + '.Heute.Morgen.Kueche', '--:--', {
        name: 'Heute Morgen Küche',
        type: 'string',
        role: 'text',
        read: true,
        write: true,
        def: '--:--'
    });

    ensureState(BASE_STATE + '.Heute.Abend.Fenster', '--:--', {
        name: 'Heute Abend Fenster',
        type: 'string',
        role: 'text',
        read: true,
        write: true,
        def: '--:--'
    });

    ensureState(BASE_STATE + '.Heute.Abend.Terrasse', '--:--', {
        name: 'Heute Abend Terrasse',
        type: 'string',
        role: 'text',
        read: true,
        write: true,
        def: '--:--'
    });

    ensureState(BASE_STATE + '.Heute.Abend.Kueche', '--:--', {
        name: 'Heute Abend Küche',
        type: 'string',
        role: 'text',
        read: true,
        write: true,
        def: '--:--'
    });

    ensureState(BASE_STATE + '.Heute.Info', '', {
        name: 'Heute Info',
        type: 'string',
        role: 'text',
        read: true,
        write: true,
        def: ''
    });

    ensureConfigTimeState(`${CONFIG_STATE}.Morgens.FensterTerrasse.Werktag.Start`, DEFAULT_TIMES.morgens.fensterTerrasse.werktag.start, 'Morgens Fenster/Terrasse Werktag Start');
    ensureConfigTimeState(`${CONFIG_STATE}.Morgens.FensterTerrasse.Werktag.Ende`, DEFAULT_TIMES.morgens.fensterTerrasse.werktag.ende, 'Morgens Fenster/Terrasse Werktag Ende');
    ensureConfigTimeState(`${CONFIG_STATE}.Morgens.FensterTerrasse.Frei.Start`, DEFAULT_TIMES.morgens.fensterTerrasse.frei.start, 'Morgens Fenster/Terrasse Frei Start');
    ensureConfigTimeState(`${CONFIG_STATE}.Morgens.FensterTerrasse.Frei.Ende`, DEFAULT_TIMES.morgens.fensterTerrasse.frei.ende, 'Morgens Fenster/Terrasse Frei Ende');

    ensureConfigTimeState(`${CONFIG_STATE}.Morgens.Kueche.Werktag.Start`, DEFAULT_TIMES.morgens.kueche.werktag.start, 'Morgens Küche Werktag Start');
    ensureConfigTimeState(`${CONFIG_STATE}.Morgens.Kueche.Werktag.Ende`, DEFAULT_TIMES.morgens.kueche.werktag.ende, 'Morgens Küche Werktag Ende');
    ensureConfigTimeState(`${CONFIG_STATE}.Morgens.Kueche.Frei.Start`, DEFAULT_TIMES.morgens.kueche.frei.start, 'Morgens Küche Frei Start');
    ensureConfigTimeState(`${CONFIG_STATE}.Morgens.Kueche.Frei.Ende`, DEFAULT_TIMES.morgens.kueche.frei.ende, 'Morgens Küche Frei Ende');

    ensureConfigTimeState(`${CONFIG_STATE}.Abends.FensterTerrasse.Werktag.Start`, DEFAULT_TIMES.abends.fensterTerrasse.werktag.start, 'Abends Fenster/Terrasse Werktag Start');
    ensureConfigTimeState(`${CONFIG_STATE}.Abends.FensterTerrasse.Werktag.Ende`, DEFAULT_TIMES.abends.fensterTerrasse.werktag.ende, 'Abends Fenster/Terrasse Werktag Ende');
    ensureConfigTimeState(`${CONFIG_STATE}.Abends.FensterTerrasse.Frei.Start`, DEFAULT_TIMES.abends.fensterTerrasse.frei.start, 'Abends Fenster/Terrasse Frei Start');
    ensureConfigTimeState(`${CONFIG_STATE}.Abends.FensterTerrasse.Frei.Ende`, DEFAULT_TIMES.abends.fensterTerrasse.frei.ende, 'Abends Fenster/Terrasse Frei Ende');

    ensureConfigTimeState(`${CONFIG_STATE}.Abends.Kueche.Werktag.Start`, DEFAULT_TIMES.abends.kueche.werktag.start, 'Abends Küche Werktag Start');
    ensureConfigTimeState(`${CONFIG_STATE}.Abends.Kueche.Werktag.Ende`, DEFAULT_TIMES.abends.kueche.werktag.ende, 'Abends Küche Werktag Ende');
    ensureConfigTimeState(`${CONFIG_STATE}.Abends.Kueche.Frei.Start`, DEFAULT_TIMES.abends.kueche.frei.start, 'Abends Küche Frei Start');
    ensureConfigTimeState(`${CONFIG_STATE}.Abends.Kueche.Frei.Ende`, DEFAULT_TIMES.abends.kueche.frei.ende, 'Abends Küche Frei Ende');

    ensureConfigTimeState(`${CONFIG_STATE}.Info.ErsteMeldung`, DEFAULT_TIMES.info.ersteMeldung, 'Info erste Meldung');
    ensureConfigTimeState(`${CONFIG_STATE}.Info.ZweiteMeldung`, DEFAULT_TIMES.info.zweiteMeldung, 'Info zweite Meldung');
}

function markAutoMove(name, id, value) {
    if (name === 'Terrasse') autoMoveTerrasse = true;
    if (name === 'Fenster')  autoMoveFenster  = true;
    if (name === 'Kueche')   autoMoveKueche   = true;

    setState(id, value);

    setTimeout(() =&gt; {
        if (name === 'Terrasse') autoMoveTerrasse = false;
        if (name === 'Fenster')  autoMoveFenster  = false;
        if (name === 'Kueche')   autoMoveKueche   = false;
    }, 5000);
}

/* =========================
   Zeitfenster aus Config
   ========================= */

function getFensterTerrasseMorningWindow(now) {
    const cfg = getWindowTimes('morgens', 'fensterTerrasse', wochenendeOderFeiertag());
    return { frueh: setZeit(now, cfg.start), spaet: setZeit(now, cfg.ende) };
}

function getFensterTerrasseEveningWindow(now) {
    const cfg = getWindowTimes('abends', 'fensterTerrasse', wochenendeOderFeiertag());
    return { frueh: setZeit(now, cfg.start), spaet: setZeit(now, cfg.ende) };
}

function getKuecheMorningWindow(now) {
    const cfg = getWindowTimes('morgens', 'kueche', wochenendeOderFeiertag());
    return { frueh: setZeit(now, cfg.start), spaet: setZeit(now, cfg.ende) };
}

function getKuecheEveningWindow(now) {
    const cfg = getWindowTimes('abends', 'kueche', wochenendeOderFeiertag());
    return { frueh: setZeit(now, cfg.start), spaet: setZeit(now, cfg.ende) };
}

function sendeTageszeiten() {
    tg(getTodayOverviewText());
}

function sendeMittagsInfo() {
    const now = new Date();
    const key = getDateKey(now) + '_' + now.getHours();

    if (lastInfoSent[key]) return;

    berechneRandomZeiten();

    tg(
        '📅 Rolladen-Zeiten heute\n\n' +
        '🌅 Morgens öffnen:\n' +
        '- Fenster: ' + formatZeit(randomMorningFenster) + '\n' +
        '- Terrasse: ' + formatZeit(randomMorningTerrasse) + '\n' +
        '- Küche: ' + formatZeit(randomMorningKueche) + '\n\n' +
        '🌙 Abends schließen:\n' +
        '- Küche: ' + formatZeit(randomEveningKueche) + '\n' +
        '- Fenster: ' + formatZeit(randomEveningFenster) + '\n' +
        '- Terrasse: ' + formatZeit(randomEveningTerrasse) +
        (urlaubsmodus() ? '\n\n🏖️ Urlaubsmodus aktiv' : '')
    );

    lastInfoSent[key] = true;
}

function updateVisStates() {
    setState(BASE_STATE + '.Heute.Morgen.Fenster', formatZeit(randomMorningFenster), true);
    setState(BASE_STATE + '.Heute.Morgen.Terrasse', formatZeit(randomMorningTerrasse), true);
    setState(BASE_STATE + '.Heute.Morgen.Kueche', formatZeit(randomMorningKueche), true);

    setState(BASE_STATE + '.Heute.Abend.Fenster', formatZeit(randomEveningFenster), true);
    setState(BASE_STATE + '.Heute.Abend.Terrasse', formatZeit(randomEveningTerrasse), true);
    setState(BASE_STATE + '.Heute.Abend.Kueche', formatZeit(randomEveningKueche), true);

    const info =
        'Morgen: Fenster ' + formatZeit(randomMorningFenster) +
        ', Terrasse ' + formatZeit(randomMorningTerrasse) +
        ', Küche ' + formatZeit(randomMorningKueche) +
        ' | Abend: Küche ' + formatZeit(randomEveningKueche) +
        ', Fenster ' + formatZeit(randomEveningFenster) +
        ', Terrasse ' + formatZeit(randomEveningTerrasse) +
        (urlaubsmodus() ? ' | Urlaubsmodus aktiv' : '');

    setState(BASE_STATE + '.Heute.Info', info, true);
}

/* =========================
   Zufallszeiten berechnen
   ========================= */

function berechneRandomZeiten() {
    const jetzt = new Date();
    const key = getDateKey(jetzt);

    if (randomDateKey === key) return;

    const dawn = getAstroDate('dawn', jetzt);
    const dusk = getAstroDate('dusk', jetzt);

    const ftMorningWindow = getFensterTerrasseMorningWindow(jetzt);
    const ftEveningWindow = getFensterTerrasseEveningWindow(jetzt);
    const kMorningWindow  = getKuecheMorningWindow(jetzt);
    const kEveningWindow  = getKuecheEveningWindow(jetzt);

    const ftMorningStart = clampDate(
        dawn &gt; ftMorningWindow.frueh ? dawn : ftMorningWindow.frueh,
        ftMorningWindow.frueh,
        ftMorningWindow.spaet
    );
    const ftMorningEnd = new Date(ftMorningWindow.spaet);

    const ftEveningStart = clampDate(
        dusk &gt; ftEveningWindow.frueh ? dusk : ftEveningWindow.frueh,
        ftEveningWindow.frueh,
        ftEveningWindow.spaet
    );
    const ftEveningEnd = new Date(ftEveningWindow.spaet);

    const kMorningStart = clampDate(
        dawn &gt; kMorningWindow.frueh ? dawn : kMorningWindow.frueh,
        kMorningWindow.frueh,
        kMorningWindow.spaet
    );
    const kMorningEnd = new Date(kMorningWindow.spaet);

    const kEveningStart = clampDate(
        dusk &gt; kEveningWindow.frueh ? dusk : kEveningWindow.frueh,
        kEveningWindow.frueh,
        kEveningWindow.spaet
    );
    const kEveningEnd = new Date(kEveningWindow.spaet);

    const morgenMin = urlaubsmodus() ? URLAUB_MORGEN_MIN : NORMAL_MORGEN_MIN;
    const morgenMax = urlaubsmodus() ? URLAUB_MORGEN_MAX : NORMAL_MORGEN_MAX;
    const abendMin  = urlaubsmodus() ? URLAUB_ABEND_MIN  : NORMAL_ABEND_MIN;
    const abendMax  = urlaubsmodus() ? URLAUB_ABEND_MAX  : NORMAL_ABEND_MAX;

    randomMorningFenster = safeRandomMinuteBetween(ftMorningStart, ftMorningEnd);

    randomMorningTerrasse = addMinutes(randomMorningFenster, randomInt(morgenMin, morgenMax));
    randomMorningTerrasse = clampDate(randomMorningTerrasse, ftMorningStart, ftMorningEnd);

    if (gleicheMinute(randomMorningFenster, randomMorningTerrasse) &amp;&amp; randomMorningTerrasse &lt; ftMorningEnd) {
        randomMorningTerrasse = addMinutes(randomMorningTerrasse, 1);
        randomMorningTerrasse = clampDate(randomMorningTerrasse, ftMorningStart, ftMorningEnd);
    }

    randomMorningKueche = safeRandomMinuteBetween(kMorningStart, kMorningEnd);

    if (gleicheMinute(randomMorningKueche, randomMorningFenster) &amp;&amp; randomMorningKueche &lt; kMorningEnd) {
        randomMorningKueche = addMinutes(randomMorningKueche, 1);
        randomMorningKueche = clampDate(randomMorningKueche, kMorningStart, kMorningEnd);
    }
    if (gleicheMinute(randomMorningKueche, randomMorningTerrasse) &amp;&amp; randomMorningKueche &lt; kMorningEnd) {
        randomMorningKueche = addMinutes(randomMorningKueche, 1);
        randomMorningKueche = clampDate(randomMorningKueche, kMorningStart, kMorningEnd);
    }

    randomEveningKueche = safeRandomMinuteBetween(kEveningStart, kEveningEnd);

    let fensterStart = addMinutes(randomEveningKueche, randomInt(abendMin, abendMax));
    fensterStart = clampDate(fensterStart, ftEveningStart, ftEveningEnd);
    randomEveningFenster = safeRandomMinuteBetween(fensterStart, ftEveningEnd);

    let terrasseStart = addMinutes(randomEveningFenster, randomInt(abendMin, abendMax));
    terrasseStart = clampDate(terrasseStart, ftEveningStart, ftEveningEnd);
    randomEveningTerrasse = safeRandomMinuteBetween(terrasseStart, ftEveningEnd);

    if (gleicheMinute(randomEveningKueche, randomEveningFenster) &amp;&amp; randomEveningFenster &lt; ftEveningEnd) {
        randomEveningFenster = addMinutes(randomEveningFenster, 1);
        randomEveningFenster = clampDate(randomEveningFenster, ftEveningStart, ftEveningEnd);
    }
    if (gleicheMinute(randomEveningFenster, randomEveningTerrasse) &amp;&amp; randomEveningTerrasse &lt; ftEveningEnd) {
        randomEveningTerrasse = addMinutes(randomEveningTerrasse, 1);
        randomEveningTerrasse = clampDate(randomEveningTerrasse, ftEveningStart, ftEveningEnd);
    }
    if (gleicheMinute(randomEveningKueche, randomEveningTerrasse) &amp;&amp; randomEveningTerrasse &lt; ftEveningEnd) {
        randomEveningTerrasse = addMinutes(randomEveningTerrasse, 1);
        randomEveningTerrasse = clampDate(randomEveningTerrasse, ftEveningStart, ftEveningEnd);
    }

    randomDateKey = key;
    updateVisStates();

    log(
        'Zufallszeiten heute: ' +
        'Morgen Fenster ' + formatZeit(randomMorningFenster) +
        ', Terrasse ' + formatZeit(randomMorningTerrasse) +
        ', Küche ' + formatZeit(randomMorningKueche) +
        ' | Abend Küche ' + formatZeit(randomEveningKueche) +
        ', Fenster ' + formatZeit(randomEveningFenster) +
        ', Terrasse ' + formatZeit(randomEveningTerrasse) +
        (urlaubsmodus() ? ' [Urlaubsmodus]' : ''),
        'info'
    );
}

/* =========================
   Sensorlogik
   ========================= */

function pruefeKuecheFenster() {
    if (!!getState(KUECHE_FENSTER).val &amp;&amp; Number(getState(KUECHE).val) === ZU) {
        markAutoMove('Kueche', KUECHE, KUECHE_SPALT);
        notifyAction('Küche', 'Küche: Fenster offen → Rolladen auf ' + posText(KUECHE_SPALT));
    }
}

function pruefeTerrasseSpalt() {
    if (terrassenTuerOffenOderGekippt() &amp;&amp; Number(getState(TERRASSE).val) === ZU) {
        markAutoMove('Terrasse', TERRASSE, TERRASSE_SPALT);
        notifyAction('Terrasse', 'Terrasse: Tür offen/gekippt → Rolladen auf ' + posText(TERRASSE_SPALT));
    }
}

function resetTagesflags() {
    terrassePending = false;

    morgenHeuteFenster = false;
    morgenHeuteTerrasse = false;
    morgenHeuteKueche = false;

    abendHeuteFenster = false;
    abendHeuteTerrasse = false;
    abendHeuteKueche = false;

    randomMorningFenster = null;
    randomMorningTerrasse = null;
    randomMorningKueche = null;
    randomEveningFenster = null;
    randomEveningTerrasse = null;
    randomEveningKueche = null;
    randomDateKey = '';
    lastInfoSent = {};
}

/* =========================
   Zeitpläne
   ========================= */

schedule('0 1 0 * * *', () =&gt; {
    resetTagesflags();
});

schedule('* * * * *', () =&gt; {
    const now = new Date();
    const hhmm = String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0');

    if (hhmm === getInfoTime('ersteMeldung')) sendeMittagsInfo();
    if (hhmm === getInfoTime('zweiteMeldung')) sendeMittagsInfo();
});

schedule('0 * 0-23 * * *', () =&gt; {
    berechneRandomZeiten();
    const jetzt = new Date();

    if (randomMorningFenster &amp;&amp; gleicheMinute(jetzt, randomMorningFenster) &amp;&amp; !morgenHeuteFenster) {
        markAutoMove('Fenster', FENSTER, AUF);
        morgenHeuteFenster = true;
        notifyAction('Fenster', (urlaubsmodus() ? '🏖️ ' : '🌅 ') + 'Fenster morgens auf ' + posText(AUF) + ' um ' + formatZeit(randomMorningFenster));
    }

    if (randomMorningTerrasse &amp;&amp; gleicheMinute(jetzt, randomMorningTerrasse) &amp;&amp; !morgenHeuteTerrasse) {
        markAutoMove('Terrasse', TERRASSE, AUF);
        morgenHeuteTerrasse = true;
        notifyAction('Terrasse', (urlaubsmodus() ? '🏖️ ' : '🌅 ') + 'Terrasse morgens auf ' + posText(AUF) + ' um ' + formatZeit(randomMorningTerrasse));
    }

    if (randomMorningKueche &amp;&amp; gleicheMinute(jetzt, randomMorningKueche) &amp;&amp; !morgenHeuteKueche) {
        markAutoMove('Kueche', KUECHE, AUF);
        morgenHeuteKueche = true;
        notifyAction('Küche', (urlaubsmodus() ? '🏖️ ' : '🌅 ') + 'Küche morgens auf ' + posText(AUF) + ' um ' + formatZeit(randomMorningKueche));
    }

    if (randomEveningKueche &amp;&amp; gleicheMinute(jetzt, randomEveningKueche) &amp;&amp; !abendHeuteKueche) {
        if (!!getState(KUECHE_FENSTER).val) {
            markAutoMove('Kueche', KUECHE, KUECHE_SPALT);
            notifyAction('Küche', (urlaubsmodus() ? '🏖️ ' : '🌙 ') + 'Küche abends auf ' + posText(KUECHE_SPALT) + ' um ' + formatZeit(randomEveningKueche) + ' (Fenster offen)');
        } else {
            markAutoMove('Kueche', KUECHE, ZU);
            notifyAction('Küche', (urlaubsmodus() ? '🏖️ ' : '🌙 ') + 'Küche abends auf ' + posText(ZU) + ' um ' + formatZeit(randomEveningKueche));
        }
        abendHeuteKueche = true;
        pruefeKuecheFenster();
    }

    if (randomEveningFenster &amp;&amp; gleicheMinute(jetzt, randomEveningFenster) &amp;&amp; !abendHeuteFenster) {
        markAutoMove('Fenster', FENSTER, ZU);
        abendHeuteFenster = true;
        notifyAction('Fenster', (urlaubsmodus() ? '🏖️ ' : '🌙 ') + 'Fenster abends auf ' + posText(ZU) + ' um ' + formatZeit(randomEveningFenster));
    }

    if (randomEveningTerrasse &amp;&amp; gleicheMinute(jetzt, randomEveningTerrasse) &amp;&amp; !abendHeuteTerrasse) {
        if (!terrassenTuerOffenOderGekippt()) {
            markAutoMove('Terrasse', TERRASSE, ZU);
            terrassePending = false;
            pruefeTerrasseSpalt();
            notifyAction('Terrasse', (urlaubsmodus() ? '🏖️ ' : '🌙 ') + 'Terrasse abends auf ' + posText(ZU) + ' um ' + formatZeit(randomEveningTerrasse));
        } else {
            terrassePending = true;
            tg((urlaubsmodus() ? '🏖️ ' : '🌙 ') + 'Terrasse abends offen/gekippt um ' + formatZeit(randomEveningTerrasse) + ', bleibt vorerst auf ' + posText(AUF));
        }
        abendHeuteTerrasse = true;
    }
});

/* =========================
   Trigger
   ========================= */

on({ id: TERRASSEN_TUER_AUF, change: 'ne' }, () =&gt; {
    if (terrassenTuerOffenOderGekippt()) {
        pruefeTerrasseSpalt();
    } else if (terrassePending) {
        markAutoMove('Terrasse', TERRASSE, ZU);
        terrassePending = false;
        notifyAction('Terrasse', 'Terrasse nachträglich auf ' + posText(ZU) + ' gefahren');
    }
});

on({ id: TERRASSEN_TUER_KIPP, change: 'ne' }, () =&gt; {
    if (terrassenTuerOffenOderGekippt()) {
        pruefeTerrasseSpalt();
    } else if (terrassePending) {
        markAutoMove('Terrasse', TERRASSE, ZU);
        terrassePending = false;
        notifyAction('Terrasse', 'Terrasse nachträglich auf ' + posText(ZU) + ' gefahren');
    }
});

on({ id: KUECHE_FENSTER, change: 'ne' }, () =&gt; {
    pruefeKuecheFenster();
});

on({ id: URLAUBSMODUS, change: 'ne' }, () =&gt; {
    randomDateKey = '';
    berechneRandomZeiten();
    tg('Urlaubsmodus ist jetzt ' + (urlaubsmodus() ? 'aktiv' : 'deaktiviert'));
});

on({ id: new RegExp('^' + CONFIG_STATE.replace(/\./g, '\\.') + '\\..*'), change: 'ne' }, () =&gt; {
    randomDateKey = '';
    lastInfoSent = {};
    berechneRandomZeiten();
    tg('🛠️ Rolladen-Zeiten wurden aktualisiert.');
});

/* =========================
   Telegram-Steuerung
   ========================= */

on({ id: TELEGRAM_INSTANCE + '.communicate.request', change: 'any' }, obj =&gt; {
    try {
        if (!obj || !obj.state) return;

        const rawText = extractTelegramText(obj.state.val);
        if (!rawText) return;

        const lower = rawText.toLowerCase();

        log('Telegram empfangen: ' + rawText, 'info');

        if (lower === '❌ abbrechen') {
            tgCancelMenu();
            return;
        }

        if (lower === '⬅️ zurück') {
            if (!telegramMenuState || !telegramMenuState.step) {
                tgMainMenu('📋 Hauptmenü');
                return;
            }

            if (telegramMenuState.step === 'area') {
                telegramMenuState = null;
                tgMainMenu('📋 Hauptmenü');
                return;
            }

            if (telegramMenuState.step === 'info_select') {
                telegramMenuState = null;
                tgMainMenu('📋 Hauptmenü');
                return;
            }

            if (telegramMenuState.step === 'daytype') {
                const tz = telegramMenuState.tageszeit;
                telegramMenuState = { step: 'area', tageszeit: tz };
                if (tz === 'morgens') tgAreaMenuMorgen('🌅 Morgen: Bereich wählen');
                else tgAreaMenuAbend('🌙 Abend: Bereich wählen');
                return;
            }

            if (telegramMenuState.step === 'boundary') {
                telegramMenuState = {
                    step: 'daytype',
                    tageszeit: telegramMenuState.tageszeit,
                    bereich: telegramMenuState.bereich
                };
                tgDaytypeMenu('Bitte Tagtyp wählen');
                return;
            }

            if (telegramMenuState.step === 'input') {
                if (telegramMenuState.mode === 'info') {
                    telegramEditTarget = null;
                    telegramMenuState = { step: 'info_select' };
                    tgInfoMenu('🕒 Infozeiten wählen');
                    return;
                }

                telegramEditTarget = null;
                telegramMenuState = {
                    step: 'boundary',
                    tageszeit: telegramMenuState.tageszeit,
                    bereich: telegramMenuState.bereich,
                    tagtyp: telegramMenuState.tagtyp
                };
                tgBoundaryMenu('Bitte Start oder Ende wählen');
                return;
            }

            if (telegramMenuState.step === 'test_main') {
                telegramMenuState = null;
                tgMainMenu('📋 Hauptmenü');
                return;
            }

            if (telegramMenuState.step === 'test_fenster' || telegramMenuState.step === 'test_terrasse' || telegramMenuState.step === 'test_kueche') {
                telegramMenuState = { step: 'test_main' };
                tgTestMenu('🧪 Testfahrten wählen');
                return;
            }
        }

        if (telegramMenuState &amp;&amp; telegramMenuState.step === 'input' &amp;&amp; telegramEditTarget) {
            if (!isValidTimeText(rawText)) {
                tgWithKeyboard(
                    '❌ Ungültiges Format.\nBitte Zeit als HH:MM senden, z. B. 06:45',
                    [['⬅️ Zurück', '❌ Abbrechen']]
                );
                return;
            }

            const saved = setConfigTimeState(telegramEditTarget, rawText);

            randomDateKey = '';
            lastInfoSent = {};
            berechneRandomZeiten();

            const label = getMenuTargetLabel(telegramMenuState);

            telegramEditTarget = null;
            telegramMenuState = null;

            tgMainMenu(telegramSavedMessage(label, saved));
            return;
        }

        if (lower === '/menu' || lower === '📋 /menu') {
            telegramMenuState = null;
            telegramEditTarget = null;
            tgMainMenu('📋 Rolladen-Menü');
            return;
        }

        if (lower === '/hilfe' || lower === 'ℹ️ hilfe') {
            telegramMenuState = null;
            telegramEditTarget = null;
            tgMainMenu(
                'ℹ️ Bedienung\n\n' +
                'Morgen/Abend:\n' +
                '1. Tageszeit\n' +
                '2. Bereich\n' +
                '3. Werktag/Frei\n' +
                '4. Start/Ende\n' +
                '5. HH:MM senden\n\n' +
                'Infozeiten:\n' +
                '1. Infozeiten\n' +
                '2. Erste/Zweite Meldung\n' +
                '3. HH:MM senden\n\n' +
                'Testfahrten:\n' +
                '1. Testfahrten\n' +
                '2. Bereich wählen\n' +
                '3. Auf / Zu / Spalt'
            );
            return;
        }

        if (lower === '/zeiten' || lower === '🕒 /zeiten') {
            telegramMenuState = null;
            telegramEditTarget = null;
            tgMainMenu(getConfigOverviewText());
            return;
        }

        if (lower === '/heute' || lower === '🔄 /heute') {
            berechneRandomZeiten();
            telegramMenuState = null;
            telegramEditTarget = null;
            tgMainMenu(getTodayOverviewText());
            return;
        }

        if (lower === '/urlaub an' || lower === '🏖️ urlaub an') {
            setState(URLAUBSMODUS, true);
            telegramMenuState = null;
            telegramEditTarget = null;
            tgMainMenu('🏖️ Urlaubsmodus aktiviert');
            return;
        }

        if (lower === '/urlaub aus' || lower === '🏠 urlaub aus') {
            setState(URLAUBSMODUS, false);
            telegramMenuState = null;
            telegramEditTarget = null;
            tgMainMenu('🏠 Urlaubsmodus deaktiviert');
            return;
        }

        if (lower === '🌅 morgen') {
            telegramMenuState = { step: 'area', tageszeit: 'morgens' };
            tgAreaMenuMorgen('🌅 Morgen: Bereich wählen');
            return;
        }

        if (lower === '🌙 abend') {
            telegramMenuState = { step: 'area', tageszeit: 'abends' };
            tgAreaMenuAbend('🌙 Abend: Bereich wählen');
            return;
        }

        if (lower === '🕒 infozeiten') {
            telegramMenuState = { step: 'info_select' };
            tgInfoMenu('🕒 Infozeiten wählen');
            return;
        }

        if (lower === '🧪 testfahrten') {
            telegramMenuState = { step: 'test_main' };
            tgTestMenu('🧪 Testfahrten wählen');
            return;
        }

        if (lower === '1️⃣ erste meldung') {
            telegramMenuState = {
                step: 'input',
                mode: 'info',
                which: 'erste'
            };
            telegramEditTarget = getMenuTargetPath(telegramMenuState);
            tgWithKeyboard(
                '⏱ Bitte neue Zeit senden für:\n' + getMenuTargetLabel(telegramMenuState) + '\n\nFormat: HH:MM',
                [['⬅️ Zurück', '❌ Abbrechen']]
            );
            return;
        }

        if (lower === '2️⃣ zweite meldung') {
            telegramMenuState = {
                step: 'input',
                mode: 'info',
                which: 'zweite'
            };
            telegramEditTarget = getMenuTargetPath(telegramMenuState);
            tgWithKeyboard(
                '⏱ Bitte neue Zeit senden für:\n' + getMenuTargetLabel(telegramMenuState) + '\n\nFormat: HH:MM',
                [['⬅️ Zurück', '❌ Abbrechen']]
            );
            return;
        }

        /* ===== FIX: Fenster Testfahrt getrennt von Fenster/Terrasse Zeitmenü ===== */
        if ((lower === '🪟 fenster' || lower === 'fenster') &amp;&amp; telegramMenuState &amp;&amp; telegramMenuState.step === 'test_main') {
            telegramMenuState = { step: 'test_fenster' };
            tgTestFensterMenu('🪟 Fenster Testfahrt');
            return;
        }

        if (lower === '🪟 fenster/terrasse') {
            if (!telegramMenuState || !telegramMenuState.tageszeit) {
                tgMainMenu('Bitte zuerst Morgen oder Abend wählen');
                return;
            }

            telegramMenuState = {
                step: 'daytype',
                tageszeit: telegramMenuState.tageszeit,
                bereich: 'ft'
            };
            tgDaytypeMenu('Bitte Tagtyp wählen');
            return;
        }

        if (lower === '🏡 terrasse' &amp;&amp; telegramMenuState &amp;&amp; telegramMenuState.step === 'test_main') {
            telegramMenuState = { step: 'test_terrasse' };
            tgTestTerrasseMenu('🏡 Terrasse Testfahrt');
            return;
        }

        if (lower === '🍽️ küche' || lower === '🍽️ k\u00fcche') {
            if (telegramMenuState &amp;&amp; telegramMenuState.step === 'test_main') {
                telegramMenuState = { step: 'test_kueche' };
                tgTestKuecheMenu('🍽️ Küche Testfahrt');
                return;
            }

            if (!telegramMenuState || !telegramMenuState.tageszeit) {
                tgMainMenu('Bitte zuerst Morgen oder Abend wählen');
                return;
            }

            telegramMenuState = {
                step: 'daytype',
                tageszeit: telegramMenuState.tageszeit,
                bereich: 'kueche'
            };
            tgDaytypeMenu('Bitte Tagtyp wählen');
            return;
        }

        if (lower === '📅 neu berechnen' &amp;&amp; telegramMenuState &amp;&amp; telegramMenuState.step === 'test_main') {
            randomDateKey = '';
            lastInfoSent = {};
            berechneRandomZeiten();
            tgTestMenu('📅 Zufallszeiten neu berechnet');
            return;
        }

        if (lower === '📤 zeiten senden' &amp;&amp; telegramMenuState &amp;&amp; telegramMenuState.step === 'test_main') {
            berechneRandomZeiten();
            tgTestMenu(getTodayOverviewText());
            return;
        }

        if (telegramMenuState &amp;&amp; telegramMenuState.step === 'test_fenster') {
            if (lower === '⬆️ auf') {
                markAutoMove('Fenster', FENSTER, AUF);
                tgTestFensterMenu('🪟 Fenster auf ' + posText(AUF));
                return;
            }
            if (lower === '⬇️ zu') {
                markAutoMove('Fenster', FENSTER, ZU);
                tgTestFensterMenu('🪟 Fenster auf ' + posText(ZU));
                return;
            }
            if (lower === '↔️ spalt') {
                markAutoMove('Fenster', FENSTER, FENSTER_SPALT);
                tgTestFensterMenu('🪟 Fenster auf ' + posText(FENSTER_SPALT));
                return;
            }
        }

        if (telegramMenuState &amp;&amp; telegramMenuState.step === 'test_terrasse') {
            if (lower === '⬆️ auf') {
                markAutoMove('Terrasse', TERRASSE, AUF);
                tgTestTerrasseMenu('🏡 Terrasse auf ' + posText(AUF));
                return;
            }
            if (lower === '⬇️ zu') {
                markAutoMove('Terrasse', TERRASSE, ZU);
                tgTestTerrasseMenu('🏡 Terrasse auf ' + posText(ZU));
                return;
            }
            if (lower === '↔️ spalt') {
                markAutoMove('Terrasse', TERRASSE, TERRASSE_SPALT);
                tgTestTerrasseMenu('🏡 Terrasse auf ' + posText(TERRASSE_SPALT));
                return;
            }
        }

        if (telegramMenuState &amp;&amp; telegramMenuState.step === 'test_kueche') {
            if (lower === '⬆️ auf') {
                markAutoMove('Kueche', KUECHE, AUF);
                tgTestKuecheMenu('🍽️ Küche auf ' + posText(AUF));
                return;
            }
            if (lower === '⬇️ zu') {
                markAutoMove('Kueche', KUECHE, ZU);
                tgTestKuecheMenu('🍽️ Küche auf ' + posText(ZU));
                return;
            }
            if (lower === '↔️ spalt') {
                markAutoMove('Kueche', KUECHE, KUECHE_SPALT);
                tgTestKuecheMenu('🍽️ Küche auf ' + posText(KUECHE_SPALT));
                return;
            }
        }

        if (lower === '💼 werktag') {
            if (!telegramMenuState || !telegramMenuState.tageszeit || !telegramMenuState.bereich) {
                tgMainMenu('Bitte neu starten');
                return;
            }

            telegramMenuState = {
                step: 'boundary',
                tageszeit: telegramMenuState.tageszeit,
                bereich: telegramMenuState.bereich,
                tagtyp: 'werktag'
            };
            tgBoundaryMenu('Bitte Start oder Ende wählen');
            return;
        }

        if (lower === '🎉 frei') {
            if (!telegramMenuState || !telegramMenuState.tageszeit || !telegramMenuState.bereich) {
                tgMainMenu('Bitte neu starten');
                return;
            }

            telegramMenuState = {
                step: 'boundary',
                tageszeit: telegramMenuState.tageszeit,
                bereich: telegramMenuState.bereich,
                tagtyp: 'frei'
            };
            tgBoundaryMenu('Bitte Start oder Ende wählen');
            return;
        }

        if (lower === '▶️ start' || lower === '⏹️ ende') {
            if (!telegramMenuState || !telegramMenuState.tageszeit || !telegramMenuState.bereich || !telegramMenuState.tagtyp) {
                tgMainMenu('Bitte neu starten');
                return;
            }

            const grenze = lower === '▶️ start' ? 'start' : 'ende';

            telegramMenuState = {
                step: 'input',
                tageszeit: telegramMenuState.tageszeit,
                bereich: telegramMenuState.bereich,
                tagtyp: telegramMenuState.tagtyp,
                grenze: grenze
            };

            telegramEditTarget = getMenuTargetPath(telegramMenuState);

            if (!telegramEditTarget) {
                tgMainMenu('❌ Ziel konnte nicht ermittelt werden');
                telegramMenuState = null;
                return;
            }

            tgWithKeyboard(
                '⏱ Bitte neue Zeit senden für:\n' + getMenuTargetLabel(telegramMenuState) + '\n\nFormat: HH:MM',
                [['⬅️ Zurück', '❌ Abbrechen']]
            );
            return;
        }

        if (lower.startsWith('/zeit ')) {
            const parts = rawText.split(/\s+/);

            if ((parts[1] || '').toLowerCase() === 'info') {
                if (parts.length &lt; 4) {
                    tgMainMenu('❌ Ungültiger Befehl.\nBeispiel:\n/zeit info 1 12:00');
                    return;
                }

                const path = resolveTelegramTimePath(parts.slice(1, 3));
                const value = parts[3];

                if (!path || !isValidTimeText(value)) {
                    tgMainMenu('❌ Ungültiger Info-Befehl.\nBeispiele:\n/zeit info 1 12:00\n/zeit info 2 18:00');
                    return;
                }

                const saved = setConfigTimeState(path, value);
                randomDateKey = '';
                lastInfoSent = {};
                berechneRandomZeiten();

                tgMainMenu(telegramSavedMessage(path, saved));
                return;
            }

            if (parts.length &lt; 6) {
                tgMainMenu(
                    '❌ Ungültiger Befehl.\n\n' +
                    'Beispiele:\n' +
                    '/zeit morgens ft werktag start 06:45\n' +
                    '/zeit morgens kueche frei ende 08:30\n' +
                    '/zeit abends ft frei start 20:40'
                );
                return;
            }

            const path = resolveTelegramTimePath(parts.slice(1, 5));
            const value = parts[5];

            if (!path || !isValidTimeText(value)) {
                tgMainMenu(
                    '❌ Ungültiger Zeit-Befehl.\n\n' +
                    'Beispiele:\n' +
                    '/zeit morgens ft werktag start 06:45\n' +
                    '/zeit morgens kueche frei ende 08:30\n' +
                    '/zeit abends ft frei start 20:40'
                );
                return;
            }

            const saved = setConfigTimeState(path, value);
            randomDateKey = '';
            lastInfoSent = {};
            berechneRandomZeiten();

            tgMainMenu(telegramSavedMessage(path, saved));
            return;
        }
    } catch (e) {
        log('Telegram Zeitsteuerung Fehler: ' + e, 'warn');
        tgMainMenu('❌ Fehler beim Verarbeiten des Telegram-Befehls.');
    }
});

/* =========================
   Manuelle Bedienung melden
   ========================= */

on({ id: TERRASSE, change: 'ne' }, obj =&gt; {
    if (autoMoveTerrasse) return;
    tg('🖐️ Terrassenrolladen wurde manuell bewegt auf ' + posText(obj.state.val));
});

on({ id: FENSTER, change: 'ne' }, obj =&gt; {
    if (autoMoveFenster) return;
    tg('🖐️ Fensterrolladen wurde manuell bewegt auf ' + posText(obj.state.val));
});

on({ id: KUECHE, change: 'ne' }, obj =&gt; {
    if (autoMoveKueche) return;
    tg('🖐️ Küchenrolladen wurde manuell bewegt auf ' + posText(obj.state.val));
});

/* =========================
   Start
   ========================= */

ensureStates();
berechneRandomZeiten();
tgMainMenu('✅ Rolladen-Skript gestartet');
</code></pre>
<p dir="auto">Vielleicht ist das was für jemanden</p>
]]></description><link>https://forum.iobroker.net/topic/84340/homepilot-rolladensteuerung-per-telegram</link><generator>RSS for Node</generator><lastBuildDate>Sun, 19 Apr 2026 13:10:50 GMT</lastBuildDate><atom:link href="https://forum.iobroker.net/topic/84340.rss" rel="self" type="application/rss+xml"/><pubDate>Sat, 18 Apr 2026 18:54:52 GMT</pubDate><ttl>60</ttl></channel></rss>