Weiter zum Inhalt
  • Home
  • Aktuell
  • Tags
  • 0 Ungelesen 0
  • Kategorien
  • Unreplied
  • Beliebt
  • GitHub
  • Docu
  • Hilfe
Skins
  • Hell
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dunkel
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Standard: (Kein Skin)
  • Kein Skin
Einklappen
ioBroker Logo

Community Forum

donate donate
  1. ioBroker Community Home
  2. Deutsch
  3. Skripten / Logik
  4. JavaScript
  5. FritzDect - Extended

NEWS

  • Monatsrückblick Januar/Februar 2026 ist online!
    BluefoxB
    Bluefox
    18
    1
    751

  • Jahresrückblick 2025 – unser neuer Blogbeitrag ist online! ✨
    BluefoxB
    Bluefox
    18
    1
    6.1k

  • Neuer Blogbeitrag: Monatsrückblick - Dezember 2025 🎄
    BluefoxB
    Bluefox
    13
    1
    1.5k

FritzDect - Extended

Geplant Angeheftet Gesperrt Verschoben JavaScript
5 Beiträge 3 Kommentatoren 36 Aufrufe 3 Beobachtet
  • Älteste zuerst
  • Neuste zuerst
  • Meiste Stimmen
Antworten
  • In einem neuen Thema antworten
Anmelden zum Antworten
Dieses Thema wurde gelöscht. Nur Nutzer mit entsprechenden Rechten können es sehen.
  • Ro75R Online
    Ro75R Online
    Ro75
    schrieb zuletzt editiert von Ro75
    #1

    Einleitung

    Mir ist bewusst, dass es bereits den FritzDect Adapter gibt. Allerdings gab es bei mir immer wieder Fehlermeldungen und eine erhöhte CPU-Last auf der Fritzbox selbst. Zusätzlich war mir die Datenlieferung für FritzDect 200 zu lang.

    Wer nur FritzDect 301, HKR, FritzDect 200 oder FritzDect 440 besitzt, kann dieses Skript poblemlos einsetzen - nur diese Geräte werden unterstützt.

    Vorteile:

    • geringere Auslastung der Fritzbox
    • aktuelle Stromdaten (Dect 200), aller 18 - 22 Sekunden
    • alle Geräte werden automatisch erkannt und als Datenpunkt angelegt.
    • Flexibel

    Hinweis: Bitte nicht ungeduldig werden, das Skript braucht ein paar Sekunden bis alle Funktionen laufen.

    Wichtig: Das Skript sollte nicht parallel zum Adapter laufen!

    Voraussetzung:

    • JS-Adapter: 9.0.x
    • Admin-Adapter: 7.7.x
    • folgende NPM-Pakete müssen zusätzlich im JS-Adapter eingetragen sein: "crypto", "xml2js", "ping"

    Die Datenstruktur ist zu 99% an den Adapter angelehnt. Einige Datenpunkte liefern für die Visualisierung idealere Werte.
    Der größte Vorteil ist, das Dect 200 Geräte jetzt sehr schnell Datenliefern.

    Was muss im Skript angepasst werden?

    const fbIp = "IP-Adresse";
    const user = "Benutzer";
    const pass = "Kennwort";
    

    Passe diese 3 Zeilen an.

    Hier das Skript:

    // @ts-nocheck
    const crypto        = require("crypto");
    const xml2js        = require("xml2js");
    const ping          = require("ping");
    
    const axiosImport   = require("axios");
    const axios         = axiosImport.default || axiosImport;
    
    //Daten der Fritzbox und Benutzer
    const fbIp = "IP-Adresse";
    const user = "Benutzer";
    const pass = "Kennwort";
    const ROOT = "0_userdata.0.fritzdect.";
    
    //AB HIER NICHTS MEHR ÄNDERN!
    let ON_HANDLERS_ACTIVE = false;
    let WRITE_ENABLED = false;
    let schedulerIntervalSlow = 20;   // Initialisierungswert Pollintervall in Sekunden
    let schedulerIntervalFast = 7;    // Initialisierungswert Dectrefresh in Sekunden
    
    let lastUiCall = 0;
    let uiBackoff = 0;
    const UI_BASE_INTERVAL = 3000;   // Mindestabstand 3s
    const UI_BACKOFF_MAX = 15000;    // maximal 15s Backoff
    
    let sid = "0000000000000000";
    let allAins = [];
    let deviceMap = {};
    
    let STOPPED = false;
    let schedulerHandle = null;
    let POLLING = false;
    
    let dect200List = [];
    let dect200Index = -1;
    let fastSchedulerHandle = null;
    let pingSchedulerHandle = null;
    
    let masterId = null;
    
    const errorCounter = {};
    
    function logError10x(group, message) {
        if (!errorCounter[group]) errorCounter[group] = 0;
        errorCounter[group]++;
        if (errorCounter[group] >= 10) {
            log(`[${hole_datum()}] FRITZ!DECT ERROR (${group}): ${message}`, "error");
            errorCounter[group] = 0;
        }
    }
    
    // Hilfsfunktionen
    function logInfo(msg) {
        if (STOPPED) return;
        log(`[${hole_datum()}] FRITZ!DECT: ${msg}`, "info");
    }
    
    async function smartCreateState(id, value, options = {}) {
        if (existsState(id)) return;
        if (STOPPED) return;
        await createState(id, value, options);
    }
    
    function smartSetState(id, value) {
        if (STOPPED) return;
    
        const state = getState(id);
        if (!state) return;
    
        const old = state.val;
        const isObject = v => v !== null && typeof v === "object";
    
        if (isObject(value)) {
            const newStr = JSON.stringify(value);
            let oldStr = null;
    
            if (typeof old === "string") oldStr = old;
            else if (isObject(old)) oldStr = JSON.stringify(old);
    
            if (newStr === oldStr) return;
            return setState(id, newStr, true);
        }
    
        if (old === value) return;
        setState(id, value, true);
    }
    
    function hardSetState(id, value) {
        if (STOPPED) return;
        setState(id, value, true);
    }
    
    function sleepMs(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
    
    function hole_datum(ts = null) {
        const d = ts ? new Date(ts) : new Date();
        const t = String(d.getDate()).padStart(2, "0");
        const m = String(d.getMonth() + 1).padStart(2, "0");
        const y = d.getFullYear();
        const h = String(d.getHours()).padStart(2, "0");
        const mi = String(d.getMinutes()).padStart(2, "0");
        const s = String(d.getSeconds()).padStart(2, "0");
        return `${t}.${m}.${y} ${h}:${mi}:${s}`;
    }
    
    function fritzboxAlive() {
        return getState(`${ROOT}ping.alive`)?.val === true;
    }
    
    function extractAin(id) {
        return id.split(".").slice(-2, -1)[0].replace("DECT_", "");
    }
    
    async function isApiLocked(ain) {
        const s = await getStateAsync(`${ROOT}DECT_${ain}.lock`);
        return s?.val === true;
    }
    
    function setAndDetectChange(id, newVal) {
        const old = getState(id)?.val;
    
        if (typeof newVal === "object") {
            const oldStr = typeof old === "string" ? old : JSON.stringify(old);
            const newStr = JSON.stringify(newVal);
            if (oldStr !== newStr) {
                smartSetState(id, newStr);
                return true;
            }
            return false;
        }
    
        if (old !== newVal) {
            smartSetState(id, newVal);
            return true;
        }
        return false;
    }
    
    async function pingHost(host, stateBase) {
        if (!host) {
            smartSetState(`${stateBase}status`, 'ERROR');
            return;
        }
    
        const result = await ping.promise.probe(host, {timeout: 2, deadline: 2, packetSize: 16});
        if (!result) {
            smartSetState(`${stateBase}status`, 'ERROR');
            return;
        }
    
        const alive = result.alive;
        const time  = ErstelleZahl(parseFloat(result.time));
    
        if (alive === undefined || isNaN(time)) {
            smartSetState(`${stateBase}status`, 'ERROR');
            return;
        }
    
        smartSetState(`${stateBase}alive`, alive);
        smartSetState(`${stateBase}time`, time);
        smartSetState(`${stateBase}status`, 'OK');
    }
    
    async function initConfig() {
        await smartCreateState(`${ROOT}WRITE_ENABLED`, false, {name: "Write Enabled", type: "boolean", read: true, write: true});
        await smartCreateState(`${ROOT}Pollintervall`, schedulerIntervalSlow, {name: "Pollintervall", type: "number", unit: "s", read: true, write: true});
        await smartCreateState(`${ROOT}Dectrefresh`, schedulerIntervalFast, {name: "Dectrefresh", type: "number", unit: "s", read: true, write: true});
        await smartCreateState(`${ROOT}ping.alive`, false, {name: "Erreichbar", type: "boolean", read: true, write: false});
        await smartCreateState(`${ROOT}ping.time`, 0, {name: "Antwortzeit", unit: "ms", type: "number", read: true, write: false});
        await smartCreateState(`${ROOT}ping.status`, "", {name: "Status", type: "string", read: true, write: false});
    
        await sleepMs(300);
    
        const we = getState(`${ROOT}WRITE_ENABLED`);
        const slow = getState(`${ROOT}Pollintervall`);
        const fast = getState(`${ROOT}Dectrefresh`);
    
        if (we && typeof we.val === "boolean") WRITE_ENABLED = we.val;
        if (slow && typeof slow.val === "number") schedulerIntervalSlow = slow.val;
        if (fast && typeof fast.val === "number") schedulerIntervalFast = fast.val;
    }
    
    // SID / Login
    function resetErrorCounter(group) {
        errorCounter[group] = 0;
    }
    
    async function getSid() {
        if (STOPPED) return;
        try {
            const xml = await fritzRequest("get", `http://${fbIp}/login_sid.lua`, false);
            if (!xml || STOPPED) return;
    
            const sidMatch = xml.match(/<SID>([0-9a-f]+)<\/SID>/i);
            if (!sidMatch) {
                logError10x("getSid:sidmatch", "SID nicht extrahierbar");
                return;
            }
    
            sid = sidMatch[1];
            resetErrorCounter("getSid:sidmatch");
    
            if (sid === "0000000000000000") {
                const challenge = xml.match(/<Challenge>(.*?)<\/Challenge>/i)?.[1];
                if (!challenge) {
                    logError10x("getSid:challenge", "Challenge fehlt");
                    return;
                }
                resetErrorCounter("getSid:challenge");
    
                const response = challenge + "-" + crypto.createHash("md5").update(challenge + "-" + pass, "utf16le").digest("hex");
                const loginXml = await fritzRequest("get", `http://${fbIp}/login_sid.lua?username=${encodeURIComponent(user)}&response=${response}`, false);
                if (!loginXml || STOPPED) return;
    
                const sid2 = loginXml.match(/<SID>([0-9a-f]+)<\/SID>/i)?.[1];
                if (!sid2 || sid2 === "0000000000000000") {
                    logError10x("getSid:login", "Login fehlgeschlagen");
                    return;
                }
    
                sid = sid2;
                resetErrorCounter("getSid:login");
            }
            resetErrorCounter("getSid:request");
        } catch (err) {
            logError10x("getSid:request", "SID-Fehler: " + err);
        }
    }
    
    // HTTP-Request mit 403-Retry
    async function fritzRequest(method, url, autoRetry = true) {
        if (STOPPED || !fritzboxAlive()) return "";
        try {
            const res = await axios({ method, url, timeout: 30000 });
            resetErrorCounter("fritzRequest:axios");
            resetErrorCounter("fritzRequest:retry");
            return STOPPED ? "" : res.data;
        } catch (err) {
            if (STOPPED) return "";
    
            // 403 → Retry
            if (autoRetry && err.response && err.response.status === 403) {
                logError10x("fritzRequest:403", "403 erhalten, versuche neuen SID");
    
                sid = "0000000000000000";
                await getSid();
                if (STOPPED) return "";
    
                const retryUrl = url.replace(/sid=[^&]+/, `sid=${sid}`);
                try {
                    const res2 = await axios({ method, url: retryUrl, timeout: 30000 });
                    resetErrorCounter("fritzRequest:403");
                    return STOPPED ? "" : res2.data;
                } catch (err2) {
                    logError10x("fritzRequest:retry", "Axios Retry: " + err2);
                    return "";
                }
            }
            logError10x("fritzRequest:axios", "Axios: " + err);
            return "";
        }
    }
    
    // XML-Parser
    async function parseXml(xml) {
        if (!xml || STOPPED) return {};
        try {
            const result = await xml2js.parseStringPromise(xml, { explicitArray: false });
            resetErrorCounter("parseXml:parse");
            return result;
        } catch (err) {
            logError10x("parseXml:parse", "XML-Parsefehler: " + err);
            return {};
        }
    }
    
    async function parseDeviceAndGroupList(xml) {
        try {
            const json = await parseXml(xml);
            if (STOPPED) return { devices: [], groups: [] };
    
            const root = json.devicelist || {};
    
            let devices = root.device || [];
            if (!Array.isArray(devices)) devices = [devices];
    
            let groups = root.group || [];
            if (!Array.isArray(groups)) groups = [groups];
    
            resetErrorCounter("parseDeviceAndGroupList:parse");
            return { devices, groups };
        } catch (err) {
            logError10x("parseDeviceAndGroupList:parse", "Parse: " + err);
            return { devices: [], groups: [] };
        }
    }
    
    function detectDeviceType(dev) {
        if (dev.groupinfo || dev.$?.functionbitmask === "37504") {
            return "group";
        }
    
        const bitmask = parseInt(dev.$?.functionbitmask || dev.functionbitmask || 0);
        const has = (bit) => (bitmask & (1 << bit)) !== 0;
    
        if (has(6)) {
            return "hkr";
        }
    
        if (has(9) && has(7)) {
            return "dect200";
        }
    
        if (has(5)) {
            return "dect440";
        }
    
        if (has(18)) {
            return "blind";
        }
    
        if (has(20)) {
            return "humidity";
        }
    
        if (has(2) || has(16) || has(17)) {
            return "light";
        }
    
        // Fallback über Produktname (Sicherheitsebene)
        const name = (dev.$?.productname || dev.productname || "").toLowerCase();
    
        if (name.includes("energy 200") ||
            name.includes("fritz!dect 200") ||
            name.includes("smart energy 200") ||
            name.includes("dect 200")) return "dect200";
    
        if (name.includes("control 440") ||
            name.includes("fritz!dect 440") ||
            name.includes("dect 440")) return "dect440";
    
        if (name.includes("fritz!dect 301") ||
            name.includes("fritz!dect 302") ||
            name.includes("comet dect")) return "hkr";
    
        return "unknown";
    }
    
    async function loadDevices() {
        if (STOPPED) return;
    
        if (sid === "0000000000000000") {
            await getSid();
            if (STOPPED) return;
        }
    
        let xml = await fritzRequest("get", `http://${fbIp}/webservices/homeautoswitch.lua?sid=${sid}&switchcmd=getdevicelistinfos`);
        if (STOPPED) return;
    
        let { devices, groups } = await parseDeviceAndGroupList(xml);
        if ((!devices || devices.length === 0) && (!groups || groups.length === 0)) {
            sid = "0000000000000000";
            await getSid();
            if (STOPPED) return;
    
            xml = await fritzRequest("get", `http://${fbIp}/webservices/homeautoswitch.lua?sid=${sid}&switchcmd=getdevicelistinfos`);
            if (STOPPED) return;
            ({ devices, groups } = await parseDeviceAndGroupList(xml));
        }
    
        allAins = [];
        deviceMap = {};
    
        for (const dev of devices) {
            let ain = dev.$?.identifier || dev.identifier;
            if (!ain) continue;
    
            ain = ain.replace(/\s/g, "");
            const type = detectDeviceType(dev);
    
            allAins.push(ain);
            deviceMap[ain] = {ain, type, raw: dev,
                meta: {
                    name: dev.name,
                    productname: dev.productname || dev.$?.productname,
                    manufacturer: dev.manufacturer || dev.$?.manufacturer,
                    fwversion: dev.fwversion || dev.$?.fwversion,
                    id: dev.$?.id,
                    present: dev.present,
                    switchtype: dev.switch?.switchtype,
                    txbusy: dev.txbusy
                }
            };
        }
    
        for (const grp of groups) {
            let ain = grp.$?.identifier || grp.identifier;
            if (!ain) continue;
    
            ain = ain.replace(/\s/g, "");
            allAins.push(ain);
    
            deviceMap[ain] = {ain, type: "group", raw: grp, meta: {name: grp.name, productname: "Group", manufacturer: "AVM", fwversion: grp.$?.fwversion || null, id: grp.$?.id || null, present: grp.present, members: grp.groupinfo?.members || ""}};
        }
    }
    
    // State-Erstellung — DECT 200
    async function createDect200States(ain) {
        if (STOPPED) return;
    
        const devName = (deviceMap[ain] && deviceMap[ain].meta && deviceMap[ain].meta.name) ? deviceMap[ain].meta.name : `DECT_${ain}`;
        await smartCreateState(`${ROOT}DECT_${ain}`, "", {name: devName, read: true, write: false});
    
        const root = `${ROOT}DECT_${ain}.`;
        const states = {
            celsius:         { type: "number", unit: "°C", name: "Temperature" },
            devicelock:      { type: "boolean", name: "Device (Button)lock", default: false },
            energy:          { type: "number", unit: "Wh", name: "Energy consumption" },
            fwversion:       { type: "string", name: "Firmware Version" },
            id:              { type: "number", name: "Device ID" },
            lock:            { type: "boolean", name: "API Lock", default: true },
            manufacturer:    { type: "string", name: "Manufacturer" },
            mode:            { type: "string", name: "Switch Mode" },
            name:            { type: "string", name: "Device Name" },
            offset:          { type: "number", unit: "°C", name: "Temperature Offset" },
            power:           { type: "number", unit: "W", name: "actual Power" },
            present:         { type: "boolean", name: "device present" },
            productname:     { type: "string", name: "Product Name" },
            state:           { type: "boolean", write: true, name: "Switch Status and Control" },
            switchtype:      { type: "string", name: "Switch Type" },
            txbusy:          { type: "boolean", name: "Transmitting active" },
            voltage:         { type: "number", unit: "V", name: "actual Voltage" },
            ampere:          { type: "number", unit: "A", name: "actual Ampere" },
            lastchange:      { type: "string", name: "Last data change timestamp" },
            PollEnergyStats: { type: "number", name: "Last Poll EnergyStats" }
        };
    
        for (const id in states) {
            if (STOPPED) return;
            const cfg = states[id];
            const initialValue = cfg.default !== undefined ? cfg.default : null;
            await smartCreateState(root + id, initialValue, {name: cfg.name, type: cfg.type, read: true, write: cfg.write ?? false, unit: cfg.unit ?? undefined});
        }
    
        const energyStatsStates = {
            "energy_stats.countd":        { type: "number", unit: "days",   name: "Anzahl Tage" },
            "energy_stats.countm":        { type: "number", unit: "months", name: "Anzahl Monate" },
            "energy_stats.datatimed":     { type: "number",                 name: "Zeitpunkt Tagesstatistik" },
            "energy_stats.datatimem":     { type: "number",                 name: "Zeitpunkt Monatsstatistik" },
            "energy_stats.energy_dtd":    { type: "number", unit: "Wh",     name: "Energie heute" },
            "energy_stats.energy_mtd":    { type: "number", unit: "Wh",     name: "Energie aktueller Monat" },
            "energy_stats.energy_ytd":    { type: "number", unit: "Wh",     name: "Energie aktuelles Jahr" },
            "energy_stats.energy_last31d":{ type: "number", unit: "Wh",     name: "Energie letzte 31 Tage" },
            "energy_stats.energy_last12m":{ type: "number", unit: "Wh",     name: "Energie letzte 12 Monate" },
            "energy_stats.gridd":         { type: "number", unit: "s",      name: "Raster Tage" },
            "energy_stats.gridm":         { type: "number", unit: "s",      name: "Raster Monate" },
            "energy_stats.stats_days":    { type: "string",                 name: "Tagesstatistik (Array)" },
            "energy_stats.stats_months":  { type: "string",                 name: "Monatsstatistik (Array)" }
        };
    
        const powerStatsStates = {
            "power_stats.count":     { type: "number", unit: "counts", name: "Anzahl Messpunkte" },
            "power_stats.datetime":  { type: "number",                 name: "Zeitpunkt Statistik" },
            "power_stats.grid":      { type: "number", unit: "s",      name: "Raster" },
            "power_stats.stats":     { type: "string",                 name: "Leistungsstatistik (Array)" }
        };
    
        const voltageStatsStates = {
            "voltage_stats.count":    { type: "number", unit: "counts", name: "Anzahl Messpunkte" },
            "voltage_stats.datetime": { type: "number",                 name: "Zeitpunkt Statistik" },
            "voltage_stats.grid":     { type: "number", unit: "s",      name: "Raster" },
            "voltage_stats.stats":    { type: "string",                 name: "Spannungsstatistik (Array)" }
        };
    
        for (const id in energyStatsStates) {
            const cfg = energyStatsStates[id];
            await smartCreateState(root + id, null, {name: cfg.name, type: cfg.type, read: true, write: false, unit: cfg.unit ?? undefined});
        }
    
        for (const id in powerStatsStates) {
            const cfg = powerStatsStates[id];
            await smartCreateState(root + id, null, {name: cfg.name, type: cfg.type, read: true, write: false, unit: cfg.unit ?? undefined});
        }
    
        for (const id in voltageStatsStates) {
            const cfg = voltageStatsStates[id];
            await smartCreateState(root + id, null, {name: cfg.name, type: cfg.type, read: true, write: false, unit: cfg.unit ?? undefined});
        }
    }
    
    async function createHkrStates(ain) {
        if (STOPPED) return;
    
        const devName = (deviceMap[ain] && deviceMap[ain].meta && deviceMap[ain].meta.name) ? deviceMap[ain].meta.name : `DECT_${ain}`;
        await smartCreateState(`${ROOT}DECT_${ain}`, "", {name: devName, read: true, write: false});
    
        const root = `${ROOT}DECT_${ain}.`;
        const states = {
            absenk:                  { type: "number", unit: "°C", name: "reduced (night) temperature" },
            adaptiveHeatingActive:   { type: "boolean", name: "adaptive Heating active status" },
            adaptiveHeatingRunning:  { type: "boolean", name: "adaptive Heating running status" },
            battery:                 { type: "number", unit: "%", name: "Battery Charge State" },
            batterylow:              { type: "boolean", name: "Battery Low State" },
    
            boostactive:             { type: "boolean", write: true, name: "Boost active status and cmd" },
            boostactiveendtime:      { type: "string", name: "Boost active end time" },
            boostactivetime:         { type: "number", unit: "min", write: true, default: 30, name: "boost active time for cmd" },
            boostremaining:          { type: "number", unit: "min", write: false, default: 0, name: "Boost remaining time" },
    
            celsius:                 { type: "number", unit: "°C", name: "Temperature" },
            devicelock:              { type: "boolean", name: "device lock, button lock", default: false },
            endperiod:               { type: "string", name: "next time for Temp change" },
            errorcode:               { type: "number", name: "Error Code" },
            errorcodestring:         { type: "string", name: "Error Code String" },
            fwversion:               { type: "string", name: "Firmware Version" },
    
            hkrmode:                 { type: "number", write: true, name: "Thermostat operation mode (0=auto, 1=closed, 2=open)" },
    
            holidayactive:           { type: "boolean", name: "Holiday Active status" },
            id:                      { type: "number", name: "Device ID" },
            komfort:                 { type: "number", unit: "°C", name: "comfort temperature" },
            lasttarget:              { type: "number", unit: "°C", name: "last setting of target temp" },
            lock:                    { type: "boolean", name: "Thermostat UI/API lock", default: true },
            manufacturer:            { type: "string", name: "Manufacturer" },
            name:                    { type: "string", name: "Device Name" },
            offset:                  { type: "number", unit: "°C", name: "Temperature Offset" },
    
            operationlist:           { type: "string", name: "List of operation modes" },
            operationmode:           { type: "string", name: "Current operation mode" },
    
            present:                 { type: "boolean", name: "device present" },
            productname:             { type: "string", name: "Product Name" },
    
            setmodeauto:             { type: "boolean", write: true, name: "Switch MODE AUTO" },
            setmodeoff:              { type: "boolean", write: true, name: "Switch MODE OFF" },
            setmodeon:               { type: "boolean", write: true, name: "Switch MODE ON" },
    
            summeractive:            { type: "boolean", name: "summer active status" },
            tchange:                 { type: "number", unit: "°C", name: "Temp after next change" },
            tist:                    { type: "number", unit: "°C", name: "Actual temperature" },
            tsoll:                   { type: "number", unit: "°C", write: true, name: "Setpoint Temperature" },
            txbusy:                  { type: "boolean", name: "Transmitting active" },
    
            windowopenactiv:         { type: "boolean", write: true, name: "Window open status and cmd" },
            windowopenactiveendtime: { type: "string", name: "window open active end time" },
            windowopenactivetime:    { type: "number", unit: "min", write: true, default: 30, name: "window open active time for cmd" },
            windowremaining:         { type: "number", unit: "min", write: false, default: 0, name: "Window open remaining time" },
            lastchange:              { type: "string", name: "Last data change timestamp" }
        };
    
        for (const id in states) {
            const cfg = states[id];
    
            let initialValue = null;
            if (cfg.type === "boolean") initialValue = false;
            if (cfg.default !== undefined) initialValue = cfg.default;
    
            await smartCreateState(root + id, initialValue, {name: id, type: cfg.type, read: true, write: cfg.write ?? false, unit: cfg.unit ?? undefined});
        }
    }
    
    async function createDect440States(ain) {
        if (STOPPED) return;
    
        const devName = (deviceMap[ain] && deviceMap[ain].meta && deviceMap[ain].meta.name) ? deviceMap[ain].meta.name : `DECT_${ain}`;
        await smartCreateState(`${ROOT}DECT_${ain}`, "", {name: devName, read: true, write: false});
    
        const root = `${ROOT}DECT_${ain}.`;
        const mainStates = {
            battery:       { type: "number", unit: "%", name: "Battery Charge State" },
            batterylow:    { type: "boolean", name: "Battery Low State" },
            celsius:       { type: "number", unit: "°C", name: "Temperature" },
            fwversion:     { type: "string", name: "Firmware Version" },
            id:            { type: "number", name: "Device ID" },
            manufacturer:  { type: "string", name: "Manufacturer" },
            name:          { type: "string", name: "Device Name" },
            offset:        { type: "number", unit: "°C", name: "Temperature Offset" },
            present:       { type: "boolean", name: "device present" },
            productname:   { type: "string", name: "Product Name" },
            rel_humidity:  { type: "number", unit: "%", name: "relative Humidity" },
            txbusy:        { type: "boolean", name: "Transmitting active" },
            lastchange:    { type: "string", name: "Last data change timestamp" }
        };
    
        for (const id in mainStates) {
            if (STOPPED) return;
            const cfg = mainStates[id];
            await smartCreateState(root + id, null, {name: cfg.name, type: cfg.type, read: true, write: false, unit: cfg.unit ?? undefined});
        }
    
        const buttonIds = ["1", "3", "5", "7"];
        for (const b of buttonIds) {
            if (STOPPED) return;
    
            const base = `${root}button.${ain}-${b}`;
            await smartCreateState(base + ".id", null, {name: "Button ID", type: "number", read: true, write: false});
            await smartCreateState(base + ".lastpressedtimestamp", null, {name: "last button Time Stamp", type: "string", read: true, write: false});
            await smartCreateState(base + ".name", null, {name: "Button Name", type: "string", read: true, write: false});
        }
    }
    
    async function createDectGroupStates(ain) {
        if (STOPPED) return;
    
        const devName = (deviceMap[ain] && deviceMap[ain].meta && deviceMap[ain].meta.name) ? deviceMap[ain].meta.name : `DECT_grp${ain}`;
        await smartCreateState(`${ROOT}DECT_grp${ain}`, "", {name: devName, read: true, write: false});
    
        const root = `${ROOT}DECT_grp${ain}.`;
        const states = {
            name:        { type: "string",  name: "Group Name" },
            present:     { type: "boolean", name: "Group present" },
            members:     { type: "string",  name: "Group Members" },
            mode:        { type: "string",  name: "Switch Mode" },
    
            power:       { type: "number",  unit: "W",  name: "actual Power" },
            voltage:     { type: "number",  unit: "V",  name: "actual Voltage" },
            ampere:      { type: "number",  unit: "A",  name: "actual Ampere" },
            energy:      { type: "number",  unit: "Wh", name: "Energy consumption" },
    
            lastchange:  { type: "string",  name: "Last data change timestamp" }
        };
    
        for (const id in states) {
            if (STOPPED) return;
    
            const cfg = states[id];
            const initialValue = cfg.default !== undefined ? cfg.default : null;
            await smartCreateState(root + id, initialValue, {name: cfg.name, type: cfg.type, read: true, write: false, unit: cfg.unit ?? undefined});
        }
    }
    
    // Parser — HKR (DECT 301/302)
    async function parseHkrDevice(ain, dev, isInitial = false) {
        if (STOPPED) return;
    
        const HKR_ERRORCODES = {
            0: "kein Fehler",
            1: "Keine Adaptierung möglich. Gerät korrekt am Heizkörper montiert?",
            2: "Ventilhub zu kurz oder Batterieleistung zu schwach. Ventilstößel per Hand mehrmals öffnen und schließen oder neue Batterien einsetzen.",
            3: "Keine Ventilbewegung möglich. Ventilstößel frei?",
            4: "Die Installation wird gerade vorbereitet.",
            5: "Der Heizkörperregler ist im Installationsmodus und kann auf das Heizungsventil montiert werden.",
            6: "Der Heizkörperregler passt sich nun an den Hub des Heizungsventils an."
        };
    
        const root  = `${ROOT}DECT_${ain}.`;
        const h     = dev.hkr         || {};
        const t     = dev.temperature || {};
        const attr  = dev.$           || {};
        const entry = deviceMap[ain]  || {};
        const meta  = entry.meta      || {};
    
        const setMeta = isInitial ? hardSetState : smartSetState;
    
        // Basisdaten
        if (dev.name !== undefined)              setMeta(root + "name",         String(dev.name));
        if (attr.productname !== undefined)      setMeta(root + "productname",  String(attr.productname  || meta.productname  || ""));
        if (attr.manufacturer !== undefined)     setMeta(root + "manufacturer", String(attr.manufacturer || meta.manufacturer || ""));
        if (attr.fwversion !== undefined)        setMeta(root + "fwversion",    String(attr.fwversion    || meta.fwversion    || ""));
        if (attr.id !== undefined)               setMeta(root + "id",           Number(attr.id));
        if (dev.present !== undefined)           setMeta(root + "present",      dev.present === "1");
    
        let changed = false;
    
        // Temperaturwerte
        if (t.celsius !== undefined) changed = setAndDetectChange(root + "celsius", Number(t.celsius) / 10) || changed;
    
        if (t.offset !== undefined) changed = setAndDetectChange(root + "offset", Number(t.offset) / 10) || changed;
    
        if (h.tist !== undefined) {
            const tist = Number(h.tist) / 2;
            if (!isNaN(tist)) changed = setAndDetectChange(root + "tist", tist) || changed;
        }
    
        // Komfort / Absenk
        let komfort = null;
        let absenk  = null;
    
        if (h.komfort !== undefined) {
            komfort = Number(h.komfort) / 2;
            if (!isNaN(komfort)) changed = setAndDetectChange(root + "komfort", komfort) || changed;
        }
    
        if (h.absenk !== undefined) {
            absenk = Number(h.absenk) / 2;
            if (!isNaN(absenk)) changed = setAndDetectChange(root + "absenk", absenk) || changed;
        }
    
        // Roh-tsoll korrekt interpretieren (OHNE Off/On Mapping)
        let rawTsoll = null;
        let tsoll    = null;
    
        if (h.tsoll !== undefined) rawTsoll = Number(h.tsoll);
    
        const boostActive   = h.boostactive === "1";
        const holidayActive = h.holidayactive === "1";
        const windowActive  = h.windowopenactiv === "1";
    
        if (boostActive && komfort !== null) {
            tsoll = komfort;
        }
        else if ((holidayActive || windowActive) && absenk !== null) {
            tsoll = absenk;
        }
        else if (rawTsoll !== null && !isNaN(rawTsoll)) {
            // WICHTIG:
            // 253 (Off), 254 (On), 255 (Auto)
            // dürfen KEIN tsoll setzen!
            if (rawTsoll >= 16 && rawTsoll <= 56) {
                tsoll = rawTsoll / 2;
            }
        }
    
        // Sicherheitsfilter
        if (tsoll !== null) {
            if (isNaN(tsoll) || tsoll < 8 || tsoll > 28) {
                tsoll = null;
            }
        }
    
        // Statuswerte
        const boolMap = ["lock", "devicelock", "windowopenactiv", "batterylow", "summeractive", "holidayactive", "adaptiveHeatingActive", "adaptiveHeatingRunning", "boostactive"];
        for (const key of boolMap) {
            if (h[key] !== undefined)  changed = setAndDetectChange(root + key, h[key] === "1") || changed;
        }
    
        const errorcode = Number(h.errorcode);
        if (h.errorcode !== undefined) changed = setAndDetectChange(root + "errorcode", errorcode) || changed;
        const errortext = HKR_ERRORCODES[errorcode] || "Unbekannter Fehlercode";
        smartSetState(root + "errorcodestring", errortext);
    
        if (h.battery !== undefined)   changed = setAndDetectChange(root + "battery", Number(h.battery)) || changed;
        if (dev.txbusy !== undefined)  changed = setAndDetectChange(root + "txbusy", dev.txbusy === "1") || changed;
    
        // Boost / Window Zeiten
        function handleTimeBlock(sourceKey, targetKeyTime, targetKeyRemain) {
            if (h[sourceKey] === undefined) return;
    
            const sec = Number(h[sourceKey]);
            const ts  = sec > 0 ? hole_datum(sec * 1000) : "";
    
            changed = setAndDetectChange(root + targetKeyTime, ts) || changed;
    
            if (sec > 0) {
                const nowSec = Math.floor(Date.now() / 1000);
                const diff   = sec - nowSec;
                changed = setAndDetectChange(root + targetKeyRemain, diff > 0 ? Math.round(diff / 60) : 0) || changed;
            } else {
                changed = setAndDetectChange(root + targetKeyRemain, 0) || changed;
            }
        }
    
        handleTimeBlock("boostactiveendtime",       "boostactiveendtime",       "boostremaining");
        handleTimeBlock("windowopenactiveendtime",  "windowopenactiveendtime",  "windowremaining");
    
        // NextChange
        const nc = h.nextchange || dev.nextchange || {};
    
        if (nc.tchange !== undefined) {
            const tchange = Number(nc.tchange) / 2;
            if (!isNaN(tchange)) changed = setAndDetectChange(root + "tchange", tchange) || changed;
        }
    
        if (nc.endperiod !== undefined) {
            const sec = Number(nc.endperiod);
            const ts  = sec > 0 ? hole_datum(sec * 1000) : "";
            changed = setAndDetectChange(root + "endperiod", ts) || changed;
        }
    
        // Betriebsmodus bestimmen (OHNE tsoll Manipulation)
        setMeta(root + "operationlist", "Auto,On,Off,Holiday");
    
        let hkrmode = 0;
        let opMode  = "Auto";
    
        if (rawTsoll === 253) {
            hkrmode = 1;
            opMode  = "Off";
        } else if (rawTsoll === 254) {
            hkrmode = 2;
            opMode  = "On";
        } else if (boostActive) {
            opMode = "Boost";
        } else if (holidayActive) {
            opMode = "Holiday";
        } else if (windowActive) {
            opMode = "WindowOpen";
        } else if (tsoll !== null) {
            if (komfort !== null && tsoll === komfort) {
                opMode = "Comfort";
            } else if (absenk !== null && tsoll === absenk) {
                opMode = "Night";
            } else {
                opMode = "Auto";
            }
        }
    
        changed = setAndDetectChange(root + "hkrmode", hkrmode) || changed;
        changed = setAndDetectChange(root + "operationmode", opMode) || changed;
    
        // tsoll nur bei echter Temperatur schreiben
        if (tsoll !== null) {
            smartSetState(root + "tsoll", tsoll);
            smartSetState(root + "lasttarget", tsoll);
        }
    
        // Änderungszeit
        if (changed) {
            smartSetState(root + "lastchange", hole_datum());
        }
    }
    
    // Parser — DECT 200
    async function parseDect200Device(ain, dev, isInitial = false) {
        if (STOPPED) return;
    
        const root = `${ROOT}DECT_${ain}.`;
        const e = dev.powermeter || {};
        const t = dev.temperature || {};
        const sw = dev.switch || {};
        const attr = dev.$ || {};
    
        const setMeta = isInitial ? hardSetState : smartSetState;
    
        if (dev.name !== undefined) setMeta(root + "name", String(dev.name));
        if (attr.productname !== undefined) setMeta(root + "productname", String(attr.productname));
        if (attr.manufacturer !== undefined) setMeta(root + "manufacturer", String(attr.manufacturer));
        if (attr.fwversion !== undefined) setMeta(root + "fwversion", String(attr.fwversion));
        if (attr.id !== undefined) setMeta(root + "id", Number(attr.id));
        if (dev.present !== undefined) setMeta(root + "present", dev.present === "1");
    
        let changed = false;
    
        if (t.celsius !== undefined) changed = setAndDetectChange(root + "celsius", Number(t.celsius) / 10) || changed;
        if (t.offset !== undefined)  changed = setAndDetectChange(root + "offset", Number(t.offset) / 10) || changed;
        if (e.power !== undefined)   changed = setAndDetectChange(root + "power", Number(e.power) / 1000) || changed;
        if (e.energy !== undefined)  changed = setAndDetectChange(root + "energy", Number(e.energy)) || changed;
        if (e.voltage !== undefined) changed = setAndDetectChange(root + "voltage", Number(e.voltage) / 1000) || changed;
    
        if (e.power !== undefined && e.voltage !== undefined) {
            const watt = Number(e.power) / 1000;
            const volt = Number(e.voltage) / 1000;
    
            let ampRounded = 0;
            if (volt > 0) {
                const amp = watt / volt;
                ampRounded = Math.round(amp * 100) / 100;
            }
            changed = setAndDetectChange(root + "ampere", ampRounded) || changed;
        }
    
        if (sw.state !== undefined) changed = setAndDetectChange(root + "state", sw.state === "1") || changed;
        if (sw.mode !== undefined)  changed = setAndDetectChange(root + "mode", String(sw.mode)) || changed;
        if (sw.lock !== undefined)  changed = setAndDetectChange(root + "lock", sw.lock === "1") || changed;
        if (sw.devicelock !== undefined) changed = setAndDetectChange(root + "devicelock", sw.devicelock === "1") || changed;
    
        smartSetState(root + "switchtype", "simpleonoff");
    
        if (dev.txbusy !== undefined) changed = setAndDetectChange(root + "txbusy", dev.txbusy === "1") || changed;
    
        if (changed) smartSetState(root + "lastchange", hole_datum());
    }
    
    // Parser für EnergyStats_10 (nur DECT200)
    async function parseEnergyStats(ain, raw) {
        if (STOPPED || !raw) return;
    
        let json = raw;
        if (!json || typeof json !== "object") {
            logError10x("EnergyStats:parse", "Ungültige Antwort, kein Objekt");
            return;
        }
        resetErrorCounter("EnergyStats:parse");
    
        const root = `${ROOT}DECT_${ain}.`;
    
        // ENERGY_STATS (nur das, was im RAW vorhanden ist)
        if (json.sum_Day !== undefined) {
            smartSetState(root + "energy_stats.energy_dtd", json.sum_Day);
        }
        if (json.sum_Month !== undefined) {
            smartSetState(root + "energy_stats.energy_mtd", json.sum_Month);
        }
        if (json.sum_Year !== undefined) {
            smartSetState(root + "energy_stats.energy_ytd", json.sum_Year);
        }
    
        if (json.CurrentDateInSec !== undefined) {
            const ts = Number(json.CurrentDateInSec) * 1000;
            smartSetState(root + "energy_stats.datatimed", ts);
            smartSetState(root + "energy_stats.datatimem", ts);
        }
    
        // POWER_STATS (EnergyStat = Leistung in mW)
        const ps = json.EnergyStat || {};
        if (ps.anzahl !== undefined) {
            smartSetState(root + "power_stats.count", ps.anzahl);
        }
        if (ps.datatimestamp !== undefined) {
            smartSetState(root + "power_stats.datetime", Number(ps.datatimestamp) * 1000);
        }
        if (ps.times_type !== undefined) {
            smartSetState(root + "power_stats.grid", ps.times_type);
        }
        if (Array.isArray(ps.values)) {
            // mW → W
            smartSetState(root + "power_stats.stats", JSON.stringify(ps.values.map(v => v / 1000)));
        }
    
        // VOLTAGE_STATS (VoltageStat = Spannung in mV)
        const vs = json.VoltageStat || {};
        if (vs.anzahl !== undefined) {
            smartSetState(root + "voltage_stats.count", vs.anzahl);
        }
        if (vs.datatimestamp !== undefined) {
            smartSetState(root + "voltage_stats.datetime", Number(vs.datatimestamp) * 1000);
        }
        if (vs.times_type !== undefined) {
            smartSetState(root + "voltage_stats.grid", vs.times_type);
        }
        if (Array.isArray(vs.values)) {
            smartSetState(root + "voltage_stats.stats", JSON.stringify(vs.values.map(v => v / 1000)));
        }
    }
    
    async function updateBasicDeviceStats(ain) {
        try {
            const url = `http://${fbIp}/webservices/homeautoswitch.lua?switchcmd=getbasicdevicestats&ain=${ain}&sid=${sid}`;
            const xml = await fritzRequest("get", url);
            if (!xml) return;
    
            const json = await parseXml(xml);
            const stats = json?.devicestats?.energy?.stats;
            if (!stats || !Array.isArray(stats)) return;
    
            const base = `${ROOT}DECT_${ain}.energy_stats.`;
    
            // Monatsstatistik (stats[0])
            if (stats[0]) {
                const m = stats[0];
                const arr = m._?.split(",").map(Number) || [];
    
                smartSetState(base + "countm", Number(m.$?.count || 0));
                smartSetState(base + "gridm", Number(m.$?.grid || 0));
                smartSetState(base + "datatimem", Number(m.$?.datatime || 0) * 1000);
    
                smartSetState(base + "stats_months", JSON.stringify(arr));
                smartSetState(base + "energy_last12m", arr.reduce((a, b) => a + b, 0));
    
                const month = new Date().getMonth() + 1;
                smartSetState(base + "energy_ytd", arr.slice(0, month).reduce((a, b) => a + b, 0));
            }
    
            // Tagesstatistik (stats[1])
            if (stats[1]) {
                const d = stats[1];
                const arr = d._?.split(",").map(Number) || [];
    
                smartSetState(base + "countd", Number(d.$?.count || 0));
                smartSetState(base + "gridd", Number(d.$?.grid || 0));
                smartSetState(base + "datatimed", Number(d.$?.datatime || 0) * 1000);
    
                smartSetState(base + "stats_days", JSON.stringify(arr));
                smartSetState(base + "energy_last31d", arr.reduce((a, b) => a + b, 0));
    
                const day = new Date().getDate();
                smartSetState(base + "energy_mtd", arr.slice(0, day).reduce((a, b) => a + b, 0));
                smartSetState(base + "energy_dtd", arr[0] || 0);
            }
        } catch (err) {
            logError10x("updateBasicDeviceStats", err);
        }
    }
    
    // Parser — DECT 440
    async function parseDect440Device(ain, dev, isInitial = false) {
        if (STOPPED) return;
    
        const root = `${ROOT}DECT_${ain}.`;
        const t = dev.temperature || {};
        const hum = dev.humidity || {};
        const attr = dev.$ || {};
        const entry = deviceMap[ain] || {};
        const meta = entry.meta || {};
    
        const setMeta = isInitial ? hardSetState : smartSetState;
    
        setMeta(root + "name", dev.name ?? meta.name ?? null);
        setMeta(root + "productname", dev.productname ?? meta.productname ?? null);
        setMeta(root + "manufacturer", dev.manufacturer ?? meta.manufacturer ?? null);
        setMeta(root + "fwversion", dev.fwversion ?? meta.fwversion ?? null);
    
        if (attr.id !== undefined) setMeta(root + "id", Number(attr.id));
        else if (meta.id !== undefined) setMeta(root + "id", Number(meta.id));
    
        if (dev.present !== undefined) {
            setMeta(root + "present", dev.present === "1");
        } else if (meta.present !== undefined) {
            setMeta(root + "present", meta.present === "1" || meta.present === 1 || meta.present === true);
        }
    
        let changed = false;
        if (t.celsius !== undefined) changed = setAndDetectChange(root + "celsius", Number(t.celsius) / 10) || changed;
        if (t.offset !== undefined)  changed = setAndDetectChange(root + "offset", Number(t.offset) / 10) || changed;
        if (hum.rel_humidity !== undefined) changed = setAndDetectChange(root + "rel_humidity", Number(hum.rel_humidity)) || changed;
        if (dev.battery !== undefined) changed = setAndDetectChange(root + "battery", Number(dev.battery)) || changed;
        if (dev.batterylow !== undefined) changed = setAndDetectChange(root + "batterylow", dev.batterylow === "1") || changed;
    
        const txb = dev.txbusy ?? meta.txbusy;
        if (txb !== undefined) changed = setAndDetectChange(root + "txbusy", txb === "1" || txb === 1 || txb === true) || changed;
    
        if (changed) {
            smartSetState(root + "lastchange", hole_datum());
        }
    
        const buttons = dev.button || [];
        const arr = Array.isArray(buttons) ? buttons : [buttons];
        const buttonIds = ["1", "3", "5", "7"];
    
        for (let i = 0; i < buttonIds.length; i++) {
            const bId = buttonIds[i];
            const bObj = arr[i];
            if (!bObj) continue;
    
            const base = `${root}button.${ain}-${bId}`;
            const src = bObj;
    
            if (src.$?.id !== undefined) {
                smartSetState(base + ".id", Number(src.$.id));
            }
            if (src.lastpressedtimestamp !== undefined) {
                const ts = src.lastpressedtimestamp ? Number(src.lastpressedtimestamp) * 1000 : 0;
                smartSetState(base + ".lastpressedtimestamp", ts ? hole_datum(ts) : "");
            }
            if (src.name !== undefined && src.name !== null && src.name !== "") {
                smartSetState(base + ".name", String(src.name));
            }
        }
    }
    
    async function parseDectGroupDevice(ain, grp) {
        if (STOPPED || !grp) return;
    
        const root = `${ROOT}DECT_grp${ain}.`;
        let changed = false;
    
        smartSetState(root + "name", grp.name || "");
        smartSetState(root + "present", grp.present === "1");
    
        const members = grp.groupinfo?.members || "";
        smartSetState(root + "members", members);
        smartSetState(root + "mode", grp.switch?.mode || "manuell");
    
        if (grp.powermeter) {
            const power = Number(grp.powermeter.power) / 1000;
            const voltage = Number(grp.powermeter.voltage) / 1000;
            const energy = Number(grp.powermeter.energy);
    
            changed = setAndDetectChange(root + "power", power) || changed;
            changed = setAndDetectChange(root + "voltage", voltage) || changed;
            changed = setAndDetectChange(root + "energy", energy) || changed;
    
            if (voltage > 0) {
                const ampere = power / voltage;
                const ampRounded = Math.round(ampere * 100) / 100;
                changed = setAndDetectChange(root + "ampere", ampRounded) || changed;
            }
        }
    
        if (changed) smartSetState(root + "lastchange", hole_datum());
    }
    
    async function pollAll(isInitial = false) {
        if (STOPPED || POLLING || !fritzboxAlive()) return;
        POLLING = true;
    
        try {
            if (sid === "0000000000000000") await getSid();
            if (STOPPED) return;
    
            const xml = await fritzRequest("get", `http://${fbIp}/webservices/homeautoswitch.lua?sid=${sid}&switchcmd=getdevicelistinfos`);
            if (STOPPED || !xml) return;
    
            const { devices, groups } = await parseDeviceAndGroupList(xml);
    
            const list = [...devices, ...groups];
            await updateMasterDevice(devices);
            if (!list.length) {
                resetErrorCounter("pollAll:parse");
                return;
            }
    
            for (const dev of list) {
                if (STOPPED) return;
    
                let ain = dev.$?.identifier || dev.identifier;
                if (!ain) continue;
                ain = ain.replace(/\s/g, "");
    
                const type = detectDeviceType(dev);
    
                if (type === "hkr") {
                    await parseHkrDevice(ain, dev, isInitial);
                } else if (type === "dect200") {
                    await parseDect200Device(ain, dev, isInitial);
                } else if (type === "dect440") {
                    await parseDect440Device(ain, dev, isInitial);
                } else if (type === "group") {
                    await parseDectGroupDevice(ain, dev);
                }
            }
    
            dect200List = [];
            for (const dev of devices) {
                let ain = dev.$?.identifier || dev.identifier;
                if (!ain) continue;
                ain = ain.replace(/\s/g, "");
    
                const type = detectDeviceType(dev);
                if (type === "dect200" && dev.present === "1") {
                    dect200List.push(ain);
                }
            }
            resetErrorCounter("pollAll:parse");
        } catch (err) {
            logError10x("pollAll:parse", "pollAll(): " + err);
        } finally {
            POLLING = false;
        }
    }
    
    async function initialPoll() {
        if (STOPPED) return;
        logInfo("Führe initialen Poll durch...");
        await pollAll(true);
        logInfo("Initialer Poll abgeschlossen.");
    }
    
    function resetErrorCounter(group) {
        errorCounter[group] = 0;
    }
    
    function setupOnHandlers() {
        if (ON_HANDLERS_ACTIVE) return;
        ON_HANDLERS_ACTIVE = true;
    
        on({ id: new RegExp("^" + ROOT.replace(/\./g, "\\.") + "DECT_[0-9]+\\.state$"), change: "any", ack: false }, async obj => {
            try {
                if (STOPPED || !WRITE_ENABLED) return;
                if (!obj || obj.state === undefined) return;
                if (obj.state.ack) return;
    
                const id  = obj.id;
                const val = obj.state.val;
                const ain = extractAin(id);
                if (!ain) return;
    
                if (await isApiLocked(ain)) {
                    const old = obj.oldState?.val;
                    if (old !== undefined) smartSetState(id, old);
                    return;
                }
    
                const cmd = val ? "setswitchon" : "setswitchoff";
                try {
                    if (sid === "0000000000000000") await getSid();
                    if (STOPPED) return;
    
                    await fritzRequest("get", `http://${fbIp}/webservices/homeautoswitch.lua?sid=${sid}&switchcmd=${cmd}&ain=${encodeURIComponent(ain)}`);
                    resetErrorCounter("DECT200-Write");
                } catch (err) {
                    logError10x("DECT200-Write", `Schalten DECT200 (${ain}): ${err}`);
                }
            } catch (err) {
                logError10x("DECT200-Handler", `Uncaught Handler Error: ${err}`);
            }
        });
    
        on({id: new RegExp("^" + ROOT.replace(/\./g, "\\.") + "DECT_[0-9]+\\.(tsoll|setmodeauto|setmodeoff|setmodeon|hkrmode|boostactivetime|windowopenactivetime)$"), change: "any", ack: false}, async obj => {
            if (STOPPED || !WRITE_ENABLED) return;
            if (!obj || obj.state === undefined) return;
            if (obj.state.ack) return;
    
            const id  = obj.id;
            const val = obj.state.val;
            const ain = extractAin(id);
            if (!ain) return;
    
            if (await isApiLocked(ain)) {
                const old = obj.oldState?.val;
                if (old !== undefined) smartSetState(id, old);
                return;
            }
    
            const dp = id.split(".").slice(-1)[0];
            try {
                await writeHkr(ain, dp, val);
                resetErrorCounter("HKR-Write");
            } catch (err) {
                logError10x("HKR-Write", `HKR-Write (${ain}/${dp}): ${err}`);
            }
        });
    
        on({ id: `${ROOT}WRITE_ENABLED`, change: "ne" }, obj => {
            if (!obj || obj.state === undefined) return;
            WRITE_ENABLED = obj.state.val === true;
        });
    
        on({id:`${ROOT}ping.alive`, change:"ne"}, obj => {
            if (obj.state.val === true) {
                sid = "0000000000000000";
                pollAll(true);
            }
    
        });
    }
    
    async function setBoostDuration(ain, minutes) {
        if (await isApiLocked(ain)) {
            const id = `${ROOT}DECT_${ain}.boostactivetime`;
            const old = getState(id)?.val;
            if (old !== undefined) smartSetState(id, old);
            return;
        }
    
        const now = Math.floor(Date.now() / 1000);
        const endTimestamp = now + (minutes * 60);
    
        const url = `http://${fbIp}/webservices/homeautoswitch.lua?sid=${sid}&ain=${encodeURIComponent(ain)}&switchcmd=sethkrboost&endtimestamp=${endTimestamp}`;
    
        try {
            const result = await fritzRequest("get", url);
            resetErrorCounter("setBoostDuration:write");
            return result;
        } catch (err) {
            // 403 → Retry
            if (err.response && err.response.status === 403) {
                logError10x("setBoostDuration:403", String(err));
    
                await getSid();
                const retryUrl = url.replace(/sid=[^&]+/, `sid=${sid}`);
    
                const result2 = await fritzRequest("get", retryUrl);
                resetErrorCounter("setBoostDuration:403");
                return result2;
            }
            logError10x("setBoostDuration:write", String(err));
        }
    }
    
    async function setWindowOpenDuration(ain, minutes) {
        if (await isApiLocked(ain)) {
            const id = `${ROOT}DECT_${ain}.windowopenactivetime`;
            const old = getState(id)?.val;
            if (old !== undefined) smartSetState(id, old);
            return;
        }
    
        const now = Math.floor(Date.now() / 1000);
        const endTimestamp = now + (minutes * 60);
    
        const url = `http://${fbIp}/webservices/homeautoswitch.lua?sid=${sid}&ain=${encodeURIComponent(ain)}&switchcmd=sethkrwindowopen&endtimestamp=${endTimestamp}`;
        try {
            const result = await fritzRequest("get", url);
            resetErrorCounter("setWindowOpenDuration:write");
            return result;
        } catch (err) {
            // 403 → Retry
            if (err.response && err.response.status === 403) {
                logError10x("setWindowOpenDuration:403", String(err));
    
                await getSid();
                const retryUrl = url.replace(/sid=[^&]+/, `sid=${sid}`);
    
                const result2 = await fritzRequest("get", retryUrl);
                resetErrorCounter("setWindowOpenDuration:403");
                return result2;
            }
            logError10x("setWindowOpenDuration:write", String(err));
        }
    }
    
    async function writeHkr(ain, dp, value) {
        if (STOPPED) return;
    
        if (await isApiLocked(ain)) {
            const id = `${ROOT}DECT_${ain}.${dp}`;
            const old = getState(id)?.val;
            if (old !== undefined) smartSetState(id, old);
            return;
        }
    
        if (!sid || sid === "0000000000000000") await getSid();
        if (STOPPED) return;
    
        const fritzAin = encodeURIComponent(ain);
        const baseUrl = `http://${fbIp}/webservices/homeautoswitch.lua?sid=${sid}&ain=${fritzAin}`;
    
        async function sendFritzRequest(url) {
            try {
                const result = await fritzRequest("get", url);
                resetErrorCounter("writeHkr:write");
                return result;
            } catch (err) {
                // 403 → Retry
                if (err.response && err.response.status === 403) {
                    logError10x("writeHkr:403", String(err));
    
                    await getSid();
                    const newUrl = url.replace(/sid=[^&]+/, `sid=${sid}`);
    
                    const result2 = await fritzRequest("get", newUrl);
                    resetErrorCounter("writeHkr:403");
                    return result2;
                }
                logError10x("writeHkr:write", String(err));
            }
        }
    
        switch (dp) {
            case "tsoll": {
                const v = Number(value);
    
                if (isNaN(v)) return;
                if (v < 8 || v > 28) return;
    
                const raw = Math.round(v * 2);
    
                if (raw < 16 || raw > 56) return; // absoluter Schutz
    
                await sendFritzRequest(`${baseUrl}&switchcmd=sethkrtsoll&param=${raw}`);
                return;
            }
    
            case "setmodeauto":
                if (value === true) {
                    smartSetState(`${ROOT}DECT_${ain}.setmodeauto`, false);
    
                    const last = await getStateAsync(`${ROOT}DECT_${ain}.lasttarget`);
                    if (!last || last.val === null) return;
    
                    const temp = Number(last.val);
                    if (isNaN(temp)) return;
                    if (temp < 8 || temp > 28) return;
    
                    const raw = Math.round(temp * 2);
    
                    await sendFritzRequest(`${baseUrl}&switchcmd=sethkrtsoll&param=${raw}`);
                }
                return;
    
            case "setmodeoff":
                if (value === true) {
                    smartSetState(`${ROOT}DECT_${ain}.setmodeoff`, false);
                    await sendFritzRequest(`${baseUrl}&switchcmd=sethkrtsoll&param=253`);
                    smartSetState(`${ROOT}DECT_${ain}.operationmode`, "Off");
                    smartSetState(`${ROOT}DECT_${ain}.hkrmode`, 1);
                }
                return;
    
            case "setmodeon":
                if (value === true) {
                    smartSetState(`${ROOT}DECT_${ain}.setmodeon`, false);
                    await sendFritzRequest(`${baseUrl}&switchcmd=sethkrtsoll&param=254`);
                    smartSetState(`${ROOT}DECT_${ain}.operationmode`, "On");
                    smartSetState(`${ROOT}DECT_${ain}.hkrmode`, 2);
                }
                return;
    
            case "hkrmode": {
                const m = Number(value);
                if (m === 0) await sendFritzRequest(`${baseUrl}&switchcmd=sethkrtsoll&param=255`);
                if (m === 1) await sendFritzRequest(`${baseUrl}&switchcmd=sethkrtsoll&param=253`);
                if (m === 2) await sendFritzRequest(`${baseUrl}&switchcmd=sethkrtsoll&param=254`);
                return;
            }
    
            case "boostactivetime": {
                const min = Number(value);
                if (isNaN(min) || min < 1 || min > 1440) return;
                await setBoostDuration(ain, min);
                return;
            }
    
            case "windowopenactivetime": {
                const min = Number(value);
                if (isNaN(min) || min < 1 || min > 1440) return;
                await setWindowOpenDuration(ain, min);
                return;
            }
        }
    }
    
    // Round-Robin Master über DECT200
    function rotateMaster() {
        if (dect200List.length === 0) {
            masterId = null;
            return;
        }
    
        dect200Index++;
        if (dect200Index >= dect200List.length) {
            dect200Index = 0;
        }
    
        const ain = dect200List[dect200Index];
        const dev = deviceMap[ain];
        if (!dev || !dev.meta) return;
    
        masterId = dev.meta.id;
    }
    
    function startPollScheduler() {
        if (schedulerHandle) return;
        schedulerHandle = schedule(`*/${schedulerIntervalSlow} * * * * *`, async () => {
            if (STOPPED) return;
            await pollAll(false);
            if (masterId) {
                await startWebUiEmulation(masterId);
            }
        });
        logInfo(`Poll gestartet (Intervall: ${schedulerIntervalSlow} Sekunden).`);
    }
    
    async function safeStartWebUiEmulation(ain) {
        const now = Date.now();
    
        if (now - lastUiCall < UI_BASE_INTERVAL + uiBackoff) return;
        lastUiCall = now;
        try {
            await startWebUiEmulation(ain);
            resetErrorCounter("WebUI:emulation");
            uiBackoff = 0;
        } catch (err) {
            uiBackoff = Math.min(uiBackoff + 2000, UI_BACKOFF_MAX);
            logError10x("WebUI:emulation", `WebUI-Emulation Fehler (${ain}): ${err.message}`);
        }
    }
    
    function startDECTRefreshScheduler() {
        if (fastSchedulerHandle) return;
    
        fastSchedulerHandle = schedule(`*/${schedulerIntervalFast} * * * * *`, async () => {
            if (STOPPED) return;
    
            rotateMaster();
            if (masterId) {
                safeStartWebUiEmulation(masterId);
            }
        });
        logInfo(`DECT-Refresh gestartet (Intervall: ${schedulerIntervalFast} Sekunden).`);
    }
    
    function startPingScheduler() {
        if (pingSchedulerHandle) return;
        pingSchedulerHandle = schedule('*/5 * * * * *', async () => {
            if (STOPPED) return;
            await pingHost(fbIp, `${ROOT}ping.`);;
        });
        logInfo(`Ping gestartet (Intervall: 5 Sekunden).`);
    }
    
    // WebUI-Emulation
    async function updateMasterDevice(devices) {
        if (masterId !== null) {
            const current = devices.find(d => d.$?.id == masterId);
            if (current && current.present === "1") {
                return masterId;
            }
        }
    
        for (const dev of devices) {
            const type = detectDeviceType(dev);
            if (type === "dect200" && dev.present === "1") {
                masterId = dev.$?.id;
                return masterId;
            }
        }
        masterId = null;
        return null;
    }
    
    async function startWebUiEmulation(id) {
        if (!id) return;
        if (!fritzboxAlive()) return;
    
        try {
            if (sid === "0000000000000000") await getSid();
    
            const url = `http://${fbIp}/net/home_auto_query.lua?sid=${sid}&no_sidrenew=1&command=EnergyStats_10&id=${id}&useajax=1&xhr=1`;
            const raw = await fritzRequest("get", url);
    
            if (!raw || typeof raw !== "object") {
                return;
            }
    
            const ain = Object.keys(deviceMap).find(a => deviceMap[a]?.meta?.id == id);
            if (!ain) {
                logError10x("startWebUiEmulation:noAIN", `Keine AIN für ID ${id} gefunden`);
                return;
            }
    
            await parseEnergyStats(ain, raw);      // Momentanwerte
    
            const stateId = `${ROOT}DECT_${ain}.PollEnergyStats`;
            const now = Date.now();
            const ONE_HOUR = 60 * 60 * 1000;
    
            const stateObj = getState(stateId);
            const lastTs = stateObj?.val ?? 0;
    
            if (!lastTs || (now - lastTs) >= ONE_HOUR) {
                await updateBasicDeviceStats(ain);     // XML-Statistik
                smartSetState(stateId, now);
            }
    
            // Fehlerzähler für alle drei Fälle zurücksetzen
            resetErrorCounter("startWebUiEmulation:noAIN");
            resetErrorCounter("startWebUiEmulation:general");
        } catch (err) {
            logError10x("startWebUiEmulation:general", "WebUI-Emulation Fehler: " + err);
        }
    }
    
    // main()
    async function main() {
        logInfo("Starte Initialisierung...");
    
        await loadDevices();
        if (STOPPED) return;
    
        await initConfig();
    
        for (const ain of allAins) {
            if (STOPPED) return;
            const entry = deviceMap[ain];
            const type  = entry.type;
    
            if (type === "dect200") {
                await createDect200States(ain);
            } else if (type === "hkr") {
                await createHkrStates(ain);
            } else if (type === "dect440") {
                await createDect440States(ain);
            } else if (type === "group") {
                await createDectGroupStates(ain);
            }
        }
        logInfo("Erstelle Datenpunkte...");
    
        await initialPoll();
        if (STOPPED) return;
    
        setupOnHandlers();
        logInfo("ON-Handler aktiviert.");
    
        await sleepMs(2000);
        startPollScheduler();
        startDECTRefreshScheduler();
        startPingScheduler();
        logInfo(`System vollständig gestartet. (WRITE_ENABLED=${WRITE_ENABLED})`);
    }
    
    // Stop-Handler
    onStop(async cb => {
        STOPPED = true;
        logInfo("Stoppe Script...");
    
        try {
            if (schedulerHandle) {
                clearSchedule(schedulerHandle);
                schedulerHandle = null;
            }
            if (fastSchedulerHandle) {
                clearSchedule(fastSchedulerHandle);
                fastSchedulerHandle = null;
            }
            if (pingSchedulerHandle) {
                clearSchedule(pingSchedulerHandle);
                pingSchedulerHandle = null;
            }
        } catch (e) {}
        cb();
    });
    
    main().catch(err => logError10x("main", "main(): " + err));
    

    Viel Spaß beim Testen.

    SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

    paul53P 1 Antwort Letzte Antwort
    0
    • Ro75R Ro75

      Einleitung

      Mir ist bewusst, dass es bereits den FritzDect Adapter gibt. Allerdings gab es bei mir immer wieder Fehlermeldungen und eine erhöhte CPU-Last auf der Fritzbox selbst. Zusätzlich war mir die Datenlieferung für FritzDect 200 zu lang.

      Wer nur FritzDect 301, HKR, FritzDect 200 oder FritzDect 440 besitzt, kann dieses Skript poblemlos einsetzen - nur diese Geräte werden unterstützt.

      Vorteile:

      • geringere Auslastung der Fritzbox
      • aktuelle Stromdaten (Dect 200), aller 18 - 22 Sekunden
      • alle Geräte werden automatisch erkannt und als Datenpunkt angelegt.
      • Flexibel

      Hinweis: Bitte nicht ungeduldig werden, das Skript braucht ein paar Sekunden bis alle Funktionen laufen.

      Wichtig: Das Skript sollte nicht parallel zum Adapter laufen!

      Voraussetzung:

      • JS-Adapter: 9.0.x
      • Admin-Adapter: 7.7.x
      • folgende NPM-Pakete müssen zusätzlich im JS-Adapter eingetragen sein: "crypto", "xml2js", "ping"

      Die Datenstruktur ist zu 99% an den Adapter angelehnt. Einige Datenpunkte liefern für die Visualisierung idealere Werte.
      Der größte Vorteil ist, das Dect 200 Geräte jetzt sehr schnell Datenliefern.

      Was muss im Skript angepasst werden?

      const fbIp = "IP-Adresse";
      const user = "Benutzer";
      const pass = "Kennwort";
      

      Passe diese 3 Zeilen an.

      Hier das Skript:

      // @ts-nocheck
      const crypto        = require("crypto");
      const xml2js        = require("xml2js");
      const ping          = require("ping");
      
      const axiosImport   = require("axios");
      const axios         = axiosImport.default || axiosImport;
      
      //Daten der Fritzbox und Benutzer
      const fbIp = "IP-Adresse";
      const user = "Benutzer";
      const pass = "Kennwort";
      const ROOT = "0_userdata.0.fritzdect.";
      
      //AB HIER NICHTS MEHR ÄNDERN!
      let ON_HANDLERS_ACTIVE = false;
      let WRITE_ENABLED = false;
      let schedulerIntervalSlow = 20;   // Initialisierungswert Pollintervall in Sekunden
      let schedulerIntervalFast = 7;    // Initialisierungswert Dectrefresh in Sekunden
      
      let lastUiCall = 0;
      let uiBackoff = 0;
      const UI_BASE_INTERVAL = 3000;   // Mindestabstand 3s
      const UI_BACKOFF_MAX = 15000;    // maximal 15s Backoff
      
      let sid = "0000000000000000";
      let allAins = [];
      let deviceMap = {};
      
      let STOPPED = false;
      let schedulerHandle = null;
      let POLLING = false;
      
      let dect200List = [];
      let dect200Index = -1;
      let fastSchedulerHandle = null;
      let pingSchedulerHandle = null;
      
      let masterId = null;
      
      const errorCounter = {};
      
      function logError10x(group, message) {
          if (!errorCounter[group]) errorCounter[group] = 0;
          errorCounter[group]++;
          if (errorCounter[group] >= 10) {
              log(`[${hole_datum()}] FRITZ!DECT ERROR (${group}): ${message}`, "error");
              errorCounter[group] = 0;
          }
      }
      
      // Hilfsfunktionen
      function logInfo(msg) {
          if (STOPPED) return;
          log(`[${hole_datum()}] FRITZ!DECT: ${msg}`, "info");
      }
      
      async function smartCreateState(id, value, options = {}) {
          if (existsState(id)) return;
          if (STOPPED) return;
          await createState(id, value, options);
      }
      
      function smartSetState(id, value) {
          if (STOPPED) return;
      
          const state = getState(id);
          if (!state) return;
      
          const old = state.val;
          const isObject = v => v !== null && typeof v === "object";
      
          if (isObject(value)) {
              const newStr = JSON.stringify(value);
              let oldStr = null;
      
              if (typeof old === "string") oldStr = old;
              else if (isObject(old)) oldStr = JSON.stringify(old);
      
              if (newStr === oldStr) return;
              return setState(id, newStr, true);
          }
      
          if (old === value) return;
          setState(id, value, true);
      }
      
      function hardSetState(id, value) {
          if (STOPPED) return;
          setState(id, value, true);
      }
      
      function sleepMs(ms) {
          return new Promise(resolve => setTimeout(resolve, ms));
      }
      
      function hole_datum(ts = null) {
          const d = ts ? new Date(ts) : new Date();
          const t = String(d.getDate()).padStart(2, "0");
          const m = String(d.getMonth() + 1).padStart(2, "0");
          const y = d.getFullYear();
          const h = String(d.getHours()).padStart(2, "0");
          const mi = String(d.getMinutes()).padStart(2, "0");
          const s = String(d.getSeconds()).padStart(2, "0");
          return `${t}.${m}.${y} ${h}:${mi}:${s}`;
      }
      
      function fritzboxAlive() {
          return getState(`${ROOT}ping.alive`)?.val === true;
      }
      
      function extractAin(id) {
          return id.split(".").slice(-2, -1)[0].replace("DECT_", "");
      }
      
      async function isApiLocked(ain) {
          const s = await getStateAsync(`${ROOT}DECT_${ain}.lock`);
          return s?.val === true;
      }
      
      function setAndDetectChange(id, newVal) {
          const old = getState(id)?.val;
      
          if (typeof newVal === "object") {
              const oldStr = typeof old === "string" ? old : JSON.stringify(old);
              const newStr = JSON.stringify(newVal);
              if (oldStr !== newStr) {
                  smartSetState(id, newStr);
                  return true;
              }
              return false;
          }
      
          if (old !== newVal) {
              smartSetState(id, newVal);
              return true;
          }
          return false;
      }
      
      async function pingHost(host, stateBase) {
          if (!host) {
              smartSetState(`${stateBase}status`, 'ERROR');
              return;
          }
      
          const result = await ping.promise.probe(host, {timeout: 2, deadline: 2, packetSize: 16});
          if (!result) {
              smartSetState(`${stateBase}status`, 'ERROR');
              return;
          }
      
          const alive = result.alive;
          const time  = ErstelleZahl(parseFloat(result.time));
      
          if (alive === undefined || isNaN(time)) {
              smartSetState(`${stateBase}status`, 'ERROR');
              return;
          }
      
          smartSetState(`${stateBase}alive`, alive);
          smartSetState(`${stateBase}time`, time);
          smartSetState(`${stateBase}status`, 'OK');
      }
      
      async function initConfig() {
          await smartCreateState(`${ROOT}WRITE_ENABLED`, false, {name: "Write Enabled", type: "boolean", read: true, write: true});
          await smartCreateState(`${ROOT}Pollintervall`, schedulerIntervalSlow, {name: "Pollintervall", type: "number", unit: "s", read: true, write: true});
          await smartCreateState(`${ROOT}Dectrefresh`, schedulerIntervalFast, {name: "Dectrefresh", type: "number", unit: "s", read: true, write: true});
          await smartCreateState(`${ROOT}ping.alive`, false, {name: "Erreichbar", type: "boolean", read: true, write: false});
          await smartCreateState(`${ROOT}ping.time`, 0, {name: "Antwortzeit", unit: "ms", type: "number", read: true, write: false});
          await smartCreateState(`${ROOT}ping.status`, "", {name: "Status", type: "string", read: true, write: false});
      
          await sleepMs(300);
      
          const we = getState(`${ROOT}WRITE_ENABLED`);
          const slow = getState(`${ROOT}Pollintervall`);
          const fast = getState(`${ROOT}Dectrefresh`);
      
          if (we && typeof we.val === "boolean") WRITE_ENABLED = we.val;
          if (slow && typeof slow.val === "number") schedulerIntervalSlow = slow.val;
          if (fast && typeof fast.val === "number") schedulerIntervalFast = fast.val;
      }
      
      // SID / Login
      function resetErrorCounter(group) {
          errorCounter[group] = 0;
      }
      
      async function getSid() {
          if (STOPPED) return;
          try {
              const xml = await fritzRequest("get", `http://${fbIp}/login_sid.lua`, false);
              if (!xml || STOPPED) return;
      
              const sidMatch = xml.match(/<SID>([0-9a-f]+)<\/SID>/i);
              if (!sidMatch) {
                  logError10x("getSid:sidmatch", "SID nicht extrahierbar");
                  return;
              }
      
              sid = sidMatch[1];
              resetErrorCounter("getSid:sidmatch");
      
              if (sid === "0000000000000000") {
                  const challenge = xml.match(/<Challenge>(.*?)<\/Challenge>/i)?.[1];
                  if (!challenge) {
                      logError10x("getSid:challenge", "Challenge fehlt");
                      return;
                  }
                  resetErrorCounter("getSid:challenge");
      
                  const response = challenge + "-" + crypto.createHash("md5").update(challenge + "-" + pass, "utf16le").digest("hex");
                  const loginXml = await fritzRequest("get", `http://${fbIp}/login_sid.lua?username=${encodeURIComponent(user)}&response=${response}`, false);
                  if (!loginXml || STOPPED) return;
      
                  const sid2 = loginXml.match(/<SID>([0-9a-f]+)<\/SID>/i)?.[1];
                  if (!sid2 || sid2 === "0000000000000000") {
                      logError10x("getSid:login", "Login fehlgeschlagen");
                      return;
                  }
      
                  sid = sid2;
                  resetErrorCounter("getSid:login");
              }
              resetErrorCounter("getSid:request");
          } catch (err) {
              logError10x("getSid:request", "SID-Fehler: " + err);
          }
      }
      
      // HTTP-Request mit 403-Retry
      async function fritzRequest(method, url, autoRetry = true) {
          if (STOPPED || !fritzboxAlive()) return "";
          try {
              const res = await axios({ method, url, timeout: 30000 });
              resetErrorCounter("fritzRequest:axios");
              resetErrorCounter("fritzRequest:retry");
              return STOPPED ? "" : res.data;
          } catch (err) {
              if (STOPPED) return "";
      
              // 403 → Retry
              if (autoRetry && err.response && err.response.status === 403) {
                  logError10x("fritzRequest:403", "403 erhalten, versuche neuen SID");
      
                  sid = "0000000000000000";
                  await getSid();
                  if (STOPPED) return "";
      
                  const retryUrl = url.replace(/sid=[^&]+/, `sid=${sid}`);
                  try {
                      const res2 = await axios({ method, url: retryUrl, timeout: 30000 });
                      resetErrorCounter("fritzRequest:403");
                      return STOPPED ? "" : res2.data;
                  } catch (err2) {
                      logError10x("fritzRequest:retry", "Axios Retry: " + err2);
                      return "";
                  }
              }
              logError10x("fritzRequest:axios", "Axios: " + err);
              return "";
          }
      }
      
      // XML-Parser
      async function parseXml(xml) {
          if (!xml || STOPPED) return {};
          try {
              const result = await xml2js.parseStringPromise(xml, { explicitArray: false });
              resetErrorCounter("parseXml:parse");
              return result;
          } catch (err) {
              logError10x("parseXml:parse", "XML-Parsefehler: " + err);
              return {};
          }
      }
      
      async function parseDeviceAndGroupList(xml) {
          try {
              const json = await parseXml(xml);
              if (STOPPED) return { devices: [], groups: [] };
      
              const root = json.devicelist || {};
      
              let devices = root.device || [];
              if (!Array.isArray(devices)) devices = [devices];
      
              let groups = root.group || [];
              if (!Array.isArray(groups)) groups = [groups];
      
              resetErrorCounter("parseDeviceAndGroupList:parse");
              return { devices, groups };
          } catch (err) {
              logError10x("parseDeviceAndGroupList:parse", "Parse: " + err);
              return { devices: [], groups: [] };
          }
      }
      
      function detectDeviceType(dev) {
          if (dev.groupinfo || dev.$?.functionbitmask === "37504") {
              return "group";
          }
      
          const bitmask = parseInt(dev.$?.functionbitmask || dev.functionbitmask || 0);
          const has = (bit) => (bitmask & (1 << bit)) !== 0;
      
          if (has(6)) {
              return "hkr";
          }
      
          if (has(9) && has(7)) {
              return "dect200";
          }
      
          if (has(5)) {
              return "dect440";
          }
      
          if (has(18)) {
              return "blind";
          }
      
          if (has(20)) {
              return "humidity";
          }
      
          if (has(2) || has(16) || has(17)) {
              return "light";
          }
      
          // Fallback über Produktname (Sicherheitsebene)
          const name = (dev.$?.productname || dev.productname || "").toLowerCase();
      
          if (name.includes("energy 200") ||
              name.includes("fritz!dect 200") ||
              name.includes("smart energy 200") ||
              name.includes("dect 200")) return "dect200";
      
          if (name.includes("control 440") ||
              name.includes("fritz!dect 440") ||
              name.includes("dect 440")) return "dect440";
      
          if (name.includes("fritz!dect 301") ||
              name.includes("fritz!dect 302") ||
              name.includes("comet dect")) return "hkr";
      
          return "unknown";
      }
      
      async function loadDevices() {
          if (STOPPED) return;
      
          if (sid === "0000000000000000") {
              await getSid();
              if (STOPPED) return;
          }
      
          let xml = await fritzRequest("get", `http://${fbIp}/webservices/homeautoswitch.lua?sid=${sid}&switchcmd=getdevicelistinfos`);
          if (STOPPED) return;
      
          let { devices, groups } = await parseDeviceAndGroupList(xml);
          if ((!devices || devices.length === 0) && (!groups || groups.length === 0)) {
              sid = "0000000000000000";
              await getSid();
              if (STOPPED) return;
      
              xml = await fritzRequest("get", `http://${fbIp}/webservices/homeautoswitch.lua?sid=${sid}&switchcmd=getdevicelistinfos`);
              if (STOPPED) return;
              ({ devices, groups } = await parseDeviceAndGroupList(xml));
          }
      
          allAins = [];
          deviceMap = {};
      
          for (const dev of devices) {
              let ain = dev.$?.identifier || dev.identifier;
              if (!ain) continue;
      
              ain = ain.replace(/\s/g, "");
              const type = detectDeviceType(dev);
      
              allAins.push(ain);
              deviceMap[ain] = {ain, type, raw: dev,
                  meta: {
                      name: dev.name,
                      productname: dev.productname || dev.$?.productname,
                      manufacturer: dev.manufacturer || dev.$?.manufacturer,
                      fwversion: dev.fwversion || dev.$?.fwversion,
                      id: dev.$?.id,
                      present: dev.present,
                      switchtype: dev.switch?.switchtype,
                      txbusy: dev.txbusy
                  }
              };
          }
      
          for (const grp of groups) {
              let ain = grp.$?.identifier || grp.identifier;
              if (!ain) continue;
      
              ain = ain.replace(/\s/g, "");
              allAins.push(ain);
      
              deviceMap[ain] = {ain, type: "group", raw: grp, meta: {name: grp.name, productname: "Group", manufacturer: "AVM", fwversion: grp.$?.fwversion || null, id: grp.$?.id || null, present: grp.present, members: grp.groupinfo?.members || ""}};
          }
      }
      
      // State-Erstellung — DECT 200
      async function createDect200States(ain) {
          if (STOPPED) return;
      
          const devName = (deviceMap[ain] && deviceMap[ain].meta && deviceMap[ain].meta.name) ? deviceMap[ain].meta.name : `DECT_${ain}`;
          await smartCreateState(`${ROOT}DECT_${ain}`, "", {name: devName, read: true, write: false});
      
          const root = `${ROOT}DECT_${ain}.`;
          const states = {
              celsius:         { type: "number", unit: "°C", name: "Temperature" },
              devicelock:      { type: "boolean", name: "Device (Button)lock", default: false },
              energy:          { type: "number", unit: "Wh", name: "Energy consumption" },
              fwversion:       { type: "string", name: "Firmware Version" },
              id:              { type: "number", name: "Device ID" },
              lock:            { type: "boolean", name: "API Lock", default: true },
              manufacturer:    { type: "string", name: "Manufacturer" },
              mode:            { type: "string", name: "Switch Mode" },
              name:            { type: "string", name: "Device Name" },
              offset:          { type: "number", unit: "°C", name: "Temperature Offset" },
              power:           { type: "number", unit: "W", name: "actual Power" },
              present:         { type: "boolean", name: "device present" },
              productname:     { type: "string", name: "Product Name" },
              state:           { type: "boolean", write: true, name: "Switch Status and Control" },
              switchtype:      { type: "string", name: "Switch Type" },
              txbusy:          { type: "boolean", name: "Transmitting active" },
              voltage:         { type: "number", unit: "V", name: "actual Voltage" },
              ampere:          { type: "number", unit: "A", name: "actual Ampere" },
              lastchange:      { type: "string", name: "Last data change timestamp" },
              PollEnergyStats: { type: "number", name: "Last Poll EnergyStats" }
          };
      
          for (const id in states) {
              if (STOPPED) return;
              const cfg = states[id];
              const initialValue = cfg.default !== undefined ? cfg.default : null;
              await smartCreateState(root + id, initialValue, {name: cfg.name, type: cfg.type, read: true, write: cfg.write ?? false, unit: cfg.unit ?? undefined});
          }
      
          const energyStatsStates = {
              "energy_stats.countd":        { type: "number", unit: "days",   name: "Anzahl Tage" },
              "energy_stats.countm":        { type: "number", unit: "months", name: "Anzahl Monate" },
              "energy_stats.datatimed":     { type: "number",                 name: "Zeitpunkt Tagesstatistik" },
              "energy_stats.datatimem":     { type: "number",                 name: "Zeitpunkt Monatsstatistik" },
              "energy_stats.energy_dtd":    { type: "number", unit: "Wh",     name: "Energie heute" },
              "energy_stats.energy_mtd":    { type: "number", unit: "Wh",     name: "Energie aktueller Monat" },
              "energy_stats.energy_ytd":    { type: "number", unit: "Wh",     name: "Energie aktuelles Jahr" },
              "energy_stats.energy_last31d":{ type: "number", unit: "Wh",     name: "Energie letzte 31 Tage" },
              "energy_stats.energy_last12m":{ type: "number", unit: "Wh",     name: "Energie letzte 12 Monate" },
              "energy_stats.gridd":         { type: "number", unit: "s",      name: "Raster Tage" },
              "energy_stats.gridm":         { type: "number", unit: "s",      name: "Raster Monate" },
              "energy_stats.stats_days":    { type: "string",                 name: "Tagesstatistik (Array)" },
              "energy_stats.stats_months":  { type: "string",                 name: "Monatsstatistik (Array)" }
          };
      
          const powerStatsStates = {
              "power_stats.count":     { type: "number", unit: "counts", name: "Anzahl Messpunkte" },
              "power_stats.datetime":  { type: "number",                 name: "Zeitpunkt Statistik" },
              "power_stats.grid":      { type: "number", unit: "s",      name: "Raster" },
              "power_stats.stats":     { type: "string",                 name: "Leistungsstatistik (Array)" }
          };
      
          const voltageStatsStates = {
              "voltage_stats.count":    { type: "number", unit: "counts", name: "Anzahl Messpunkte" },
              "voltage_stats.datetime": { type: "number",                 name: "Zeitpunkt Statistik" },
              "voltage_stats.grid":     { type: "number", unit: "s",      name: "Raster" },
              "voltage_stats.stats":    { type: "string",                 name: "Spannungsstatistik (Array)" }
          };
      
          for (const id in energyStatsStates) {
              const cfg = energyStatsStates[id];
              await smartCreateState(root + id, null, {name: cfg.name, type: cfg.type, read: true, write: false, unit: cfg.unit ?? undefined});
          }
      
          for (const id in powerStatsStates) {
              const cfg = powerStatsStates[id];
              await smartCreateState(root + id, null, {name: cfg.name, type: cfg.type, read: true, write: false, unit: cfg.unit ?? undefined});
          }
      
          for (const id in voltageStatsStates) {
              const cfg = voltageStatsStates[id];
              await smartCreateState(root + id, null, {name: cfg.name, type: cfg.type, read: true, write: false, unit: cfg.unit ?? undefined});
          }
      }
      
      async function createHkrStates(ain) {
          if (STOPPED) return;
      
          const devName = (deviceMap[ain] && deviceMap[ain].meta && deviceMap[ain].meta.name) ? deviceMap[ain].meta.name : `DECT_${ain}`;
          await smartCreateState(`${ROOT}DECT_${ain}`, "", {name: devName, read: true, write: false});
      
          const root = `${ROOT}DECT_${ain}.`;
          const states = {
              absenk:                  { type: "number", unit: "°C", name: "reduced (night) temperature" },
              adaptiveHeatingActive:   { type: "boolean", name: "adaptive Heating active status" },
              adaptiveHeatingRunning:  { type: "boolean", name: "adaptive Heating running status" },
              battery:                 { type: "number", unit: "%", name: "Battery Charge State" },
              batterylow:              { type: "boolean", name: "Battery Low State" },
      
              boostactive:             { type: "boolean", write: true, name: "Boost active status and cmd" },
              boostactiveendtime:      { type: "string", name: "Boost active end time" },
              boostactivetime:         { type: "number", unit: "min", write: true, default: 30, name: "boost active time for cmd" },
              boostremaining:          { type: "number", unit: "min", write: false, default: 0, name: "Boost remaining time" },
      
              celsius:                 { type: "number", unit: "°C", name: "Temperature" },
              devicelock:              { type: "boolean", name: "device lock, button lock", default: false },
              endperiod:               { type: "string", name: "next time for Temp change" },
              errorcode:               { type: "number", name: "Error Code" },
              errorcodestring:         { type: "string", name: "Error Code String" },
              fwversion:               { type: "string", name: "Firmware Version" },
      
              hkrmode:                 { type: "number", write: true, name: "Thermostat operation mode (0=auto, 1=closed, 2=open)" },
      
              holidayactive:           { type: "boolean", name: "Holiday Active status" },
              id:                      { type: "number", name: "Device ID" },
              komfort:                 { type: "number", unit: "°C", name: "comfort temperature" },
              lasttarget:              { type: "number", unit: "°C", name: "last setting of target temp" },
              lock:                    { type: "boolean", name: "Thermostat UI/API lock", default: true },
              manufacturer:            { type: "string", name: "Manufacturer" },
              name:                    { type: "string", name: "Device Name" },
              offset:                  { type: "number", unit: "°C", name: "Temperature Offset" },
      
              operationlist:           { type: "string", name: "List of operation modes" },
              operationmode:           { type: "string", name: "Current operation mode" },
      
              present:                 { type: "boolean", name: "device present" },
              productname:             { type: "string", name: "Product Name" },
      
              setmodeauto:             { type: "boolean", write: true, name: "Switch MODE AUTO" },
              setmodeoff:              { type: "boolean", write: true, name: "Switch MODE OFF" },
              setmodeon:               { type: "boolean", write: true, name: "Switch MODE ON" },
      
              summeractive:            { type: "boolean", name: "summer active status" },
              tchange:                 { type: "number", unit: "°C", name: "Temp after next change" },
              tist:                    { type: "number", unit: "°C", name: "Actual temperature" },
              tsoll:                   { type: "number", unit: "°C", write: true, name: "Setpoint Temperature" },
              txbusy:                  { type: "boolean", name: "Transmitting active" },
      
              windowopenactiv:         { type: "boolean", write: true, name: "Window open status and cmd" },
              windowopenactiveendtime: { type: "string", name: "window open active end time" },
              windowopenactivetime:    { type: "number", unit: "min", write: true, default: 30, name: "window open active time for cmd" },
              windowremaining:         { type: "number", unit: "min", write: false, default: 0, name: "Window open remaining time" },
              lastchange:              { type: "string", name: "Last data change timestamp" }
          };
      
          for (const id in states) {
              const cfg = states[id];
      
              let initialValue = null;
              if (cfg.type === "boolean") initialValue = false;
              if (cfg.default !== undefined) initialValue = cfg.default;
      
              await smartCreateState(root + id, initialValue, {name: id, type: cfg.type, read: true, write: cfg.write ?? false, unit: cfg.unit ?? undefined});
          }
      }
      
      async function createDect440States(ain) {
          if (STOPPED) return;
      
          const devName = (deviceMap[ain] && deviceMap[ain].meta && deviceMap[ain].meta.name) ? deviceMap[ain].meta.name : `DECT_${ain}`;
          await smartCreateState(`${ROOT}DECT_${ain}`, "", {name: devName, read: true, write: false});
      
          const root = `${ROOT}DECT_${ain}.`;
          const mainStates = {
              battery:       { type: "number", unit: "%", name: "Battery Charge State" },
              batterylow:    { type: "boolean", name: "Battery Low State" },
              celsius:       { type: "number", unit: "°C", name: "Temperature" },
              fwversion:     { type: "string", name: "Firmware Version" },
              id:            { type: "number", name: "Device ID" },
              manufacturer:  { type: "string", name: "Manufacturer" },
              name:          { type: "string", name: "Device Name" },
              offset:        { type: "number", unit: "°C", name: "Temperature Offset" },
              present:       { type: "boolean", name: "device present" },
              productname:   { type: "string", name: "Product Name" },
              rel_humidity:  { type: "number", unit: "%", name: "relative Humidity" },
              txbusy:        { type: "boolean", name: "Transmitting active" },
              lastchange:    { type: "string", name: "Last data change timestamp" }
          };
      
          for (const id in mainStates) {
              if (STOPPED) return;
              const cfg = mainStates[id];
              await smartCreateState(root + id, null, {name: cfg.name, type: cfg.type, read: true, write: false, unit: cfg.unit ?? undefined});
          }
      
          const buttonIds = ["1", "3", "5", "7"];
          for (const b of buttonIds) {
              if (STOPPED) return;
      
              const base = `${root}button.${ain}-${b}`;
              await smartCreateState(base + ".id", null, {name: "Button ID", type: "number", read: true, write: false});
              await smartCreateState(base + ".lastpressedtimestamp", null, {name: "last button Time Stamp", type: "string", read: true, write: false});
              await smartCreateState(base + ".name", null, {name: "Button Name", type: "string", read: true, write: false});
          }
      }
      
      async function createDectGroupStates(ain) {
          if (STOPPED) return;
      
          const devName = (deviceMap[ain] && deviceMap[ain].meta && deviceMap[ain].meta.name) ? deviceMap[ain].meta.name : `DECT_grp${ain}`;
          await smartCreateState(`${ROOT}DECT_grp${ain}`, "", {name: devName, read: true, write: false});
      
          const root = `${ROOT}DECT_grp${ain}.`;
          const states = {
              name:        { type: "string",  name: "Group Name" },
              present:     { type: "boolean", name: "Group present" },
              members:     { type: "string",  name: "Group Members" },
              mode:        { type: "string",  name: "Switch Mode" },
      
              power:       { type: "number",  unit: "W",  name: "actual Power" },
              voltage:     { type: "number",  unit: "V",  name: "actual Voltage" },
              ampere:      { type: "number",  unit: "A",  name: "actual Ampere" },
              energy:      { type: "number",  unit: "Wh", name: "Energy consumption" },
      
              lastchange:  { type: "string",  name: "Last data change timestamp" }
          };
      
          for (const id in states) {
              if (STOPPED) return;
      
              const cfg = states[id];
              const initialValue = cfg.default !== undefined ? cfg.default : null;
              await smartCreateState(root + id, initialValue, {name: cfg.name, type: cfg.type, read: true, write: false, unit: cfg.unit ?? undefined});
          }
      }
      
      // Parser — HKR (DECT 301/302)
      async function parseHkrDevice(ain, dev, isInitial = false) {
          if (STOPPED) return;
      
          const HKR_ERRORCODES = {
              0: "kein Fehler",
              1: "Keine Adaptierung möglich. Gerät korrekt am Heizkörper montiert?",
              2: "Ventilhub zu kurz oder Batterieleistung zu schwach. Ventilstößel per Hand mehrmals öffnen und schließen oder neue Batterien einsetzen.",
              3: "Keine Ventilbewegung möglich. Ventilstößel frei?",
              4: "Die Installation wird gerade vorbereitet.",
              5: "Der Heizkörperregler ist im Installationsmodus und kann auf das Heizungsventil montiert werden.",
              6: "Der Heizkörperregler passt sich nun an den Hub des Heizungsventils an."
          };
      
          const root  = `${ROOT}DECT_${ain}.`;
          const h     = dev.hkr         || {};
          const t     = dev.temperature || {};
          const attr  = dev.$           || {};
          const entry = deviceMap[ain]  || {};
          const meta  = entry.meta      || {};
      
          const setMeta = isInitial ? hardSetState : smartSetState;
      
          // Basisdaten
          if (dev.name !== undefined)              setMeta(root + "name",         String(dev.name));
          if (attr.productname !== undefined)      setMeta(root + "productname",  String(attr.productname  || meta.productname  || ""));
          if (attr.manufacturer !== undefined)     setMeta(root + "manufacturer", String(attr.manufacturer || meta.manufacturer || ""));
          if (attr.fwversion !== undefined)        setMeta(root + "fwversion",    String(attr.fwversion    || meta.fwversion    || ""));
          if (attr.id !== undefined)               setMeta(root + "id",           Number(attr.id));
          if (dev.present !== undefined)           setMeta(root + "present",      dev.present === "1");
      
          let changed = false;
      
          // Temperaturwerte
          if (t.celsius !== undefined) changed = setAndDetectChange(root + "celsius", Number(t.celsius) / 10) || changed;
      
          if (t.offset !== undefined) changed = setAndDetectChange(root + "offset", Number(t.offset) / 10) || changed;
      
          if (h.tist !== undefined) {
              const tist = Number(h.tist) / 2;
              if (!isNaN(tist)) changed = setAndDetectChange(root + "tist", tist) || changed;
          }
      
          // Komfort / Absenk
          let komfort = null;
          let absenk  = null;
      
          if (h.komfort !== undefined) {
              komfort = Number(h.komfort) / 2;
              if (!isNaN(komfort)) changed = setAndDetectChange(root + "komfort", komfort) || changed;
          }
      
          if (h.absenk !== undefined) {
              absenk = Number(h.absenk) / 2;
              if (!isNaN(absenk)) changed = setAndDetectChange(root + "absenk", absenk) || changed;
          }
      
          // Roh-tsoll korrekt interpretieren (OHNE Off/On Mapping)
          let rawTsoll = null;
          let tsoll    = null;
      
          if (h.tsoll !== undefined) rawTsoll = Number(h.tsoll);
      
          const boostActive   = h.boostactive === "1";
          const holidayActive = h.holidayactive === "1";
          const windowActive  = h.windowopenactiv === "1";
      
          if (boostActive && komfort !== null) {
              tsoll = komfort;
          }
          else if ((holidayActive || windowActive) && absenk !== null) {
              tsoll = absenk;
          }
          else if (rawTsoll !== null && !isNaN(rawTsoll)) {
              // WICHTIG:
              // 253 (Off), 254 (On), 255 (Auto)
              // dürfen KEIN tsoll setzen!
              if (rawTsoll >= 16 && rawTsoll <= 56) {
                  tsoll = rawTsoll / 2;
              }
          }
      
          // Sicherheitsfilter
          if (tsoll !== null) {
              if (isNaN(tsoll) || tsoll < 8 || tsoll > 28) {
                  tsoll = null;
              }
          }
      
          // Statuswerte
          const boolMap = ["lock", "devicelock", "windowopenactiv", "batterylow", "summeractive", "holidayactive", "adaptiveHeatingActive", "adaptiveHeatingRunning", "boostactive"];
          for (const key of boolMap) {
              if (h[key] !== undefined)  changed = setAndDetectChange(root + key, h[key] === "1") || changed;
          }
      
          const errorcode = Number(h.errorcode);
          if (h.errorcode !== undefined) changed = setAndDetectChange(root + "errorcode", errorcode) || changed;
          const errortext = HKR_ERRORCODES[errorcode] || "Unbekannter Fehlercode";
          smartSetState(root + "errorcodestring", errortext);
      
          if (h.battery !== undefined)   changed = setAndDetectChange(root + "battery", Number(h.battery)) || changed;
          if (dev.txbusy !== undefined)  changed = setAndDetectChange(root + "txbusy", dev.txbusy === "1") || changed;
      
          // Boost / Window Zeiten
          function handleTimeBlock(sourceKey, targetKeyTime, targetKeyRemain) {
              if (h[sourceKey] === undefined) return;
      
              const sec = Number(h[sourceKey]);
              const ts  = sec > 0 ? hole_datum(sec * 1000) : "";
      
              changed = setAndDetectChange(root + targetKeyTime, ts) || changed;
      
              if (sec > 0) {
                  const nowSec = Math.floor(Date.now() / 1000);
                  const diff   = sec - nowSec;
                  changed = setAndDetectChange(root + targetKeyRemain, diff > 0 ? Math.round(diff / 60) : 0) || changed;
              } else {
                  changed = setAndDetectChange(root + targetKeyRemain, 0) || changed;
              }
          }
      
          handleTimeBlock("boostactiveendtime",       "boostactiveendtime",       "boostremaining");
          handleTimeBlock("windowopenactiveendtime",  "windowopenactiveendtime",  "windowremaining");
      
          // NextChange
          const nc = h.nextchange || dev.nextchange || {};
      
          if (nc.tchange !== undefined) {
              const tchange = Number(nc.tchange) / 2;
              if (!isNaN(tchange)) changed = setAndDetectChange(root + "tchange", tchange) || changed;
          }
      
          if (nc.endperiod !== undefined) {
              const sec = Number(nc.endperiod);
              const ts  = sec > 0 ? hole_datum(sec * 1000) : "";
              changed = setAndDetectChange(root + "endperiod", ts) || changed;
          }
      
          // Betriebsmodus bestimmen (OHNE tsoll Manipulation)
          setMeta(root + "operationlist", "Auto,On,Off,Holiday");
      
          let hkrmode = 0;
          let opMode  = "Auto";
      
          if (rawTsoll === 253) {
              hkrmode = 1;
              opMode  = "Off";
          } else if (rawTsoll === 254) {
              hkrmode = 2;
              opMode  = "On";
          } else if (boostActive) {
              opMode = "Boost";
          } else if (holidayActive) {
              opMode = "Holiday";
          } else if (windowActive) {
              opMode = "WindowOpen";
          } else if (tsoll !== null) {
              if (komfort !== null && tsoll === komfort) {
                  opMode = "Comfort";
              } else if (absenk !== null && tsoll === absenk) {
                  opMode = "Night";
              } else {
                  opMode = "Auto";
              }
          }
      
          changed = setAndDetectChange(root + "hkrmode", hkrmode) || changed;
          changed = setAndDetectChange(root + "operationmode", opMode) || changed;
      
          // tsoll nur bei echter Temperatur schreiben
          if (tsoll !== null) {
              smartSetState(root + "tsoll", tsoll);
              smartSetState(root + "lasttarget", tsoll);
          }
      
          // Änderungszeit
          if (changed) {
              smartSetState(root + "lastchange", hole_datum());
          }
      }
      
      // Parser — DECT 200
      async function parseDect200Device(ain, dev, isInitial = false) {
          if (STOPPED) return;
      
          const root = `${ROOT}DECT_${ain}.`;
          const e = dev.powermeter || {};
          const t = dev.temperature || {};
          const sw = dev.switch || {};
          const attr = dev.$ || {};
      
          const setMeta = isInitial ? hardSetState : smartSetState;
      
          if (dev.name !== undefined) setMeta(root + "name", String(dev.name));
          if (attr.productname !== undefined) setMeta(root + "productname", String(attr.productname));
          if (attr.manufacturer !== undefined) setMeta(root + "manufacturer", String(attr.manufacturer));
          if (attr.fwversion !== undefined) setMeta(root + "fwversion", String(attr.fwversion));
          if (attr.id !== undefined) setMeta(root + "id", Number(attr.id));
          if (dev.present !== undefined) setMeta(root + "present", dev.present === "1");
      
          let changed = false;
      
          if (t.celsius !== undefined) changed = setAndDetectChange(root + "celsius", Number(t.celsius) / 10) || changed;
          if (t.offset !== undefined)  changed = setAndDetectChange(root + "offset", Number(t.offset) / 10) || changed;
          if (e.power !== undefined)   changed = setAndDetectChange(root + "power", Number(e.power) / 1000) || changed;
          if (e.energy !== undefined)  changed = setAndDetectChange(root + "energy", Number(e.energy)) || changed;
          if (e.voltage !== undefined) changed = setAndDetectChange(root + "voltage", Number(e.voltage) / 1000) || changed;
      
          if (e.power !== undefined && e.voltage !== undefined) {
              const watt = Number(e.power) / 1000;
              const volt = Number(e.voltage) / 1000;
      
              let ampRounded = 0;
              if (volt > 0) {
                  const amp = watt / volt;
                  ampRounded = Math.round(amp * 100) / 100;
              }
              changed = setAndDetectChange(root + "ampere", ampRounded) || changed;
          }
      
          if (sw.state !== undefined) changed = setAndDetectChange(root + "state", sw.state === "1") || changed;
          if (sw.mode !== undefined)  changed = setAndDetectChange(root + "mode", String(sw.mode)) || changed;
          if (sw.lock !== undefined)  changed = setAndDetectChange(root + "lock", sw.lock === "1") || changed;
          if (sw.devicelock !== undefined) changed = setAndDetectChange(root + "devicelock", sw.devicelock === "1") || changed;
      
          smartSetState(root + "switchtype", "simpleonoff");
      
          if (dev.txbusy !== undefined) changed = setAndDetectChange(root + "txbusy", dev.txbusy === "1") || changed;
      
          if (changed) smartSetState(root + "lastchange", hole_datum());
      }
      
      // Parser für EnergyStats_10 (nur DECT200)
      async function parseEnergyStats(ain, raw) {
          if (STOPPED || !raw) return;
      
          let json = raw;
          if (!json || typeof json !== "object") {
              logError10x("EnergyStats:parse", "Ungültige Antwort, kein Objekt");
              return;
          }
          resetErrorCounter("EnergyStats:parse");
      
          const root = `${ROOT}DECT_${ain}.`;
      
          // ENERGY_STATS (nur das, was im RAW vorhanden ist)
          if (json.sum_Day !== undefined) {
              smartSetState(root + "energy_stats.energy_dtd", json.sum_Day);
          }
          if (json.sum_Month !== undefined) {
              smartSetState(root + "energy_stats.energy_mtd", json.sum_Month);
          }
          if (json.sum_Year !== undefined) {
              smartSetState(root + "energy_stats.energy_ytd", json.sum_Year);
          }
      
          if (json.CurrentDateInSec !== undefined) {
              const ts = Number(json.CurrentDateInSec) * 1000;
              smartSetState(root + "energy_stats.datatimed", ts);
              smartSetState(root + "energy_stats.datatimem", ts);
          }
      
          // POWER_STATS (EnergyStat = Leistung in mW)
          const ps = json.EnergyStat || {};
          if (ps.anzahl !== undefined) {
              smartSetState(root + "power_stats.count", ps.anzahl);
          }
          if (ps.datatimestamp !== undefined) {
              smartSetState(root + "power_stats.datetime", Number(ps.datatimestamp) * 1000);
          }
          if (ps.times_type !== undefined) {
              smartSetState(root + "power_stats.grid", ps.times_type);
          }
          if (Array.isArray(ps.values)) {
              // mW → W
              smartSetState(root + "power_stats.stats", JSON.stringify(ps.values.map(v => v / 1000)));
          }
      
          // VOLTAGE_STATS (VoltageStat = Spannung in mV)
          const vs = json.VoltageStat || {};
          if (vs.anzahl !== undefined) {
              smartSetState(root + "voltage_stats.count", vs.anzahl);
          }
          if (vs.datatimestamp !== undefined) {
              smartSetState(root + "voltage_stats.datetime", Number(vs.datatimestamp) * 1000);
          }
          if (vs.times_type !== undefined) {
              smartSetState(root + "voltage_stats.grid", vs.times_type);
          }
          if (Array.isArray(vs.values)) {
              smartSetState(root + "voltage_stats.stats", JSON.stringify(vs.values.map(v => v / 1000)));
          }
      }
      
      async function updateBasicDeviceStats(ain) {
          try {
              const url = `http://${fbIp}/webservices/homeautoswitch.lua?switchcmd=getbasicdevicestats&ain=${ain}&sid=${sid}`;
              const xml = await fritzRequest("get", url);
              if (!xml) return;
      
              const json = await parseXml(xml);
              const stats = json?.devicestats?.energy?.stats;
              if (!stats || !Array.isArray(stats)) return;
      
              const base = `${ROOT}DECT_${ain}.energy_stats.`;
      
              // Monatsstatistik (stats[0])
              if (stats[0]) {
                  const m = stats[0];
                  const arr = m._?.split(",").map(Number) || [];
      
                  smartSetState(base + "countm", Number(m.$?.count || 0));
                  smartSetState(base + "gridm", Number(m.$?.grid || 0));
                  smartSetState(base + "datatimem", Number(m.$?.datatime || 0) * 1000);
      
                  smartSetState(base + "stats_months", JSON.stringify(arr));
                  smartSetState(base + "energy_last12m", arr.reduce((a, b) => a + b, 0));
      
                  const month = new Date().getMonth() + 1;
                  smartSetState(base + "energy_ytd", arr.slice(0, month).reduce((a, b) => a + b, 0));
              }
      
              // Tagesstatistik (stats[1])
              if (stats[1]) {
                  const d = stats[1];
                  const arr = d._?.split(",").map(Number) || [];
      
                  smartSetState(base + "countd", Number(d.$?.count || 0));
                  smartSetState(base + "gridd", Number(d.$?.grid || 0));
                  smartSetState(base + "datatimed", Number(d.$?.datatime || 0) * 1000);
      
                  smartSetState(base + "stats_days", JSON.stringify(arr));
                  smartSetState(base + "energy_last31d", arr.reduce((a, b) => a + b, 0));
      
                  const day = new Date().getDate();
                  smartSetState(base + "energy_mtd", arr.slice(0, day).reduce((a, b) => a + b, 0));
                  smartSetState(base + "energy_dtd", arr[0] || 0);
              }
          } catch (err) {
              logError10x("updateBasicDeviceStats", err);
          }
      }
      
      // Parser — DECT 440
      async function parseDect440Device(ain, dev, isInitial = false) {
          if (STOPPED) return;
      
          const root = `${ROOT}DECT_${ain}.`;
          const t = dev.temperature || {};
          const hum = dev.humidity || {};
          const attr = dev.$ || {};
          const entry = deviceMap[ain] || {};
          const meta = entry.meta || {};
      
          const setMeta = isInitial ? hardSetState : smartSetState;
      
          setMeta(root + "name", dev.name ?? meta.name ?? null);
          setMeta(root + "productname", dev.productname ?? meta.productname ?? null);
          setMeta(root + "manufacturer", dev.manufacturer ?? meta.manufacturer ?? null);
          setMeta(root + "fwversion", dev.fwversion ?? meta.fwversion ?? null);
      
          if (attr.id !== undefined) setMeta(root + "id", Number(attr.id));
          else if (meta.id !== undefined) setMeta(root + "id", Number(meta.id));
      
          if (dev.present !== undefined) {
              setMeta(root + "present", dev.present === "1");
          } else if (meta.present !== undefined) {
              setMeta(root + "present", meta.present === "1" || meta.present === 1 || meta.present === true);
          }
      
          let changed = false;
          if (t.celsius !== undefined) changed = setAndDetectChange(root + "celsius", Number(t.celsius) / 10) || changed;
          if (t.offset !== undefined)  changed = setAndDetectChange(root + "offset", Number(t.offset) / 10) || changed;
          if (hum.rel_humidity !== undefined) changed = setAndDetectChange(root + "rel_humidity", Number(hum.rel_humidity)) || changed;
          if (dev.battery !== undefined) changed = setAndDetectChange(root + "battery", Number(dev.battery)) || changed;
          if (dev.batterylow !== undefined) changed = setAndDetectChange(root + "batterylow", dev.batterylow === "1") || changed;
      
          const txb = dev.txbusy ?? meta.txbusy;
          if (txb !== undefined) changed = setAndDetectChange(root + "txbusy", txb === "1" || txb === 1 || txb === true) || changed;
      
          if (changed) {
              smartSetState(root + "lastchange", hole_datum());
          }
      
          const buttons = dev.button || [];
          const arr = Array.isArray(buttons) ? buttons : [buttons];
          const buttonIds = ["1", "3", "5", "7"];
      
          for (let i = 0; i < buttonIds.length; i++) {
              const bId = buttonIds[i];
              const bObj = arr[i];
              if (!bObj) continue;
      
              const base = `${root}button.${ain}-${bId}`;
              const src = bObj;
      
              if (src.$?.id !== undefined) {
                  smartSetState(base + ".id", Number(src.$.id));
              }
              if (src.lastpressedtimestamp !== undefined) {
                  const ts = src.lastpressedtimestamp ? Number(src.lastpressedtimestamp) * 1000 : 0;
                  smartSetState(base + ".lastpressedtimestamp", ts ? hole_datum(ts) : "");
              }
              if (src.name !== undefined && src.name !== null && src.name !== "") {
                  smartSetState(base + ".name", String(src.name));
              }
          }
      }
      
      async function parseDectGroupDevice(ain, grp) {
          if (STOPPED || !grp) return;
      
          const root = `${ROOT}DECT_grp${ain}.`;
          let changed = false;
      
          smartSetState(root + "name", grp.name || "");
          smartSetState(root + "present", grp.present === "1");
      
          const members = grp.groupinfo?.members || "";
          smartSetState(root + "members", members);
          smartSetState(root + "mode", grp.switch?.mode || "manuell");
      
          if (grp.powermeter) {
              const power = Number(grp.powermeter.power) / 1000;
              const voltage = Number(grp.powermeter.voltage) / 1000;
              const energy = Number(grp.powermeter.energy);
      
              changed = setAndDetectChange(root + "power", power) || changed;
              changed = setAndDetectChange(root + "voltage", voltage) || changed;
              changed = setAndDetectChange(root + "energy", energy) || changed;
      
              if (voltage > 0) {
                  const ampere = power / voltage;
                  const ampRounded = Math.round(ampere * 100) / 100;
                  changed = setAndDetectChange(root + "ampere", ampRounded) || changed;
              }
          }
      
          if (changed) smartSetState(root + "lastchange", hole_datum());
      }
      
      async function pollAll(isInitial = false) {
          if (STOPPED || POLLING || !fritzboxAlive()) return;
          POLLING = true;
      
          try {
              if (sid === "0000000000000000") await getSid();
              if (STOPPED) return;
      
              const xml = await fritzRequest("get", `http://${fbIp}/webservices/homeautoswitch.lua?sid=${sid}&switchcmd=getdevicelistinfos`);
              if (STOPPED || !xml) return;
      
              const { devices, groups } = await parseDeviceAndGroupList(xml);
      
              const list = [...devices, ...groups];
              await updateMasterDevice(devices);
              if (!list.length) {
                  resetErrorCounter("pollAll:parse");
                  return;
              }
      
              for (const dev of list) {
                  if (STOPPED) return;
      
                  let ain = dev.$?.identifier || dev.identifier;
                  if (!ain) continue;
                  ain = ain.replace(/\s/g, "");
      
                  const type = detectDeviceType(dev);
      
                  if (type === "hkr") {
                      await parseHkrDevice(ain, dev, isInitial);
                  } else if (type === "dect200") {
                      await parseDect200Device(ain, dev, isInitial);
                  } else if (type === "dect440") {
                      await parseDect440Device(ain, dev, isInitial);
                  } else if (type === "group") {
                      await parseDectGroupDevice(ain, dev);
                  }
              }
      
              dect200List = [];
              for (const dev of devices) {
                  let ain = dev.$?.identifier || dev.identifier;
                  if (!ain) continue;
                  ain = ain.replace(/\s/g, "");
      
                  const type = detectDeviceType(dev);
                  if (type === "dect200" && dev.present === "1") {
                      dect200List.push(ain);
                  }
              }
              resetErrorCounter("pollAll:parse");
          } catch (err) {
              logError10x("pollAll:parse", "pollAll(): " + err);
          } finally {
              POLLING = false;
          }
      }
      
      async function initialPoll() {
          if (STOPPED) return;
          logInfo("Führe initialen Poll durch...");
          await pollAll(true);
          logInfo("Initialer Poll abgeschlossen.");
      }
      
      function resetErrorCounter(group) {
          errorCounter[group] = 0;
      }
      
      function setupOnHandlers() {
          if (ON_HANDLERS_ACTIVE) return;
          ON_HANDLERS_ACTIVE = true;
      
          on({ id: new RegExp("^" + ROOT.replace(/\./g, "\\.") + "DECT_[0-9]+\\.state$"), change: "any", ack: false }, async obj => {
              try {
                  if (STOPPED || !WRITE_ENABLED) return;
                  if (!obj || obj.state === undefined) return;
                  if (obj.state.ack) return;
      
                  const id  = obj.id;
                  const val = obj.state.val;
                  const ain = extractAin(id);
                  if (!ain) return;
      
                  if (await isApiLocked(ain)) {
                      const old = obj.oldState?.val;
                      if (old !== undefined) smartSetState(id, old);
                      return;
                  }
      
                  const cmd = val ? "setswitchon" : "setswitchoff";
                  try {
                      if (sid === "0000000000000000") await getSid();
                      if (STOPPED) return;
      
                      await fritzRequest("get", `http://${fbIp}/webservices/homeautoswitch.lua?sid=${sid}&switchcmd=${cmd}&ain=${encodeURIComponent(ain)}`);
                      resetErrorCounter("DECT200-Write");
                  } catch (err) {
                      logError10x("DECT200-Write", `Schalten DECT200 (${ain}): ${err}`);
                  }
              } catch (err) {
                  logError10x("DECT200-Handler", `Uncaught Handler Error: ${err}`);
              }
          });
      
          on({id: new RegExp("^" + ROOT.replace(/\./g, "\\.") + "DECT_[0-9]+\\.(tsoll|setmodeauto|setmodeoff|setmodeon|hkrmode|boostactivetime|windowopenactivetime)$"), change: "any", ack: false}, async obj => {
              if (STOPPED || !WRITE_ENABLED) return;
              if (!obj || obj.state === undefined) return;
              if (obj.state.ack) return;
      
              const id  = obj.id;
              const val = obj.state.val;
              const ain = extractAin(id);
              if (!ain) return;
      
              if (await isApiLocked(ain)) {
                  const old = obj.oldState?.val;
                  if (old !== undefined) smartSetState(id, old);
                  return;
              }
      
              const dp = id.split(".").slice(-1)[0];
              try {
                  await writeHkr(ain, dp, val);
                  resetErrorCounter("HKR-Write");
              } catch (err) {
                  logError10x("HKR-Write", `HKR-Write (${ain}/${dp}): ${err}`);
              }
          });
      
          on({ id: `${ROOT}WRITE_ENABLED`, change: "ne" }, obj => {
              if (!obj || obj.state === undefined) return;
              WRITE_ENABLED = obj.state.val === true;
          });
      
          on({id:`${ROOT}ping.alive`, change:"ne"}, obj => {
              if (obj.state.val === true) {
                  sid = "0000000000000000";
                  pollAll(true);
              }
      
          });
      }
      
      async function setBoostDuration(ain, minutes) {
          if (await isApiLocked(ain)) {
              const id = `${ROOT}DECT_${ain}.boostactivetime`;
              const old = getState(id)?.val;
              if (old !== undefined) smartSetState(id, old);
              return;
          }
      
          const now = Math.floor(Date.now() / 1000);
          const endTimestamp = now + (minutes * 60);
      
          const url = `http://${fbIp}/webservices/homeautoswitch.lua?sid=${sid}&ain=${encodeURIComponent(ain)}&switchcmd=sethkrboost&endtimestamp=${endTimestamp}`;
      
          try {
              const result = await fritzRequest("get", url);
              resetErrorCounter("setBoostDuration:write");
              return result;
          } catch (err) {
              // 403 → Retry
              if (err.response && err.response.status === 403) {
                  logError10x("setBoostDuration:403", String(err));
      
                  await getSid();
                  const retryUrl = url.replace(/sid=[^&]+/, `sid=${sid}`);
      
                  const result2 = await fritzRequest("get", retryUrl);
                  resetErrorCounter("setBoostDuration:403");
                  return result2;
              }
              logError10x("setBoostDuration:write", String(err));
          }
      }
      
      async function setWindowOpenDuration(ain, minutes) {
          if (await isApiLocked(ain)) {
              const id = `${ROOT}DECT_${ain}.windowopenactivetime`;
              const old = getState(id)?.val;
              if (old !== undefined) smartSetState(id, old);
              return;
          }
      
          const now = Math.floor(Date.now() / 1000);
          const endTimestamp = now + (minutes * 60);
      
          const url = `http://${fbIp}/webservices/homeautoswitch.lua?sid=${sid}&ain=${encodeURIComponent(ain)}&switchcmd=sethkrwindowopen&endtimestamp=${endTimestamp}`;
          try {
              const result = await fritzRequest("get", url);
              resetErrorCounter("setWindowOpenDuration:write");
              return result;
          } catch (err) {
              // 403 → Retry
              if (err.response && err.response.status === 403) {
                  logError10x("setWindowOpenDuration:403", String(err));
      
                  await getSid();
                  const retryUrl = url.replace(/sid=[^&]+/, `sid=${sid}`);
      
                  const result2 = await fritzRequest("get", retryUrl);
                  resetErrorCounter("setWindowOpenDuration:403");
                  return result2;
              }
              logError10x("setWindowOpenDuration:write", String(err));
          }
      }
      
      async function writeHkr(ain, dp, value) {
          if (STOPPED) return;
      
          if (await isApiLocked(ain)) {
              const id = `${ROOT}DECT_${ain}.${dp}`;
              const old = getState(id)?.val;
              if (old !== undefined) smartSetState(id, old);
              return;
          }
      
          if (!sid || sid === "0000000000000000") await getSid();
          if (STOPPED) return;
      
          const fritzAin = encodeURIComponent(ain);
          const baseUrl = `http://${fbIp}/webservices/homeautoswitch.lua?sid=${sid}&ain=${fritzAin}`;
      
          async function sendFritzRequest(url) {
              try {
                  const result = await fritzRequest("get", url);
                  resetErrorCounter("writeHkr:write");
                  return result;
              } catch (err) {
                  // 403 → Retry
                  if (err.response && err.response.status === 403) {
                      logError10x("writeHkr:403", String(err));
      
                      await getSid();
                      const newUrl = url.replace(/sid=[^&]+/, `sid=${sid}`);
      
                      const result2 = await fritzRequest("get", newUrl);
                      resetErrorCounter("writeHkr:403");
                      return result2;
                  }
                  logError10x("writeHkr:write", String(err));
              }
          }
      
          switch (dp) {
              case "tsoll": {
                  const v = Number(value);
      
                  if (isNaN(v)) return;
                  if (v < 8 || v > 28) return;
      
                  const raw = Math.round(v * 2);
      
                  if (raw < 16 || raw > 56) return; // absoluter Schutz
      
                  await sendFritzRequest(`${baseUrl}&switchcmd=sethkrtsoll&param=${raw}`);
                  return;
              }
      
              case "setmodeauto":
                  if (value === true) {
                      smartSetState(`${ROOT}DECT_${ain}.setmodeauto`, false);
      
                      const last = await getStateAsync(`${ROOT}DECT_${ain}.lasttarget`);
                      if (!last || last.val === null) return;
      
                      const temp = Number(last.val);
                      if (isNaN(temp)) return;
                      if (temp < 8 || temp > 28) return;
      
                      const raw = Math.round(temp * 2);
      
                      await sendFritzRequest(`${baseUrl}&switchcmd=sethkrtsoll&param=${raw}`);
                  }
                  return;
      
              case "setmodeoff":
                  if (value === true) {
                      smartSetState(`${ROOT}DECT_${ain}.setmodeoff`, false);
                      await sendFritzRequest(`${baseUrl}&switchcmd=sethkrtsoll&param=253`);
                      smartSetState(`${ROOT}DECT_${ain}.operationmode`, "Off");
                      smartSetState(`${ROOT}DECT_${ain}.hkrmode`, 1);
                  }
                  return;
      
              case "setmodeon":
                  if (value === true) {
                      smartSetState(`${ROOT}DECT_${ain}.setmodeon`, false);
                      await sendFritzRequest(`${baseUrl}&switchcmd=sethkrtsoll&param=254`);
                      smartSetState(`${ROOT}DECT_${ain}.operationmode`, "On");
                      smartSetState(`${ROOT}DECT_${ain}.hkrmode`, 2);
                  }
                  return;
      
              case "hkrmode": {
                  const m = Number(value);
                  if (m === 0) await sendFritzRequest(`${baseUrl}&switchcmd=sethkrtsoll&param=255`);
                  if (m === 1) await sendFritzRequest(`${baseUrl}&switchcmd=sethkrtsoll&param=253`);
                  if (m === 2) await sendFritzRequest(`${baseUrl}&switchcmd=sethkrtsoll&param=254`);
                  return;
              }
      
              case "boostactivetime": {
                  const min = Number(value);
                  if (isNaN(min) || min < 1 || min > 1440) return;
                  await setBoostDuration(ain, min);
                  return;
              }
      
              case "windowopenactivetime": {
                  const min = Number(value);
                  if (isNaN(min) || min < 1 || min > 1440) return;
                  await setWindowOpenDuration(ain, min);
                  return;
              }
          }
      }
      
      // Round-Robin Master über DECT200
      function rotateMaster() {
          if (dect200List.length === 0) {
              masterId = null;
              return;
          }
      
          dect200Index++;
          if (dect200Index >= dect200List.length) {
              dect200Index = 0;
          }
      
          const ain = dect200List[dect200Index];
          const dev = deviceMap[ain];
          if (!dev || !dev.meta) return;
      
          masterId = dev.meta.id;
      }
      
      function startPollScheduler() {
          if (schedulerHandle) return;
          schedulerHandle = schedule(`*/${schedulerIntervalSlow} * * * * *`, async () => {
              if (STOPPED) return;
              await pollAll(false);
              if (masterId) {
                  await startWebUiEmulation(masterId);
              }
          });
          logInfo(`Poll gestartet (Intervall: ${schedulerIntervalSlow} Sekunden).`);
      }
      
      async function safeStartWebUiEmulation(ain) {
          const now = Date.now();
      
          if (now - lastUiCall < UI_BASE_INTERVAL + uiBackoff) return;
          lastUiCall = now;
          try {
              await startWebUiEmulation(ain);
              resetErrorCounter("WebUI:emulation");
              uiBackoff = 0;
          } catch (err) {
              uiBackoff = Math.min(uiBackoff + 2000, UI_BACKOFF_MAX);
              logError10x("WebUI:emulation", `WebUI-Emulation Fehler (${ain}): ${err.message}`);
          }
      }
      
      function startDECTRefreshScheduler() {
          if (fastSchedulerHandle) return;
      
          fastSchedulerHandle = schedule(`*/${schedulerIntervalFast} * * * * *`, async () => {
              if (STOPPED) return;
      
              rotateMaster();
              if (masterId) {
                  safeStartWebUiEmulation(masterId);
              }
          });
          logInfo(`DECT-Refresh gestartet (Intervall: ${schedulerIntervalFast} Sekunden).`);
      }
      
      function startPingScheduler() {
          if (pingSchedulerHandle) return;
          pingSchedulerHandle = schedule('*/5 * * * * *', async () => {
              if (STOPPED) return;
              await pingHost(fbIp, `${ROOT}ping.`);;
          });
          logInfo(`Ping gestartet (Intervall: 5 Sekunden).`);
      }
      
      // WebUI-Emulation
      async function updateMasterDevice(devices) {
          if (masterId !== null) {
              const current = devices.find(d => d.$?.id == masterId);
              if (current && current.present === "1") {
                  return masterId;
              }
          }
      
          for (const dev of devices) {
              const type = detectDeviceType(dev);
              if (type === "dect200" && dev.present === "1") {
                  masterId = dev.$?.id;
                  return masterId;
              }
          }
          masterId = null;
          return null;
      }
      
      async function startWebUiEmulation(id) {
          if (!id) return;
          if (!fritzboxAlive()) return;
      
          try {
              if (sid === "0000000000000000") await getSid();
      
              const url = `http://${fbIp}/net/home_auto_query.lua?sid=${sid}&no_sidrenew=1&command=EnergyStats_10&id=${id}&useajax=1&xhr=1`;
              const raw = await fritzRequest("get", url);
      
              if (!raw || typeof raw !== "object") {
                  return;
              }
      
              const ain = Object.keys(deviceMap).find(a => deviceMap[a]?.meta?.id == id);
              if (!ain) {
                  logError10x("startWebUiEmulation:noAIN", `Keine AIN für ID ${id} gefunden`);
                  return;
              }
      
              await parseEnergyStats(ain, raw);      // Momentanwerte
      
              const stateId = `${ROOT}DECT_${ain}.PollEnergyStats`;
              const now = Date.now();
              const ONE_HOUR = 60 * 60 * 1000;
      
              const stateObj = getState(stateId);
              const lastTs = stateObj?.val ?? 0;
      
              if (!lastTs || (now - lastTs) >= ONE_HOUR) {
                  await updateBasicDeviceStats(ain);     // XML-Statistik
                  smartSetState(stateId, now);
              }
      
              // Fehlerzähler für alle drei Fälle zurücksetzen
              resetErrorCounter("startWebUiEmulation:noAIN");
              resetErrorCounter("startWebUiEmulation:general");
          } catch (err) {
              logError10x("startWebUiEmulation:general", "WebUI-Emulation Fehler: " + err);
          }
      }
      
      // main()
      async function main() {
          logInfo("Starte Initialisierung...");
      
          await loadDevices();
          if (STOPPED) return;
      
          await initConfig();
      
          for (const ain of allAins) {
              if (STOPPED) return;
              const entry = deviceMap[ain];
              const type  = entry.type;
      
              if (type === "dect200") {
                  await createDect200States(ain);
              } else if (type === "hkr") {
                  await createHkrStates(ain);
              } else if (type === "dect440") {
                  await createDect440States(ain);
              } else if (type === "group") {
                  await createDectGroupStates(ain);
              }
          }
          logInfo("Erstelle Datenpunkte...");
      
          await initialPoll();
          if (STOPPED) return;
      
          setupOnHandlers();
          logInfo("ON-Handler aktiviert.");
      
          await sleepMs(2000);
          startPollScheduler();
          startDECTRefreshScheduler();
          startPingScheduler();
          logInfo(`System vollständig gestartet. (WRITE_ENABLED=${WRITE_ENABLED})`);
      }
      
      // Stop-Handler
      onStop(async cb => {
          STOPPED = true;
          logInfo("Stoppe Script...");
      
          try {
              if (schedulerHandle) {
                  clearSchedule(schedulerHandle);
                  schedulerHandle = null;
              }
              if (fastSchedulerHandle) {
                  clearSchedule(fastSchedulerHandle);
                  fastSchedulerHandle = null;
              }
              if (pingSchedulerHandle) {
                  clearSchedule(pingSchedulerHandle);
                  pingSchedulerHandle = null;
              }
          } catch (e) {}
          cb();
      });
      
      main().catch(err => logError10x("main", "main(): " + err));
      

      Viel Spaß beim Testen.

      paul53P Offline
      paul53P Offline
      paul53
      schrieb zuletzt editiert von paul53
      #2

      @Ro75 [sagte]: folgende NPM-Pakete müssen zusätzlich im JS-Adapter eingetragen sein: "axios"

      "axios" ist bereits im Javascript-Adapter enthalten.

      The following modules are preloaded: node:dgram, node:crypto, node:dns, node:events, node:fs, node:http, node:https, node:http2, node:net, node:os, node:path, node:util, node:stream, node:zlib, suncalc2, axios, wake_on_lan, request (deprecated)

      Bitte verzichtet auf Chat-Nachrichten, denn die Handhabung ist grauenhaft !
      Produktiv: RPi 2 mit S.USV, HM-MOD-RPI und SLC-USB-Stick mit root fs

      1 Antwort Letzte Antwort
      0
      • Ro75R Online
        Ro75R Online
        Ro75
        schrieb zuletzt editiert von
        #3

        Das ist korrekt, allerdings mit Aktualisierung auf 9.x von 8.x war es notwendig. Warum auch immer. Wenn axios als zusätzliches NPM Paket eingetragen ist, schadet es nicht.

        Ro75.

        SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

        Thomas BraunT 1 Antwort Letzte Antwort
        0
        • Ro75R Ro75

          Das ist korrekt, allerdings mit Aktualisierung auf 9.x von 8.x war es notwendig. Warum auch immer. Wenn axios als zusätzliches NPM Paket eingetragen ist, schadet es nicht.

          Ro75.

          Thomas BraunT Online
          Thomas BraunT Online
          Thomas Braun
          Most Active
          schrieb zuletzt editiert von Thomas Braun
          #4

          @Ro75

          Nur sorgen solche modul-Doubletten ggfls. für Ärger.
          Und generell sollte man versuchen axios zu vermeiden und fetch zu verwenden. Siehe z. B. das aktuelle Changelog des ical-Adapters:

          1.20.0 (2026-04-07)
          
              (jens-maus) Replaced axios usage with node.js built-in fetch
          

          Linux-Werkzeugkasten:
          https://forum.iobroker.net/topic/42952/der-kleine-iobroker-linux-werkzeugkasten
          NodeJS Fixer Skript:
          https://forum.iobroker.net/topic/68035/iob-node-fix-skript
          iob_diag: curl -sLf -o diag.sh https://iobroker.net/diag.sh && bash diag.sh

          1 Antwort Letzte Antwort
          1
          • Ro75R Online
            Ro75R Online
            Ro75
            schrieb zuletzt editiert von
            #5

            Ich habe den Code bzgl. axios angepasst. Wie gesagt, unter JS-Adapter 8.x war es nicht nötig, unter 9.x bei mir schon. Ich habe wie gesagt, den Code jetzt etwas angepasst und nun geht es auch ohne das axios als zusätzliches NPM-Paket eingetragen werden muss.

            Ro75.

            SERVER = Beelink U59 16GB DDR4 RAM 512GB SSD, FB 7490, FritzDect 200+301+440, ConBee II, Zigbee Aqara Sensoren + NOUS A1Z, NOUS A1T, Philips Hue ** ioBroker, REDIS, influxdb2, Grafana, PiHole, Plex-Mediaserver, paperless-ngx (Docker), MariaDB + phpmyadmin *** VIS-Runtime = Intel NUC 8GB RAM 128GB SSD + 24" Touchscreen

            1 Antwort Letzte Antwort
            0

            Hey! Du scheinst an dieser Unterhaltung interessiert zu sein, hast aber noch kein Konto.

            Hast du es satt, bei jedem Besuch durch die gleichen Beiträge zu scrollen? Wenn du dich für ein Konto anmeldest, kommst du immer genau dorthin zurück, wo du zuvor warst, und kannst dich über neue Antworten benachrichtigen lassen (entweder per E-Mail oder Push-Benachrichtigung). Du kannst auch Lesezeichen speichern und Beiträge positiv bewerten, um anderen Community-Mitgliedern deine Wertschätzung zu zeigen.

            Mit deinem Input könnte dieser Beitrag noch besser werden 💗

            Registrieren Anmelden
            Antworten
            • In einem neuen Thema antworten
            Anmelden zum Antworten
            • Älteste zuerst
            • Neuste zuerst
            • Meiste Stimmen


            Support us

            ioBroker
            Community Adapters
            Donate

            379

            Online

            32.8k

            Benutzer

            82.7k

            Themen

            1.3m

            Beiträge
            Community
            Impressum | Datenschutz-Bestimmungen | Nutzungsbedingungen | Einwilligungseinstellungen
            ioBroker Community 2014-2025
            logo
            • Anmelden

            • Du hast noch kein Konto? Registrieren

            • Anmelden oder registrieren, um zu suchen
            • Erster Beitrag
              Letzter Beitrag
            0
            • Home
            • Aktuell
            • Tags
            • Ungelesen 0
            • Kategorien
            • Unreplied
            • Beliebt
            • GitHub
            • Docu
            • Hilfe