FHEM Forum

FHEM => Automatisierung => DOIF => Thema gestartet von: cwagner am 19 Oktober 2026, 08:33:28

Titel: Grafische Darstellung und Bedienung unserer kontrollierten Wohnraumlüftung
Beitrag von: cwagner am 19 Oktober 2026, 08:33:28
Angeregt von Damians Flows in seiner Energiefluss-Grafik (hier (https://wiki.fhem.de/wiki/DOIF/uiTable_Anwendung#Visualisierung:_Energiefluss_als_Energie-Karte)) habe ich eine grafische Darstellung und Bedienung unserer kontrollierten Wohnraumlüftung gebastelt (mit KI-Unterstützung).

Die skalierbare `uiTable`-Karte zeigt die Temperaturen von Frischluft, Zuluft, Abluft und Fortluft im Raumluftgerät, die Stellwerte beider Lüfter sowie CO₂-Konzentration, Außentemperatur im Wechsel mit Luftfeuchtigkeit und die mittlere Ablufttemperatur des Vortags. Animierte, sich kreuzende Luftwege veranschaulichen die Strömungsrichtungen. Steht der zugehörige Steller für die Lüftergeschwindigkeit auf `0 %`, wird der Luftweg ausgegraut und die Animation abgeschaltet.

Ein Klick öffnet eine vergrößerte Popup-Ansicht. Nur dort erscheint am unteren Rand die Betriebsartenwahl `auto`, `max`, `min` und `AUS`, die den gewählten Wert in den Dummy `Lueftung` schreibt. Dieser wird von einem weiteren DOIF als Regler ausgelesen, der die Hersteller-Regelung ersetzt und eine CO2-Messung im Abluftstrom als Regelungsgröße realisiert.

defmod DI_KWL DOIF {}

attr DI_KWL event-on-change-reading .*
attr DI_KWL group KWL
attr DI_KWL room Aktuell,Debug
attr DI_KWL sortby 2
attr DI_KWL stateFormat [DI_Lueftung:state] [DI_EWT:state]
attr DI_KWL uiTable {\
$ATTRIBUTESFIRST = 1;;\
\
$TABLE = 'zoom: 0.8;; width: 360px;; height: 240px;; text-align: left;; vertical-align: top;; border-radius:0%;; position:relative;; background: linear-gradient(to bottom, rgb(40,40,40), rgb(60,60,60));; ';;\
\
package ui_Table;;\
\
## Transparente Klickflaeche ueber der gesamten Karte\
## Popup-Ziel an einer Stelle festlegen\
## Popup doppelt so gross wie die Dashboard-Karte anzeigen\
\
sub vent_popup_link {\
\
  my $ziel = "DI_KWL";;\
\
  my $link = ::DOIF_tablePopUp(\
    $ziel,\
    $ziel,\
    "WRG"\
  );;\
\
  return '<style>'\
    . '.ventPopupHit > a {'\
    . 'position:absolute;;'\
    . 'left:0;;top:0;;'\
    . 'width:100%;;height:100%;;'\
    . 'display:block;;'\
    . 'box-sizing:border-box;;'\
    . 'opacity:0;;'\
    . 'cursor:pointer;;'\
    . '}'\
\
    ## Nur die Ziel-Tabelle innerhalb eines Popup-Dialogs vergroessern\
    ## Dashboard-Zoom 0.8 wird im Popup durch 1.6 ersetzt\
    . '.ui-dialog table[uitabid="DOIF-' . $ziel . '"] {'\
    . 'zoom:1.6 !important;;'\
    . '}'\
\
    . '</style>'\
\
    . '<div class="ventPopupHit" '\
    . 'title="Vergrößerte Ansicht öffnen" '\
    . 'style="'\
    . 'position:absolute;;'\
    . 'left:0;;top:0;;'\
    . 'width:360px;;height:240px;;'\
    . 'z-index:20;;">'\
    . $link\
    . '</div>';;\
}\
\
## Numerische Werte lediglich fuer die Anzeige formatieren\
## Keine Numerikpruefung und keine Werteextraktion\
\
sub vent_number_text {\
  my ($value, $decimal) = @_;;\
\
  return sprintf("%." . $decimal . "f", $value);;\
}\
\
\
## Stufenloser Temperaturfarbverlauf\
## RGB-Interpolation zwischen den Stuetzpunkten\
## Unterhalb und oberhalb der Skala bleibt die Endfarbe erhalten\
\
sub vent_temp_color {\
  my ($temp) = @_;;\
\
  my @farben = (\
    [-5,  85,  35, 190],\
    [ 0,  20,  45, 180],\
    [ 8,   0, 115, 235],\
    [15,  90, 205, 255],\
    [18,  45, 195, 105],\
    [24, 115, 205,  45],\
    [27, 255, 165,  25],\
    [31, 245,  65,  20],\
    [35, 150,   0,  15]\
  );;\
\
  if ($temp <= $farben[0]->[0]) {\
    return sprintf(\
      "#%02x%02x%02x",\
      @{$farben[0]}[1,2,3]\
    );;\
  }\
\
  if ($temp >= $farben[-1]->[0]) {\
    return sprintf(\
      "#%02x%02x%02x",\
      @{$farben[-1]}[1,2,3]\
    );;\
  }\
\
  for (my $i = 0;; $i < $#farben;; $i++) {\
    my $unten = $farben[$i];;\
    my $oben  = $farben[$i + 1];;\
\
    next unless $temp >= $unten->[0]\
             && $temp <= $oben->[0];;\
\
    my $anteil = ($temp - $unten->[0])\
      / ($oben->[0] - $unten->[0]);;\
\
    my @rgb;;\
\
    for my $kanal (1..3) {\
      push @rgb, int(\
        $unten->[$kanal]\
        + $anteil * ($oben->[$kanal] - $unten->[$kanal])\
        + 0.5\
      );;\
    }\
\
    return sprintf("#%02x%02x%02x", @rgb);;\
  }\
}\
\
\
## Gemeinsamer Rahmen fuer alle rechteckigen Widgets\
## Einheitliche Breite 52 und Hoehe 35\
\
sub vent_box {\
  my ($content) = @_;;\
\
  return '<div style="'\
    . 'box-sizing:border-box;;'\
    . 'width:52px;;height:35px;;'\
    . 'border:1px solid rgba(255,255,255,0.25);;'\
    . 'border-radius:6px;;'\
    . 'background:#303030;;'\
    . 'display:flex;;'\
    . 'align-items:center;;'\
    . 'justify-content:center;;'\
    . 'font-family:Arial Narrow,Roboto Condensed,sans-serif;;'\
    . 'white-space:nowrap;;'\
    . 'color:silver;;">'\
    . $content\
    . '</div>';;\
}\
\
\
## Positionierung mit Gegenzoom\
## Alle rechteckigen Widgets verwenden denselben Faktor\
\
sub vent_box_position {\
  my ($left, $top, $origin, $content) = @_;;\
\
  return '<div style="'\
    . 'position:absolute;;'\
    . 'left:' . $left . 'px;;'\
    . 'top:' . $top . 'px;;'\
    . 'width:52px;;height:35px;;'\
    . 'transform:scale(1.5);;'\
    . 'transform-origin:' . $origin . ';;'\
    . 'z-index:4;;">'\
    . vent_box($content)\
    . '</div>';;\
}\
\
\
## Temperaturinhalt mit stufenlosem Farbverlauf\
\
sub vent_temp_content {\
  my ($label, $temp) = @_;;\
\
  my $anzeige = vent_number_text($temp, 1);;\
  my $farbe = vent_temp_color($temp);;\
\
  return '<div style="'\
    . 'display:flex;;'\
    . 'flex-direction:column;;'\
    . 'align-items:center;;'\
    . 'justify-content:center;;">'\
    . '<div style="font-size:10.4px;;line-height:12px;;">'\
    . $label\
    . '</div>'\
    . '<div style="font-size:15.6px;;line-height:18px;;'\
    . 'font-weight:bold;;color:' . $farbe . ';;">'\
    . $anzeige . '&nbsp;;&deg;;C'\
    . '</div></div>';;\
}\
\
\
## Fortluft links oben\
\
sub fortluft_widget {\
  my ($temp) = @_;;\
\
  return vent_box_position(\
    2, 2, "left top",\
    vent_temp_content("Fortluft", $temp)\
  );;\
}\
\
\
## Frischluft links unten\
\
sub frischluft_widget {\
  my ($temp) = @_;;\
\
  return vent_box_position(\
    2, 203, "left bottom",\
    vent_temp_content("Frischluft", $temp)\
  );;\
}\
\
\
## Zuluft rechts oben\
\
sub zuluft_widget {\
  my ($temp) = @_;;\
\
  return vent_box_position(\
    306, 2, "right top",\
    vent_temp_content("Zuluft", $temp)\
  );;\
}\
\
\
## Abluft rechts unten\
\
sub abluft_widget {\
  my ($temp) = @_;;\
\
  return vent_box_position(\
    306, 203, "right bottom",\
    vent_temp_content("Abluft", $temp)\
  );;\
}\
\
\
## Aussentemperatur und Feuchte abwechselnd anzeigen\
## Vorhandene CSS-Klassen humchange und tempchange verwenden\
## Gleicher Rahmen und Gegenzoom wie bei den anderen Widgets\
\
sub vent_outside_widget {\
  my ($temp, $humidity) = @_;;\
\
  my $t = vent_number_text($temp, 1);;\
  my $h = vent_number_text($humidity, 0);;\
  my $farbe = vent_temp_color($temp);;\
\
  my $content = '<div style="'\
    . 'position:relative;;'\
    . 'width:100%;;height:100%;;'\
    . 'text-align:center;;">';;\
\
  ## Wechselanzeige Luftfeuchte\
  $content .= '<div class="humchange" style="'\
    . 'position:absolute;;left:0;;top:0;;'\
    . 'width:100%;;height:100%;;">'\
    . '<div style="'\
    . 'font-size:10.4px;;line-height:12px;;margin-top:2px;;">'\
    . 'Feuchte'\
    . '</div>'\
    . '<div style="'\
    . 'font-size:15.6px;;line-height:18px;;'\
    . 'font-weight:bold;;color:red;;">'\
    . $h . '&nbsp;;%'\
    . '</div></div>';;\
\
  ## Wechselanzeige Aussentemperatur\
  $content .= '<div class="tempchange" style="'\
    . 'position:absolute;;left:0;;top:0;;'\
    . 'width:100%;;height:100%;;">'\
    . '<div style="'\
    . 'font-size:10.4px;;line-height:12px;;margin-top:2px;;">'\
    . 'AT'\
    . '</div>'\
    . '<div style="'\
    . 'font-size:15.6px;;line-height:18px;;'\
    . 'font-weight:bold;;color:' . $farbe . ';;">'\
    . $t . '&nbsp;;&deg;;C'\
    . '</div></div>';;\
\
  $content .= '</div>';;\
\
  return vent_box_position(\
    2, 127, "left top", $content\
  );;\
}\
\
## Temperaturmittelwert vom Vortag\
## Rechts oberhalb von Abluft\
\
sub vent_yesterday_widget {\
  my ($temp) = @_;;\
\
  return vent_box_position(\
    306, 127, "right top",\
    vent_temp_content("&Oslash;; gestern", $temp)\
  );;\
}\
\
\
## Kreuzende Luftstroeme\
## Abluft rechts unten nach Fortluft links oben\
## Frischluft links unten nach Zuluft rechts oben\
## Bei Stellerwert 0: jeweiliger Luftweg grau und ohne Animation\
\
sub kreuzwaerme {\
  my ($abluft_flow, $frischluft_flow) = @_;;\
\
  ## Eindeutige SVG-IDs, damit Dashboard und Popup nicht kollidieren\
  our $vent_svg_seq;;\
  $vent_svg_seq = 0 unless defined $vent_svg_seq;;\
  $vent_svg_seq++;;\
\
  my $uid = 'ventX_' . $vent_svg_seq;;\
\
  my $out = '<svg xmlns="http://www.w3.org/2000/svg" '\
    . 'viewBox="0 0 360 240" width="360" height="240" '\
    . 'style="position:absolute;;left:0;;top:0;;'\
    . 'width:360px;;height:240px;;'\
    . 'display:block;;pointer-events:none;;z-index:1;;">';;\
\
  ## Abluft: unten rot nach Fortluft oben blau\
  ## Frischluft: unten blau nach Zuluft oben rot\
  my @stroeme = (\
    [\
      "ab",\
      $abluft_flow,\
      "M 280 211.75 C 210 211.75 150 28.25 80 28.25",\
      "#ff453a",\
      "#2196f3"\
    ],\
    [\
      "frisch",\
      $frischluft_flow,\
      "M 80 211.75 C 150 211.75 210 28.25 280 28.25",\
      "#2196f3",\
      "#ff453a"\
    ]\
  );;\
\
  ## Gemeinsame Zeitbasis fuer beide Laufbalken\
  ## Ein kompletter Wechsel dauert acht Sekunden\
  ## Jeder Durchlauf dauert 3.6 Sekunden\
  ## Zwischen den Durchlaeufen liegen 0.4 Sekunden Pause\
  my $zyklus = 8;;\
  my $grau = '#808080';;\
\
  for my $strom (@stroeme) {\
    my ($id, $flow, $pfad, $startfarbe, $zielfarbe) = @$strom;;\
\
    ## Numerisch interpretieren;; leere oder ungueltige Werte gelten als 0\
    $flow = 0 unless defined $flow && $flow =~ /^-?\d+(?:\.\d+)?$/;;\
    $flow += 0;;\
\
    my $aktiv = $flow > 0;;\
\
    my $gradient_id = $uid . '_farbe_' . $id;;\
    my $pfeil_id    = $uid . '_pfeil_' . $id;;\
\
    my $linienfarbe = $aktiv\
      ? 'url(#' . $gradient_id . ')'\
      : $grau;;\
\
    my $pfeilfarbe = $aktiv ? $zielfarbe : $grau;;\
\
    $out .= '<defs>';;\
\
    ## Farbverlauf von unten nach oben\
    $out .= '<linearGradient id="' . $gradient_id . '" '\
      . 'gradientUnits="userSpaceOnUse" '\
      . 'x1="0" y1="211.75" x2="0" y2="28.25">'\
      . '<stop offset="0%" stop-color="' . $startfarbe . '"/>'\
      . '<stop offset="43%" stop-color="' . $startfarbe . '"/>'\
      . '<stop offset="57%" stop-color="' . $zielfarbe . '"/>'\
      . '<stop offset="100%" stop-color="' . $zielfarbe . '"/>'\
      . '</linearGradient>';;\
\
    ## Pfeilspitze passend zum Betriebszustand\
    $out .= '<marker id="' . $pfeil_id . '" '\
      . 'viewBox="0 0 12 12" refX="12" refY="6" '\
      . 'markerUnits="userSpaceOnUse" '\
      . 'markerWidth="10" markerHeight="10" orient="auto">'\
      . '<path d="M 0 0 L 12 6 L 0 12 Z" '\
      . 'fill="' . $pfeilfarbe . '"/>'\
      . '</marker>';;\
\
    $out .= '</defs>';;\
\
    ## Breiter Grundbalken\
    $out .= '<path d="' . $pfad . '" '\
      . 'fill="none" stroke="' . $linienfarbe . '" '\
      . 'stroke-width="8" stroke-opacity="0.35" '\
      . 'stroke-linecap="round"/>';;\
\
    ## Duennere Linie mit Pfeilspitze\
    $out .= '<path d="' . $pfad . '" '\
      . 'fill="none" stroke="' . $linienfarbe . '" '\
      . 'stroke-width="2" stroke-opacity="0.85" '\
      . 'marker-end="url(#' . $pfeil_id . ')"/>';;\
\
    ## Bei Stellerwert 0 keinen Laufbalken und keine Animation erzeugen\
    next unless $aktiv;;\
\
    ## Ein einzelner heller Laufbalken\
    ## Grosser Abstand verhindert weitere Balken im Pfad\
    $out .= '<path d="' . $pfad . '" pathLength="100" '\
      . 'fill="none" stroke="' . $linienfarbe . '" '\
      . 'stroke-width="8" stroke-linecap="round" '\
      . 'stroke-dasharray="18 200" '\
      . 'stroke-dashoffset="18">';;\
\
    if ($id eq "ab") {\
      ## Abluft in der ersten Zyklushaelfte\
      $out .= '<animate '\
        . 'attributeName="stroke-dashoffset" '\
        . 'values="18;;-100;;-100" '\
        . 'keyTimes="0;;0.45;;1" '\
        . 'dur="' . $zyklus . 's" begin="0s" '\
        . 'repeatCount="indefinite" calcMode="linear"/>';;\
\
      ## Nach dem Durchlauf unsichtbar\
      $out .= '<animate '\
        . 'attributeName="stroke-opacity" '\
        . 'values="1;;0;;0" '\
        . 'keyTimes="0;;0.45;;1" '\
        . 'dur="' . $zyklus . 's" begin="0s" '\
        . 'repeatCount="indefinite" calcMode="discrete"/>';;\
    } else {\
      ## Frischluft in der zweiten Zyklushaelfte\
      $out .= '<animate '\
        . 'attributeName="stroke-dashoffset" '\
        . 'values="18;;18;;-100;;-100" '\
        . 'keyTimes="0;;0.5;;0.95;;1" '\
        . 'dur="' . $zyklus . 's" begin="0s" '\
        . 'repeatCount="indefinite" calcMode="linear"/>';;\
\
      ## Nur waehrend des eigenen Durchlaufs sichtbar\
      $out .= '<animate '\
        . 'attributeName="stroke-opacity" '\
        . 'values="0;;1;;0;;0" '\
        . 'keyTimes="0;;0.5;;0.95;;1" '\
        . 'dur="' . $zyklus . 's" begin="0s" '\
        . 'repeatCount="indefinite" calcMode="discrete"/>';;\
    }\
\
    $out .= '</path>';;\
  }\
\
  $out .= '</svg>';;\
\
  ## Reine HTML-Rueckgabe;; uiTable aktualisiert das gesamte Widget bei Events\
  return $out;;\
}\
\
\
## Zentralwidget mit kompakter Zahlengruppe\
## Abluft und erster Wert um einen Pixel nach oben\
## Zuluft und zweiter Wert um einen Pixel nach unten\
## Mittelpunkt auf der Flow-Kreuzung bei 180 / 120\
\
sub vent_fan_circle {\
  my ($ab, $zu) = @_;;\
\
  my $ring = ring2(\
    $ab,\
    0,\
    100,\
    undef,\
    undef,\
    "Abluft",\
    120,\
    [(33.3,40,66.6,65,100,85)],\
    "0,,fill:silver, %",\
    $zu,\
    0,\
    100,\
    190,\
    170,\
    "Zuluft",\
    undef,\
    "0,,fill:silver, %",\
    undef,\
    undef,\
    "nogradient,,innerring,nopointer"\
  );;\
\
  my $html = ref($ring) eq "ARRAY"\
    ? $ring->[0]\
    : $ring;;\
\
  my $ab_text = sprintf("%.0f", $ab);;\
  my $zu_text = sprintf("%.0f", $zu);;\
\
  ## Ringgrafik auf feste Abmessungen setzen\
  ## Originaltexte werden separat neu angeordnet\
  my $out = '<style>'\
    . '.ventCentralRing svg {'\
    . 'width:110px !important;;'\
    . 'height:110px !important;;'\
    . 'display:block;;'\
    . '}'\
    . '.ventCentralRing svg text {'\
    . 'display:none !important;;'\
    . '}'\
    . '</style>';;\
\
  $out .= '<div style="'\
    . 'position:absolute;;'\
    . 'left:180px;;top:120px;;'\
    . 'transform:translate(-50%,-50%);;'\
    . 'width:110px;;height:110px;;'\
    . 'border-radius:50%;;'\
    . 'background:#303030;;'\
    . 'z-index:5;;">';;\
\
  ## Ringsegmente\
  $out .= '<div class="ventCentralRing" style="'\
    . 'position:absolute;;left:0;;top:0;;'\
    . 'width:110px;;height:110px;;">'\
    . $html\
    . '</div>';;\
\
  ## Gemeinsame Textebene\
  $out .= '<div style="'\
    . 'position:absolute;;inset:0;;'\
    . 'font-family:Arial Narrow,Roboto Condensed,sans-serif;;'\
    . 'text-align:center;;'\
    . 'white-space:nowrap;;'\
    . 'color:silver;;'\
    . 'pointer-events:none;;">';;\
\
  ## Abluft-Beschriftung\
  $out .= '<div style="'\
    . 'position:absolute;;left:0;;right:0;;top:18px;;'\
    . 'font-size:15.6px;;line-height:18px;;">'\
    . 'Abluft'\
    . '</div>';;\
\
  ## Abluft-Prozentwert\
  $out .= '<div style="'\
    . 'position:absolute;;left:0;;right:0;;top:35px;;'\
    . 'font-size:23.4px;;line-height:23.4px;;'\
    . 'font-weight:bold;;">'\
    . $ab_text\
    . '<span style="font-size:15.6px;;font-weight:normal;;">'\
    . '&nbsp;;%</span>'\
    . '</div>';;\
\
  ## Zuluft-Prozentwert\
  $out .= '<div style="'\
    . 'position:absolute;;left:0;;right:0;;top:55px;;'\
    . 'font-size:23.4px;;line-height:23.4px;;'\
    . 'font-weight:bold;;">'\
    . $zu_text\
    . '<span style="font-size:15.6px;;font-weight:normal;;">'\
    . '&nbsp;;%</span>'\
    . '</div>';;\
\
  ## Zuluft-Beschriftung\
  $out .= '<div style="'\
    . 'position:absolute;;left:0;;right:0;;top:77px;;'\
    . 'font-size:15.6px;;line-height:18px;;">'\
    . 'Zuluft'\
    . '</div>';;\
\
  $out .= '</div></div>';;\
\
  return ref($ring) eq "ARRAY"\
    ? [$out, $ring->[1]]\
    : $out;;\
}\
\
\
## CO2-Widget \
## Farbring als Ampel\
\
## CO2-Anzeige auf Basis des urspruenglichen icon_uring\
## Kein Icon\
## CO2 als Text oben\
## Messwert und Einheit ppm durch das Widget selbst\
\
sub vent_co2_widget {\
  my ($co2) = @_;;\
\
  my $widget = icon_uring(\
    "0,0,1,8",\
    "",\
    $co2,\
    600,\
    1300,\
    undef,\
    undef,\
    "ppm",\
    "0,fill:silver,fill:silver",\
    130,\
    [(800,120,1000,60,1300,0)]\
  );;\
\
  my $html = ref($widget) eq "ARRAY"\
    ? $widget->[0]\
    : $widget;;\
\
  ## Nur die aeussere Grafik skalieren\
  ## Keine SVG-Texte ausblenden\
  my $out = '<style>'\
    . '.ventCO2Top > svg {'\
    . 'width:60px !important;;'\
    . 'height:60px !important;;'\
    . 'display:block;;'\
    . '}'\
    . '</style>';;\
\
  $out .= '<div class="ventCO2Top" style="'\
    . 'position:absolute;;'\
    . 'left:180px;;top:2px;;'\
    . 'transform:translateX(-50%);;'\
    . 'width:60px;;height:60px;;'\
    . 'z-index:6;;">'\
    . $html;;\
\
  ## Text anstelle des bisherigen Icons\
  $out .= '<div style="'\
    . 'position:absolute;;'\
    . 'left:-1px;;right:1px;;top:13px;;'\
    . 'text-align:center;;'\
    . 'font-family:Arial Narrow,Roboto Condensed,sans-serif;;'\
    . 'font-size:10.4px;;line-height:12px;;'\
    . 'color:silver;;'\
    . 'pointer-events:none;;">'\
    . 'CO2'\
    . '</div>';;\
\
  $out .= '</div>';;\
\
  return ref($widget) eq "ARRAY"\
    ? [$out, $widget->[1]]\
    : $out;;\
}\
\
\
## Bedienleiste nur im Popup anzeigen\
## Mittig unten zwischen den beiden Temperaturwidgets\
## Oberhalb der transparenten Popup-Klickflaeche positionieren\
\
sub vent_popup_control_style {\
\
  return '<style>'\
\
    ## In der normalen Dashboard-Karte unsichtbar\
    . 'table[uitabid="DOIF-DI_KWL"] .ventPopupControl {'\
    . 'display:none;;'\
    . '}'\
\
    ## Nur innerhalb eines Popup-Dialogs einblenden\
    . '.ui-dialog table[uitabid="DOIF-DI_KWL"] .ventPopupControl {'\
    . 'display:flex;;'\
    . 'position:absolute;;'\
    . 'left:82px;;'\
    . 'bottom:4px;;'\
    . 'width:196px;;'\
    . 'min-height:28px;;'\
    . 'box-sizing:border-box;;'\
    . 'align-items:center;;'\
    . 'justify-content:center;;'\
    . 'padding:3px 2px;;'\
    . 'border:1px solid rgba(255,255,255,0.25);;'\
    . 'border-radius:6px;;'\
    . 'background:#303030;;'\
    . 'color:silver;;'\
    . 'font-family:Arial Narrow,Roboto Condensed,sans-serif;;'\
    . 'font-size:12px;;'\
    . 'white-space:nowrap;;'\
    . 'z-index:30;;'\
    . 'pointer-events:auto;;'\
    . '}'\
\
    ## Im bereits geoeffneten Popup keine weitere Popup-Klickflaeche\
    . '.ui-dialog table[uitabid="DOIF-DI_KWL"] .ventPopupHit {'\
    . 'display:none;;'\
    . '}'\
\
    . '</style>';;\
}\
\
}## Ende des Perlbereichs\
\
## Tabellendefinition\
## Jeden Aufruf in einer Zeile belassen\
## Flows direkt an die Prozentwerte der Steller koppeln\
## Bedienleiste nur im Popup sichtbar\
\
fortluft_widget([Fortluft:temperature:d1]).\
frischluft_widget([Frischluft:temperature:d1]).\
zuluft_widget([T.Zuluft2:temperature:d1]).\
abluft_widget([Abluft:temperature:d1]).\
kreuzwaerme([Steller_Abluft:Stufe],[Steller_Zuluft:Stufe]).\
vent_fan_circle([Steller_Abluft:Stufe],[Steller_Zuluft:Stufe]).\
vent_outside_widget([TF.Aussen:temperature:d1],[TF.Aussen:humidity:d0]).\
vent_yesterday_widget([Abluft:temperature_AVG_LD:d1]).\
vent_co2_widget([CO2Sensor:CO2:d]).\
vent_popup_control_style().\
"<div class='ventPopupControl'>".\
widget([Lueftung:state],"uzsuSelectRadio,auto,max,min,AUS","set").\
"</div>".\
vent_popup_link()
attr DI_KWL verbose 0

setstate DI_KWL auto EWT wärmt
setstate DI_KWL 2026-10-14 14:03:59 abluft_flow
setstate DI_KWL 2026-10-15 11:36:02 abluft_temp 19.4
setstate DI_KWL 2026-10-15 11:28:27 fortluft_temp 13.7
setstate DI_KWL 2026-10-14 14:03:59 frischluft_flow
setstate DI_KWL 2026-10-15 12:24:28 frischluft_temp 13.5
setstate DI_KWL 2026-10-18 00:27:08 mode enabled
setstate DI_KWL 2026-10-18 00:27:09 state initialized
setstate DI_KWL 2026-10-15 12:16:30 zuluft_temp


Freue mich auf Verbesserungsvorschläge und wenn's jemand anderen nützt oder auf eine Idee bringt.

Und ehe jemand Fake ruft: Vor dem Eintritt in das Raumluftgerät wird die Luft über einen Erdwärmetauscher im Winter angewärmt (daher der Unterschied zwischen AT und Frischluft) und im Sommer (bei umgangenen Wärmetaucher) gekühlt.

Christian