@mrMuppet
Hallo, kannst du das Skript auch für Historie Adapter machen?
Pro
Erfahrene
Beiträge
-
Google Material 3 / Material You - Vis2 -
Google Material 3 / Material You - Vis2Hier 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:
- InfluxDB: Eine laufende Instanz (im Skript definiert als
influxdb.0), die die Verlaufsdaten loggt. - 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 idealerweiseVALVE(Ventilöffnung in %). Optional: Fenster-Kontakte (STATE) und Luftfeuchtigkeit (HUMIDITY).
So wird es eingerichtet:
- Ein neues TypeScript im Javascript-Adapter anlegen und den Code einfügen.
- Oben im Skript den
INFLUX_INSTANCENamen anpassen, falls eine andere Instanz genutzt wird. - Das
rooms-Array an die eigenen Alias-Pfade und Raumnamen anpassen. Das Grid skaliert danach automatisch. - Das Skript starten. Es generiert automatisch alle nötigen Konfigurations-Datenpunkte sowie den finalen HTML-Datenpunkt:
0_userdata.0.dashboard.heizungHTML. - 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.

// ============================================================ // 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(); } }); -
-
Zigbee Adapter mit LAN Dongle kein automatisches verbindendahin viel zu weit für Kabel
Für stabiles WLAN aber wohl auch.
-
Zigbee Adapter mit LAN Dongle kein automatisches verbinden@Thomas-Braun
schön wäre es, Gartenhaus in der Ecke auf Grundstück, dahin viel zu weit für Kabel einbuddeln... -
Bambulab 3d-Drucker adapterBei mir sieht der String übrigens so aus (also, wenn er nicht gerade wieder leer ist ;))
2026-06-08T10:34:14.188Z
-
Neuer Adapter pi-hole2 für pihole>=V6Und Kausalität ist nicht immer so einfach.
Vergleiche mal Datum der Nachrichten -
VIS-2 Filter Bar keine FunktionDas war der entscheidende Hinweis!
Ich war noch auf 0.1.3.32.
Nach dem Adapter Update klappts nun auch mit echarts

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
-
MOVA Saugroboter Adapter -
[gelöst] Schwierigkeiten mit alexa-AdapterDanke für die Erklärung.