Skip to content
  • Home
  • Aktuell
  • Tags
  • 0 Ungelesen 0
  • Kategorien
  • Unreplied
  • Beliebt
  • GitHub
  • Docu
  • Hilfe
Skins
  • Light
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

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

Community Forum

donate donate
  1. ioBroker Community Home
  2. Deutsch
  3. Visualisierung
  4. Google Material 3 / Material You - Vis2

NEWS

  • Monatsrückblick Januar/Februar 2026 ist online!
    BluefoxB
    Bluefox
    17
    1
    477

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

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

Google Material 3 / Material You - Vis2

Geplant Angeheftet Gesperrt Verschoben Visualisierung
vismaterial css
11 Beiträge 3 Kommentatoren 233 Aufrufe 6 Watching
  • Älteste zuerst
  • Neuste zuerst
  • Meiste Stimmen
Antworten
  • In einem neuen Thema antworten
Anmelden zum Antworten
Dieses Thema wurde gelöscht. Nur Nutzer mit entsprechenden Rechten können es sehen.
  • M Offline
    M Offline
    mrMuppet
    schrieb am zuletzt editiert von
    #2

    Hier habe ich noch mal eine etwas ausführlichere, technischere Erklärung:

    Material You (M3) Theme Engine für ioBroker vis-2 — Technik-Deep-Dive

    Ich möchte hier meine Erfahrungen teilen, wie ich ein vollständig dynamisches Material Design 3 (Material You) Dashboard in ioBroker vis-2 umgesetzt habe. Das Ganze besteht aus einem zentralen Theme Master und mehreren HTML-Renderern, die als TypeScript-Skripte im JavaScript-Adapter laufen und ihr fertiges HTML in Datenpunkte schreiben. vis-2 zeigt diese dann über basic-html Widgets an.

    Vielleicht hilft es dem einen oder anderen, der auch ein modernes, einheitliches Dashboard bauen möchte.


    1. Die Grundidee: CSS-Variablen als zentrale Farbquelle

    Der wichtigste Baustein des gesamten Systems ist ein einziger Datenpunkt (0_userdata.0.dashboard.themeCSS), der ein <style>-Tag mit CSS Custom Properties enthält. Dieser Datenpunkt wird von einem zentralen Skript — dem Theme Master — befüllt und von allen anderen Renderern konsumiert.

    Das CSS sieht so aus:

    :root {
        --m3-primary: #3d6838;
        --m3-on-primary: #ffffff;
        --m3-primary-container: #bef0b2;
        --m3-surface-container-low: #f3f4ed;
        /* ... ca. 25 weitere M3-Tokens ... */
        --m3-bg-image: url('/vis.0/main/img/wallpaper.jpg');
    }
    

    Warum das so mächtig ist: Jeder Renderer referenziert ausschließlich var(--m3-primary) etc. — nie feste Farbwerte. Wenn der Theme Master die Variablen ändert, passt sich alles sofort an. Dark Mode, Farbwechsel, Wallpaper — ein einziger Datenpunkt steuert das komplette Look & Feel.

    Tipp für Nachbauer

    In vis-2 bindet ihr das CSS über ein basic-html Widget ein, das den Datenpunkt themeCSS anzeigt. Das Widget kann unsichtbar sein — es injiziert nur das <style>-Tag in den DOM.


    2. Der Theme Master — Drei Wege zur Farbe

    Der Theme Master (V7) unterstützt drei Farbquellen mit einer klaren Prioritätskette:

    Priorität 1 — JSON-Import (Google Material Theme Builder):
    Man kann auf material-foundation.github.io ein Theme zusammenklicken und das JSON exportieren. Das Skript liest schemes.dark bzw. schemes.light aus und generiert daraus die CSS-Variablen. Das ist der schnellste Weg zu einem professionellen Farbschema.

    Priorität 2 — Bildanalyse (Monet-Algorithmus):
    Wenn kein JSON aktiv ist, aber ein Hintergrundbild gesetzt wurde, wird die Dominant-Farbe aus dem Bild extrahiert — genau wie Android es bei Material You macht. Dafür werden zwei NPM-Pakete verwendet:

    • jimp — Bild laden und auf 128px verkleinern (Performance!)
    • @material/material-color-utilities — Die offizielle Google-Library mit QuantizerCelebi und Score zur Farbextraktion, und themeFromSourceColor zur Generierung aller M3-Tonal-Paletten

    Das Bild wird pixelweise gescannt, die Farben quantisiert, und die beste Farbe wird als Seed für das gesamte M3-Schema verwendet. Das ist exakt der gleiche Algorithmus, den Android 12+ nutzt.

    Ein entscheidender Punkt, der in vielen Tutorials fehlt: Die Surface-Container-Farben (die 5 Abstufungen von surfaceContainerLowest bis surfaceContainerHighest) werden von themeFromSourceColor nicht korrekt für alle Abstufungen geliefert. Deshalb werden die Tonal Palettes manuell mit den korrekten M3-Tone-Werten überschrieben:

    // Dark Mode Surface Tones (aus der M3-Spezifikation)
    scheme.surfaceContainerLowest  = neutralPalette.tone(4);
    scheme.surfaceContainerLow     = neutralPalette.tone(10);
    scheme.surfaceContainer        = neutralPalette.tone(12);
    scheme.surfaceContainerHigh    = neutralPalette.tone(17);
    scheme.surfaceContainerHighest = neutralPalette.tone(22);
    

    Priorität 3 — Hardcoded Fallback:
    Wenn weder JSON noch Bild verfügbar sind, greift ein eingebautes Default-Theme (ein dezentes Grün).

    Tipp für Nachbauer

    Die NPM-Module müssen im JavaScript-Adapter unter Zusätzliche NPM-Module eingetragen werden: @material/material-color-utilities@0.2.7, jimp@0.22.10. Neuere Versionen von Jimp haben eine andere API — bei der Version bleiben!


    3. Automatischer Dark/Light-Mode via Astro

    Der Theme Master kann den Dark Mode automatisch nach Sonnenstand umschalten. Dafür wird die ioBroker-eigene Astro-Funktion genutzt:

    schedule({astro: "sunrise"}, checkAstro);
    schedule({astro: "sunset"}, checkAstro);
    

    Die Funktion isAstroDay() prüft, ob die Sonne über dem Horizont steht, und setzt den darkMode-Datenpunkt entsprechend. Das Schöne: Man kann den Auto-Modus per Schalter deaktivieren und manuell umschalten — z.B. wenn man abends im Hellen auf der Terrasse sitzt.


    4. Das Renderer-Pattern — HTML in Datenpunkte

    Jeder Bereich des Dashboards (Licht, Heizung, Beschattung, Favoriten, Anwesenheit, Alerts…) hat ein eigenes TypeScript-Skript, das nach dem gleichen Muster arbeitet:

    1. M3-Farbobjekt definieren — aber nur mit var()-Referenzen, nie mit festen Farben
    2. HTML als String zusammenbauen — mit Inline-Styles, die die CSS-Variablen nutzen
    3. In einen Datenpunkt schreiben (setState(DP_HTML, html, true))
    4. Auf Änderungen der Geräte-Datenpunkte subscriben und bei Änderung neu rendern
    const M3 = {
        primary:             'var(--m3-primary)',
        onPrimary:           'var(--m3-on-primary)',
        primaryContainer:    'var(--m3-primary-container)',
        surfaceContainerLow: 'var(--m3-surface-container-low)',
        // ...
    };
    

    Warum Inline-Styles und kein Stylesheet?
    Weil vis-2 die HTML-Widgets als isolierte Fragmente rendert. Ein zentrales Stylesheet wäre schwierig zu managen. Mit Inline-Styles, die var() nutzen, bekommt man das Beste aus beiden Welten: Die Farben sind zentral gesteuert (über die CSS-Variablen im :root), aber die Styles sind im HTML-Fragment selbst enthalten.

    Tipp für Nachbauer

    Jeder Renderer hat ein const M3 = { ... } Objekt am Anfang. Wenn ihr neue Renderer baut, kopiert dieses Objekt einfach — es ist bei allen gleich. So habt ihr automatisch die Theme-Anbindung.


    5. Clevere UI-Tricks im Detail

    5.1 Live Color-Mix bei Slidern

    Die Licht- und Beschattungs-Karten werden beim Dimmen dynamisch eingefärbt. Das funktioniert mit CSS color-mix():

    const mixPct = Math.round(20 + (percentage * 0.8));
    const bg = active 
        ? `color-mix(in srgb, ${M3.primaryContainer} ${mixPct}%, ${M3.surfaceContainerLow})` 
        : M3.surfaceContainerLow;
    

    Bei 0% ist die Karte grau (Surface), bei 100% in der Akzentfarbe (Primary Container). Der Übergang ist fließend. Das gibt ein haptisches Feedback, das man sonst nur von nativen Apps kennt.

    5.2 M3 Ripple-Effekt

    Jede klickbare Karte hat einen Material-Ripple-Effekt — komplett in Vanilla JS, ohne Framework:

    const rippleJS = `(function(e,el){
        var d = Math.max(el.clientWidth, el.clientHeight);
        var rect = el.getBoundingClientRect();
        var c = document.createElement('span');
        c.style.width = c.style.height = d + 'px';
        c.style.left = (e.clientX - rect.left - d/2) + 'px';
        c.style.top = (e.clientY - rect.top - d/2) + 'px';
        c.style.position = 'absolute';
        c.style.borderRadius = '50%';
        c.style.backgroundColor = '${rippleColor}';
        c.style.opacity = '0.15';
        c.style.transform = 'scale(0)';
        c.style.animation = 'm3-ripple-anim 0.5s linear';
        el.appendChild(c);
        setTimeout(function(){c.remove()}, 500);
    })(event, this)`;
    

    Das wird als onmousedown und ontouchstart direkt in den HTML-String geschrieben. Kein externen JS nötig.

    5.3 Filled/Outlined Icon-Umschaltung

    Die Tab-Navigation nutzt ein Konzept aus Material You: Aktive Tabs zeigen ein gefülltes Icon, inaktive ein Outline-Icon. Das wird mit zwei übereinander liegenden SVG-Pfaden gelöst, deren fill per vis-Binding umgeschaltet wird:

    <!-- Gefüllter Pfad: nur sichtbar wenn aktiv -->
    <path fill="{a:active_tab; a === 0 ? 'var(--m3-on-tertiary-container)' : 'transparent'}" d="M12 17.27L18.18 21..."/>
    <!-- Outline-Pfad: nur sichtbar wenn inaktiv -->
    <path fill="{a:active_tab; a === 0 ? 'transparent' : 'var(--m3-on-surface-variant)'}" d="M22 9.24l-7.19..."/>
    

    Das ist eine elegante Lösung ohne JavaScript — rein über vis-Bindings.

    5.4 Heizungs-Karten mit Amber-Logik

    Die Heizungskarten haben drei Zustände: inaktiv (grau), aktiv (Primary), und „Ventil offen, aber Heizung aus" (Amber/Warning). Das gibt sofortiges visuelles Feedback, ob die Heizung auch wirklich heizt oder nur das Ventil offen steht.

    5.5 Circadian-Flurbeleuchtung

    Ein separates Skript berechnet anhand der Astro-Daten (Sonnenaufgang, Golden Hour, Sonnenuntergang, Dämmerung) die passende Farbtemperatur und Helligkeit für das Flurlicht. Tagsüber: 6000K bei 70%. Nachts: 2200K bei 6%. Dazwischen wird linear gefaded. Getriggert wird durch Präsenz-, Tür- und Bewegungssensoren.


    6. Wallpaper als Hintergrund — vis-2 überschreiben

    vis-2 setzt seinen eigenen Hintergrund. Um ein dynamisches Wallpaper zu nutzen, überschreibt der Theme Master aggressiv alle möglichen Container:

    body, html, #root, #vis_container, .vis-view, .app-container {
        background-image: var(--m3-bg-image) !important;
        background-size: cover !important;
        background-position: center center !important;
        background-attachment: fixed !important;
        background-color: transparent !important;
    }
    

    Das Wallpaper wird als Pfad im Datenpunkt themeImagePath gesetzt (z.B. vis.0/main/img/wallpaper.jpg). Es wird unabhängig von der Farbquelle angezeigt — man kann also ein JSON-Theme haben und trotzdem ein Wallpaper.


    7. Weitere Automatisierungen rund ums Dashboard

    • Dashboard Auto-Return: Nach 5 Minuten Inaktivität springt der Tab automatisch zurück auf die Favoriten-Seite (Kiosk-Modus).
    • Gute-Nacht-Routine: Ein Button-Taster löst eine Routine aus, die alle Lichter aus, das Flurlicht an und die Haustür verriegelt. Geräte werden zeitversetzt geschaltet (setStateDelayed mit 150ms Abstand), um den Duty Cycle der Funk-Bridges zu schonen.
    • Alert-System: Warnungen (Sicherheit, Komfort, Info) werden über einen zentralen Datenpunkt getriggert, mit Prefix-Codierung für Typ und Dismiss-Verhalten (EM: = Error/Manuell, CT: = Comfort/Timeout, etc.).

    8. Zusammenfassung — Was braucht man?

    Komponente Zweck
    Theme Master (JS-Skript) Erzeugt CSS-Variablen aus JSON, Bild oder Fallback
    themeCSS Datenpunkt Enthält das <style>-Tag, wird von vis-2 angezeigt
    Pro Bereich ein Renderer (JS/TS) Erzeugt HTML mit var()-Referenzen
    Pro Bereich ein basic-html Widget Zeigt den HTML-Datenpunkt an
    NPM: @material/material-color-utilities M3-Farbberechnung
    NPM: jimp Bildanalyse für Monet-Algorithmus

    Die Architektur in einem Satz: Ein zentrales Skript schreibt CSS-Variablen in einen Datenpunkt, alle anderen Skripte rendern HTML, das diese Variablen nutzt, und vis-2 zeigt das Ganze an.


    Ich hoffe, das hilft jemandem weiter! Bei Fragen gerne melden.

    ioBroker auf NUC (Celeron mit Ubuntu-Server)

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

    OliverIOO 1 Antwort Letzte Antwort
    0
    • M mrMuppet

      Hier habe ich noch mal eine etwas ausführlichere, technischere Erklärung:

      Material You (M3) Theme Engine für ioBroker vis-2 — Technik-Deep-Dive

      Ich möchte hier meine Erfahrungen teilen, wie ich ein vollständig dynamisches Material Design 3 (Material You) Dashboard in ioBroker vis-2 umgesetzt habe. Das Ganze besteht aus einem zentralen Theme Master und mehreren HTML-Renderern, die als TypeScript-Skripte im JavaScript-Adapter laufen und ihr fertiges HTML in Datenpunkte schreiben. vis-2 zeigt diese dann über basic-html Widgets an.

      Vielleicht hilft es dem einen oder anderen, der auch ein modernes, einheitliches Dashboard bauen möchte.


      1. Die Grundidee: CSS-Variablen als zentrale Farbquelle

      Der wichtigste Baustein des gesamten Systems ist ein einziger Datenpunkt (0_userdata.0.dashboard.themeCSS), der ein <style>-Tag mit CSS Custom Properties enthält. Dieser Datenpunkt wird von einem zentralen Skript — dem Theme Master — befüllt und von allen anderen Renderern konsumiert.

      Das CSS sieht so aus:

      :root {
          --m3-primary: #3d6838;
          --m3-on-primary: #ffffff;
          --m3-primary-container: #bef0b2;
          --m3-surface-container-low: #f3f4ed;
          /* ... ca. 25 weitere M3-Tokens ... */
          --m3-bg-image: url('/vis.0/main/img/wallpaper.jpg');
      }
      

      Warum das so mächtig ist: Jeder Renderer referenziert ausschließlich var(--m3-primary) etc. — nie feste Farbwerte. Wenn der Theme Master die Variablen ändert, passt sich alles sofort an. Dark Mode, Farbwechsel, Wallpaper — ein einziger Datenpunkt steuert das komplette Look & Feel.

      Tipp für Nachbauer

      In vis-2 bindet ihr das CSS über ein basic-html Widget ein, das den Datenpunkt themeCSS anzeigt. Das Widget kann unsichtbar sein — es injiziert nur das <style>-Tag in den DOM.


      2. Der Theme Master — Drei Wege zur Farbe

      Der Theme Master (V7) unterstützt drei Farbquellen mit einer klaren Prioritätskette:

      Priorität 1 — JSON-Import (Google Material Theme Builder):
      Man kann auf material-foundation.github.io ein Theme zusammenklicken und das JSON exportieren. Das Skript liest schemes.dark bzw. schemes.light aus und generiert daraus die CSS-Variablen. Das ist der schnellste Weg zu einem professionellen Farbschema.

      Priorität 2 — Bildanalyse (Monet-Algorithmus):
      Wenn kein JSON aktiv ist, aber ein Hintergrundbild gesetzt wurde, wird die Dominant-Farbe aus dem Bild extrahiert — genau wie Android es bei Material You macht. Dafür werden zwei NPM-Pakete verwendet:

      • jimp — Bild laden und auf 128px verkleinern (Performance!)
      • @material/material-color-utilities — Die offizielle Google-Library mit QuantizerCelebi und Score zur Farbextraktion, und themeFromSourceColor zur Generierung aller M3-Tonal-Paletten

      Das Bild wird pixelweise gescannt, die Farben quantisiert, und die beste Farbe wird als Seed für das gesamte M3-Schema verwendet. Das ist exakt der gleiche Algorithmus, den Android 12+ nutzt.

      Ein entscheidender Punkt, der in vielen Tutorials fehlt: Die Surface-Container-Farben (die 5 Abstufungen von surfaceContainerLowest bis surfaceContainerHighest) werden von themeFromSourceColor nicht korrekt für alle Abstufungen geliefert. Deshalb werden die Tonal Palettes manuell mit den korrekten M3-Tone-Werten überschrieben:

      // Dark Mode Surface Tones (aus der M3-Spezifikation)
      scheme.surfaceContainerLowest  = neutralPalette.tone(4);
      scheme.surfaceContainerLow     = neutralPalette.tone(10);
      scheme.surfaceContainer        = neutralPalette.tone(12);
      scheme.surfaceContainerHigh    = neutralPalette.tone(17);
      scheme.surfaceContainerHighest = neutralPalette.tone(22);
      

      Priorität 3 — Hardcoded Fallback:
      Wenn weder JSON noch Bild verfügbar sind, greift ein eingebautes Default-Theme (ein dezentes Grün).

      Tipp für Nachbauer

      Die NPM-Module müssen im JavaScript-Adapter unter Zusätzliche NPM-Module eingetragen werden: @material/material-color-utilities@0.2.7, jimp@0.22.10. Neuere Versionen von Jimp haben eine andere API — bei der Version bleiben!


      3. Automatischer Dark/Light-Mode via Astro

      Der Theme Master kann den Dark Mode automatisch nach Sonnenstand umschalten. Dafür wird die ioBroker-eigene Astro-Funktion genutzt:

      schedule({astro: "sunrise"}, checkAstro);
      schedule({astro: "sunset"}, checkAstro);
      

      Die Funktion isAstroDay() prüft, ob die Sonne über dem Horizont steht, und setzt den darkMode-Datenpunkt entsprechend. Das Schöne: Man kann den Auto-Modus per Schalter deaktivieren und manuell umschalten — z.B. wenn man abends im Hellen auf der Terrasse sitzt.


      4. Das Renderer-Pattern — HTML in Datenpunkte

      Jeder Bereich des Dashboards (Licht, Heizung, Beschattung, Favoriten, Anwesenheit, Alerts…) hat ein eigenes TypeScript-Skript, das nach dem gleichen Muster arbeitet:

      1. M3-Farbobjekt definieren — aber nur mit var()-Referenzen, nie mit festen Farben
      2. HTML als String zusammenbauen — mit Inline-Styles, die die CSS-Variablen nutzen
      3. In einen Datenpunkt schreiben (setState(DP_HTML, html, true))
      4. Auf Änderungen der Geräte-Datenpunkte subscriben und bei Änderung neu rendern
      const M3 = {
          primary:             'var(--m3-primary)',
          onPrimary:           'var(--m3-on-primary)',
          primaryContainer:    'var(--m3-primary-container)',
          surfaceContainerLow: 'var(--m3-surface-container-low)',
          // ...
      };
      

      Warum Inline-Styles und kein Stylesheet?
      Weil vis-2 die HTML-Widgets als isolierte Fragmente rendert. Ein zentrales Stylesheet wäre schwierig zu managen. Mit Inline-Styles, die var() nutzen, bekommt man das Beste aus beiden Welten: Die Farben sind zentral gesteuert (über die CSS-Variablen im :root), aber die Styles sind im HTML-Fragment selbst enthalten.

      Tipp für Nachbauer

      Jeder Renderer hat ein const M3 = { ... } Objekt am Anfang. Wenn ihr neue Renderer baut, kopiert dieses Objekt einfach — es ist bei allen gleich. So habt ihr automatisch die Theme-Anbindung.


      5. Clevere UI-Tricks im Detail

      5.1 Live Color-Mix bei Slidern

      Die Licht- und Beschattungs-Karten werden beim Dimmen dynamisch eingefärbt. Das funktioniert mit CSS color-mix():

      const mixPct = Math.round(20 + (percentage * 0.8));
      const bg = active 
          ? `color-mix(in srgb, ${M3.primaryContainer} ${mixPct}%, ${M3.surfaceContainerLow})` 
          : M3.surfaceContainerLow;
      

      Bei 0% ist die Karte grau (Surface), bei 100% in der Akzentfarbe (Primary Container). Der Übergang ist fließend. Das gibt ein haptisches Feedback, das man sonst nur von nativen Apps kennt.

      5.2 M3 Ripple-Effekt

      Jede klickbare Karte hat einen Material-Ripple-Effekt — komplett in Vanilla JS, ohne Framework:

      const rippleJS = `(function(e,el){
          var d = Math.max(el.clientWidth, el.clientHeight);
          var rect = el.getBoundingClientRect();
          var c = document.createElement('span');
          c.style.width = c.style.height = d + 'px';
          c.style.left = (e.clientX - rect.left - d/2) + 'px';
          c.style.top = (e.clientY - rect.top - d/2) + 'px';
          c.style.position = 'absolute';
          c.style.borderRadius = '50%';
          c.style.backgroundColor = '${rippleColor}';
          c.style.opacity = '0.15';
          c.style.transform = 'scale(0)';
          c.style.animation = 'm3-ripple-anim 0.5s linear';
          el.appendChild(c);
          setTimeout(function(){c.remove()}, 500);
      })(event, this)`;
      

      Das wird als onmousedown und ontouchstart direkt in den HTML-String geschrieben. Kein externen JS nötig.

      5.3 Filled/Outlined Icon-Umschaltung

      Die Tab-Navigation nutzt ein Konzept aus Material You: Aktive Tabs zeigen ein gefülltes Icon, inaktive ein Outline-Icon. Das wird mit zwei übereinander liegenden SVG-Pfaden gelöst, deren fill per vis-Binding umgeschaltet wird:

      <!-- Gefüllter Pfad: nur sichtbar wenn aktiv -->
      <path fill="{a:active_tab; a === 0 ? 'var(--m3-on-tertiary-container)' : 'transparent'}" d="M12 17.27L18.18 21..."/>
      <!-- Outline-Pfad: nur sichtbar wenn inaktiv -->
      <path fill="{a:active_tab; a === 0 ? 'transparent' : 'var(--m3-on-surface-variant)'}" d="M22 9.24l-7.19..."/>
      

      Das ist eine elegante Lösung ohne JavaScript — rein über vis-Bindings.

      5.4 Heizungs-Karten mit Amber-Logik

      Die Heizungskarten haben drei Zustände: inaktiv (grau), aktiv (Primary), und „Ventil offen, aber Heizung aus" (Amber/Warning). Das gibt sofortiges visuelles Feedback, ob die Heizung auch wirklich heizt oder nur das Ventil offen steht.

      5.5 Circadian-Flurbeleuchtung

      Ein separates Skript berechnet anhand der Astro-Daten (Sonnenaufgang, Golden Hour, Sonnenuntergang, Dämmerung) die passende Farbtemperatur und Helligkeit für das Flurlicht. Tagsüber: 6000K bei 70%. Nachts: 2200K bei 6%. Dazwischen wird linear gefaded. Getriggert wird durch Präsenz-, Tür- und Bewegungssensoren.


      6. Wallpaper als Hintergrund — vis-2 überschreiben

      vis-2 setzt seinen eigenen Hintergrund. Um ein dynamisches Wallpaper zu nutzen, überschreibt der Theme Master aggressiv alle möglichen Container:

      body, html, #root, #vis_container, .vis-view, .app-container {
          background-image: var(--m3-bg-image) !important;
          background-size: cover !important;
          background-position: center center !important;
          background-attachment: fixed !important;
          background-color: transparent !important;
      }
      

      Das Wallpaper wird als Pfad im Datenpunkt themeImagePath gesetzt (z.B. vis.0/main/img/wallpaper.jpg). Es wird unabhängig von der Farbquelle angezeigt — man kann also ein JSON-Theme haben und trotzdem ein Wallpaper.


      7. Weitere Automatisierungen rund ums Dashboard

      • Dashboard Auto-Return: Nach 5 Minuten Inaktivität springt der Tab automatisch zurück auf die Favoriten-Seite (Kiosk-Modus).
      • Gute-Nacht-Routine: Ein Button-Taster löst eine Routine aus, die alle Lichter aus, das Flurlicht an und die Haustür verriegelt. Geräte werden zeitversetzt geschaltet (setStateDelayed mit 150ms Abstand), um den Duty Cycle der Funk-Bridges zu schonen.
      • Alert-System: Warnungen (Sicherheit, Komfort, Info) werden über einen zentralen Datenpunkt getriggert, mit Prefix-Codierung für Typ und Dismiss-Verhalten (EM: = Error/Manuell, CT: = Comfort/Timeout, etc.).

      8. Zusammenfassung — Was braucht man?

      Komponente Zweck
      Theme Master (JS-Skript) Erzeugt CSS-Variablen aus JSON, Bild oder Fallback
      themeCSS Datenpunkt Enthält das <style>-Tag, wird von vis-2 angezeigt
      Pro Bereich ein Renderer (JS/TS) Erzeugt HTML mit var()-Referenzen
      Pro Bereich ein basic-html Widget Zeigt den HTML-Datenpunkt an
      NPM: @material/material-color-utilities M3-Farbberechnung
      NPM: jimp Bildanalyse für Monet-Algorithmus

      Die Architektur in einem Satz: Ein zentrales Skript schreibt CSS-Variablen in einen Datenpunkt, alle anderen Skripte rendern HTML, das diese Variablen nutzt, und vis-2 zeigt das Ganze an.


      Ich hoffe, das hilft jemandem weiter! Bei Fragen gerne melden.

      OliverIOO Offline
      OliverIOO Offline
      OliverIO
      schrieb am zuletzt editiert von
      #3

      @mrMuppet

      vis 2 nutzt ja eigentlich die mui bibliothek, welche react komponenten auf basis des google material konzepts anbietet.

      das mit dem farbschema war hier auch immer wieder mal ein thema.
      ich weiß du nutzt das eher mit server side rendering,
      aber wenn du magst kannst du die lösung mal liken,
      dann stehen die cssVariablen (mit prefix mui-) auch im vis-2 mit zur Verfügung.

      https://github.com/ioBroker/ioBroker.vis-2/issues/581

      damit könnte dann jeder sein eigenes theme zusammenbauen und müsste nicht in allen einstellungen immer wieder die farben neu definieren, bzw kommt man an manche einstellungen da nicht dran.

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

      1 Antwort Letzte Antwort
      0
      • CyberraphC Online
        CyberraphC Online
        Cyberraph
        schrieb am zuletzt editiert von
        #4

        Ich knie nieder. W o W. ^^
        Einfach nur faszinierend.
        Das muss ich mal durchgehen und versuchen im Ansatz zu verstehen.
        Toll.
        Und danke vielmals für das Teilen deines Wissens / deiner Erarbeitung / deiner Erfahrung. :-)

        Beste Grüße!

        io-Broker Neuling 2024 :-)

        Bislang jedoch einiges an Beiträgen und Grundlagen eingeflößt, um etwas besser empor zu irren.

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

          Freut mich, dass es euch gefällt. Aber mein Wissen ist tatsächlich hier mehr konzeptionell: ich habe das ganze ja von Gemini schreiben lassen. Ich habe einfach beschrieben was ich haben möchte, welche Datenpunkte ich dazu habe (am besten natürlich alles über alias und userdata) und dann wieder korrigieren lassen. Immer wieder musste ich auch bereits fertiggestellte Scripte wieder hochladen, damit Gemini die Zusammenhänge nicht vergisst, aber so hat es langsam funktioniert. Auch die Material 3 Unterlagen von Google habe ich mir recht ausführlich angeschaut, damit ich Gemini auf Fehler aufmerksam machen konnte.
          Also viel so in dem Stil "So, dass ist der Skript meiner Heizungsseite, jetzt füge bitte noch die Funktion XY ein." - "der neue Skript hat noch nicht funktioniert, ich habe folgenden Fehler: ..." - "jetzt klappts. bitte ergänze noch diese Funktion"...

          Wobei man natürlich schon ein bisschen verstehen muss, wie TS und JS Dateien aufgebaut sind, wie der Vis-Editor bedient wird etc. Aber letztlich kann einem die KI natürlich auch bei fast allen Problemen helfen.

          ioBroker auf NUC (Celeron mit Ubuntu-Server)

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

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

            Weil die Wechsel der Themes und die sonstigen Animationen hier etwas schwer zu erkennen sind, habe ich ein Video gemacht. https://youtu.be/cMpPOGAlOhc

            ioBroker auf NUC (Celeron mit Ubuntu-Server)

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

            1 Antwort Letzte Antwort
            2
            • M Offline
              M Offline
              mrMuppet
              schrieb am zuletzt editiert von mrMuppet
              #7

              Ich werde hier mal einige meiner Skripte posten:

              Das Theme-Engine

              Was macht das Skript?

              Dieses Skript arbeitet als Theme-Engine für ioBroker vis (und vis-2), basierend auf dem Material 3 (Material You) Design von Google. Es nimmt ein von euch festgelegtes Hintergrundbild, analysiert dessen Farbspektrum und generiert daraus automatisch eine perfekt abgestimmte CSS-Farbpalette. Zusätzlich enthält das Skript eine automatische Astro-Steuerung: Sobald die Sonne in der Dämmerung einen bestimmten Winkel (standardmäßig -3 Grad) unterschreitet, wechselt das gesamte Dashboard nahtlos und weich animiert in den Dark-Mode. Alternativ können auch eigene JSON-Farbpaletten importiert werden. Diese JSON könnt ihr am einfachsten mit dem Material Theme Builder erstellen. Die Automatikfunktion ist hier technisch identisch zu der in meinem Script.

              Was muss vorbereitet werden?

              Damit das Skript Bilder analysieren kann, müssen zwingend zwei zusätzliche NPM-Module installiert werden. Geht dazu im ioBroker auf die Instanz-Einstellungen eures JavaScript-Adapters und tragt im Reiter "Zusätzliche NPM-Module" folgendes ein:

              • @material/material-color-utilities
              • jimp

              Bitte speichert die Instanz ab und wartet einen Moment, bis der Adapter die Module heruntergeladen hat. Startet ihr das Skript ohne diese Einträge, hagelt es sofort Fehlermeldungen im Log, da die Werkzeuge für die komplexe Farbberechnung fehlen.

              Wie wendet man das CSS in der vis an?

              Wenn das Skript läuft, legt es unter anderem den Datenpunkt "themeCSS" an (standardmäßig unter 0_userdata.0.dashboard.themeCSS). Um das dynamische Theme nun in eurer Visualisierung zu aktivieren, geht ihr so vor:

              1. Öffnet eure vis im Editor.
              2. Zieht ein einfaches "Basic - String" (oder "Basic - HTML") Widget auf eure Ansicht.
              3. Tragt in den Widget-Einstellungen unter HTML / Inhalt einfach den Pfad zum Datenpunkt in geschweiften Klammern ein:
              {0_userdata.0.dashboard.themeCSS}
              

              (Falls ihr den Pfad im Skript ganz oben geändert habt, müsst ihr hier natürlich euren eigenen Pfad eintragen).

              Das war es schon. Das Widget injiziert nun das gesamte generierte CSS in eure Ansicht. Sobald das Skript ab jetzt die Farben ändert oder in den Dark-Mode schaltet, reagiert eure komplette vis automatisch darauf.

              // ============================================================
              // M3 Theme Master V7 — Astro, Monet, JSON & Always-On Wallpaper
              // ============================================================
              // WICHTIG - VOR DEM START:
              // Damit das Skript Bilder analysieren und Farben berechnen kann, 
              // müssen im JavaScript-Adapter unter "Zusätzliche NPM-Module" 
              // folgende zwei Module eingetragen werden:
              // @material/material-color-utilities, jimp
              // ============================================================
              
              // === 1. KONFIGURATION ===
              // Wo sollen die Datenpunkte für das Dashboard angelegt werden?
              const BASE_PATH = '0_userdata.0.dashboard'; 
              
              // Datenpunkt für den Sonnenstand (Elevation / Sonnenhöhe in Grad).
              // Wird für die automatische Umschaltung in den Darkmode benötigt (-3 Grad Regel).
              // Falls du keinen hast, leer lassen ('') – das Skript nutzt dann die ungenaueren ioBroker-Bordmittel.
              const DP_SUN_ELEVATION = ''; // z.B. 'javascript.0.Astro.Elevation' oder 'suncalc.0.elevation'
              
              
              // ============================================================
              // === AB HIER NICHTS MEHR ÄNDERN ===
              // ============================================================
              
              const { themeFromSourceColor, QuantizerCelebi, Score } = require('@material/material-color-utilities');
              const Jimp = require('jimp');
              
              // --- DATENPUNKTE ---
              const DP_THEME_CSS  = `${BASE_PATH}.themeCSS`;
              const DP_DARK_MODE  = `${BASE_PATH}.darkMode`;
              const DP_AUTO_MODE  = `${BASE_PATH}.autoTheme`;
              const DP_THEME_JSON = `${BASE_PATH}.themeJSON`;
              const DP_IMAGE_PATH = `${BASE_PATH}.themeImagePath`;
              const DP_USE_JSON   = `${BASE_PATH}.themeUseJson`;
              
              const DEFAULT_THEME = {
                  schemes: {
                      light: {
                          primary: '#3d6838', primaryContainer: '#bef0b2', onPrimaryContainer: '#002202',
                          surfaceContainerLow: '#f3f4ed', surfaceContainerHighest: '#e1e3dc', surfaceVariant: '#dde5d8',
                          onSurface: '#191c18', onSurfaceVariant: '#42493f', outline: '#72796f', outlineVariant: '#c1c9bc',
                      },
                      dark: {
                          primary: '#a3d398', primaryContainer: '#265022', onPrimaryContainer: '#bef0b2',
                          surfaceContainerLow: '#191d17', surfaceContainerHighest: '#323630', surfaceVariant: '#42493f',
                          onSurface: '#e0e4da', onSurfaceVariant: '#c2c8bc', outline: '#8c9388', outlineVariant: '#42493f',
                      },
                  },
              };
              
              function camelToKebab(str) {
                  return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase();
              }
              
              function toHex(val) {
                  if (typeof val === 'string') return val;
                  return '#' + (val & 0x00ffffff).toString(16).padStart(6, '0');
              }
              
              // === BILD-ANALYSE (Monet) ===
              async function extractColorFromImage(path) {
                  try {
                      let realPath = path.trim();
                      if (realPath.startsWith('vis-') || realPath.startsWith('vis.')) {
                          realPath = '/' + realPath;
                      }
              
                      // Standard ioBroker Pfad für Web-Dateien (Linux/Raspberry)
                      let serverPath = realPath;
                      if (serverPath.startsWith('/vis')) {
                          serverPath = '/opt/iobroker/iobroker-data/files' + serverPath;
                      }
              
                      const image = await Jimp.read(serverPath);
                      image.resize(128, Jimp.AUTO); // Extrem verkleinern für Speed
              
                      const pixels = [];
                      image.scan(0, 0, image.bitmap.width, image.bitmap.height, function (x, y, idx) {
                          const r = this.bitmap.data[idx + 0];
                          const g = this.bitmap.data[idx + 1];
                          const b = this.bitmap.data[idx + 2];
                          const a = this.bitmap.data[idx + 3];
                          const argb = (a << 24) | (r << 16) | (g << 8) | b;
                          pixels.push(argb);
                      });
              
                      const quantizedColors = QuantizerCelebi.quantize(pixels, 128);
                      const bestColors = Score.score(quantizedColors);
                      return { color: bestColors[0], webPath: realPath };
                  } catch (error) {
                      log(`[M3 Theme] Bild konnte nicht analysiert werden (${path}): ${error.message}`, 'warn');
                      return null;
                  }
              }
              
              // === HAUPT-LOGIK ===
              async function updateTheme() {
                  const isDark = getState(DP_DARK_MODE).val;
                  const useJson = getState(DP_USE_JSON).val;
                  const imgPath = getState(DP_IMAGE_PATH).val;
              
                  let scheme = null;
                  let bgImageUrl = ''; 
              
                  try {
                      // 1. HINTERGRUNDBILD VORBEREITEN
                      if (imgPath && imgPath.trim() !== '') {
                          let webPath = imgPath.trim();
                          if (webPath.startsWith('vis-') || webPath.startsWith('vis.')) webPath = '/' + webPath;
                          bgImageUrl = `url('${webPath}')`;
                      }
              
                      // 2. FARBEN: JSON Priorität
                      if (useJson) {
                          const rawJson = getState(DP_THEME_JSON).val;
                          if (rawJson && rawJson.trim().startsWith('{')) {
                              const parsed = JSON.parse(rawJson);
                              if (parsed.schemes) {
                                  scheme = isDark ? parsed.schemes.dark : parsed.schemes.light;
                                  log(`[M3 Theme] Theme erfolgreich aus JSON geladen.`);
                              }
                          } else {
                              log(`[M3 Theme] JSON-Schalter aktiv, aber JSON fehlerhaft oder leer.`, 'warn');
                          }
                      }
              
                      // 3. FARBEN: Bildanalyse (Monet)
                      if (!scheme && imgPath && imgPath.trim() !== '') {
                          const result = await extractColorFromImage(imgPath);
                          if (result) {
                              const theme = themeFromSourceColor(result.color);
                              scheme = isDark ? theme.schemes.dark.toJSON() : theme.schemes.light.toJSON();
              
                              const nPalette = theme.palettes.neutral;
                              if (isDark) {
                                  scheme.surfaceContainerLowest = nPalette.tone(4);
                                  scheme.surfaceContainerLow = nPalette.tone(10);
                                  scheme.surfaceContainer = nPalette.tone(12);
                                  scheme.surfaceContainerHigh = nPalette.tone(17);
                                  scheme.surfaceContainerHighest = nPalette.tone(22);
                                  scheme.surfaceDim = nPalette.tone(6);
                                  scheme.surfaceBright = nPalette.tone(24);
                              } else {
                                  scheme.surfaceContainerLowest = nPalette.tone(100);
                                  scheme.surfaceContainerLow = nPalette.tone(96);
                                  scheme.surfaceContainer = nPalette.tone(94);
                                  scheme.surfaceContainerHigh = nPalette.tone(92);
                                  scheme.surfaceContainerHighest = nPalette.tone(90);
                                  scheme.surfaceDim = nPalette.tone(87);
                                  scheme.surfaceBright = nPalette.tone(98);
                              }
                              log(`[M3 Theme] Farben perfekt aus Hintergrundbild berechnet.`);
                          }
                      }
                  } catch (e) {
                      log(`[M3 Theme] Fehler bei der Berechnung: ${e.message}`, 'error');
                  }
              
                  // 4. FALLBACK
                  if (!scheme) scheme = isDark ? DEFAULT_THEME.schemes.dark : DEFAULT_THEME.schemes.light;
              
                  // --- CSS ZUSAMMENBAUEN ---
                  let cssVars = '';
                  for (const [key, value] of Object.entries(scheme)) {
                      cssVars += `    --m3-${camelToKebab(key)}: ${toHex(value)};\n`;
                  }
                  cssVars += `    --m3-bg-image: ${bgImageUrl !== '' ? bgImageUrl : 'none'};\n`;
              
                  const css = `<style>
              :root {
              ${cssVars}
              }
              
              /* 1. Weicher Übergang für den Haupt-Hintergrund von vis-2 */
              body, html, #root, #vis_container, .vis-view, .app-container {
                  background-image: var(--m3-bg-image) !important;
                  background-size: cover !important;
                  background-position: center center !important;
                  background-attachment: fixed !important;
                  background-repeat: no-repeat !important;
                  background-color: var(--m3-surface-container-lowest) !important; 
                  transition: background-color 1.5s ease-in-out, color 1.5s ease-in-out !important;
              }
              
              /* 2. Sanfter Fade für Dashboard-Elemente */
              .vis-view *, .app-container * {
                  transition-property: background-color, background, color, border-color, fill;
                  transition-duration: 1.0s;
                  transition-timing-function: ease-in-out;
              }
              </style>`;
              
                  setState(DP_THEME_CSS, css, true);
              }
              
              // === ASTRO-LOGIK ===
              function checkAstro() {
                  if (getState(DP_AUTO_MODE).val === true) {
                      let elevation = 0;
                      
                      if (DP_SUN_ELEVATION && existsState(DP_SUN_ELEVATION)) {
                          let rawVal = getState(DP_SUN_ELEVATION).val;
                          if (typeof rawVal === 'string') rawVal = rawVal.replace(',', '.');
                          elevation = parseFloat(rawVal);
                      } else {
                          // Fallback auf ungenauere ioBroker Standard-Funktion
                          elevation = isAstroDay() ? 10 : -10; 
                      }
              
                      if (isNaN(elevation)) {
                          log('[M3 Theme] Fehler: Sonnenstand konnte nicht berechnet werden.', 'warn');
                          elevation = isAstroDay() ? 10 : -10;
                      }
                      
                      // Dunkel ab -3 Grad
                      const shouldBeDark = elevation < -3;
                      
                      if (getState(DP_DARK_MODE).val !== shouldBeDark) {
                          setState(DP_DARK_MODE, shouldBeDark);
                          log(`[M3 Theme] Auto-Astro Umschaltung: Dark Mode ist jetzt ${shouldBeDark}`);
                      }
                  }
              }
              
              // === INIT & SUBSCRIPTIONS ===
              createState(DP_IMAGE_PATH, '', { type: 'string', name: 'M3 Wallpaper Pfad', role: 'text' }, () => {
                  createState(DP_USE_JSON, false, { type: 'boolean', name: 'M3 Use JSON Theme', role: 'switch' }, () => {
                      createState(DP_DARK_MODE, false, { type: 'boolean', name: 'M3 Dark Mode Toggle', role: 'switch' }, () => {
                          createState(DP_AUTO_MODE, false, { type: 'boolean', name: 'M3 Auto Theme (Astro)', role: 'switch' }, () => {
                              createState(DP_THEME_CSS, '', { type: 'string', name: 'M3 Theme CSS', role: 'html' }, () => {
                                  createState(DP_THEME_JSON, '', { type: 'string', name: 'JSON Import', role: 'text' }, () => {
                                      
                                      on({ id: [DP_DARK_MODE, DP_THEME_JSON, DP_IMAGE_PATH, DP_USE_JSON], change: 'ne' }, () => updateTheme());
                                      on({ id: DP_AUTO_MODE, change: 'any' }, () => checkAstro());
              
                                      if (DP_SUN_ELEVATION && existsState(DP_SUN_ELEVATION)) {
                                          on({ id: DP_SUN_ELEVATION, change: 'ne' }, () => checkAstro());
                                      }
              
                                      checkAstro();
                                      updateTheme();
                                  });
                              });
                          });
                      });
                  });
              });
              

              ioBroker auf NUC (Celeron mit Ubuntu-Server)

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

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

                Hier mein "Favoriten-Skript" mit den bei mir am häufigsten genutzten Funktionen.

                Was macht das Skript?

                Dieses Skript generiert ein interaktives Widget für ioBroker vis (oder vis-2), das im aktuellen Material 3 (Material You) Design gehalten ist. Es erzeugt eine Kachel-Ansicht (Grid), in der ihr eure wichtigsten Geräte ("Favoriten") als Schalter (Toggles) oder als Schieberegler (Slider) hinterlegen könnt.

                Die Besonderheiten:

                • Theme-Engine Ready: Das Skript nutzt CSS-Variablen. Wenn ihr meinen "M3 Theme Master" (siehe anderen Thread) nutzt, passen sich die Farben dieser Kacheln live und vollautomatisch an euer Hintergrundbild oder den Dark-Mode an!
                • Touch-Optimiert: Eingebaute Wisch-Gesten für Slider und der typische "Ripple-Effekt" (Wasser-Welle) beim Antippen.
                • Keine Widgets nötig: Es wird reiner HTML-Code generiert, der extrem performant ist.
                • Momentary-Funktion: Unterstützt Taster (die nach dem Drücken wieder auf "aus" springen, z.B. für Staubsauger-Routinen).

                Was muss vorbereitet werden?

                Eigentlich nichts Großes. Ihr müsst nur das Array devices im Skript mit euren eigenen ioBroker-Datenpunkten füllen. Es gibt zwei Typen: toggle (für normales An/Aus) und slider (für Dimmer oder Markisen). Im Skript sind ein paar Beispiele hinterlegt, die ihr anpassen könnt.

                Wie wendet man das Skript an?

                Das Skript legt den Datenpunkt 0_userdata.0.dashboard.favoritenHTML an.

                1. Zieht einfach ein "Basic - String" (oder "Basic - HTML") Widget in eure vis.
                2. Tragt unter HTML / Inhalt den Datenpunkt-Namen in geschweiften Klammern ein: {0_userdata.0.dashboard.favoritenHTML}
                3. Zieht das Widget breit genug, damit die Kacheln Platz haben (das Grid bricht automatisch um).

                Das Skript (TypeScript)

                // ============================================================
                // renderFavoriten — M3 Material You Styling (THEME ENGINE READY)
                // ioBroker TypeScript — rendert HTML in Datenpunkt für vis2
                // ============================================================
                
                // === KONFIGURATION: WO SOLL DAS HTML GESPEICHERT WERDEN? ===
                const DP_FAVORITEN = '0_userdata.0.dashboard.favoritenHTML';
                
                // === GRID-KONFIGURATION ===
                const COL_WIDTH = 202;  // Breite einer 1-Spalten-Kachel (in px)
                const GUTTER    = 15;   // Abstand zwischen den Kacheln (in px)
                
                // ============================================================
                // === AB HIER NUR NOCH ANPASSEN, WENN IHR WISST WAS IHR TUT ===
                // ============================================================
                
                // === M3 FARBEN (Dynamisch gekoppelt an den Theme-Master!) ===
                const M3 = {
                    primary:                'var(--m3-primary)',
                    onPrimary:              'var(--m3-on-primary)',
                    primaryContainer:       'var(--m3-primary-container)',
                    onPrimaryContainer:     'var(--m3-on-primary-container)',
                    surfaceContainerLow:    'var(--m3-surface-container-low)',
                    surfaceContainerHighest:'var(--m3-surface-container-highest)',
                    surfaceVariant:         'var(--m3-surface-variant)',
                    onSurface:              'var(--m3-on-surface)',
                    onSurfaceVariant:       'var(--m3-on-surface-variant)',
                    outlineVariant:         'var(--m3-outline-variant)'
                };
                
                // === SVG ICONS (Material Design Icons / MDI) ===
                // Sucht euch neue Icons auf materialdesignicons.com, kopiert den SVG-Pfad (d) und fügt ihn hier ein.
                const ICONS: Record<string, string> = {
                    'hygge':  'M15.5,9.63C15.31,6.84 14.18,4.12 12.06,2C9.92,4.14 8.74,6.86 8.5,9.63C9.79,10.31 10.97,11.19 12,12.26C13.03,11.2 14.21,10.32 15.5,9.63M12,15.45C9.85,12.17 6.18,10 2,10C2,20 11.32,21.89 12,22C12.68,21.88 22,20 22,10C17.82,10 14.15,12.17 12,15.45Z',
                    'garten': 'M12,2L6.36,10.61C6.07,11.06 6.4,11.66 6.94,11.66H9L6.11,16.5A0.75,0.75 0 0,0 6.75,17.66H11V21H13V17.66H17.25A0.75,0.75 0 0,0 17.89,16.5L15,11.66H17.06C17.6,11.66 17.93,11.06 17.64,10.61L12,2Z',
                    'vacuum': 'M12,2C14.65,2 17.19,3.06 19.07,4.93L17.65,6.35C16.15,4.85 14.12,4 12,4C9.88,4 7.84,4.84 6.35,6.35L4.93,4.93C6.81,3.06 9.35,2 12,2M3.66,6.5L5.11,7.94C4.39,9.17 4,10.57 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12C20,10.57 19.61,9.17 18.88,7.94L20.34,6.5C21.42,8.12 22,10.04 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12C2,10.04 2.58,8.12 3.66,6.5M12,6A6,6 0 0,1 18,12C18,13.59 17.37,15.12 16.24,16.24L14.83,14.83C14.08,15.58 13.06,16 12,16C10.94,16 9.92,15.58 9.17,14.83L7.76,16.24C6.63,15.12 6,13.59 6,12A6,6 0 0,1 12,6M12,8A1,1 0 0,0 11,9A1,1 0 0,0 12,10A1,1 0 0,0 13,9A1,1 0 0,0 12,8Z',
                    'dining': 'M8.1,13.34L3.91,9.16C2.35,7.59 2.35,5.06 3.91,3.5L10.93,10.5L8.1,13.34M14.88,11.53L13.41,13L20.29,19.88L18.88,21.29L12,14.41L5.12,21.29L3.71,19.88L13.47,10.12C12.76,8.59 13.26,6.44 14.85,4.85C16.76,2.93 19.5,2.57 20.96,4.03C22.43,5.5 22.07,8.24 20.15,10.15C18.56,11.74 16.41,12.24 14.88,11.53Z',
                    'awning': 'M2 4V6H4V4H2M6 4V6H8V4H6M10 4V6H12V4H10M14 4V6H16V4H14M18 4V6H20V4H18M2 8V10H22V8H2M4 12V20H6V12H4M18 12V20H20V12H18M8 14V20H16V14H8'
                };
                
                // === GERÄTE-DEFINITIONEN ===
                // Hier tragt ihr eure eigenen Geräte ein. 
                // "cols: 1" macht die Kachel quadratisch. "cols: 2" macht sie doppelt so breit (ideal für Slider).
                // "isMomentary" simuliert einen echten Tasterdruck (springt sofort auf aus zurück).
                
                interface ToggleDevice { type: 'toggle'; label: string; icon: string; oid: string; cols: number; isMomentary?: boolean; }
                interface SliderDevice { type: 'slider'; label: string; icon: string; oid: string; stateOid?: string; min: number; max: number; unit: string; step: number; cols: number; invert?: boolean; }
                
                type Device = ToggleDevice | SliderDevice;
                
                const devices: Device[] = [
                    { type: 'toggle', label: 'Hygge',         icon: 'hygge',  oid: 'scene.0.abendliches_Licht',            cols: 1, isMomentary: true },
                    { type: 'toggle', label: 'Garten',        icon: 'garten', oid: 'alias.0.Licht.Terrasse.STATE',         cols: 1 },
                    { type: 'toggle', label: 'Staubsauger',   icon: 'vacuum', oid: 'alias.0.Hausgeraete.Staubsauger.start',cols: 1, isMomentary: true },
                    { type: 'slider', label: 'Esstisch',      icon: 'dining', oid: 'alias.0.Licht.Essbereich.LEVEL', stateOid: 'alias.0.Licht.Essbereich.STATE', min: 0, max: 100, unit: '%', step: 1,  cols: 2 },
                    { type: 'slider', label: 'Markise',       icon: 'awning', oid: 'alias.0.Beschattung.Markise.LEVEL',    min: 0, max: 100, unit: '%', step: 10, cols: 2, invert:true },
                ];
                
                
                function colsToWidth(cols: number) {
                    return cols * COL_WIDTH + (cols - 1) * GUTTER;
                }
                
                function svgIcon(name: string, color: string, size: number = 24): string {
                    const path = ICONS[name] || ICONS['hygge'];
                    return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 24 24" style="flex-shrink:0;"><path fill="${color}" d="${path}"/></svg>`;
                }
                
                // === TOGGLE-CARD RENDERN ===
                function renderToggleCard(dev: ToggleDevice) {
                    const val = existsState(dev.oid) ? getState(dev.oid).val : false;
                    const active = dev.isMomentary ? false : (val === true || val === 1 || val === 'true');
                    const w = colsToWidth(dev.cols);
                
                    const bg     = active ? M3.primaryContainer   : M3.surfaceContainerLow; 
                    const fg     = active ? M3.onPrimaryContainer : M3.onSurfaceVariant;
                    const iconBg = active ? M3.primary            : M3.surfaceContainerHighest;
                    const iconFg = active ? M3.onPrimary          : M3.onSurfaceVariant;
                
                    const shadow = '0px 1px 2px 0px rgba(0,0,0,0.3), 0px 1px 3px 1px rgba(0,0,0,0.15)';
                
                    const clickAction = dev.isMomentary 
                        ? `vis.setValue('${dev.oid}', true)` 
                        : `vis.setValue('${dev.oid}', ${active ? 'false' : 'true'})`;
                
                    const rippleJS = `(function(e,el){var evt=e.touches?e.touches[0]:e;if(!evt.clientX)return;var d=Math.max(el.clientWidth,el.clientHeight);var r=d/2;var rect=el.getBoundingClientRect();var x=evt.clientX-rect.left;var y=evt.clientY-rect.top;var c=document.createElement('span');c.style.width=c.style.height=d+'px';c.style.left=(x-r)+'px';c.style.top=(y-r)+'px';c.style.position='absolute';c.style.borderRadius='50%';c.style.backgroundColor='${active ? M3.onPrimaryContainer : M3.onSurfaceVariant}';c.style.opacity='0.15';c.style.transform='scale(0)';c.style.animation='m3-ripple-anim 0.5s linear';c.style.pointerEvents='none';el.appendChild(c);setTimeout(function(){c.remove();},500);})(event,this)`;
                
                    return `
                <div onmousedown="${rippleJS}" ontouchstart="${rippleJS}" onclick="${clickAction}"
                  style="position:relative; overflow:hidden; width:${w}px; height:${COL_WIDTH}px; border-radius:12px; padding:16px; box-sizing:border-box;
                    background:${bg}; box-shadow: ${shadow}; border: none; cursor:pointer; user-select:none;
                    display:flex; flex-direction:column; justify-content:space-between;
                    font-family:Roboto,'Segoe UI',system-ui,sans-serif;
                    -webkit-tap-highlight-color:transparent; transition: all 0.3s ease;">
                  <div style="width:40px; height:40px; border-radius:20px; background:${iconBg}; display:flex; align-items:center; justify-content:center; transition: all 0.3s ease;">
                    ${svgIcon(dev.icon, iconFg, 24)}
                  </div>
                  <div>
                    <div style="font-size:20px; font-weight:500; color:${fg}; line-height:1.3; transition: color 0.3s ease;">${dev.label}</div>
                    <div style="font-size:14px; font-weight:500; color:${fg}; opacity:0.7; margin-top:2px; transition: color 0.3s ease;">
                      ${dev.isMomentary ? 'Aktivieren' : (active ? 'An' : 'Aus')}
                    </div>
                  </div>
                </div>`;
                }
                
                // === SLIDER-CARD RENDERN ===
                function renderSliderCard(dev: SliderDevice) {
                    let rawVal = existsState(dev.oid) ? Number(getState(dev.oid).val) : 0;
                    
                    // Optional: Falls eine Lampe ausgeschaltet wurde (über den stateOid), zwinge den Slider optisch auf 0
                    if (dev.stateOid && existsState(dev.stateOid) && getState(dev.stateOid).val === false) {
                        rawVal = 0;
                    }
                
                    const isInv = dev.invert ? true : false;
                    const val = isInv ? (dev.max - rawVal + dev.min) : rawVal;
                    const pct = ((val - dev.min) / (dev.max - dev.min)) * 100;
                    const active = pct > 0;
                    const w = colsToWidth(dev.cols);
                
                    const mixPct = Math.round(20 + (pct * 0.8));
                    const bg     = active ? `color-mix(in srgb, ${M3.primaryContainer} ${mixPct}%, ${M3.surfaceContainerLow})` : M3.surfaceContainerLow; 
                    const fg     = active ? M3.onPrimaryContainer : M3.onSurfaceVariant;
                    const iconBg = active ? M3.primary            : M3.surfaceContainerHighest;
                    const iconFg = active ? M3.onPrimary          : M3.onSurfaceVariant;
                
                    const shadow = '0px 1px 2px 0px rgba(0,0,0,0.3), 0px 1px 3px 1px rgba(0,0,0,0.15)';
                
                    const cardToggleJS = `(function(e){
                        var isOn = ${active ? 'true' : 'false'};
                        var oid = '${dev.oid}';
                        var stateOid = '${dev.stateOid || ''}';
                        
                        if (typeof vis !== 'undefined' && vis.conn && vis.conn.setState) {
                            if (stateOid) vis.conn.setState(stateOid, !isOn, false);
                            else vis.conn.setState(oid, isOn ? 0 : 100, false); 
                        } else if (typeof vis !== 'undefined' && vis.setValue) {
                            if (stateOid) vis.setValue(stateOid, !isOn);
                            else vis.setValue(oid, isOn ? 0 : 100);
                        }
                    })(event);`.replace(/\n/g, ' ');
                
                    const rippleJS = `(function(e,el){var evt=e.touches?e.touches[0]:e;if(!evt.clientX)return;var d=Math.max(el.clientWidth,el.clientHeight);var r=d/2;var rect=el.getBoundingClientRect();var x=evt.clientX-rect.left;var y=evt.clientY-rect.top;var c=document.createElement('span');c.style.width=c.style.height=d+'px';c.style.left=(x-r)+'px';c.style.top=(y-r)+'px';c.style.position='absolute';c.style.borderRadius='50%';c.style.backgroundColor='${active ? M3.onPrimaryContainer : M3.onSurfaceVariant}';c.style.opacity='0.15';c.style.transform='scale(0)';c.style.animation='m3-ripple-anim 0.5s linear';c.style.pointerEvents='none';el.appendChild(c);setTimeout(function(){c.remove();},500);})(event,this)`;
                
                    const sliderJS = `(function(e, track, oid, min, max, step, unit, invert, stateOid) {
                        var currentVal = 0; var headerValObj = track.parentElement.querySelector('.val-text'); var cardObj = track.parentElement;
                        function move(ev) {
                            var evt = ev.touches ? ev.touches[0] : ev;
                            var rect = track.getBoundingClientRect();
                            var x = evt.clientX - rect.left;
                            var pctRaw = Math.max(0, Math.min(1, x / rect.width));
                            currentVal = min + pctRaw * (max - min);
                            if (step) currentVal = Math.round(currentVal / step) * step;
                            currentVal = Math.max(min, Math.min(max, currentVal));
                            var p = ((currentVal - min) / (max - min) * 100).toFixed(1);
                            
                            track.children[0].style.width = 'calc(' + p + '% - 4px)';
                            track.children[1].style.left  = 'calc(' + p + '% - 2px)';
                            track.children[2].style.left  = 'calc(' + p + '% - 6px)';
                            track.children[3].style.left  = 'calc(' + p + '% + 2px)';
                            track.children[4].style.left  = 'calc(' + p + '% + 6px)';
                            if (headerValObj) headerValObj.innerText = currentVal + unit;
                            
                            var mixP = Math.round(20 + (p * 0.8));
                            var newBg = currentVal > 0 ? 'color-mix(in srgb, var(--m3-primary-container) ' + mixP + '%, var(--m3-surface-container-low))' : 'var(--m3-surface-container-low)';
                            if (cardObj) cardObj.style.background = newBg;
                            track.children[2].style.background = newBg; track.children[3].style.background = newBg;
                        }
                        function up(ev) {
                            document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up);
                            document.removeEventListener('touchmove', move); document.removeEventListener('touchend', up);
                            var sendVal = invert ? (max - currentVal + min) : currentVal;
                            var isOn = sendVal > 0 ? true : false;
                            
                            if (typeof vis !== 'undefined' && vis.conn && vis.conn.setState) {
                                vis.conn.setState(oid, sendVal, false); 
                                if (stateOid && stateOid !== 'undefined') vis.conn.setState(stateOid, isOn, false);
                            } else if (typeof vis !== 'undefined' && vis.setValue) {
                                vis.setValue(oid, sendVal);
                                if (stateOid && stateOid !== 'undefined') vis.setValue(stateOid, isOn);
                            }
                        }
                        document.addEventListener('mousemove', move); document.addEventListener('mouseup', up);
                        document.addEventListener('touchmove', move, {passive: false}); document.addEventListener('touchend', up);
                        move(e);
                    })(event, this, '${dev.oid}', ${dev.min}, ${dev.max}, ${dev.step || 1}, '${dev.unit}', ${isInv}, '${dev.stateOid}')`;
                
                    return `
                <div onclick="${cardToggleJS}" onmousedown="${rippleJS}" ontouchstart="${rippleJS}" 
                  style="position:relative; overflow:hidden; cursor:pointer; width:${w}px; height:${COL_WIDTH}px; border-radius:12px; padding:16px; box-sizing:border-box;
                  background:${bg}; box-shadow: ${shadow}; border: none; font-family:Roboto,'Segoe UI',system-ui,sans-serif;
                  display:flex; flex-direction:column; justify-content:space-between; user-select:none; transition: background-color 0.3s ease;">
                
                  <div style="display:flex; align-items:center; gap:12px;">
                    <div style="width:40px; height:40px; border-radius:20px; background:${iconBg}; display:flex; align-items:center; justify-content:center; flex-shrink:0; transition: all 0.3s ease;">
                      ${svgIcon(dev.icon, iconFg, 24)}
                    </div>
                    <div style="flex:1;">
                      <div style="font-size:20px; font-weight:500; color:${fg}; line-height:1.2; transition: color 0.3s ease;">${dev.label}</div>
                    </div>
                    <div class="val-text" style="font-size:16px; font-weight:600; color:${fg}; transition: color 0.3s ease;">${val}${dev.unit}</div>
                  </div>
                
                  <div style="position:relative; height:44px; display:flex; align-items:center; touch-action:none; cursor:grab;"
                    onclick="event.stopPropagation();" onmousedown="event.stopPropagation(); ${sliderJS.replace(/\n/g, ' ')}" ontouchstart="event.stopPropagation(); ${sliderJS.replace(/\n/g, ' ')}">
                
                    <div style="position:absolute; left:0; width:calc(${pct}% - 4px); height:16px; border-radius:8px; background:${M3.primary}; top:50%; transform:translateY(-50%); pointer-events:none; transition: background-color 0.3s ease;"></div>
                    <div style="position:absolute; left:calc(${pct}% - 2px); width:4px; height:44px; border-radius:2px; background:${M3.primary}; top:50%; margin-top:-22px; z-index:10; pointer-events:none; transition: background-color 0.3s ease;"></div>
                    <div style="position:absolute; left:calc(${pct}% - 6px); width:4px; height:16px; background:${bg}; top:50%; transform:translateY(-50%); pointer-events:none; transition: background-color 0.3s ease;"></div>
                    <div style="position:absolute; left:calc(${pct}% + 2px); width:4px; height:16px; background:${bg}; top:50%; transform:translateY(-50%); pointer-events:none; transition: background-color 0.3s ease;"></div>
                    <div style="position:absolute; left:calc(${pct}% + 6px); right:0; height:16px; border-radius:8px; background:${M3.surfaceVariant}; top:50%; transform:translateY(-50%); pointer-events:none; transition: background-color 0.3s ease;"></div>
                
                  </div>
                </div>`;
                }
                
                // === HAUPT-RENDER ===
                function renderFavoriten() {
                    const cards = devices.map(dev => {
                        if (dev.type === 'toggle') return renderToggleCard(dev);
                        if (dev.type === 'slider') return renderSliderCard(dev);
                        return '';
                    }).join('');
                
                    const html = `
                <style>
                  @keyframes m3-ripple-anim { to { transform: scale(4); opacity: 0; } }
                </style>
                
                <div style="background: var(--m3-surface-container-low, rgb(237, 239, 232)); border-radius: 0 0 28px 28px; padding: 24px; display: flex; flex-wrap: wrap; gap: ${GUTTER}px; width: 100%; max-width: 901px; box-sizing: border-box; font-family: Roboto, 'Segoe UI', system-ui, sans-serif; transition: background-color 0.3s ease;">
                  ${cards}
                </div>`;
                
                    setState(DP_FAVORITEN, html, true);
                }
                
                // === SUBSCRIPTIONS & INIT ===
                createState(DP_FAVORITEN, '', { type: 'string', name: 'Favoriten HTML (M3)', role: 'html' }, () => {
                    
                    // Abos einrichten
                    devices.forEach(dev => {
                        if (existsState(dev.oid)) on({ id: dev.oid, change: 'any' }, () => renderFavoriten());
                        if (dev.type === 'slider' && dev.stateOid && existsState(dev.stateOid)) {
                            on({ id: dev.stateOid, change: 'any' }, () => renderFavoriten());
                        }
                    });
                
                    renderFavoriten();
                    log('[Favoriten] M3 Renderer gestartet.');
                });
                
                

                Favoriten.jpg

                ioBroker auf NUC (Celeron mit Ubuntu-Server)

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

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

                  Hier mein Navigations-Widget

                  Was macht dieses Widget?

                  Dieser HTML-Code erzeugt eine vollwertige, interaktive Navigationsleiste im Material 3 Design. Sie besteht aus 5 Tabs (Favoriten, Licht, Heizung, Beschattung und Setup).
                  Das Besondere: Die Leiste nutzt ioBroker-Bindings. Klickt man auf einen Tab, schreibt das Widget den entsprechenden Wert (0 bis 4) in einen Datenpunkt. Gleichzeitig reagiert die Leiste sofort visuell darauf: Der aktive Tab erhält einen pillenförmigen Hintergrund (M3-typisch in der tertiary-Farbe), der Text wird fett gedruckt und das Icon wechselt weich animiert von der "Outlined" (Umriss) zur "Filled" (Ausgefüllt) Variante.

                  Was muss vorbereitet werden?

                  1. Der Datenpunkt: Ihr benötigt einen Datenpunkt vom Typ Zahl (Number), in dem der aktuelle Tab gespeichert wird. Im Code wird standardmäßig 0_userdata.0.dashboard.active_tab verwendet. Legt diesen Datenpunkt also einfach im Objektbaum an.
                  2. Die Farben: Das Widget greift auf die CSS-Variablen aus meinem M3 Theme Master Skript zu (z.B. var(--m3-surface-container-low) oder var(--m3-tertiary-container)). Wenn ihr das Theme-Skript am Laufen habt, passt sich die Leiste vollautomatisch an Tag/Nacht oder euer Wallpaper an.

                  Wie wendet man das an?

                  1. Zieht euch ein einfaches "Basic - HTML" Widget in eure vis.
                  2. Kopiert den unten stehenden Code komplett in das HTML-Inhaltsfeld des Widgets.
                  3. Zieht das Widget am besten an den unteren oder oberen Rand eures Dashboards und zieht es über die volle Breite.
                  4. Optional: Wenn ihr euren Datenpunkt anders nennen wollt als 0_userdata.0.dashboard.active_tab, kopiert den Code vorher kurz in einen Texteditor (wie Notepad, Word oder VS Code) und nutzt die Funktion "Suchen und Ersetzen", um den Pfad überall in einem Rutsch auszutauschen.

                  Tipp für eure Views: Auf euren eigentlichen Kacheln, Graphen oder anderen Widgets nutzt ihr nun einfach die Sichtbarkeits-Bedingung in der vis (Sichtbarkeit -> Objekt-ID: 0_userdata.0.dashboard.active_tab -> Bedingung: == 1), damit z.B. eure Lampen nur dann auftauchen, wenn in der Navigation der "Licht"-Tab aktiv ist.


                  Der HTML-Code

                  <div class="m3-widget" style="width:100%; height:100%; display:flex; align-items:stretch; background:var(--m3-surface-container-low); border-radius:28px 28px 0 0; font-family:Roboto,'Segoe UI',system-ui,sans-serif; transition: background-color 0.3s ease;">
                    
                    <div onclick="vis.setValue('0_userdata.0.dashboard.active_tab', 0)" style="flex:1; display:flex; align-items:center; justify-content:center; cursor:pointer; user-select:none; -webkit-tap-highlight-color:transparent;">
                      <div style="position:relative; display:flex; align-items:center; justify-content:center; gap:10px; height:48px; padding:0 24px; min-width:80px;">
                        <div style="position:absolute; inset:0; border-radius:24px; transition: background-color 0.3s ease; background:{a:0_userdata.0.dashboard.active_tab; a === 0 ? 'var(--m3-tertiary-container)' : 'transparent'};"></div>
                        <svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 24 24" style="position:relative; z-index:1; flex-shrink:0;">
                          <path style="transition: fill 0.3s ease;" fill="{a:0_userdata.0.dashboard.active_tab; a === 0 ? 'var(--m3-on-tertiary-container)' : 'transparent'}" d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2L9.19 8.63L2 9.24l5.46 4.73L5.82 21z"/>
                          <path style="transition: fill 0.3s ease;" fill="{a:0_userdata.0.dashboard.active_tab; a === 0 ? 'transparent' : 'var(--m3-on-surface-variant)'}" d="M22 9.24l-7.19-.62L12 2L9.19 8.63L2 9.24l5.46 4.73L5.82 21L12 17.27L18.18 21l-1.64-7.03L22 9.24zM12 15.4l-3.76 2.27l1-4.28l-3.32-2.88l4.38-.38L12 6.1l1.71 4.04l4.38.38l-3.32 2.88l1 4.28L12 15.4z"/>
                        </svg>
                        <span style="position:relative; z-index:1; font-size:18px; letter-spacing:0.1px; white-space:nowrap; transition: color 0.3s ease; font-weight:{a:0_userdata.0.dashboard.active_tab; a === 0 ? '600' : '500'}; color:{a:0_userdata.0.dashboard.active_tab; a === 0 ? 'var(--m3-on-tertiary-container)' : 'var(--m3-on-surface-variant)'};">Favoriten</span>
                      </div>
                    </div>
                  
                    <div onclick="vis.setValue('0_userdata.0.dashboard.active_tab', 1)" style="flex:1; display:flex; align-items:center; justify-content:center; cursor:pointer; user-select:none; -webkit-tap-highlight-color:transparent;">
                      <div style="position:relative; display:flex; align-items:center; justify-content:center; gap:10px; height:48px; padding:0 24px; min-width:80px;">
                        <div style="position:absolute; inset:0; border-radius:24px; transition: background-color 0.3s ease; background:{a:0_userdata.0.dashboard.active_tab; a === 1 ? 'var(--m3-tertiary-container)' : 'transparent'};"></div>
                        <svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 24 24" style="position:relative; z-index:1; flex-shrink:0;">
                          <path style="transition: fill 0.3s ease;" fill="{a:0_userdata.0.dashboard.active_tab; a === 1 ? 'var(--m3-on-tertiary-container)' : 'transparent'}" d="M12 2C8.13 2 5 5.13 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74c0-3.87-3.13-7-7-7zM9 21c0 .55.45 1 1 1h4c.55 0 1-.45 1-1v-1H9v1z"/>
                          <path style="transition: fill 0.3s ease;" fill="{a:0_userdata.0.dashboard.active_tab; a === 1 ? 'transparent' : 'var(--m3-on-surface-variant)'}" d="M9 21c0 .55.45 1 1 1h4c.55 0 1-.45 1-1v-1H9v1zm3-19C8.14 2 5 5.14 5 9c0 2.38 1.19 4.47 3 5.74V17c0 .55.45 1 1 1h6c.55 0 1-.45 1-1v-2.26c1.81-1.27 3-3.36 3-5.74c0-3.86-3.14-7-7-7zm2.85 11.1l-.85.6V16h-4v-2.3l-.85-.6A4.997 4.997 0 0 1 7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 1.63-.8 3.16-2.15 4.1z"/>
                        </svg>
                        <span style="position:relative; z-index:1; font-size:18px; letter-spacing:0.1px; white-space:nowrap; transition: color 0.3s ease; font-weight:{a:0_userdata.0.dashboard.active_tab; a === 1 ? '600' : '500'}; color:{a:0_userdata.0.dashboard.active_tab; a === 1 ? 'var(--m3-on-tertiary-container)' : 'var(--m3-on-surface-variant)'};">Licht</span>
                      </div>
                    </div>
                  
                    <div onclick="vis.setValue('0_userdata.0.dashboard.active_tab', 2)" style="flex:1; display:flex; align-items:center; justify-content:center; cursor:pointer; user-select:none; -webkit-tap-highlight-color:transparent;">
                      <div style="position:relative; display:flex; align-items:center; justify-content:center; gap:10px; height:48px; padding:0 24px; min-width:80px;">
                        <div style="position:absolute; inset:0; border-radius:24px; transition: background-color 0.3s ease; background:{a:0_userdata.0.dashboard.active_tab; a === 2 ? 'var(--m3-tertiary-container)' : 'transparent'};"></div>
                        <svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 24 24" style="position:relative; z-index:1; flex-shrink:0;">
                          <path style="transition: fill 0.3s ease;" fill="{a:0_userdata.0.dashboard.active_tab; a === 2 ? 'var(--m3-on-tertiary-container)' : 'transparent'}" d="M15 13V5a3 3 0 0 0-6 0v8a5 5 0 1 0 6 0m-3-9a1 1 0 0 1 1 1v3h-2V5a1 1 0 0 1 1-1"/>
                          <path style="transition: fill 0.3s ease;" fill="{a:0_userdata.0.dashboard.active_tab; a === 2 ? 'transparent' : 'var(--m3-on-surface-variant)'}" d="M15 13V5a3 3 0 0 0-6 0v8a5 5 0 1 0 6 0m-3 7a3 3 0 0 1-1.79-5.4l.79-.58V5a1 1 0 0 1 2 0v9.02l.79.58A3 3 0 0 1 12 20m1-15a1 1 0 0 0-2 0v3h2V5"/>
                        </svg>
                        <span style="position:relative; z-index:1; font-size:18px; letter-spacing:0.1px; white-space:nowrap; transition: color 0.3s ease; font-weight:{a:0_userdata.0.dashboard.active_tab; a === 2 ? '600' : '500'}; color:{a:0_userdata.0.dashboard.active_tab; a === 2 ? 'var(--m3-on-tertiary-container)' : 'var(--m3-on-surface-variant)'};">Heizung</span>
                      </div>
                    </div>
                  
                    <div onclick="vis.setValue('0_userdata.0.dashboard.active_tab', 3)" style="flex:1; display:flex; align-items:center; justify-content:center; cursor:pointer; user-select:none; -webkit-tap-highlight-color:transparent;">
                      <div style="position:relative; display:flex; align-items:center; justify-content:center; gap:10px; height:48px; padding:0 24px; min-width:80px;">
                        <div style="position:absolute; inset:0; border-radius:24px; transition: background-color 0.3s ease; background:{a:0_userdata.0.dashboard.active_tab; a === 3 ? 'var(--m3-tertiary-container)' : 'transparent'};"></div>
                        <svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 24 24" style="position:relative; z-index:1; flex-shrink:0;">
                          <path style="transition: fill 0.3s ease;" fill="{a:0_userdata.0.dashboard.active_tab; a === 3 ? 'var(--m3-on-tertiary-container)' : 'transparent'}" d="M20 19V3H4v16H2v2h20v-2h-2zM6 5h12v2H6V5zm0 4h12v2H6V9zm0 4h12v2H6v-2zm0 4h5v2H6v-2z"/>
                          <path style="transition: fill 0.3s ease;" fill="{a:0_userdata.0.dashboard.active_tab; a === 3 ? 'transparent' : 'var(--m3-on-surface-variant)'}" d="M20 19V3H4v16H2v2h20v-2h-2zM18 5v2H6V5h12zM6 9h12v2H6V9zm0 4h12v2H6v-2zm0 6v-2h5v2H6z"/>
                        </svg>
                        <span style="position:relative; z-index:1; font-size:18px; letter-spacing:0.1px; white-space:nowrap; transition: color 0.3s ease; font-weight:{a:0_userdata.0.dashboard.active_tab; a === 3 ? '600' : '500'}; color:{a:0_userdata.0.dashboard.active_tab; a === 3 ? 'var(--m3-on-tertiary-container)' : 'var(--m3-on-surface-variant)'};">Beschattung</span>
                      </div>
                    </div>
                  
                    <div style="flex:0 0 1px; height:32px; align-self:center; background:var(--m3-outline-variant); opacity:0.5; transition: background-color 0.3s ease;"></div>
                  
                    <div onclick="vis.setValue('0_userdata.0.dashboard.active_tab', 4)" style="flex:0 0 auto; display:flex; align-items:center; justify-content:center; padding:0 12px; cursor:pointer; user-select:none; -webkit-tap-highlight-color:transparent;">
                      <div style="position:relative; display:flex; align-items:center; justify-content:center; gap:10px; height:48px; padding:0 24px;">
                        <div style="position:absolute; inset:0; border-radius:24px; transition: background-color 0.3s ease; background:{a:0_userdata.0.dashboard.active_tab; a === 4 ? 'var(--m3-tertiary-container)' : 'transparent'};"></div>
                        <svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 24 24" style="position:relative; z-index:1; flex-shrink:0;">
                          <path style="transition: fill 0.3s ease;" fill="{a:0_userdata.0.dashboard.active_tab; a === 4 ? 'var(--m3-on-tertiary-container)' : 'transparent'}" d="M19.14 12.94c.04-.3.06-.61.06-.94c0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.49.49 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96a.49.49 0 0 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6A3.6 3.6 0 1 1 12 8.4a3.6 3.6 0 0 1 0 7.2z"/>
                          <path style="transition: fill 0.3s ease;" fill="{a:0_userdata.0.dashboard.active_tab; a === 4 ? 'transparent' : 'var(--m3-on-surface-variant)'}" d="M19.14 12.94c.04-.3.06-.61.06-.94c0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.49.49 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96a.49.49 0 0 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6a3.6 3.6 0 1 1 0-7.2a3.6 3.6 0 0 1 0 7.2zm0-5.76a2.16 2.16 0 1 0 0 4.32a2.16 2.16 0 0 0 0-4.32z"/>
                        </svg>
                        <span style="position:relative; z-index:1; font-size:18px; letter-spacing:0.1px; white-space:nowrap; transition: color 0.3s ease; font-weight:{a:0_userdata.0.dashboard.active_tab; a === 4 ? '600' : '500'}; color:{a:0_userdata.0.dashboard.active_tab; a === 4 ? 'var(--m3-on-tertiary-container)' : 'var(--m3-on-surface-variant)'};">Setup</span>
                      </div>
                    </div>
                  
                  </div>
                  
                  

                  Navigtation_Tab.jpg

                  ioBroker auf NUC (Celeron mit Ubuntu-Server)

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

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

                    Das Alert-Script:

                    Was macht dieses Skript?

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

                    Die Features:

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

                    Wie nutze ich das Skript?

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

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

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

                    Beispiele:

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

                    Das Skript (TypeScript)

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

                    alerts.jpg

                    ioBroker auf NUC (Celeron mit Ubuntu-Server)

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

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

                      Hier die Einstellungen:

                      Was macht dieses Skript?

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

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

                      Was muss vorbereitet werden?

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

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

                      Wie wendet man das an?

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

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

                      Das Skript (TypeScript)

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

                      setup2.jpg setup1.jpg

                      ioBroker auf NUC (Celeron mit Ubuntu-Server)

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

                      1 Antwort Letzte Antwort
                      0
                      Antworten
                      • In einem neuen Thema antworten
                      Anmelden zum Antworten
                      • Älteste zuerst
                      • Neuste zuerst
                      • Meiste Stimmen


                      Support us

                      ioBroker
                      Community Adapters
                      Donate

                      287

                      Online

                      32.7k

                      Benutzer

                      82.5k

                      Themen

                      1.3m

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

                      • Du hast noch kein Konto? Registrieren

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