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

Pro

Privat

Erfahrene

Beiträge


  • Google Material 3 / Material You - Vis2
    sigi234S sigi234

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

    Visualisierung vis material css

  • Google Material 3 / Material You - Vis2
    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();
        }
    });
    
    Visualisierung vis material css

  • Zigbee Adapter mit LAN Dongle kein automatisches verbinden
    Thomas BraunT Thomas Braun

    @BigMike71 sagte:

    dahin viel zu weit für Kabel

    Für stabiles WLAN aber wohl auch.

    Error/Bug

  • Zigbee Adapter mit LAN Dongle kein automatisches verbinden
    B BigMike71

    @Thomas-Braun
    schön wäre es, Gartenhaus in der Ecke auf Grundstück, dahin viel zu weit für Kabel einbuddeln...

    Error/Bug

  • Bambulab 3d-Drucker adapter
    padrinoP padrino

    Bei mir sieht der String übrigens so aus (also, wenn er nicht gerade wieder leer ist ;))

    2026-06-08T10:34:14.188Z

    Tester

  • Neuer Adapter pi-hole2 für pihole>=V6
    OliverIOO OliverIO

    @patricknitsch

    Und Kausalität ist nicht immer so einfach.
    Vergleiche mal Datum der Nachrichten

    Tester

  • VIS-2 Filter Bar keine Funktion
    Q Qlink

    @simonf04

    Das war der entscheidende Hinweis!
    Ich war noch auf 0.1.3.32.
    Nach dem Adapter Update klappts nun auch mit echarts 😊
    8791079d-b2a7-4868-b811-a48c8506226d-image.jpeg
    Eine kosmetische Frage noch:

    Weißt du ob es mit echarts möglich ist die Werte der jeweiligen Stunde direkt auf der Kurve anzuzeigen ? (so wie in meinem obigen Screenshot beim json chart widget)
    Ich konnte dazu keine passende Einstellung finden.

    Beste Grüße

    Visualisierung

  • MOVA Saugroboter Adapter
    T tombox

    @maloross

    https://github.com/TA2k/ioBroker.dreame

    ioBroker Allgemein

  • [gelöst] Schwierigkeiten mit alexa-Adapter
    DuffyD Duffy

    @samson71

    Danke für die Erklärung.

    Error/Bug

Mitgliederliste

apollon77A apollon77
DutchmanD Dutchman
HomoranH Homoran
BluefoxB Bluefox
eric2905E eric2905
StabilostickS Stabilostick
D Daniel81
O Olivbus
L LaplaceII
Sickboy78S Sickboy78
T tony63526
Peter V.P Peter V.
F femi
U UV-on-fire
patricknitschP patricknitsch
I I-Punkt
B Berny-K
V Verblizz
K Krissie777
A Automatisierer 0
  • 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