Da unsere beiden Funkwetterstationen inzwischen diverse Ausfallerscheinungen zeigen (Außensensor nicht erkannt, tw. falsche Zeit, …), habe ich überlegt mir eine eigene Wetterstation zu basteln.
Dabei wollte ich verschiedene Datenquellen kombinieren und konnte auf bereits vorhandene Teilsysteme zurückgreifen.
Schon seit längerem habe ich einen USB-Datenlogger mit Kombisensor für Außentemperatur und -luftfeuchte am Laufen. Leider gibt es keine kompatiblen Sensoren mehr dafür zu kaufen. Deshalb kann ich die angedachte Erweiterung um einen Regen- und Windsensor nicht mehr umsetzen.
Für die Innentemperatur und den Luftdruck nutze ich gleichfalls schon länger Komponenten von tinkerforge. Die Übertragung erfolgt hier via WLAN.
Anfangs habe ich die empfangenen Daten in eine MySQL-DB geschrieben. Das wurde aber auf Dauer bissel viel, so dass ich irgendwann auf influxdb umgestiegen bin.
Das Dashboard ist dann mit grafana zusammengebastelt.
Die lokale Datenerfassung läuft auf einem intel NUC mit debian, wo auch mein Server-Monitoring mit check_mk läuft (hiermit überwache ich u.a. meine vServer, mein NAS mit openmediavault und meine Sophos-Firewall). Aber ich schweife ab…
Grob sieht die Systemarchitektur wie folgt aus:
Folgende Daten sind lokal erhoben (auf dem NUC):
- Temperatur außen (incl. Min- + Max-Werte sowie Verlauf der letzten 7 Tage)
- Innentemperatur aktuell
- Luftdruckverlauf
Die anderen Werte (5Tages-Vorhersage Temperatur und Regen, aktuelle Regenstärke, Wind) und Bewölkungsbildchen stammen aus der openweatermap-API und werden direkt auf dem Raspi erstellt.
Programmiert ist alles mit Python3. Für die Aneinanderreihung der Vorschau-Icons nutze ich das tool montage aus dem ImageMagick Paket. Die Icons habe ich dazu separat runtergeladen, die Icon-ID kommt über die API und steht in der influxdb.
Und so sieht das derzeitige Ergebnis aus:
Durch das Raster-Layout von grafana ist das 7″ Touch-Display des Raspi sehr schnell “voll”. Also muss ich mich auf die wichtigsten Sachen beschränken. Wahrscheinlich fliegen Wind und aktuelle Regenmenge wieder raus. Mal sehen.
Code für die lokale Datenerfassung (tinkerforge und ELV):
#!/usr/bin/env python3 # -*- coding: utf-8 -*- __author__ = 'fs' import logging import logging.handlers import argparse import sys import time import datetime #: install pyserial, nicht serial! import serial from influxdb import InfluxDBClient from tinkerforge.ip_connection import IPConnection from tinkerforge.brick_master import Master import tinkerforge.bricklet_temperature import tinkerforge.bricklet_humidity import tinkerforge.bricklet_barometer # Deafults LOG_FILENAME = "/var/log/tf/collectweatherdata.log" LOG_LEVEL = logging.DEBUG # Could be e.g. "DEBUG" or "WARNING" # Define and parse command line arguments parser = argparse.ArgumentParser(description="service for inserting tinkerforge bricklet data to influxdb") parser.add_argument("-l", "--log", help="file to write log to (default '" + LOG_FILENAME + "')") # If the log file is specified on the command line then override the default args = parser.parse_args() if args.log: LOG_FILENAME = args.log #### Logging # Configure logging to log to a file, making a new file at midnight and keeping the last 3 day's data # Give the logger a unique name (good practice) logger = logging.getLogger(__name__) # Set the log level to LOG_LEVEL logger.setLevel(LOG_LEVEL) # Make a handler that writes to a file, making a new file at midnight and keeping 3 backups handler = logging.handlers.TimedRotatingFileHandler(LOG_FILENAME, when="midnight", backupCount=3) # Format each log message like this formatter = logging.Formatter('%(asctime)s %(levelname)-8s %(message)s') # Attach the formatter to the handler handler.setFormatter(formatter) # Attach the handler to the logger logger.addHandler(handler) # Make a class we can use to capture stdout and sterr in the log class MyLogger(object): def __init__(self, logger, level): """Needs a logger and a logger level.""" self.logger = logger self.level = level def write(self, message): # Only log if there is a message (not just a new line) if message.rstrip() != "": self.logger.log(self.level, message.rstrip()) def flush(self): pass # Replace stdout with logging to file at INFO level sys.stdout = MyLogger(logger, logging.INFO) # Replace stderr with logging to file at ERROR level sys.stderr = MyLogger(logger, logging.ERROR) step = 60 #elv_ elvport = serial.Serial('/dev/ttyUSB0',9600,timeout = 2); # Hoehe ueber NN fuer LE: altitude_loc = 114 tg = 0.0065 # Korrekturfaktoren Temperatur + Feuchte cft1 = 0.9 cft2 = 0.95 cfh1 = 1.16 cfh2 = 1.25 # initiale definition temp_location = 20.005 #influxdb dbclient = InfluxDBClient(host='localhost', port=8086) dbclient.switch_database('fswetter') # tinkerforge # bricks: tfbrick01_opts = { 'host': "192.168.x.y1", 'port': 4223, 'uid_master': "m1", 'uid_tempbricklet': "nm5", 'uid_humbricklet': "nHB", 'uid_pressbricklet': "qFi" } tfbrick02_opts = { 'host': "192.168.x.y2", 'port': 4223, 'uid_master': "m2", 'uid_tempbricklet': "nqw", 'uid_humbricklet': "nyD" } ipconb1 = IPConnection() # Create IP connection ipconb2 = IPConnection() # Create IP connection # Create device object mb1 = Master(tfbrick01_opts['uid_master'],ipconb1) bt1 = tinkerforge.bricklet_temperature.Temperature(tfbrick01_opts['uid_tempbricklet'], ipconb1) bh1 = tinkerforge.bricklet_humidity.Humidity(tfbrick01_opts['uid_humbricklet'], ipconb1) bp1 = tinkerforge.bricklet_barometer.Barometer(tfbrick01_opts['uid_pressbricklet'], ipconb1) mb2 = Master(tfbrick02_opts['uid_master'],ipconb2) bt2 = tinkerforge.bricklet_temperature.Temperature(tfbrick02_opts['uid_tempbricklet'], ipconb2) bh2 = tinkerforge.bricklet_humidity.Humidity(tfbrick02_opts['uid_humbricklet'], ipconb2) print('======================================') print("Start-Time: " + str(time.time())) # Callback for temperature def cbt1(): try: temperature1 = bt1.get_temperature() print('temp1 raw: ' + str(temperature1/100.0) + ' °C.') vt1 = temperature1/100.0*cft1 print('temp1 cor: ' + str(vt1) + ' °C.') global temp_location temp_location = vt1 print("temp_location: "+str(temp_location)) json_body = [ { "measurement": "Temperatur", "tags": { "location": "innen", "room": "Wohnstube" }, "fields": { "value": vt1 } } ] except: e,v,tb = sys.exc_info() print('error get_temp1') print(e) print(v) else: try: dbclient.write_points(json_body) except: print('influx error temp1') def cbt2(): try: temperature2 = bt2.get_temperature() print('temp2: ' + str(temperature2/100.0) + ' °C.') vt2 = temperature2/100.0*cft2 print('temp2 cor: ' + str(vt2) + ' °C.') json_body = [ { "measurement": "Temperatur", "tags": { "location": "innen", "room": "Schlafstube" }, "fields": { "value": vt2 } } ] except: e,v,tb = sys.exc_info() print('error get_temp2') print(e) print(v) else: try: dbclient.write_points(json_body) except: print('influx error temp2') # Callback for humidity def cbh1(): try: humidity1 = bh1.get_humidity() print('hum1: ' + str(humidity1/10.0) + ' %.') vh1 = humidity1/10.0*cfh1 print('hum1 cor: ' + str(vh1) + ' %.') json_body = [ { "measurement": "Luftfeuchte", "tags": { "location": "innen", "room": "Wohnstube" }, "fields": { "value": vh1 } } ] except: e,v,tb = sys.exc_info() print('error get_hum1') print(e) print(v) else: try: dbclient.write_points(json_body) except: print('influx error hum1') def cbh2(): try: humidity2 = bh2.get_humidity() print('hum2: ' + str(humidity2/10.0) + ' %.') vh2 = humidity2/10.0*cfh2 print('hum2 cor: ' + str(vh2) + ' %.') json_body = [ { "measurement": "Luftfeuchte", "tags": { "location": "innen", "room": "Schlafstube" }, "fields": { "value": vh2 } } ] except: e,v,tb = sys.exc_info() print('error get_hum2') print(e) print(v) else: try: dbclient.write_points(json_body) except: print('influx error hum2') # Callback for air pressure def cbp1(): global temp_location try: air_pressure = bp1.get_air_pressure() print('press1: ' + str(air_pressure/1000.0) + ' mbar (raw).') print('localtemp: ' + str(temp_location)) # temp_nn = temp_location + tg * altitude_loc # value = air_pressure/1000.0/(1-tg * altitude_loc/temp_nn)**(0.03416/tg) value = air_pressure/1000.0/(1 - tg * altitude_loc / (273.15 + temp_location + tg * altitude_loc)) ** (0.034163 / tg) value = round(value,1) print('press1: ' + str(value) + ' mbar (QFF).') json_body = [ { "measurement": "Luftdruck", "tags": { "location": "aussen" }, "fields": { "value": value } } ] except: e,v,tb = sys.exc_info() print('error get_press1') print(e) print(v) else: try: dbclient.write_points(json_body) except: print('influx error press1') def getelv(): try: bline = elvport.readline() sline = bline.decode() except: print('error elv readline') else: if len(sline) > 0: print("elv-input:" + sline) values = sline.split(";") if values[0] == "$1": if values[3] == "": v3 = "0,0" else: v3 = values[3] if values[1] == "": v11 = "0,0" else: v11 = values[11] print("elv: "+v3+" °C ; "+v11+" %") if v3: t1 = float(v3.replace(",",".")) json_body = [ { "measurement": "Temperatur", "tags": { "location": "aussen" }, "fields": { "value": t1 } } ] try: dbclient.write_points(json_body) except: print('influx error t aussen') if v11: h1 = float(v11.replace(",",".")) json_body = [ { "measurement": "Luftfeuchte", "tags": { "location": "aussen" }, "fields": { "value": h1 } } ] try: dbclient.write_points(json_body) except: print('influx error h aussen') else: print('elv-input[0] is empty ($1)') try: ipconb1.connect(tfbrick01_opts['host'], tfbrick01_opts['port']) # Connect to brickd mb1.disable_status_led() except: print("Connection error to brick01") try: ipconb2.connect(tfbrick02_opts['host'], tfbrick02_opts['port']) # Connect to brickd mb2.disable_status_led() except: print("Connection error to brick02") #next_call_time = time.time() while True: cbt1() cbh1() cbt2() cbh2() cbp1() getelv() # if temp_location != 20.005: cbp1() # next_call_time += 1 time.sleep(60) #raw_input('Press key to exit\n') # Use input() in Python 3 ipconb1.disconnect() ipconb2.disconnect()
Code für openweathermap:
#!/usr/bin/env python3 # -*- coding: utf-8 -*- __author__ = 'fs' from influxdb import InfluxDBClient import datetime import logging import logging.handlers import requests import json import subprocess import os import sys # --- Logging logging.basicConfig(format='%(asctime)s %(message)s', filename='/var/log/weather/owm_forecast.log',level=logging.INFO) # --- Defaults openweathermap_api_key = 'xxxxxxxxxxxxxxxxxxxxxx' openweathermap_city_id = '6548737' openweathermap_api_url = 'http://api.openweathermap.org/data/2.5/forecast?id=' + openweathermap_city_id + '&appid=' + openweathermap_api_key + '&units=metric&lang=de' def get_rain_level_from_weather(weather): rain_level = 0 if len(weather) > 0: for w in weather: description = w['description'] condition_icon = w['icon'] condition_id = w['id'] cid = int(condition_id) # leichter Niederschlag if cid == 200 or cid == 210 or cid == 230 or cid == 300 or cid == 310 or cid == 500 or cid == 520 or cid == 600 or cid == 611 or cid == 612 or cid == 613 or cid == 615 or cid == 620: rain_level = 1 # mittlerer Niederschlag elif cid == 201 or cid == 211 or cid == 231 or cid == 301 or cid == 311 or cid == 313 or cid == 321 or cid == 501 or cid == 511 or cid == 521 or cid == 531 or cid == 601 or cid == 616 or cid == 621: rain_level = 2 # starker Niederschlag elif cid == 202 or cid == 212 or cid == 232 or cid == 302 or cid == 312 or cid == 314 or cid == 502 or cid == 522 or cid == 602 or cid == 622: rain_level = 3 # extremer Niederschlag elif cid == 503 or cid == 504: rain_level = 4 return rain_level, condition_icon, cid, description def openweathermap(): data = {} r = requests.get(openweathermap_api_url) if r.status_code == 200: # current_data = r.json() # data['weather'] = current_data['main'] # rain, rain_level = get_rain_level_from_weather(current_data['weather']) # data['weather']['rain'] = rain # data['weather']['rain_level'] = rain_level forecast = r.json()['list'] data['forecast'] = [] for f in forecast: rain_level, condition_icon, condition_id, description = get_rain_level_from_weather(f['weather']) data['forecast'].append({ "dt": f['dt'], "dt_txt": f['dt_txt'], "fields": { "temp": float(f['main']['temp']), "description": description, "icon": condition_icon, "weather_id": condition_id, "rain_level": int(rain_level), "pressure": float(float(f['main']['pressure'])) } }) return data def persists(measurement, fields, time): i=1 logging.info("{} {}".format(time, fields)) json_body = [ { "measurement": measurement, "time": time, "fields": fields } ] influx_client.write_points(json_body) def get_owmdata(): try: out_info = openweathermap() for f in out_info['forecast']: atime = str(f['dt']) + 'Z' persists(measurement='forecast', fields=f['fields'], # time=atime) time=datetime.datetime.utcfromtimestamp(f['dt']).isoformat()) except Exception as err: logging.error(err) influx_client = InfluxDBClient(host='localhost', port=8086) influx_client.switch_database('owmweather') influx_client.query('delete from forecast') get_owmdata() # nur die Tages-Icons #current = influx_client.query('select icon from forecast where icon =~ /.d/;') current = influx_client.query('select icon from forecast;') icons = list(current.get_points()) ii = 1 for ic in icons: imagesourcepath = '/opt/weather-dashboard/grafana/images/'+ic['icon']+'.png' imagedestpath = '/var/www/html/f'+str(ii)+'.png' cmd = 'cp ' + imagesourcepath + ' ' + imagedestpath ii += 1 # print(cmd) subprocess.run([cmd],shell=True) i = 1 picrootpath = '/var/www/html/' cmd = 'montage ' cmd1 = '' while i < ii: cmd1 += picrootpath+'f'+str(i)+'.png ' i += 1 cmd = cmd + cmd1 + ' -mode Concatenate -tile 40x1 -background none '+picrootpath+'forecast.png' #print(cmd) subprocess.run([cmd],shell=True) influx_client.close()