Wetterstation

Jeder kennt diese Wetterstationen mit Anzeige fuer Innen- und Aussentemperatur, Uhrzeit, Luftdruck und was man sonst noch so messen kann. Sowas will ich auch haben – aber gefuettert mit den Daten aus openhab. Ebenso wie diese Wetterstationen soll das Ding moeglichst mit einer Batterie auskommen. Daher habe ich mir als Anzeige E-paper vorgestellt. So suche ich schon seit Monaten ein E-pager Display, welches sich per Wifi mit Daten fuettern laesst. leider waren die diversen Projekte, welche ich bisher gefunden habe, viel zu komplex fuer meine beschaenkten Faehigkeiten.

Da kam dann Anfang des Jahres der c’t Artikel zum digitalen Tuerschild genau richtig. Das Display kann man in Deutschland kaufen, den esp32 habe ich mir aus China besorgt (gleich in einer Variante mit Batteriefach). Als Rahmen habe ich erstmal eine “BÅS Vitrinekistj” genommen, da passt alles einfach rein, auch wenn es nicht besonders schoen aussieht. Das Ganze war schnell aufgebaut. Wieder Erwarten funktionierten alle Loetstellen auf Anhieb, dafuer von der Software fast gar nichts. Es hat eine Weile gedauert bis ich gemerkt habe, dass es im Source-Code diverse Stellen mit Kommentaren der Art “hier aendern je nach php-Version” oder “hier anpassen je nach Display-Groesse’ gibt. Irgendwann ging es dann. Auf dem esp32 laeuft ein Programm, welches sich einfach das rohe Bild (also 640*384 bits entsprechend 30720 bytes) per http und wifi vom Server holt.

Damit brauche ich nur noch auf dem Server ein Bild mit Daten aus dem openhab generieren, den Rest vom c’t-Projekt kann ich direkt nutzen.

Leider enthaelt das c’t Projekt keinen Code, um das rohe Bild zu testen. Das kann aber schnell selber gebaut werden: da der esp32 es per http abholt, kann man es sich quasi im Browser anschauen. Dafuer braucht es nur ein paar Zeilen Javascript, die die Rohdaten in ein html5 canvas malen. Das ist ganz praktisch, wenn man das Bild selber generiert:

<!DOCTYPE html >
<html>
        <head>
                <meta charset="UTF-8">
                <script>
function loadImage(tb) {
        console.log("loading image from "+tb.value);
        var xhr = new XMLHttpRequest();
        xhr.onload = function(e) {
                var buffer = xhr.response;
                if (buffer) {
                        var byteArray = new Uint8Array(buffer);
                        var c = document.getElementById("bild");
                        var ctx = c.getContext("2d");
                        ctx.moveTo(0,0);
                        ctx.clearRect(0,0,c.width,c.height);
                        var x = 0, y = 0;

                        for (var i=0; i<byteArray.byteLength;i++) { 
                                var v = byteArray[i];
                                for (var j = 0; j < 8; ++j ) {
                                        var bit = (v & (1 << (7-j)));
                                        if (bit) {
                                                ctx.fillRect(x,y,1,1);
                                        }
                                        x = x + 1;
                                        if (x >= 640) {
                                                x = 0;
                                                y = y + 1;
                                        }
                                }
                        }
                        tb.readOnly = false;
                }
        };
        xhr.open('GET', tb.value, 'true')
                xhr.responseType = 'arraybuffer'
                xhr.send(null);
}

function keyEvent(event) {
        var tb = document.getElementsByName("urlinput")[0]
                if (event.keyCode === 13) {
                        tb.readOnly = true;
                        loadImage(tb);
                }
}
                </script>
        </head>
        <body>
                URL: <input type="text" name="urlinput" size="100" value="/server/openhab.raw"
                                                     onkeyup="keyEvent(event)"><br>
                <canvas id="bild" width="640" height="384"></canvas>
        </body>
</html>

Laeuft bei mir in Chrome und Firefox.

Das sieht dann in etwas so aus:




Gewinnt sicher keinen Designpreis, funktioniert aber gut.

Um die Daten aus dem openhab zu generieren, gibt es fuer den Anfang einen recht einfachen Weg:

  1. bauen wir uns eine passende Sitemap, egal ob classic oder habpanel oder wie auch immer, Hauptsache wir haben eine Website mit allen Daten
  2. holen wir uns diese Seite als Bild: firefox -screenshot http://.../basicui/app -P screenshots (ein eigenes Profile fuer Batch ist immer gut, damit es keine Konflikte gibt mit laufenden Instanzen vom Firefox)
  3. passen wir das entsprechende Bild in der Groesse an: convert screenshot.png -resize 640x384\! small-screenshot.png
  4. konvertieren wir das Bild in pbm: convert small-screenshot.png small-screenshot.pbm (pbm gibt es als binary und als ascii, wir brauchen ersteres, das ist der default)
  5. schneiden wir den bpm-Header ab: tail -n +3 small-screenshot.pbm > small-screenshot.raw (bpm ist naemlich genau unser rohes Format, nur mit ein paar Zeilen Metadaten vorweg)

Das kann man einfach in ein cgi giessen und loslegen. Das Ergebnis sieht schon ganz gut aus:



Im naechsten Schritt muss ich jetzt die Anzeige etwas aufhuebschen. Das ist nicht so meine Staerke. Die Sitemap ist dafuer nicht so optimal geeignet. Im naechsten Schritt wird es also darum gehen, ein bisschen was zu programmieren, das sich Daten aus dem openhab holt (und vielleicht noch ein paar Wetternachrichten), das alles nett in ein Bild packt und zur Abholung bereitstellt.

Bescheid!

Im Bad gibt es ein Fenster zum Lueften und das ist auch gut so, auf die Gruende moechte ich hier nicht weiter eingehen. Natuerlich soll die Heizung beim Lueften ausgestellt sein. Bei homematic kann man dazu entweder den Fensterkontakt direkt mit dem Heizungsthermostat koppeln, oder die Steuerung jemand anders ueberlassen, in meinem Fall dem openhab. Das kann dann so aussehen:

var Number targetTemp = 19
rule "heizung bad unten aus wenn fenster offen"
when
  Item Fenster_BadUnten changed from CLOSED to OPEN
then
  targetTemp = Soll_Temperatur_BadUnten.state
  sendCommand(Soll_Temperatur_BadUnten, 1.0)
end
rule "heizung bad unten an wenn fenster wieder zu"
when
  Item Fenster_BadUnten changed from OPEN to CLOSED
then
  sendCommand(Soll_Temperatur_BadUnten, targetTemp)
end

Das ist schonmal ganz gut, nur leider vergesse ich das Fenster ab und zu mal und so friert sich der naechste dann beim aktuellen Wetter (gestern Nacht -13 Grad) im wahrsten Sinne des Wortes den Allerwertesten ab. Openhab kann ja auch Mails schicken, dass passt ganz gut, fuege ich einfach ein

   sendMail("...", "Bad unten Fenster auf", "Bad unten Fenster auf")

hinter das Heizung-Ausschalten.

Leider kommt die Mail ja, wenn ich das Fenster aufmache; nach 5 Minuten, wenn ich es schliessen muesste, habe ich es laengst wieder vergessen. Das Senden der Mail muss also verzoegert werden. Dafuer gibt es Timer in openhab. Die sehen leider etwas komplizierter aus: generell funktioniert ein Timer ja praktisch immer so, dass ich eine Funktion aufrufe welche als Parameter ein Delay bekommt und einen Callback, welcher nach dem Delay aufgerufen wird. Die Rule-Syntax in openhab kennt leider keine Funktionen, aber lambdas. Fuer den Programmierer macht das keinen Unterschied, nur fuer die Lesbarkeit:

  timer = createTimer(now.plusMinutes(5), [|
      if (Fenster_BadUnten.state == OPEN) 
        sendMail("...", "Bad unten Fenster auf", "Bad unten Fenster seit 5min auf")
      timer = null
    ])
end

Zum Glueck komme ich ja aus der Welt von C++, da gibt es ja weitaus Schlimmeres.

Nun bekommen wir also unsere Mail 5 Minuten nachdem das Fenster geoeffnet wurde. Wenn es dann noch auf ist. Der Ordnung halber loeschen wir den Timer ordentlich (also genau genommen loeschen wir hier nichts, das ueberlassen wir dem GarbageCollector), wenn wir ihn nicht mehr brauchen, das Fenster also wieder zu ist:

    timer = null

Zuletzt brauchen wir noch etwas Feintuning: im schoensten Hamburger Sommer stoert ein offenes Fenster nicht wirklich. Dafuer habe ich einen Temperatursensor auf dem Balkon, mit dem verfeinern wir das lambda:

  timer = createTimer(now.plusMinutes(5), [|
      if (Fenster_BadUnten.state == OPEN && Temperature_Balkon.state < 15) 
        sendMail("...", "Bad unten Fenster auf", "Bad unten Fenster seit 5min auf")
      timer = null
    ])

Zuletzt nochmal alles am Stueck:

var Number targetTemp = 19
var Timer timer = null

rule "heizung bad unten aus wenn fenster offen"
when
  Item Fenster_BadUnten changed from CLOSED to OPEN
then
  targetTemp = Soll_Temperatur_BadUnten.state
  sendCommand(Soll_Temperatur_BadUnten, 1.0)

  // inform me if open too long
  timer = createTimer(now.plusMinutes(5), [|
      if (Fenster_BadUnten.state == OPEN && Temperature_Balkon.state < 15) 
        sendMail("...", "Bad unten Fenster auf", "Bad unten Fenster seit 5min auf")
      timer = null
    ])
end


rule "heizung bad unten an wenn fenster wieder zu"
when
  Item Fenster_BadUnten changed from OPEN to CLOSED
then
  sendCommand(Soll_Temperatur_BadUnten, targetTemp)
  timer = null
end

Heizungsfernsteuerung

Meine Heizung (und noch ein paar Dinge mehr) steuert ein openhab2. Man koennte nun meinen, das Thema Fernsteuerung ist damit erledigt. Aber halt, nicht so schnell, ich meine etwas anderes: mein Wohnzimmer grenzt auf der einen Seite an einen Balkon, mit schoen grosser Fensterfront, darunter der Heizkoerper. Das ist die gute Nachricht. Auf der anderen Seite oeffnet sich das Zimmer zu einer Diele, in welcher eine Treppe ins kuehle Obergeschoss fuehrt. Das ist die schlechte Nachricht, genau genommen eine Katastrophe: waehrend es auf der Couch direkt an der Heizung kuschelig warm ist, friert der, welcher mit dem Ruecken zur Diele sitzt.

Als Abhilfe fallen mir ohne viel Nachdenken gleich drei Loesungen ein:

  1. Feingefuehl gepaart mit Erfahrung: nach ein paar Jahren in der Wohnung wird man wohl ein sicheres Gefuehl dafuer entwickeln, auf welche Temperatur man die Heizung einstellen muss, um im ganzen Wohnzimmer eine behagliche Temperatur zu haben. Vielleicht noch unter Beachtung der Aussentemperatur. Im Prinzip kann man das dann auch in Code giessen. Ist sicher eine gangbare Loesung fuer jemanden mit Geduld und eben Feingefuehl. Faellt fuer mich also flach.
  2. Eine Art KI-Algorithmus, welcher die Temperatur regelmaessig reguliert, aus den Ergebnissen lernt, damit quasi eine Wissendatenbank aufbaut und damit regelt. Offenbar muss man fuer diese Loesung die Temperatur irgendwo im Uebergang zwischen Wohnzimmer und Diele messen. Und dann realisiert der Computer im Prinzip die Variante 1. Das waere mein Favorit, wenn da nicht meine pragmatische Faulheit waere.
  3. Wenn ich fuer Variante 2 eh schon einen weiteren Messpunkt habe, kann ich es mir auch einfach machen: der Messpunkt steuert einfach die Heizung. Wenn ich am Messpunkt 20 Grad einstelle, dreht openhab einfach die Heizung solange hoch, bis die 20 Grad erreicht sind. Und dann halt wieder runter auf 20 Grad. Das klingt doch gut: einfach und funktional.

Fuer die Realisierung braucht man also einen Heizungsthermostat, bei welchen openhab2 die Temperatur lesen und setzen kann. Dazu ein Thermometer, welches openhab2 auslesen kann. Ich habe mich bei letzteren fuer das “HomeMatic Funk-Wandthermostat, Aufputzmontage” entschieden. Das ist batteriegetrieben, kann also beliebig platziert werden, und hat einen Einstellknopf, man kann also auch manuell regeln. Damit es nicht zu einfach ist, haben wir noch eine Balkontuer – deren Oeffnung soll natuerlich die Heizung abschalten.

So, lange Vorrede, jetzt endlich mal etwas Substanz:

Wir brauchen einige Items:

  • Temperature_Wall_Wohnzimmer: die Ist-Temperatur des Wandthermostats,
  • Soll_Temperatur_Wall_Wohnzimmer: die Soll-Temperatur des Wandthormostats,
  • Soll_Temperatur_Wohnzimmer: die Soll-Temperatur des Heizungsthermostats
  • Fenster_Wohnzimmer: der Fensterkontakt an der Balkontuer

Und damit bauen wir uns einfach eine Regel:

var Number targetTemp =  18
rule "heizung aufdrehen falls zu kalt oder zudrehen falls zu warm"
when
  Item Temperature_Wall_Wohnzimmer received update
then
  val Number sollTemp = Soll_Temperatur_Wohnzimmer.state
  val Number istTemp = Temperature_Wall_Wohnzimmer.state

  // sometimes targetTemp is null if this rule gets called...
  if (targetTemp == null) return;

  // no adjustments during window open
  if (Fenster_Wohnzimmer.state == OPEN) return; 

  if (istTemp < targetTemp) {
    if (sollTemp < targetTemp) {
      sendCommand(Soll_Temperatur_Wohnzimmer, targetTemp)
    }
    else {
      if (sollTemp - targetTemp > 5) {
        /* diff to high, do nothing */ 
      }
      else {
        sendCommand(Soll_Temperatur_Wohnzimmer, sollTemp+1)
      }
    }
  }
  if (istTemp > targetTemp && istTemp - targetTemp > 0.5) {
    if (sollTemp > targetTemp) {
      logInfo("dyn wohnz", "set sollTemp runter")
      sendCommand(Soll_Temperatur_Wohnzimmer, targetTemp)
    }
  }
end

Dazu kommt dann noch eine Regel, mit welcher der Wandthermostat quasi die Heizung ueberstimmt:

rule "heizung wohnzimmer wall thermostat overrules"
when 
  Item Soll_Temperatur_Wall_Wohnzimmer received update
then
  targetTemp = Soll_Temperatur_Wall_Wohnzimmer.state
  if (Fenster_Wohnzimmer.state == CLOSED && targetTemp != Soll_Temperatur_Wohnzimmer.state) {
    sendCommand(Soll_Temperatur_Wohnzimmer, targetTemp)
  }
end

Ich habe dann noch ein paar Regeln, welche die Heizung ab- und anschalten, wenn die Balkontuer geoeffnet oder geschlossen wird, am Wochenende Morgens die Temperatur etwas hochdrehent und spaet abends auf jeden Fall runterdrehen.

Im Ergebnis sieht das dann so aus:

Man kann schoen sehen, wie die Heizung staendig nachregelt. Offenbar wurde abends dann die Temperatur runtergedreht und am spaeten Vormittag hat wohl jemand gelueftet.

Im Ergebnis hat schon lange niemand mehr im Wohnzimmer manuell an der Heizung rumgestellt. So wie es sein soll.