Für eine IoT Steuerung gibt es fertige Angebote zu Hauf. Viel mehr Spaß macht es, wenn man das alles selbst entwickelt und so auch volle Kontrolle über seine Daten behält.
Vor einiger Zeit ist meine Heizungssteuerung wegen Überspannung im Stromnetz über den Jordan gegangen – ein alter Analogrechner, einfach durchgeschmort. Eberhard, der genialische Heizungsmonteur, konnte die Heizung zwar wieder in Gang bringen aber nur mit einer Regelung über den Kesselthermostaten. Die Heizung läuft vor sich hin und produziert Wärme, egal, ob diese gebraucht wird oder nicht.
Da die Heizungsanlage sowieso jenseits des "End of Life" ist, habe ich zur Überbrückung, bis die neue Anlage kommt, eine Raspberry Pi Steuerung entwickelt, welche die Umlaufpumpe über einen Funkschalter nach Bedarf ein- und ausschaltet. Features sind Einstellung und Statusanzeige über ein LCD Panel, Nachtabsenkung und Fernsteuerung über Internet – neudeutsch IoT oder Internet of Things.
Diese Anleitung könnt ihr modifiziert für alle möglichen anderen Einsatzzwecke verwenden z.B. Klimaanlage, Lüftung, Rollosteuerung etc., das Prinzip ist meist dasselbe.
Voraussetzungen: Neben den üblichen Voraussetzungen in Bezug auf Programmier- und Linux Kenntnisse auf leicht fortgeschrittenem Niveau, etwas handwerklichem Geschick mit dem Lötkolben und Englischkenntnissen für die Adafruit Anleitungen braucht ihr:
- einen Raspberry Pi – egal welches Modell, ein uralter Raspberry 1 Modell A reicht auch.
- Ein aktuelles Raspbian Betriebssystem – ob Wheezy oder Jessie ist egal.
- einen BMP 180 oder BMP 085 Temperatur- und Luftdrucksensor (z.B. bei Watterott.com), es geht aber auch ein beliebiger anderer Temperatursensor, wenn ihr wisst, wie der angesteuert wird. Die Luftdruckmessung wird hier nicht benötigt, man könnte die Funktion aber für eine kleine Wetterstation verwenden. Der BMP180 hat den Vorteil Temperaturen auf 0,1 °C genau messen zu können. Einfachere Sensoren, z.B. der DHT11 können nur auf ein Grad genau messen.
- einen 433MHz Sender (gibt es bei Watterott) sowie eine Schaltsteckdose (möglichst von Elro) aus dem Baumarkt , von Amazon oder einem Elektronik Versender.
- Ein Adafruit RGB 16×2 LCD+Keypad Kit for Raspberry Pi. RGB muss nicht unbedingt sein, ob die Darstellung positiv oder negativ ist, ist auch egal. Ich empfehle euch, das Teil bei Watterott zu kaufen. Da kommt die Ware schnell und günstig aus Deutschland und nicht aus Fernost. Achtung: Etwas Lötarbeit ist erforderlich.
- optional ein Gehäuse
- Und… nein, ich habe keinerlei Verbindung zu der Firma Watterott. Reichelt.de oder conrad.de sind z.B. sehr gute Alternativen.
Dieses Instructable baut auf den oben erwähnten Komponenten auf; es spricht aber nichts dagegen, es mit anderen Bauteilen zu versuchen. Dann müsst ihr halt etwas herumprobieren. Der Pi arbeitet "headless" im Terminal Modus via Putty oder einem anderen Terminalprogramm und muss nicht an einen Bildschirm angeschlossen sein.
Inhalt
Messung, Steuerung und Bedienung
Damit das alles nicht zu schwierig wird gehen wir Schritt für Schritt vor. Als erstes kümmern wir uns um die.
Anzeige im LCD Panel
Für das Panel gibt es eine sehr gut gemachte englische Anleitung bei Adafruit. Nachdem ihr das Panel gemäß Anleitung zusammengelötet habt, solltet ihr zuerst einmal das I2C Interface aktivieren. I2C braucht ihr nachher auch, um das BMP180 Modul anzusprechen.
I2C ist eine Möglichkeit, angeschlossene Peripherie mit nur drei Leitungen anzusteuern, und wird gebraucht um das Adafruit Panel sowie den BMP180 Sensor anzusteuern. Das geschieht über sudo raspi-config . Dort dann die "Advanced Options" auswählen und im nächsten Screen I2C auswählen und im folgenden Bildschirm die Frage "Would you like the ARM I2C interface to be enabled?" mit "JA" beantworten.
Im nächsten Screen mit OK bestätigen und dann auf die Frage "Would you like the I2C kernel module to be loaded by default?" ebenfalls mit "JA" beantworten. Nochmal bestätigen und dann mit "Finish" alles speichern und anschließend neu booten.
Anschließend haltet ihr euch an folgende englischsprachige Anleitung bei Adafruit: https://learn.adafruit.com/adafruit-16×2-character-lcd-plus-keypad-for-raspberry-pi/usage
Zum Testen verwendet ihr am besten das im Zuge der Installation heruntergeladene Testprogramm char_lcd_plate.py das ihr wie üblich mit chmod +x char_lcd_plate.pylauffähig machen müsst.
Nochwas: Ich habe das Teil mit Raspbian Wheezy entwickelt. Das heißt, alle Programme, die den I2C Anschluss oder sonstige GPIO Funktionen des Pi benutzen, müssen mit einem
sudo vornedran aufgerufen werden. Mit dem neueren Raspbian Jessie ist das nicht mehr erforderlich.
Anschließend wird der
BMP180 Temperatur- und Lufdrucksensor
angeschlossen. Für den Anfang empfehle ich, das erst einmal auf einem Steckbrett (Breadboad) auszuprobieren. Auch hierfür gibt es eine sehr gut gemachte Anleitung bei Adafruit. Achtet beim Installieren der Software und Treiber darauf, die neueste Library aus dem Abschnitt Using the Adafruit BMP Python Library (Updated) zu verwenden.
Die Pinbelegung ist gerade für Einsteiger etwas verwirrend, weil sie keinem erkennbaren Schema folgt. Eine gute Übersicht findet ihr bei Elinux.org. Hier sind die Pins bei allen verschiedenen Varianten A/B oder A+/B+ und 2B beschrieben.
Die i2C Pinbelegung ist vorgegeben und ist wie folgt:
- SDA wird mit Pin 3 verbunden,
- SCL mit Pin 5,
- GND mit Pin 6 für Masse,
- VIN mit Pin 1 – 3,3 V. Auch wenn der Sensor 5 V verträgt solltet ihr bei 3,3 V Betriebsspannnung bleiben
Wir brauchen für später ein kleines Pyhton Skript namens get_temperature.py, das den Temperatursensor anspricht und die Temperatur ausgibt. Den Luftdruck brauchen wir hier nicht.
1 2 3 4 5 |
#!/usr/bin/python import Adafruit_BMP.BMP085 as BMP085 sensor = BMP085.BMP085() print sensor.read_temperature() |
Wenn alles wie gewünscht funktioniert, könnt ihr den Sensor direkt mit dem LCD Panel verlöten oder per Drahtschlinge drekt auf die GPIO Steckkontakte aufschieben und anschließend das Panel aufstecken.
433 MHz Sender
Die Heizungspumpe wird mit einer handelsüblichen 433 MHz Schaltsteckdose ein- und ausgeschaltet.
Der dafür erforderliche Sender hängt am Pi und wird entweder mit den passenden Kontakten auf dem LCD Panel verlötet. Bei mir ist das
- Pin 17 für 3,3V,
- Pin 25 für Masse und
- Pin 26 für GPO 7 – hier könnt ihr auch einen anderen freien Kontakt verwenden und die Anleitung entsprechend verändern
Die Ansteuerung des Senders habe ich bereits im Beitrag 433 MHZ Schaltsteckdosen fernsteuern beschrieben. Wenn ihr diese Anleitung befolgt, kann eigentlich nichts schiefgehen.
Nachdem wir die Hardware entsprechend vorbereit haben, geht es jetzt an die Software:
Python Steuer- und Bedienlogik
Das Programm besteht im Wesentlichen aus einer Endlosschleife, in der laufend gecheckt wird, ob eine der Tasten des LCD Panels gedrückt wurde, innerhalb dieser Schleife wird in regelmäßigen Abständen (timeout) die Temperatur gemessen und bei Bedarf die Schaltsteckdose (de-)aktiviert. Nebenbei wird natürlich die Anzeige im LCD Panel aktualisiert. Obwohl das eigentlich keine besonderen Anforderungen an den Prozessor stellt, liegt die Systemauslastung aufgrund der Endlosschleife bei 40%. Interruptgesteuert wäre das wesentlich eleganter, aber ich wüsste nicht, wie man das mit Python und dem Adafruit Panel hinbekommen sollte.
Das Menü ist 6-stufig und fängt nach Erreichen des letzten Punkts wieder von vorne an:
- Standardanzeige (Datum/Zeit/Isttemperatur/Solltemperatur)
- Einstellen Solltemperatur Tag
- Einstellen Solltemperatur Nacht
- Einstellen Nachtabsenkung von
- Einstellen Nachtabsenkung bis
- Shutdown (zum sauberen Herunterfahren des Pi)
Der Programmablauf ist wie folgt strukturiert:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
Laden der Bibliotheken Initialisieren der Variablen Funktionsblock für Cursortasten Endlosschleife Abfrage ob Taste gedrückt wurde rechts/links schaltet durch die Menüs Tastendruck oben/unten bedeutet Wertänderung. abhängig von Menüposition Temperatur, Zeit ändern bzw. Shutdown einstellen Tastendruck oben/unten wird zwischengespeichert Select Taste speichert Werte in settings.dat Anzeige entsprechend Menüposition einstellen Wenn Timeout erreicht (alle 60 Sekunden) Einstellungen aus Datei settings.dat lesen (diese Datei wird auch von der Web Applikation beschrieben) Temperatur messen Check ob Nachtabsenkungszeitraum - Temperaturbereich Tag/Nacht verwenden Falls Temperatur zu niedrig: Pumpe an Falls Temperatur zu hoch: Pumpe aus |
Hier das ganze Programm namens lcd_menu_integrated.py. Nicht vergessen, das Ganze mit
chmod +x lcd_menu_integrated.py ausführbar zu machen:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 |
#!/usr/bin/python #coding=UTF-8 import math import time import string import Adafruit_CharLCD as LCD import Adafruit_BMP.BMP085 as BMP085 from elropi import RemoteSwitch import os import logging #logging logging.basicConfig(filename='/var/log/heizung.log',level='INFO',format='%(asctime)s %(message)s',filemode='a') ##############INITS################################## # ELRO constants # change device number according to your requirements deviceno=1 # Change the key[] variable below according to the dipswitches on your Elro receivers. default_key = [1,0,0,0,1] # change the GPIO pin according to your wiring default_pin =7 # set parameters for class elropi.py device = RemoteSwitch(device=deviceno,key=default_key,pin=default_pin) # constants hystoff = 0.1 # Lower hysteresis: temperature must be desiredTemp+hysoff to turn pump off hyston = 0.1 # Upper hysteresis: temperature must be desiredTemp+hysoff to turn pump n menu = 0 # Default display: Date-Time/actual temperature - set temperature timeout_start = time.time() # initialize timeout for main loop backlite_start = time.time() # initialize timeout for backlite timeout = 60 # Display refresh and temperature measurement every xxx seconds backlite_timeout = 30 # timeout for backlight menumax = 5 # maximum number of menue-items statsign = "---" # status sign will be >>> when pump is on udaction = "" # init up down keys lcd_ebene0 = "xx" # some value to force new display on startup display = "yy" # some value to force new display on startup prev_display = "" # init display comparison - forces new display shutdown = False # init shutdown value pump_status = False # init pump status santosubito = True # run timer logic at once without waiting for timeout #write pump status to scratch to ensure correct status display in web app statusfile=open("/tmp/status.dat","w") statusfile.write("AUS") statusfile.close() # Initialize the LCD using the pins lcd = LCD.Adafruit_CharLCDPlate() lcd.set_color(1,1,1) # white backlite #initialize BMP085 sensor = BMP085.BMP085() # create custom characters lcd.create_char(1, [7, 5, 7, 0, 0, 0, 0, 0]) # Degree sign lcd.clear() lcd.message("Start...") ####LCD menu structure # rechts/links Schleife über Basis/Tageinstellung/Temp/Nacht Beginn/Nacht Ende/Aus-Ein # 0. Basis Anzeige: # 22:15 23.05.2015 # 20,2°C>>>22,5°C # 1. Temp oben/unten Solltemp +/- 0,5° # Anzeige: Z1: "Tagtemperatur:" Z2: "22,5°C" # Enter speichert # 2. Temp oben/unten Solltemp +/- 0,5° # Anzeige: Z1: "Nachttemperatur:" Z2: "18,5°C" # Enter speichert # 3. oben/unten Zeit +/- 10Min Schritten Start NA # Anzeige: Z1: "Nacht Beginn:" / Z2: "22:15" # enter speichert # 4. oben/unten Zeit +/- 10Min Schritten Ende NA # Anzeige: Z1: "Nacht Ende:" / Z2: "06:15" # Enter speichert # 5. pi runterfahren # Anzeige "Stop Pi" # Enter führt aus #############FUNCTIONS############################# def pretty_time(): # used to display date and time in human readable format act_time = time.localtime() year, month, day, hour, minute, second = act_time[0:6] pretty_time="%02i.%02i.%04i-%02i:%02i" %(day, month, year, hour, minute) return pretty_time def change_temp(dir, temp): # increase/decrease temperature setting if dir == "u": temp = temp + 0.5 elif dir == "d": temp = temp - 0.5 return temp def change_time(dir, ctime): # increase/decrease set time ct = string.split(ctime,":") ctimehour = int(ct[0]) ctimemin = int(ct[1]) if dir == "u": ctimemin = ctimemin + 10 if ctimemin > 50: #carry forward full 60 minutes = + 1 hour, 0 mins ctimemin = 0 ctimehour += 1 if ctimehour > 23: # carry forward 24 hours = 0 hours ctimehour = 0 elif dir == "d": ctimemin = ctimemin - 10 if ctimemin < 0: #carry forward -10 minutes = - 1 hour, 0 mins ctimemin = 50 ctimehour -= 1 if ctimehour < 0: ctimehour = 23 if ctimemin == 0: # ensure double digit minutes (as string) ctimeminstr="00" else: ctimeminstr=str(ctimemin) # convert integer minute to string ctime=str(ctimehour) + ":" + ctimeminstr return ctime def change_shutdown(tf): # shutdown selection if tf == True: tf = False elif tf == False: tf = True return tf # ################################################################################################## ############ Endless loop ######## ############ Cycle through each button and check if it was pressed ######## ############ check timer event, read settings, compare, turn on/off, create default output######## ############ then perform action depending on button and menu position ######## ################################################################################################## while True: ### Menu handling if lcd.is_pressed(LCD.RIGHT): #Move to next menu time.sleep(0.2) backlite_start=time.time() lcd.set_color(1,1,1) menu += 1 if menu>menumax: menu=0 #time.sleep(1) elif lcd.is_pressed(LCD.UP): # store action "up" time.sleep(0.2) lcd.set_color(1,1,1) backlite_start=time.time() udaction = "u" elif lcd.is_pressed(LCD.DOWN): # store action "down" time.sleep(0.2) backlite_start=time.time() lcd.set_color(1,1,1) udaction = "d" elif lcd.is_pressed(LCD.LEFT): # move to previous menu time.sleep(0.2) backlite_start=time.time() lcd.set_color(1,1,1) menu -= 1 if menu < 0: menu=menumax elif lcd.is_pressed(LCD.SELECT): # execute. Save settings or shutdown time.sleep(0.2) backlite_start=time.time() lcd.set_color(1,1,1) if 0 < menu < 5: # save settings and return to main display (menu0) settings = open("/var/www/settings.dat","w") settings.writelines(nstart) settings.writelines(nend) settings.write(str(dtemp) + "\n") settings.write(str(ntemp) + "\n") settings.write(onoff) settings.close() lcd.clear() lcd.message("Werte\ngespeichert") time.sleep(1) menu = 0 santosubito = True if menu == 5: if shutdown: # shutdown lcd.clear() lcd.message("BYE! Wait 30sec\nthen disconnect") for x in range(0,3): device.switchOff() time.sleep(2) os.system("sudo halt") else: menu = 0 santosubito = True ### Main display menu 0 if menu == 0: if lcd_ebene0 <> prev_display: # update display only if changed; this lcd.clear() # avoids display flicker caused by lcd.message(lcd_ebene0) # unnecessary updates prev_display = lcd_ebene0 # repeated for every menu item ###set day temperature if menu == 1: display = 'Solltemperatur\nTag:' + str(dtemp) +'\x01C' if display <> prev_display: # update display only if changed lcd.clear() lcd.message(display) prev_display = display if udaction <> "": dtemp = change_temp(udaction,dtemp) udaction = "" ###set night temperature if menu == 2: display = 'Solltemperatur\nNacht:' + str(ntemp) +'\x01C' if display <> prev_display: # update display only if changed lcd.clear() lcd.message(display) prev_display=display if udaction <> "": ntemp = change_temp(udaction,ntemp) udaction = "" ### set night start if menu == 3: display = 'Nachtabsenkung\nvon ' + nstart.rstrip()+' Uhr' if display <> prev_display: # update display only if changed lcd.clear() lcd.message(display) prev_display = display if udaction <> "": nstart=change_time(udaction,nstart) udaction = "" ### set night end if menu == 4: display = 'Nachtabsenkung\nbis ' + nend.rstrip()+' Uhr' if display <> prev_display: # update display only if changed lcd.clear() lcd.message(display) prev_display = display if udaction <> "": nend=change_time(udaction,nend) udaction = "" ### set shutdown if menu == 5: if shutdown: display = 'SHUTDOWN?\nJA' else: display = 'SHUTDOWN?\nNEIN' if display <> prev_display: # update display only if changed lcd.clear() lcd.message(display) prev_display = display if udaction <> "": shutdown = change_shutdown(shutdown) udaction = "" #display timeout - reset to default menu after given time w/o input if time.time() - backlite_start > backlite_timeout: lcd.set_color(0,0,0) # backlite off menu = 0 shutdown = False # just to go sure... santosubito = True # calculate default display right away backlite_start = time.time() # reset backlite timer ##################################################### ################Timerloop############################ ##################################################### # reads settings (temperature and time profile) # reads temperature sensor # turns on or off pump depending on thresholds # constructs LCD output for default screen if time.time() - timeout_start > timeout or santosubito: # read settings file try: settings = open("/var/www/settings.dat","r") nstart = settings.readline() nend = settings.readline() dtemp = float(settings.readline()) ntemp = float(settings.readline()) onoff = settings.readline() onoff = onoff.rstrip() settings.close() except IOError: # if settings don't exist, take default values nstart = "22:00" nend = "06:00" dtemp = 21.00 ntemp = 18.00 onoff = "AN" # Time handling # time constants act_time = time.localtime() year, month, day, hour, minute, second = act_time[0:6] now = time.mktime(act_time) # "now" has float format zerotime = year, month, day, 0, 0, 0, 0, 0, 0 zerotime=time.mktime(zerotime) # convert night time settings resulting in seconds since midnight of the active day starttime = string.split(nstart,":") nstarthour = int(starttime[0]) nstartmin = int(starttime[1]) nstarttime = int(nstarthour) * 3600 + int(nstartmin) * 60 endtime = string.split(nend,":") nendhour = int(endtime[0]) nendmin = int(endtime[1]) nendtime = int(nendhour) * 3600 + int(nendmin) * 60 nstarttime=nstarttime+zerotime # epoch of night setting start nendtime=nendtime+zerotime # epoch of night setting end if nstarttime > now > nendtime: print "Day" set_temp = dtemp else: print "Night" set_temp = ntemp # read temperature from BMP180 act_temp = float(sensor.read_temperature()) # compare actual temperature with set temperature and change status if necessary if (act_temp > set_temp + hystoff) and pump_status: print "turning off pump" lcd.set_color(0,0,1) # show blue backlite for x in range(0,3): # to go sure the ELRO switch is triggered three times device.switchOff() time.sleep(5) pump_status = False statusfile = open("/tmp/status.dat","w") # needed for web interface status display statusfile.write("AUS") statusfile.close() logging.info('Pump OFF ' + str(act_temp)) elif act_temp < set_temp - hyston and not pump_status: print "turning on pump" lcd.set_color(1,0,0) # show red backlite for x in range(0,3): #to go sure the ELRO switch is triggered three times device.switchOn() time.sleep(5) pump_status = True statusfile=open("/tmp/status.dat","w") statusfile.write("AN") statusfile.close() logging.info('Pump ON ' + str(act_temp)) #construct lcd output if not pump_status: statsign = "---" else: statsign = ">>>" lcd_ebene0 = pretty_time() + '\n' + str(act_temp) + '\x01C' + statsign + str(set_temp) +'\x01C' #print lcd_ebene0 santosubito = False # reset forced timeloop timeout_start = time.time() # reset timeout ######################################################################## ####################### End timer Loop ################################# ######################################################################## |
Die Datei settings.dat wird in /var/www gespeichert, da dort das Webinterface liegt, mit dem wir unsere Heizung auch von unterwegs steuern können. Aus Sicherheitsgründen darf der vom Webserver verwendete User – bei mir ist das www-data – nicht in Dateien außerhalb seines Verzeichnisses hinein schreiben.
Die Datei status.dat wird in das Verzeichnis /tmp geschrieben, da sie nur zur Laufzeit benötigt wird. im Beitrag SD-Karten Verschleiß vermeiden erkläre ich noch, wie man sich häufig ändernde Dateien und Logs ins RAM anstatt auf unsere "Festplatte", die SD-Karte schreibt.
Bei Fragen zum Programm einfach die Kommentarfunktion verwenden.
Das Webinterface steht aus Performancegründen und der Übersichtlichkeit wegen in einem eigenen Beitrag.