Grafische Darstellung und Bedienung unserer kontrollierten Wohnraumlüftung

Begonnen von cwagner, 19 Oktober 2026, 08:33:28

Vorheriges Thema - Nächstes Thema

cwagner

Angeregt von Damians Flows in seiner Energiefluss-Grafik (hier) 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
320 Devices in MariaDB: Lambda WP an Ecolink-Speicher, FBH, Solarthermie, kontroll. Lüftung mit WRG und Erdwärmetauscher
HMCUL, 1-Wire (FT232RL ; DS2480B), EnOcean (TCM ESP3), MQTT2, Shelly, SMA (eC2, PV, HM, HS)
DOIF (Perl/uiTable), PID20,Micropelt IRTV, SolarForecast auf PI5