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. Visualisierung
  4. Google Material 3 / Material You - Vis2

NEWS

  • wichtiges UPDATE für controller 7.2.2 im stable
    HomoranH
    Homoran
    10
    1
    2.2k

  • Neues YouTube-Video: Visualisierung im Devices-Adapter
    BluefoxB
    Bluefox
    16
    1
    4.1k

  • Neuer ioBroker-Blog online: Monatsrückblick März/April 2026
    BluefoxB
    Bluefox
    8
    1
    3.6k

Google Material 3 / Material You - Vis2

Geplant Angeheftet Gesperrt Verschoben Visualisierung
vismaterial css
19 Beiträge 5 Kommentatoren 2.2k Aufrufe 12 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.
  • M Offline
    M Offline
    mrMuppet
    schrieb am zuletzt editiert von mrMuppet
    #10

    Das Alert-Script:

    Was macht dieses Skript?

    Dieses Skript ist eine vollautomatische "Benachrichtigungszentrale" für euer ioBroker-Dashboard. Es sammelt Warnungen, Infos oder Statusmeldungen (die ihr aus anderen Skripten schickt) und stellt sie als wunderschöne, gestapelte Kacheln im Material 3 Design dar.

    Die Features:

    • Theme-Engine Ready: Nutzt nahtlos die Farben eures Themes (Hell/Dunkel).
    • Farb-Codes: Fehlermeldungen sind rot, Komfort-Infos blaugrün, reine Infos grün.
    • Auto-Icons: Das Skript liest den Text der Warnung mit (z.B. "Garage steht offen") und weist automatisch das passende Icon zu!
    • Drei Verhaltensweisen: Ihr könnt steuern, ob der User die Meldung am Tablet manuell wegklicken muss (M), ob sie nach X Minuten von selbst verschwindet (T), oder ob sie so lange bleibt, bis euer anderes Skript "Entwarnung" gibt (A).

    Wie nutze ich das Skript?

    1. Startet das Skript. Es legt automatisch die nötigen Datenpunkte (u.a. 0_userdata.0.Warnungen.warnung_new) an.
    2. Zieht ein "Basic - HTML" Widget in eure vis und tragt dort {0_userdata.0.alertMessages2_html} ein.
    3. Meldungen erzeugen: Wenn ihr aus euren Blockly- oder JavaScripts eine Warnung auf dem Tablet anzeigen wollt, schreibt ihr einfach einen Text in den Datenpunkt 0_userdata.0.Warnungen.warnung_new.

    Der Trick (Das Prefix-System):
    Ihr schreibt vor euren Text immer ein kurzes Präfix, das Farbe und Verhalten steuert: TYP + VERHALTEN : Text

    • Typen: E (Error/Rot), C (Comfort/Blaugrün), I (Info/Grün)
    • Verhalten: M (Manuell schließen), T (Timeout/Verschwindet von allein), A (Auto/Bleibt bis zur Entwarnung)

    Beispiele:

    • Ihr schreibt: EM:Haustürschloss klemmt! (Roter Fehler, User muss wegklicken).
    • Ihr schreibt: IT:Waschmaschine ist fertig. (Grüne Info, verschwindet nach 30 Minuten von selbst).
    • Ihr schreibt: CA:Temperatur im Wohnzimmer zu hoch. (Bleibt stehen). Um diese "Auto"-Meldung wieder verschwinden zu lassen, schickt ihr exakt denselben Text mit einem Minus-Zeichen davor: -CA:Temperatur im Wohnzimmer zu hoch.

    Das Skript (TypeScript)

    // =============================================================================
    // M3 ALERT RENDERER für vis2 Dashboard (THEME ENGINE READY)
    // =============================================================================
    
    // === KONFIGURATION ============================================================
    
    // Wo sollen die Datenpunkte angelegt werden?
    const DP_WARNUNG_NEW   = '0_userdata.0.Warnungen.warnung_new';
    const DP_ALERT_HTML    = '0_userdata.0.alertMessages2_html';
    const DP_ALERT_DISMISS = '0_userdata.0.alerts_dismiss';
    
    // Einstellungen
    const TIMEOUT_MS = 30 * 60 * 1000; // Wie lange bleiben 'T' (Timeout) Meldungen? (Hier: 30 Min)
    const MAX_ALERTS = 5;              // Wie viele Meldungen sollen maximal gestapelt werden?
    
    
    // =============================================================================
    // === AB HIER NUR ANPASSEN, WENN IHR NEUE ICONS ODER FARBEN WOLLT ===
    // =============================================================================
    
    // === M3 FARBEN (Dynamisch gekoppelt an Theme-Master inkl. Fallbacks) ==========
    const COLORS = {
        E: { // Error — Sicherheit (Rot)
            bg:    'var(--m3-error-container, rgb(255,218,214))',
            text:  'var(--m3-on-error-container, rgb(147,0,10))',
            icon:  'var(--m3-error, rgb(186,26,26))',
        },
        C: { // Comfort — Komfort (Blaugrün)
            bg:    'var(--m3-tertiary-container, rgb(188,235,239))',
            text:  'var(--m3-on-tertiary-container, rgb(30,77,81))',
            icon:  'var(--m3-tertiary, rgb(56,101,105))',
        },
        I: { // Info (Grün)
            bg:    'var(--m3-secondary-container, rgb(214,232,206))',
            text:  'var(--m3-on-secondary-container, rgb(60,75,56))',
            icon:  'var(--m3-secondary, rgb(83,99,78))',
        },
    };
    
    // === M3 SVG ICONS ===
    const ICONS: Record<string, string> = {
        'door':     'M12 3H2v18h10v-2H4V5h8V3m7 9l-4-4v3H8v2h7v3l4-4',
        'garage':   'M20 9h-2V4H6v5H4L2 20h20L20 9M7 19v-6h2v6H7m4 0v-6h2v6h-2m4 0v-6h2v6h-2',
        'lock':     'M12 17a2 2 0 0 0 2-2a2 2 0 0 0-2-2a2 2 0 0 0-2 2a2 2 0 0 0 2 2m6-9a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V10a2 2 0 0 1 2-2h1V6a5 5 0 0 1 5-5a5 5 0 0 1 5 5v2h1m-6-5a3 3 0 0 0-3 3v2h6V6a3 3 0 0 0-3-3',
        'alert':    'M13 14h-2V9h2m0 9h-2v-2h2M1 21h22L12 2L1 21',
        'humidity': 'M12 3.25C12 3.25 6 10 6 14a6 6 0 0 0 6 6a6 6 0 0 0 6-6c0-4-6-10.75-6-10.75',
        'temp':     'M15 13V5a3 3 0 0 0-6 0v8a5 5 0 1 0 6 0m-3-9a1 1 0 0 1 1 1v3h-2V5a1 1 0 0 1 1-1',
        'washer':   'M6 2h12a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2m1 2v2h6V4H7m5 10a4 4 0 0 1-4-4a4 4 0 0 1 4 4m0 0a4 4 0 0 0 4-4a4 4 0 0 0-4 4m0-6a6 6 0 0 0-6 6a6 6 0 0 0 6 6a6 6 0 0 0 6-6a6 6 0 0 0-6-6',
        'info':     'M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 0 0 2 12a10 10 0 0 0 10 10a10 10 0 0 0 10-10A10 10 0 0 0 12 2',
        'close':    'M19 6.41L17.59 5L12 10.59L6.41 5L5 6.41L10.59 12L5 17.59L6.41 19L12 13.41L17.59 19L19 17.59L13.41 12L19 6.41',
    };
    
    // === ICON-ZUORDNUNG nach Keywords im Text ===
    const ICON_KEYWORDS: Array<{ keyword: string; icon: string }> = [
        { keyword: 'Haustür',        icon: 'door' },
        { keyword: 'Tür',            icon: 'door' },
        { keyword: 'Garage',         icon: 'garage' },
        { keyword: 'Schloss',        icon: 'lock' },
        { keyword: 'verriegelt',     icon: 'lock' },
        { keyword: 'Feucht',         icon: 'humidity' },
        { keyword: 'Temperatur',     icon: 'temp' },
        { keyword: 'Waschmaschine',  icon: 'washer' },
        { keyword: 'Trockner',       icon: 'washer' },
    ];
    
    const DEFAULT_ICONS: Record<string, string> = { E: 'alert', C: 'humidity', I: 'info' };
    
    // === DATENSTRUKTUR ===
    interface Alert {
        id: string;
        type: 'E' | 'C' | 'I';
        dismiss: 'M' | 'T' | 'A';
        text: string;
        time: string;
        timestamp: number;
        icon: string;
    }
    
    let alerts: Alert[] = [];
    let timeoutTimers: Record<string, any> = {};
    
    // === HILFSFUNKTIONEN ===
    function parsePrefix(raw: string): { type: string; dismiss: string; text: string; remove: boolean } | null {
        const trimmed = raw.trim();
        const remove = trimmed.startsWith('-');
        const cleaned = remove ? trimmed.substring(1) : trimmed;
    
        const match = cleaned.match(/^([ECI])([MTA]):(.+)$/);
        if (!match) {
            log(`[AlertRenderer] Ungültiges Format: "${raw}" — erwartet z.B. "EM:Text"`, 'warn');
            return null;
        }
    
        return { type: match[1], dismiss: match[2], text: match[3].trim(), remove };
    }
    
    function findIcon(text: string, type: string): string {
        for (const entry of ICON_KEYWORDS) {
            if (text.toLowerCase().includes(entry.keyword.toLowerCase())) return entry.icon;
        }
        return DEFAULT_ICONS[type] || 'alert';
    }
    
    function makeId(type: string, dismiss: string, text: string): string {
        return `${type}${dismiss}:${text}`;
    }
    
    function timeNow(): string {
        const d = new Date();
        return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`;
    }
    
    function svgIcon(name: string, color: string, size: number = 20): string {
        const path = ICONS[name] || ICONS['alert'];
        return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" style="flex-shrink:0; transition: fill 0.3s ease;"><path fill="${color}" d="${path}"/></svg>`;
    }
    
    function removeAlertAndTimer(id: string) {
        const idx = alerts.findIndex(a => a.id === id);
        if (idx >= 0) {
            if (timeoutTimers[id]) {
                clearTimeout(timeoutTimers[id]);
                delete timeoutTimers[id];
            }
            alerts.splice(idx, 1);
        }
    }
    
    // === ALERT HINZUFÜGEN =========================================================
    function addAlert(raw: string): void {
        const parsed = parsePrefix(raw);
        if (!parsed) return;
    
        const { type, dismiss, text, remove } = parsed;
        const id = makeId(type, dismiss, text);
    
        // Entwarnung (-) empfangen
        if (remove) {
            const exists = alerts.some(a => a.id === id);
            if (exists) {
                removeAlertAndTimer(id);
                log(`[AlertRenderer] Alert entfernt (Auto-Entwarnung): "${text}"`);
                renderAlerts();
            } else {
                log(`[AlertRenderer] Ignoriert: Entwarnung für "${text}" (war ohnehin nicht aktiv).`, 'debug');
            }
            return;
        }
    
        // Vorhandenen Alert nur aktualisieren (Uhrzeit pushen)
        const existing = alerts.find(a => a.id === id);
        if (existing) {
            existing.time = timeNow();
            existing.timestamp = Date.now();
            
            // Timer bei T-Meldungen zurücksetzen, damit sie nicht sofort verschwinden
            if (dismiss === 'T') {
                if (timeoutTimers[id]) clearTimeout(timeoutTimers[id]);
                timeoutTimers[id] = setTimeout(() => {
                    removeAlertAndTimer(id);
                    log(`[AlertRenderer] Timeout abgelaufen (nach Update): "${text}"`);
                    renderAlerts();
                }, TIMEOUT_MS);
            }
            
            log(`[AlertRenderer] Alert aktualisiert: "${text}"`);
            renderAlerts();
            return;
        }
    
        // Neuen Alert anlegen
        const alert: Alert = {
            id,
            type: type as Alert['type'],
            dismiss: dismiss as Alert['dismiss'],
            text,
            time: timeNow(),
            timestamp: Date.now(),
            icon: findIcon(text, type),
        };
    
        alerts.push(alert);
    
        // Limit überwachen und älteste löschen
        while (alerts.length > MAX_ALERTS) {
            const removed = alerts[0];
            removeAlertAndTimer(removed.id);
        }
    
        // Timeout-Timer setzen, falls 'T'
        if (dismiss === 'T') {
            timeoutTimers[id] = setTimeout(() => {
                removeAlertAndTimer(id);
                log(`[AlertRenderer] Timeout abgelaufen: "${text}"`);
                renderAlerts();
            }, TIMEOUT_MS);
        }
    
        log(`[AlertRenderer] Neuer Alert: [${type}${dismiss}] "${text}"`);
        renderAlerts();
    }
    
    // === HTML RENDERN =============================================================
    function renderAlerts(): void {
        if (alerts.length === 0) {
            setState(DP_ALERT_HTML, '', true);
            return;
        }
    
        const sorted = [...alerts].reverse();
    
        const cardsHTML = sorted.map(a => {
            const c = COLORS[a.type];
            const showClose = a.dismiss === 'M' || a.dismiss === 'T'; 
    
            const clickAction = `(function(e){
                e.stopPropagation();
                if (window.vis && window.vis.conn && window.vis.conn.setState) {
                    window.vis.conn.setState('${DP_ALERT_DISMISS}', '${a.id}', false);
                } else if (window.vis && window.vis.setValue) {
                    window.vis.setValue('${DP_ALERT_DISMISS}', '${a.id}');
                }
            })(event);`.replace(/\n/g, ' ');
    
            return `
    <div style="
        display:flex; align-items:center; gap:12px;
        background:${c.bg}; color:${c.text};
        border-radius:12px; padding:12px 16px;
        font-family:'Roboto','Segoe UI',system-ui,sans-serif;
        margin-bottom:8px; transition: all 0.3s ease;
    ">
        ${svgIcon(a.icon, c.icon, 22)}
        <div style="flex:1; min-width:0;">
            <div style="font-size:14px; font-weight:500; line-height:1.4; word-wrap:break-word; transition: color 0.3s ease;">${a.text}</div>
            <div style="font-size:11px; font-weight:500; opacity:0.7; margin-top:2px; transition: color 0.3s ease;">${a.time}</div>
        </div>
        ${showClose ? `
        <div onclick="${clickAction}"
             style="cursor:pointer; padding:4px; margin:-4px -4px -4px 0; flex-shrink:0; opacity:0.7; -webkit-tap-highlight-color:transparent;">
            ${svgIcon('close', c.text, 18)}
        </div>` : ''}
    </div>`;
        }).join('');
    
        const html = `<div style="display:flex; flex-direction:column; width:100%; height:100%; overflow-y:auto; padding:0;">${cardsHTML}</div>`;
        setState(DP_ALERT_HTML, html, true);
    }
    
    // === DATENPUNKTE ERSTELLEN =====================================================
    function ensureDatapoints(callback: () => void): void {
        let pending = 3;
        function done() {
            pending--;
            if (pending === 0) callback();
        }
        createState(DP_WARNUNG_NEW, '', { type: 'string', name: 'Alert Input', role: 'state' }, done);
        createState(DP_ALERT_HTML, '', { type: 'string', name: 'Alert HTML Output (vis2)', role: 'html' }, done);
        createState(DP_ALERT_DISMISS, '', { type: 'string', name: 'Alert Dismiss Trigger', role: 'state' }, done);
    }
    
    // === INITIALISIERUNG ===========================================================
    ensureDatapoints(() => {
        
        // Lauscht auf neue Text-Eingaben
        on({ id: DP_WARNUNG_NEW, change: 'any' }, (obj) => {
            if (obj.state.val && String(obj.state.val).trim() !== '') {
                addAlert(String(obj.state.val));
            }
        });
    
        // Lauscht auf Klicks im vis-Widget (X-Button)
        on({ id: DP_ALERT_DISMISS, change: 'any' }, (obj) => {
            if (obj.state.val && String(obj.state.val).trim() !== '') {
                const idToDismiss = String(obj.state.val);
                removeAlertAndTimer(idToDismiss);
                log(`[AlertRenderer] Manuell geschlossen: "${idToDismiss}"`);
                renderAlerts();
                setState(DP_ALERT_DISMISS, '', true);
            }
        });
    
        renderAlerts();
        log('[AlertRenderer] M3 Alert Renderer gestartet.');
    });
    
    

    alerts.jpg

    ioBroker auf NUC (Celeron mit Ubuntu-Server)

    Homematic, HMIP, Hue, Unifi, Plex, Nest, Roborock, Google Assistant

    1 Antwort Letzte Antwort
    0
    • M Offline
      M Offline
      mrMuppet
      schrieb am zuletzt editiert von mrMuppet
      #11

      Hier die Einstellungen:

      Was macht dieses Skript?

      Dieses Skript generiert ein komplettes Einstellungs-Menü (Setup-Tab) im Material 3 Design für euer vis-Dashboard. Es rendert sauberes HTML und bringt drei Hauptfunktionen mit:

      1. System & Automatiken: Eine elegante Listenansicht mit Schaltern, um z.B. Skripte (Rollladenautomatik, Urlaubsmodus) ein- und auszuschalten.
      2. Graphen-Filter (Chips): Interaktive Buttons (Chips), mit denen man Datenpunkte für ein Diagramm (z.B. Heizung) ein- und ausblenden kann.
      3. Dynamische Wallpaper-Auswahl: Das absolute Highlight! Das Skript liest einen festgelegten Ordner eures ioBrokers aus und generiert automatisch ein Popup-Menü mit allen darin enthaltenen Bildern (.jpg, .png, .webp). Wählt ihr ein Bild aus, wird der Pfad in einen Datenpunkt geschrieben (perfekt für die Kombination mit meinem "M3 Theme Master" Skript).

      Was muss vorbereitet werden?

      Damit das Skript auf euren ioBroker-Dateispeicher zugreifen darf, müsst ihr einen wichtigen Haken setzen:

      1. Geht in die Instanz-Einstellungen eures JavaScript-Adapters.
      2. Setzt den Haken bei "Erlaube das Kommando exec" (falls noch nicht geschehen) und tragt unter Zusätzliche NPM-Module das Modul fs ein, falls es dort nicht standardmäßig aktiv ist. Alternativ reicht es oft schon, in den JS-Adapter-Einstellungen den Haken bei "Erlaube Zugriff auf Dateisystem" zu setzen.
      3. Wallpaper-Ordner: Legt im ioBroker Dateimanager (unter vis.0 oder vis-2.0) einen Ordner an und ladet dort ein paar Bilder hoch. Passt den Pfad im Skript (WALLPAPER_WEB_DIR) entsprechend an.
      4. Eure Geräte eintragen: Schaut im Skript in die Funktion getListItems(). Dort tragt ihr einfach die Datenpunkte eurer eigenen ioBroker-Skripte oder Schalter ein.

      Wie wendet man das an?

      Das Skript erstellt alle nötigen Datenpunkte automatisch (standardmäßig unter 0_userdata.0.dashboard.).

      1. Startet das Skript.
      2. Zieht ein "Basic - String" (oder "Basic - HTML") Widget in eure vis.
      3. Tragt unter HTML / Inhalt den Datenpunkt-Namen in geschweiften Klammern ein: {0_userdata.0.dashboard.setupHTML}
      4. Zieht das Widget so groß wie nötig, das Skript bringt eine eigene, stylische M3-Scrollbar mit, falls der Platz nicht reicht.

      Das Skript (TypeScript)

      // ============================================================
      // renderSetup — M3 Material You Styling (Kompakt & Robust-Scroll)
      // ============================================================
      
      const fs = require('fs'); 
      
      // === KONFIGURATION ==========================================================
      
      const BASE_PATH = '0_userdata.0.dashboard';
      
      const DP_SETUP_HTML = `${BASE_PATH}.setupHTML`;
      const DP_IMAGE_PATH = `${BASE_PATH}.themeImagePath`;
      const DP_PRIVACY    = `${BASE_PATH}.privacyMode`;
      
      // GRAPHEN-FILTER (Beispiele für eine Heizungs-Ansicht)
      const DP_CHART_BRENNER = `${BASE_PATH}.chartShowBrenner`;
      const DP_CHART_VALVE   = `${BASE_PATH}.chartShowValve`;
      const DP_CHART_ACT     = `${BASE_PATH}.chartShowAct`;
      const DP_CHART_SET     = `${BASE_PATH}.chartShowSet`;
      const DP_CHART_WINDOW  = `${BASE_PATH}.chartShowWindow`;
      const DP_CHART_HUMID   = `${BASE_PATH}.chartShowHumid`;
      
      // ORDNER FÜR DIE HINTERGRUNDBILDER
      // Wichtig: Der Ordner muss im ioBroker Dateimanager existieren!
      const WALLPAPER_WEB_DIR = '/vis-2.0/Material_You/img/wallpaper/';
      const WALLPAPER_SYS_DIR = '/opt/iobroker/iobroker-data/files' + WALLPAPER_WEB_DIR;
      
      // ============================================================================
      // === AB HIER NUR ANPASSEN, WENN EIGENE LISTEN-EINTRÄGE GEWÜNSCHT SIND ===
      // ============================================================================
      
      const M3 = {
          primary:                'var(--m3-primary)',
          onPrimary:              'var(--m3-on-primary)',
          primaryContainer:       'var(--m3-primary-container)',
          onPrimaryContainer:     'var(--m3-on-primary-container)',
          surfaceContainerLow:    'var(--m3-surface-container-low)',
          surfaceContainerHighest:'var(--m3-surface-container-highest)',
          surfaceVariant:         'var(--m3-surface-variant)',
          onSurface:              'var(--m3-on-surface)',
          onSurfaceVariant:       'var(--m3-on-surface-variant)',
          outline:                'var(--m3-outline)', 
          outlineVariant:         'var(--m3-outline-variant)'
      };
      
      const ICONS: Record<string, string> = {
          'party':   'M2 22l5-14 9 9-14 5zm3.3-3.3l7.05-2.5-4.55-4.55-2.5 7.05zM14.55 12.55l-1.05-1.05 5.6-5.6q.8-.8 1.925-.8t1.925.8l.6.6-1.05 1.05-.6-.6q-.35-.35-.875-.35t-.875.35l-5.6 5.6zM10.55 8.55l-1.05-1.05.6-.6q.35-.35.35-.85t-.35-.85l-.65-.65 1.05-1.05.65.65q.8.8.8 1.9t-.8 1.9l-.6.6zm2 2l-1.05-1.05 3.6-3.6q.35-.35.35-.875t-.35-.875l-1.6-1.6 1.05-1.05 1.6 1.6q.8.8.8 1.925t-.8 1.925l-3.6 3.6zm4 4l-1.05-1.05 1.6-1.6q.8-.8 1.925-.8t1.925.8l1.6 1.6-1.05 1.05-1.6-1.6q-.35-.35-.875-.35t-.875.35l-1.6 1.6z',
          'guests':  'M16 11C17.66 11 18.99 9.66 18.99 8C18.99 6.34 17.66 5 16 5C14.34 5 13 6.34 13 8C13 9.66 14.34 11 16 11M8 11C9.66 11 10.99 9.66 10.99 8C10.99 6.34 9.66 5 8 5C6.34 5 5 6.34 5 8C5 9.66 6.34 11 8 11M8 13C5.67 13 1 14.17 1 16.5V19H15V16.5C15 14.17 10.33 13 8 13M16 13C15.71 13 15.38 13.02 15.03 13.05C16.19 13.89 17 15.02 17 16.5V19H23V16.5C23 14.17 18.33 13 16 13Z',
          'sun':     'M20 14H18L14.8 23H16.7L17.4 21H20.6L21.3 23H23.2L20 14M17.8 19.7L19 16L20.2 19.7H17.8M7 9H15V11H7V9M7 12H15V14H7V12M7 15H15V16.5L14.8 17H7V15M13.7 20H7V18H14.5L13.7 20M16 8H6V20H4V8H2V4H20V8H18V12H16.6L16.1 13.3L16 13.7V8Z',
          'alarm':   'M12 20A7 7 0 0 1 5 13A7 7 0 0 1 12 6A7 7 0 0 1 19 13A7 7 0 0 1 12 20M12 4A9 9 0 0 0 3 13A9 9 0 0 0 12 22A9 9 0 0 0 21 13A9 9 0 0 0 12 4M7.88 3.39L6.6 1.86L2 5.71L3.29 7.24L7.88 3.39M22 5.72L17.4 1.86L16.11 3.39L20.71 7.25L22 5.72M12.5 8H11V14L15.75 16.85L16.5 15.62L12.5 13.25V8Z',
          'person':  'M12 4A4 4 0 0 1 16 8A4 4 0 0 1 12 12A4 4 0 0 1 8 8A4 4 0 0 1 12 4M12 14C16.42 14 20 15.79 20 18V20H4V18C4 15.79 7.58 14 12 14Z',
          'moon':    'M12 21q-3.75 0-6.375-2.625T3 12q0-3.75 2.625-6.375T12 3q.35 0 .688.025t.662.075q-1.025.725-1.637 1.888T11.1 7.5q0 2.25 1.575 3.825T16.5 12.9q1.375 0 2.525-.612t1.875-1.638q.05.325.075.663T21 12q0 3.75-2.625 6.375T12 21Zm0-2q2.2 0 3.95-1.212T18.5 14.625q-.5.125-1 .2t-1 .075q-3.075 0-5.238-2.162T9.1 7.5q0-.5.075-1t.2-1q-1.95.8-3.162 2.55T5 12q0 2.9 2.05 4.95T12 19Zm-.25-6.75Z',
          'auto':    'M11 19q1.3 0 2.47-.52t2.03-1.5q-3.2-.2-5.35-2.49T8 9q0-.32.02-.64t.08-.61q-1.42.8-2.26 2.2T5 13q0 2.5 1.75 4.25T11 19Zm0 2q-3.35 0-5.67-2.32t-2.33-5.68q0-3.35 2.32-5.67t5.68-2.33q.13 0 .25.01t.25.01q-.72.8-1.11 1.83T10 9q0 2.5 1.75 4.25T16 15q.78 0 1.51-.19t1.4-.56q-.45 2.95-2.7 4.85T11 21Zm2.8-10 3.2-9h2l3.2 9h-1.9l-.7-2H17.4l-.7 2h-1.9Zm3.05-3.35h2.3l-1.15-3.65-1.15 3.65ZM10.18-9.53Z',
          'palette': 'M12 22q-2.05 0-3.875-.788t-3.187-2.15Q3.575 17.7 2.788 15.875T2 12q0-2.075.812-3.9t2.2-3.175Q6.4 3.575 8.25 2.788T12.2 2q2 0 3.775.688t3.112 1.9q1.338 1.212 2.125 2.875T22 10.95q0 2.875-1.75 4.413T16 17h-1.85q-.225 0-.313.125t-.087.275q0 .3 0.375.863T14.5 20.3q0 1.25-.688 1.85T12 22Zm0-10Zm-4.425.575q.425-.425.425-1.075t-.425-1.075q-.425-.425-1.075-.425t-1.075.425q-.425.425-.425 1.075t.425 1.075q.425.425 1.075.425t1.075-.425Zm3-4q.425-.425.425-1.075t-.425-1.075q-.425-.425-1.075-.425t-1.075.425q-.425.425-.425 1.075t.425 1.075q.425.425 1.075.425t1.075-.425Zm5 0q.425-.425.425-1.075t-.425-1.075q-.425-.425-1.075-.425t-1.075.425q-.425.425-.425 1.075t.425 1.075q.425.425 1.075.425t1.075-.425Zm3 4q.425-.425.425-1.075t-.425-1.075q-.425-.425-1.075-.425t-1.075.425q-.425.425-.425 1.075t.425 1.075q.425.425 1.075.425t1.075-.425ZM12 20q.225 0 .363-.125t.137-.325q0-.35-.375-.825T11.75 17.3q0-1.05.725-1.675T14.25 15h1.75q1.65 0 2.825-.963T20 10.95q0-3.025-2.312-5.038T12.2 3.9q-3.4 0-5.8 2.325T4 11.95q0 3.325 2.338 5.663T12 20Z',
          'dropdown': 'M7 10l5 5 5-5H7z',
          'thermostat': 'M15 13V5A3 3 0 0 0 9 5V13A5 5 0 1 0 15 13M12 4A1 1 0 0 1 13 5V8H11V5A1 1 0 0 1 12 4Z'
      };
      
      interface ListItem { label: string; description: string; icon: string; oid: string; disableIf?: string; category: 'system' | 'appearance'; }
      
      // === EURE LISTEN-EINTRÄGE ===
      function getListItems(): ListItem[] {
          const isPrivacy = getSafeVal(DP_PRIVACY, false) === true;
          const nameUser1 = isPrivacy ? 'Bewohner 1' : 'Eigener Name';
          
          return [
              { category: 'system', label: 'Rollladen Automatik', description: 'Die Rollladen werden nach Sonnenuntergang automatisch geschlossen.', icon: 'sun', oid: 'javascript.0.routines.auto_blinds' },
              { category: 'system', label: 'WakeUp-Automatik',    description: 'Bei Bewegung nach 6 Uhr am Morgen werden die Rolladen geöffnet.', icon: 'alarm', oid: 'javascript.0.routines.wakeup' },
              { category: 'system', label: 'Partymodus',          description: 'Schlaf-Taste bleibt verborgen. Automatiken werden pausiert.', icon: 'party', oid: '0_userdata.0.System.Partymodus' },
              { category: 'system', label: 'Gäste im Haus',       description: 'Verhindert das automatische Ausschalten in bestimmten Bereichen.', icon: 'guests', oid: '0_userdata.0.System.Gaeste' },
              { category: 'system', label: nameUser1,             description: `Für ${nameUser1} wird eine individuelle Routine aktiviert.`, icon: 'person', oid: 'javascript.0.routines.user1_routine' },
              
              { category: 'appearance', label: 'Privacy Mode (Demo)', description: 'Anonymisiert Namen und Bilder für Screenshots.', icon: 'person', oid: DP_PRIVACY },
              { category: 'appearance', label: 'Google JSON Theme', description: 'Aktiv: Nutzt Custom-JSON. Inaktiv: Farben aus dem Wallpaper.', icon: 'palette', oid: `${BASE_PATH}.themeUseJson` },
              { category: 'appearance', label: 'Auto-Theme (Astro)', description: 'Wechselt bei Sonnenuntergang in den Dark-Mode.', icon: 'auto', oid: `${BASE_PATH}.autoTheme` },
              { category: 'appearance', label: 'Manueller Dark Mode',description: 'Überschreibt das Farbschema (Wenn Auto-Theme aus ist).', icon: 'moon', oid: `${BASE_PATH}.darkMode`, disableIf: `${BASE_PATH}.autoTheme` }
          ];
      }
      
      interface WallpaperItem { label: string; path: string; }
      
      function getDynamicWallpapers(): WallpaperItem[] {
          let wallpapers: WallpaperItem[] = [];
          try {
              if (fs.existsSync(WALLPAPER_SYS_DIR)) {
                  const files = fs.readdirSync(WALLPAPER_SYS_DIR);
                  files.forEach((file: string) => {
                      const ext = file.toLowerCase();
                      if (ext.endsWith('.jpg') || ext.endsWith('.jpeg') || ext.endsWith('.png') || ext.endsWith('.webp')) {
                          let cleanName = file.replace(/\.[^/.]+$/, "").replace(/[_-]/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
                          wallpapers.push({ label: cleanName, path: WALLPAPER_WEB_DIR + file });
                      }
                  });
              }
          } catch (e: any) {
              // Ordner existiert nicht oder fs Modul fehlt
          }
          wallpapers.sort((a, b) => a.label.localeCompare(b.label)); 
          return wallpapers;
      }
      
      let DYNAMIC_WALLPAPERS: WallpaperItem[] = getDynamicWallpapers();
      
      function svgIcon(name: string, color: string, size: number = 24): string {
          const path = ICONS[name] || ICONS['person'];
          return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" style="flex-shrink:0; transition: fill 0.3s ease;"><path fill="${color}" d="${path}"/></svg>`;
      }
      
      function getSafeVal(oid: string, fallback: any = false): any { if (existsState(oid)) return getState(oid).val; return fallback; }
      function getWallpaperNameByPath(path: string): string { const found = DYNAMIC_WALLPAPERS.find(wp => wp.path === path); return found ? found.label : 'Auswählen...'; }
      
      function renderListItem(item: ListItem): string {
          const rawVal = getSafeVal(item.oid, false); const active = (rawVal === true || rawVal === 1 || rawVal === 'true');
          let disabled = false; if (item.disableIf) disabled = (getSafeVal(item.disableIf, false) === true);
          
          const trackBg    = active ? M3.primary : M3.surfaceContainerHighest; const trackBorder = active ? M3.primary : M3.outline;
          const thumbSize  = active ? 24 : 16; const thumbColor = active ? M3.onPrimary : M3.outline; const thumbLeft = active ? 'calc(100% - 26px)' : '6px'; 
          const opacity = disabled ? '0.4' : '1'; const pointerEvents = disabled ? 'none' : 'auto';
      
          const clickAction = disabled ? "" : `(function(e){ e.stopPropagation(); if(typeof vis!=='undefined'&&vis.conn&&vis.conn.setState)vis.conn.setState('${item.oid}', ${!active}); else if(typeof vis!=='undefined'&&vis.setValue)vis.setValue('${item.oid}', ${!active}); })(event);`.replace(/\n/g, ' ');
          const rippleJS = disabled ? "" : `(function(e,el){var evt=e.touches?e.touches[0]:e;if(!evt.clientX)return;var d=Math.max(el.clientWidth,el.clientHeight);var r=d/2;var rect=el.getBoundingClientRect();var x=evt.clientX-rect.left;var y=evt.clientY-rect.top;var c=document.createElement('span');c.style.width=c.style.height=d+'px';c.style.left=(x-r)+'px';c.style.top=(y-r)+'px';c.style.position='absolute';c.style.borderRadius='50%';c.style.backgroundColor='var(--m3-on-surface-variant)';c.style.opacity='0.1';c.style.transform='scale(0)';c.style.animation='m3-ripple-anim 0.5s linear';c.style.pointerEvents='none';el.appendChild(c);setTimeout(function(){c.remove();},500);})(event,this)`;
      
          return `
      <div onclick="${clickAction}" onmousedown="${rippleJS}" ontouchstart="${rippleJS}"
        style="position:relative; overflow:hidden; width:100%; border-radius:16px; padding:12px 16px; box-sizing:border-box;
          background:${M3.surfaceContainerLow}; cursor:${disabled ? 'default' : 'pointer'}; user-select:none;
          display:flex; align-items:center; opacity:${opacity}; pointer-events:${pointerEvents};
          font-family:Roboto,'Segoe UI',system-ui,sans-serif; margin-bottom: 8px;
          -webkit-tap-highlight-color:transparent; transition: background-color 0.3s ease;">
        <div style="flex: 0 0 40px; width:40px; height:40px; border-radius:50%; background:${M3.surfaceVariant}; display:flex; align-items:center; justify-content:center; margin-right:14px;">
          ${svgIcon(item.icon, M3.onSurfaceVariant, 20)}
        </div>
        <div style="flex: 1 1 0%; min-width: 0; display: block; padding: 2px 0;">
          <div style="font-size:16px; font-weight:500; color:${M3.onSurface}; line-height:1.3; margin-bottom:2px; white-space:normal; overflow-wrap:break-word;">${item.label}</div>
          <div style="font-size:13px; font-weight:400; color:${M3.onSurfaceVariant}; opacity:0.85; line-height:1.4; white-space:normal; overflow-wrap:break-word;">${item.description}</div>
        </div>
        <div style="flex: 0 0 52px; width:52px; height:32px; margin-left:12px; border-radius:16px; background:${trackBg}; border:2px solid ${trackBorder}; box-sizing:border-box; position:relative; transition:all 0.25s cubic-bezier(0.2, 0, 0, 1);">
          <div style="position:absolute; left:${thumbLeft}; top:50%; transform:translateY(-50%); width:${thumbSize}px; height:${thumbSize}px; border-radius:50%; background:${thumbColor}; transition:all 0.25s cubic-bezier(0.2, 0, 0, 1);"></div>
        </div>
      </div>`;
      }
      
      // === RECHTSBÜNDIGE FILTER CHIPS ===
      function renderChartChips(): string {
          const actVal = getSafeVal(DP_CHART_ACT, true); const setVal = getSafeVal(DP_CHART_SET, true);
          const winVal = getSafeVal(DP_CHART_WINDOW, true); const brennerVal = getSafeVal(DP_CHART_BRENNER, true);
          const valveVal = getSafeVal(DP_CHART_VALVE, false); const humidVal = getSafeVal(DP_CHART_HUMID, true);
      
          const makeChip = (label: string, active: boolean, dp: string) => {
              const bg = active ? M3.primaryContainer : 'transparent';
              const fg = active ? M3.onPrimaryContainer : M3.onSurfaceVariant;
              const border = active ? `1px solid ${M3.primaryContainer}` : `1px solid ${M3.outline}`;
              const iconHtml = active ? `<svg width="18" height="18" viewBox="0 0 24 24" style="margin-right:4px; margin-left:-4px;"><path fill="currentColor" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>` : '';
              const js = `(function(e){ e.stopPropagation(); if(typeof vis!=='undefined'&&vis.conn&&vis.conn.setState)vis.conn.setState('${dp}', ${!active}); else if(typeof vis!=='undefined'&&vis.setValue)vis.setValue('${dp}', ${!active}); })(event);`.replace(/\n/g, ' ');
              return `<div onclick="${js}" style="display:inline-flex; align-items:center; height:32px; padding:0 12px; border-radius:8px; background:${bg}; border:${border}; color:${fg}; font-size:14px; font-weight:500; cursor:pointer; transition:all 0.2s ease;">${iconHtml}${label}</div>`;
          };
      
          return `
          <div style="width:100%; border-radius:16px; padding:12px 16px; box-sizing:border-box; background:${M3.surfaceContainerLow}; margin-bottom:8px; font-family:Roboto,'Segoe UI',system-ui,sans-serif; display:flex; align-items:flex-start;">
              <div style="flex:0 0 40px; width:40px; height:40px; border-radius:50%; background:${M3.surfaceVariant}; display:flex; align-items:center; justify-content:center; margin-right:14px;">
                  ${svgIcon('thermostat', M3.onSurfaceVariant, 20)}
              </div>
              <div style="flex: 1 1 0%; min-width: 0;">
                  <div style="font-size:16px; font-weight:500; color:${M3.onSurface}; margin-bottom:2px;">Graphen-Anzeige</div>
                  <div style="font-size:13px; color:${M3.onSurfaceVariant}; opacity:0.85; margin-bottom:12px; line-height:1.4;">Wähle aus, welche Linien in den Diagrammen gezeichnet werden.</div>
                  <div style="display:flex; flex-wrap:wrap; gap:8px; justify-content:flex-end;">
                      ${makeChip('Ist-Temp', actVal, DP_CHART_ACT)}
                      ${makeChip('Soll-Temp', setVal, DP_CHART_SET)}
                      ${makeChip('Luftfeuchte', humidVal, DP_CHART_HUMID)}
                      ${makeChip('Fenster', winVal, DP_CHART_WINDOW)}
                      ${makeChip('Brenner', brennerVal, DP_CHART_BRENNER)}
                      ${makeChip('Ventil', valveVal, DP_CHART_VALVE)}
                  </div>
              </div>
          </div>`;
      }
      
      function renderMenuWallpaperItem(wp: WallpaperItem, currentPath: string): string {
          const active = (wp.path === currentPath);
          const bg = active ? M3.surfaceVariant : 'transparent'; const textColor = active ? M3.primary : M3.onSurface;
          const clickAction = `(function(e){ e.stopPropagation(); if(typeof vis!=='undefined'&&vis.conn&&vis.conn.setState)vis.conn.setState('${DP_IMAGE_PATH}', '${wp.path}'); else if(typeof vis!=='undefined'&&vis.setValue)vis.setValue('${DP_IMAGE_PATH}', '${wp.path}'); document.getElementById('m3-wallpaper-menu').style.opacity='0'; document.getElementById('m3-wallpaper-menu').style.pointerEvents='none'; })(event);`.replace(/\n/g, ' ');
          return `<div onclick="${clickAction}" style="position:relative; width:100%; padding:10px 16px; box-sizing:border-box; border-radius:12px; background:${bg}; cursor:pointer; user-select:none; margin-bottom:2px; display:flex; align-items:center; font-family:Roboto,'Segoe UI',system-ui,sans-serif; -webkit-tap-highlight-color:transparent; transition: background-color 0.2s ease;"> <img src="${wp.path}" style="flex:0 0 44px; width:44px; height:32px; border-radius:6px; object-fit:cover; background:${M3.surfaceVariant}; margin-right:14px;"> <div style="flex:1 1 0%; min-width:0; display:block; font-size:15px; font-weight:500; color:${textColor}; line-height:1.4; white-space:normal; overflow-wrap:break-word;">${wp.label}</div> </div>`;
      }
      
      function renderSetup(): void {
          const allItems = getListItems();
          const systemHtml = allItems.filter(item => item.category === 'system').map(item => renderListItem(item)).join('');
          const appearanceHtml = allItems.filter(item => item.category === 'appearance').map(item => renderListItem(item)).join('');
          
          const chartChipsHtml = renderChartChips();
          
          const currentWpPath = getSafeVal(DP_IMAGE_PATH, ''); const currentWpName = getWallpaperNameByPath(currentWpPath);
          const menuItemsHtml = DYNAMIC_WALLPAPERS.map(wp => renderMenuWallpaperItem(wp, currentWpPath)).join('');
      
          const openMenuJS = `document.getElementById('m3-wallpaper-menu').style.opacity='1'; document.getElementById('m3-wallpaper-menu').style.pointerEvents='auto';`;
          const closeMenuJS = `document.getElementById('m3-wallpaper-menu').style.opacity='0'; document.getElementById('m3-wallpaper-menu').style.pointerEvents='none';`;
          const rippleJS = `(function(e,el){var evt=e.touches?e.touches[0]:e;if(!evt.clientX)return;var d=Math.max(el.clientWidth,el.clientHeight);var r=d/2;var rect=el.getBoundingClientRect();var x=evt.clientX-rect.left;var y=evt.clientY-rect.top;var c=document.createElement('span');c.style.width=c.style.height=d+'px';c.style.left=(x-r)+'px';c.style.top=(y-r)+'px';c.style.position='absolute';c.style.borderRadius='50%';c.style.backgroundColor='var(--m3-on-surface-variant)';c.style.opacity='0.1';c.style.transform='scale(0)';c.style.animation='m3-ripple-anim 0.5s linear';c.style.pointerEvents='none';el.appendChild(c);setTimeout(function(){c.remove();},500);})(event,this)`;
      
          const html = `
      <style>
        @keyframes m3-ripple-anim { to { transform: scale(4); opacity: 0; } }
        [id^="vis_widget_"] .vis-view-inner-html-html, [id^="vis_widget_"] .vis-view-inner-html-html > div { height: 100% !important; }
        .m3-scroll-container::-webkit-scrollbar, .m3-menu-scroll::-webkit-scrollbar { width: 6px; }
        .m3-scroll-container::-webkit-scrollbar-track, .m3-menu-scroll::-webkit-scrollbar-track { background: transparent; }
        .m3-scroll-container::-webkit-scrollbar-thumb, .m3-menu-scroll::-webkit-scrollbar-thumb { background: var(--m3-outline-variant); border-radius: 3px; }
        .m3-scroll-container::-webkit-scrollbar-thumb:hover, .m3-menu-scroll::-webkit-scrollbar-thumb:hover { background: var(--m3-outline); }
      </style>
      
      <div class="m3-scroll-container" style="position: absolute; inset: 0; background: var(--m3-surface-container-low, rgb(237, 239, 232)); border-radius: 0 0 28px 28px; padding: 24px; display: block; width: 100%; box-sizing: border-box; overflow-y: auto; transition: background-color 0.3s ease;">
        
        <div style="font-size:12px; font-weight:700; color:${M3.primary}; text-transform:uppercase; letter-spacing:1.2px; margin: 4px 0 8px 4px;">Erscheinungsbild & Anzeige</div>
        ${appearanceHtml}
        
        ${chartChipsHtml}
        
        <div style="position:relative; width:100%; border-radius:16px; padding:12px 16px; box-sizing:border-box; background:${M3.surfaceContainerLow}; user-select:none; margin-bottom:24px; display:flex; align-items:center; font-family:Roboto,'Segoe UI',system-ui,sans-serif; -webkit-tap-highlight-color:transparent; transition: all 0.3s ease;">
          <div style="flex:0 0 40px; width:40px; height:40px; border-radius:50%; background:${M3.surfaceVariant}; display:flex; align-items:center; justify-content:center; margin-right:14px;">${svgIcon('palette', M3.onSurfaceVariant, 20)}</div>
          <div style="flex:1 1 0%; min-width:0; display:block;"><div style="font-size:16px; font-weight:500; color:${M3.onSurface}; line-height:normal;">Hintergrundbild</div></div>
          <div onclick="${openMenuJS}" onmousedown="${rippleJS}" ontouchstart="${rippleJS}" style="flex:0 0 auto; margin-left:12px; position:relative; overflow:hidden; display:flex; align-items:center; gap:8px; background:${M3.surfaceVariant}; padding:8px 16px; border-radius:20px; cursor:pointer; color:${M3.onSurfaceVariant}; transition: background-color 0.2s ease;">
            <span style="font-size:14px; font-weight:500; line-height:normal;">${currentWpName}</span>${svgIcon('dropdown', M3.onSurfaceVariant, 20)}
          </div>
        </div>
      
        <div style="font-size:12px; font-weight:700; color:${M3.primary}; text-transform:uppercase; letter-spacing:1.2px; margin: 0 0 8px 4px;">System & Automatiken</div>
        ${systemHtml}
        <div style="height: 24px; flex-shrink: 0;"></div>
      </div>
      
      <div id="m3-wallpaper-menu" style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 999999; display: flex; align-items: center; justify-content: center; opacity: 0; pointer-events: none; transition: opacity 0.2s;">
        <div onclick="${closeMenuJS}" style="position: absolute; inset: 0; background: rgba(0,0,0,0.4);"></div>
        <div style="position: relative; width: 380px; max-height: 80vh; background: var(--m3-surface-container-highest, #e1e3dc); border-radius: 20px; box-shadow: 0 12px 32px rgba(0,0,0,0.3); display: flex; flex-direction: column; overflow: hidden; font-family:Roboto,'Segoe UI',system-ui,sans-serif;">
          <div style="padding: 20px 24px 12px 24px; font-size: 15px; font-weight: 600; color:${M3.onSurfaceVariant}; border-bottom: 1px solid ${M3.outlineVariant};">Hintergrundbild auswählen</div>
          <div class="m3-menu-scroll" style="overflow-y: auto; padding: 12px; display:flex; flex-direction:column;">
            ${DYNAMIC_WALLPAPERS.length > 0 ? menuItemsHtml : `<div style="padding:20px; text-align:center; color:${M3.onSurfaceVariant}; font-size:14px;">Keine Bilder gefunden. Ordner prüfen!</div>`}
          </div>
        </div>
      </div>`;
          setState(DP_SETUP_HTML, html, true);
      }
      
      // === INIT & SUBSCRIPTIONS ======================================================
      // Verschachtelte createState Aufrufe stellen sicher, dass alle Datenpunkte existieren, bevor abonniert wird.
      createState(DP_SETUP_HTML, '', { type: 'string', name: 'Setup HTML (M3)', role: 'html' }, () => {
          createState(DP_CHART_BRENNER, true, { type: 'boolean', name: 'Show Brenner Chart', role: 'switch' }, () => {
          createState(DP_CHART_VALVE, false, { type: 'boolean', name: 'Show Valve Chart', role: 'switch' }, () => {
          createState(DP_CHART_ACT, true, { type: 'boolean', name: 'Show Actual Temp Chart', role: 'switch' }, () => {
          createState(DP_CHART_SET, true, { type: 'boolean', name: 'Show Set Temp Chart', role: 'switch' }, () => {
          createState(DP_CHART_WINDOW, true, { type: 'boolean', name: 'Show Window Chart', role: 'switch' }, () => {
          createState(DP_CHART_HUMID, true, { type: 'boolean', name: 'Show Humidity Chart', role: 'switch' }, () => { 
              createState(DP_PRIVACY, false, { type: 'boolean', name: 'Privacy Mode Toggle', role: 'switch' }, () => {
                  createState(DP_IMAGE_PATH, '', { type: 'string', name: 'Theme Image Path', role: 'text' }, () => {
                      
                      getListItems().forEach(item => {
                          if (existsState(item.oid)) on({ id: item.oid, change: 'any' }, () => renderSetup());
                          if (item.disableIf && existsState(item.disableIf)) on({ id: item.disableIf, change: 'any' }, () => renderSetup());
                      });
                      
                      on({ id: [DP_CHART_BRENNER, DP_CHART_VALVE, DP_CHART_ACT, DP_CHART_SET, DP_CHART_WINDOW, DP_CHART_HUMID], change: 'any' }, () => renderSetup());
                      on({ id: DP_IMAGE_PATH, change: 'any' }, () => renderSetup());
                      
                      renderSetup();
                  });
              });
          });});});});});});
      });
      

      setup2.jpg setup1.jpg

      ioBroker auf NUC (Celeron mit Ubuntu-Server)

      Homematic, HMIP, Hue, Unifi, Plex, Nest, Roborock, Google Assistant

      1 Antwort Letzte Antwort
      0
      • patricknitschP Online
        patricknitschP Online
        patricknitsch
        Developer
        schrieb am zuletzt editiert von
        #12

        @mrmuppet sehr beeindruckend! Kannst du auch mal zeigen, wie die Scripte für deine Thermostate, Lichter und Beschattungen aussehen?

        Systeme:

        • Proxmox 1 mit ioBroker, HomeAssistant, piHole
        • Proxmox 2 mit SOLECTRUS, Mosquitto, piHole und Frigate
        • RPi4 mit Octoprint

        Eigene Adapter: grohe-smarthome, solectrus-influxdb, schlueter-thermostat

        1 Antwort Letzte Antwort
        0
        • M Offline
          M Offline
          mrMuppet
          schrieb am zuletzt editiert von
          #13

          Hier ist auch mein aktuelles Heizungs-Dashboard.
          Es generiert ein eigenständiges, responsives HTML-Widget im modernen Material Design 3 (M3) Look, das sich direkt in Vis, Vis-2 oder anderen Visualisierungen einbinden lässt.

          Das Skript kombiniert die Steuerung der Thermostate mit dynamisch generierten ApexCharts für historische Daten – alles verpackt in sanft aufklappenden Kacheln.


          Die Funktionen im Überblick

          • Zentrale Übersicht: Eine große Hauptkachel zeigt den Status der Zentralheizung (Brenner-Modulation) und die Anzahl der aktuell heizenden Räume an.

          • M3 Room-Cards: Jeder Raum hat eine eigene, dynamisch eingefärbte Kachel.

          • Rot = Raum heizt aktuell.

          • Orange = Raum fordert Wärme, aber Zentralheizung ist aus.

          • Grau/Grün = Standby / Zieltemperatur erreicht.

          • Direkte Steuerung: Über die integrierten Plus- und Minus-Buttons lässt sich die Zieltemperatur (SET) direkt in der Kachel in 0,5er-Schritten anpassen.

          • Smarte Sensorik: Die Kacheln zeigen neben der Temperatur auch die aktuelle Luftfeuchtigkeit an. Ist ein Fenster geöffnet, warnt ein rot blinkendes Fenster-Icon.

          • Animierte ApexCharts: Klickt man auf eine Raumkachel, klappt sie weich auf und rendert sofort einen interaktiven Graphen der letzten 24 Stunden.

          • Graphen-Inhalt: Ist-Temperatur, Soll-Temperatur, Ventilöffnung, Brenner-Modulation, Luftfeuchtigkeit und farblich hinterlegte Zonen für Zeiträume mit geöffnetem Fenster.


          Voraussetzungen & Einrichtung

          Um das Skript nutzen zu können, wird Folgendes benötigt:

          1. InfluxDB: Eine laufende Instanz (im Skript definiert als influxdb.0), die die Verlaufsdaten loggt.
          2. Aliase: Die Thermostate und Sensoren sollten als Aliase angelegt sein. Das Skript erwartet pro Raum Datenpunkte für ACTUAL (Ist-Temperatur), SET (Soll-Temperatur) und idealerweise VALVE (Ventilöffnung in %). Optional: Fenster-Kontakte (STATE) und Luftfeuchtigkeit (HUMIDITY).

          So wird es eingerichtet:

          1. Ein neues TypeScript im Javascript-Adapter anlegen und den Code einfügen.
          2. Oben im Skript den INFLUX_INSTANCE Namen anpassen, falls eine andere Instanz genutzt wird.
          3. Das rooms-Array an die eigenen Alias-Pfade und Raumnamen anpassen. Das Grid skaliert danach automatisch.
          4. Das Skript starten. Es generiert automatisch alle nötigen Konfigurations-Datenpunkte sowie den finalen HTML-Datenpunkt: 0_userdata.0.dashboard.heizungHTML.
          5. Diesen HTML-Datenpunkt in der Vis einfach als "Basis - String (unescaped)" oder HTML-Widget einbinden.

          Anpassung der Graphen

          Das Skript legt unter 0_userdata.0.dashboard. automatisch mehrere Switch-Datenpunkte an (z. B. chartShowBrenner, chartShowWindow). Hierüber lässt sich jederzeit global und live umschalten, welche Linien in den Charts der Räume gezeichnet werden sollen.

          Viel Spaß beim Ausprobieren und Anpassen! Wenn es Fragen gibt, gerne melden.

          Screenshot_20260608-140056.png

          // ============================================================
          // renderHeizung — M3 Material You Styling (9 Räume Grid) - v1.2
          // ioBroker TypeScript — Inkl. Luftfeuchtigkeit, SYNC-FIX & MULTI-FENSTER
          // CHANGELOG v1.2:
          // - FIX: ApexCharts Memory-Leak — chart.destroy() vor jedem Re-Render und beim Schließen.
          // - FIX: updateHistories() nutzt Promise.all — parallele InfluxDB-Abfragen pro Raum
          //        (45 sequentielle → ~5 parallele Batches, deutlich schneller).
          // - FIX: createState ohne existsState-Guard überschreibt bei Neustart.
          // - FIX: getSafeVal() null-Guard für getState() ergänzt.
          // - FIX: Alle getState()-Aufrufe in renderHeizung() mit null-Guard.
          // - FIX: createState Chart-Konfig-Datenpunkte fehlen — werden nun ebenfalls angelegt.
          // ============================================================
          
          const DP_HEIZUNG_HTML: string = '0_userdata.0.dashboard.heizungHTML';
          const DP_PRIVACY:      string = '0_userdata.0.dashboard.privacyMode';
          
          const DP_CHART_BRENNER: string = '0_userdata.0.dashboard.chartShowBrenner';
          const DP_CHART_VALVE:   string = '0_userdata.0.dashboard.chartShowValve';
          const DP_CHART_ACT:     string = '0_userdata.0.dashboard.chartShowAct';
          const DP_CHART_SET:     string = '0_userdata.0.dashboard.chartShowSet';
          const DP_CHART_WINDOW:  string = '0_userdata.0.dashboard.chartShowWindow';
          const DP_CHART_HUMID:   string = '0_userdata.0.dashboard.chartShowHumid';
          
          const DP_HEIZUNG_AN: string = '0_userdata.0.Heizung.Heizung_an';
          const DP_MODULATION: string = 'ems-esp.0.heatSources.actualModulation';
          
          const INFLUX_INSTANCE: string = 'influxdb.0';
          let globalHeatHistory: any[] = [];
          let roomHistories: Record<string, { act: any[]; set: any[]; valve: any[]; heat: any[]; window: any[]; humid: any[] }> = {};
          
          const M3: Record<string, string> = {
              primary:                 'var(--m3-primary, rgb(18, 66, 24))',
              onPrimary:               'var(--m3-on-primary, rgb(255, 255, 255))',
              primaryContainer:        'var(--m3-primary-container, rgb(43, 90, 45))',
              onPrimaryContainer:      'var(--m3-on-primary-container, rgb(155, 208, 151))',
              surfaceContainerLow:     'var(--m3-surface-container-low, rgb(243, 244, 237))',
              surfaceContainerHighest: 'var(--m3-surface-container-highest, rgb(225, 227, 220))',
              surfaceVariant:          'var(--m3-surface-variant, rgb(221, 229, 216))',
              onSurface:               'var(--m3-on-surface, rgb(25, 28, 24))',
              onSurfaceVariant:        'var(--m3-on-surface-variant, rgb(66, 73, 63))',
              heatPrimary:             'var(--m3-error, rgb(186, 26, 26))',
              heatContainer:           'var(--m3-error-container, rgb(255, 218, 214))',
              onHeatContainer:         'var(--m3-on-error-container, rgb(65, 0, 2))',
              pendingPrimary:          'rgb(176, 96, 0)',
              pendingContainer:        'rgb(255, 220, 193)',
              onPendingContainer:      'rgb(43, 20, 0)',
          };
          
          const ICONS: Record<string, string> = {
              thermostat: 'M15 13V5A3 3 0 0 0 9 5V13A5 5 0 1 0 15 13M12 4A1 1 0 0 1 13 5V8H11V5A1 1 0 0 1 12 4Z',
              fire:       'M17.5 11C15 7.5 13.5 8.5 12 6C10.5 8.5 9 7.5 6.5 11C4 14.5 5.5 19 12 19C18.5 19 20 14.5 17.5 11M12 17C9 17 8.5 14 10 12.5C11.5 11 12 12.5 12 12.5C12 12.5 12.5 11 14 12.5C15.5 14 15 17 12 17Z',
              minus:      'M19 13H5V11H19V13Z',
              plus:       'M19 13H13V19H11V13H5V11H11V5H13V11H19V13Z',
              power:      'M16.56 5.44L15.11 6.89C16.84 7.94 18 9.83 18 12A6 6 0 0 1 12 18A6 6 0 0 1 6 12C6 9.83 7.16 7.94 8.88 6.88L7.44 5.44C5.36 6.88 4 9.28 4 12A8 8 0 0 0 12 20A8 8 0 0 0 20 12C20 9.28 18.64 6.88 16.56 5.44M11 3H13V13H11V3Z',
              window:     'M3 3H21V21H3V3M5 5V11H11V5H5M13 5V11H19V5H13M5 13V19H11V13H5M13 13V19H19V13H13Z',
              water:      'M12 20a6 6 0 0 1-6-6c0-4 6-10.75 6-10.75S18 10 18 14a6 6 0 0 1-6 6z'
          };
          
          interface RoomHeater {
              id:          string;
              label:       string;
              anonLabel?:  string;
              actOid:      string;
              setOid:      string;
              valveOid:    string;
              windowOids?: string[];
              humidOid?:   string;
          }
          
          const rooms: RoomHeater[] = [
              { id: 'wohnzimmer',  label: 'Wohnzimmer',    actOid: 'alias.0.Heizung.Wohnzimmer.ACTUAL',   setOid: 'alias.0.Heizung.Wohnzimmer.SET',   valveOid: 'alias.0.Heizung.Wohnzimmer.VALVE',   windowOids: ['alias.0.Sicherheit.Wohnzimmer.Fenster.STATE', 'alias.0.Sicherheit.Wohnzimmer.Tuer.STATE'], humidOid: 'alias.0.Klima.Wohnzimmer.HUMIDITY' },
              { id: 'schlafzimmer',label: 'Zimmer Mina',    anonLabel: 'Schlafzimmer',  actOid: 'alias.0.Heizung.Schlafzimmer.ACTUAL', setOid: 'alias.0.Heizung.Schlafzimmer.SET', valveOid: 'alias.0.Heizung.Schlafzimmer.VALVE', windowOids: ['alias.0.Sicherheit.Schlafzimmer.Fenster.STATE'], humidOid: 'alias.0.Klima.Schlafzimmer.HUMIDITY' },
              { id: 'kind1',       label: 'Zimmer Nova',    anonLabel: 'Kinderzimmer 1', actOid: 'alias.0.Heizung.Kind1.ACTUAL',        setOid: 'alias.0.Heizung.Kind1.SET',        valveOid: 'alias.0.Heizung.Kind1.VALVE',        windowOids: ['alias.0.Sicherheit.Kinderzimmer1.Fenster.STATE'], humidOid: 'alias.0.Klima.Kind1.HUMIDITY' },
              { id: 'kind2',       label: 'Zimmer Vasco',   anonLabel: 'Kinderzimmer 2', actOid: 'alias.0.Heizung.Kind2.ACTUAL',        setOid: 'alias.0.Heizung.Kind2.SET',        valveOid: 'alias.0.Heizung.Kind2.VALVE',        windowOids: ['alias.0.Sicherheit.Kinderzimmer2.Fenster.STATE'], humidOid: 'alias.0.Klima.Kind2.HUMIDITY' },
              { id: 'bad',         label: 'Bad',            actOid: 'alias.0.Heizung.Bad.ACTUAL',          setOid: 'alias.0.Heizung.Bad.SET',          valveOid: 'alias.0.Heizung.Bad.VALVE',          windowOids: ['alias.0.Sicherheit.Bad.Fenster.STATE'], humidOid: 'alias.0.Klima.Bad.HUMIDITY' },
              { id: 'dachzimmer',  label: 'Dachzimmer',     actOid: 'alias.0.Heizung.Dachzimmer.ACTUAL',   setOid: 'alias.0.Heizung.Dachzimmer.SET',   valveOid: 'alias.0.Heizung.Dachzimmer.VALVE',   windowOids: ['alias.0.Sicherheit.Dachzimmer.Fenster_Garten.STATE', 'alias.0.Sicherheit.Dachzimmer.Fenster_Strasse.STATE'], humidOid: 'alias.0.Klima.Dachzimmer.HUMIDITY' },
              { id: 'flur',        label: 'Flur',            actOid: 'alias.0.Heizung.Flur.ACTUAL',         setOid: 'alias.0.Heizung.Flur.SET',         valveOid: 'alias.0.Heizung.Flur.VALVE',         windowOids: ['alias.0.Sicherheit.Haustuer.STATE'], humidOid: 'alias.0.Klima.Flur.HUMIDITY' },
              { id: 'gaesteklo',   label: 'Gäste WC',       actOid: 'alias.0.Heizung.GaesteKlo.ACTUAL',    setOid: 'alias.0.Heizung.GaesteKlo.SET',    valveOid: 'alias.0.Heizung.GaesteKlo.VALVE' },
              { id: 'hwr',         label: 'HWR',             actOid: 'alias.0.Heizung.HWR.ACTUAL',          setOid: 'alias.0.Heizung.HWR.SET',          valveOid: 'alias.0.Heizung.HWR.VALVE',          windowOids: ['alias.0.Sicherheit.HWR.Fenster.STATE'] },
          ];
          
          const COL_WIDTH:  number = 202;
          const HERO_WIDTH: number = 636;
          const CARD_HEIGHT:number = 144;
          const GUTTER:     number = 15;
          const SHADOW:     string = '0px 1px 2px 0px rgba(0,0,0,0.3), 0px 1px 3px 1px rgba(0,0,0,0.15)';
          
          // FIX v1.1: null-Guard für getState()
          function getSafeVal(oid: string | undefined, fallback: any = 0): any {
              if (oid && existsState(oid)) {
                  const s = getState(oid);
                  return s ? s.val : fallback;
              }
              return fallback;
          }
          
          function svgIcon(name: string, color: string, size: number = 24): string {
              const path = ICONS[name] || ICONS['thermostat'];
              return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" style="flex-shrink:0; transition: fill 0.3s ease;"><path fill="${color}" d="${path}"/></svg>`;
          }
          
          async function fetchInfluxData(oid: string | undefined, start: number, end: number, isRaw: boolean = false): Promise<any[]> {
              if (!oid || !existsState(oid)) return [];
              try {
                  const queryOptions = isRaw
                      ? { start, end, aggregate: 'none', limit: 400, ignoreNull: true }
                      : { start, end, step: 1800000, aggregate: 'average', ignoreNull: true };
          
                  const result = await new Promise<any>(resolve => {
                      sendTo(INFLUX_INSTANCE, 'getHistory', { id: oid, options: queryOptions }, res => resolve(res));
                  });
          
                  if (result && result.result && result.result.length > 0) {
                      let cleanData = result.result
                          .filter((r: any) => r.val !== null)
                          .map((r: any) => {
                              let v = r.val;
                              if (typeof v === 'boolean')      v = v ? 100 : 0;
                              else if (typeof v === 'string')  v = Number(v) || 0;
                              return [r.ts, Math.round(v * 10) / 10];
                          })
                          .filter((r: any) => !isNaN(r[1]));
          
                      if (isRaw && cleanData.length > 0) {
                          const maxVal = Math.max(...cleanData.map((d: any) => d[1]));
                          if (maxVal <= 1.0 && maxVal > 0) cleanData.forEach((d: any) => (d[1] = d[1] * 100));
                      }
                      return cleanData;
                  }
              } catch (e) {}
              return [];
          }
          
          async function updateHistories(): Promise<void> {
              const end   = new Date().getTime();
              const start = end - 24 * 60 * 60 * 1000;
          
              globalHeatHistory = await fetchInfluxData(DP_MODULATION, start, end, true);
          
              for (const room of rooms) {
                  // FIX v1.2: Promise.all — alle Sensoren eines Raums parallel abfragen
                  const [actData, setData, valveData, humidData] = await Promise.all([
                      fetchInfluxData(room.actOid,   start, end, false),
                      fetchInfluxData(room.setOid,   start, end, false),
                      fetchInfluxData(room.valveOid, start, end, true),
                      fetchInfluxData(room.humidOid, start, end, false),
                  ]);
          
                  // FIX v1.2: Fenster eines Raums ebenfalls parallel abfragen
                  let allWindowData: any[] = [];
                  if (room.windowOids && room.windowOids.length > 0) {
                      const wResults = await Promise.all(room.windowOids.map(wOid => fetchInfluxData(wOid, start, end, true)));
                      allWindowData = wResults.filter(wData => wData.length > 0);
                  }
          
                  roomHistories[room.id] = { act: actData, set: setData, valve: valveData, heat: globalHeatHistory, window: allWindowData, humid: humidData };
              }
              renderHeizung();
          }
          
          function renderHeroCard(activeRoomsCount: number, isZentralAn: boolean): string {
              const mod    = Number(getSafeVal(DP_MODULATION, 0)) || 0;
              const bg     = isZentralAn ? M3.primaryContainer : M3.surfaceContainerLow;
              const fg     = isZentralAn ? M3.onPrimaryContainer : M3.onSurfaceVariant;
              const iconBg = isZentralAn ? M3.primary : M3.surfaceContainerHighest;
              const iconFg = isZentralAn ? M3.onPrimary : M3.onSurfaceVariant;
              return `<div style="width:${HERO_WIDTH}px; height:${CARD_HEIGHT}px; border-radius:12px; padding:20px 24px; box-sizing:border-box; background:${bg}; box-shadow:${SHADOW}; display:flex; align-items:center; justify-content:space-between; font-family:Roboto,sans-serif; transition:all 0.3s ease;"> <div style="display:flex; align-items:center; gap:20px;"> <div style="width:56px; height:56px; border-radius:28px; background:${iconBg}; display:flex; align-items:center; justify-content:center; transition:all 0.3s ease;"> ${svgIcon(isZentralAn ? 'fire' : 'power', iconFg, 32)} </div> <div> <div style="font-size:24px; font-weight:500; color:${fg}; margin-bottom:4px;">Zentralheizung</div> <div style="font-size:15px; font-weight:500; color:${fg}; opacity:0.8;">${isZentralAn ? `Heizbetrieb aktiv • ${mod}% Leistung` : 'Standby / Aus'}</div> </div> </div> <div style="text-align:right;"> <div style="font-size:36px; font-weight:600; color:${isZentralAn ? M3.heatPrimary : fg}; line-height:1;">${activeRoomsCount}</div> <div style="font-size:13px; font-weight:500; color:${fg}; text-transform:uppercase; letter-spacing:0.5px; margin-top:4px;">Räume fordern Wärme</div> </div> </div>`;
          }
          
          function renderRoomCard(room: RoomHeater, isZentralAn: boolean, isPrivacyMode: boolean): string {
              const actVal     = Number(getSafeVal(room.actOid, 0)) || 0;
              const setVal     = Number(getSafeVal(room.setOid, 0)) || 0;
              const valveRaw   = getSafeVal(room.valveOid, 0);
              const humidVal   = room.humidOid ? Number(getSafeVal(room.humidOid, 0)) || 0 : null;
              const displayName = isPrivacyMode && room.anonLabel ? room.anonLabel : room.label;
          
              let windowOpen = false;
              if (room.windowOids && room.windowOids.length > 0) {
                  windowOpen = room.windowOids.some(oid => {
                      const raw = getSafeVal(oid, false);
                      return raw === true || raw === 1 || raw === '1' || raw === 'true';
                  });
              }
          
              const wantsHeat = (typeof valveRaw === 'number' && valveRaw > 0)
                  || (typeof valveRaw === 'string' && Number(valveRaw) > 0)
                  || valveRaw === true || valveRaw === 'true';
          
              let bg: string, fg: string, fgSub: string, iconBg: string, iconFg: string;
              if (wantsHeat && isZentralAn) {
                  bg = M3.heatContainer; fg = M3.onHeatContainer; fgSub = M3.onHeatContainer; iconBg = M3.heatPrimary; iconFg = '#ffffff';
              } else if (wantsHeat && !isZentralAn) {
                  bg = M3.pendingContainer; fg = M3.onPendingContainer; fgSub = M3.onPendingContainer; iconBg = M3.pendingPrimary; iconFg = '#ffffff';
              } else {
                  bg = M3.surfaceContainerLow; fg = M3.onSurface; fgSub = M3.onSurfaceVariant; iconBg = M3.surfaceContainerHighest; iconFg = M3.onSurfaceVariant;
              }
          
              const jsMinus = `(function(e, btn){ e.stopPropagation(); var card = btn.closest('.m3-room-card'); var span = card.querySelector('.target-temp'); var current = parseFloat(span.innerText); var newVal = current - 0.5; span.innerText = newVal.toFixed(1) + '°'; if(typeof vis!=='undefined'&&vis.conn&&vis.conn.setState) vis.conn.setState('${room.setOid}', newVal); else if(typeof vis!=='undefined'&&vis.setValue) vis.setValue('${room.setOid}', newVal); })(event, this);`;
              const jsPlus  = `(function(e, btn){ e.stopPropagation(); var card = btn.closest('.m3-room-card'); var span = card.querySelector('.target-temp'); var current = parseFloat(span.innerText); var newVal = current + 0.5; span.innerText = newVal.toFixed(1) + '°'; if(typeof vis!=='undefined'&&vis.conn&&vis.conn.setState) vis.conn.setState('${room.setOid}', newVal); else if(typeof vis!=='undefined'&&vis.setValue) vis.setValue('${room.setOid}', newVal); })(event, this);`;
          
              const windowHtml = windowOpen
                  ? `<div style="width:24px; height:24px; display:flex; align-items:center; justify-content:center; animation: blink 2s infinite;">${svgIcon('window', M3.heatPrimary, 20)}</div>`
                  : '';
          
              const humidHtml = (humidVal !== null && humidVal > 0)
                  ? `<div style="position:absolute; top:36px; right:-4px; width:40px; display:flex; justify-content:center; align-items:center; font-size:11px; font-weight:600; color:${fgSub}; opacity:0.85; letter-spacing:-0.5px;">${Math.round(humidVal)}%<svg width="10" height="10" viewBox="0 0 24 24" style="margin-left:1px"><path fill="currentColor" d="M12 20a6 6 0 0 1-6-6c0-4 6-10.75 6-10.75S18 10 18 14a6 6 0 0 1-6 6z"/></svg></div>`
                  : '';
          
              const historyData = roomHistories[room.id] ?? { act: [], set: [], valve: [], heat: [], window: [], humid: [] };
          
              return `<div id="wrapper_${room.id}" style="width:${COL_WIDTH}px; height:${CARD_HEIGHT}px; position:relative; z-index:1;">
                  <div id="card_${room.id}" class="m3-room-card" data-history='${JSON.stringify(historyData)}' onclick="if(typeof window.toggleCard === 'function') window.toggleCard(this, '${room.id}');" style="position:absolute; top:0; left:0; width:100%; height:100%; border-radius:12px; padding:14px 16px; box-sizing:border-box; background:${bg}; box-shadow:${SHADOW}; display:flex; flex-direction:column; font-family:Roboto,sans-serif; transition:all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); cursor:pointer; overflow:hidden;">
                      <div style="display:flex; justify-content:space-between; align-items:flex-start; width:100%;">
                          <div style="font-size:16px; font-weight:500; color:${fg}; max-width:145px; line-height:1.2; overflow:hidden; text-overflow:ellipsis; transition:color 0.3s ease;">${displayName}</div>
                          <div style="display:flex; gap:6px; align-items:flex-start; position:relative;">
                              ${windowHtml}
                              <div style="width:32px; height:32px; border-radius:16px; background:${iconBg}; display:flex; align-items:center; justify-content:center; flex-shrink:0;">${svgIcon('thermostat', iconFg, 18)}</div>
                              ${humidHtml}
                          </div>
                      </div>
                      <div style="display:flex; align-items:baseline; margin-top:-6px; width:100%;">
                          <div style="font-size:36px; font-weight:400; color:${fg}; letter-spacing:-1px;">${actVal.toFixed(1)}</div>
                          <div style="font-size:18px; font-weight:500; color:${fgSub}; margin-left:2px; opacity:0.7;">°C</div>
                      </div>
                      <div id="controls_${room.id}" style="display:flex; justify-content:space-between; align-items:center; width:100%; margin-top:auto;">
                          <div style="display:flex; flex-direction:column;">
                              <span style="font-size:11px; font-weight:500; text-transform:uppercase; letter-spacing:0.5px; color:${fgSub}; opacity:0.7;">Ziel</span>
                              <span class="target-temp" style="font-size:15px; font-weight:600; color:${fg};">${setVal.toFixed(1)}°</span>
                          </div>
                          <div style="display:flex; gap:6px;">
                              <div onclick="${jsMinus}" style="width:32px; height:32px; border-radius:16px; background:rgba(0,0,0,0.06); display:flex; align-items:center; justify-content:center; cursor:pointer; user-select:none; transition:opacity 0.1s; -webkit-tap-highlight-color:transparent;">${svgIcon('minus', fg, 20)}</div>
                              <div onclick="${jsPlus}"  style="width:32px; height:32px; border-radius:16px; background:rgba(0,0,0,0.06); display:flex; align-items:center; justify-content:center; cursor:pointer; user-select:none; transition:opacity 0.1s; -webkit-tap-highlight-color:transparent;">${svgIcon('plus', fg, 20)}</div>
                          </div>
                      </div>
                      <div id="chart_wrapper_${room.id}" style="opacity:0; height:0px; width:100%; transition:opacity 0.4s ease; margin-top:0px; pointer-events:none;">
                          <div id="chart_${room.id}" style="width:100%; height:100%;"></div>
                      </div>
                  </div>
              </div>`;
          }
          
          function renderHeizung(): void {
              let activeRooms = 0;
              const isZentralAn   = getSafeVal(DP_HEIZUNG_AN, false) === true;
              const isPrivacyMode = getSafeVal(DP_PRIVACY,     false) === true;
          
              const showBrenner = getSafeVal(DP_CHART_BRENNER, true)  === true;
              const showValve   = getSafeVal(DP_CHART_VALVE,   false) === true;
              const showAct     = getSafeVal(DP_CHART_ACT,     true)  === true;
              const showSet     = getSafeVal(DP_CHART_SET,     true)  === true;
              const showWindow  = getSafeVal(DP_CHART_WINDOW,  true)  === true;
              const showHumid   = getSafeVal(DP_CHART_HUMID,   true)  === true;
          
              for (const room of rooms) {
                  const valveRaw = getSafeVal(room.valveOid, 0);
                  if ((typeof valveRaw === 'number' && valveRaw > 0)
                      || (typeof valveRaw === 'string' && Number(valveRaw) > 0)
                      || valveRaw === true || valveRaw === 'true') activeRooms++;
              }
          
              let cardsHtml = renderHeroCard(activeRooms, isZentralAn);
              for (const room of rooms) cardsHtml += renderRoomCard(room, isZentralAn, isPrivacyMode);
          
              const html = `
          <style>
            @keyframes blink { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
            .m3-room-card * { white-space: nowrap; }
            .apexcharts-legend { padding: 0 !important; margin-top: -10px !important; }
            .apexcharts-legend-text { color: var(--m3-on-surface-variant) !important; font-family: Roboto, sans-serif !important; font-size: 11px !important; }
          </style>
          
          <div id="m3-dashboard-container"
               data-cfg-brenner="${showBrenner}"
               data-cfg-valve="${showValve}"
               data-cfg-act="${showAct}"
               data-cfg-set="${showSet}"
               data-cfg-window="${showWindow}"
               data-cfg-humid="${showHumid}"
               style="position: relative; background: var(--m3-surface-container-low, rgb(237, 239, 232)); border-radius: 0 0 28px 28px; padding: 24px; display: flex; flex-wrap: wrap; align-content: flex-start; gap: ${GUTTER}px; width: 901px; box-sizing: border-box; transition: background-color 0.3s ease;">
            ${cardsHtml}
          </div>
          
          <script>
            window.activeM3CardId = window.activeM3CardId || null;
            window.toggleCard = function(el, roomId) {
                if (window.activeM3CardId === roomId) { window.closeCard(el, roomId); return; }
                if (window.activeM3CardId) { const oldEl = document.getElementById('card_' + window.activeM3CardId); if (oldEl) window.closeCard(oldEl, window.activeM3CardId); }
                window.openCard(el, roomId);
            };
            window.closeCard = function(el, roomId) {
                el.style.width = '100%'; el.style.height = '100%'; el.style.boxShadow = '${SHADOW}';
                const chartWrapper = document.getElementById('chart_wrapper_' + roomId);
                if (chartWrapper) {
                    chartWrapper.style.opacity = '0';
                    setTimeout(() => {
                        chartWrapper.style.height = '0px';
                        const chartDiv = document.getElementById('chart_' + roomId);
                        if (chartDiv) {
                            if (chartDiv._apexChart) { chartDiv._apexChart.destroy(); delete chartDiv._apexChart; }
                            chartDiv.innerHTML = '';
                        }
                    }, 400);
                }
                setTimeout(() => { el.parentElement.style.zIndex = '1'; }, 400); window.activeM3CardId = null;
            };
            window.openCard = function(el, roomId) {
                window.activeM3CardId = roomId; const wrapper = el.parentElement; const container = document.getElementById('m3-dashboard-container');
                wrapper.style.zIndex = '999'; const wrapperRect = wrapper.getBoundingClientRect(); const containerRect = container.getBoundingClientRect();
                const expandedWidth = ${COL_WIDTH * 2 + GUTTER}; const expandedHeight = 310;
                el.style.top = 'auto'; el.style.bottom = 'auto'; el.style.left = 'auto'; el.style.right = 'auto';
                if (wrapperRect.left + expandedWidth > containerRect.right - 10) { el.style.right = '0'; } else { el.style.left = '0'; }
                if (wrapperRect.top + expandedHeight > containerRect.bottom - 10) { el.style.bottom = '0'; } else { el.style.top = '0'; }
                el.style.width = expandedWidth + 'px'; el.style.height = expandedHeight + 'px'; el.style.boxShadow = '0px 14px 28px rgba(0,0,0,0.35), 0px 10px 10px rgba(0,0,0,0.22)';
                const chartWrapper = document.getElementById('chart_wrapper_' + roomId);
                if (chartWrapper) { chartWrapper.style.height = '140px'; chartWrapper.style.marginTop = '16px'; setTimeout(() => { chartWrapper.style.opacity = '1'; window.renderChart(el, roomId); }, 450); }
            };
            window.renderChart = function(el, roomId) {
                const chartDiv = el.querySelector('#chart_' + roomId);
                if (!chartDiv || chartDiv.innerHTML.trim() !== '') return;
                const rawData = el.getAttribute('data-history'); let chartData = { act: [], set: [], valve: [], heat: [], window: [], humid: [] };
                try { chartData = JSON.parse(rawData); } catch(e) {}
                if (chartData.act.length === 0 && chartData.set.length === 0) { chartDiv.innerHTML = '<div style="color:var(--m3-on-surface-variant); font-size:13px; text-align:center; padding-top:50px; font-weight:500;">Keine Historie</div>'; return; }
                const rootStyles = getComputedStyle(document.documentElement);
                const colorIst     = rootStyles.getPropertyValue('--m3-primary').trim()  || '#4caf50';
                const colorSoll    = rootStyles.getPropertyValue('--m3-outline').trim()   || '#9e9e9e';
                const colorVentil  = rootStyles.getPropertyValue('--m3-error').trim()     || '#ffb4ab';
                const colorBrenner = '#ff9800';
                const colorFenster = '#00bfff';
                const colorHumid   = '#2196f3';
                const container    = el.closest('#m3-dashboard-container');
                const cfgShowBrenner = container.getAttribute('data-cfg-brenner') === 'true';
                const cfgShowValve   = container.getAttribute('data-cfg-valve')   === 'true';
                const cfgShowAct     = container.getAttribute('data-cfg-act')     === 'true';
                const cfgShowSet     = container.getAttribute('data-cfg-set')     === 'true';
                const cfgShowWindow  = container.getAttribute('data-cfg-window')  === 'true';
                const cfgShowHumid   = container.getAttribute('data-cfg-humid')   === 'true';
                function drawGraph() {
                    chartDiv.innerHTML = '';
                    var s_series = []; var s_colors = []; var s_curve = []; var s_width = []; var s_dash = []; var s_fillType = []; var s_opacity = []; var s_yaxis = [];
                    var actHasData = chartData.act && chartData.act.length > 0;
                    var setHasData = chartData.set && chartData.set.length > 0;
                    var minTemp = 999; var maxTemp = -999;
                    if (cfgShowAct && actHasData) { for (var i = 0; i < chartData.act.length; i++) { if (chartData.act[i][1] < minTemp) minTemp = chartData.act[i][1]; if (chartData.act[i][1] > maxTemp) maxTemp = chartData.act[i][1]; } }
                    if (cfgShowSet && setHasData) { for (var j = 0; j < chartData.set.length; j++) { if (chartData.set[j][1] < minTemp) minTemp = chartData.set[j][1]; if (chartData.set[j][1] > maxTemp) maxTemp = chartData.set[j][1]; } }
                    if (minTemp === 999) { minTemp = 15; maxTemp = 25; } else { minTemp = Math.floor(minTemp) - 1; maxTemp = Math.ceil(maxTemp) + 1; }
                    if (cfgShowHumid && chartData.humid && chartData.humid.length > 0) { s_series.push({ name: 'Luftfeuchte', type: 'area', data: chartData.humid }); s_colors.push(colorHumid); s_curve.push('smooth'); s_width.push(0); s_dash.push(0); s_fillType.push('solid'); s_opacity.push(0.1); s_yaxis.push({ min: 0, max: 100, show: false }); }
                    if (cfgShowBrenner) { s_series.push({ name: 'Brenner', type: 'area', data: chartData.heat || [] }); s_colors.push(colorBrenner); s_curve.push('smooth'); s_width.push(2); s_dash.push(0); s_fillType.push('gradient'); s_opacity.push(1); s_yaxis.push({ min: 0, max: 100, show: false }); }
                    if (cfgShowValve)   { s_series.push({ name: 'Ventil',  type: 'line', data: chartData.valve || [] }); s_colors.push(colorVentil);  s_curve.push('stepline'); s_width.push(2); s_dash.push(0); s_fillType.push('solid'); s_opacity.push(1); s_yaxis.push({ min: 0, max: 100, show: false }); }
                    if (cfgShowAct)     { s_series.push({ name: 'Ist-Temp',  type: 'area', data: chartData.act || [] }); s_colors.push(colorIst);    s_curve.push('smooth');   s_width.push(3); s_dash.push(0); s_fillType.push('gradient'); s_opacity.push(1); s_yaxis.push({ min: minTemp, max: maxTemp, show: false, tickAmount: 5 }); }
                    if (cfgShowSet)     { s_series.push({ name: 'Ziel-Temp', type: 'line', data: chartData.set || [] }); s_colors.push(colorSoll);   s_curve.push('stepline'); s_width.push(2); s_dash.push(4); s_fillType.push('solid');    s_opacity.push(1); s_yaxis.push({ min: minTemp, max: maxTemp, show: false, tickAmount: 5 }); }
                    var windowAnnotations = [];
                    if (cfgShowWindow && chartData.window && chartData.window.length > 0) {
                        for (var w = 0; w < chartData.window.length; w++) {
                            var wData = chartData.window[w]; var inWindowOpen = false; var winStart = null;
                            for (var k = 0; k < wData.length; k++) { var ts = wData[k][0]; var val = wData[k][1]; if (val > 0 && !inWindowOpen) { inWindowOpen = true; winStart = ts; } else if (val === 0 && inWindowOpen) { inWindowOpen = false; windowAnnotations.push({ x: winStart, x2: ts, fillColor: colorFenster, opacity: 0.15 }); } }
                            if (inWindowOpen && winStart) windowAnnotations.push({ x: winStart, x2: new Date().getTime(), fillColor: colorFenster, opacity: 0.15 });
                        }
                    }
                    if (cfgShowWindow) { let safeTime = (actHasData) ? chartData.act[0][0] : new Date().getTime() - 3600000; s_series.push({ name: 'Fenster offen', type: 'area', data: [[safeTime, 0]] }); s_colors.push(colorFenster); s_curve.push('stepline'); s_width.push(0); s_dash.push(0); s_fillType.push('solid'); s_opacity.push(0); s_yaxis.push({ min: minTemp, max: maxTemp, show: false, tickAmount: 5 }); }
                    if (s_series.length === 0) { chartDiv.innerHTML = '<div style="color:var(--m3-on-surface-variant); font-size:13px; text-align:center; padding-top:50px; font-weight:500;">Alle Graphen ausgeblendet</div>'; return; }
                    var options = { chart: { type: 'line', height: 140, width: '100%', parentHeightOffset: 0, toolbar: { show: false }, animations: { enabled: true, easing: 'easeinout', speed: 800, dynamicAnimation: { enabled: false } } }, series: s_series, colors: s_colors, stroke: { curve: s_curve, width: s_width, dashArray: s_dash }, fill: { type: s_fillType, opacity: s_opacity, gradient: { shadeIntensity: 1, opacityFrom: 0.45, opacityTo: 0.0, stops: [0, 100] } }, dataLabels: { enabled: false }, legend: { show: true, position: 'top', horizontalAlign: 'right' }, tooltip: { theme: 'dark', x: { format: 'HH:mm' } }, xaxis: { type: 'datetime', labels: { show: false }, axisBorder: { show: false }, axisTicks: { show: false }, tooltip: { enabled: false } }, yaxis: s_yaxis, grid: { show: false }, annotations: { xaxis: windowAnnotations } };
                    if (chartDiv._apexChart) { chartDiv._apexChart.destroy(); delete chartDiv._apexChart; }
                    try { var chart = new ApexCharts(chartDiv, options); chart.render(); chartDiv._apexChart = chart; }
                    catch(err) { console.error(err); chartDiv.innerHTML = '<div style="color:var(--m3-error, red); font-size:13px; text-align:center; padding-top:50px;">Graphen-Fehler</div>'; }
                }
                if (typeof ApexCharts === 'undefined') {
                    chartDiv.innerHTML = '<div style="color:var(--m3-on-surface-variant); font-size:13px; text-align:center; padding-top:50px; font-weight:500;">Lade Chart-Engine...</div>';
                    if (!window.apexLoadingPromise) { window.apexLoadingPromise = new Promise((resolve) => { const tempDefine = window.define; window.define = undefined; let script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/apexcharts@5.6.0/dist/apexcharts.min.js'; script.onload = () => { window.define = tempDefine; resolve(); }; document.head.appendChild(script); }); }
                    window.apexLoadingPromise.then(drawGraph);
                } else { drawGraph(); }
            };
          </script>`;
          
              setState(DP_HEIZUNG_HTML, html, true);
          }
          
          // === INIT ===
          // FIX v1.1: existsState-Guard + Chart-Konfig-Datenpunkte werden ebenfalls angelegt
          function startHeizungRenderer(): void {
              on({ id: [DP_CHART_BRENNER, DP_CHART_VALVE, DP_CHART_ACT, DP_CHART_SET, DP_CHART_WINDOW, DP_CHART_HUMID], change: 'any' }, () => renderHeizung());
              on({ id: DP_HEIZUNG_AN, change: 'any' }, () => renderHeizung());
              on({ id: DP_MODULATION,  change: 'any' }, () => renderHeizung());
              on({ id: DP_PRIVACY,     change: 'any' }, () => renderHeizung());
          
              rooms.forEach(room => {
                  if (room.actOid)   on({ id: room.actOid,   change: 'any' }, () => renderHeizung());
                  if (room.setOid)   on({ id: room.setOid,   change: 'any' }, () => renderHeizung());
                  if (room.valveOid) on({ id: room.valveOid, change: 'any' }, () => renderHeizung());
                  if (room.humidOid) on({ id: room.humidOid, change: 'any' }, () => renderHeizung());
                  if (room.windowOids) {
                      room.windowOids.forEach(oid => on({ id: oid, change: 'any' }, () => renderHeizung()));
                  }
              });
          
              updateHistories();
              schedule('*/15 * * * *', updateHistories);
              log('[Heizung] Renderer v1.1 gestartet.');
          }
          
          // Chart-Konfig-Datenpunkte anlegen falls nicht vorhanden, dann HTML-DP, dann starten
          const chartDPs: Array<{ id: string; val: boolean; name: string }> = [
              { id: DP_CHART_BRENNER, val: true,  name: 'Chart: Brenner anzeigen' },
              { id: DP_CHART_VALVE,   val: false, name: 'Chart: Ventil anzeigen'  },
              { id: DP_CHART_ACT,     val: true,  name: 'Chart: Ist-Temp anzeigen' },
              { id: DP_CHART_SET,     val: true,  name: 'Chart: Ziel-Temp anzeigen' },
              { id: DP_CHART_WINDOW,  val: true,  name: 'Chart: Fenster anzeigen'  },
              { id: DP_CHART_HUMID,   val: true,  name: 'Chart: Luftfeuchte anzeigen' },
          ];
          
          function ensureChartDPs(index: number, done: () => void): void {
              if (index >= chartDPs.length) { done(); return; }
              const dp = chartDPs[index];
              if (!existsState(dp.id)) {
                  createState(dp.id, dp.val, { type: 'boolean', name: dp.name, role: 'switch', read: true, write: true }, () => ensureChartDPs(index + 1, done));
              } else {
                  ensureChartDPs(index + 1, done);
              }
          }
          
          ensureChartDPs(0, () => {
              if (!existsState(DP_HEIZUNG_HTML)) {
                  createState(DP_HEIZUNG_HTML, '', { type: 'string', name: 'Heizung HTML', role: 'html' }, startHeizungRenderer);
              } else {
                  startHeizungRenderer();
              }
          });
          

          ioBroker auf NUC (Celeron mit Ubuntu-Server)

          Homematic, HMIP, Hue, Unifi, Plex, Nest, Roborock, Google Assistant

          sigi234S 1 Antwort Letzte Antwort
          0
          • M mrMuppet

            Hier ist auch mein aktuelles Heizungs-Dashboard.
            Es generiert ein eigenständiges, responsives HTML-Widget im modernen Material Design 3 (M3) Look, das sich direkt in Vis, Vis-2 oder anderen Visualisierungen einbinden lässt.

            Das Skript kombiniert die Steuerung der Thermostate mit dynamisch generierten ApexCharts für historische Daten – alles verpackt in sanft aufklappenden Kacheln.


            Die Funktionen im Überblick

            • Zentrale Übersicht: Eine große Hauptkachel zeigt den Status der Zentralheizung (Brenner-Modulation) und die Anzahl der aktuell heizenden Räume an.

            • M3 Room-Cards: Jeder Raum hat eine eigene, dynamisch eingefärbte Kachel.

            • Rot = Raum heizt aktuell.

            • Orange = Raum fordert Wärme, aber Zentralheizung ist aus.

            • Grau/Grün = Standby / Zieltemperatur erreicht.

            • Direkte Steuerung: Über die integrierten Plus- und Minus-Buttons lässt sich die Zieltemperatur (SET) direkt in der Kachel in 0,5er-Schritten anpassen.

            • Smarte Sensorik: Die Kacheln zeigen neben der Temperatur auch die aktuelle Luftfeuchtigkeit an. Ist ein Fenster geöffnet, warnt ein rot blinkendes Fenster-Icon.

            • Animierte ApexCharts: Klickt man auf eine Raumkachel, klappt sie weich auf und rendert sofort einen interaktiven Graphen der letzten 24 Stunden.

            • Graphen-Inhalt: Ist-Temperatur, Soll-Temperatur, Ventilöffnung, Brenner-Modulation, Luftfeuchtigkeit und farblich hinterlegte Zonen für Zeiträume mit geöffnetem Fenster.


            Voraussetzungen & Einrichtung

            Um das Skript nutzen zu können, wird Folgendes benötigt:

            1. InfluxDB: Eine laufende Instanz (im Skript definiert als influxdb.0), die die Verlaufsdaten loggt.
            2. Aliase: Die Thermostate und Sensoren sollten als Aliase angelegt sein. Das Skript erwartet pro Raum Datenpunkte für ACTUAL (Ist-Temperatur), SET (Soll-Temperatur) und idealerweise VALVE (Ventilöffnung in %). Optional: Fenster-Kontakte (STATE) und Luftfeuchtigkeit (HUMIDITY).

            So wird es eingerichtet:

            1. Ein neues TypeScript im Javascript-Adapter anlegen und den Code einfügen.
            2. Oben im Skript den INFLUX_INSTANCE Namen anpassen, falls eine andere Instanz genutzt wird.
            3. Das rooms-Array an die eigenen Alias-Pfade und Raumnamen anpassen. Das Grid skaliert danach automatisch.
            4. Das Skript starten. Es generiert automatisch alle nötigen Konfigurations-Datenpunkte sowie den finalen HTML-Datenpunkt: 0_userdata.0.dashboard.heizungHTML.
            5. Diesen HTML-Datenpunkt in der Vis einfach als "Basis - String (unescaped)" oder HTML-Widget einbinden.

            Anpassung der Graphen

            Das Skript legt unter 0_userdata.0.dashboard. automatisch mehrere Switch-Datenpunkte an (z. B. chartShowBrenner, chartShowWindow). Hierüber lässt sich jederzeit global und live umschalten, welche Linien in den Charts der Räume gezeichnet werden sollen.

            Viel Spaß beim Ausprobieren und Anpassen! Wenn es Fragen gibt, gerne melden.

            Screenshot_20260608-140056.png

            // ============================================================
            // renderHeizung — M3 Material You Styling (9 Räume Grid) - v1.2
            // ioBroker TypeScript — Inkl. Luftfeuchtigkeit, SYNC-FIX & MULTI-FENSTER
            // CHANGELOG v1.2:
            // - FIX: ApexCharts Memory-Leak — chart.destroy() vor jedem Re-Render und beim Schließen.
            // - FIX: updateHistories() nutzt Promise.all — parallele InfluxDB-Abfragen pro Raum
            //        (45 sequentielle → ~5 parallele Batches, deutlich schneller).
            // - FIX: createState ohne existsState-Guard überschreibt bei Neustart.
            // - FIX: getSafeVal() null-Guard für getState() ergänzt.
            // - FIX: Alle getState()-Aufrufe in renderHeizung() mit null-Guard.
            // - FIX: createState Chart-Konfig-Datenpunkte fehlen — werden nun ebenfalls angelegt.
            // ============================================================
            
            const DP_HEIZUNG_HTML: string = '0_userdata.0.dashboard.heizungHTML';
            const DP_PRIVACY:      string = '0_userdata.0.dashboard.privacyMode';
            
            const DP_CHART_BRENNER: string = '0_userdata.0.dashboard.chartShowBrenner';
            const DP_CHART_VALVE:   string = '0_userdata.0.dashboard.chartShowValve';
            const DP_CHART_ACT:     string = '0_userdata.0.dashboard.chartShowAct';
            const DP_CHART_SET:     string = '0_userdata.0.dashboard.chartShowSet';
            const DP_CHART_WINDOW:  string = '0_userdata.0.dashboard.chartShowWindow';
            const DP_CHART_HUMID:   string = '0_userdata.0.dashboard.chartShowHumid';
            
            const DP_HEIZUNG_AN: string = '0_userdata.0.Heizung.Heizung_an';
            const DP_MODULATION: string = 'ems-esp.0.heatSources.actualModulation';
            
            const INFLUX_INSTANCE: string = 'influxdb.0';
            let globalHeatHistory: any[] = [];
            let roomHistories: Record<string, { act: any[]; set: any[]; valve: any[]; heat: any[]; window: any[]; humid: any[] }> = {};
            
            const M3: Record<string, string> = {
                primary:                 'var(--m3-primary, rgb(18, 66, 24))',
                onPrimary:               'var(--m3-on-primary, rgb(255, 255, 255))',
                primaryContainer:        'var(--m3-primary-container, rgb(43, 90, 45))',
                onPrimaryContainer:      'var(--m3-on-primary-container, rgb(155, 208, 151))',
                surfaceContainerLow:     'var(--m3-surface-container-low, rgb(243, 244, 237))',
                surfaceContainerHighest: 'var(--m3-surface-container-highest, rgb(225, 227, 220))',
                surfaceVariant:          'var(--m3-surface-variant, rgb(221, 229, 216))',
                onSurface:               'var(--m3-on-surface, rgb(25, 28, 24))',
                onSurfaceVariant:        'var(--m3-on-surface-variant, rgb(66, 73, 63))',
                heatPrimary:             'var(--m3-error, rgb(186, 26, 26))',
                heatContainer:           'var(--m3-error-container, rgb(255, 218, 214))',
                onHeatContainer:         'var(--m3-on-error-container, rgb(65, 0, 2))',
                pendingPrimary:          'rgb(176, 96, 0)',
                pendingContainer:        'rgb(255, 220, 193)',
                onPendingContainer:      'rgb(43, 20, 0)',
            };
            
            const ICONS: Record<string, string> = {
                thermostat: 'M15 13V5A3 3 0 0 0 9 5V13A5 5 0 1 0 15 13M12 4A1 1 0 0 1 13 5V8H11V5A1 1 0 0 1 12 4Z',
                fire:       'M17.5 11C15 7.5 13.5 8.5 12 6C10.5 8.5 9 7.5 6.5 11C4 14.5 5.5 19 12 19C18.5 19 20 14.5 17.5 11M12 17C9 17 8.5 14 10 12.5C11.5 11 12 12.5 12 12.5C12 12.5 12.5 11 14 12.5C15.5 14 15 17 12 17Z',
                minus:      'M19 13H5V11H19V13Z',
                plus:       'M19 13H13V19H11V13H5V11H11V5H13V11H19V13Z',
                power:      'M16.56 5.44L15.11 6.89C16.84 7.94 18 9.83 18 12A6 6 0 0 1 12 18A6 6 0 0 1 6 12C6 9.83 7.16 7.94 8.88 6.88L7.44 5.44C5.36 6.88 4 9.28 4 12A8 8 0 0 0 12 20A8 8 0 0 0 20 12C20 9.28 18.64 6.88 16.56 5.44M11 3H13V13H11V3Z',
                window:     'M3 3H21V21H3V3M5 5V11H11V5H5M13 5V11H19V5H13M5 13V19H11V13H5M13 13V19H19V13H13Z',
                water:      'M12 20a6 6 0 0 1-6-6c0-4 6-10.75 6-10.75S18 10 18 14a6 6 0 0 1-6 6z'
            };
            
            interface RoomHeater {
                id:          string;
                label:       string;
                anonLabel?:  string;
                actOid:      string;
                setOid:      string;
                valveOid:    string;
                windowOids?: string[];
                humidOid?:   string;
            }
            
            const rooms: RoomHeater[] = [
                { id: 'wohnzimmer',  label: 'Wohnzimmer',    actOid: 'alias.0.Heizung.Wohnzimmer.ACTUAL',   setOid: 'alias.0.Heizung.Wohnzimmer.SET',   valveOid: 'alias.0.Heizung.Wohnzimmer.VALVE',   windowOids: ['alias.0.Sicherheit.Wohnzimmer.Fenster.STATE', 'alias.0.Sicherheit.Wohnzimmer.Tuer.STATE'], humidOid: 'alias.0.Klima.Wohnzimmer.HUMIDITY' },
                { id: 'schlafzimmer',label: 'Zimmer Mina',    anonLabel: 'Schlafzimmer',  actOid: 'alias.0.Heizung.Schlafzimmer.ACTUAL', setOid: 'alias.0.Heizung.Schlafzimmer.SET', valveOid: 'alias.0.Heizung.Schlafzimmer.VALVE', windowOids: ['alias.0.Sicherheit.Schlafzimmer.Fenster.STATE'], humidOid: 'alias.0.Klima.Schlafzimmer.HUMIDITY' },
                { id: 'kind1',       label: 'Zimmer Nova',    anonLabel: 'Kinderzimmer 1', actOid: 'alias.0.Heizung.Kind1.ACTUAL',        setOid: 'alias.0.Heizung.Kind1.SET',        valveOid: 'alias.0.Heizung.Kind1.VALVE',        windowOids: ['alias.0.Sicherheit.Kinderzimmer1.Fenster.STATE'], humidOid: 'alias.0.Klima.Kind1.HUMIDITY' },
                { id: 'kind2',       label: 'Zimmer Vasco',   anonLabel: 'Kinderzimmer 2', actOid: 'alias.0.Heizung.Kind2.ACTUAL',        setOid: 'alias.0.Heizung.Kind2.SET',        valveOid: 'alias.0.Heizung.Kind2.VALVE',        windowOids: ['alias.0.Sicherheit.Kinderzimmer2.Fenster.STATE'], humidOid: 'alias.0.Klima.Kind2.HUMIDITY' },
                { id: 'bad',         label: 'Bad',            actOid: 'alias.0.Heizung.Bad.ACTUAL',          setOid: 'alias.0.Heizung.Bad.SET',          valveOid: 'alias.0.Heizung.Bad.VALVE',          windowOids: ['alias.0.Sicherheit.Bad.Fenster.STATE'], humidOid: 'alias.0.Klima.Bad.HUMIDITY' },
                { id: 'dachzimmer',  label: 'Dachzimmer',     actOid: 'alias.0.Heizung.Dachzimmer.ACTUAL',   setOid: 'alias.0.Heizung.Dachzimmer.SET',   valveOid: 'alias.0.Heizung.Dachzimmer.VALVE',   windowOids: ['alias.0.Sicherheit.Dachzimmer.Fenster_Garten.STATE', 'alias.0.Sicherheit.Dachzimmer.Fenster_Strasse.STATE'], humidOid: 'alias.0.Klima.Dachzimmer.HUMIDITY' },
                { id: 'flur',        label: 'Flur',            actOid: 'alias.0.Heizung.Flur.ACTUAL',         setOid: 'alias.0.Heizung.Flur.SET',         valveOid: 'alias.0.Heizung.Flur.VALVE',         windowOids: ['alias.0.Sicherheit.Haustuer.STATE'], humidOid: 'alias.0.Klima.Flur.HUMIDITY' },
                { id: 'gaesteklo',   label: 'Gäste WC',       actOid: 'alias.0.Heizung.GaesteKlo.ACTUAL',    setOid: 'alias.0.Heizung.GaesteKlo.SET',    valveOid: 'alias.0.Heizung.GaesteKlo.VALVE' },
                { id: 'hwr',         label: 'HWR',             actOid: 'alias.0.Heizung.HWR.ACTUAL',          setOid: 'alias.0.Heizung.HWR.SET',          valveOid: 'alias.0.Heizung.HWR.VALVE',          windowOids: ['alias.0.Sicherheit.HWR.Fenster.STATE'] },
            ];
            
            const COL_WIDTH:  number = 202;
            const HERO_WIDTH: number = 636;
            const CARD_HEIGHT:number = 144;
            const GUTTER:     number = 15;
            const SHADOW:     string = '0px 1px 2px 0px rgba(0,0,0,0.3), 0px 1px 3px 1px rgba(0,0,0,0.15)';
            
            // FIX v1.1: null-Guard für getState()
            function getSafeVal(oid: string | undefined, fallback: any = 0): any {
                if (oid && existsState(oid)) {
                    const s = getState(oid);
                    return s ? s.val : fallback;
                }
                return fallback;
            }
            
            function svgIcon(name: string, color: string, size: number = 24): string {
                const path = ICONS[name] || ICONS['thermostat'];
                return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" style="flex-shrink:0; transition: fill 0.3s ease;"><path fill="${color}" d="${path}"/></svg>`;
            }
            
            async function fetchInfluxData(oid: string | undefined, start: number, end: number, isRaw: boolean = false): Promise<any[]> {
                if (!oid || !existsState(oid)) return [];
                try {
                    const queryOptions = isRaw
                        ? { start, end, aggregate: 'none', limit: 400, ignoreNull: true }
                        : { start, end, step: 1800000, aggregate: 'average', ignoreNull: true };
            
                    const result = await new Promise<any>(resolve => {
                        sendTo(INFLUX_INSTANCE, 'getHistory', { id: oid, options: queryOptions }, res => resolve(res));
                    });
            
                    if (result && result.result && result.result.length > 0) {
                        let cleanData = result.result
                            .filter((r: any) => r.val !== null)
                            .map((r: any) => {
                                let v = r.val;
                                if (typeof v === 'boolean')      v = v ? 100 : 0;
                                else if (typeof v === 'string')  v = Number(v) || 0;
                                return [r.ts, Math.round(v * 10) / 10];
                            })
                            .filter((r: any) => !isNaN(r[1]));
            
                        if (isRaw && cleanData.length > 0) {
                            const maxVal = Math.max(...cleanData.map((d: any) => d[1]));
                            if (maxVal <= 1.0 && maxVal > 0) cleanData.forEach((d: any) => (d[1] = d[1] * 100));
                        }
                        return cleanData;
                    }
                } catch (e) {}
                return [];
            }
            
            async function updateHistories(): Promise<void> {
                const end   = new Date().getTime();
                const start = end - 24 * 60 * 60 * 1000;
            
                globalHeatHistory = await fetchInfluxData(DP_MODULATION, start, end, true);
            
                for (const room of rooms) {
                    // FIX v1.2: Promise.all — alle Sensoren eines Raums parallel abfragen
                    const [actData, setData, valveData, humidData] = await Promise.all([
                        fetchInfluxData(room.actOid,   start, end, false),
                        fetchInfluxData(room.setOid,   start, end, false),
                        fetchInfluxData(room.valveOid, start, end, true),
                        fetchInfluxData(room.humidOid, start, end, false),
                    ]);
            
                    // FIX v1.2: Fenster eines Raums ebenfalls parallel abfragen
                    let allWindowData: any[] = [];
                    if (room.windowOids && room.windowOids.length > 0) {
                        const wResults = await Promise.all(room.windowOids.map(wOid => fetchInfluxData(wOid, start, end, true)));
                        allWindowData = wResults.filter(wData => wData.length > 0);
                    }
            
                    roomHistories[room.id] = { act: actData, set: setData, valve: valveData, heat: globalHeatHistory, window: allWindowData, humid: humidData };
                }
                renderHeizung();
            }
            
            function renderHeroCard(activeRoomsCount: number, isZentralAn: boolean): string {
                const mod    = Number(getSafeVal(DP_MODULATION, 0)) || 0;
                const bg     = isZentralAn ? M3.primaryContainer : M3.surfaceContainerLow;
                const fg     = isZentralAn ? M3.onPrimaryContainer : M3.onSurfaceVariant;
                const iconBg = isZentralAn ? M3.primary : M3.surfaceContainerHighest;
                const iconFg = isZentralAn ? M3.onPrimary : M3.onSurfaceVariant;
                return `<div style="width:${HERO_WIDTH}px; height:${CARD_HEIGHT}px; border-radius:12px; padding:20px 24px; box-sizing:border-box; background:${bg}; box-shadow:${SHADOW}; display:flex; align-items:center; justify-content:space-between; font-family:Roboto,sans-serif; transition:all 0.3s ease;"> <div style="display:flex; align-items:center; gap:20px;"> <div style="width:56px; height:56px; border-radius:28px; background:${iconBg}; display:flex; align-items:center; justify-content:center; transition:all 0.3s ease;"> ${svgIcon(isZentralAn ? 'fire' : 'power', iconFg, 32)} </div> <div> <div style="font-size:24px; font-weight:500; color:${fg}; margin-bottom:4px;">Zentralheizung</div> <div style="font-size:15px; font-weight:500; color:${fg}; opacity:0.8;">${isZentralAn ? `Heizbetrieb aktiv • ${mod}% Leistung` : 'Standby / Aus'}</div> </div> </div> <div style="text-align:right;"> <div style="font-size:36px; font-weight:600; color:${isZentralAn ? M3.heatPrimary : fg}; line-height:1;">${activeRoomsCount}</div> <div style="font-size:13px; font-weight:500; color:${fg}; text-transform:uppercase; letter-spacing:0.5px; margin-top:4px;">Räume fordern Wärme</div> </div> </div>`;
            }
            
            function renderRoomCard(room: RoomHeater, isZentralAn: boolean, isPrivacyMode: boolean): string {
                const actVal     = Number(getSafeVal(room.actOid, 0)) || 0;
                const setVal     = Number(getSafeVal(room.setOid, 0)) || 0;
                const valveRaw   = getSafeVal(room.valveOid, 0);
                const humidVal   = room.humidOid ? Number(getSafeVal(room.humidOid, 0)) || 0 : null;
                const displayName = isPrivacyMode && room.anonLabel ? room.anonLabel : room.label;
            
                let windowOpen = false;
                if (room.windowOids && room.windowOids.length > 0) {
                    windowOpen = room.windowOids.some(oid => {
                        const raw = getSafeVal(oid, false);
                        return raw === true || raw === 1 || raw === '1' || raw === 'true';
                    });
                }
            
                const wantsHeat = (typeof valveRaw === 'number' && valveRaw > 0)
                    || (typeof valveRaw === 'string' && Number(valveRaw) > 0)
                    || valveRaw === true || valveRaw === 'true';
            
                let bg: string, fg: string, fgSub: string, iconBg: string, iconFg: string;
                if (wantsHeat && isZentralAn) {
                    bg = M3.heatContainer; fg = M3.onHeatContainer; fgSub = M3.onHeatContainer; iconBg = M3.heatPrimary; iconFg = '#ffffff';
                } else if (wantsHeat && !isZentralAn) {
                    bg = M3.pendingContainer; fg = M3.onPendingContainer; fgSub = M3.onPendingContainer; iconBg = M3.pendingPrimary; iconFg = '#ffffff';
                } else {
                    bg = M3.surfaceContainerLow; fg = M3.onSurface; fgSub = M3.onSurfaceVariant; iconBg = M3.surfaceContainerHighest; iconFg = M3.onSurfaceVariant;
                }
            
                const jsMinus = `(function(e, btn){ e.stopPropagation(); var card = btn.closest('.m3-room-card'); var span = card.querySelector('.target-temp'); var current = parseFloat(span.innerText); var newVal = current - 0.5; span.innerText = newVal.toFixed(1) + '°'; if(typeof vis!=='undefined'&&vis.conn&&vis.conn.setState) vis.conn.setState('${room.setOid}', newVal); else if(typeof vis!=='undefined'&&vis.setValue) vis.setValue('${room.setOid}', newVal); })(event, this);`;
                const jsPlus  = `(function(e, btn){ e.stopPropagation(); var card = btn.closest('.m3-room-card'); var span = card.querySelector('.target-temp'); var current = parseFloat(span.innerText); var newVal = current + 0.5; span.innerText = newVal.toFixed(1) + '°'; if(typeof vis!=='undefined'&&vis.conn&&vis.conn.setState) vis.conn.setState('${room.setOid}', newVal); else if(typeof vis!=='undefined'&&vis.setValue) vis.setValue('${room.setOid}', newVal); })(event, this);`;
            
                const windowHtml = windowOpen
                    ? `<div style="width:24px; height:24px; display:flex; align-items:center; justify-content:center; animation: blink 2s infinite;">${svgIcon('window', M3.heatPrimary, 20)}</div>`
                    : '';
            
                const humidHtml = (humidVal !== null && humidVal > 0)
                    ? `<div style="position:absolute; top:36px; right:-4px; width:40px; display:flex; justify-content:center; align-items:center; font-size:11px; font-weight:600; color:${fgSub}; opacity:0.85; letter-spacing:-0.5px;">${Math.round(humidVal)}%<svg width="10" height="10" viewBox="0 0 24 24" style="margin-left:1px"><path fill="currentColor" d="M12 20a6 6 0 0 1-6-6c0-4 6-10.75 6-10.75S18 10 18 14a6 6 0 0 1-6 6z"/></svg></div>`
                    : '';
            
                const historyData = roomHistories[room.id] ?? { act: [], set: [], valve: [], heat: [], window: [], humid: [] };
            
                return `<div id="wrapper_${room.id}" style="width:${COL_WIDTH}px; height:${CARD_HEIGHT}px; position:relative; z-index:1;">
                    <div id="card_${room.id}" class="m3-room-card" data-history='${JSON.stringify(historyData)}' onclick="if(typeof window.toggleCard === 'function') window.toggleCard(this, '${room.id}');" style="position:absolute; top:0; left:0; width:100%; height:100%; border-radius:12px; padding:14px 16px; box-sizing:border-box; background:${bg}; box-shadow:${SHADOW}; display:flex; flex-direction:column; font-family:Roboto,sans-serif; transition:all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); cursor:pointer; overflow:hidden;">
                        <div style="display:flex; justify-content:space-between; align-items:flex-start; width:100%;">
                            <div style="font-size:16px; font-weight:500; color:${fg}; max-width:145px; line-height:1.2; overflow:hidden; text-overflow:ellipsis; transition:color 0.3s ease;">${displayName}</div>
                            <div style="display:flex; gap:6px; align-items:flex-start; position:relative;">
                                ${windowHtml}
                                <div style="width:32px; height:32px; border-radius:16px; background:${iconBg}; display:flex; align-items:center; justify-content:center; flex-shrink:0;">${svgIcon('thermostat', iconFg, 18)}</div>
                                ${humidHtml}
                            </div>
                        </div>
                        <div style="display:flex; align-items:baseline; margin-top:-6px; width:100%;">
                            <div style="font-size:36px; font-weight:400; color:${fg}; letter-spacing:-1px;">${actVal.toFixed(1)}</div>
                            <div style="font-size:18px; font-weight:500; color:${fgSub}; margin-left:2px; opacity:0.7;">°C</div>
                        </div>
                        <div id="controls_${room.id}" style="display:flex; justify-content:space-between; align-items:center; width:100%; margin-top:auto;">
                            <div style="display:flex; flex-direction:column;">
                                <span style="font-size:11px; font-weight:500; text-transform:uppercase; letter-spacing:0.5px; color:${fgSub}; opacity:0.7;">Ziel</span>
                                <span class="target-temp" style="font-size:15px; font-weight:600; color:${fg};">${setVal.toFixed(1)}°</span>
                            </div>
                            <div style="display:flex; gap:6px;">
                                <div onclick="${jsMinus}" style="width:32px; height:32px; border-radius:16px; background:rgba(0,0,0,0.06); display:flex; align-items:center; justify-content:center; cursor:pointer; user-select:none; transition:opacity 0.1s; -webkit-tap-highlight-color:transparent;">${svgIcon('minus', fg, 20)}</div>
                                <div onclick="${jsPlus}"  style="width:32px; height:32px; border-radius:16px; background:rgba(0,0,0,0.06); display:flex; align-items:center; justify-content:center; cursor:pointer; user-select:none; transition:opacity 0.1s; -webkit-tap-highlight-color:transparent;">${svgIcon('plus', fg, 20)}</div>
                            </div>
                        </div>
                        <div id="chart_wrapper_${room.id}" style="opacity:0; height:0px; width:100%; transition:opacity 0.4s ease; margin-top:0px; pointer-events:none;">
                            <div id="chart_${room.id}" style="width:100%; height:100%;"></div>
                        </div>
                    </div>
                </div>`;
            }
            
            function renderHeizung(): void {
                let activeRooms = 0;
                const isZentralAn   = getSafeVal(DP_HEIZUNG_AN, false) === true;
                const isPrivacyMode = getSafeVal(DP_PRIVACY,     false) === true;
            
                const showBrenner = getSafeVal(DP_CHART_BRENNER, true)  === true;
                const showValve   = getSafeVal(DP_CHART_VALVE,   false) === true;
                const showAct     = getSafeVal(DP_CHART_ACT,     true)  === true;
                const showSet     = getSafeVal(DP_CHART_SET,     true)  === true;
                const showWindow  = getSafeVal(DP_CHART_WINDOW,  true)  === true;
                const showHumid   = getSafeVal(DP_CHART_HUMID,   true)  === true;
            
                for (const room of rooms) {
                    const valveRaw = getSafeVal(room.valveOid, 0);
                    if ((typeof valveRaw === 'number' && valveRaw > 0)
                        || (typeof valveRaw === 'string' && Number(valveRaw) > 0)
                        || valveRaw === true || valveRaw === 'true') activeRooms++;
                }
            
                let cardsHtml = renderHeroCard(activeRooms, isZentralAn);
                for (const room of rooms) cardsHtml += renderRoomCard(room, isZentralAn, isPrivacyMode);
            
                const html = `
            <style>
              @keyframes blink { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
              .m3-room-card * { white-space: nowrap; }
              .apexcharts-legend { padding: 0 !important; margin-top: -10px !important; }
              .apexcharts-legend-text { color: var(--m3-on-surface-variant) !important; font-family: Roboto, sans-serif !important; font-size: 11px !important; }
            </style>
            
            <div id="m3-dashboard-container"
                 data-cfg-brenner="${showBrenner}"
                 data-cfg-valve="${showValve}"
                 data-cfg-act="${showAct}"
                 data-cfg-set="${showSet}"
                 data-cfg-window="${showWindow}"
                 data-cfg-humid="${showHumid}"
                 style="position: relative; background: var(--m3-surface-container-low, rgb(237, 239, 232)); border-radius: 0 0 28px 28px; padding: 24px; display: flex; flex-wrap: wrap; align-content: flex-start; gap: ${GUTTER}px; width: 901px; box-sizing: border-box; transition: background-color 0.3s ease;">
              ${cardsHtml}
            </div>
            
            <script>
              window.activeM3CardId = window.activeM3CardId || null;
              window.toggleCard = function(el, roomId) {
                  if (window.activeM3CardId === roomId) { window.closeCard(el, roomId); return; }
                  if (window.activeM3CardId) { const oldEl = document.getElementById('card_' + window.activeM3CardId); if (oldEl) window.closeCard(oldEl, window.activeM3CardId); }
                  window.openCard(el, roomId);
              };
              window.closeCard = function(el, roomId) {
                  el.style.width = '100%'; el.style.height = '100%'; el.style.boxShadow = '${SHADOW}';
                  const chartWrapper = document.getElementById('chart_wrapper_' + roomId);
                  if (chartWrapper) {
                      chartWrapper.style.opacity = '0';
                      setTimeout(() => {
                          chartWrapper.style.height = '0px';
                          const chartDiv = document.getElementById('chart_' + roomId);
                          if (chartDiv) {
                              if (chartDiv._apexChart) { chartDiv._apexChart.destroy(); delete chartDiv._apexChart; }
                              chartDiv.innerHTML = '';
                          }
                      }, 400);
                  }
                  setTimeout(() => { el.parentElement.style.zIndex = '1'; }, 400); window.activeM3CardId = null;
              };
              window.openCard = function(el, roomId) {
                  window.activeM3CardId = roomId; const wrapper = el.parentElement; const container = document.getElementById('m3-dashboard-container');
                  wrapper.style.zIndex = '999'; const wrapperRect = wrapper.getBoundingClientRect(); const containerRect = container.getBoundingClientRect();
                  const expandedWidth = ${COL_WIDTH * 2 + GUTTER}; const expandedHeight = 310;
                  el.style.top = 'auto'; el.style.bottom = 'auto'; el.style.left = 'auto'; el.style.right = 'auto';
                  if (wrapperRect.left + expandedWidth > containerRect.right - 10) { el.style.right = '0'; } else { el.style.left = '0'; }
                  if (wrapperRect.top + expandedHeight > containerRect.bottom - 10) { el.style.bottom = '0'; } else { el.style.top = '0'; }
                  el.style.width = expandedWidth + 'px'; el.style.height = expandedHeight + 'px'; el.style.boxShadow = '0px 14px 28px rgba(0,0,0,0.35), 0px 10px 10px rgba(0,0,0,0.22)';
                  const chartWrapper = document.getElementById('chart_wrapper_' + roomId);
                  if (chartWrapper) { chartWrapper.style.height = '140px'; chartWrapper.style.marginTop = '16px'; setTimeout(() => { chartWrapper.style.opacity = '1'; window.renderChart(el, roomId); }, 450); }
              };
              window.renderChart = function(el, roomId) {
                  const chartDiv = el.querySelector('#chart_' + roomId);
                  if (!chartDiv || chartDiv.innerHTML.trim() !== '') return;
                  const rawData = el.getAttribute('data-history'); let chartData = { act: [], set: [], valve: [], heat: [], window: [], humid: [] };
                  try { chartData = JSON.parse(rawData); } catch(e) {}
                  if (chartData.act.length === 0 && chartData.set.length === 0) { chartDiv.innerHTML = '<div style="color:var(--m3-on-surface-variant); font-size:13px; text-align:center; padding-top:50px; font-weight:500;">Keine Historie</div>'; return; }
                  const rootStyles = getComputedStyle(document.documentElement);
                  const colorIst     = rootStyles.getPropertyValue('--m3-primary').trim()  || '#4caf50';
                  const colorSoll    = rootStyles.getPropertyValue('--m3-outline').trim()   || '#9e9e9e';
                  const colorVentil  = rootStyles.getPropertyValue('--m3-error').trim()     || '#ffb4ab';
                  const colorBrenner = '#ff9800';
                  const colorFenster = '#00bfff';
                  const colorHumid   = '#2196f3';
                  const container    = el.closest('#m3-dashboard-container');
                  const cfgShowBrenner = container.getAttribute('data-cfg-brenner') === 'true';
                  const cfgShowValve   = container.getAttribute('data-cfg-valve')   === 'true';
                  const cfgShowAct     = container.getAttribute('data-cfg-act')     === 'true';
                  const cfgShowSet     = container.getAttribute('data-cfg-set')     === 'true';
                  const cfgShowWindow  = container.getAttribute('data-cfg-window')  === 'true';
                  const cfgShowHumid   = container.getAttribute('data-cfg-humid')   === 'true';
                  function drawGraph() {
                      chartDiv.innerHTML = '';
                      var s_series = []; var s_colors = []; var s_curve = []; var s_width = []; var s_dash = []; var s_fillType = []; var s_opacity = []; var s_yaxis = [];
                      var actHasData = chartData.act && chartData.act.length > 0;
                      var setHasData = chartData.set && chartData.set.length > 0;
                      var minTemp = 999; var maxTemp = -999;
                      if (cfgShowAct && actHasData) { for (var i = 0; i < chartData.act.length; i++) { if (chartData.act[i][1] < minTemp) minTemp = chartData.act[i][1]; if (chartData.act[i][1] > maxTemp) maxTemp = chartData.act[i][1]; } }
                      if (cfgShowSet && setHasData) { for (var j = 0; j < chartData.set.length; j++) { if (chartData.set[j][1] < minTemp) minTemp = chartData.set[j][1]; if (chartData.set[j][1] > maxTemp) maxTemp = chartData.set[j][1]; } }
                      if (minTemp === 999) { minTemp = 15; maxTemp = 25; } else { minTemp = Math.floor(minTemp) - 1; maxTemp = Math.ceil(maxTemp) + 1; }
                      if (cfgShowHumid && chartData.humid && chartData.humid.length > 0) { s_series.push({ name: 'Luftfeuchte', type: 'area', data: chartData.humid }); s_colors.push(colorHumid); s_curve.push('smooth'); s_width.push(0); s_dash.push(0); s_fillType.push('solid'); s_opacity.push(0.1); s_yaxis.push({ min: 0, max: 100, show: false }); }
                      if (cfgShowBrenner) { s_series.push({ name: 'Brenner', type: 'area', data: chartData.heat || [] }); s_colors.push(colorBrenner); s_curve.push('smooth'); s_width.push(2); s_dash.push(0); s_fillType.push('gradient'); s_opacity.push(1); s_yaxis.push({ min: 0, max: 100, show: false }); }
                      if (cfgShowValve)   { s_series.push({ name: 'Ventil',  type: 'line', data: chartData.valve || [] }); s_colors.push(colorVentil);  s_curve.push('stepline'); s_width.push(2); s_dash.push(0); s_fillType.push('solid'); s_opacity.push(1); s_yaxis.push({ min: 0, max: 100, show: false }); }
                      if (cfgShowAct)     { s_series.push({ name: 'Ist-Temp',  type: 'area', data: chartData.act || [] }); s_colors.push(colorIst);    s_curve.push('smooth');   s_width.push(3); s_dash.push(0); s_fillType.push('gradient'); s_opacity.push(1); s_yaxis.push({ min: minTemp, max: maxTemp, show: false, tickAmount: 5 }); }
                      if (cfgShowSet)     { s_series.push({ name: 'Ziel-Temp', type: 'line', data: chartData.set || [] }); s_colors.push(colorSoll);   s_curve.push('stepline'); s_width.push(2); s_dash.push(4); s_fillType.push('solid');    s_opacity.push(1); s_yaxis.push({ min: minTemp, max: maxTemp, show: false, tickAmount: 5 }); }
                      var windowAnnotations = [];
                      if (cfgShowWindow && chartData.window && chartData.window.length > 0) {
                          for (var w = 0; w < chartData.window.length; w++) {
                              var wData = chartData.window[w]; var inWindowOpen = false; var winStart = null;
                              for (var k = 0; k < wData.length; k++) { var ts = wData[k][0]; var val = wData[k][1]; if (val > 0 && !inWindowOpen) { inWindowOpen = true; winStart = ts; } else if (val === 0 && inWindowOpen) { inWindowOpen = false; windowAnnotations.push({ x: winStart, x2: ts, fillColor: colorFenster, opacity: 0.15 }); } }
                              if (inWindowOpen && winStart) windowAnnotations.push({ x: winStart, x2: new Date().getTime(), fillColor: colorFenster, opacity: 0.15 });
                          }
                      }
                      if (cfgShowWindow) { let safeTime = (actHasData) ? chartData.act[0][0] : new Date().getTime() - 3600000; s_series.push({ name: 'Fenster offen', type: 'area', data: [[safeTime, 0]] }); s_colors.push(colorFenster); s_curve.push('stepline'); s_width.push(0); s_dash.push(0); s_fillType.push('solid'); s_opacity.push(0); s_yaxis.push({ min: minTemp, max: maxTemp, show: false, tickAmount: 5 }); }
                      if (s_series.length === 0) { chartDiv.innerHTML = '<div style="color:var(--m3-on-surface-variant); font-size:13px; text-align:center; padding-top:50px; font-weight:500;">Alle Graphen ausgeblendet</div>'; return; }
                      var options = { chart: { type: 'line', height: 140, width: '100%', parentHeightOffset: 0, toolbar: { show: false }, animations: { enabled: true, easing: 'easeinout', speed: 800, dynamicAnimation: { enabled: false } } }, series: s_series, colors: s_colors, stroke: { curve: s_curve, width: s_width, dashArray: s_dash }, fill: { type: s_fillType, opacity: s_opacity, gradient: { shadeIntensity: 1, opacityFrom: 0.45, opacityTo: 0.0, stops: [0, 100] } }, dataLabels: { enabled: false }, legend: { show: true, position: 'top', horizontalAlign: 'right' }, tooltip: { theme: 'dark', x: { format: 'HH:mm' } }, xaxis: { type: 'datetime', labels: { show: false }, axisBorder: { show: false }, axisTicks: { show: false }, tooltip: { enabled: false } }, yaxis: s_yaxis, grid: { show: false }, annotations: { xaxis: windowAnnotations } };
                      if (chartDiv._apexChart) { chartDiv._apexChart.destroy(); delete chartDiv._apexChart; }
                      try { var chart = new ApexCharts(chartDiv, options); chart.render(); chartDiv._apexChart = chart; }
                      catch(err) { console.error(err); chartDiv.innerHTML = '<div style="color:var(--m3-error, red); font-size:13px; text-align:center; padding-top:50px;">Graphen-Fehler</div>'; }
                  }
                  if (typeof ApexCharts === 'undefined') {
                      chartDiv.innerHTML = '<div style="color:var(--m3-on-surface-variant); font-size:13px; text-align:center; padding-top:50px; font-weight:500;">Lade Chart-Engine...</div>';
                      if (!window.apexLoadingPromise) { window.apexLoadingPromise = new Promise((resolve) => { const tempDefine = window.define; window.define = undefined; let script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/apexcharts@5.6.0/dist/apexcharts.min.js'; script.onload = () => { window.define = tempDefine; resolve(); }; document.head.appendChild(script); }); }
                      window.apexLoadingPromise.then(drawGraph);
                  } else { drawGraph(); }
              };
            </script>`;
            
                setState(DP_HEIZUNG_HTML, html, true);
            }
            
            // === INIT ===
            // FIX v1.1: existsState-Guard + Chart-Konfig-Datenpunkte werden ebenfalls angelegt
            function startHeizungRenderer(): void {
                on({ id: [DP_CHART_BRENNER, DP_CHART_VALVE, DP_CHART_ACT, DP_CHART_SET, DP_CHART_WINDOW, DP_CHART_HUMID], change: 'any' }, () => renderHeizung());
                on({ id: DP_HEIZUNG_AN, change: 'any' }, () => renderHeizung());
                on({ id: DP_MODULATION,  change: 'any' }, () => renderHeizung());
                on({ id: DP_PRIVACY,     change: 'any' }, () => renderHeizung());
            
                rooms.forEach(room => {
                    if (room.actOid)   on({ id: room.actOid,   change: 'any' }, () => renderHeizung());
                    if (room.setOid)   on({ id: room.setOid,   change: 'any' }, () => renderHeizung());
                    if (room.valveOid) on({ id: room.valveOid, change: 'any' }, () => renderHeizung());
                    if (room.humidOid) on({ id: room.humidOid, change: 'any' }, () => renderHeizung());
                    if (room.windowOids) {
                        room.windowOids.forEach(oid => on({ id: oid, change: 'any' }, () => renderHeizung()));
                    }
                });
            
                updateHistories();
                schedule('*/15 * * * *', updateHistories);
                log('[Heizung] Renderer v1.1 gestartet.');
            }
            
            // Chart-Konfig-Datenpunkte anlegen falls nicht vorhanden, dann HTML-DP, dann starten
            const chartDPs: Array<{ id: string; val: boolean; name: string }> = [
                { id: DP_CHART_BRENNER, val: true,  name: 'Chart: Brenner anzeigen' },
                { id: DP_CHART_VALVE,   val: false, name: 'Chart: Ventil anzeigen'  },
                { id: DP_CHART_ACT,     val: true,  name: 'Chart: Ist-Temp anzeigen' },
                { id: DP_CHART_SET,     val: true,  name: 'Chart: Ziel-Temp anzeigen' },
                { id: DP_CHART_WINDOW,  val: true,  name: 'Chart: Fenster anzeigen'  },
                { id: DP_CHART_HUMID,   val: true,  name: 'Chart: Luftfeuchte anzeigen' },
            ];
            
            function ensureChartDPs(index: number, done: () => void): void {
                if (index >= chartDPs.length) { done(); return; }
                const dp = chartDPs[index];
                if (!existsState(dp.id)) {
                    createState(dp.id, dp.val, { type: 'boolean', name: dp.name, role: 'switch', read: true, write: true }, () => ensureChartDPs(index + 1, done));
                } else {
                    ensureChartDPs(index + 1, done);
                }
            }
            
            ensureChartDPs(0, () => {
                if (!existsState(DP_HEIZUNG_HTML)) {
                    createState(DP_HEIZUNG_HTML, '', { type: 'string', name: 'Heizung HTML', role: 'html' }, startHeizungRenderer);
                } else {
                    startHeizungRenderer();
                }
            });
            
            sigi234S Online
            sigi234S Online
            sigi234
            Forum Testing Most Active
            schrieb am zuletzt editiert von sigi234
            #14

            @mrMuppet
            Hallo, kannst du das Skript auch für Historie Adapter machen?

            Bitte benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.
            Immer Daten sichern!

            1 Antwort Letzte Antwort
            0
            • M Offline
              M Offline
              mrMuppet
              schrieb am zuletzt editiert von
              #15

              So sollte es gehen:

              Das Schöne an ioBroker ist: Der history.0, der sql.0 und der influxdb.0 Adapter teilen sich exakt denselben Befehl (getHistory), um Daten abzufragen. Wir müssen also nur die Ziel-Instanz ändern und die Variablen-Namen der Übersicht halber anpassen.

              Hier ist das vollständig auf den history-Adapter (history.0) umgeschriebene Heizungs-Skript:

              // ============================================================
              // renderHeizung — M3 Material You Styling (9 Räume Grid) - v1.2 (History Edition)
              // ioBroker TypeScript — Inkl. Luftfeuchtigkeit, SYNC-FIX & MULTI-FENSTER
              // CHANGELOG v1.2:
              // - ANPASSUNG: Abfrage der Graphen-Daten von 'influxdb.0' auf den 
              //   Standard 'history.0' Adapter umgestellt.
              // - FIX: ApexCharts Memory-Leak — chart.destroy() vor jedem Re-Render und beim Schließen.
              // - FIX: updateHistories() nutzt Promise.all — parallele Abfragen pro Raum
              //        (deutlich schneller).
              // - FIX: createState ohne existsState-Guard überschreibt bei Neustart.
              // ============================================================
              
              const DP_HEIZUNG_HTML: string = '0_userdata.0.dashboard.heizungHTML';
              const DP_PRIVACY:      string = '0_userdata.0.dashboard.privacyMode';
              
              const DP_CHART_BRENNER: string = '0_userdata.0.dashboard.chartShowBrenner';
              const DP_CHART_VALVE:   string = '0_userdata.0.dashboard.chartShowValve';
              const DP_CHART_ACT:     string = '0_userdata.0.dashboard.chartShowAct';
              const DP_CHART_SET:     string = '0_userdata.0.dashboard.chartShowSet';
              const DP_CHART_WINDOW:  string = '0_userdata.0.dashboard.chartShowWindow';
              const DP_CHART_HUMID:   string = '0_userdata.0.dashboard.chartShowHumid';
              
              const DP_HEIZUNG_AN: string = '0_userdata.0.Heizung.Heizung_an';
              const DP_MODULATION: string = 'ems-esp.0.heatSources.actualModulation';
              
              // INSTANZ DES HISTORY-ADAPTERS
              const HISTORY_INSTANCE: string = 'history.0';
              
              let globalHeatHistory: any[] = [];
              let roomHistories: Record<string, { act: any[]; set: any[]; valve: any[]; heat: any[]; window: any[]; humid: any[] }> = {};
              
              const M3: Record<string, string> = {
                  primary:                 'var(--m3-primary, rgb(18, 66, 24))',
                  onPrimary:               'var(--m3-on-primary, rgb(255, 255, 255))',
                  primaryContainer:        'var(--m3-primary-container, rgb(43, 90, 45))',
                  onPrimaryContainer:      'var(--m3-on-primary-container, rgb(155, 208, 151))',
                  surfaceContainerLow:     'var(--m3-surface-container-low, rgb(243, 244, 237))',
                  surfaceContainerHighest: 'var(--m3-surface-container-highest, rgb(225, 227, 220))',
                  surfaceVariant:          'var(--m3-surface-variant, rgb(221, 229, 216))',
                  onSurface:               'var(--m3-on-surface, rgb(25, 28, 24))',
                  onSurfaceVariant:        'var(--m3-on-surface-variant, rgb(66, 73, 63))',
                  heatPrimary:             'var(--m3-error, rgb(186, 26, 26))',
                  heatContainer:           'var(--m3-error-container, rgb(255, 218, 214))',
                  onHeatContainer:         'var(--m3-on-error-container, rgb(65, 0, 2))',
                  pendingPrimary:          'rgb(176, 96, 0)',
                  pendingContainer:        'rgb(255, 220, 193)',
                  onPendingContainer:      'rgb(43, 20, 0)',
              };
              
              const ICONS: Record<string, string> = {
                  thermostat: 'M15 13V5A3 3 0 0 0 9 5V13A5 5 0 1 0 15 13M12 4A1 1 0 0 1 13 5V8H11V5A1 1 0 0 1 12 4Z',
                  fire:       'M17.5 11C15 7.5 13.5 8.5 12 6C10.5 8.5 9 7.5 6.5 11C4 14.5 5.5 19 12 19C18.5 19 20 14.5 17.5 11M12 17C9 17 8.5 14 10 12.5C11.5 11 12 12.5 12 12.5C12 12.5 12.5 11 14 12.5C15.5 14 15 17 12 17Z',
                  minus:      'M19 13H5V11H19V13Z',
                  plus:       'M19 13H13V19H11V13H5V11H11V5H13V11H19V13Z',
                  power:      'M16.56 5.44L15.11 6.89C16.84 7.94 18 9.83 18 12A6 6 0 0 1 12 18A6 6 0 0 1 6 12C6 9.83 7.16 7.94 8.88 6.88L7.44 5.44C5.36 6.88 4 9.28 4 12A8 8 0 0 0 12 20A8 8 0 0 0 20 12C20 9.28 18.64 6.88 16.56 5.44M11 3H13V13H11V3Z',
                  window:     'M3 3H21V21H3V3M5 5V11H11V5H5M13 5V11H19V5H13M5 13V19H11V13H5M13 13V19H19V13H13Z',
                  water:      'M12 20a6 6 0 0 1-6-6c0-4 6-10.75 6-10.75S18 10 18 14a6 6 0 0 1-6 6z'
              };
              
              interface RoomHeater {
                  id:          string;
                  label:       string;
                  anonLabel?:  string;
                  actOid:      string;
                  setOid:      string;
                  valveOid:    string;
                  windowOids?: string[];
                  humidOid?:   string;
              }
              
              const rooms: RoomHeater[] = [
                  { id: 'wohnzimmer',  label: 'Wohnzimmer',    actOid: 'alias.0.Heizung.Wohnzimmer.ACTUAL',   setOid: 'alias.0.Heizung.Wohnzimmer.SET',   valveOid: 'alias.0.Heizung.Wohnzimmer.VALVE',   windowOids: ['alias.0.Sicherheit.Wohnzimmer.Fenster.STATE', 'alias.0.Sicherheit.Wohnzimmer.Tuer.STATE'], humidOid: 'alias.0.Klima.Wohnzimmer.HUMIDITY' },
                  { id: 'schlafzimmer',label: 'Zimmer Mina',    anonLabel: 'Schlafzimmer',  actOid: 'alias.0.Heizung.Schlafzimmer.ACTUAL', setOid: 'alias.0.Heizung.Schlafzimmer.SET', valveOid: 'alias.0.Heizung.Schlafzimmer.VALVE', windowOids: ['alias.0.Sicherheit.Schlafzimmer.Fenster.STATE'], humidOid: 'alias.0.Klima.Schlafzimmer.HUMIDITY' },
                  { id: 'kind1',       label: 'Zimmer Nova',    anonLabel: 'Kinderzimmer 1', actOid: 'alias.0.Heizung.Kind1.ACTUAL',        setOid: 'alias.0.Heizung.Kind1.SET',        valveOid: 'alias.0.Heizung.Kind1.VALVE',        windowOids: ['alias.0.Sicherheit.Kinderzimmer1.Fenster.STATE'], humidOid: 'alias.0.Klima.Kind1.HUMIDITY' },
                  { id: 'kind2',       label: 'Zimmer Vasco',   anonLabel: 'Kinderzimmer 2', actOid: 'alias.0.Heizung.Kind2.ACTUAL',        setOid: 'alias.0.Heizung.Kind2.SET',        valveOid: 'alias.0.Heizung.Kind2.VALVE',        windowOids: ['alias.0.Sicherheit.Kinderzimmer2.Fenster.STATE'], humidOid: 'alias.0.Klima.Kind2.HUMIDITY' },
                  { id: 'bad',         label: 'Bad',            actOid: 'alias.0.Heizung.Bad.ACTUAL',          setOid: 'alias.0.Heizung.Bad.SET',          valveOid: 'alias.0.Heizung.Bad.VALVE',          windowOids: ['alias.0.Sicherheit.Bad.Fenster.STATE'], humidOid: 'alias.0.Klima.Bad.HUMIDITY' },
                  { id: 'dachzimmer',  label: 'Dachzimmer',     actOid: 'alias.0.Heizung.Dachzimmer.ACTUAL',   setOid: 'alias.0.Heizung.Dachzimmer.SET',   valveOid: 'alias.0.Heizung.Dachzimmer.VALVE',   windowOids: ['alias.0.Sicherheit.Dachzimmer.Fenster_Garten.STATE', 'alias.0.Sicherheit.Dachzimmer.Fenster_Strasse.STATE'], humidOid: 'alias.0.Klima.Dachzimmer.HUMIDITY' },
                  { id: 'flur',        label: 'Flur',            actOid: 'alias.0.Heizung.Flur.ACTUAL',         setOid: 'alias.0.Heizung.Flur.SET',         valveOid: 'alias.0.Heizung.Flur.VALVE',         windowOids: ['alias.0.Sicherheit.Haustuer.STATE'], humidOid: 'alias.0.Klima.Flur.HUMIDITY' },
                  { id: 'gaesteklo',   label: 'Gäste WC',       actOid: 'alias.0.Heizung.GaesteKlo.ACTUAL',    setOid: 'alias.0.Heizung.GaesteKlo.SET',    valveOid: 'alias.0.Heizung.GaesteKlo.VALVE' },
                  { id: 'hwr',         label: 'HWR',             actOid: 'alias.0.Heizung.HWR.ACTUAL',          setOid: 'alias.0.Heizung.HWR.SET',          valveOid: 'alias.0.Heizung.HWR.VALVE',          windowOids: ['alias.0.Sicherheit.HWR.Fenster.STATE'] },
              ];
              
              const COL_WIDTH:  number = 202;
              const HERO_WIDTH: number = 636;
              const CARD_HEIGHT:number = 144;
              const GUTTER:     number = 15;
              const SHADOW:     string = '0px 1px 2px 0px rgba(0,0,0,0.3), 0px 1px 3px 1px rgba(0,0,0,0.15)';
              
              function getSafeVal(oid: string | undefined, fallback: any = 0): any {
                  if (oid && existsState(oid)) {
                      const s = getState(oid);
                      return s ? s.val : fallback;
                  }
                  return fallback;
              }
              
              function svgIcon(name: string, color: string, size: number = 24): string {
                  const path = ICONS[name] || ICONS['thermostat'];
                  return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" style="flex-shrink:0; transition: fill 0.3s ease;"><path fill="${color}" d="${path}"/></svg>`;
              }
              
              async function fetchHistoryData(oid: string | undefined, start: number, end: number, isRaw: boolean = false): Promise<any[]> {
                  if (!oid || !existsState(oid)) return [];
                  try {
                      const queryOptions = isRaw
                          ? { start, end, aggregate: 'none', limit: 400, ignoreNull: true }
                          : { start, end, step: 1800000, aggregate: 'average', ignoreNull: true };
              
                      const result = await new Promise<any>(resolve => {
                          sendTo(HISTORY_INSTANCE, 'getHistory', { id: oid, options: queryOptions }, res => resolve(res));
                      });
              
                      if (result && result.result && result.result.length > 0) {
                          let cleanData = result.result
                              .filter((r: any) => r.val !== null)
                              .map((r: any) => {
                                  let v = r.val;
                                  if (typeof v === 'boolean')      v = v ? 100 : 0;
                                  else if (typeof v === 'string')  v = Number(v) || 0;
                                  return [r.ts, Math.round(v * 10) / 10];
                              })
                              .filter((r: any) => !isNaN(r[1]));
              
                          if (isRaw && cleanData.length > 0) {
                              const maxVal = Math.max(...cleanData.map((d: any) => d[1]));
                              if (maxVal <= 1.0 && maxVal > 0) cleanData.forEach((d: any) => (d[1] = d[1] * 100));
                          }
                          return cleanData;
                      }
                  } catch (e) {}
                  return [];
              }
              
              async function updateHistories(): Promise<void> {
                  const end   = new Date().getTime();
                  const start = end - 24 * 60 * 60 * 1000;
              
                  globalHeatHistory = await fetchHistoryData(DP_MODULATION, start, end, true);
              
                  for (const room of rooms) {
                      const [actData, setData, valveData, humidData] = await Promise.all([
                          fetchHistoryData(room.actOid,   start, end, false),
                          fetchHistoryData(room.setOid,   start, end, false),
                          fetchHistoryData(room.valveOid, start, end, true),
                          fetchHistoryData(room.humidOid, start, end, false),
                      ]);
              
                      let allWindowData: any[] = [];
                      if (room.windowOids && room.windowOids.length > 0) {
                          const wResults = await Promise.all(room.windowOids.map(wOid => fetchHistoryData(wOid, start, end, true)));
                          allWindowData = wResults.filter(wData => wData.length > 0);
                      }
              
                      roomHistories[room.id] = { act: actData, set: setData, valve: valveData, heat: globalHeatHistory, window: allWindowData, humid: humidData };
                  }
                  renderHeizung();
              }
              
              function renderHeroCard(activeRoomsCount: number, isZentralAn: boolean): string {
                  const mod    = Number(getSafeVal(DP_MODULATION, 0)) || 0;
                  const bg     = isZentralAn ? M3.primaryContainer : M3.surfaceContainerLow;
                  const fg     = isZentralAn ? M3.onPrimaryContainer : M3.onSurfaceVariant;
                  const iconBg = isZentralAn ? M3.primary : M3.surfaceContainerHighest;
                  const iconFg = isZentralAn ? M3.onPrimary : M3.onSurfaceVariant;
                  return `<div style="width:${HERO_WIDTH}px; height:${CARD_HEIGHT}px; border-radius:12px; padding:20px 24px; box-sizing:border-box; background:${bg}; box-shadow:${SHADOW}; display:flex; align-items:center; justify-content:space-between; font-family:Roboto,sans-serif; transition:all 0.3s ease;"> <div style="display:flex; align-items:center; gap:20px;"> <div style="width:56px; height:56px; border-radius:28px; background:${iconBg}; display:flex; align-items:center; justify-content:center; transition:all 0.3s ease;"> ${svgIcon(isZentralAn ? 'fire' : 'power', iconFg, 32)} </div> <div> <div style="font-size:24px; font-weight:500; color:${fg}; margin-bottom:4px;">Zentralheizung</div> <div style="font-size:15px; font-weight:500; color:${fg}; opacity:0.8;">${isZentralAn ? `Heizbetrieb aktiv • ${mod}% Leistung` : 'Standby / Aus'}</div> </div> </div> <div style="text-align:right;"> <div style="font-size:36px; font-weight:600; color:${isZentralAn ? M3.heatPrimary : fg}; line-height:1;">${activeRoomsCount}</div> <div style="font-size:13px; font-weight:500; color:${fg}; text-transform:uppercase; letter-spacing:0.5px; margin-top:4px;">Räume fordern Wärme</div> </div> </div>`;
              }
              
              function renderRoomCard(room: RoomHeater, isZentralAn: boolean, isPrivacyMode: boolean): string {
                  const actVal     = Number(getSafeVal(room.actOid, 0)) || 0;
                  const setVal     = Number(getSafeVal(room.setOid, 0)) || 0;
                  const valveRaw   = getSafeVal(room.valveOid, 0);
                  const humidVal   = room.humidOid ? Number(getSafeVal(room.humidOid, 0)) || 0 : null;
                  const displayName = isPrivacyMode && room.anonLabel ? room.anonLabel : room.label;
              
                  let windowOpen = false;
                  if (room.windowOids && room.windowOids.length > 0) {
                      windowOpen = room.windowOids.some(oid => {
                          const raw = getSafeVal(oid, false);
                          return raw === true || raw === 1 || raw === '1' || raw === 'true';
                      });
                  }
              
                  const wantsHeat = (typeof valveRaw === 'number' && valveRaw > 0)
                      || (typeof valveRaw === 'string' && Number(valveRaw) > 0)
                      || valveRaw === true || valveRaw === 'true';
              
                  let bg: string, fg: string, fgSub: string, iconBg: string, iconFg: string;
                  if (wantsHeat && isZentralAn) {
                      bg = M3.heatContainer; fg = M3.onHeatContainer; fgSub = M3.onHeatContainer; iconBg = M3.heatPrimary; iconFg = '#ffffff';
                  } else if (wantsHeat && !isZentralAn) {
                      bg = M3.pendingContainer; fg = M3.onPendingContainer; fgSub = M3.onPendingContainer; iconBg = M3.pendingPrimary; iconFg = '#ffffff';
                  } else {
                      bg = M3.surfaceContainerLow; fg = M3.onSurface; fgSub = M3.onSurfaceVariant; iconBg = M3.surfaceContainerHighest; iconFg = M3.onSurfaceVariant;
                  }
              
                  const jsMinus = `(function(e, btn){ e.stopPropagation(); var card = btn.closest('.m3-room-card'); var span = card.querySelector('.target-temp'); var current = parseFloat(span.innerText); var newVal = current - 0.5; span.innerText = newVal.toFixed(1) + '°'; if(typeof vis!=='undefined'&&vis.conn&&vis.conn.setState) vis.conn.setState('${room.setOid}', newVal); else if(typeof vis!=='undefined'&&vis.setValue) vis.setValue('${room.setOid}', newVal); })(event, this);`;
                  const jsPlus  = `(function(e, btn){ e.stopPropagation(); var card = btn.closest('.m3-room-card'); var span = card.querySelector('.target-temp'); var current = parseFloat(span.innerText); var newVal = current + 0.5; span.innerText = newVal.toFixed(1) + '°'; if(typeof vis!=='undefined'&&vis.conn&&vis.conn.setState) vis.conn.setState('${room.setOid}', newVal); else if(typeof vis!=='undefined'&&vis.setValue) vis.setValue('${room.setOid}', newVal); })(event, this);`;
              
                  const windowHtml = windowOpen
                      ? `<div style="width:24px; height:24px; display:flex; align-items:center; justify-content:center; animation: blink 2s infinite;">${svgIcon('window', M3.heatPrimary, 20)}</div>`
                      : '';
              
                  const humidHtml = (humidVal !== null && humidVal > 0)
                      ? `<div style="position:absolute; top:36px; right:-4px; width:40px; display:flex; justify-content:center; align-items:center; font-size:11px; font-weight:600; color:${fgSub}; opacity:0.85; letter-spacing:-0.5px;">${Math.round(humidVal)}%<svg width="10" height="10" viewBox="0 0 24 24" style="margin-left:1px"><path fill="currentColor" d="M12 20a6 6 0 0 1-6-6c0-4 6-10.75 6-10.75S18 10 18 14a6 6 0 0 1-6 6z"/></svg></div>`
                      : '';
              
                  const historyData = roomHistories[room.id] ?? { act: [], set: [], valve: [], heat: [], window: [], humid: [] };
              
                  return `<div id="wrapper_${room.id}" style="width:${COL_WIDTH}px; height:${CARD_HEIGHT}px; position:relative; z-index:1;">
                      <div id="card_${room.id}" class="m3-room-card" data-history='${JSON.stringify(historyData)}' onclick="if(typeof window.toggleCard === 'function') window.toggleCard(this, '${room.id}');" style="position:absolute; top:0; left:0; width:100%; height:100%; border-radius:12px; padding:14px 16px; box-sizing:border-box; background:${bg}; box-shadow:${SHADOW}; display:flex; flex-direction:column; font-family:Roboto,sans-serif; transition:all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); cursor:pointer; overflow:hidden;">
                          <div style="display:flex; justify-content:space-between; align-items:flex-start; width:100%;">
                              <div style="font-size:16px; font-weight:500; color:${fg}; max-width:145px; line-height:1.2; overflow:hidden; text-overflow:ellipsis; transition:color 0.3s ease;">${displayName}</div>
                              <div style="display:flex; gap:6px; align-items:flex-start; position:relative;">
                                  ${windowHtml}
                                  <div style="width:32px; height:32px; border-radius:16px; background:${iconBg}; display:flex; align-items:center; justify-content:center; flex-shrink:0;">${svgIcon('thermostat', iconFg, 18)}</div>
                                  ${humidHtml}
                              </div>
                          </div>
                          <div style="display:flex; align-items:baseline; margin-top:-6px; width:100%;">
                              <div style="font-size:36px; font-weight:400; color:${fg}; letter-spacing:-1px;">${actVal.toFixed(1)}</div>
                              <div style="font-size:18px; font-weight:500; color:${fgSub}; margin-left:2px; opacity:0.7;">°C</div>
                          </div>
                          <div id="controls_${room.id}" style="display:flex; justify-content:space-between; align-items:center; width:100%; margin-top:auto;">
                              <div style="display:flex; flex-direction:column;">
                                  <span style="font-size:11px; font-weight:500; text-transform:uppercase; letter-spacing:0.5px; color:${fgSub}; opacity:0.7;">Ziel</span>
                                  <span class="target-temp" style="font-size:15px; font-weight:600; color:${fg};">${setVal.toFixed(1)}°</span>
                              </div>
                              <div style="display:flex; gap:6px;">
                                  <div onclick="${jsMinus}" style="width:32px; height:32px; border-radius:16px; background:rgba(0,0,0,0.06); display:flex; align-items:center; justify-content:center; cursor:pointer; user-select:none; transition:opacity 0.1s; -webkit-tap-highlight-color:transparent;">${svgIcon('minus', fg, 20)}</div>
                                  <div onclick="${jsPlus}"  style="width:32px; height:32px; border-radius:16px; background:rgba(0,0,0,0.06); display:flex; align-items:center; justify-content:center; cursor:pointer; user-select:none; transition:opacity 0.1s; -webkit-tap-highlight-color:transparent;">${svgIcon('plus', fg, 20)}</div>
                              </div>
                          </div>
                          <div id="chart_wrapper_${room.id}" style="opacity:0; height:0px; width:100%; transition:opacity 0.4s ease; margin-top:0px; pointer-events:none;">
                              <div id="chart_${room.id}" style="width:100%; height:100%;"></div>
                          </div>
                      </div>
                  </div>`;
              }
              
              function renderHeizung(): void {
                  let activeRooms = 0;
                  const isZentralAn   = getSafeVal(DP_HEIZUNG_AN, false) === true;
                  const isPrivacyMode = getSafeVal(DP_PRIVACY,     false) === true;
              
                  const showBrenner = getSafeVal(DP_CHART_BRENNER, true)  === true;
                  const showValve   = getSafeVal(DP_CHART_VALVE,   false) === true;
                  const showAct     = getSafeVal(DP_CHART_ACT,     true)  === true;
                  const showSet     = getSafeVal(DP_CHART_SET,     true)  === true;
                  const showWindow  = getSafeVal(DP_CHART_WINDOW,  true)  === true;
                  const showHumid   = getSafeVal(DP_CHART_HUMID,   true)  === true;
              
                  for (const room of rooms) {
                      const valveRaw = getSafeVal(room.valveOid, 0);
                      if ((typeof valveRaw === 'number' && valveRaw > 0)
                          || (typeof valveRaw === 'string' && Number(valveRaw) > 0)
                          || valveRaw === true || valveRaw === 'true') activeRooms++;
                  }
              
                  let cardsHtml = renderHeroCard(activeRooms, isZentralAn);
                  for (const room of rooms) cardsHtml += renderRoomCard(room, isZentralAn, isPrivacyMode);
              
                  const html = `
              <style>
                @keyframes blink { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
                .m3-room-card * { white-space: nowrap; }
                .apexcharts-legend { padding: 0 !important; margin-top: -10px !important; }
                .apexcharts-legend-text { color: var(--m3-on-surface-variant) !important; font-family: Roboto, sans-serif !important; font-size: 11px !important; }
              </style>
              
              <div id="m3-dashboard-container"
                   data-cfg-brenner="${showBrenner}"
                   data-cfg-valve="${showValve}"
                   data-cfg-act="${showAct}"
                   data-cfg-set="${showSet}"
                   data-cfg-window="${showWindow}"
                   data-cfg-humid="${showHumid}"
                   style="position: relative; background: var(--m3-surface-container-low, rgb(237, 239, 232)); border-radius: 0 0 28px 28px; padding: 24px; display: flex; flex-wrap: wrap; align-content: flex-start; gap: ${GUTTER}px; width: 901px; box-sizing: border-box; transition: background-color 0.3s ease;">
                ${cardsHtml}
              </div>
              
              <script>
                window.activeM3CardId = window.activeM3CardId || null;
                window.toggleCard = function(el, roomId) {
                    if (window.activeM3CardId === roomId) { window.closeCard(el, roomId); return; }
                    if (window.activeM3CardId) { const oldEl = document.getElementById('card_' + window.activeM3CardId); if (oldEl) window.closeCard(oldEl, window.activeM3CardId); }
                    window.openCard(el, roomId);
                };
                window.closeCard = function(el, roomId) {
                    el.style.width = '100%'; el.style.height = '100%'; el.style.boxShadow = '${SHADOW}';
                    const chartWrapper = document.getElementById('chart_wrapper_' + roomId);
                    if (chartWrapper) {
                        chartWrapper.style.opacity = '0';
                        setTimeout(() => {
                            chartWrapper.style.height = '0px';
                            const chartDiv = document.getElementById('chart_' + roomId);
                            if (chartDiv) {
                                if (chartDiv._apexChart) { chartDiv._apexChart.destroy(); delete chartDiv._apexChart; }
                                chartDiv.innerHTML = '';
                            }
                        }, 400);
                    }
                    setTimeout(() => { el.parentElement.style.zIndex = '1'; }, 400); window.activeM3CardId = null;
                };
                window.openCard = function(el, roomId) {
                    window.activeM3CardId = roomId; const wrapper = el.parentElement; const container = document.getElementById('m3-dashboard-container');
                    wrapper.style.zIndex = '999'; const wrapperRect = wrapper.getBoundingClientRect(); const containerRect = container.getBoundingClientRect();
                    const expandedWidth = ${COL_WIDTH * 2 + GUTTER}; const expandedHeight = 310;
                    el.style.top = 'auto'; el.style.bottom = 'auto'; el.style.left = 'auto'; el.style.right = 'auto';
                    if (wrapperRect.left + expandedWidth > containerRect.right - 10) { el.style.right = '0'; } else { el.style.left = '0'; }
                    if (wrapperRect.top + expandedHeight > containerRect.bottom - 10) { el.style.bottom = '0'; } else { el.style.top = '0'; }
                    el.style.width = expandedWidth + 'px'; el.style.height = expandedHeight + 'px'; el.style.boxShadow = '0px 14px 28px rgba(0,0,0,0.35), 0px 10px 10px rgba(0,0,0,0.22)';
                    const chartWrapper = document.getElementById('chart_wrapper_' + roomId);
                    if (chartWrapper) { chartWrapper.style.height = '140px'; chartWrapper.style.marginTop = '16px'; setTimeout(() => { chartWrapper.style.opacity = '1'; window.renderChart(el, roomId); }, 450); }
                };
                window.renderChart = function(el, roomId) {
                    const chartDiv = el.querySelector('#chart_' + roomId);
                    if (!chartDiv || chartDiv.innerHTML.trim() !== '') return;
                    const rawData = el.getAttribute('data-history'); let chartData = { act: [], set: [], valve: [], heat: [], window: [], humid: [] };
                    try { chartData = JSON.parse(rawData); } catch(e) {}
                    if (chartData.act.length === 0 && chartData.set.length === 0) { chartDiv.innerHTML = '<div style="color:var(--m3-on-surface-variant); font-size:13px; text-align:center; padding-top:50px; font-weight:500;">Keine Historie</div>'; return; }
                    const rootStyles = getComputedStyle(document.documentElement);
                    const colorIst     = rootStyles.getPropertyValue('--m3-primary').trim()  || '#4caf50';
                    const colorSoll    = rootStyles.getPropertyValue('--m3-outline').trim()   || '#9e9e9e';
                    const colorVentil  = rootStyles.getPropertyValue('--m3-error').trim()     || '#ffb4ab';
                    const colorBrenner = '#ff9800';
                    const colorFenster = '#00bfff';
                    const colorHumid   = '#2196f3';
                    const container    = el.closest('#m3-dashboard-container');
                    const cfgShowBrenner = container.getAttribute('data-cfg-brenner') === 'true';
                    const cfgShowValve   = container.getAttribute('data-cfg-valve')   === 'true';
                    const cfgShowAct     = container.getAttribute('data-cfg-act')     === 'true';
                    const cfgShowSet     = container.getAttribute('data-cfg-set')     === 'true';
                    const cfgShowWindow  = container.getAttribute('data-cfg-window')  === 'true';
                    const cfgShowHumid   = container.getAttribute('data-cfg-humid')   === 'true';
                    function drawGraph() {
                        chartDiv.innerHTML = '';
                        var s_series = []; var s_colors = []; var s_curve = []; var s_width = []; var s_dash = []; var s_fillType = []; var s_opacity = []; var s_yaxis = [];
                        var actHasData = chartData.act && chartData.act.length > 0;
                        var setHasData = chartData.set && chartData.set.length > 0;
                        var minTemp = 999; var maxTemp = -999;
                        if (cfgShowAct && actHasData) { for (var i = 0; i < chartData.act.length; i++) { if (chartData.act[i][1] < minTemp) minTemp = chartData.act[i][1]; if (chartData.act[i][1] > maxTemp) maxTemp = chartData.act[i][1]; } }
                        if (cfgShowSet && setHasData) { for (var j = 0; j < chartData.set.length; j++) { if (chartData.set[j][1] < minTemp) minTemp = chartData.set[j][1]; if (chartData.set[j][1] > maxTemp) maxTemp = chartData.set[j][1]; } }
                        if (minTemp === 999) { minTemp = 15; maxTemp = 25; } else { minTemp = Math.floor(minTemp) - 1; maxTemp = Math.ceil(maxTemp) + 1; }
                        if (cfgShowHumid && chartData.humid && chartData.humid.length > 0) { s_series.push({ name: 'Luftfeuchte', type: 'area', data: chartData.humid }); s_colors.push(colorHumid); s_curve.push('smooth'); s_width.push(0); s_dash.push(0); s_fillType.push('solid'); s_opacity.push(0.1); s_yaxis.push({ min: 0, max: 100, show: false }); }
                        if (cfgShowBrenner) { s_series.push({ name: 'Brenner', type: 'area', data: chartData.heat || [] }); s_colors.push(colorBrenner); s_curve.push('smooth'); s_width.push(2); s_dash.push(0); s_fillType.push('gradient'); s_opacity.push(1); s_yaxis.push({ min: 0, max: 100, show: false }); }
                        if (cfgShowValve)   { s_series.push({ name: 'Ventil',  type: 'line', data: chartData.valve || [] }); s_colors.push(colorVentil);  s_curve.push('stepline'); s_width.push(2); s_dash.push(0); s_fillType.push('solid'); s_opacity.push(1); s_yaxis.push({ min: 0, max: 100, show: false }); }
                        if (cfgShowAct)     { s_series.push({ name: 'Ist-Temp',  type: 'area', data: chartData.act || [] }); s_colors.push(colorIst);    s_curve.push('smooth');   s_width.push(3); s_dash.push(0); s_fillType.push('gradient'); s_opacity.push(1); s_yaxis.push({ min: minTemp, max: maxTemp, show: false, tickAmount: 5 }); }
                        if (cfgShowSet)     { s_series.push({ name: 'Ziel-Temp', type: 'line', data: chartData.set || [] }); s_colors.push(colorSoll);   s_curve.push('stepline'); s_width.push(2); s_dash.push(4); s_fillType.push('solid');    s_opacity.push(1); s_yaxis.push({ min: minTemp, max: maxTemp, show: false, tickAmount: 5 }); }
                        var windowAnnotations = [];
                        if (cfgShowWindow && chartData.window && chartData.window.length > 0) {
                            for (var w = 0; w < chartData.window.length; w++) {
                                var wData = chartData.window[w]; var inWindowOpen = false; var winStart = null;
                                for (var k = 0; k < wData.length; k++) { var ts = wData[k][0]; var val = wData[k][1]; if (val > 0 && !inWindowOpen) { inWindowOpen = true; winStart = ts; } else if (val === 0 && inWindowOpen) { inWindowOpen = false; windowAnnotations.push({ x: winStart, x2: ts, fillColor: colorFenster, opacity: 0.15 }); } }
                                if (inWindowOpen && winStart) windowAnnotations.push({ x: winStart, x2: new Date().getTime(), fillColor: colorFenster, opacity: 0.15 });
                            }
                        }
                        if (cfgShowWindow) { let safeTime = (actHasData) ? chartData.act[0][0] : new Date().getTime() - 3600000; s_series.push({ name: 'Fenster offen', type: 'area', data: [[safeTime, 0]] }); s_colors.push(colorFenster); s_curve.push('stepline'); s_width.push(0); s_dash.push(0); s_fillType.push('solid'); s_opacity.push(0); s_yaxis.push({ min: minTemp, max: maxTemp, show: false, tickAmount: 5 }); }
                        if (s_series.length === 0) { chartDiv.innerHTML = '<div style="color:var(--m3-on-surface-variant); font-size:13px; text-align:center; padding-top:50px; font-weight:500;">Alle Graphen ausgeblendet</div>'; return; }
                        var options = { chart: { type: 'line', height: 140, width: '100%', parentHeightOffset: 0, toolbar: { show: false }, animations: { enabled: true, easing: 'easeinout', speed: 800, dynamicAnimation: { enabled: false } } }, series: s_series, colors: s_colors, stroke: { curve: s_curve, width: s_width, dashArray: s_dash }, fill: { type: s_fillType, opacity: s_opacity, gradient: { shadeIntensity: 1, opacityFrom: 0.45, opacityTo: 0.0, stops: [0, 100] } }, dataLabels: { enabled: false }, legend: { show: true, position: 'top', horizontalAlign: 'right' }, tooltip: { theme: 'dark', x: { format: 'HH:mm' } }, xaxis: { type: 'datetime', labels: { show: false }, axisBorder: { show: false }, axisTicks: { show: false }, tooltip: { enabled: false } }, yaxis: s_yaxis, grid: { show: false }, annotations: { xaxis: windowAnnotations } };
                        if (chartDiv._apexChart) { chartDiv._apexChart.destroy(); delete chartDiv._apexChart; }
                        try { var chart = new ApexCharts(chartDiv, options); chart.render(); chartDiv._apexChart = chart; }
                        catch(err) { console.error(err); chartDiv.innerHTML = '<div style="color:var(--m3-error, red); font-size:13px; text-align:center; padding-top:50px;">Graphen-Fehler</div>'; }
                    }
                    if (typeof ApexCharts === 'undefined') {
                        chartDiv.innerHTML = '<div style="color:var(--m3-on-surface-variant); font-size:13px; text-align:center; padding-top:50px; font-weight:500;">Lade Chart-Engine...</div>';
                        if (!window.apexLoadingPromise) { window.apexLoadingPromise = new Promise((resolve) => { const tempDefine = window.define; window.define = undefined; let script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/apexcharts@5.6.0/dist/apexcharts.min.js'; script.onload = () => { window.define = tempDefine; resolve(); }; document.head.appendChild(script); }); }
                        window.apexLoadingPromise.then(drawGraph);
                    } else { drawGraph(); }
                };
              </script>`;
              
                  setState(DP_HEIZUNG_HTML, html, true);
              }
              
              // === INIT ===
              function startHeizungRenderer(): void {
                  on({ id: [DP_CHART_BRENNER, DP_CHART_VALVE, DP_CHART_ACT, DP_CHART_SET, DP_CHART_WINDOW, DP_CHART_HUMID], change: 'any' }, () => renderHeizung());
                  on({ id: DP_HEIZUNG_AN, change: 'any' }, () => renderHeizung());
                  on({ id: DP_MODULATION,  change: 'any' }, () => renderHeizung());
                  on({ id: DP_PRIVACY,     change: 'any' }, () => renderHeizung());
              
                  rooms.forEach(room => {
                      if (room.actOid)   on({ id: room.actOid,   change: 'any' }, () => renderHeizung());
                      if (room.setOid)   on({ id: room.setOid,   change: 'any' }, () => renderHeizung());
                      if (room.valveOid) on({ id: room.valveOid, change: 'any' }, () => renderHeizung());
                      if (room.humidOid) on({ id: room.humidOid, change: 'any' }, () => renderHeizung());
                      if (room.windowOids) {
                          room.windowOids.forEach(oid => on({ id: oid, change: 'any' }, () => renderHeizung()));
                      }
                  });
              
                  updateHistories();
                  schedule('*/15 * * * *', updateHistories);
                  log('[Heizung] Renderer v1.2 (History Edition) gestartet.');
              }
              
              const chartDPs: Array<{ id: string; val: boolean; name: string }> = [
                  { id: DP_CHART_BRENNER, val: true,  name: 'Chart: Brenner anzeigen' },
                  { id: DP_CHART_VALVE,   val: false, name: 'Chart: Ventil anzeigen'  },
                  { id: DP_CHART_ACT,     val: true,  name: 'Chart: Ist-Temp anzeigen' },
                  { id: DP_CHART_SET,     val: true,  name: 'Chart: Ziel-Temp anzeigen' },
                  { id: DP_CHART_WINDOW,  val: true,  name: 'Chart: Fenster anzeigen'  },
                  { id: DP_CHART_HUMID,   val: true,  name: 'Chart: Luftfeuchte anzeigen' },
              ];
              
              function ensureChartDPs(index: number, done: () => void): void {
                  if (index >= chartDPs.length) { done(); return; }
                  const dp = chartDPs[index];
                  if (!existsState(dp.id)) {
                      createState(dp.id, dp.val, { type: 'boolean', name: dp.name, role: 'switch', read: true, write: true }, () => ensureChartDPs(index + 1, done));
                  } else {
                      ensureChartDPs(index + 1, done);
                  }
              }
              
              ensureChartDPs(0, () => {
                  if (!existsState(DP_HEIZUNG_HTML)) {
                      createState(DP_HEIZUNG_HTML, '', { type: 'string', name: 'Heizung HTML', role: 'html' }, startHeizungRenderer);
                  } else {
                      startHeizungRenderer();
                  }
              });
              
              

              Teste gerne mal und berichte!

              ioBroker auf NUC (Celeron mit Ubuntu-Server)

              Homematic, HMIP, Hue, Unifi, Plex, Nest, Roborock, Google Assistant

              sigi234S 1 Antwort Letzte Antwort
              1
              • M mrMuppet

                So sollte es gehen:

                Das Schöne an ioBroker ist: Der history.0, der sql.0 und der influxdb.0 Adapter teilen sich exakt denselben Befehl (getHistory), um Daten abzufragen. Wir müssen also nur die Ziel-Instanz ändern und die Variablen-Namen der Übersicht halber anpassen.

                Hier ist das vollständig auf den history-Adapter (history.0) umgeschriebene Heizungs-Skript:

                // ============================================================
                // renderHeizung — M3 Material You Styling (9 Räume Grid) - v1.2 (History Edition)
                // ioBroker TypeScript — Inkl. Luftfeuchtigkeit, SYNC-FIX & MULTI-FENSTER
                // CHANGELOG v1.2:
                // - ANPASSUNG: Abfrage der Graphen-Daten von 'influxdb.0' auf den 
                //   Standard 'history.0' Adapter umgestellt.
                // - FIX: ApexCharts Memory-Leak — chart.destroy() vor jedem Re-Render und beim Schließen.
                // - FIX: updateHistories() nutzt Promise.all — parallele Abfragen pro Raum
                //        (deutlich schneller).
                // - FIX: createState ohne existsState-Guard überschreibt bei Neustart.
                // ============================================================
                
                const DP_HEIZUNG_HTML: string = '0_userdata.0.dashboard.heizungHTML';
                const DP_PRIVACY:      string = '0_userdata.0.dashboard.privacyMode';
                
                const DP_CHART_BRENNER: string = '0_userdata.0.dashboard.chartShowBrenner';
                const DP_CHART_VALVE:   string = '0_userdata.0.dashboard.chartShowValve';
                const DP_CHART_ACT:     string = '0_userdata.0.dashboard.chartShowAct';
                const DP_CHART_SET:     string = '0_userdata.0.dashboard.chartShowSet';
                const DP_CHART_WINDOW:  string = '0_userdata.0.dashboard.chartShowWindow';
                const DP_CHART_HUMID:   string = '0_userdata.0.dashboard.chartShowHumid';
                
                const DP_HEIZUNG_AN: string = '0_userdata.0.Heizung.Heizung_an';
                const DP_MODULATION: string = 'ems-esp.0.heatSources.actualModulation';
                
                // INSTANZ DES HISTORY-ADAPTERS
                const HISTORY_INSTANCE: string = 'history.0';
                
                let globalHeatHistory: any[] = [];
                let roomHistories: Record<string, { act: any[]; set: any[]; valve: any[]; heat: any[]; window: any[]; humid: any[] }> = {};
                
                const M3: Record<string, string> = {
                    primary:                 'var(--m3-primary, rgb(18, 66, 24))',
                    onPrimary:               'var(--m3-on-primary, rgb(255, 255, 255))',
                    primaryContainer:        'var(--m3-primary-container, rgb(43, 90, 45))',
                    onPrimaryContainer:      'var(--m3-on-primary-container, rgb(155, 208, 151))',
                    surfaceContainerLow:     'var(--m3-surface-container-low, rgb(243, 244, 237))',
                    surfaceContainerHighest: 'var(--m3-surface-container-highest, rgb(225, 227, 220))',
                    surfaceVariant:          'var(--m3-surface-variant, rgb(221, 229, 216))',
                    onSurface:               'var(--m3-on-surface, rgb(25, 28, 24))',
                    onSurfaceVariant:        'var(--m3-on-surface-variant, rgb(66, 73, 63))',
                    heatPrimary:             'var(--m3-error, rgb(186, 26, 26))',
                    heatContainer:           'var(--m3-error-container, rgb(255, 218, 214))',
                    onHeatContainer:         'var(--m3-on-error-container, rgb(65, 0, 2))',
                    pendingPrimary:          'rgb(176, 96, 0)',
                    pendingContainer:        'rgb(255, 220, 193)',
                    onPendingContainer:      'rgb(43, 20, 0)',
                };
                
                const ICONS: Record<string, string> = {
                    thermostat: 'M15 13V5A3 3 0 0 0 9 5V13A5 5 0 1 0 15 13M12 4A1 1 0 0 1 13 5V8H11V5A1 1 0 0 1 12 4Z',
                    fire:       'M17.5 11C15 7.5 13.5 8.5 12 6C10.5 8.5 9 7.5 6.5 11C4 14.5 5.5 19 12 19C18.5 19 20 14.5 17.5 11M12 17C9 17 8.5 14 10 12.5C11.5 11 12 12.5 12 12.5C12 12.5 12.5 11 14 12.5C15.5 14 15 17 12 17Z',
                    minus:      'M19 13H5V11H19V13Z',
                    plus:       'M19 13H13V19H11V13H5V11H11V5H13V11H19V13Z',
                    power:      'M16.56 5.44L15.11 6.89C16.84 7.94 18 9.83 18 12A6 6 0 0 1 12 18A6 6 0 0 1 6 12C6 9.83 7.16 7.94 8.88 6.88L7.44 5.44C5.36 6.88 4 9.28 4 12A8 8 0 0 0 12 20A8 8 0 0 0 20 12C20 9.28 18.64 6.88 16.56 5.44M11 3H13V13H11V3Z',
                    window:     'M3 3H21V21H3V3M5 5V11H11V5H5M13 5V11H19V5H13M5 13V19H11V13H5M13 13V19H19V13H13Z',
                    water:      'M12 20a6 6 0 0 1-6-6c0-4 6-10.75 6-10.75S18 10 18 14a6 6 0 0 1-6 6z'
                };
                
                interface RoomHeater {
                    id:          string;
                    label:       string;
                    anonLabel?:  string;
                    actOid:      string;
                    setOid:      string;
                    valveOid:    string;
                    windowOids?: string[];
                    humidOid?:   string;
                }
                
                const rooms: RoomHeater[] = [
                    { id: 'wohnzimmer',  label: 'Wohnzimmer',    actOid: 'alias.0.Heizung.Wohnzimmer.ACTUAL',   setOid: 'alias.0.Heizung.Wohnzimmer.SET',   valveOid: 'alias.0.Heizung.Wohnzimmer.VALVE',   windowOids: ['alias.0.Sicherheit.Wohnzimmer.Fenster.STATE', 'alias.0.Sicherheit.Wohnzimmer.Tuer.STATE'], humidOid: 'alias.0.Klima.Wohnzimmer.HUMIDITY' },
                    { id: 'schlafzimmer',label: 'Zimmer Mina',    anonLabel: 'Schlafzimmer',  actOid: 'alias.0.Heizung.Schlafzimmer.ACTUAL', setOid: 'alias.0.Heizung.Schlafzimmer.SET', valveOid: 'alias.0.Heizung.Schlafzimmer.VALVE', windowOids: ['alias.0.Sicherheit.Schlafzimmer.Fenster.STATE'], humidOid: 'alias.0.Klima.Schlafzimmer.HUMIDITY' },
                    { id: 'kind1',       label: 'Zimmer Nova',    anonLabel: 'Kinderzimmer 1', actOid: 'alias.0.Heizung.Kind1.ACTUAL',        setOid: 'alias.0.Heizung.Kind1.SET',        valveOid: 'alias.0.Heizung.Kind1.VALVE',        windowOids: ['alias.0.Sicherheit.Kinderzimmer1.Fenster.STATE'], humidOid: 'alias.0.Klima.Kind1.HUMIDITY' },
                    { id: 'kind2',       label: 'Zimmer Vasco',   anonLabel: 'Kinderzimmer 2', actOid: 'alias.0.Heizung.Kind2.ACTUAL',        setOid: 'alias.0.Heizung.Kind2.SET',        valveOid: 'alias.0.Heizung.Kind2.VALVE',        windowOids: ['alias.0.Sicherheit.Kinderzimmer2.Fenster.STATE'], humidOid: 'alias.0.Klima.Kind2.HUMIDITY' },
                    { id: 'bad',         label: 'Bad',            actOid: 'alias.0.Heizung.Bad.ACTUAL',          setOid: 'alias.0.Heizung.Bad.SET',          valveOid: 'alias.0.Heizung.Bad.VALVE',          windowOids: ['alias.0.Sicherheit.Bad.Fenster.STATE'], humidOid: 'alias.0.Klima.Bad.HUMIDITY' },
                    { id: 'dachzimmer',  label: 'Dachzimmer',     actOid: 'alias.0.Heizung.Dachzimmer.ACTUAL',   setOid: 'alias.0.Heizung.Dachzimmer.SET',   valveOid: 'alias.0.Heizung.Dachzimmer.VALVE',   windowOids: ['alias.0.Sicherheit.Dachzimmer.Fenster_Garten.STATE', 'alias.0.Sicherheit.Dachzimmer.Fenster_Strasse.STATE'], humidOid: 'alias.0.Klima.Dachzimmer.HUMIDITY' },
                    { id: 'flur',        label: 'Flur',            actOid: 'alias.0.Heizung.Flur.ACTUAL',         setOid: 'alias.0.Heizung.Flur.SET',         valveOid: 'alias.0.Heizung.Flur.VALVE',         windowOids: ['alias.0.Sicherheit.Haustuer.STATE'], humidOid: 'alias.0.Klima.Flur.HUMIDITY' },
                    { id: 'gaesteklo',   label: 'Gäste WC',       actOid: 'alias.0.Heizung.GaesteKlo.ACTUAL',    setOid: 'alias.0.Heizung.GaesteKlo.SET',    valveOid: 'alias.0.Heizung.GaesteKlo.VALVE' },
                    { id: 'hwr',         label: 'HWR',             actOid: 'alias.0.Heizung.HWR.ACTUAL',          setOid: 'alias.0.Heizung.HWR.SET',          valveOid: 'alias.0.Heizung.HWR.VALVE',          windowOids: ['alias.0.Sicherheit.HWR.Fenster.STATE'] },
                ];
                
                const COL_WIDTH:  number = 202;
                const HERO_WIDTH: number = 636;
                const CARD_HEIGHT:number = 144;
                const GUTTER:     number = 15;
                const SHADOW:     string = '0px 1px 2px 0px rgba(0,0,0,0.3), 0px 1px 3px 1px rgba(0,0,0,0.15)';
                
                function getSafeVal(oid: string | undefined, fallback: any = 0): any {
                    if (oid && existsState(oid)) {
                        const s = getState(oid);
                        return s ? s.val : fallback;
                    }
                    return fallback;
                }
                
                function svgIcon(name: string, color: string, size: number = 24): string {
                    const path = ICONS[name] || ICONS['thermostat'];
                    return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" style="flex-shrink:0; transition: fill 0.3s ease;"><path fill="${color}" d="${path}"/></svg>`;
                }
                
                async function fetchHistoryData(oid: string | undefined, start: number, end: number, isRaw: boolean = false): Promise<any[]> {
                    if (!oid || !existsState(oid)) return [];
                    try {
                        const queryOptions = isRaw
                            ? { start, end, aggregate: 'none', limit: 400, ignoreNull: true }
                            : { start, end, step: 1800000, aggregate: 'average', ignoreNull: true };
                
                        const result = await new Promise<any>(resolve => {
                            sendTo(HISTORY_INSTANCE, 'getHistory', { id: oid, options: queryOptions }, res => resolve(res));
                        });
                
                        if (result && result.result && result.result.length > 0) {
                            let cleanData = result.result
                                .filter((r: any) => r.val !== null)
                                .map((r: any) => {
                                    let v = r.val;
                                    if (typeof v === 'boolean')      v = v ? 100 : 0;
                                    else if (typeof v === 'string')  v = Number(v) || 0;
                                    return [r.ts, Math.round(v * 10) / 10];
                                })
                                .filter((r: any) => !isNaN(r[1]));
                
                            if (isRaw && cleanData.length > 0) {
                                const maxVal = Math.max(...cleanData.map((d: any) => d[1]));
                                if (maxVal <= 1.0 && maxVal > 0) cleanData.forEach((d: any) => (d[1] = d[1] * 100));
                            }
                            return cleanData;
                        }
                    } catch (e) {}
                    return [];
                }
                
                async function updateHistories(): Promise<void> {
                    const end   = new Date().getTime();
                    const start = end - 24 * 60 * 60 * 1000;
                
                    globalHeatHistory = await fetchHistoryData(DP_MODULATION, start, end, true);
                
                    for (const room of rooms) {
                        const [actData, setData, valveData, humidData] = await Promise.all([
                            fetchHistoryData(room.actOid,   start, end, false),
                            fetchHistoryData(room.setOid,   start, end, false),
                            fetchHistoryData(room.valveOid, start, end, true),
                            fetchHistoryData(room.humidOid, start, end, false),
                        ]);
                
                        let allWindowData: any[] = [];
                        if (room.windowOids && room.windowOids.length > 0) {
                            const wResults = await Promise.all(room.windowOids.map(wOid => fetchHistoryData(wOid, start, end, true)));
                            allWindowData = wResults.filter(wData => wData.length > 0);
                        }
                
                        roomHistories[room.id] = { act: actData, set: setData, valve: valveData, heat: globalHeatHistory, window: allWindowData, humid: humidData };
                    }
                    renderHeizung();
                }
                
                function renderHeroCard(activeRoomsCount: number, isZentralAn: boolean): string {
                    const mod    = Number(getSafeVal(DP_MODULATION, 0)) || 0;
                    const bg     = isZentralAn ? M3.primaryContainer : M3.surfaceContainerLow;
                    const fg     = isZentralAn ? M3.onPrimaryContainer : M3.onSurfaceVariant;
                    const iconBg = isZentralAn ? M3.primary : M3.surfaceContainerHighest;
                    const iconFg = isZentralAn ? M3.onPrimary : M3.onSurfaceVariant;
                    return `<div style="width:${HERO_WIDTH}px; height:${CARD_HEIGHT}px; border-radius:12px; padding:20px 24px; box-sizing:border-box; background:${bg}; box-shadow:${SHADOW}; display:flex; align-items:center; justify-content:space-between; font-family:Roboto,sans-serif; transition:all 0.3s ease;"> <div style="display:flex; align-items:center; gap:20px;"> <div style="width:56px; height:56px; border-radius:28px; background:${iconBg}; display:flex; align-items:center; justify-content:center; transition:all 0.3s ease;"> ${svgIcon(isZentralAn ? 'fire' : 'power', iconFg, 32)} </div> <div> <div style="font-size:24px; font-weight:500; color:${fg}; margin-bottom:4px;">Zentralheizung</div> <div style="font-size:15px; font-weight:500; color:${fg}; opacity:0.8;">${isZentralAn ? `Heizbetrieb aktiv • ${mod}% Leistung` : 'Standby / Aus'}</div> </div> </div> <div style="text-align:right;"> <div style="font-size:36px; font-weight:600; color:${isZentralAn ? M3.heatPrimary : fg}; line-height:1;">${activeRoomsCount}</div> <div style="font-size:13px; font-weight:500; color:${fg}; text-transform:uppercase; letter-spacing:0.5px; margin-top:4px;">Räume fordern Wärme</div> </div> </div>`;
                }
                
                function renderRoomCard(room: RoomHeater, isZentralAn: boolean, isPrivacyMode: boolean): string {
                    const actVal     = Number(getSafeVal(room.actOid, 0)) || 0;
                    const setVal     = Number(getSafeVal(room.setOid, 0)) || 0;
                    const valveRaw   = getSafeVal(room.valveOid, 0);
                    const humidVal   = room.humidOid ? Number(getSafeVal(room.humidOid, 0)) || 0 : null;
                    const displayName = isPrivacyMode && room.anonLabel ? room.anonLabel : room.label;
                
                    let windowOpen = false;
                    if (room.windowOids && room.windowOids.length > 0) {
                        windowOpen = room.windowOids.some(oid => {
                            const raw = getSafeVal(oid, false);
                            return raw === true || raw === 1 || raw === '1' || raw === 'true';
                        });
                    }
                
                    const wantsHeat = (typeof valveRaw === 'number' && valveRaw > 0)
                        || (typeof valveRaw === 'string' && Number(valveRaw) > 0)
                        || valveRaw === true || valveRaw === 'true';
                
                    let bg: string, fg: string, fgSub: string, iconBg: string, iconFg: string;
                    if (wantsHeat && isZentralAn) {
                        bg = M3.heatContainer; fg = M3.onHeatContainer; fgSub = M3.onHeatContainer; iconBg = M3.heatPrimary; iconFg = '#ffffff';
                    } else if (wantsHeat && !isZentralAn) {
                        bg = M3.pendingContainer; fg = M3.onPendingContainer; fgSub = M3.onPendingContainer; iconBg = M3.pendingPrimary; iconFg = '#ffffff';
                    } else {
                        bg = M3.surfaceContainerLow; fg = M3.onSurface; fgSub = M3.onSurfaceVariant; iconBg = M3.surfaceContainerHighest; iconFg = M3.onSurfaceVariant;
                    }
                
                    const jsMinus = `(function(e, btn){ e.stopPropagation(); var card = btn.closest('.m3-room-card'); var span = card.querySelector('.target-temp'); var current = parseFloat(span.innerText); var newVal = current - 0.5; span.innerText = newVal.toFixed(1) + '°'; if(typeof vis!=='undefined'&&vis.conn&&vis.conn.setState) vis.conn.setState('${room.setOid}', newVal); else if(typeof vis!=='undefined'&&vis.setValue) vis.setValue('${room.setOid}', newVal); })(event, this);`;
                    const jsPlus  = `(function(e, btn){ e.stopPropagation(); var card = btn.closest('.m3-room-card'); var span = card.querySelector('.target-temp'); var current = parseFloat(span.innerText); var newVal = current + 0.5; span.innerText = newVal.toFixed(1) + '°'; if(typeof vis!=='undefined'&&vis.conn&&vis.conn.setState) vis.conn.setState('${room.setOid}', newVal); else if(typeof vis!=='undefined'&&vis.setValue) vis.setValue('${room.setOid}', newVal); })(event, this);`;
                
                    const windowHtml = windowOpen
                        ? `<div style="width:24px; height:24px; display:flex; align-items:center; justify-content:center; animation: blink 2s infinite;">${svgIcon('window', M3.heatPrimary, 20)}</div>`
                        : '';
                
                    const humidHtml = (humidVal !== null && humidVal > 0)
                        ? `<div style="position:absolute; top:36px; right:-4px; width:40px; display:flex; justify-content:center; align-items:center; font-size:11px; font-weight:600; color:${fgSub}; opacity:0.85; letter-spacing:-0.5px;">${Math.round(humidVal)}%<svg width="10" height="10" viewBox="0 0 24 24" style="margin-left:1px"><path fill="currentColor" d="M12 20a6 6 0 0 1-6-6c0-4 6-10.75 6-10.75S18 10 18 14a6 6 0 0 1-6 6z"/></svg></div>`
                        : '';
                
                    const historyData = roomHistories[room.id] ?? { act: [], set: [], valve: [], heat: [], window: [], humid: [] };
                
                    return `<div id="wrapper_${room.id}" style="width:${COL_WIDTH}px; height:${CARD_HEIGHT}px; position:relative; z-index:1;">
                        <div id="card_${room.id}" class="m3-room-card" data-history='${JSON.stringify(historyData)}' onclick="if(typeof window.toggleCard === 'function') window.toggleCard(this, '${room.id}');" style="position:absolute; top:0; left:0; width:100%; height:100%; border-radius:12px; padding:14px 16px; box-sizing:border-box; background:${bg}; box-shadow:${SHADOW}; display:flex; flex-direction:column; font-family:Roboto,sans-serif; transition:all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); cursor:pointer; overflow:hidden;">
                            <div style="display:flex; justify-content:space-between; align-items:flex-start; width:100%;">
                                <div style="font-size:16px; font-weight:500; color:${fg}; max-width:145px; line-height:1.2; overflow:hidden; text-overflow:ellipsis; transition:color 0.3s ease;">${displayName}</div>
                                <div style="display:flex; gap:6px; align-items:flex-start; position:relative;">
                                    ${windowHtml}
                                    <div style="width:32px; height:32px; border-radius:16px; background:${iconBg}; display:flex; align-items:center; justify-content:center; flex-shrink:0;">${svgIcon('thermostat', iconFg, 18)}</div>
                                    ${humidHtml}
                                </div>
                            </div>
                            <div style="display:flex; align-items:baseline; margin-top:-6px; width:100%;">
                                <div style="font-size:36px; font-weight:400; color:${fg}; letter-spacing:-1px;">${actVal.toFixed(1)}</div>
                                <div style="font-size:18px; font-weight:500; color:${fgSub}; margin-left:2px; opacity:0.7;">°C</div>
                            </div>
                            <div id="controls_${room.id}" style="display:flex; justify-content:space-between; align-items:center; width:100%; margin-top:auto;">
                                <div style="display:flex; flex-direction:column;">
                                    <span style="font-size:11px; font-weight:500; text-transform:uppercase; letter-spacing:0.5px; color:${fgSub}; opacity:0.7;">Ziel</span>
                                    <span class="target-temp" style="font-size:15px; font-weight:600; color:${fg};">${setVal.toFixed(1)}°</span>
                                </div>
                                <div style="display:flex; gap:6px;">
                                    <div onclick="${jsMinus}" style="width:32px; height:32px; border-radius:16px; background:rgba(0,0,0,0.06); display:flex; align-items:center; justify-content:center; cursor:pointer; user-select:none; transition:opacity 0.1s; -webkit-tap-highlight-color:transparent;">${svgIcon('minus', fg, 20)}</div>
                                    <div onclick="${jsPlus}"  style="width:32px; height:32px; border-radius:16px; background:rgba(0,0,0,0.06); display:flex; align-items:center; justify-content:center; cursor:pointer; user-select:none; transition:opacity 0.1s; -webkit-tap-highlight-color:transparent;">${svgIcon('plus', fg, 20)}</div>
                                </div>
                            </div>
                            <div id="chart_wrapper_${room.id}" style="opacity:0; height:0px; width:100%; transition:opacity 0.4s ease; margin-top:0px; pointer-events:none;">
                                <div id="chart_${room.id}" style="width:100%; height:100%;"></div>
                            </div>
                        </div>
                    </div>`;
                }
                
                function renderHeizung(): void {
                    let activeRooms = 0;
                    const isZentralAn   = getSafeVal(DP_HEIZUNG_AN, false) === true;
                    const isPrivacyMode = getSafeVal(DP_PRIVACY,     false) === true;
                
                    const showBrenner = getSafeVal(DP_CHART_BRENNER, true)  === true;
                    const showValve   = getSafeVal(DP_CHART_VALVE,   false) === true;
                    const showAct     = getSafeVal(DP_CHART_ACT,     true)  === true;
                    const showSet     = getSafeVal(DP_CHART_SET,     true)  === true;
                    const showWindow  = getSafeVal(DP_CHART_WINDOW,  true)  === true;
                    const showHumid   = getSafeVal(DP_CHART_HUMID,   true)  === true;
                
                    for (const room of rooms) {
                        const valveRaw = getSafeVal(room.valveOid, 0);
                        if ((typeof valveRaw === 'number' && valveRaw > 0)
                            || (typeof valveRaw === 'string' && Number(valveRaw) > 0)
                            || valveRaw === true || valveRaw === 'true') activeRooms++;
                    }
                
                    let cardsHtml = renderHeroCard(activeRooms, isZentralAn);
                    for (const room of rooms) cardsHtml += renderRoomCard(room, isZentralAn, isPrivacyMode);
                
                    const html = `
                <style>
                  @keyframes blink { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
                  .m3-room-card * { white-space: nowrap; }
                  .apexcharts-legend { padding: 0 !important; margin-top: -10px !important; }
                  .apexcharts-legend-text { color: var(--m3-on-surface-variant) !important; font-family: Roboto, sans-serif !important; font-size: 11px !important; }
                </style>
                
                <div id="m3-dashboard-container"
                     data-cfg-brenner="${showBrenner}"
                     data-cfg-valve="${showValve}"
                     data-cfg-act="${showAct}"
                     data-cfg-set="${showSet}"
                     data-cfg-window="${showWindow}"
                     data-cfg-humid="${showHumid}"
                     style="position: relative; background: var(--m3-surface-container-low, rgb(237, 239, 232)); border-radius: 0 0 28px 28px; padding: 24px; display: flex; flex-wrap: wrap; align-content: flex-start; gap: ${GUTTER}px; width: 901px; box-sizing: border-box; transition: background-color 0.3s ease;">
                  ${cardsHtml}
                </div>
                
                <script>
                  window.activeM3CardId = window.activeM3CardId || null;
                  window.toggleCard = function(el, roomId) {
                      if (window.activeM3CardId === roomId) { window.closeCard(el, roomId); return; }
                      if (window.activeM3CardId) { const oldEl = document.getElementById('card_' + window.activeM3CardId); if (oldEl) window.closeCard(oldEl, window.activeM3CardId); }
                      window.openCard(el, roomId);
                  };
                  window.closeCard = function(el, roomId) {
                      el.style.width = '100%'; el.style.height = '100%'; el.style.boxShadow = '${SHADOW}';
                      const chartWrapper = document.getElementById('chart_wrapper_' + roomId);
                      if (chartWrapper) {
                          chartWrapper.style.opacity = '0';
                          setTimeout(() => {
                              chartWrapper.style.height = '0px';
                              const chartDiv = document.getElementById('chart_' + roomId);
                              if (chartDiv) {
                                  if (chartDiv._apexChart) { chartDiv._apexChart.destroy(); delete chartDiv._apexChart; }
                                  chartDiv.innerHTML = '';
                              }
                          }, 400);
                      }
                      setTimeout(() => { el.parentElement.style.zIndex = '1'; }, 400); window.activeM3CardId = null;
                  };
                  window.openCard = function(el, roomId) {
                      window.activeM3CardId = roomId; const wrapper = el.parentElement; const container = document.getElementById('m3-dashboard-container');
                      wrapper.style.zIndex = '999'; const wrapperRect = wrapper.getBoundingClientRect(); const containerRect = container.getBoundingClientRect();
                      const expandedWidth = ${COL_WIDTH * 2 + GUTTER}; const expandedHeight = 310;
                      el.style.top = 'auto'; el.style.bottom = 'auto'; el.style.left = 'auto'; el.style.right = 'auto';
                      if (wrapperRect.left + expandedWidth > containerRect.right - 10) { el.style.right = '0'; } else { el.style.left = '0'; }
                      if (wrapperRect.top + expandedHeight > containerRect.bottom - 10) { el.style.bottom = '0'; } else { el.style.top = '0'; }
                      el.style.width = expandedWidth + 'px'; el.style.height = expandedHeight + 'px'; el.style.boxShadow = '0px 14px 28px rgba(0,0,0,0.35), 0px 10px 10px rgba(0,0,0,0.22)';
                      const chartWrapper = document.getElementById('chart_wrapper_' + roomId);
                      if (chartWrapper) { chartWrapper.style.height = '140px'; chartWrapper.style.marginTop = '16px'; setTimeout(() => { chartWrapper.style.opacity = '1'; window.renderChart(el, roomId); }, 450); }
                  };
                  window.renderChart = function(el, roomId) {
                      const chartDiv = el.querySelector('#chart_' + roomId);
                      if (!chartDiv || chartDiv.innerHTML.trim() !== '') return;
                      const rawData = el.getAttribute('data-history'); let chartData = { act: [], set: [], valve: [], heat: [], window: [], humid: [] };
                      try { chartData = JSON.parse(rawData); } catch(e) {}
                      if (chartData.act.length === 0 && chartData.set.length === 0) { chartDiv.innerHTML = '<div style="color:var(--m3-on-surface-variant); font-size:13px; text-align:center; padding-top:50px; font-weight:500;">Keine Historie</div>'; return; }
                      const rootStyles = getComputedStyle(document.documentElement);
                      const colorIst     = rootStyles.getPropertyValue('--m3-primary').trim()  || '#4caf50';
                      const colorSoll    = rootStyles.getPropertyValue('--m3-outline').trim()   || '#9e9e9e';
                      const colorVentil  = rootStyles.getPropertyValue('--m3-error').trim()     || '#ffb4ab';
                      const colorBrenner = '#ff9800';
                      const colorFenster = '#00bfff';
                      const colorHumid   = '#2196f3';
                      const container    = el.closest('#m3-dashboard-container');
                      const cfgShowBrenner = container.getAttribute('data-cfg-brenner') === 'true';
                      const cfgShowValve   = container.getAttribute('data-cfg-valve')   === 'true';
                      const cfgShowAct     = container.getAttribute('data-cfg-act')     === 'true';
                      const cfgShowSet     = container.getAttribute('data-cfg-set')     === 'true';
                      const cfgShowWindow  = container.getAttribute('data-cfg-window')  === 'true';
                      const cfgShowHumid   = container.getAttribute('data-cfg-humid')   === 'true';
                      function drawGraph() {
                          chartDiv.innerHTML = '';
                          var s_series = []; var s_colors = []; var s_curve = []; var s_width = []; var s_dash = []; var s_fillType = []; var s_opacity = []; var s_yaxis = [];
                          var actHasData = chartData.act && chartData.act.length > 0;
                          var setHasData = chartData.set && chartData.set.length > 0;
                          var minTemp = 999; var maxTemp = -999;
                          if (cfgShowAct && actHasData) { for (var i = 0; i < chartData.act.length; i++) { if (chartData.act[i][1] < minTemp) minTemp = chartData.act[i][1]; if (chartData.act[i][1] > maxTemp) maxTemp = chartData.act[i][1]; } }
                          if (cfgShowSet && setHasData) { for (var j = 0; j < chartData.set.length; j++) { if (chartData.set[j][1] < minTemp) minTemp = chartData.set[j][1]; if (chartData.set[j][1] > maxTemp) maxTemp = chartData.set[j][1]; } }
                          if (minTemp === 999) { minTemp = 15; maxTemp = 25; } else { minTemp = Math.floor(minTemp) - 1; maxTemp = Math.ceil(maxTemp) + 1; }
                          if (cfgShowHumid && chartData.humid && chartData.humid.length > 0) { s_series.push({ name: 'Luftfeuchte', type: 'area', data: chartData.humid }); s_colors.push(colorHumid); s_curve.push('smooth'); s_width.push(0); s_dash.push(0); s_fillType.push('solid'); s_opacity.push(0.1); s_yaxis.push({ min: 0, max: 100, show: false }); }
                          if (cfgShowBrenner) { s_series.push({ name: 'Brenner', type: 'area', data: chartData.heat || [] }); s_colors.push(colorBrenner); s_curve.push('smooth'); s_width.push(2); s_dash.push(0); s_fillType.push('gradient'); s_opacity.push(1); s_yaxis.push({ min: 0, max: 100, show: false }); }
                          if (cfgShowValve)   { s_series.push({ name: 'Ventil',  type: 'line', data: chartData.valve || [] }); s_colors.push(colorVentil);  s_curve.push('stepline'); s_width.push(2); s_dash.push(0); s_fillType.push('solid'); s_opacity.push(1); s_yaxis.push({ min: 0, max: 100, show: false }); }
                          if (cfgShowAct)     { s_series.push({ name: 'Ist-Temp',  type: 'area', data: chartData.act || [] }); s_colors.push(colorIst);    s_curve.push('smooth');   s_width.push(3); s_dash.push(0); s_fillType.push('gradient'); s_opacity.push(1); s_yaxis.push({ min: minTemp, max: maxTemp, show: false, tickAmount: 5 }); }
                          if (cfgShowSet)     { s_series.push({ name: 'Ziel-Temp', type: 'line', data: chartData.set || [] }); s_colors.push(colorSoll);   s_curve.push('stepline'); s_width.push(2); s_dash.push(4); s_fillType.push('solid');    s_opacity.push(1); s_yaxis.push({ min: minTemp, max: maxTemp, show: false, tickAmount: 5 }); }
                          var windowAnnotations = [];
                          if (cfgShowWindow && chartData.window && chartData.window.length > 0) {
                              for (var w = 0; w < chartData.window.length; w++) {
                                  var wData = chartData.window[w]; var inWindowOpen = false; var winStart = null;
                                  for (var k = 0; k < wData.length; k++) { var ts = wData[k][0]; var val = wData[k][1]; if (val > 0 && !inWindowOpen) { inWindowOpen = true; winStart = ts; } else if (val === 0 && inWindowOpen) { inWindowOpen = false; windowAnnotations.push({ x: winStart, x2: ts, fillColor: colorFenster, opacity: 0.15 }); } }
                                  if (inWindowOpen && winStart) windowAnnotations.push({ x: winStart, x2: new Date().getTime(), fillColor: colorFenster, opacity: 0.15 });
                              }
                          }
                          if (cfgShowWindow) { let safeTime = (actHasData) ? chartData.act[0][0] : new Date().getTime() - 3600000; s_series.push({ name: 'Fenster offen', type: 'area', data: [[safeTime, 0]] }); s_colors.push(colorFenster); s_curve.push('stepline'); s_width.push(0); s_dash.push(0); s_fillType.push('solid'); s_opacity.push(0); s_yaxis.push({ min: minTemp, max: maxTemp, show: false, tickAmount: 5 }); }
                          if (s_series.length === 0) { chartDiv.innerHTML = '<div style="color:var(--m3-on-surface-variant); font-size:13px; text-align:center; padding-top:50px; font-weight:500;">Alle Graphen ausgeblendet</div>'; return; }
                          var options = { chart: { type: 'line', height: 140, width: '100%', parentHeightOffset: 0, toolbar: { show: false }, animations: { enabled: true, easing: 'easeinout', speed: 800, dynamicAnimation: { enabled: false } } }, series: s_series, colors: s_colors, stroke: { curve: s_curve, width: s_width, dashArray: s_dash }, fill: { type: s_fillType, opacity: s_opacity, gradient: { shadeIntensity: 1, opacityFrom: 0.45, opacityTo: 0.0, stops: [0, 100] } }, dataLabels: { enabled: false }, legend: { show: true, position: 'top', horizontalAlign: 'right' }, tooltip: { theme: 'dark', x: { format: 'HH:mm' } }, xaxis: { type: 'datetime', labels: { show: false }, axisBorder: { show: false }, axisTicks: { show: false }, tooltip: { enabled: false } }, yaxis: s_yaxis, grid: { show: false }, annotations: { xaxis: windowAnnotations } };
                          if (chartDiv._apexChart) { chartDiv._apexChart.destroy(); delete chartDiv._apexChart; }
                          try { var chart = new ApexCharts(chartDiv, options); chart.render(); chartDiv._apexChart = chart; }
                          catch(err) { console.error(err); chartDiv.innerHTML = '<div style="color:var(--m3-error, red); font-size:13px; text-align:center; padding-top:50px;">Graphen-Fehler</div>'; }
                      }
                      if (typeof ApexCharts === 'undefined') {
                          chartDiv.innerHTML = '<div style="color:var(--m3-on-surface-variant); font-size:13px; text-align:center; padding-top:50px; font-weight:500;">Lade Chart-Engine...</div>';
                          if (!window.apexLoadingPromise) { window.apexLoadingPromise = new Promise((resolve) => { const tempDefine = window.define; window.define = undefined; let script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/apexcharts@5.6.0/dist/apexcharts.min.js'; script.onload = () => { window.define = tempDefine; resolve(); }; document.head.appendChild(script); }); }
                          window.apexLoadingPromise.then(drawGraph);
                      } else { drawGraph(); }
                  };
                </script>`;
                
                    setState(DP_HEIZUNG_HTML, html, true);
                }
                
                // === INIT ===
                function startHeizungRenderer(): void {
                    on({ id: [DP_CHART_BRENNER, DP_CHART_VALVE, DP_CHART_ACT, DP_CHART_SET, DP_CHART_WINDOW, DP_CHART_HUMID], change: 'any' }, () => renderHeizung());
                    on({ id: DP_HEIZUNG_AN, change: 'any' }, () => renderHeizung());
                    on({ id: DP_MODULATION,  change: 'any' }, () => renderHeizung());
                    on({ id: DP_PRIVACY,     change: 'any' }, () => renderHeizung());
                
                    rooms.forEach(room => {
                        if (room.actOid)   on({ id: room.actOid,   change: 'any' }, () => renderHeizung());
                        if (room.setOid)   on({ id: room.setOid,   change: 'any' }, () => renderHeizung());
                        if (room.valveOid) on({ id: room.valveOid, change: 'any' }, () => renderHeizung());
                        if (room.humidOid) on({ id: room.humidOid, change: 'any' }, () => renderHeizung());
                        if (room.windowOids) {
                            room.windowOids.forEach(oid => on({ id: oid, change: 'any' }, () => renderHeizung()));
                        }
                    });
                
                    updateHistories();
                    schedule('*/15 * * * *', updateHistories);
                    log('[Heizung] Renderer v1.2 (History Edition) gestartet.');
                }
                
                const chartDPs: Array<{ id: string; val: boolean; name: string }> = [
                    { id: DP_CHART_BRENNER, val: true,  name: 'Chart: Brenner anzeigen' },
                    { id: DP_CHART_VALVE,   val: false, name: 'Chart: Ventil anzeigen'  },
                    { id: DP_CHART_ACT,     val: true,  name: 'Chart: Ist-Temp anzeigen' },
                    { id: DP_CHART_SET,     val: true,  name: 'Chart: Ziel-Temp anzeigen' },
                    { id: DP_CHART_WINDOW,  val: true,  name: 'Chart: Fenster anzeigen'  },
                    { id: DP_CHART_HUMID,   val: true,  name: 'Chart: Luftfeuchte anzeigen' },
                ];
                
                function ensureChartDPs(index: number, done: () => void): void {
                    if (index >= chartDPs.length) { done(); return; }
                    const dp = chartDPs[index];
                    if (!existsState(dp.id)) {
                        createState(dp.id, dp.val, { type: 'boolean', name: dp.name, role: 'switch', read: true, write: true }, () => ensureChartDPs(index + 1, done));
                    } else {
                        ensureChartDPs(index + 1, done);
                    }
                }
                
                ensureChartDPs(0, () => {
                    if (!existsState(DP_HEIZUNG_HTML)) {
                        createState(DP_HEIZUNG_HTML, '', { type: 'string', name: 'Heizung HTML', role: 'html' }, startHeizungRenderer);
                    } else {
                        startHeizungRenderer();
                    }
                });
                
                

                Teste gerne mal und berichte!

                sigi234S Online
                sigi234S Online
                sigi234
                Forum Testing Most Active
                schrieb am zuletzt editiert von sigi234
                #16

                @mrMuppet

                Ok, stürzt ab:

                Screenshot (2102).png

                web.0
                2026-06-08 17:03:57.041	error	Cannot subscribe "curve: s_curve, width: s_width, dashArray: s_dash": Error The pattern "curve: s_curve, width: s_width, dashArray: s_dash" is not a valid ID pattern
                
                web.0
                2026-06-08 17:03:57.041	error	Cannot subscribe "min: 0, max: 100, show: false": Error The pattern "min: 0, max: 100, show: false" is not a valid ID pattern
                
                host.SmartHome-Server
                2026-06-08 17:03:57.034	warn	States 127.0.0.1:65371 Error from InMemDB: Error: The pattern "curve: s_curve, width: s_width, dashArray: s_dash" is not a valid ID pattern at Module.pattern2RegEx (file:///C:/SmartHome/node_modules/@iobroker/js-controller-common-db/src/lib/common/tools.ts:2515:15) at StatesInMemoryServer.handleSubscribe (file:///C:/SmartHome/node_modules/@iobroker/db-base/src/lib/inMemFileDB.ts:323:59) at StatesInMemoryServer._subscribeForClient (file:///C:/SmartHome/node_modules/@iobroker/db-states-file/src/lib/states/statesInMemFileDB.js:240:14) at RedisHandler.<anonymous> (file:///C:/SmartHome/node_modules/@iobroker/db-states-jsonl/src/lib/states/statesInMemServerRedis.js:380:26) at RedisHandler.emit (node:events:519:28) at Immediate._onImmediate (file:///C:/SmartHome/node_modules/@iobroker/db-base/src/lib/redisHandler.ts:210:37) at processImmediate (node:internal/timers:484:21)
                
                host.SmartHome-Server
                2026-06-08 17:03:57.034	warn	States 127.0.0.1:65371 Error from InMemDB: Error: The pattern "min: 0, max: 100, show: false" is not a valid ID pattern at Module.pattern2RegEx (file:///C:/SmartHome/node_modules/@iobroker/js-controller-common-db/src/lib/common/tools.ts:2515:15) at StatesInMemoryServer.handleSubscribe (file:///C:/SmartHome/node_modules/@iobroker/db-base/src/lib/inMemFileDB.ts:323:59) at StatesInMemoryServer._subscribeForClient (file:///C:/SmartHome/node_modules/@iobroker/db-states-file/src/lib/states/statesInMemFileDB.js:240:14) at RedisHandler.<anonymous> (file:///C:/SmartHome/node_modules/@iobroker/db-states-jsonl/src/lib/states/statesInMemServerRedis.js:380:26) at RedisHandler.emit (node:events:519:28) at Immediate._onImmediate (file:///C:/SmartHome/node_modules/@iobroker/db-base/src/lib/redisHandler.ts:210:37) at processImmediate (node:internal/timers:484:21)
                
                web.0
                2026-06-08 17:03:57.034	error	Cannot subscribe "chartDiv.innerHTML = ''": Error The pattern "chartDiv.innerHTML = ''" is not a valid ID pattern
                
                web.0
                2026-06-08 17:03:57.034	error	Cannot subscribe "el.style.top = '0'": Error The pattern "el.style.top = '0'" is not a valid ID pattern
                
                web.0
                2026-06-08 17:03:57.034	error	Cannot subscribe "const oldEl = document.getElementById('card_' + window.activeM3CardId)": Error The pattern "const oldEl = document.getElementById('card_' + window.activeM3CardId)" is not a valid ID pattern
                
                const rooms: RoomHeater[] = [
                    { id: 'bad',  label: 'Bad',    actOid: 'hm-rpc.1.000A1D8997CC3E.1.ACTUAL_TEMPERATURE',   setOid: 'hm-rpc.1.000A1D8997CC3E.1.SET_POINT_TEMPERATURE',   valveOid: 'hm-rpc.1.000A1D8997CC3E.1.LEVEL',   windowOids: ['hm-rpc.0.OEQ0926852.1.STATE'], humidOid: 'hm-rpc.0.OEQ0670648.1.HUMIDITY' },
                    { id: 'schlafzimmer',label: 'Zimmer 1',    anonLabel: 'Schlafzimmer',  actOid: 'hm-rpc.1.000A1D8997CC3E.1.ACTUAL_TEMPERATURE',   setOid: 'hm-rpc.1.000A1D8997CC3E.1.SET_POINT_TEMPERATURE',   valveOid: 'hm-rpc.1.000A1D8997CC3E.1.LEVEL',   windowOids: ['hm-rpc.0.OEQ0926852.1.STATE'], humidOid: 'hm-rpc.0.OEQ0670648.1.HUMIDITY' },
                    { id: 'kind1',       label: 'Zimmer 2',    anonLabel: 'Kinderzimmer 1', actOid: 'hm-rpc.1.000A1D8997CC3E.1.ACTUAL_TEMPERATURE',   setOid: 'hm-rpc.1.000A1D8997CC3E.1.SET_POINT_TEMPERATURE',   valveOid: 'hm-rpc.1.000A1D8997CC3E.1.LEVEL',   windowOids: ['hm-rpc.0.OEQ0926852.1.STATE'], humidOid: 'hm-rpc.0.OEQ0670648.1.HUMIDITY' },
                 
                ];
                

                Bitte benutzt das Voting rechts unten im Beitrag wenn er euch geholfen hat.
                Immer Daten sichern!

                OliverIOO M 2 Antworten Letzte Antwort
                0
                • sigi234S sigi234

                  @mrMuppet

                  Ok, stürzt ab:

                  Screenshot (2102).png

                  web.0
                  2026-06-08 17:03:57.041	error	Cannot subscribe "curve: s_curve, width: s_width, dashArray: s_dash": Error The pattern "curve: s_curve, width: s_width, dashArray: s_dash" is not a valid ID pattern
                  
                  web.0
                  2026-06-08 17:03:57.041	error	Cannot subscribe "min: 0, max: 100, show: false": Error The pattern "min: 0, max: 100, show: false" is not a valid ID pattern
                  
                  host.SmartHome-Server
                  2026-06-08 17:03:57.034	warn	States 127.0.0.1:65371 Error from InMemDB: Error: The pattern "curve: s_curve, width: s_width, dashArray: s_dash" is not a valid ID pattern at Module.pattern2RegEx (file:///C:/SmartHome/node_modules/@iobroker/js-controller-common-db/src/lib/common/tools.ts:2515:15) at StatesInMemoryServer.handleSubscribe (file:///C:/SmartHome/node_modules/@iobroker/db-base/src/lib/inMemFileDB.ts:323:59) at StatesInMemoryServer._subscribeForClient (file:///C:/SmartHome/node_modules/@iobroker/db-states-file/src/lib/states/statesInMemFileDB.js:240:14) at RedisHandler.<anonymous> (file:///C:/SmartHome/node_modules/@iobroker/db-states-jsonl/src/lib/states/statesInMemServerRedis.js:380:26) at RedisHandler.emit (node:events:519:28) at Immediate._onImmediate (file:///C:/SmartHome/node_modules/@iobroker/db-base/src/lib/redisHandler.ts:210:37) at processImmediate (node:internal/timers:484:21)
                  
                  host.SmartHome-Server
                  2026-06-08 17:03:57.034	warn	States 127.0.0.1:65371 Error from InMemDB: Error: The pattern "min: 0, max: 100, show: false" is not a valid ID pattern at Module.pattern2RegEx (file:///C:/SmartHome/node_modules/@iobroker/js-controller-common-db/src/lib/common/tools.ts:2515:15) at StatesInMemoryServer.handleSubscribe (file:///C:/SmartHome/node_modules/@iobroker/db-base/src/lib/inMemFileDB.ts:323:59) at StatesInMemoryServer._subscribeForClient (file:///C:/SmartHome/node_modules/@iobroker/db-states-file/src/lib/states/statesInMemFileDB.js:240:14) at RedisHandler.<anonymous> (file:///C:/SmartHome/node_modules/@iobroker/db-states-jsonl/src/lib/states/statesInMemServerRedis.js:380:26) at RedisHandler.emit (node:events:519:28) at Immediate._onImmediate (file:///C:/SmartHome/node_modules/@iobroker/db-base/src/lib/redisHandler.ts:210:37) at processImmediate (node:internal/timers:484:21)
                  
                  web.0
                  2026-06-08 17:03:57.034	error	Cannot subscribe "chartDiv.innerHTML = ''": Error The pattern "chartDiv.innerHTML = ''" is not a valid ID pattern
                  
                  web.0
                  2026-06-08 17:03:57.034	error	Cannot subscribe "el.style.top = '0'": Error The pattern "el.style.top = '0'" is not a valid ID pattern
                  
                  web.0
                  2026-06-08 17:03:57.034	error	Cannot subscribe "const oldEl = document.getElementById('card_' + window.activeM3CardId)": Error The pattern "const oldEl = document.getElementById('card_' + window.activeM3CardId)" is not a valid ID pattern
                  
                  const rooms: RoomHeater[] = [
                      { id: 'bad',  label: 'Bad',    actOid: 'hm-rpc.1.000A1D8997CC3E.1.ACTUAL_TEMPERATURE',   setOid: 'hm-rpc.1.000A1D8997CC3E.1.SET_POINT_TEMPERATURE',   valveOid: 'hm-rpc.1.000A1D8997CC3E.1.LEVEL',   windowOids: ['hm-rpc.0.OEQ0926852.1.STATE'], humidOid: 'hm-rpc.0.OEQ0670648.1.HUMIDITY' },
                      { id: 'schlafzimmer',label: 'Zimmer 1',    anonLabel: 'Schlafzimmer',  actOid: 'hm-rpc.1.000A1D8997CC3E.1.ACTUAL_TEMPERATURE',   setOid: 'hm-rpc.1.000A1D8997CC3E.1.SET_POINT_TEMPERATURE',   valveOid: 'hm-rpc.1.000A1D8997CC3E.1.LEVEL',   windowOids: ['hm-rpc.0.OEQ0926852.1.STATE'], humidOid: 'hm-rpc.0.OEQ0670648.1.HUMIDITY' },
                      { id: 'kind1',       label: 'Zimmer 2',    anonLabel: 'Kinderzimmer 1', actOid: 'hm-rpc.1.000A1D8997CC3E.1.ACTUAL_TEMPERATURE',   setOid: 'hm-rpc.1.000A1D8997CC3E.1.SET_POINT_TEMPERATURE',   valveOid: 'hm-rpc.1.000A1D8997CC3E.1.LEVEL',   windowOids: ['hm-rpc.0.OEQ0926852.1.STATE'], humidOid: 'hm-rpc.0.OEQ0670648.1.HUMIDITY' },
                   
                  ];
                  
                  OliverIOO Offline
                  OliverIOO Offline
                  OliverIO
                  schrieb am zuletzt editiert von
                  #17

                  @sigi234

                  als mit dem adapter unbeteiligter entdecke ich einen effekt, den ich auch bei jsontemplate habe wenn man nichts davon weiß

                  4b12811d-058e-48d5-90f3-8c9eeb8aa593-image.jpeg

                  alles html was durch ein widget in vis2 (auch vis1) eingebracht wird, wird durch vis nochmal auf binding notationen untersucht.
                  ich vermute mal, das an den stellen an denen ich undefined markiert habe klammerkonstrukte stehen, also
                  { .... }
                  das wird als binding erkannt und interpretiert. da da ungültiges drin steht ist das ergebnis undefined.

                  verhindern kann man das aktuell nur, das man die klammern {} jeweils in separate zeilen schreibt.
                  also aus

                  { ... }
                  

                  wird

                  {
                  ...
                  }
                  

                  Meine Adapter und Widgets
                  TVProgram, SqueezeboxRPC, OpenLiga, RSSFeed, MyTime,, pi-hole2, vis-json-template, skiinfo, vis-mapwidgets, vis-2-widgets-rssfeed
                  Links im Profil

                  M 1 Antwort Letzte Antwort
                  1
                  • sigi234S sigi234

                    @mrMuppet

                    Ok, stürzt ab:

                    Screenshot (2102).png

                    web.0
                    2026-06-08 17:03:57.041	error	Cannot subscribe "curve: s_curve, width: s_width, dashArray: s_dash": Error The pattern "curve: s_curve, width: s_width, dashArray: s_dash" is not a valid ID pattern
                    
                    web.0
                    2026-06-08 17:03:57.041	error	Cannot subscribe "min: 0, max: 100, show: false": Error The pattern "min: 0, max: 100, show: false" is not a valid ID pattern
                    
                    host.SmartHome-Server
                    2026-06-08 17:03:57.034	warn	States 127.0.0.1:65371 Error from InMemDB: Error: The pattern "curve: s_curve, width: s_width, dashArray: s_dash" is not a valid ID pattern at Module.pattern2RegEx (file:///C:/SmartHome/node_modules/@iobroker/js-controller-common-db/src/lib/common/tools.ts:2515:15) at StatesInMemoryServer.handleSubscribe (file:///C:/SmartHome/node_modules/@iobroker/db-base/src/lib/inMemFileDB.ts:323:59) at StatesInMemoryServer._subscribeForClient (file:///C:/SmartHome/node_modules/@iobroker/db-states-file/src/lib/states/statesInMemFileDB.js:240:14) at RedisHandler.<anonymous> (file:///C:/SmartHome/node_modules/@iobroker/db-states-jsonl/src/lib/states/statesInMemServerRedis.js:380:26) at RedisHandler.emit (node:events:519:28) at Immediate._onImmediate (file:///C:/SmartHome/node_modules/@iobroker/db-base/src/lib/redisHandler.ts:210:37) at processImmediate (node:internal/timers:484:21)
                    
                    host.SmartHome-Server
                    2026-06-08 17:03:57.034	warn	States 127.0.0.1:65371 Error from InMemDB: Error: The pattern "min: 0, max: 100, show: false" is not a valid ID pattern at Module.pattern2RegEx (file:///C:/SmartHome/node_modules/@iobroker/js-controller-common-db/src/lib/common/tools.ts:2515:15) at StatesInMemoryServer.handleSubscribe (file:///C:/SmartHome/node_modules/@iobroker/db-base/src/lib/inMemFileDB.ts:323:59) at StatesInMemoryServer._subscribeForClient (file:///C:/SmartHome/node_modules/@iobroker/db-states-file/src/lib/states/statesInMemFileDB.js:240:14) at RedisHandler.<anonymous> (file:///C:/SmartHome/node_modules/@iobroker/db-states-jsonl/src/lib/states/statesInMemServerRedis.js:380:26) at RedisHandler.emit (node:events:519:28) at Immediate._onImmediate (file:///C:/SmartHome/node_modules/@iobroker/db-base/src/lib/redisHandler.ts:210:37) at processImmediate (node:internal/timers:484:21)
                    
                    web.0
                    2026-06-08 17:03:57.034	error	Cannot subscribe "chartDiv.innerHTML = ''": Error The pattern "chartDiv.innerHTML = ''" is not a valid ID pattern
                    
                    web.0
                    2026-06-08 17:03:57.034	error	Cannot subscribe "el.style.top = '0'": Error The pattern "el.style.top = '0'" is not a valid ID pattern
                    
                    web.0
                    2026-06-08 17:03:57.034	error	Cannot subscribe "const oldEl = document.getElementById('card_' + window.activeM3CardId)": Error The pattern "const oldEl = document.getElementById('card_' + window.activeM3CardId)" is not a valid ID pattern
                    
                    const rooms: RoomHeater[] = [
                        { id: 'bad',  label: 'Bad',    actOid: 'hm-rpc.1.000A1D8997CC3E.1.ACTUAL_TEMPERATURE',   setOid: 'hm-rpc.1.000A1D8997CC3E.1.SET_POINT_TEMPERATURE',   valveOid: 'hm-rpc.1.000A1D8997CC3E.1.LEVEL',   windowOids: ['hm-rpc.0.OEQ0926852.1.STATE'], humidOid: 'hm-rpc.0.OEQ0670648.1.HUMIDITY' },
                        { id: 'schlafzimmer',label: 'Zimmer 1',    anonLabel: 'Schlafzimmer',  actOid: 'hm-rpc.1.000A1D8997CC3E.1.ACTUAL_TEMPERATURE',   setOid: 'hm-rpc.1.000A1D8997CC3E.1.SET_POINT_TEMPERATURE',   valveOid: 'hm-rpc.1.000A1D8997CC3E.1.LEVEL',   windowOids: ['hm-rpc.0.OEQ0926852.1.STATE'], humidOid: 'hm-rpc.0.OEQ0670648.1.HUMIDITY' },
                        { id: 'kind1',       label: 'Zimmer 2',    anonLabel: 'Kinderzimmer 1', actOid: 'hm-rpc.1.000A1D8997CC3E.1.ACTUAL_TEMPERATURE',   setOid: 'hm-rpc.1.000A1D8997CC3E.1.SET_POINT_TEMPERATURE',   valveOid: 'hm-rpc.1.000A1D8997CC3E.1.LEVEL',   windowOids: ['hm-rpc.0.OEQ0926852.1.STATE'], humidOid: 'hm-rpc.0.OEQ0670648.1.HUMIDITY' },
                     
                    ];
                    
                    M Offline
                    M Offline
                    mrMuppet
                    schrieb am zuletzt editiert von
                    #18

                    @sigi234 Versuche es mal mit dieser Version:

                    // ============================================================
                    // renderHeizung — M3 Material You Styling (9 Räume Grid) - v1.3 (History Edition)
                    // ioBroker TypeScript — Inkl. Luftfeuchtigkeit, SYNC-FIX & MULTI-FENSTER
                    // CHANGELOG v1.3:
                    // - FIX: Vis/Web-Adapter Abstürze ("is not a valid ID pattern") behoben.
                    //   Der im HTML eingebettete JavaScript-Code für ApexCharts wurde 
                    //   umstrukturiert, damit der ioBroker-Parser die JSON-Objekte nicht
                    //   mehr fälschlicherweise für Vis-Datenpunkt-Bindings hält.
                    // CHANGELOG v1.2:
                    // - ANPASSUNG: Abfrage auf Standard 'history.0' Adapter umgestellt.
                    // ============================================================
                    
                    const DP_HEIZUNG_HTML: string = '0_userdata.0.dashboard.heizungHTML';
                    const DP_PRIVACY:      string = '0_userdata.0.dashboard.privacyMode';
                    
                    const DP_CHART_BRENNER: string = '0_userdata.0.dashboard.chartShowBrenner';
                    const DP_CHART_VALVE:   string = '0_userdata.0.dashboard.chartShowValve';
                    const DP_CHART_ACT:     string = '0_userdata.0.dashboard.chartShowAct';
                    const DP_CHART_SET:     string = '0_userdata.0.dashboard.chartShowSet';
                    const DP_CHART_WINDOW:  string = '0_userdata.0.dashboard.chartShowWindow';
                    const DP_CHART_HUMID:   string = '0_userdata.0.dashboard.chartShowHumid';
                    
                    const DP_HEIZUNG_AN: string = '0_userdata.0.Heizung.Heizung_an';
                    const DP_MODULATION: string = 'ems-esp.0.heatSources.actualModulation';
                    
                    // INSTANZ DES HISTORY-ADAPTERS
                    const HISTORY_INSTANCE: string = 'history.0';
                    
                    let globalHeatHistory: any[] = [];
                    let roomHistories: Record<string, { act: any[]; set: any[]; valve: any[]; heat: any[]; window: any[]; humid: any[] }> = {};
                    
                    const M3: Record<string, string> = {
                        primary:                 'var(--m3-primary, rgb(18, 66, 24))',
                        onPrimary:               'var(--m3-on-primary, rgb(255, 255, 255))',
                        primaryContainer:        'var(--m3-primary-container, rgb(43, 90, 45))',
                        onPrimaryContainer:      'var(--m3-on-primary-container, rgb(155, 208, 151))',
                        surfaceContainerLow:     'var(--m3-surface-container-low, rgb(243, 244, 237))',
                        surfaceContainerHighest: 'var(--m3-surface-container-highest, rgb(225, 227, 220))',
                        surfaceVariant:          'var(--m3-surface-variant, rgb(221, 229, 216))',
                        onSurface:               'var(--m3-on-surface, rgb(25, 28, 24))',
                        onSurfaceVariant:        'var(--m3-on-surface-variant, rgb(66, 73, 63))',
                        heatPrimary:             'var(--m3-error, rgb(186, 26, 26))',
                        heatContainer:           'var(--m3-error-container, rgb(255, 218, 214))',
                        onHeatContainer:         'var(--m3-on-error-container, rgb(65, 0, 2))',
                        pendingPrimary:          'rgb(176, 96, 0)',
                        pendingContainer:        'rgb(255, 220, 193)',
                        onPendingContainer:      'rgb(43, 20, 0)',
                    };
                    
                    const ICONS: Record<string, string> = {
                        thermostat: 'M15 13V5A3 3 0 0 0 9 5V13A5 5 0 1 0 15 13M12 4A1 1 0 0 1 13 5V8H11V5A1 1 0 0 1 12 4Z',
                        fire:       'M17.5 11C15 7.5 13.5 8.5 12 6C10.5 8.5 9 7.5 6.5 11C4 14.5 5.5 19 12 19C18.5 19 20 14.5 17.5 11M12 17C9 17 8.5 14 10 12.5C11.5 11 12 12.5 12 12.5C12 12.5 12.5 11 14 12.5C15.5 14 15 17 12 17Z',
                        minus:      'M19 13H5V11H19V13Z',
                        plus:       'M19 13H13V19H11V13H5V11H11V5H13V11H19V13Z',
                        power:      'M16.56 5.44L15.11 6.89C16.84 7.94 18 9.83 18 12A6 6 0 0 1 12 18A6 6 0 0 1 6 12C6 9.83 7.16 7.94 8.88 6.88L7.44 5.44C5.36 6.88 4 9.28 4 12A8 8 0 0 0 12 20A8 8 0 0 0 20 12C20 9.28 18.64 6.88 16.56 5.44M11 3H13V13H11V3Z',
                        window:     'M3 3H21V21H3V3M5 5V11H11V5H5M13 5V11H19V5H13M5 13V19H11V13H5M13 13V19H19V13H13Z',
                        water:      'M12 20a6 6 0 0 1-6-6c0-4 6-10.75 6-10.75S18 10 18 14a6 6 0 0 1-6 6z'
                    };
                    
                    interface RoomHeater {
                        id:          string;
                        label:       string;
                        anonLabel?:  string;
                        actOid:      string;
                        setOid:      string;
                        valveOid:    string;
                        windowOids?: string[];
                        humidOid?:   string;
                    }
                    
                    const rooms: RoomHeater[] = [
                        { id: 'wohnzimmer',  label: 'Wohnzimmer',    actOid: 'alias.0.Heizung.Wohnzimmer.ACTUAL',   setOid: 'alias.0.Heizung.Wohnzimmer.SET',   valveOid: 'alias.0.Heizung.Wohnzimmer.VALVE',   windowOids: ['alias.0.Sicherheit.Wohnzimmer.Fenster.STATE', 'alias.0.Sicherheit.Wohnzimmer.Tuer.STATE'], humidOid: 'alias.0.Klima.Wohnzimmer.HUMIDITY' },
                        { id: 'schlafzimmer',label: 'Zimmer Mina',    anonLabel: 'Schlafzimmer',  actOid: 'alias.0.Heizung.Schlafzimmer.ACTUAL', setOid: 'alias.0.Heizung.Schlafzimmer.SET', valveOid: 'alias.0.Heizung.Schlafzimmer.VALVE', windowOids: ['alias.0.Sicherheit.Schlafzimmer.Fenster.STATE'], humidOid: 'alias.0.Klima.Schlafzimmer.HUMIDITY' },
                        { id: 'kind1',       label: 'Zimmer Nova',    anonLabel: 'Kinderzimmer 1', actOid: 'alias.0.Heizung.Kind1.ACTUAL',        setOid: 'alias.0.Heizung.Kind1.SET',        valveOid: 'alias.0.Heizung.Kind1.VALVE',        windowOids: ['alias.0.Sicherheit.Kinderzimmer1.Fenster.STATE'], humidOid: 'alias.0.Klima.Kind1.HUMIDITY' },
                        { id: 'kind2',       label: 'Zimmer Vasco',   anonLabel: 'Kinderzimmer 2', actOid: 'alias.0.Heizung.Kind2.ACTUAL',        setOid: 'alias.0.Heizung.Kind2.SET',        valveOid: 'alias.0.Heizung.Kind2.VALVE',        windowOids: ['alias.0.Sicherheit.Kinderzimmer2.Fenster.STATE'], humidOid: 'alias.0.Klima.Kind2.HUMIDITY' },
                        { id: 'bad',         label: 'Bad',            actOid: 'alias.0.Heizung.Bad.ACTUAL',          setOid: 'alias.0.Heizung.Bad.SET',          valveOid: 'alias.0.Heizung.Bad.VALVE',          windowOids: ['alias.0.Sicherheit.Bad.Fenster.STATE'], humidOid: 'alias.0.Klima.Bad.HUMIDITY' },
                        { id: 'dachzimmer',  label: 'Dachzimmer',     actOid: 'alias.0.Heizung.Dachzimmer.ACTUAL',   setOid: 'alias.0.Heizung.Dachzimmer.SET',   valveOid: 'alias.0.Heizung.Dachzimmer.VALVE',   windowOids: ['alias.0.Sicherheit.Dachzimmer.Fenster_Garten.STATE', 'alias.0.Sicherheit.Dachzimmer.Fenster_Strasse.STATE'], humidOid: 'alias.0.Klima.Dachzimmer.HUMIDITY' },
                        { id: 'flur',        label: 'Flur',            actOid: 'alias.0.Heizung.Flur.ACTUAL',         setOid: 'alias.0.Heizung.Flur.SET',         valveOid: 'alias.0.Heizung.Flur.VALVE',         windowOids: ['alias.0.Sicherheit.Haustuer.STATE'], humidOid: 'alias.0.Klima.Flur.HUMIDITY' },
                        { id: 'gaesteklo',   label: 'Gäste WC',       actOid: 'alias.0.Heizung.GaesteKlo.ACTUAL',    setOid: 'alias.0.Heizung.GaesteKlo.SET',    valveOid: 'alias.0.Heizung.GaesteKlo.VALVE' },
                        { id: 'hwr',         label: 'HWR',             actOid: 'alias.0.Heizung.HWR.ACTUAL',          setOid: 'alias.0.Heizung.HWR.SET',          valveOid: 'alias.0.Heizung.HWR.VALVE',          windowOids: ['alias.0.Sicherheit.HWR.Fenster.STATE'] },
                    ];
                    
                    const COL_WIDTH:  number = 202;
                    const HERO_WIDTH: number = 636;
                    const CARD_HEIGHT:number = 144;
                    const GUTTER:     number = 15;
                    const SHADOW:     string = '0px 1px 2px 0px rgba(0,0,0,0.3), 0px 1px 3px 1px rgba(0,0,0,0.15)';
                    
                    function getSafeVal(oid: string | undefined, fallback: any = 0): any {
                        if (oid && existsState(oid)) {
                            const s = getState(oid);
                            return s ? s.val : fallback;
                        }
                        return fallback;
                    }
                    
                    function svgIcon(name: string, color: string, size: number = 24): string {
                        const path = ICONS[name] || ICONS['thermostat'];
                        return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" style="flex-shrink:0; transition: fill 0.3s ease;"><path fill="${color}" d="${path}"/></svg>`;
                    }
                    
                    async function fetchHistoryData(oid: string | undefined, start: number, end: number, isRaw: boolean = false): Promise<any[]> {
                        if (!oid || !existsState(oid)) return [];
                        try {
                            const queryOptions = isRaw
                                ? { start, end, aggregate: 'none', limit: 400, ignoreNull: true }
                                : { start, end, step: 1800000, aggregate: 'average', ignoreNull: true };
                    
                            const result = await new Promise<any>(resolve => {
                                sendTo(HISTORY_INSTANCE, 'getHistory', { id: oid, options: queryOptions }, res => resolve(res));
                            });
                    
                            if (result && result.result && result.result.length > 0) {
                                let cleanData = result.result
                                    .filter((r: any) => r.val !== null)
                                    .map((r: any) => {
                                        let v = r.val;
                                        if (typeof v === 'boolean')      v = v ? 100 : 0;
                                        else if (typeof v === 'string')  v = Number(v) || 0;
                                        return [r.ts, Math.round(v * 10) / 10];
                                    })
                                    .filter((r: any) => !isNaN(r[1]));
                    
                                if (isRaw && cleanData.length > 0) {
                                    const maxVal = Math.max(...cleanData.map((d: any) => d[1]));
                                    if (maxVal <= 1.0 && maxVal > 0) cleanData.forEach((d: any) => (d[1] = d[1] * 100));
                                }
                                return cleanData;
                            }
                        } catch (e) {}
                        return [];
                    }
                    
                    async function updateHistories(): Promise<void> {
                        const end   = new Date().getTime();
                        const start = end - 24 * 60 * 60 * 1000;
                    
                        globalHeatHistory = await fetchHistoryData(DP_MODULATION, start, end, true);
                    
                        for (const room of rooms) {
                            const [actData, setData, valveData, humidData] = await Promise.all([
                                fetchHistoryData(room.actOid,   start, end, false),
                                fetchHistoryData(room.setOid,   start, end, false),
                                fetchHistoryData(room.valveOid, start, end, true),
                                fetchHistoryData(room.humidOid, start, end, false),
                            ]);
                    
                            let allWindowData: any[] = [];
                            if (room.windowOids && room.windowOids.length > 0) {
                                const wResults = await Promise.all(room.windowOids.map(wOid => fetchHistoryData(wOid, start, end, true)));
                                allWindowData = wResults.filter(wData => wData.length > 0);
                            }
                    
                            roomHistories[room.id] = { act: actData, set: setData, valve: valveData, heat: globalHeatHistory, window: allWindowData, humid: humidData };
                        }
                        renderHeizung();
                    }
                    
                    function renderHeroCard(activeRoomsCount: number, isZentralAn: boolean): string {
                        const mod    = Number(getSafeVal(DP_MODULATION, 0)) || 0;
                        const bg     = isZentralAn ? M3.primaryContainer : M3.surfaceContainerLow;
                        const fg     = isZentralAn ? M3.onPrimaryContainer : M3.onSurfaceVariant;
                        const iconBg = isZentralAn ? M3.primary : M3.surfaceContainerHighest;
                        const iconFg = isZentralAn ? M3.onPrimary : M3.onSurfaceVariant;
                        return `<div style="width:${HERO_WIDTH}px; height:${CARD_HEIGHT}px; border-radius:12px; padding:20px 24px; box-sizing:border-box; background:${bg}; box-shadow:${SHADOW}; display:flex; align-items:center; justify-content:space-between; font-family:Roboto,sans-serif; transition:all 0.3s ease;"> <div style="display:flex; align-items:center; gap:20px;"> <div style="width:56px; height:56px; border-radius:28px; background:${iconBg}; display:flex; align-items:center; justify-content:center; transition:all 0.3s ease;"> ${svgIcon(isZentralAn ? 'fire' : 'power', iconFg, 32)} </div> <div> <div style="font-size:24px; font-weight:500; color:${fg}; margin-bottom:4px;">Zentralheizung</div> <div style="font-size:15px; font-weight:500; color:${fg}; opacity:0.8;">${isZentralAn ? `Heizbetrieb aktiv • ${mod}% Leistung` : 'Standby / Aus'}</div> </div> </div> <div style="text-align:right;"> <div style="font-size:36px; font-weight:600; color:${isZentralAn ? M3.heatPrimary : fg}; line-height:1;">${activeRoomsCount}</div> <div style="font-size:13px; font-weight:500; color:${fg}; text-transform:uppercase; letter-spacing:0.5px; margin-top:4px;">Räume fordern Wärme</div> </div> </div>`;
                    }
                    
                    function renderRoomCard(room: RoomHeater, isZentralAn: boolean, isPrivacyMode: boolean): string {
                        const actVal     = Number(getSafeVal(room.actOid, 0)) || 0;
                        const setVal     = Number(getSafeVal(room.setOid, 0)) || 0;
                        const valveRaw   = getSafeVal(room.valveOid, 0);
                        const humidVal   = room.humidOid ? Number(getSafeVal(room.humidOid, 0)) || 0 : null;
                        const displayName = isPrivacyMode && room.anonLabel ? room.anonLabel : room.label;
                    
                        let windowOpen = false;
                        if (room.windowOids && room.windowOids.length > 0) {
                            windowOpen = room.windowOids.some(oid => {
                                const raw = getSafeVal(oid, false);
                                return raw === true || raw === 1 || raw === '1' || raw === 'true';
                            });
                        }
                    
                        const wantsHeat = (typeof valveRaw === 'number' && valveRaw > 0)
                            || (typeof valveRaw === 'string' && Number(valveRaw) > 0)
                            || valveRaw === true || valveRaw === 'true';
                    
                        let bg: string, fg: string, fgSub: string, iconBg: string, iconFg: string;
                        if (wantsHeat && isZentralAn) {
                            bg = M3.heatContainer; fg = M3.onHeatContainer; fgSub = M3.onHeatContainer; iconBg = M3.heatPrimary; iconFg = '#ffffff';
                        } else if (wantsHeat && !isZentralAn) {
                            bg = M3.pendingContainer; fg = M3.onPendingContainer; fgSub = M3.onPendingContainer; iconBg = M3.pendingPrimary; iconFg = '#ffffff';
                        } else {
                            bg = M3.surfaceContainerLow; fg = M3.onSurface; fgSub = M3.onSurfaceVariant; iconBg = M3.surfaceContainerHighest; iconFg = M3.onSurfaceVariant;
                        }
                    
                        const jsMinus = `(function(e, btn){ e.stopPropagation(); var card = btn.closest('.m3-room-card'); var span = card.querySelector('.target-temp'); var current = parseFloat(span.innerText); var newVal = current - 0.5; span.innerText = newVal.toFixed(1) + '°'; if(typeof vis!=='undefined'&&vis.conn&&vis.conn.setState) vis.conn.setState('${room.setOid}', newVal); else if(typeof vis!=='undefined'&&vis.setValue) vis.setValue('${room.setOid}', newVal); })(event, this);`;
                        const jsPlus  = `(function(e, btn){ e.stopPropagation(); var card = btn.closest('.m3-room-card'); var span = card.querySelector('.target-temp'); var current = parseFloat(span.innerText); var newVal = current + 0.5; span.innerText = newVal.toFixed(1) + '°'; if(typeof vis!=='undefined'&&vis.conn&&vis.conn.setState) vis.conn.setState('${room.setOid}', newVal); else if(typeof vis!=='undefined'&&vis.setValue) vis.setValue('${room.setOid}', newVal); })(event, this);`;
                    
                        const windowHtml = windowOpen
                            ? `<div style="width:24px; height:24px; display:flex; align-items:center; justify-content:center; animation: blink 2s infinite;">${svgIcon('window', M3.heatPrimary, 20)}</div>`
                            : '';
                    
                        const humidHtml = (humidVal !== null && humidVal > 0)
                            ? `<div style="position:absolute; top:36px; right:-4px; width:40px; display:flex; justify-content:center; align-items:center; font-size:11px; font-weight:600; color:${fgSub}; opacity:0.85; letter-spacing:-0.5px;">${Math.round(humidVal)}%<svg width="10" height="10" viewBox="0 0 24 24" style="margin-left:1px"><path fill="currentColor" d="M12 20a6 6 0 0 1-6-6c0-4 6-10.75 6-10.75S18 10 18 14a6 6 0 0 1-6 6z"/></svg></div>`
                            : '';
                    
                        const historyData = roomHistories[room.id] ?? { act: [], set: [], valve: [], heat: [], window: [], humid: [] };
                    
                        return `<div id="wrapper_${room.id}" style="width:${COL_WIDTH}px; height:${CARD_HEIGHT}px; position:relative; z-index:1;">
                            <div id="card_${room.id}" class="m3-room-card" data-history='${JSON.stringify(historyData)}' onclick="if(typeof window.toggleCard === 'function') window.toggleCard(this, '${room.id}');" style="position:absolute; top:0; left:0; width:100%; height:100%; border-radius:12px; padding:14px 16px; box-sizing:border-box; background:${bg}; box-shadow:${SHADOW}; display:flex; flex-direction:column; font-family:Roboto,sans-serif; transition:all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); cursor:pointer; overflow:hidden;">
                                <div style="display:flex; justify-content:space-between; align-items:flex-start; width:100%;">
                                    <div style="font-size:16px; font-weight:500; color:${fg}; max-width:145px; line-height:1.2; overflow:hidden; text-overflow:ellipsis; transition:color 0.3s ease;">${displayName}</div>
                                    <div style="display:flex; gap:6px; align-items:flex-start; position:relative;">
                                        ${windowHtml}
                                        <div style="width:32px; height:32px; border-radius:16px; background:${iconBg}; display:flex; align-items:center; justify-content:center; flex-shrink:0;">${svgIcon('thermostat', iconFg, 18)}</div>
                                        ${humidHtml}
                                    </div>
                                </div>
                                <div style="display:flex; align-items:baseline; margin-top:-6px; width:100%;">
                                    <div style="font-size:36px; font-weight:400; color:${fg}; letter-spacing:-1px;">${actVal.toFixed(1)}</div>
                                    <div style="font-size:18px; font-weight:500; color:${fgSub}; margin-left:2px; opacity:0.7;">°C</div>
                                </div>
                                <div id="controls_${room.id}" style="display:flex; justify-content:space-between; align-items:center; width:100%; margin-top:auto;">
                                    <div style="display:flex; flex-direction:column;">
                                        <span style="font-size:11px; font-weight:500; text-transform:uppercase; letter-spacing:0.5px; color:${fgSub}; opacity:0.7;">Ziel</span>
                                        <span class="target-temp" style="font-size:15px; font-weight:600; color:${fg};">${setVal.toFixed(1)}°</span>
                                    </div>
                                    <div style="display:flex; gap:6px;">
                                        <div onclick="${jsMinus}" style="width:32px; height:32px; border-radius:16px; background:rgba(0,0,0,0.06); display:flex; align-items:center; justify-content:center; cursor:pointer; user-select:none; transition:opacity 0.1s; -webkit-tap-highlight-color:transparent;">${svgIcon('minus', fg, 20)}</div>
                                        <div onclick="${jsPlus}"  style="width:32px; height:32px; border-radius:16px; background:rgba(0,0,0,0.06); display:flex; align-items:center; justify-content:center; cursor:pointer; user-select:none; transition:opacity 0.1s; -webkit-tap-highlight-color:transparent;">${svgIcon('plus', fg, 20)}</div>
                                    </div>
                                </div>
                                <div id="chart_wrapper_${room.id}" style="opacity:0; height:0px; width:100%; transition:opacity 0.4s ease; margin-top:0px; pointer-events:none;">
                                    <div id="chart_${room.id}" style="width:100%; height:100%;"></div>
                                </div>
                            </div>
                        </div>`;
                    }
                    
                    function renderHeizung(): void {
                        let activeRooms = 0;
                        const isZentralAn   = getSafeVal(DP_HEIZUNG_AN, false) === true;
                        const isPrivacyMode = getSafeVal(DP_PRIVACY,     false) === true;
                    
                        const showBrenner = getSafeVal(DP_CHART_BRENNER, true)  === true;
                        const showValve   = getSafeVal(DP_CHART_VALVE,   false) === true;
                        const showAct     = getSafeVal(DP_CHART_ACT,     true)  === true;
                        const showSet     = getSafeVal(DP_CHART_SET,     true)  === true;
                        const showWindow  = getSafeVal(DP_CHART_WINDOW,  true)  === true;
                        const showHumid   = getSafeVal(DP_CHART_HUMID,   true)  === true;
                    
                        for (const room of rooms) {
                            const valveRaw = getSafeVal(room.valveOid, 0);
                            if ((typeof valveRaw === 'number' && valveRaw > 0)
                                || (typeof valveRaw === 'string' && Number(valveRaw) > 0)
                                || valveRaw === true || valveRaw === 'true') activeRooms++;
                        }
                    
                        let cardsHtml = renderHeroCard(activeRooms, isZentralAn);
                        for (const room of rooms) cardsHtml += renderRoomCard(room, isZentralAn, isPrivacyMode);
                    
                        const html = `
                    <style>
                      @keyframes blink { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
                      .m3-room-card * { white-space: nowrap; }
                      .apexcharts-legend { padding: 0 !important; margin-top: -10px !important; }
                      .apexcharts-legend-text { color: var(--m3-on-surface-variant) !important; font-family: Roboto, sans-serif !important; font-size: 11px !important; }
                    </style>
                    
                    <div id="m3-dashboard-container"
                         data-cfg-brenner="${showBrenner}"
                         data-cfg-valve="${showValve}"
                         data-cfg-act="${showAct}"
                         data-cfg-set="${showSet}"
                         data-cfg-window="${showWindow}"
                         data-cfg-humid="${showHumid}"
                         style="position: relative; background: var(--m3-surface-container-low, rgb(237, 239, 232)); border-radius: 0 0 28px 28px; padding: 24px; display: flex; flex-wrap: wrap; align-content: flex-start; gap: ${GUTTER}px; width: 901px; box-sizing: border-box; transition: background-color 0.3s ease;">
                      ${cardsHtml}
                    </div>
                    
                    <script>
                      // Helper-Variablen ausserhalb der Objekte deklarieren (Vis-sicher)
                      var _act = 'act'; var _set = 'set'; var _valve = 'valve'; var _heat = 'heat'; var _win = 'window'; var _hum = 'humid';
                    
                      window.activeM3CardId = window.activeM3CardId || null;
                      window.toggleCard = function(el, roomId) {
                          if (window.activeM3CardId === roomId) { window.closeCard(el, roomId); return; }
                          if (window.activeM3CardId) { 
                              var oldEl = document.getElementById('card_' + window.activeM3CardId); 
                              if (oldEl) window.closeCard(oldEl, window.activeM3CardId); 
                          }
                          window.openCard(el, roomId);
                      };
                      window.closeCard = function(el, roomId) {
                          el.style.width = '100%'; el.style.height = '100%'; el.style.boxShadow = '${SHADOW}';
                          var chartWrapper = document.getElementById('chart_wrapper_' + roomId);
                          if (chartWrapper) {
                              chartWrapper.style.opacity = '0';
                              setTimeout(function() {
                                  chartWrapper.style.height = '0px';
                                  var chartDiv = document.getElementById('chart_' + roomId);
                                  if (chartDiv) {
                                      if (chartDiv._apexChart) { chartDiv._apexChart.destroy(); delete chartDiv._apexChart; }
                                      chartDiv.innerHTML = '';
                                  }
                              }, 400);
                          }
                          setTimeout(function() { el.parentElement.style.zIndex = '1'; }, 400); 
                          window.activeM3CardId = null;
                      };
                      window.openCard = function(el, roomId) {
                          window.activeM3CardId = roomId; 
                          var wrapper = el.parentElement; 
                          var container = document.getElementById('m3-dashboard-container');
                          wrapper.style.zIndex = '999'; 
                          var wrapperRect = wrapper.getBoundingClientRect(); 
                          var containerRect = container.getBoundingClientRect();
                          var expandedWidth = ${COL_WIDTH * 2 + GUTTER}; 
                          var expandedHeight = 310;
                          
                          el.style.top = 'auto'; el.style.bottom = 'auto'; el.style.left = 'auto'; el.style.right = 'auto';
                          if (wrapperRect.left + expandedWidth > containerRect.right - 10) { el.style.right = '0'; } else { el.style.left = '0'; }
                          if (wrapperRect.top + expandedHeight > containerRect.bottom - 10) { el.style.bottom = '0'; } else { el.style.top = '0'; }
                          el.style.width = expandedWidth + 'px'; el.style.height = expandedHeight + 'px'; el.style.boxShadow = '0px 14px 28px rgba(0,0,0,0.35), 0px 10px 10px rgba(0,0,0,0.22)';
                          
                          var chartWrapper = document.getElementById('chart_wrapper_' + roomId);
                          if (chartWrapper) { 
                              chartWrapper.style.height = '140px'; chartWrapper.style.marginTop = '16px'; 
                              setTimeout(function() { chartWrapper.style.opacity = '1'; window.renderChart(el, roomId); }, 450); 
                          }
                      };
                      window.renderChart = function(el, roomId) {
                          var chartDiv = el.querySelector('#chart_' + roomId);
                          if (!chartDiv || chartDiv.innerHTML.trim() !== '') return;
                          var rawData = el.getAttribute('data-history'); 
                          var cData = {}; cData[_act] = []; cData[_set] = []; cData[_valve] = []; cData[_heat] = []; cData[_win] = []; cData[_hum] = [];
                          try { cData = JSON.parse(rawData); } catch(e) {}
                          if (cData[_act].length === 0 && cData[_set].length === 0) { 
                              chartDiv.innerHTML = '<div style="color:var(--m3-on-surface-variant); font-size:13px; text-align:center; padding-top:50px; font-weight:500;">Keine Historie</div>'; 
                              return; 
                          }
                          
                          var rootStyles = getComputedStyle(document.documentElement);
                          var colorIst     = rootStyles.getPropertyValue('--m3-primary').trim()  || '#4caf50';
                          var colorSoll    = rootStyles.getPropertyValue('--m3-outline').trim()   || '#9e9e9e';
                          var colorVentil  = rootStyles.getPropertyValue('--m3-error').trim()     || '#ffb4ab';
                          var colorBrenner = '#ff9800';
                          var colorFenster = '#00bfff';
                          var colorHumid   = '#2196f3';
                          
                          var container    = el.closest('#m3-dashboard-container');
                          var cfgShowBrenner = container.getAttribute('data-cfg-brenner') === 'true';
                          var cfgShowValve   = container.getAttribute('data-cfg-valve')   === 'true';
                          var cfgShowAct     = container.getAttribute('data-cfg-act')     === 'true';
                          var cfgShowSet     = container.getAttribute('data-cfg-set')     === 'true';
                          var cfgShowWindow  = container.getAttribute('data-cfg-window')  === 'true';
                          var cfgShowHumid   = container.getAttribute('data-cfg-humid')   === 'true';
                          
                          function drawGraph() {
                              chartDiv.innerHTML = '';
                              var s_series = []; var s_colors = []; var s_curve = []; var s_width = []; var s_dash = []; var s_fillType = []; var s_opacity = []; var s_yaxis = [];
                              var actHasData = cData[_act] && cData[_act].length > 0;
                              var setHasData = cData[_set] && cData[_set].length > 0;
                              var minTemp = 999; var maxTemp = -999;
                              
                              if (cfgShowAct && actHasData) { for (var i = 0; i < cData[_act].length; i++) { if (cData[_act][i][1] < minTemp) minTemp = cData[_act][i][1]; if (cData[_act][i][1] > maxTemp) maxTemp = cData[_act][i][1]; } }
                              if (cfgShowSet && setHasData) { for (var j = 0; j < cData[_set].length; j++) { if (cData[_set][j][1] < minTemp) minTemp = cData[_set][j][1]; if (cData[_set][j][1] > maxTemp) maxTemp = cData[_set][j][1]; } }
                              if (minTemp === 999) { minTemp = 15; maxTemp = 25; } else { minTemp = Math.floor(minTemp) - 1; maxTemp = Math.ceil(maxTemp) + 1; }
                              
                              var axisHum = Object.assign({}, {min: 0}, {max: 100}, {show: false});
                              var axisTemp = Object.assign({}, {min: minTemp}, {max: maxTemp}, {show: false}, {tickAmount: 5});
                              var lgnd = Object.assign({}, {show: true}, {position: 'top'}, {horizontalAlign: 'right'});
                              var xax = Object.assign({}, {type: 'datetime'}, {labels: Object.assign({}, {show: false})}, {axisBorder: Object.assign({}, {show: false})}, {axisTicks: Object.assign({}, {show: false})}, {tooltip: Object.assign({}, {enabled: false})});
                              var gridCfg = Object.assign({}, {show: false});
                              var ttip = Object.assign({}, {theme: 'dark'}, {x: Object.assign({}, {format: 'HH:mm'})});
                              var animCfg = Object.assign({}, {enabled: true}, {easing: 'easeinout'}, {speed: 800}, {dynamicAnimation: Object.assign({}, {enabled: false})});
                              var tbar = Object.assign({}, {show: false});
                              var dl = Object.assign({}, {enabled: false});
                              
                              if (cfgShowHumid && cData[_hum] && cData[_hum].length > 0) { 
                                  s_series.push(Object.assign({}, {name: 'Luftfeuchte'}, {type: 'area'}, {data: cData[_hum]})); 
                                  s_colors.push(colorHumid); s_curve.push('smooth'); s_width.push(0); s_dash.push(0); s_fillType.push('solid'); s_opacity.push(0.1); s_yaxis.push(axisHum); 
                              }
                              if (cfgShowBrenner) { 
                                  s_series.push(Object.assign({}, {name: 'Brenner'}, {type: 'area'}, {data: cData[_heat] || []})); 
                                  s_colors.push(colorBrenner); s_curve.push('smooth'); s_width.push(2); s_dash.push(0); s_fillType.push('gradient'); s_opacity.push(1); s_yaxis.push(axisHum); 
                              }
                              if (cfgShowValve)   { 
                                  s_series.push(Object.assign({}, {name: 'Ventil'}, {type: 'line'}, {data: cData[_valve] || []})); 
                                  s_colors.push(colorVentil);  s_curve.push('stepline'); s_width.push(2); s_dash.push(0); s_fillType.push('solid'); s_opacity.push(1); s_yaxis.push(axisHum); 
                              }
                              if (cfgShowAct)     { 
                                  s_series.push(Object.assign({}, {name: 'Ist-Temp'}, {type: 'area'}, {data: cData[_act] || []})); 
                                  s_colors.push(colorIst);    s_curve.push('smooth');   s_width.push(3); s_dash.push(0); s_fillType.push('gradient'); s_opacity.push(1); s_yaxis.push(axisTemp); 
                              }
                              if (cfgShowSet)     { 
                                  s_series.push(Object.assign({}, {name: 'Ziel-Temp'}, {type: 'line'}, {data: cData[_set] || []})); 
                                  s_colors.push(colorSoll);   s_curve.push('stepline'); s_width.push(2); s_dash.push(4); s_fillType.push('solid');    s_opacity.push(1); s_yaxis.push(axisTemp); 
                              }
                              
                              var windowAnnotations = [];
                              if (cfgShowWindow && cData[_win] && cData[_win].length > 0) {
                                  for (var w = 0; w < cData[_win].length; w++) {
                                      var wData = cData[_win][w]; var inWindowOpen = false; var winStart = null;
                                      for (var k = 0; k < wData.length; k++) { 
                                          var ts = wData[k][0]; var val = wData[k][1]; 
                                          if (val > 0 && !inWindowOpen) { inWindowOpen = true; winStart = ts; } 
                                          else if (val === 0 && inWindowOpen) { 
                                              inWindowOpen = false; 
                                              windowAnnotations.push(Object.assign({}, {x: winStart}, {x2: ts}, {fillColor: colorFenster}, {opacity: 0.15})); 
                                          } 
                                      }
                                      if (inWindowOpen && winStart) windowAnnotations.push(Object.assign({}, {x: winStart}, {x2: new Date().getTime()}, {fillColor: colorFenster}, {opacity: 0.15}));
                                  }
                              }
                              
                              if (cfgShowWindow) { 
                                  let safeTime = (actHasData) ? cData[_act][0][0] : new Date().getTime() - 3600000; 
                                  s_series.push(Object.assign({}, {name: 'Fenster offen'}, {type: 'area'}, {data: [[safeTime, 0]]})); 
                                  s_colors.push(colorFenster); s_curve.push('stepline'); s_width.push(0); s_dash.push(0); s_fillType.push('solid'); s_opacity.push(0); s_yaxis.push(axisTemp); 
                              }
                              if (s_series.length === 0) { chartDiv.innerHTML = '<div style="color:var(--m3-on-surface-variant); font-size:13px; text-align:center; padding-top:50px; font-weight:500;">Alle Graphen ausgeblendet</div>'; return; }
                              
                              var chartObj = Object.assign({}, {type: 'line'}, {height: 140}, {width: '100%'}, {parentHeightOffset: 0}, {toolbar: tbar}, {animations: animCfg});
                              var strkObj = Object.assign({}, {curve: s_curve}, {width: s_width}, {dashArray: s_dash});
                              var gradObj = Object.assign({}, {shadeIntensity: 1}, {opacityFrom: 0.45}, {opacityTo: 0.0}, {stops: [0, 100]});
                              var fillObj = Object.assign({}, {type: s_fillType}, {opacity: s_opacity}, {gradient: gradObj});
                              var annObj = Object.assign({}, {xaxis: windowAnnotations});
                              var options = Object.assign({}, {chart: chartObj}, {series: s_series}, {colors: s_colors}, {stroke: strkObj}, {fill: fillObj}, {dataLabels: dl}, {legend: lgnd}, {tooltip: ttip}, {xaxis: xax}, {yaxis: s_yaxis}, {grid: gridCfg}, {annotations: annObj});
                              
                              if (chartDiv._apexChart) { chartDiv._apexChart.destroy(); delete chartDiv._apexChart; }
                              try { var chart = new ApexCharts(chartDiv, options); chart.render(); chartDiv._apexChart = chart; }
                              catch(err) { console.error(err); chartDiv.innerHTML = '<div style="color:var(--m3-error, red); font-size:13px; text-align:center; padding-top:50px;">Graphen-Fehler</div>'; }
                          }
                          
                          if (typeof ApexCharts === 'undefined') {
                              chartDiv.innerHTML = '<div style="color:var(--m3-on-surface-variant); font-size:13px; text-align:center; padding-top:50px; font-weight:500;">Lade Chart-Engine...</div>';
                              if (!window.apexLoadingPromise) { 
                                  window.apexLoadingPromise = new Promise(function(resolve) { 
                                      var tempDefine = window.define; window.define = undefined; 
                                      var script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/apexcharts@5.6.0/dist/apexcharts.min.js'; 
                                      script.onload = function() { window.define = tempDefine; resolve(); }; 
                                      document.head.appendChild(script); 
                                  }); 
                              }
                              window.apexLoadingPromise.then(drawGraph);
                          } else { drawGraph(); }
                      };
                    </script>`;
                    
                        setState(DP_HEIZUNG_HTML, html, true);
                    }
                    
                    // === INIT ===
                    function startHeizungRenderer(): void {
                        on({ id: [DP_CHART_BRENNER, DP_CHART_VALVE, DP_CHART_ACT, DP_CHART_SET, DP_CHART_WINDOW, DP_CHART_HUMID], change: 'any' }, () => renderHeizung());
                        on({ id: DP_HEIZUNG_AN, change: 'any' }, () => renderHeizung());
                        on({ id: DP_MODULATION,  change: 'any' }, () => renderHeizung());
                        on({ id: DP_PRIVACY,     change: 'any' }, () => renderHeizung());
                    
                        rooms.forEach(room => {
                            if (room.actOid)   on({ id: room.actOid,   change: 'any' }, () => renderHeizung());
                            if (room.setOid)   on({ id: room.setOid,   change: 'any' }, () => renderHeizung());
                            if (room.valveOid) on({ id: room.valveOid, change: 'any' }, () => renderHeizung());
                            if (room.humidOid) on({ id: room.humidOid, change: 'any' }, () => renderHeizung());
                            if (room.windowOids) {
                                room.windowOids.forEach(oid => on({ id: oid, change: 'any' }, () => renderHeizung()));
                            }
                        });
                    
                        updateHistories();
                        schedule('*/15 * * * *', updateHistories);
                        log('[Heizung] Renderer v1.3 (History Edition & Vis Fix) gestartet.');
                    }
                    
                    const chartDPs: Array<{ id: string; val: boolean; name: string }> = [
                        { id: DP_CHART_BRENNER, val: true,  name: 'Chart: Brenner anzeigen' },
                        { id: DP_CHART_VALVE,   val: false, name: 'Chart: Ventil anzeigen'  },
                        { id: DP_CHART_ACT,     val: true,  name: 'Chart: Ist-Temp anzeigen' },
                        { id: DP_CHART_SET,     val: true,  name: 'Chart: Ziel-Temp anzeigen' },
                        { id: DP_CHART_WINDOW,  val: true,  name: 'Chart: Fenster anzeigen'  },
                        { id: DP_CHART_HUMID,   val: true,  name: 'Chart: Luftfeuchte anzeigen' },
                    ];
                    
                    function ensureChartDPs(index: number, done: () => void): void {
                        if (index >= chartDPs.length) { done(); return; }
                        const dp = chartDPs[index];
                        if (!existsState(dp.id)) {
                            createState(dp.id, dp.val, { type: 'boolean', name: dp.name, role: 'switch', read: true, write: true }, () => ensureChartDPs(index + 1, done));
                        } else {
                            ensureChartDPs(index + 1, done);
                        }
                    }
                    
                    ensureChartDPs(0, () => {
                        if (!existsState(DP_HEIZUNG_HTML)) {
                            createState(DP_HEIZUNG_HTML, '', { type: 'string', name: 'Heizung HTML', role: 'html' }, startHeizungRenderer);
                        } else {
                            startHeizungRenderer();
                        }
                    });
                    

                    Der ioBroker "web"-Adapter (der die Vis-Oberfläche an den Browser ausliefert) durchsucht das HTML nach Datenpunkten, die er abonnieren ("subscriben") muss, um sie live zu aktualisieren. Er sucht dabei nach Mustern wie {id: "datenpunkt"}.
                    Leider haben wir in unserem JavaScript (das direkt als String in den HTML-Code eingebettet ist) sehr viele geschweifte Klammern für die Konfiguration des ApexCharts-Graphen verwendet:

                    stroke: { curve: s_curve, width: s_width, dashArray: s_dash }
                    // oder
                    yaxis: { min: 0, max: 100, show: false }
                    

                    Der ioBroker-Parser ist an dieser Stelle etwas zu simpel gestrickt. Er sieht die geschweiften Klammern und denkt sofort: "Aha! Der User will hier einen ioBroker-Datenpunkt abonnieren!" Dann versucht er, "curve: s_curve..." als Datenpunkt in der Datenbank zu finden. Das schlägt natürlich fehl, wirft Fehler im Log und bringt bei manchen Usern sogar den Web-Adapter zum Absturz.

                    Wir müssen den Code innerhalb des <script>-Tags so umschreiben, dass der ioBroker-Parser ihn in Ruhe lässt. Das machen wir, indem wir die geschweiften Klammern für JSON-Objekte so formatieren, dass der ioBroker sie nicht mehr für Bindings hält (z.B. indem wir einen Zeilenumbruch nach der öffnenden Klammer erzwingen oder die Objekt-Erstellung etwas entzerren).

                    ioBroker auf NUC (Celeron mit Ubuntu-Server)

                    Homematic, HMIP, Hue, Unifi, Plex, Nest, Roborock, Google Assistant

                    1 Antwort Letzte Antwort
                    0
                    • OliverIOO OliverIO

                      @sigi234

                      als mit dem adapter unbeteiligter entdecke ich einen effekt, den ich auch bei jsontemplate habe wenn man nichts davon weiß

                      4b12811d-058e-48d5-90f3-8c9eeb8aa593-image.jpeg

                      alles html was durch ein widget in vis2 (auch vis1) eingebracht wird, wird durch vis nochmal auf binding notationen untersucht.
                      ich vermute mal, das an den stellen an denen ich undefined markiert habe klammerkonstrukte stehen, also
                      { .... }
                      das wird als binding erkannt und interpretiert. da da ungültiges drin steht ist das ergebnis undefined.

                      verhindern kann man das aktuell nur, das man die klammern {} jeweils in separate zeilen schreibt.
                      also aus

                      { ... }
                      

                      wird

                      {
                      ...
                      }
                      
                      M Offline
                      M Offline
                      mrMuppet
                      schrieb am zuletzt editiert von
                      #19

                      @OliverIO Stimmt!

                      // ============================================================
                      // renderHeizung — M3 Material You Styling (9 Räume Grid) - v1.4 (Binding Fix)
                      // ioBroker TypeScript — Inkl. Luftfeuchtigkeit, SYNC-FIX & MULTI-FENSTER
                      // CHANGELOG v1.4:
                      // - CRITICAL FIX: "Cannot subscribe..." / "undefined" Fehler in vis/vis-2 behoben!
                      //   Das ApexCharts-Konfigurationsobjekt wird nun als umgewandelter JSON-String 
                      //   zusammengebaut und erst zur Laufzeit im Browser per JSON.parse() in ein 
                      //   Objekt verwandelt. Dadurch findet der ioBroker-Parser keine { ... } 
                      //   Muster mehr im HTML-Code, die er fälschlicherweise als Bindings interpretieren könnte.
                      // ============================================================
                      
                      const DP_HEIZUNG_HTML: string = '0_userdata.0.dashboard.heizungHTML';
                      const DP_PRIVACY:      string = '0_userdata.0.dashboard.privacyMode';
                      
                      const DP_CHART_BRENNER: string = '0_userdata.0.dashboard.chartShowBrenner';
                      const DP_CHART_VALVE:   string = '0_userdata.0.dashboard.chartShowValve';
                      const DP_CHART_ACT:     string = '0_userdata.0.dashboard.chartShowAct';
                      const DP_CHART_SET:     string = '0_userdata.0.dashboard.chartShowSet';
                      const DP_CHART_WINDOW:  string = '0_userdata.0.dashboard.chartShowWindow';
                      const DP_CHART_HUMID:   string = '0_userdata.0.dashboard.chartShowHumid';
                      
                      const DP_HEIZUNG_AN: string = '0_userdata.0.Heizung.Heizung_an';
                      const DP_MODULATION: string = 'ems-esp.0.heatSources.actualModulation';
                      
                      // INSTANZ DES HISTORY-ADAPTERS
                      const HISTORY_INSTANCE: string = 'history.0';
                      
                      let globalHeatHistory: any[] = [];
                      let roomHistories: Record<string, { act: any[]; set: any[]; valve: any[]; heat: any[]; window: any[]; humid: any[] }> = {};
                      
                      const M3: Record<string, string> = {
                          primary:                 'var(--m3-primary, rgb(18, 66, 24))',
                          onPrimary:               'var(--m3-on-primary, rgb(255, 255, 255))',
                          primaryContainer:        'var(--m3-primary-container, rgb(43, 90, 45))',
                          onPrimaryContainer:      'var(--m3-on-primary-container, rgb(155, 208, 151))',
                          surfaceContainerLow:     'var(--m3-surface-container-low, rgb(243, 244, 237))',
                          surfaceContainerHighest: 'var(--m3-surface-container-highest, rgb(225, 227, 220))',
                          surfaceVariant:          'var(--m3-surface-variant, rgb(221, 229, 216))',
                          onSurface:               'var(--m3-on-surface, rgb(25, 28, 24))',
                          onSurfaceVariant:        'var(--m3-on-surface-variant, rgb(66, 73, 63))',
                          heatPrimary:             'var(--m3-error, rgb(186, 26, 26))',
                          heatContainer:           'var(--m3-error-container, rgb(255, 218, 214))',
                          onHeatContainer:         'var(--m3-on-error-container, rgb(65, 0, 2))',
                          pendingPrimary:          'rgb(176, 96, 0)',
                          pendingContainer:        'rgb(255, 220, 193)',
                          onPendingContainer:      'rgb(43, 20, 0)',
                      };
                      
                      const ICONS: Record<string, string> = {
                          thermostat: 'M15 13V5A3 3 0 0 0 9 5V13A5 5 0 1 0 15 13M12 4A1 1 0 0 1 13 5V8H11V5A1 1 0 0 1 12 4Z',
                          fire:       'M17.5 11C15 7.5 13.5 8.5 12 6C10.5 8.5 9 7.5 6.5 11C4 14.5 5.5 19 12 19C18.5 19 20 14.5 17.5 11M12 17C9 17 8.5 14 10 12.5C11.5 11 12 12.5 12 12.5C12 12.5 12.5 11 14 12.5C15.5 14 15 17 12 17Z',
                          minus:      'M19 13H5V11H19V13Z',
                          plus:       'M19 13H13V19H11V13H5V11H11V5H13V11H19V13Z',
                          power:      'M16.56 5.44L15.11 6.89C16.84 7.94 18 9.83 18 12A6 6 0 0 1 12 18A6 6 0 0 1 6 12C6 9.83 7.16 7.94 8.88 6.88L7.44 5.44C5.36 6.88 4 9.28 4 12A8 8 0 0 0 12 20A8 8 0 0 0 20 12C20 9.28 18.64 6.88 16.56 5.44M11 3H13V13H11V3Z',
                          window:     'M3 3H21V21H3V3M5 5V11H11V5H5M13 5V11H19V5H13M5 13V19H11V13H5M13 13V19H19V13H13Z',
                          water:      'M12 20a6 6 0 0 1-6-6c0-4 6-10.75 6-10.75S18 10 18 14a6 6 0 0 1-6 6z'
                      };
                      
                      interface RoomHeater {
                          id:          string;
                          label:       string;
                          anonLabel?:  string;
                          actOid:      string;
                          setOid:      string;
                          valveOid:    string;
                          windowOids?: string[];
                          humidOid?:   string;
                      }
                      
                      const rooms: RoomHeater[] = [
                          { id: 'wohnzimmer',  label: 'Wohnzimmer',    actOid: 'alias.0.Heizung.Wohnzimmer.ACTUAL',   setOid: 'alias.0.Heizung.Wohnzimmer.SET',   valveOid: 'alias.0.Heizung.Wohnzimmer.VALVE',   windowOids: ['alias.0.Sicherheit.Wohnzimmer.Fenster.STATE', 'alias.0.Sicherheit.Wohnzimmer.Tuer.STATE'], humidOid: 'alias.0.Klima.Wohnzimmer.HUMIDITY' },
                          { id: 'schlafzimmer',label: 'Zimmer Mina',    anonLabel: 'Schlafzimmer',  actOid: 'alias.0.Heizung.Schlafzimmer.ACTUAL', setOid: 'alias.0.Heizung.Schlafzimmer.SET', valveOid: 'alias.0.Heizung.Schlafzimmer.VALVE', windowOids: ['alias.0.Sicherheit.Schlafzimmer.Fenster.STATE'], humidOid: 'alias.0.Klima.Schlafzimmer.HUMIDITY' },
                          { id: 'kind1',       label: 'Zimmer Nova',    anonLabel: 'Kinderzimmer 1', actOid: 'alias.0.Heizung.Kind1.ACTUAL',        setOid: 'alias.0.Heizung.Kind1.SET',        valveOid: 'alias.0.Heizung.Kind1.VALVE',        windowOids: ['alias.0.Sicherheit.Kinderzimmer1.Fenster.STATE'], humidOid: 'alias.0.Klima.Kind1.HUMIDITY' },
                          { id: 'kind2',       label: 'Zimmer Vasco',   anonLabel: 'Kinderzimmer 2', actOid: 'alias.0.Heizung.Kind2.ACTUAL',        setOid: 'alias.0.Heizung.Kind2.SET',        valveOid: 'alias.0.Heizung.Kind2.VALVE',        windowOids: ['alias.0.Sicherheit.Kinderzimmer2.Fenster.STATE'], humidOid: 'alias.0.Klima.Kind2.HUMIDITY' },
                          { id: 'bad',         label: 'Bad',            actOid: 'alias.0.Heizung.Bad.ACTUAL',          setOid: 'alias.0.Heizung.Bad.SET',          valveOid: 'alias.0.Heizung.Bad.VALVE',          windowOids: ['alias.0.Sicherheit.Bad.Fenster.STATE'], humidOid: 'alias.0.Klima.Bad.HUMIDITY' },
                          { id: 'dachzimmer',  label: 'Dachzimmer',     actOid: 'alias.0.Heizung.Dachzimmer.ACTUAL',   setOid: 'alias.0.Heizung.Dachzimmer.SET',   valveOid: 'alias.0.Heizung.Dachzimmer.VALVE',   windowOids: ['alias.0.Sicherheit.Dachzimmer.Fenster_Garten.STATE', 'alias.0.Sicherheit.Dachzimmer.Fenster_Strasse.STATE'], humidOid: 'alias.0.Klima.Dachzimmer.HUMIDITY' },
                          { id: 'flur',        label: 'Flur',            actOid: 'alias.0.Heizung.Flur.ACTUAL',         setOid: 'alias.0.Heizung.Flur.SET',         valveOid: 'alias.0.Heizung.Flur.VALVE',         windowOids: ['alias.0.Sicherheit.Haustuer.STATE'], humidOid: 'alias.0.Klima.Flur.HUMIDITY' },
                          { id: 'gaesteklo',   label: 'Gäste WC',       actOid: 'alias.0.Heizung.GaesteKlo.ACTUAL',    setOid: 'alias.0.Heizung.GaesteKlo.SET',    valveOid: 'alias.0.Heizung.GaesteKlo.VALVE' },
                          { id: 'hwr',         label: 'HWR',             actOid: 'alias.0.Heizung.HWR.ACTUAL',          setOid: 'alias.0.Heizung.HWR.SET',          valveOid: 'alias.0.Heizung.HWR.VALVE',          windowOids: ['alias.0.Sicherheit.HWR.Fenster.STATE'] },
                      ];
                      
                      const COL_WIDTH:  number = 202;
                      const HERO_WIDTH: number = 636;
                      const CARD_HEIGHT:number = 144;
                      const GUTTER:     number = 15;
                      const SHADOW:     string = '0px 1px 2px 0px rgba(0,0,0,0.3), 0px 1px 3px 1px rgba(0,0,0,0.15)';
                      
                      function getSafeVal(oid: string | undefined, fallback: any = 0): any {
                          if (oid && existsState(oid)) {
                              const s = getState(oid);
                              return s ? s.val : fallback;
                          }
                          return fallback;
                      }
                      
                      function svgIcon(name: string, color: string, size: number = 24): string {
                          const path = ICONS[name] || ICONS['thermostat'];
                          return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" style="flex-shrink:0; transition: fill 0.3s ease;"><path fill="${color}" d="${path}"/></svg>`;
                      }
                      
                      async function fetchHistoryData(oid: string | undefined, start: number, end: number, isRaw: boolean = false): Promise<any[]> {
                          if (!oid || !existsState(oid)) return [];
                          try {
                              const queryOptions = isRaw
                                  ? { start, end, aggregate: 'none', limit: 400, ignoreNull: true }
                                  : { start, end, step: 1800000, aggregate: 'average', ignoreNull: true };
                      
                              const result = await new Promise<any>(resolve => {
                                  sendTo(HISTORY_INSTANCE, 'getHistory', { id: oid, options: queryOptions }, res => resolve(res));
                              });
                      
                              if (result && result.result && result.result.length > 0) {
                                  let cleanData = result.result
                                      .filter((r: any) => r.val !== null)
                                      .map((r: any) => {
                                          let v = r.val;
                                          if (typeof v === 'boolean')      v = v ? 100 : 0;
                                          else if (typeof v === 'string')  v = Number(v) || 0;
                                          return [r.ts, Math.round(v * 10) / 10];
                                      })
                                      .filter((r: any) => !isNaN(r[1]));
                      
                                  if (isRaw && cleanData.length > 0) {
                                      const maxVal = Math.max(...cleanData.map((d: any) => d[1]));
                                      if (maxVal <= 1.0 && maxVal > 0) cleanData.forEach((d: any) => (d[1] = d[1] * 100));
                                  }
                                  return cleanData;
                              }
                          } catch (e) {}
                          return [];
                      }
                      
                      async function updateHistories(): Promise<void> {
                          const end   = new Date().getTime();
                          const start = end - 24 * 60 * 60 * 1000;
                      
                          globalHeatHistory = await fetchHistoryData(DP_MODULATION, start, end, true);
                      
                          for (const room of rooms) {
                              const [actData, setData, valveData, humidData] = await Promise.all([
                                  fetchHistoryData(room.actOid,   start, end, false),
                                  fetchHistoryData(room.setOid,   start, end, false),
                                  fetchHistoryData(room.valveOid, start, end, true),
                                  fetchHistoryData(room.humidOid, start, end, false),
                              ]);
                      
                              let allWindowData: any[] = [];
                              if (room.windowOids && room.windowOids.length > 0) {
                                  const wResults = await Promise.all(room.windowOids.map(wOid => fetchHistoryData(wOid, start, end, true)));
                                  allWindowData = wResults.filter(wData => wData.length > 0);
                              }
                      
                              roomHistories[room.id] = { act: actData, set: setData, valve: valveData, heat: globalHeatHistory, window: allWindowData, humid: humidData };
                          }
                          renderHeizung();
                      }
                      
                      function renderHeroCard(activeRoomsCount: number, isZentralAn: boolean): string {
                          const mod    = Number(getSafeVal(DP_MODULATION, 0)) || 0;
                          const bg     = isZentralAn ? M3.primaryContainer : M3.surfaceContainerLow;
                          const fg     = isZentralAn ? M3.onPrimaryContainer : M3.onSurfaceVariant;
                          const iconBg = isZentralAn ? M3.primary : M3.surfaceContainerHighest;
                          const iconFg = isZentralAn ? M3.onPrimary : M3.onSurfaceVariant;
                          return `<div style="width:${HERO_WIDTH}px; height:${CARD_HEIGHT}px; border-radius:12px; padding:20px 24px; box-sizing:border-box; background:${bg}; box-shadow:${SHADOW}; display:flex; align-items:center; justify-content:space-between; font-family:Roboto,sans-serif; transition:all 0.3s ease;"> <div style="display:flex; align-items:center; gap:20px;"> <div style="width:56px; height:56px; border-radius:28px; background:${iconBg}; display:flex; align-items:center; justify-content:center; transition:all 0.3s ease;"> ${svgIcon(isZentralAn ? 'fire' : 'power', iconFg, 32)} </div> <div> <div style="font-size:24px; font-weight:500; color:${fg}; margin-bottom:4px;">Zentralheizung</div> <div style="font-size:15px; font-weight:500; color:${fg}; opacity:0.8;">${isZentralAn ? `Heizbetrieb aktiv • ${mod}% Leistung` : 'Standby / Aus'}</div> </div> </div> <div style="text-align:right;"> <div style="font-size:36px; font-weight:600; color:${isZentralAn ? M3.heatPrimary : fg}; line-height:1;">${activeRoomsCount}</div> <div style="font-size:13px; font-weight:500; color:${fg}; text-transform:uppercase; letter-spacing:0.5px; margin-top:4px;">Räume fordern Wärme</div> </div> </div>`;
                      }
                      
                      function renderRoomCard(room: RoomHeater, isZentralAn: boolean, isPrivacyMode: boolean): string {
                          const actVal     = Number(getSafeVal(room.actOid, 0)) || 0;
                          const setVal     = Number(getSafeVal(room.setOid, 0)) || 0;
                          const valveRaw   = getSafeVal(room.valveOid, 0);
                          const humidVal   = room.humidOid ? Number(getSafeVal(room.humidOid, 0)) || 0 : null;
                          const displayName = isPrivacyMode && room.anonLabel ? room.anonLabel : room.label;
                      
                          let windowOpen = false;
                          if (room.windowOids && room.windowOids.length > 0) {
                              windowOpen = room.windowOids.some(oid => {
                                  const raw = getSafeVal(oid, false);
                                  return raw === true || raw === 1 || raw === '1' || raw === 'true';
                              });
                          }
                      
                          const wantsHeat = (typeof valveRaw === 'number' && valveRaw > 0)
                              || (typeof valveRaw === 'string' && Number(valveRaw) > 0)
                              || valveRaw === true || valveRaw === 'true';
                      
                          let bg: string, fg: string, fgSub: string, iconBg: string, iconFg: string;
                          if (wantsHeat && isZentralAn) {
                              bg = M3.heatContainer; fg = M3.onHeatContainer; fgSub = M3.onHeatContainer; iconBg = M3.heatPrimary; iconFg = '#ffffff';
                          } else if (wantsHeat && !isZentralAn) {
                              bg = M3.pendingContainer; fg = M3.onPendingContainer; fgSub = M3.onPendingContainer; iconBg = M3.pendingPrimary; iconFg = '#ffffff';
                          } else {
                              bg = M3.surfaceContainerLow; fg = M3.onSurface; fgSub = M3.onSurfaceVariant; iconBg = M3.surfaceContainerHighest; iconFg = M3.onSurfaceVariant;
                          }
                      
                          const jsMinus = `(function(e, btn){ e.stopPropagation(); var card = btn.closest('.m3-room-card'); var span = card.querySelector('.target-temp'); var current = parseFloat(span.innerText); var newVal = current - 0.5; span.innerText = newVal.toFixed(1) + '°'; if(typeof vis!=='undefined'&&vis.conn&&vis.conn.setState) vis.conn.setState('${room.setOid}', newVal); else if(typeof vis!=='undefined'&&vis.setValue) vis.setValue('${room.setOid}', newVal); })(event, this);`;
                          const jsPlus  = `(function(e, btn){ e.stopPropagation(); var card = btn.closest('.m3-room-card'); var span = card.querySelector('.target-temp'); var current = parseFloat(span.innerText); var newVal = current + 0.5; span.innerText = newVal.toFixed(1) + '°'; if(typeof vis!=='undefined'&&vis.conn&&vis.conn.setState) vis.conn.setState('${room.setOid}', newVal); else if(typeof vis!=='undefined'&&vis.setValue) vis.setValue('${room.setOid}', newVal); })(event, this);`;
                      
                          const windowHtml = windowOpen
                              ? `<div style="width:24px; height:24px; display:flex; align-items:center; justify-content:center; animation: blink 2s infinite;">${svgIcon('window', M3.heatPrimary, 20)}</div>`
                              : '';
                      
                          const humidHtml = (humidVal !== null && humidVal > 0)
                              ? `<div style="position:absolute; top:36px; right:-4px; width:40px; display:flex; justify-content:center; align-items:center; font-size:11px; font-weight:600; color:${fgSub}; opacity:0.85; letter-spacing:-0.5px;">${Math.round(humidVal)}%<svg width="10" height="10" viewBox="0 0 24 24" style="margin-left:1px"><path fill="currentColor" d="M12 20a6 6 0 0 1-6-6c0-4 6-10.75 6-10.75S18 10 18 14a6 6 0 0 1-6 6z"/></svg></div>`
                              : '';
                      
                          const historyData = roomHistories[room.id] ?? { act: [], set: [], valve: [], heat: [], window: [], humid: [] };
                      
                          return `<div id="wrapper_${room.id}" style="width:${COL_WIDTH}px; height:${CARD_HEIGHT}px; position:relative; z-index:1;">
                              <div id="card_${room.id}" class="m3-room-card" data-history='${JSON.stringify(historyData)}' onclick="if(typeof window.toggleCard === 'function') window.toggleCard(this, '${room.id}');" style="position:absolute; top:0; left:0; width:100%; height:100%; border-radius:12px; padding:14px 16px; box-sizing:border-box; background:${bg}; box-shadow:${SHADOW}; display:flex; flex-direction:column; font-family:Roboto,sans-serif; transition:all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); cursor:pointer; overflow:hidden;">
                                  <div style="display:flex; justify-content:space-between; align-items:flex-start; width:100%;">
                                      <div style="font-size:16px; font-weight:500; color:${fg}; max-width:145px; line-height:1.2; overflow:hidden; text-overflow:ellipsis; transition:color 0.3s ease;">${displayName}</div>
                                      <div style="display:flex; gap:6px; align-items:flex-start; position:relative;">
                                          ${windowHtml}
                                          <div style="width:32px; height:32px; border-radius:16px; background:${iconBg}; display:flex; align-items:center; justify-content:center; flex-shrink:0;">${svgIcon('thermostat', iconFg, 18)}</div>
                                          ${humidHtml}
                                      </div>
                                  </div>
                                  <div style="display:flex; align-items:baseline; margin-top:-6px; width:100%;">
                                      <div style="font-size:36px; font-weight:400; color:${fg}; letter-spacing:-1px;">${actVal.toFixed(1)}</div>
                                      <div style="font-size:18px; font-weight:500; color:${fgSub}; margin-left:2px; opacity:0.7;">°C</div>
                                  </div>
                                  <div id="controls_${room.id}" style="display:flex; justify-content:space-between; align-items:center; width:100%; margin-top:auto;">
                                      <div style="display:flex; flex-direction:column;">
                                          <span style="font-size:11px; font-weight:500; text-transform:uppercase; letter-spacing:0.5px; color:${fgSub}; opacity:0.7;">Ziel</span>
                                          <span class="target-temp" style="font-size:15px; font-weight:600; color:${fg};">${setVal.toFixed(1)}°</span>
                                      </div>
                                      <div style="display:flex; gap:6px;">
                                          <div onclick="${jsMinus}" style="width:32px; height:32px; border-radius:16px; background:rgba(0,0,0,0.06); display:flex; align-items:center; justify-content:center; cursor:pointer; user-select:none; transition:opacity 0.1s; -webkit-tap-highlight-color:transparent;">${svgIcon('minus', fg, 20)}</div>
                                          <div onclick="${jsPlus}"  style="width:32px; height:32px; border-radius:16px; background:rgba(0,0,0,0.06); display:flex; align-items:center; justify-content:center; cursor:pointer; user-select:none; transition:opacity 0.1s; -webkit-tap-highlight-color:transparent;">${svgIcon('plus', fg, 20)}</div>
                                      </div>
                                  </div>
                                  <div id="chart_wrapper_${room.id}" style="opacity:0; height:0px; width:100%; transition:opacity 0.4s ease; margin-top:0px; pointer-events:none;">
                                      <div id="chart_${room.id}" style="width:100%; height:100%;"></div>
                                  </div>
                              </div>
                          </div>`;
                      }
                      
                      function renderHeizung(): void {
                          let activeRooms = 0;
                          const isZentralAn   = getSafeVal(DP_HEIZUNG_AN, false) === true;
                          const isPrivacyMode = getSafeVal(DP_PRIVACY,     false) === true;
                      
                          const showBrenner = getSafeVal(DP_CHART_BRENNER, true)  === true;
                          const showValve   = getSafeVal(DP_CHART_VALVE,   false) === true;
                          const showAct     = getSafeVal(DP_CHART_ACT,     true)  === true;
                          const showSet     = getSafeVal(DP_CHART_SET,     true)  === true;
                          const showWindow  = getSafeVal(DP_CHART_WINDOW,  true)  === true;
                          const showHumid   = getSafeVal(DP_CHART_HUMID,   true)  === true;
                      
                          for (const room of rooms) {
                              const valveRaw = getSafeVal(room.valveOid, 0);
                              if ((typeof valveRaw === 'number' && valveRaw > 0)
                                  || (typeof valveRaw === 'string' && Number(valveRaw) > 0)
                                  || valveRaw === true || valveRaw === 'true') activeRooms++;
                          }
                      
                          let cardsHtml = renderHeroCard(activeRooms, isZentralAn);
                          for (const room of rooms) cardsHtml += renderRoomCard(room, isZentralAn, isPrivacyMode);
                      
                          const html = `
                      <style>
                        @keyframes blink { 0% { opacity: 1; } 50% { opacity: 0.3; } 100% { opacity: 1; } }
                        .m3-room-card * { white-space: nowrap; }
                        .apexcharts-legend { padding: 0 !important; margin-top: -10px !important; }
                        .apexcharts-legend-text { color: var(--m3-on-surface-variant) !important; font-family: Roboto, sans-serif !important; font-size: 11px !important; }
                      </style>
                      
                      <div id="m3-dashboard-container"
                           data-cfg-brenner="${showBrenner}"
                           data-cfg-valve="${showValve}"
                           data-cfg-act="${showAct}"
                           data-cfg-set="${showSet}"
                           data-cfg-window="${showWindow}"
                           data-cfg-humid="${showHumid}"
                           style="position: relative; background: var(--m3-surface-container-low, rgb(237, 239, 232)); border-radius: 0 0 28px 28px; padding: 24px; display: flex; flex-wrap: wrap; align-content: flex-start; gap: ${GUTTER}px; width: 901px; box-sizing: border-box; transition: background-color 0.3s ease;">
                        ${cardsHtml}
                      </div>
                      
                      <script>
                        window.activeM3CardId = window.activeM3CardId || null;
                        window.toggleCard = function(el, roomId) {
                            if (window.activeM3CardId === roomId) { window.closeCard(el, roomId); return; }
                            if (window.activeM3CardId) { const oldEl = document.getElementById('card_' + window.activeM3CardId); if (oldEl) window.closeCard(oldEl, window.activeM3CardId); }
                            window.openCard(el, roomId);
                        };
                        window.closeCard = function(el, roomId) {
                            el.style.width = '100%'; el.style.height = '100%'; el.style.boxShadow = '${SHADOW}';
                            const chartWrapper = document.getElementById('chart_wrapper_' + roomId);
                            if (chartWrapper) {
                                chartWrapper.style.opacity = '0';
                                setTimeout(() => {
                                    chartWrapper.style.height = '0px';
                                    const chartDiv = document.getElementById('chart_' + roomId);
                                    if (chartDiv) {
                                        if (chartDiv._apexChart) { chartDiv._apexChart.destroy(); delete chartDiv._apexChart; }
                                        chartDiv.innerHTML = '';
                                    }
                                }, 400);
                            }
                            setTimeout(() => { el.parentElement.style.zIndex = '1'; }, 400); window.activeM3CardId = null;
                        };
                        window.openCard = function(el, roomId) {
                            window.activeM3CardId = roomId; const wrapper = el.parentElement; const container = document.getElementById('m3-dashboard-container');
                            wrapper.style.zIndex = '999'; const wrapperRect = wrapper.getBoundingClientRect(); const containerRect = container.getBoundingClientRect();
                            const expandedWidth = ${COL_WIDTH * 2 + GUTTER}; const expandedHeight = 310;
                            el.style.top = 'auto'; el.style.bottom = 'auto'; el.style.left = 'auto'; el.style.right = 'auto';
                            if (wrapperRect.left + expandedWidth > containerRect.right - 10) { el.style.right = '0'; } else { el.style.left = '0'; }
                            if (wrapperRect.top + expandedHeight > containerRect.bottom - 10) { el.style.bottom = '0'; } else { el.style.top = '0'; }
                            el.style.width = expandedWidth + 'px'; el.style.height = expandedHeight + 'px'; el.style.boxShadow = '0px 14px 28px rgba(0,0,0,0.35), 0px 10px 10px rgba(0,0,0,0.22)';
                            const chartWrapper = document.getElementById('chart_wrapper_' + roomId);
                            if (chartWrapper) { chartWrapper.style.height = '140px'; chartWrapper.style.marginTop = '16px'; setTimeout(() => { chartWrapper.style.opacity = '1'; window.renderChart(el, roomId); }, 450); }
                        };
                        window.renderChart = function(el, roomId) {
                            const chartDiv = el.querySelector('#chart_' + roomId);
                            if (!chartDiv || chartDiv.innerHTML.trim() !== '') return;
                            const rawData = el.getAttribute('data-history'); let chartData = { act: [], set: [], valve: [], heat: [], window: [], humid: [] };
                            try { chartData = JSON.parse(rawData); } catch(e) {}
                            if (chartData.act.length === 0 && chartData.set.length === 0) { chartDiv.innerHTML = '<div style="color:var(--m3-on-surface-variant); font-size:13px; text-align:center; padding-top:50px; font-weight:500;">Keine Historie</div>'; return; }
                            
                            const rootStyles = getComputedStyle(document.documentElement);
                            const colorIst     = rootStyles.getPropertyValue('--m3-primary').trim()  || '#4caf50';
                            const colorSoll    = rootStyles.getPropertyValue('--m3-outline').trim()   || '#9e9e9e';
                            const colorVentil  = rootStyles.getPropertyValue('--m3-error').trim()     || '#ffb4ab';
                            const colorBrenner = '#ff9800';
                            const colorFenster = '#00bfff';
                            const colorHumid   = '#2196f3';
                            
                            const container    = el.closest('#m3-dashboard-container');
                            const cfgShowBrenner = container.getAttribute('data-cfg-brenner') === 'true';
                            const cfgShowValve   = container.getAttribute('data-cfg-valve')   === 'true';
                            const cfgShowAct     = container.getAttribute('data-cfg-act')     === 'true';
                            const cfgShowSet     = container.getAttribute('data-cfg-set')     === 'true';
                            const cfgShowWindow  = container.getAttribute('data-cfg-window')  === 'true';
                            const cfgShowHumid   = container.getAttribute('data-cfg-humid')   === 'true';
                            
                            function drawGraph() {
                                chartDiv.innerHTML = '';
                                var s_series = []; var s_colors = [];
                                var actHasData = chartData.act && chartData.act.length > 0;
                                var setHasData = chartData.set && chartData.set.length > 0;
                                var minTemp = 999; var maxTemp = -999;
                                if (cfgShowAct && actHasData) { for (var i = 0; i < chartData.act.length; i++) { if (chartData.act[i][1] < minTemp) minTemp = chartData.act[i][1]; if (chartData.act[i][1] > maxTemp) maxTemp = chartData.act[i][1]; } }
                                if (cfgShowSet && setHasData) { for (var j = 0; j < chartData.set.length; j++) { if (chartData.set[j][1] < minTemp) minTemp = chartData.set[j][1]; if (chartData.set[j][1] > maxTemp) maxTemp = chartData.set[j][1]; } }
                                if (minTemp === 999) { minTemp = 15; maxTemp = 25; } else { minTemp = Math.floor(minTemp) - 1; maxTemp = Math.ceil(maxTemp) + 1; }
                                
                                if (cfgShowHumid && chartData.humid && chartData.humid.length > 0) { s_series.push({ name: 'Luftfeuchte', type: 'area', data: chartData.humid }); s_colors.push(colorHumid); }
                                if (cfgShowBrenner) { s_series.push({ name: 'Brenner', type: 'area', data: chartData.heat || [] }); s_colors.push(colorBrenner); }
                                if (cfgShowValve)   { s_series.push({ name: 'Ventil',  type: 'line', data: chartData.valve || [] }); s_colors.push(colorVentil); }
                                if (cfgShowAct)     { s_series.push({ name: 'Ist-Temp',  type: 'area', data: chartData.act || [] }); s_colors.push(colorIst); }
                                if (cfgShowSet)     { s_series.push({ name: 'Ziel-Temp', type: 'line', data: chartData.set || [] }); s_colors.push(colorSoll); }
                                
                                var windowAnnotations = [];
                                if (cfgShowWindow && chartData.window && chartData.window.length > 0) {
                                    for (var w = 0; w < chartData.window.length; w++) {
                                        var wData = chartData.window[w]; var inWindowOpen = false; var winStart = null;
                                        for (var k = 0; k < wData.length; k++) { var ts = wData[k][0]; var val = wData[k][1]; if (val > 0 && !inWindowOpen) { inWindowOpen = true; winStart = ts; } else if (val === 0 && inWindowOpen) { inWindowOpen = false; windowAnnotations.push({ x: winStart, x2: ts, fillColor: colorFenster, opacity: 0.15 }); } }
                                        if (inWindowOpen && winStart) windowAnnotations.push({ x: winStart, x2: new Date().getTime(), fillColor: colorFenster, opacity: 0.15 });
                                    }
                                }
                                if (cfgShowWindow) { let safeTime = (actHasData) ? chartData.act[0][0] : new Date().getTime() - 3600000; s_series.push({ name: 'Fenster offen', type: 'area', data: [[safeTime, 0]] }); s_colors.push(colorFenster); }
                                if (s_series.length === 0) { chartDiv.innerHTML = '<div style="color:var(--m3-on-surface-variant); font-size:13px; text-align:center; padding-top:50px; font-weight:500;">Alle Graphen ausgeblendet</div>'; return; }
                                
                                // CRITICAL FIX v1.4: Komplettes Apex-Config-Objekt als JSON-String generieren und parsen
                                var configString = '{"chart":{"type":"line","height":140,"width":"100%","parentHeightOffset":0,"toolbar":{"show":false},"animations":{"enabled":true,"easing":"easeinout","speed":800,"dynamicAnimation":{"enabled":false}}},"stroke":{"curve":["smooth","smooth","stepline","smooth","stepline","stepline"],"width":[0,2,2,3,2,0],"dashArray":[0,0,0,0,4,0]},"fill":{"type":["solid","gradient","solid","gradient","solid","solid"],"opacity":[0.1,1,1,1,1,0],"gradient":{"shadeIntensity":1,"opacityFrom":0.45,"opacityTo":0.0,"stops":[0,100]}},"dataLabels":{"enabled":false},"legend":{"show":true,"position":"top","horizontalAlign":"right"},"tooltip":{"theme":"dark","x":{"format":"HH:mm"}},"xaxis":{"type":"datetime","labels":{"show":false},"axisBorder":{"show":false},"axisTicks":{"show":false},"tooltip":{"enabled":false}},"grid":{"show":false}}';
                                var options = JSON.parse(configString);
                                
                                options.series = s_series;
                                options.colors = s_colors;
                                options.annotations = { xaxis: windowAnnotations };
                                options.yaxis = [];
                                if (cfgShowHumid) options.yaxis.push({ min: 0, max: 100, show: false });
                                if (cfgShowBrenner) options.yaxis.push({ min: 0, max: 100, show: false });
                                if (cfgShowValve) options.yaxis.push({ min: 0, max: 100, show: false });
                                if (cfgShowAct) options.yaxis.push({ min: minTemp, max: maxTemp, show: false, tickAmount: 5 });
                                if (cfgShowSet) options.yaxis.push({ min: minTemp, max: maxTemp, show: false, tickAmount: 5 });
                                if (cfgShowWindow) options.yaxis.push({ min: minTemp, max: maxTemp, show: false, tickAmount: 5 });
                      
                                if (chartDiv._apexChart) { chartDiv._apexChart.destroy(); delete chartDiv._apexChart; }
                                try { var chart = new ApexCharts(chartDiv, options); chart.render(); chartDiv._apexChart = chart; }
                                catch(err) { console.error(err); chartDiv.innerHTML = '<div style="color:var(--m3-error, red); font-size:13px; text-align:center; padding-top:50px;">Graphen-Fehler</div>'; }
                            }
                            if (typeof ApexCharts === 'undefined') {
                                chartDiv.innerHTML = '<div style="color:var(--m3-on-surface-variant); font-size:13px; text-align:center; padding-top:50px; font-weight:500;">Lade Chart-Engine...</div>';
                                if (!window.apexLoadingPromise) { window.apexLoadingPromise = new Promise((resolve) => { const tempDefine = window.define; window.define = undefined; let script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/apexcharts@5.6.0/dist/apexcharts.min.js'; script.onload = () => { window.define = tempDefine; resolve(); }; document.head.appendChild(script); }); }
                                window.apexLoadingPromise.then(drawGraph);
                            } else { drawGraph(); }
                        };
                      </script>`;
                      
                          setState(DP_HEIZUNG_HTML, html, true);
                      }
                      
                      // === INIT ===
                      function startHeizungRenderer(): void {
                          on({ id: [DP_CHART_BRENNER, DP_CHART_VALVE, DP_CHART_ACT, DP_CHART_SET, DP_CHART_WINDOW, DP_CHART_HUMID], change: 'any' }, () => renderHeizung());
                          on({ id: DP_HEIZUNG_AN, change: 'any' }, () => renderHeizung());
                          on({ id: DP_MODULATION,  change: 'any' }, () => renderHeizung());
                          on({ id: DP_PRIVACY,     change: 'any' }, () => renderHeizung());
                      
                          rooms.forEach(room => {
                              if (room.actOid)   on({ id: room.actOid,   change: 'any' }, () => renderHeizung());
                              if (room.setOid)   on({ id: room.setOid,   change: 'any' }, () => renderHeizung());
                              if (room.valveOid) on({ id: room.valveOid, change: 'any' }, () => renderHeizung());
                              if (room.humidOid) on({ id: room.humidOid, change: 'any' }, () => renderHeizung());
                              if (room.windowOids) {
                                  room.windowOids.forEach(oid => on({ id: oid, change: 'any' }, () => renderHeizung()));
                              }
                          });
                      
                          updateHistories();
                          schedule('*/15 * * * *', updateHistories);
                          log('[Heizung] Renderer v1.4 (History Edition & Binding Fix) gestartet.');
                      }
                      
                      const chartDPs: Array<{ id: string; val: boolean; name: string }> = [
                          { id: DP_CHART_BRENNER, val: true,  name: 'Chart: Brenner anzeigen' },
                          { id: DP_CHART_VALVE,   val: false, name: 'Chart: Ventil anzeigen'  },
                          { id: DP_CHART_ACT,     val: true,  name: 'Chart: Ist-Temp anzeigen' },
                          { id: DP_CHART_SET,     val: true,  name: 'Chart: Ziel-Temp anzeigen' },
                          { id: DP_CHART_WINDOW,  val: true,  name: 'Chart: Fenster anzeigen'  },
                          { id: DP_CHART_HUMID,   val: true,  name: 'Chart: Luftfeuchte anzeigen' },
                      ];
                      
                      function ensureChartDPs(index: number, done: () => void): void {
                          if (index >= chartDPs.length) { done(); return; }
                          const dp = chartDPs[index];
                          if (!existsState(dp.id)) {
                              createState(dp.id, dp.val, { type: 'boolean', name: dp.name, role: 'switch', read: true, write: true }, () => ensureChartDPs(index + 1, done));
                          } else {
                              ensureChartDPs(index + 1, done);
                          }
                      }
                      
                      ensureChartDPs(0, () => {
                          if (!existsState(DP_HEIZUNG_HTML)) {
                              createState(DP_HEIZUNG_HTML, '', { type: 'string', name: 'Heizung HTML', role: 'html' }, startHeizungRenderer);
                          } else {
                              startHeizungRenderer();
                          }
                      });
                      

                      ioBroker auf NUC (Celeron mit Ubuntu-Server)

                      Homematic, HMIP, Hue, Unifi, Plex, Nest, Roborock, Google Assistant

                      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

                      333

                      Online

                      33.0k

                      Benutzer

                      83.4k

                      Themen

                      1.3m

                      Beiträge
                      Community
                      Impressum | Datenschutz-Bestimmungen | Nutzungsbedingungen | Einwilligungseinstellungen
                      ioBroker Community 2014-2026
                      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