Aus Linux-Magazin 06/2010

Selbstbauprojekt: Linux steuert Wärmepumpe

© kallejipp, Photocase.com© Matt Biddulph, Wikimedia Commons

Linux-Magazin-Leser Alexander-Philipp Lintenhofer heizt sein Haus mit einer Wärmepumpenanlage. Weil sich deren Steuerung als grottig erwies, startete er ein Selbstbauprojekt. Heute sitzt er stets im Warmen und weiß viel über stimmiges Management von Projekten .

Eine Volksweisheit meint, ein Mann solle in seinem Leben ein Kind zeugen, einen Baum pflanzen und ein Haus bauen. Diese antiquierten Vorgaben habe ich erfüllt, merke aber an, dass sich der Hausbau im Vergleich zum Kinderzeugen und Bäumepflanzen wesentlich mühsamer gestaltet hat. Zumal dabei Entscheidungen zu treffen sind, die man(n) a priori in ihrer Tragweite schwer abschätzen kann. Eine ist die des Heizungssystems.

Der Familienrat wählte letztendlich eine Fußbodenheizung, gespeist über eine Wärmepumpe mit Tiefenbohrung. Das ist zwar teuer in der Anschaffung, dafür aber nahezu wartungsfrei und kostengünstig im Betrieb. Also ließ ich diese einbauen und zwei Löcher, je 90 Meter tief, in die Erde bohren.

Unscharf formuliert funktioniert ein Wärmepumpensystem umgekehrt wie ein Kühlschrank: Während der Kühlschrank seinem Inneren Wärme entzieht und über Metallrippen auf der Rückseite in die Raumluft abgibt, erwärmt die Wärmepumpe das durch die Rohre der Fußbodenheizung gepumpte Wasser durch “Kälteentzug”. Ein Wasser-Glykol-Gemisch übernimmt diese “Kälte” über einen Wärmetauscher, führt sie über einen geschlossenen Kreislauf in den Erdboden ab und erwärmt sich dabei wieder.

Die technische Umsetzung des zugrunde liegenden thermodynamischen Vorgangs (Verdampfen, Komprimieren, Verflüssigen, Expandieren) benötigt elektrische Energie. Diese Antriebsleistung beträgt jedoch nur zirka ein Viertel der tatsächlich erzeugten Heizleistung. Das ist effizient und spart im Vergleich zu anderen Heizsystemen Kosten [1].

Scheitern als Antrieb

Für die Mehrheit der Hausbesitzer mag der physikalische Hintergrund uninteressant sein – sie will es einfach angenehm warm haben und möglichst wenig dafür bezahlen. Mir ging es nach der Inbetriebnahme im Sommer 2007 ähnlich. Der graue, waschmaschinengroße Kasten brummte im Keller mit seiner Standardeinstellung zufriedenstellend vor sich hin – vergleichbar mit einem Apache-Webserver, der mit der Default-»httpd.conf« auch (einigermaßen) funktioniert.

Andererseits: Bei jedem meiner 50-Euro-Elektrogeräte studiere ich brav die Gebrauchsanleitung und stelle es sorgfältig ein. Sollte ich einer Wärmepumpenanlage für 25 000 Euro weniger Aufmerksamkeit angedeihen lassen? Nach etlichen Fehlschlägen trieb mich mein Unvermögen, die wichtigen Parameter aufeinander abzustimmen und ein für Analysen geeignetes Benchmarking zu veranstalten, zu dem Projekt “Web-basierte Wärmepumpensteuerung mit Monitoring”, das der Artikel beschreibt.

Träges System

Die Problemstellung lässt sich mit der Abbildung 1 skizzieren, die die eingestellte Heizkurve als lineare Funktion zeigt. Ausgehend von einer Grundeinstellung muss die Anlage umso mehr heizen, je kälter es draußen ist. Die Parameter Steilheit, Außentemperatur sowie Einsatz-/Basis-Solltemperatur (y=kx+d) bestimmen das Systemverhalten.

Das erscheint physikalisch plausibel, erweist sich in der Praxis jedoch in zweierlei Weise als problematisch: Zwar kann ich mit dem Bedienelement an der Wärmepumpe einzelne Einstellungen manuell ändern, dies verschiebt allerdings die gesamte Geometrie der Heizkurve und wirkt sich daher über den gesamten Außentemperaturbereich aus (Raumthermostate habe ich nicht). Dass sich die Werte überdies gegenseitig beeinflussen, was bei dem Bedienteil ohne grafische Darstellung unbemerkt bleibt, bringt das System gänzlich durcheinander. Eine Fußbodenheizung verhält sich überdies so träge, dass der Hausbesitzer die Auswirkungen seines Tuns nicht unmittelbar zu spüren bekommt.

So heizte die Anlage eines sehr kalten Tages unser Haus auf über 25 Grad auf – woraufhin ich die Basis-Solltemperatur um 3 Grad zurückregelte. Die Maßnahme erwies sich im Moment nicht als sonderlich wirksam, denn die Familie schwitzte weiter. Ein paar Tage darauf kletterte die Außentemperatur über den Gefrierpunkt, nun hatte meine Absenkung zur Folge, dass Eltern wie Kinder froren. Ich hatte die Heizkurve in geometrischer Hinsicht nämlich parallel nach unten verschoben, statt deren Steigung abzuflachen.

Sich eine lineare Funktion über einen diskreten Wertebereich hinweg bildlich vorzustellen und die Parameter exakt anzupassen geht in der Regel schief. Hier setzt mein Projekt den Hebel an, indem es die Heizanlage Web-basiert, benutzerfreundlich und grafisch bedienbar gestaltet und den Verlauf einzelner Werte übersichtlich aufbereitet.

“Man kann nicht nicht kommunizieren”

Die Wärmepumpe in meinem Keller verfügt über eine RS232-Schnittstelle für Wartungsfälle. Der Servicetechniker zapft sie mit einem Nullmodem an und liest auf seinem Laptop mit einer Win32-Software Werte aus oder setzt welche neu. Es reizte mich, zuerst das zugehörige Protokoll zu analysieren, da der erfolgreiche Schreib- und Lesezugriff auf die Anlage die Voraussetzung für mein Vorhaben war. Die Überlegungen zur Funktionalität und Architektur meines Projekts mussten solange warten.

Die Wartungssoftware des Herstellers ist kostenlos verfügbar, den Freischaltkey erhielt ich per Mail. Während ich das Programm bediente und Werte veränderte, ließ ich einen Serialport-Sniffer den Datenverkehr mitschneiden. Die zutage getretene Syntax des Protokolls war schnell durchschaut: Ein Request-Frame formuliert eine Anfrage, beispielsweise das Auslesen einer Anzahl an Bytes ab einer gewissen Speicheradresse: »DLE STX Lesekommando Adresse Bytes DLE ETX CRC«. Die Wärmepumpe antwortet sodann mit: »DLE STX Daten DLE ETX CRC«.

Die aktuelle Außentemperatur beispielsweise liegt als 32-Bit-Fließkommawert ab der Adresse »0x0008« im Speicher. Als ich am 11. März 2010 um 16 Uhr in Wien Hütteldorf den Frame »10 02 01 15 00 08 00 04 10 03 7e a0« sandte, kam als Antwort »10 02 00 17 a4 47 dd 3f 10 03 9d 39« in der Byte-Reihenfolge Little-Endian zurück, also 0x3fdd47a4 oder 1,7 Grad Celsius. Das war zwar zu kalt für die Jahreszeit, aber ein erster Teilerfolg für mein Vorhaben.

Stoffliger Hersteller

Die Freude verflog allerdings schnell, als ich begann die gesamte Speicherbelegung zu interpretieren: 143 Attribute unterschiedlicher Länge und verschiedener Datentypen (Temperaturwerte, Zeitangaben, Drücke, Betriebszustände, …) ergaben einen Hex-Urwald. Um ihn zu lichten, erhoffte ich mir Hilfe von der Herstellerfirma. Die erwies sich trotz meiner Charmeoffensive leider als unkooperativ.

Also blieb es an mir hängen, die Werte manuell zu ändern, je nach Datentyp binär umzurechnen und mit dem Speicher zu vergleichen, Stunde um Stunde. Hilfreich waren dabei – wie so oft – Internetrecherche und Technikforen, über die sich zahlreiche Leidensgenossen austauschen. Als Lohn der Mühe winkte irgendwann das Hasharray »%wp_memory«, auszugsweise ersichtlich in Listing 1. Es dient in der weiteren Folge der Adressierung und Typisierung der Attribute im Speicher. (Alle Listings in voller Länge gibt’s unter [2].)

Listing 1: Array
»%wp_memory« als Katalog der Attribute

01 %wp_memory = (
02 
     
      [...]
     
03 'Temp-Aussen'       => { addr => 0x0008, bytes => 0x0004, menu => '01.00',
04                          acl => 'r-', type => TYPE_FLOAT },
05 'Hzg:TempEinsatz'   => { addr => 0x00F4, bytes => 0x0004, menu => '02.01',
06                          acl => 'rw', type => TYPE_FLOAT, minVal => 10.0, maxVal => 20.0 },
07 'Uhrzeit'           => { addr => 0x0064, bytes => 0x0003, menu => '05.00',
08                          acl => 'rw', type => TYPE_TIME },
09 'Ausfall:DO-Buffer' => { addr => 0x0098, bytes => 0x0001, menu => '07.03',
10                          acl => 'r-', type => TYPE_BIN },
11 
     
      [...]
     
12 );

Die Plattform

Als Plattform für das Projekt wählte ich mein Lieblingsspielzeug aus: Marvells Sheevaplug [3], ein faustgroßer Plug-Computer mit ARM-kompatibler Marvell-Kirkwood-6281-CPU im 1,2-GHz-Takt, 512 MByte RAM, einem internen SD-Card-Steckplatz, 1-GBit/s-Ethernet sowie einem USB-2.0-Port (Abbildung 2). Das vorinstallierte Ubuntu 9.04 wich sogleich einer Slackware-Distribution, Version 12.2, die verschlankt und mit einem maßgeschneiderten Kern 2.6.31 ausgestattet ist [4]. Ein Serial-to-Ethernet-Konverter Moxa Nport 5110 [5] öffnet den direkten Zugriff auf die RS232-Schnittstelle der Wärmepumpe übers Heimnetz. Nach dem Laden des mitgelieferten Kernelmoduls »npreal2.ko« kann ich den entfernten Port lokal über »/dev/ttyr00« ansprechen.

Abbildung 2: Ein Sheevaplug-Minirechner, wie er in der vorgestellten Heizungssteuerung zum Einsatz kommt, im Größenvergleich zu einem I-Phone.

Abbildung 2: Ein Sheevaplug-Minirechner, wie er in der vorgestellten Heizungssteuerung zum Einsatz kommt, im Größenvergleich zu einem I-Phone.

Die Kommunikation mit der Wärmepumpe übernehmen die Perl-Skripte »WPgetVal.pl« (auszugsweise in Listing 2) sowie »WPsetVal.pl«, beide aufgerufen mit den passenden Argumenten. Beispielsweise liefert »WPgetVal.pl WW:Temp-Ist« die aktuelle Temperatur des Warmwassers beziehungsweise setzt »WPsetVal.pl Ww:Temp-Soll 45.0« den Sollwert des Warmwassers auf 45 Grad. Das globale Hasharray »%wp_memory« hilft den Frame entsprechend der Speicheradressen und Werte zusammenzusetzen. Dann baut die Software dank des CPAN-Moduls Device::SerialPort die Verbindung zur Wärmepumpe auf, sendet den Frame und wartet auf Antwort. Wichtig ist, Nebenläufigkeiten bei Zugriffen auf die serielle Schnittstelle zu vermeiden:

my $serialObj = Device::SerialPort->new('/dev/ttyr00',0,'/tmp/wp_op.lock')

Das dritte Argument im Konstruktor definiert ein Lockfile und stellt so den exklusiven Zugriff sicher. Überhaupt geben sich beide Skripte mit der Fehlerbehandlung viel Mühe, insbesondere fangen sie Verbindungsprobleme und Datenübertragungsfehler ab. Das umfasst auch die Prüfung der CRC-Checksumme des eingehenden Antwort-Frame, was vergleichbare Ansätzen gerne weglassen.

Listing 2:
»WPgetVal.pl« liest Attribute aus der
Wärmepumpe

01 
     
      [...]
     
02 @frame = buildFrame($attr);
03 
04 if ((scalar(@frame) > $minFrameSize) &&
05     (my $serial = Device::SerialPort->new("/dev/ttyr00",0,'/tmp/wp_op.lock')))
06 {
07     $serial->baudrate(9600);
08     $serial->parity("none");
09     $serial->databits(8);
10     $serial->stopbits(1);
11     $serial->write_settings || undef $serial;
12 
13     # request frame is byte array, but for serial->write we need a string
14     if (($serial->write(pack("C*",@frame)))==scalar(@frame))
15     {
16         
     
      [...]
     
17         my ($count,$retVal) = $serial->read(16);
18         if ($count > $minFrameSize)
19         {
20             # build and interpret response frame in this block
21         }
22         else { print STDERR "Read operation failedn"; exit 1; }
23     }
24     else { print STDERR ("Error writing to rs232n"); exit 1; }
25 
26     $serial->close() || print STDERR ("Close failedn");
27     exit 0;
28 }
29 else { 
     
      [...]
      }

Das Pflichtenheft

Nachdem der Datenaustausch mit der Wärmepumpe flutschte, überlegte ich, was meine “Web-basierte Wärmepumpensteuerung mit Monitoring” können und welche Architektur ihr zugrunde liegen sollte. Diese Features mussten her:

  • Anzeige eines Blockschaltbilds mit den aktuellen Werten
  • Statusanzeige und Warnungen mit deren Historie
  • Tabelle aller Daten mit der Möglichkeit, Werte zu
    ändern
  • Grafische Anzeige der gerade eingestellten Heizkurve und
    Erzeugen einer Vergleichskurve mit der Möglichkeit, diese
    gleich zu übernehmen
  • Diagramme mit beliebigen Werten in einem beliebigen
    Zeitintervall
  • Statistiken über Betriebsdauer oder Stromverbrauch,
    aufgeteilt nach Tag- und Nachtstrom, sowie über Kosten
  • Unterschiedliche Zugriffsrechte
  • Alarmierung per Mail und SMS bei Fehlfunktion

Die Aufgabenverteilung der einzelnen Komponenten in Abbildung 3 lehnt sich an das klassische Model-View-Controller-Konzept an. Bei der Datenhaltung mit MySQL konnte ich auf ein relationales Modell zu Gunsten zweier flacher Tabellen verzichten. Stattdessen befülle ich periodisch eine Tabelle mit den wichtigsten, frisch ausgelesenen Werten. Die andere listet Jobs (in der Wärmepumpe zu setzende Parameter) auf.

Abbildung 3: Den architektonischen Kern bilden Datentabellen mit ausgelesenen Werten beziehungsweise auszuführenden Jobs.

Abbildung 3: Den architektonischen Kern bilden Datentabellen mit ausgelesenen Werten beziehungsweise auszuführenden Jobs.

Ein Perl-Daemon füttert die erste Tabelle im Minutentakt. Ein weiterer prüft im Gegenzug, ob in der Jobtabelle Änderungen vorliegen, von denen die Wärmepumpe wissen sollte. Der Benutzer steuert das Ganze über eine in PHP verfasste Webapplikation, die zugleich die Werte grafisch aufbereitet. Vereinfachend ließe sich diese als Frontend begreifen, während sich die Verbindung zur Wärmepumpe als Backend darstellt. Im Backend arbeitet übrigens auch die periodische Prüfung auf Unzulänglichkeiten, die nötigenfalls zur Alarmierung führt.

Kontakt mit der Heizanlage

Das Arbeitspferd im Backend hört auf den Namen »WPpolld.pl«. Als Daemon (CPAN-Modul Proc::Daemon) sendet es minütlich den fix kodierten Request-Frame »10 02 01 15 00 00 01 76 10 03 79 2c« an die Wärmepumpe, um ihr ab der Adresse »0x0000« 374 Bytes (»0x0176«) zu entlocken – die wichtigsten Werte im Speicher. Den empfangenen Response-Frame bekommt die Helperfunktion »fetchFrameData()« übergeben. Diese prüft die CRC16-Checksumme, streift Header und Trailer ab, reversiert das Byte-Stuffing »0x10« und löst somit den Rohling aus dem Frame aus.

Die resultierende Bytefolge retourniert als Vektor an die Funktion »interpretData()«. Dort liefert eine Iteration durch »%wp_memory« die jeweiligen Adress- und Längenangaben sowie den Datentyp, was, nach Berechnung des Offsets, ein gezieltes Herauspicken der Attribute aus dem Rohdatenvektor erlaubt. Außerdem ist sichergestellt, dass die angewendete Interpretationsvorschrift zum Datentyp passt. Die resultierenden Dezimal-, Fließkomma-, Zeit- beziehungsweise Boolean-Werte wandern zum späteren Ausschlachten in das global verfügbare Array »%yield« (Listing 3).

Erwähnenswert sind auch die Statusflags, die als Bitmuster vorliegen. Eine darüber gelegte Bitmaske (beziehungsweise ein Shiften) bringt Fehler und Ausfälle der Anlage ans Tageslicht und stößt geeignete Alarmierungsmaßnahmen an. Im Anschluss übergibt das Skript die Einträge von »%yield« an die Datenbank. Sollte dieser Vorgang scheitern, schreibt der Daemon den Datensatz als XML-Struktur ins Filesystem, was die Persistierung auf jeden Fall sicherstellt. Der Daemon »WPsetValAgentd.pl« sieht in der Jobtabelle nach, ob Einstellungen der Wärmepumpe zu verändern, also Werte in den Speicher zu schreiben sind.

Liegen neue Jobs an, wird im Vorfeld geprüft, ob der Eintrag von einem Benutzer oder Prozess stammt, der dafür die nötige Berechtigung besitzt, und ob der zu ändernde Wert im vorgegebenen Wertebereich liegt. Ein weiterer Konsistenzcheck stellt sicher, dass kein in der Jobliste nachfolgendes Attribut das zu editierende wieder überschreibt.

Sind alle Voraussetzungen erfüllt, nimmt »WPsetVal.pl« die Einstellungsänderung vor und wartet auf den Rückgabewert. Als hilfreich erweist sich in dem Zusammenhang die ab Perl 5.10 neu eingeführte interne Variable »${^CHILD_ERROR_NATIVE}«, die zusätzlich zur direkt abzufangenden Ausgabe über den Rückgabewert eines über Backticks aufgerufenen Skripts informiert:

my $cmd_output = `$setValScript $currAttr $currVal 2>&1`;
my $cmd_retval = ${^CHILD_ERROR_NATIVE}/256;

Zur detaillierten Information gelangt die Rückgabe von »WPsetVal.pl« in die Datenbanktabelle, dort wechselt der Status des Jobs abschließend von »pending« auf »outdated«, »fail« oder »success«.

Listing 3:
»fetchFrameData()« liefert Rohdaten, die
»interpretData()« entsprechend darstellt

01 sub fetchFrameData
02 {
03    my @frame = @_;
04    my $crc1 = sprintf("%02x%02x",@frame[(scalar(@frame)-3)],@frame[(scalar(@frame)-2)]);
05    my $crc2 = sprintf("%04x",GetCRC16(@frame));
06 
07    if ($crc1 eq $crc2)
08    {
09       my $headerbytes  = 5; # strip 'FF' DLE STX '00 17'
10       my $trailerbytes = 5; # strip DLE ETX <CRC> 'FF'
11       my @data;
12       # byte stuffing?
13       for(my $i=0,my $offset=$headerbytes;
14            $offset<(scalar(@frame)-$trailerbytes);
15            $offset++,$i++)
16       {
17            if (($frame[$offset]==16)&&($frame[$offset+1]==16)) { $offset++; }
18            $data[$i] = $frame[$offset];
19       }
20       return @data;
21    }
22    else { return (); }
23 }
24 
25 sub interpretData
26 {
27    my ($startaddr,$bytes,@comdump) = @_;
28    my @data = fetchFrameData(@comdump);
29 
30    if ($bytes == scalar(@data))
31    {
32       while (my ($currKey, $currAttr) = each(%wp_memory))
33       {
34         # is current attribute of %wp_memory within received data chunk?
35         if ((   $currAttr->{addr}>=$startaddr)
36             && ($currAttr->{addr}<($startaddr+$bytes))
37             && ($currAttr->{type} != TYPE_UNDEF))
38         {
39             my ($currVal,$arrIdx);
40             # pick $currVal from @data
41             for (my $offset=$currAttr->{addr};
42                   $offset<($currAttr->{addr}+$currAttr->{bytes});
43                   $offset++)
44             {
45                  # attention: offset of address is only equal
46                  # to index of array if $startaddr == 0!
47                  $arrIdx = $offset-$startaddr;
48                  $currVal .= sprintf("%02x",$data[$arrIdx]);
49             }
50             if    ($currAttr->{type} == TYPE_DEC)    { $currVal = 
     
      [...]
      }
51             elsif ($currAttr->{type} == TYPE_FLOAT1) { $currVal = 
     
      [...]
      }
52             elsif ($currAttr->{type} == TYPE_DATE)   { $currVal = 
     
      [...]
      }
53             elsif ($currAttr->{type} == TYPE_TIME)   { $currVal = 
     
      [...]
      }
54             elsif ($currAttr->{type} == TYPE_BIN)    { $currVal = 
     
      [...]
      }
55             elsif ($currAttr->{type} == TYPE_BOOL)   { $currVal = 
     
      [...]
      }
56             $yield{$currAttr->{menu}} = {'desc' => $currKey, 'val' => $currVal};
57          }
58       }
59    }
60    else { 
     
      [...]
      }
61 }

Was der Benutzer sieht

Die Webapplikation läuft auf einem Apache-Server mit PHP-Modul, für die Authentifizierung wie empfohlen mit dem Modul »auth_digest«. Das Zeichnen der Diagramme übernimmt die für nicht kommerzielle Zwecke frei verfügbare Bibliothek Jp Graph [6]. Als Prerequisite ist die Grafikbibliothek GD2 [7] nötig.

Jp Graph eignet sich für derlei Aufgaben hervorragend. Die Grafikbibliothek ist nicht nur installationstechnisch anspruchslos, sondern arbeitet auch performant und ist durch ihre vorzügliche Dokumentation einfach in eigenen Programmen einzusetzen. Entwickler können Diagramme kombinieren und aggregieren, die abzubildenden Werte übergeben sie jeweils über ein Array.

Da die Standardeinstellungen der grafischen Elemente meist passen und die automatische Skalierung, vor allem bei Zeitachsen, gute Arbeit leistet, reduziert sich der Aufwand des Entwicklers – hier meiner – auf das saubere Befüllen der zu übergebenden Arrays. Der letzte Abschnitt des Listing 5 zeigt den Code, der ein Liniendiagramm erzeugt.

Haus-Web 2.0

Beim Frontend selbst setze ich Client-seitig massiv auf Javascript, das den für die diskreten Zeitbereiche notwendigen Zeitstempel umrechnet sowie die zu den Anzeigen passenden Formularfelder ein- und ausblendet. Javascript formuliert die Query-Strings der jeweils im Menü aufgelisteten Links dynamisch und übergibt so die Zeitparameter an die PHP-Skripte, die die Diagramme zeichnen.

Dass ich die fertig erstellten Diagramme und Tabellen in einen Iframe setze, macht das Frontend Client-seitig zustandsbehaftet. Die Requests laden die aufrufende Mutterseite somit nicht neu, was die in den Variablen gesetzten Werte schützt. Das mag zwar primitiver gelöst sein als per Ajax, ist aber funktional fast ebenbürtig. Ich gestehe zudem, dass ich die Accessibility-Richtlinien, wie sie WCAG 2.0 beschreibt, nicht einhalte. Das Frontend ist daher ohne Javascript oder mit einem reinen Textbrowser wie Lynx nicht bedienbar. Für den (im Doppelsinn) Hausgebrauch lässt sich mit dieser Einschränkung jedoch gut leben.

Statusanzeige mit geborgter Optik

Beim Start präsentiert das Frontend das Blockschaltbild der Anlage, das über deren aktuellen Status informiert (Abbildung 4). Die Grafik selbst habe ich der Wartungssoftware des Herstellers entnommen und modifiziert. Das leere Diagramm ohne Werte bildet den Seitenhintergrund. Darüber legt die Software, absolut positioniert, die aktuellen Werte sowie Icons, die den Zustand der Ventile, Pumpen und des Kompressors zeigen.

Abbildung 4: Das Blockschaltbild dient zur Statusanzeige. Hier heizt die Anlage gerade Warmwasser auf.

Abbildung 4: Das Blockschaltbild dient zur Statusanzeige. Hier heizt die Anlage gerade Warmwasser auf.

Das Listing 4 skizziert auszugsweise die Umsetzung: Über ein Array definiert es die Dateinamen der Icons, deren Koordinaten sowie die Bitmasken – jeweils für Normalbetrieb und Störung. Die Schleife ab Zeile 17 iteriert das Array und verknüpft die jeweiligen Bitmasken mit den aus der Datenbanktabelle ausgelesenen Statusbytes mit logischem Und. Je nach Ergebnis packt das Skript das entsprechende Icon in einen Layer und positioniert diesen entsprechend.

Eine Übersicht über Status, Warnungen und Ausfälle lasse ich mir gesondert in einer Tabelle anzeigen, die die Unregelmäßigkeiten der letzten Tage aufdeckt. So fällt mir beispielsweise die häufige Warnung auf, dass die Spreizung, also der Temperaturunterschied zwischen Vor- und Rücklauf, zu hoch ist. Tatsächlich kehrt das Heizungswasser um 7 bis 8 Grad kälter aus dem Estrich zurück, als es der Vorlauf hineingeschickt hat. Wünschenwert sind 4 bis 5 Grad Spreizung, was die Vorlauftemperatur und damit den Heizaufwand senkt. Ich plane mit einer stärkeren Umwälzpumpe Abhilfe zu schaffen.

Listing 4: Schleife positioniert
Status-Icons

01 $arrItems = array(
02     array( 'name' => 'Kompressor', 'mask_do_buffer' => 0x02, 'mask_di_buffer' => 0x01,
03            'icon_on' => 'ico_kompr_on.png', 'icon_fail' => 'ico_kompr_fail.png',
04            'right' => '347', 'top' => '355'),
05     
     
      [...]
     
06     array( 'name' => 'Pumpe Wärmequelle', 'mask_do_buffer' => 0x80, 'mask_di_buffer' => 0x08,
07            'icon_on' => 'ico_pumpewq_on.png', 'icon_fail' => 'ico_pumpewq_fail.png',
08            'right' => '740', 'top' => '278'),
09     
     
      [...]
     
10     array( 'name' => 'Pumpe Heizung', 'mask_do_buffer' => 0x20, 'mask_di_buffer' => 0x00,
11            'icon_on' => 'ico_pumpehzg_on.png', 'icon_fail' => 'ico_pumpehzg_fail.png',
12            'right' => '240', 'top' => '433'),
13     
     
      [...]
     
14     );
15 
     
      [...]
     
16 
17 foreach ($arrItems as $currItem)
18 {
19     $currIcon = (bindec($arrQ['DO_buffer']) & $currItem['mask_do_buffer'])?$currItem['icon_on']:'spacer.png';
20     if (bindec($arrQ['DI_buffer']) & $currItem['mask_di_buffer']) $currIcon = $currItem['icon_fail'];
21 
22     echo "<div style='position:absolute; right:".$currItem['right']."px; top:".$currItem['top']."px;'>n" .
23          "t<img src='img/".$currIcon."' alt='' width='50' height='40' title='".$currItem['name']."'>n" .
24          "</div>n";
25 }

Die Kurve kriegen

Die Anzeige der Heizkurve ist das wichtigste Instrument zum Konfigurieren der Anlage. Aus den aktuell gesetzten Parametern zeichnet das Skript den linearen Verlauf der Rücklauf-Solltemperatur über den gesamten Außentemperaturbereich. Ein Beispiel: Der Screenshot in Abbildung 1 stammt vom 27. Februar 2010, 23 Uhr, als die Außentemperatur 4,2 Grad Celsius betrug. Gemeinsam mit den anderen Heizeinstellungen folgt daraus eine Mindesttemperatur des Rücklaufs von 24,8 Grad – es ergibt sich kein Heizbedarf, da die Temperatur des Rücklaufwassers gerade 26,4 Grad warm war.

Im Laufe der nächsten Stunden fällt das dunkelblaue Kügelchen kontinuierlich ab. Beim Erreichen der Mindesttemperatur erwärmt die Anlage das Heizungswasser bis zum Wert der zartrosa Linie (Hysterese) und der Vorgang beginnt von Neuem: Bei den Außentemperaturen Ende Februar heizte die Anlage alle zweieinhalb Stunden ungefähr 40 Minuten lang, Abbildung 5 zeigt den Verlauf über 14 Stunden.

Die blaue Heizkurve in Abbildung 1 wartet darauf, über das Frontend neu positioniert zu werden. Ich darf die kippen und verschieben, bis die Heizeinstellungen zu den Außentemperaturbereichen passen. Ein Klick auf das Icon »Werte in die Wärmepumpe übernehmen« trägt die neuen Parameter in die Jobtabelle ein. Der Perl-Daemon hat daraufhin wieder Arbeit.

Abbildung 1: Die Heizkurve beschreibt den Zusammenhang zwischen Außentemperatur und Heizaufwand. In der Praxis beeinflussen sich die Werte heftig.

Abbildung 1: Die Heizkurve beschreibt den Zusammenhang zwischen Außentemperatur und Heizaufwand. In der Praxis beeinflussen sich die Werte heftig.

Blick ins Zeitfenster

Fünf Liniendiagramme veranschaulichen die Verläufe: Temperatur Heizung/Warmwasser, Temperatur Erdsonde ein/aus, Temperatur Vorlauf/Kondensator, Temperatur Wärmequelle aus/Kondensator sowie Wirkungsgrad. Um Code zu sparen, verwende ich das globale Array »$arrLineValues«, das die Eigenschaften jedes Linienwerts über die ganze Anwendung hinweg global definiert. Dazu gehören die Auswahlliste des Select-Statements, die Farbe der Linie und ob diese zu zeichnen ist, wenn die Anlage gerade nicht läuft. Beim Anzeigen der Außentemperatur scheint dies sinnvoll, bei der Vorlauf-Rücklauf-Spreizung dagegen nicht.

Das Array »$arrGraphs« definiert, welche Einträge aus »$arrLineValues« in jedem der fünf oben genannten Liniendiagramme zum Tragen kommen. Über den im Query-String als Parameter enthaltenen Index adressiert, generiert das Skript aus Listing 5 die Diagramme. Diese Vorgangsweise erlaubt es, die Liste jederzeit zu modifizieren oder zu ergänzen. Feinschmecker mit Hang zu individueller Sicht erhalten zudem die Möglichkeit, sich ein Diagramm aus wählbaren Attributen zusammenzustellen.

Die Abbildung 5 zeigt das Diagramm Heizung/Warmwasser. Im Vergleich zur Heizkurve, die zeitlich gesehen einen Schnappschuss darstellt, veranschaulicht das Liniendiagramm über einen längeren Zeitraum die zur Außentemperatur (türkise Linie) korrelierende Rücklauf-Soll-Temperatur (hellblaue Linie). Wird es draußen kälter, steigt die Linie des Sollwerts. Sinkt daraufhin die Temperatur des Istwerts (Rücklauf-Ist, dunkelblaue Linie) unter den Sollwert, beginnt der Heizbetrieb. Die Pumpe leitet warmes Wasser als Vorlauf in den Heizkreislauf und hebt so den Rücklauf bis zum Erreichen des Hysteresewerts an. Die orange Linie zeigt die Vorlauftemperatur an.

Abbildung 5: Im Schnitt heizt die Anlage alle 2,5 Stunden für 40 Minuten, um vier Uhr morgens bereitet sie Warmwasser.

Abbildung 5: Im Schnitt heizt die Anlage alle 2,5 Stunden für 40 Minuten, um vier Uhr morgens bereitet sie Warmwasser.

Das Verfahren bei der Warmwasserbereitung ist einfacher strukturiert: Sinkt die Temperatur des Speichers unter einen Sollwert, dann heizt die Anlage ihn auf. Das ist deutlich erkennbar an der roten Linie und den entsprechend hohen Vor- und Rücklauftemperaturen zwischen 04:00 und 04:45 Uhr.

Die Liniendiagramme ermöglichten es, das Langzeitverhalten der Anlage zu optimieren. Durch gezieltes Anpassen der Hysterese gelang es mir beispielsweise, die Schalthäufigkeiten in eine gute Relation zu den Temperaturschwankungen der Innenräume zu bringen. Denn zu häufige Starts verringern die Lebensdauer des Kompressors.

Listing 5: Auswahl der Parameter
und Zeichnen der Liniendiagramme

01 $arrLineValues = array(
02     tempaussen => array(
03         'attribute'      => 'Aussentemperatur',
04         'select_expr'    => 'm_0100',
05         'color'          => 'cyan3',
06         'idleVals'       => true
07     ),
08 
     
      [...]
     
09     delta_vl_rl => array(
10         'attribute'      => 'Spreizung VL-RL',
11         'select_expr'    => 'abs(m_0105-m_0104)',
12         'color'          => 'green',
13         'idleVals'       => false
14     ),
15 
     
      [...]
     
16     carnot => array(
17         'attribute'      => 'Carnot-Wirkungsgrad',
18         'select_expr'    => '((m_0113+273.15)/((m_0113+273.15)-(m_0112+273.15)))+1',
19         'color'          => 'yellow3',
20         'idleVals'       => false
21     )
22 );
23 
     
      [...]
     
24 $arrGraphs = array(
25     array(
26         'title'      => 'Hzg/Ww',
27         'lines'      => array('tempeinsatz','tempaussen','vorlauf',
28                               'rlSoll','ruecklauf','warmwasser','delta_vl_rl'),
29         'idleVals'   => true
30         ),
31 
     
      [...]
     
32     array(
33         'title'      => 'Carnot-Wirkungsgrad',
34         'lines'      => array('kondensator','verdampfer',
35                               'leistungsaufnahme','carnot'),
36         'idleVals'   => false
37         )
38     );
39 
     
      [...]
     
40 
41 // combination of both arrays builds first part of select-statement
42 $sql = "SELECT tsp_0501_0500 AS tsp";
43 foreach ($arrGraphs[$graphID]['lines'] as $key)
44     $sql .= ','.$arrLineValues[$key]['select_expr'].' AS '.$key;
45 $sql .= " FROM $db_getTable";
46 
     
      [...]
     
47 
48 // iterate through result-set from database ($arrQ)
49 for ($i=$minVal=$maxVal=0; $i < count($arrQ); $i++)
50 {
51     // timestamps for x-axis
52     $arrXLabels[$i]  = $arrQ[$i]['tsp'];
53 
54     // build and fill arrays named after $arrGraphs[$graphID]['lines']
55     foreach ($arrGraphs[$graphID]['lines'] as $key)
56     {
57     // put values dependend on machine activity, mask 0x03 means 'idle'
58     // 'x'-value discontinues line in jpgraph
59         if (!$arrLineValues[$key]['idleVals'] &&
60            (($arrQ[$i]['betriebszustand'] & 0x03)==0))
61         {
62             ${$key}[$i] = 'x';
63         }
64         else
65         {
66             ${$key}[$i] = round($arrQ[$i][$key],1);
67             if ($arrQ[$i][$key] < $minVal) $minVal = $arrQ[$i][$key];
68             if ($arrQ[$i][$key] > $maxVal) $maxVal = $arrQ[$i][$key];
69         }
70     }
71 }
72 
     
      [...]
     
73 
74 $graph = new Graph($width, $height,"auto",1);
75 $graph->img->SetAntiAliasing();
76 $graph->SetScale("datlin",$minVal,ceil($maxVal+($maxVal/20)));
77 
     
      [...]
     
78 
79 // prepare plots
80 foreach ($arrGraphs[$graphID]['lines'] as $key)
81 {
82     $lineplot[$key] = new LinePlot($$key,$arrXLabels);
83     $lineplot[$key]->SetColor($arrLineValues[$key]['color']);
84     $lineplot[$key]->SetLegend($arrLineValues[$key]['attribute']);
85     $graph->Add($lineplot[$key]);
86 }
87 
     
      [...]
     
88 $graph->Stroke();

Statistik statt Stoppuhr

Rücklauftemperatur, Steilheit der Heizkurve, … – die interessanteste Kenngröße eines Heizungssystemens sind und bleiben die Betriebskosten. Ich könnte es mir mit Stoppuhr und Schreibblock bewaffnet im Heizungsraum gemütlich machen. Der Alltagskultur zuträglicher sind zwei Säulendiagramme, welche die Betriebsstunden sowie den Stromverbrauch der letzten beiden Monate anzeigen, getrennt nach Tag- und Nachtstrom sowie Heizungs- und Warmwasserbetrieb (Abbildung 6). Eine Jahresbilanz mit Monatshochrechnung (Abbildung 7) sorgt für den Vergleich über die Saison hinweg.

Abbildung 6: Das Diagramm zeigt den täglichen Stromverbrauch, getrennt nach Tarif sowie Heizungs- und Warmwasserbetrieb.

Abbildung 6: Das Diagramm zeigt den täglichen Stromverbrauch, getrennt nach Tarif sowie Heizungs- und Warmwasserbetrieb.

Abbildung 7: Die heizperiodisch geprägte Jahresbilanz, getrennt nach Betriebsdauer und Stromverbrauch.

Abbildung 7: Die heizperiodisch geprägte Jahresbilanz, getrennt nach Betriebsdauer und Stromverbrauch.

Das Netzdiagramm in Abbildung 8 verdeutlicht die Tagesverteilung der Betriebsstunden. Hier erfahre ich, dass der Stromverbrauch nachmittags beziehungsweise am frühen Abend massiv und in der (billigen) Nachtzeit kümmerlich ist.

Abbildung 8: Das Netzdiagramm der Betriebsstundenverteilung zeigt Optimierungspotenzial: Die Warmwasserbereitung (rot) sollte in der Nacht erfolgen.

Abbildung 8: Das Netzdiagramm der Betriebsstundenverteilung zeigt Optimierungspotenzial: Die Warmwasserbereitung (rot) sollte in der Nacht erfolgen.

Knackpunkt Körperpflege

Klarer Fall: Am Abend baden die Kinder und duschen die Erwachsenen. In der Folge sinkt die Temperatur im Wasserspeicher und das System heizt ihn wieder hoch. Es würde allerhand Stromkosten sparen, wenn dieser Vorgang zum billigen Nachttarif passen würde. Seit einigen Wochen setzt ein Cronjob über »WPsetVal.pl« die Werte im Laufe des Tages derart, dass die Warmwasserbereitung nachts stattfindet.

Die auswertenden PHP-Skripte erwiesen sich als flinke Sortierer und Addierer. Die SQL-Statements jedoch, welche die Daten aus der Datenbank saugen, sind im Gegensatz dazu langsam. Anders als die Liniendiagramme müssen sie lange Zeiträume abgrasen – und dies minutengenau, da ja der oszillierend geschaltete Kompressor die Stromlast bestimmt.

Schneller und sicherer

Seit November 2008 hat der Sammelknecht »WPpolld.pl« Zeile um Zeile fleißig in die Datenbank geschaufelt. Mittlerweile stehen über 675 000 Einträge in der Tabelle »wp_data«, jeder 145 Datenfelder groß. Leider werden die SQL-Anfragen dadurch immer träger und damit das Generieren der Diagramme. Wie bei datenbankgestützten Applikationen üblich rückt nun das Optimieren der SQL-Statements in den Vordergrund.

Die PHP-Skripte, die das Blockschaltbild, die Status-, Warn- und Datentabellen sowie die Heizkurve zeichnen, benötigen aus der Tabelle nur eine Zeile. Die zugehörigen Statements sind recht schnell. Um Verläufe und Statistiken anzuzeigen, sind viel mehr Datensätze nötig. Hier ist es extrem wichtig, die zu verarbeitenden Elemente frühzeitig mit einer durchdachten Where-Klausel auf ein Minimum zu reduzieren. Bei den Bedingungen muss ich mit Indizes arbeiten, besonders wenn Aggregatfunktionen im Spiel sind.

Ein Beispiel: Ohne zeitliche Vorgabe zeigen die Liniendiagramme jeweils die letzten 6 Stunden an. Im ersten Moment elegant wirkt hier die Abfrage:

SELECT * from wp_data ORDER BY tsp DESC LIMIT 0,360

Im Benchmarking auf meinem System schlägt sie aber mit 60,7 Millisekunden zu Buche. Die Where-Klausel

SELECT * from wp_data WHERE tsp > (UNIX_TIMESTAMP()-(6*60*60)) ORDER BY tsp DESC

beschleunigt die Ausgabe um den Faktor 10 auf 6,3 Millisekunden. Wenn ich das Statement von der arithmetischen Operation befreie und den Timestamp-Wert direkt einsetze, erziele ich lediglich eine geringe Verbesserung auf 6,2 Millisekunden, weil die Berechnung in der Where-Klausel sowieso nur einmal passiert.

Heizen, nicht hacken

Obwohl es sich um eine Anwendung für den (im Doppelsinn) Hausgebrauch handelt, ist die Zugriffskontrolle ein Thema für mich. Erlangten Fremde Zugang zur Applikation, könnten sie einerseits den Tagesablauf meiner Familie analysieren und andererseits die Wärmepumpe manipulieren. Der Sheevaplug-Rechner ist darum nur über das Heim-Ethernet erreichbar, das strikt getrennte WLAN nur über einen Open-VPN-Tunnel.

Nach außen schottet ein als Reverse-Proxy konfigurierter Apache die Anwendung ab und blockiert feindliche Requests per Mod_security. Dahinter werkelt IPtables als Paketfilter. Die applikationsspezifische Authentifizierung erfolgt über Auth_digest – je nach angemeldetem Benutzer warten andere Capability Tables. Als weiteren Schutzwall plane ich TLS mit einem Client-Zertifikat einzuführen.

Nützliche Helferlein

Um die Kernkomponenten meines Projekts habe ich diverse Tools gruppiert. So übernimmt die SMS-Alarmierung ein Skript, das auf dem CPAN-Modul Device::Gsm aufsetzt. Eine eigentlich einfache, weil nach der CPAN-Dokumentation gehaltene Angelegenheit. Der recht bescheidene Empfang des per Datenkabel angenabelten Uralt-Handys im Keller (Abbildung 9) macht es aber nötig, vor dem Sendevorgang eine günstige Gelegenheit über ein pollendes »$gsmDevice->signal_quality()« abzuwarten. Erweist sich die Signalqualität auch nach mehreren Versuchen als schlecht, weicht das Skript als Fallback auf einen VoIP-Anbieter im Internet aus.

Abbildung 9: Elektronische Kellerkinder des Autors: Das weiße Kästchen ist der Sheevaplug-Computer. Das Mobiltelefon links oben dient der SMS-Alarmierung.

Abbildung 9: Elektronische Kellerkinder des Autors: Das weiße Kästchen ist der Sheevaplug-Computer. Das Mobiltelefon links oben dient der SMS-Alarmierung.

Legionellen sind Bakterien, die sich gerne in Warmwasseranlagen aufhalten [8]. Wer Aerosol eines damit verseuchten Wassers einatmet, kann schwer erkranken. Am ersten Sonntag im Monat um 19 Uhr stößt ein Cronjob daher eine thermische Desinfektion an: Das Skript heizt den Warmwasserboiler dann bis zur Maximaltemperatur hoch und weist anschließend sowohl meine liebe Frau als auch mich per SMS an, die Brauseköpfe und Wasserhähne zu spülen. Das hält die ungesunden Tierchen auch von den Armaturen fern. Das Säulendiagramm in Abbildung 6 quittierte diese Maßnahme beispielsweise am 7. März mit 5,7 Kilowattstunden Stromverbrauch.

Unser Haus ist südseitig mit großen Fenstern bestückt. Strahlender Sonnenschein erwärmt daher nicht nur unsere Gemüter, sondern auch die Innenräume. Das Heizverhalten der gewöhnlichen Wärmepumpe zeigt sich davon leider unbeeindruckt. Ein weiterer in Perl geschriebener Helfer zapft daher jeden Morgen per HTTP die Wetterauskunft an. Dass die METAR-Meldungen standardisiert [9] sind, macht sich das Modul Geo::METAR zunutze. Sobald der ausgewertete Bewölkungsgrad unter zwei Achteln liegt, senkt das Skript die Solltemperatur des Rücklaufs über die Mittagsstunden ab.

Resümee und Ausblick

Dass ich bei meinem Projekt die Architektur schon im Vorfeld gründlich durchdacht und möglichst skalierbar konzipiert hatte, erwies sich als richtig und verhinderte ein späteres Flickwerk. Wichtig waren mir weiterhin die Fehlerbehandlung, SQL-Optimierung und Sicherheit. Die Wahl der Werkzeuge ist Geschmackssache. Ich bevorzuge Perl, da es elegant und schnell ist und für alle möglichen und unmöglichen Probleme fertige Module breithält.

Heute läuft das System stabil und ist bequem und benutzerfreundlich konfigurierbar. Es liefert informative Statistiken und ermöglicht eine an unsere Lebensgewohnheiten angepasste Kostenersparnis. In den nächsten Monaten wird mir zwar sicher noch die eine oder andere Erweiterung einfallen – das Thema Heizung ist jedoch vorerst vom Tisch. In Anlehnung an die eingangs zitierte Volksweisheit gewinne ich jetzt wieder Zeit zum Kinderzeugen und Bäumepflanzen. (jk)

Infos

[1] Wärmepumpenheizung: [http://de.wikipedia.org/wiki/W%C3%A4rmepumpenheizung]

[2] Vollständige Listings zum Artikel: [ftp://www.linux-magazin.de/pub/listings/magazin/2010/06/Pumpensteuerung]

[3] Plug-Computer von Marvell: [http://www.marvell.com/platforms/plug_computer/], [http://www.sheevaplug.de]

[4] Slackware ARM: [http://www.armedslack.org]

[5] Serielle Geräteserver Nport 5110: [http://de.moxa.com/product/NPort_5110.htm]

[6] Jp Graph [http://www.aditus.nu/jpgraph/]

[7] GD2:[http://www.libgd.org/Main_Page]

[8] Legionellen und deren Bekämpfung: [http://de.wikipedia.org/wiki/Legionellen]

[9] Erläuterungen zu METAR: [http://www.wetterklima.de/flug/metar/Metarcode.htm]

Der Autor

Alexander-Philipp Lintenhofer ist Offizier beim Österreichischen Bundesheer und dort in der Presseabteilung tätig. Nebenberuflich unterrichtet er IT-Security an der Fachhochschule Technikum-Wien. In seiner Freizeit ist der zweifache Vater vorwiegend mit seiner Familie und dem Nestbau beschäftigt.

LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben