Weiter zum Inhalt

Visualisierung

9.5k Themen 105.4k Beiträge

Hilfe zu Visualisierungen

NEWS

  • [gelöst] Carousel-Widget als Navigation nutzbar?

    Verschoben
    6
    1
    0 Stimmen
    6 Beiträge
    2k Aufrufe
    L
    @marco.k , hast Du die VIEW mit dem Carusel noch am Start. Ich würde soetwas auch gerne bauen/nutzen, finde zum Einstellen des Widgets aber nahezu keine Informationen - würde mich über Tips oder gar einen Export Deiner View sehr freuen. Gruß Andreas
  • Doku zum Carousel Widget?

    2
    0 Stimmen
    2 Beiträge
    246 Aufrufe
    L
    @iobnordlicht , hast Du es irgendwann herausgefunden? Ich suche die Doku wie Du damals - es scheint so, dass dieses Widget kaum bekannt ist.
  • Stand bei "Gruppierungen" (vis2)?

    8
    0 Stimmen
    8 Beiträge
    279 Aufrufe
    I
    iobroker2001 sagte: Danke für die Überlegung, den Ursprung zu verlegen, das ist eine Idee. Ja, die XY-Koordinaten werden 1:1 übernommen. Leider funktioniert das doch nicht so, wie ich dachte und Du vorgeschlagen hattest. Also: Original X-Y-mäßig an die neuen Stellen verschieben, komplett markieren, exportieren. Dann in der neuen View importieren: die neuen XY-Koordinaten werden nicht genommen, alles umsonst. Wenn sich dann die neuen 30 Widgets mit den darunterliegenden Widgets vermischen, macht das keinen Spaß. Alleine schon beim Verschieben von ca. 20 - 30 Widgets gleichzeitig (also ohne Gruppierung) passieren schon so abenteuerliche Sachen, da brauche ich keine Beispiele zu liefern. Das fällt jedem auf, der das verwendet. Im Editor reicht es manchmal schon aus, ein einziges Widget zu verschieben, das ist im Editor verschoben, in der Runtime aber nicht (nein, auch nicht nach Auffrischen). Es steht dann 10 - 20 Pixel daneben oder darunter. Erst mit neuerlichem Verschieben per Tastatur wird die Position erreicht.
  • Lokale-Webseite auf dem IOBroker , wohin kopieren ?

    5
    0 Stimmen
    5 Beiträge
    183 Aufrufe
    David G.D
    Ich hab das früher immer über den proxy Adapter gemacht. Aber es geht auch einfach im Admin unter Dateien hochladen
  • VIS-2 Inventwo Widget

    4
    0 Stimmen
    4 Beiträge
    174 Aufrufe
    Holger EtzelH
    @jkvarel danke für die Bemühung, ich habe es jetzt anders gelöst. Ich habe im Popup-Fenster einen zusätzlichen Button zum zurücksetzen des Datenpunktes eingefügt.
  • VIS2 Material Design Line History Chart

    3
    1
    0 Stimmen
    3 Beiträge
    117 Aufrufe
    P
    @icebear Hallo, danke für die Hilfe Ich habe jetzt das echart Widget eingefügt. Das klappt mit einem Datenpunkt prima. Hast du evtl. noch ein Widget wo ich mit 2 oder 3 Datenpunkten arbeiten kann Vg
  • VIS-2 Widget für Anzeige der Anrufliste

    24
    0 Stimmen
    24 Beiträge
    860 Aufrufe
    Holger EtzelH
    @skvarel danke schon mal. Die Einstellungen vom 'ernergiefluss-erweitert' Adapter verstehe ich aktuell nicht. Hast du zufällig eine Beispiel-Projekt und kann man das im Adapter einspielen?
  • Wallbox von E3dc Efy einbinden

    1
    0 Stimmen
    1 Beiträge
    68 Aufrufe
    Niemand hat geantwortet
  • Zeigt her eure Visu

    vis
    130
    2 Stimmen
    130 Beiträge
    37k Aufrufe
    H
    Moin ! ich möchte einen mobilen, smarten Luftentfeuchter (Klamer) der mit der Tuya App komplett gesteuert werden kann visualisieren. Ich suche noch nach einer Inspiration, wie dich eine Visualisierung umsetzten kann. Hat jemand ein Beispiel oder etwas ähnliches schon umgesetzt und würde es gerne mal zeigen ? Danke.
  • iQontrol Vis Support Thread

    android app vis how-to ios app iqontrol mobile ui vis
    2k
    5
    7 Stimmen
    2k Beiträge
    866k Aufrufe
    Z
    @Markus_ Das scheint ja der selbe Fehler zu sein, den du bereits oben erwähntest. Daher meine Vorschläge einer andersartigen Erzeugung. Hast du die mal versucht?
  • [ERLEDIGT] IOB VIS - Projektanzahl begrenzt ?

    4
    0 Stimmen
    4 Beiträge
    615 Aufrufe
    arteckA
    @Mario64 sagte: Ich hatte mal irgendwo gelesen das max. 3 Projekte möglich sind. das ist quack.. ich hab 15 bei mir zur Zeit..
  • Lovelace Enhanced-Entities-Card

    1
    3
    1 Stimmen
    1 Beiträge
    171 Aufrufe
    Niemand hat geantwortet
  • dynamische SVG-Generierung: Wifi, RF, Temp, Humi, CO2, Noise

    vis template how-to
    3
    2
    1 Stimmen
    3 Beiträge
    424 Aufrufe
    Ro75R
    Ich habe es nochmal in template literale gesetzt. Gleichzeitig waren zwei svg "gebrochen". Da war beim Kopieren was schiefgelaufen. Jetzt passt es. Code in Post #1 korrigiert. Ro75.
  • Sichtbarkeit bei NaN/null

    3
    4
    0 Stimmen
    3 Beiträge
    1k Aufrufe
    F
    @arteck Schöne Idee, aber es gibt auch Temperaturen unter 0 Grad
  • ioBroker.VISU-App zeigt mir am iPhone Ungültige Lizenz

    3
    0 Stimmen
    3 Beiträge
    993 Aufrufe
    haselchenH
    @SSW-mcor Behalt es lieber für Dich. Nachher könnte die Lösung für Andere hilfreich sein . Das wäre doch schade .
  • Support Adapter Energiefluss-erweitert v0.7.7

    Verschoben vis
    5k
    9
    20 Stimmen
    5k Beiträge
    6m Aufrufe
    hardwarefehlerH
    Hi, was ist eigentlich der Bezugswert einer Überschreibung? Allein die Datenquelle aus den Basic-Einstellungen oder werden die Additionen/Subtraktionen aus der erweiterten Einstellung mit einbezogen?
  • Meine neue DIY-Visualisierung für ioBroker

    9
    9
    1 Stimmen
    9 Beiträge
    3k Aufrufe
    MrLarodosM
    @dirkhe Hier die aktuelle Version - nicht als Adapter, sondern momentan Stand-Alone-Script: https://github.com/MrLarodos/LaroVis
  • Google Material 3 / Material You - Vis2

    vis material css
    19
    7
    3 Stimmen
    19 Beiträge
    6k Aufrufe
    M
    @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(); } });
  • vis-2 basic string widget color binding zeigt nichts an

    18
    1
    0 Stimmen
    18 Beiträge
    5k Aufrufe
    Q
    @oliverio Danke für deine Hilfe. Hab jetzt mit deinem css padding fix und dem Basic Bool HTML Widget gelöst.
  • [gelöst] Balkendiagramm für Momentanwerte?

    9
    1
    0 Stimmen
    9 Beiträge
    3k Aufrufe
    U
    @hardwarefehler sagte: Wenn ich das richtig verstehe, steuerst du das Refresh-Intervall mit dem setTimeout() ganz am Ende. das ist nur eine Verzögerung beim Start. Damit beim ersten Rendern die Definition verfügbar ist und die Charts sauber gerendert werden

450

Online

33.0k

Benutzer

83.5k

Themen

1.3m

Beiträge