Bei einem Kunden ist seit Jahren ein Lüftungsgerät AP310 im Einsatz – Tag und Nacht auf der selben Stufe, unabhängig von der Nutzung des belüfteten Raums. Das geht besser!
Im Zuge der Suche nach Energiesparpotenzialen kam dieses Lüftungsgerät mit Wärmerückgewinnung in den Blick. Es verbraucht zwar nur begrenzt elektrische Energie (etwa 18W auf niedrigster, 160W auf höchster Stufe), deutlich höher ist dagegen der Energieverlust, wenn unnötig Luft aus dem geheizten Raum “hinausgelüftet” wird.
Das Potenzial des Geräts lag durch die fixe Wahl einer Lüftungsstufe jahrelang brach – dass ein Feuchtefühler integriert ist, der bereits geräteintern automatisch die Lüfterstufe regeln kann war ebenso wenig bekannt wie die Tatsache, dass das Gerät via Ethernet Modbus/TCP spricht und auf diesem Weg noch eine weit feinere Steuerung erlaubt.
Im ersten Schritt wurde also die geräteeigene Automatik aktiviert, das war aber sehr unbefriedigend. Im Gerät ist ein einziger Ziel-Luftfeuchtewert hinterlegt, in unserem Fall 45% RH. Wird der unterschritten, schalten die Lüfter auf die erste Drehzahlstufe, andernfalls auf die dritte. Das ist sehr grob, und an Tagen mit hoher Außen-Luftfeuchte lief das Gerät stundenlang mit hoher Leistung, obwohl die Luft innen dadurch nicht trockener wurde.
Pluggit stellt zum Glück eine umfangreiche Doku der Modbus/TCP-Parameter bereit. Meine Versuche, den Zielwert per Modbus/TCP zu schreiben, scheiterte aber. Der Wert lässt sich nur lokal am Gerät per USB und der Windows-Software iFlow ändern. Dafür ist ein Servicetechniker-Passwort nötig, das ich hier nicht preisgeben möchte. Der Hinweis, dass das Passwort vier Ziffern umfasst und lächerlich einfach ist, soll genügen. Mit dieser Regelung wollte ich mich aber nicht zufriedengeben.
Daher habe ich in IOBroker begonnen, Werte einzulesen und zunächst einige Tage zu beobachten. Hier zunächst die Grundkonfiguration des Modbus-Adapters:

Hier die Holding-Register, mit denen ich mich befasst habe:

So sieht das dann im Objekte-Baum aus:

In Grafana sehen die Temperaturen so aus:

Die Begriffe sind womöglich etwas verwirrend. Außenluft ist das, was das Gerät von außen ansaugt. Die Luft wird im Wärmetauscher erwärmt, das ist dann die Zuluft. Die Abluft wiederum ist die Luft, die den Raum verlässt, und nach dem Abkühlen im Wärmetauscher ist es die Fortluft. Man kann also schön die Wirkung der Wärmerückgewinnung beobachten.
Das Gerät unterscheidet also vier Lüfterstufen, mit der Stellung “0” lassen sich die Ventilatoren ganz abschalten. Das sollte mit Bedacht gewählt werden, denn ein wenig Luftbewegung ist immer gut. Andererseits braucht ein Lüftungsgerät im Sommer bei sehr trockener Luft und offenen Fenstern nicht laufen. Wir haben also auch diese Option in die Regelung aufgenommen. Definiert wurden anhand der gewonnenen Erfahrungswerte vier Schaltschwellen, die der Reihe nach geprüft werden. Trifft keine davon zu, greift die fünfte Option – der Stillstand. In Blockly sieht das so aus:

Und das ist der Blockly-Code zum Übernehmen in eine eigene Steuerung (Pfeil links klicken, um die Code-Ansicht aufzuklappen)
<xml xmlns="https://developers.google.com/blockly/xml">
<variables>
<variable id="..p$a28a7(9FtOXh@2=Y">Verzögerung</variable>
<variable id="D^mPo3kPHu_LUns^mRGc">nächste Stufe</variable>
</variables>
<block type="variables_set" id="KU*!iIU4i*pBt%eI]*q6" x="129" y="83">
<field name="VAR" id="..p$a28a7(9FtOXh@2=Y">Verzögerung</field>
<value name="VALUE">
<block type="math_number" id="=0pVnPDK=#]uEQo:kTj*">
<field name="NUM">0</field>
</block>
</value>
<next>
<block type="schedule" id="0}uQ:inK9+,M4E3tHlF(">
<field name="SCHEDULE">*/5 * * * *</field>
<statement name="STATEMENT">
<block type="comment" id="Wi2xPHqM-@?%=N3#bZ.@">
<field name="COMMENT">neuen Zielwert Lüfterstufe festlegen und zwischenspeichern</field>
<next>
<block type="controls_if" id=":*qHmbDypTHI%#drSZSy">
<mutation elseif="3" else="1"></mutation>
<value name="IF0">
<block type="logic_compare" id="j+ZZ(/iO]R[Arjnv^V;p">
<field name="OP">GTE</field>
<value name="A">
<block type="get_value" id="O5GzM(uF@^^m{c~+KTx}">
<field name="ATTR">val</field>
<field name="OID">modbus.7.holdingRegisters.40197_Luftfeuchte</field>
</block>
</value>
<value name="B">
<block type="math_number" id="i=+,q2j;cSPIw(f/EjJI">
<field name="NUM">76</field>
</block>
</value>
</block>
</value>
<statement name="DO0">
<block type="variables_set" id="E._Yj6UH?P0sw_)3CnOr">
<field name="VAR" id="D^mPo3kPHu_LUns^mRGc">nächste Stufe</field>
<value name="VALUE">
<block type="math_number" id="+bO.vEHU%t;=~Dq5k|cD">
<field name="NUM">4</field>
</block>
</value>
</block>
</statement>
<value name="IF1">
<block type="logic_compare" id="{^)d16!aSyYgi.mBJI}7">
<field name="OP">GTE</field>
<value name="A">
<block type="get_value" id="qF~vLk;JHJYCQhmy#zoO">
<field name="ATTR">val</field>
<field name="OID">modbus.7.holdingRegisters.40197_Luftfeuchte</field>
</block>
</value>
<value name="B">
<block type="math_number" id="9:DX9bK+{ScL4S17x1_l">
<field name="NUM">63</field>
</block>
</value>
</block>
</value>
<statement name="DO1">
<block type="variables_set" id="|c.D-l%e^}+JJ0`nis7_">
<field name="VAR" id="D^mPo3kPHu_LUns^mRGc">nächste Stufe</field>
<value name="VALUE">
<block type="math_number" id="2LWzyRF|ihFU_*ud:_*$">
<field name="NUM">3</field>
</block>
</value>
</block>
</statement>
<value name="IF2">
<block type="logic_compare" id="25vDmyr^.~kY_^dzf{_n">
<field name="OP">GTE</field>
<value name="A">
<block type="get_value" id="9it2~9:#Oc8*(qZBp:F3">
<field name="ATTR">val</field>
<field name="OID">modbus.7.holdingRegisters.40197_Luftfeuchte</field>
</block>
</value>
<value name="B">
<block type="math_number" id="|Y2;}:|4MmMfi.o=,r*9">
<field name="NUM">53</field>
</block>
</value>
</block>
</value>
<statement name="DO2">
<block type="variables_set" id="RN-S9J]QTI_H/5OK,bn,">
<field name="VAR" id="D^mPo3kPHu_LUns^mRGc">nächste Stufe</field>
<value name="VALUE">
<block type="math_number" id="{Ci7Jvii9/M+Kzet(oii">
<field name="NUM">2</field>
</block>
</value>
</block>
</statement>
<value name="IF3">
<block type="logic_compare" id="r9|se95:o0p2w(b}E}}B">
<field name="OP">GTE</field>
<value name="A">
<block type="get_value" id="MuCb4_{OxA~ai1n#}5]|">
<field name="ATTR">val</field>
<field name="OID">modbus.7.holdingRegisters.40197_Luftfeuchte</field>
</block>
</value>
<value name="B">
<block type="math_number" id="~OQqhnwWI_C:MWU?Nu{g">
<field name="NUM">43</field>
</block>
</value>
</block>
</value>
<statement name="DO3">
<block type="variables_set" id="=me`[G#8UWN7]E2C:bXL">
<field name="VAR" id="D^mPo3kPHu_LUns^mRGc">nächste Stufe</field>
<value name="VALUE">
<block type="math_number" id=";$7Fc*)rNtf*yGk$?*~X">
<field name="NUM">1</field>
</block>
</value>
</block>
</statement>
<statement name="ELSE">
<block type="variables_set" id="N]Zw!VzytSQxe,xDnrT#">
<field name="VAR" id="D^mPo3kPHu_LUns^mRGc">nächste Stufe</field>
<value name="VALUE">
<block type="math_number" id="mcoMGhd7T:|NDp_y^/`M">
<field name="NUM">0</field>
</block>
</value>
</block>
</statement>
<next>
<block type="controls_if" id=",:N:h-_QC/]Y!vuY`_IB">
<mutation else="1"></mutation>
<value name="IF0">
<block type="logic_compare" id="43wh?g^MMKR8TTcKJ*iQ">
<field name="OP">GTE</field>
<value name="A">
<block type="variables_get" id="@E*^LKcNVxFB]|}5mm.X">
<field name="VAR" id="D^mPo3kPHu_LUns^mRGc">nächste Stufe</field>
</block>
</value>
<value name="B">
<block type="get_value" id="@%3;#o~#hC`S!gv]z0Nn">
<field name="ATTR">val</field>
<field name="OID">modbus.7.holdingRegisters.40325_FanSpeed</field>
</block>
</value>
</block>
</value>
<statement name="DO0">
<block type="comment" id="tA%cGBueWTjNDy@S?Y18">
<field name="COMMENT">Nächste Stufe ist höher als aktuelle: sofort erhöhen</field>
<next>
<block type="control" id="pDsu/@R|bI|j4-rtfD0a">
<mutation xmlns="http://www.w3.org/1999/xhtml" delay_input="false"></mutation>
<field name="OID">modbus.7.holdingRegisters.40325_FanSpeed</field>
<field name="WITH_DELAY">FALSE</field>
<value name="VALUE">
<block type="variables_get" id="0X[yt@ns^o|!}O9Abb[F">
<field name="VAR" id="D^mPo3kPHu_LUns^mRGc">nächste Stufe</field>
</block>
</value>
<next>
<block type="variables_set" id="B{IBB)nZDrX1a.ps7OH?">
<field name="VAR" id="..p$a28a7(9FtOXh@2=Y">Verzögerung</field>
<value name="VALUE">
<block type="math_number" id="U(E=gp=ZU}L[xAN?ta)0">
<field name="NUM">0</field>
</block>
</value>
</block>
</next>
</block>
</next>
</block>
</statement>
<statement name="ELSE">
<block type="comment" id="+T1AiubT/*]vv4hqCuMV">
<field name="COMMENT">vor dem Senken der Lüfterstufe zwei Takte warten, so dass länger nachgelüftet wird und das System weniger schwingt</field>
<next>
<block type="math_change" id="#ea8be$lF)0EYoTLZvgr">
<field name="VAR" id="..p$a28a7(9FtOXh@2=Y">Verzögerung</field>
<value name="DELTA">
<shadow type="math_number" id="O+fqdW~Z:r!ShI`jIP](">
<field name="NUM">1</field>
</shadow>
<block type="math_number" id="re0b|w@,;V80{KbszyU)">
<field name="NUM">1</field>
</block>
</value>
<next>
<block type="controls_if" id="2h!Xy!`k7@2`+]`(ws+d">
<value name="IF0">
<block type="logic_compare" id="oB2@.JMfHZr+mJVS,;K|">
<field name="OP">EQ</field>
<value name="A">
<block type="variables_get" id="H7UZVWB6Ono!BarhC@Ek">
<field name="VAR" id="..p$a28a7(9FtOXh@2=Y">Verzögerung</field>
</block>
</value>
<value name="B">
<block type="math_number" id="H5.@d(KDG[F.x;Btbz?{">
<field name="NUM">3</field>
</block>
</value>
</block>
</value>
<statement name="DO0">
<block type="comment" id="WR%b0xff^C`!VbDa6JIF">
<field name="COMMENT">dann um eine Stufe senken</field>
<next>
<block type="control" id="79pE;^~YZeALaXng4_vE">
<mutation xmlns="http://www.w3.org/1999/xhtml" delay_input="false"></mutation>
<field name="OID">modbus.7.holdingRegisters.40325_FanSpeed</field>
<field name="WITH_DELAY">FALSE</field>
<value name="VALUE">
<block type="math_arithmetic" id="w`]b~y/D8~U2D!jZ!{OX">
<field name="OP">MINUS</field>
<value name="A">
<shadow type="math_number" id="j@ywave7|?;@8?n|(#}!">
<field name="NUM">1</field>
</shadow>
<block type="get_value" id="R4c(Jy.!iBBI,lD/R19y">
<field name="ATTR">val</field>
<field name="OID">modbus.7.holdingRegisters.40325_FanSpeed</field>
</block>
</value>
<value name="B">
<shadow type="math_number" id="O^Wv=DRyF(T#xb3Hwb6#">
<field name="NUM">1</field>
</shadow>
</value>
</block>
</value>
<next>
<block type="variables_set" id="`YO)ReN6yHw|M!W]LHvD">
<field name="VAR" id="..p$a28a7(9FtOXh@2=Y">Verzögerung</field>
<value name="VALUE">
<block type="math_number" id="MpV,sc51R#h#)Z?#[mCZ">
<field name="NUM">0</field>
</block>
</value>
</block>
</next>
</block>
</next>
</block>
</statement>
</block>
</next>
</block>
</next>
</block>
</statement>
</block>
</next>
</block>
</next>
</block>
</statement>
</block>
</next>
</block>
</xml>
Zu Beginn des Skripts wird eine Variable gesetzt, die gleich noch näher erläutert wird, anschließend die Ausführung des Skripts alle fünf Minuten geplant. Es werden die vier Schaltschwellen mit der aktuellen Luftfeuchte verglichen. Falls eine davon zutrifft, wird die passende Lüftungsstufe vorgewählt, falls keine davon zutrifft, ist die Luft so trocken, dass die Stufe “0” lauten darf.
Die Lüftungsstufe wird in einem zweistufigen Prozess gesetzt, um das Gerät nicht mit ständigen Änderungen zu belästigen. Dabei gilt folgende Maxime: fordert die aktuelle Luftfeuchte eine Erhöhung der Lüfterdrehzahl, wird das sofort umgesetzt. Hätte sie jedoch eine Senkung der Drehzahl zur Folge, wird erst noch zwei Takte abgewartet – also insgesamt 15 Minuten. Erst dann wird die Drehzahl gesenkt, und zwar auch nur um eine Stufe – selbst wenn das Beibehalten der hohen Drehzahlstufe zur Folge hätte, dass anschließend direkt zwei Stufen nach unten gesprungen werden könnte. Auf diese Weise reagiert das Gerät relativ schnell auf steigende Luftfeuchte, behält hohe Lüftungsstufen aber gleichzeitig lange genug bei, um eine gründliche Lüftung sicherzustellen. Außerdem wird Hin- und Herpendeln zwischen zwei Lüftungsstufen vermieden.
In der Praxis sieht das an einem Tag mit hoher Luftfeuchte außen und intensiver Nutzung innen so aus:

Die farbigen Balken im Hintergrund zeigen die Schaltschwellen an. Wechselt die grüne Kurve (Luftfeuchte innen) also z.B. vom grünen in den gelben Bereich, hat das eine Erhöhung der Drehzahl zur Folge. Kurz aufeinanderfolgende Drehzahlerhöhungen (erkennbar an der gelben Kurve) sind erlaubt, beim Absenken ist die Regelung träger. Die graue Stufe ganz unten (Stillstand der Lüfter) wird an diesem Tag nicht erreicht.
Bei Bedarf können die Schwellwerte noch angepasst werden, aber das Ziel einer sehr feingranularen Regelung ist erreicht.