Browse Source

initial checkin after rework of "Jeelink2MQTT"

FloKra 4 years ago
commit
fe4b98a0e7

+ 4 - 0
README.md

@@ -0,0 +1,4 @@
+# JeeLinkLogMQTT
+
+Backend for JeeLabs JeeLink receiver. 
+Receives sensor data from LaCrosse temperature/humidity sensors, logs data to InfluxDB and publishes via MQTT. 

+ 46 - 0
jeelinklog/jeelinklog.ini

@@ -0,0 +1,46 @@
+[main]
+publish_on_mqtt = True
+store_in_influxdb = True
+sensors_config_yml = jeelinklog_sensors.yml
+influx_config_yml = jeelinklog_influxdb.yml
+log_path = /home/pi/logs/jeelinklog
+logfile_new_sensors = jeelink_new_sensors.log
+
+[jeelink]
+serialport = /dev/serial/by-id/usb-FTDI_FT232R_USB_UART_AL01MYTF-if00-port0
+baudrate = 57600
+
+[mqtt]
+enable = true
+server = mqtt.lan
+port = 1883
+user = 
+password = 
+
+# topic prefix
+topic_prefix = LaCrosse
+
+topic_prefix_outside_temphum = wetter/atemphum
+
+# additional single topics for outside temp/hum
+topic_outside_temp = wetter/atemp
+topic_outside_hum = wetter/ahum
+
+# topic for notifications (low battery, new sensor found...)
+subtopic_notify = NOTIFY
+
+[sensors]
+# average sensor values - average over that many received readings
+average_value_steps = 5
+
+max_off_value = 2.0
+
+ignore_off_value_timeout = 60
+
+# interval in s to report data via MQTT
+publish_interval = 60
+
+# interval in s to store data in InfluxDB
+store_interval = 300
+
+data_maxage = 600

+ 776 - 0
jeelinklog/jeelinklog.py

@@ -0,0 +1,776 @@
+#!/usr/bin/python3 -u
+# -*- coding: utf-8 -*-
+#
+
+import serial
+import time
+from time import localtime, strftime
+from datetime import datetime
+
+import configparser
+from influxdb import InfluxDBClient
+
+import os
+import sys
+import paho.mqtt.client as mqtt
+import yaml
+import json
+#import math
+#import numpy as np
+#import httplib
+
+# Change working dir to the same dir as this script
+os.chdir(sys.path[0])
+
+config = configparser.ConfigParser()
+config.read('jeelinklog.ini')
+
+serialport = config['jeelink'].get('serialport')
+serialbaud = int(config['jeelink'].get('baudrate'))
+
+mqtt_server = config['mqtt'].get('server')
+mqtt_port = int(config['mqtt'].get('port'))
+mqtt_user = config['mqtt'].get('user')
+mqtt_password = config['mqtt'].get('password')
+
+mqtt_topic_prefix = config['mqtt'].get('topic_prefix')
+topic_prefix_outside_temphum = config['mqtt'].get('topic_prefix_outside_temphum')
+mqtt_topic_atemp = config['mqtt'].get('topic_outside_temp')
+mqtt_topic_ahum = config['mqtt'].get('topic_outside_hum')
+mqtt_topic_domoticz_in = "domoticz/in"
+
+mqtt_subtopic_notify = config['mqtt'].get('subtopic_notify')
+
+logfile_new_sensors = config['main'].get('logfile_new_sensors')
+path_new_sensors_folder = config['main'].get('path_new_sensors_folder')
+
+log_path = config['main'].get('log_path')
+if not os.path.exists(log_path):
+    os.makedirs(log_path)
+
+verbosemode = False
+
+#sensors_conf_file = "/home/pi/jeelink_sensors.csv"
+
+sensordata_maxage = int(config['sensors'].get('data_maxage'))
+
+#minUpdateInterval = 60
+#aTempHumPublishInterval = 60
+
+#override_updateinterval_on_change = False
+#atemp_sensor_idx = 94
+#atemp_sensor_idx_2 = 113
+
+
+average_value_steps = int(config['sensors'].get('average_value_steps'))
+if average_value_steps == 0:
+    average_value_steps = 1
+
+
+if len(sys.argv) > 1 and str(sys.argv[1]) == "-v":
+    verbosemode = True
+    
+
+def touch(fname, times=None):
+    with open(fname, 'a'):
+        os.utime(fname, times)
+
+def on_connect(client, userdata, flags, rc):
+    if verbosemode:
+        print("MQTT connected with result code " + str(rc) + "\n")
+    #client.subscribe("wetter/atemp")
+
+def on_disconnect(client, userdata, rc):
+    if rc != 0:
+        print("Unexpected MQTT disconnection. Will auto-reconnect\n")
+        
+#def on_message(client, userdata, msg):
+#    #print(msg.topic + " " + str(msg.payload))
+#    global atemp
+#    atemp = msg.payload
+
+# dont edit below
+
+#starting values only..
+##atemp = 61 
+##ahum = 101
+##atemp1 = 61
+##ahum1 = 101
+##atemp2 = 61
+##ahum2 = 101
+##atemp_last = 61
+##ahum_last = 101
+
+checkLastUpdateInterval = 60
+checkLastUpdateInterval_lastRun = 0
+
+lastPublishTime = 0
+lastStoreTime = 0
+
+sensors = dict()
+sensors_id_to_name = dict()
+#sensors_idx = dict()
+sensors_lastTemp = dict()
+sensors_lastHum = dict()
+sensors_lastUpdate = dict()
+sensors_lastReceivedValue_temp = dict()
+sensors_lastReceivedTime = dict()
+sensors_lastValues_temp = dict()
+sensors_lastValues_hum = dict()
+sensors_lastAvgValue_temp = dict()
+sensors_lastAvgValue_hum = dict()
+sensors_lastValues_lastIndex = dict()
+#sensors_unavailable = dict()
+sensors_batteryState = dict()
+sensors_new_alreadyNotified = dict()
+
+sensors_outside_sensors = []
+outSens_lastRun = 0
+outSens_interval = 120
+
+influx_default_fieldname_temperature = 'Temperature'
+influx_default_fieldname_humidity = 'Humidity'
+influx_default_datatype_temperature = 'float'
+influx_default_datatype_temperature = 'int'
+
+sensors_yaml = yaml.load(open(config['main'].get('sensors_config_yml')), Loader=yaml.FullLoader)
+
+def list_duplicates(seq):
+  seen = set()
+  seen_add = seen.add
+  # adds all elements it doesn't know yet to seen and all other to seen_twice
+  seen_twice = set( x for x in seq if x in seen or seen_add(x) )
+  # turn the set into a list (as requested)
+  return list( seen_twice )
+
+if verbosemode:
+    print("JeeLink2MQTT by Flo Kra")
+    print("=======================================================================")
+    print("loading InfluxDB configuration...")
+    
+influxdb_yaml = yaml.load(open(config['main'].get('influx_config_yml')), Loader=yaml.SafeLoader)
+
+if verbosemode: 
+    print("InfluxDB Instances:")
+    print(json.dumps(influxdb_yaml, indent=4))
+
+influxclient = dict()
+for instance in influxdb_yaml:
+    i_host = influxdb_yaml[instance].get('host', None)
+    i_port = int(influxdb_yaml[instance].get('port', 8086))
+    i_username = influxdb_yaml[instance].get('username', None)
+    i_password = influxdb_yaml[instance].get('password', None)
+    i_database = influxdb_yaml[instance].get('database', None)
+    if i_host != None and i_database != None:
+        if i_username != None and i_password != None:
+            influxclient[instance] = InfluxDBClient(i_host, i_port, i_username, i_password, i_database)
+        else:
+            influxclient[instance] = InfluxDBClient(i_host, i_port, i_database)
+            
+    
+if verbosemode:    
+    print("loading sensors configuration: ")
+    print(json.dumps(sensors_yaml, indent=3))
+
+for key in sensors_yaml:
+    #print(key, '->', sensors_yaml[key])
+    if verbosemode: print("Sensor name:", key)
+    sensorName = key
+    if sensors_yaml[key].get('LaCrosseID'):
+        if verbosemode: print("LaCrosseID:", sensors_yaml[key].get('LaCrosseID'))
+        sensorId = sensors_yaml[key].get('LaCrosseID')
+        sensors_id_to_name[sensorId] = sensorName
+        
+        if sensors_yaml[key].get('DomoticzIdx'):
+            if verbosemode: print("DomoticzIdx:", sensors_yaml[key].get('DomoticzIdx'))
+            #sensors_idx[sensorId] = sensors_yaml[key].get('DomoticzIdx')
+            
+        if sensors_yaml[key].get('Topic_Temp'):
+            if verbosemode: print("Topic_Temp:", sensors_yaml[key].get('Topic_Temp'))
+            
+        if sensors_yaml[key].get('Topic_Hum'):
+            if verbosemode: print("Topic_Hum:", sensors_yaml[key].get('Topic_Hum'))
+        
+        if sensors_yaml[key].get('InfluxDB_Instance'):
+            if verbosemode: 
+                print("InfluxDB_Instance:", sensors_yaml[key].get('InfluxDB_Instance'))
+            if sensors_yaml[key].get('InfluxDB_Instance') not in influxdb_yaml:
+                print("Error: invalid InfluxDB instance '" + sensors_yaml[key].get('InfluxDB_Instance') + "' configured for sensor '" + sensorName + "'")
+            
+        if sensors_yaml[key].get('isOutsideTempSensor'):
+            if verbosemode: print("isOutsideTempSensor:", sensors_yaml[key].get('isOutsideTempSensor'))
+            sensors_outside_sensors.append(sensorId)
+        
+        
+    else: 
+        print("WARNING: Sensor " + key + " has no LaCrosseID!")
+    
+        
+    if verbosemode: print()
+    
+#print(json.dumps(sensor))
+#print("sensors_id_to_name =",sensors_id_to_name)
+#print("outside sensors: ", sensors_outside_sensors)
+
+if verbosemode:
+    print("\n")
+    
+    
+mqttc = mqtt.Client()
+mqttc.on_connect = on_connect
+mqttc.on_disconnect = on_disconnect
+##mqttc.on_message = on_message
+
+if mqtt_user != "" and mqtt_password != "":
+    mqttc.username_pw_set(mqtt_user, mqtt_password)
+mqttc.connect(mqtt_server, mqtt_port, 60)
+
+mqttc.loop_start()
+#mqttc.loop_forever()
+
+ser = serial.Serial(port=serialport,
+    baudrate = serialbaud,
+    parity=serial.PARITY_NONE,
+    stopbits=serial.STOPBITS_ONE,
+    bytesize=serial.EIGHTBITS,
+    timeout=1)
+
+
+checkLastUpdateInterval_lastRun = time.time() # first check after timeout expired
+    
+try:
+    while True:
+        msg_was_sent = 0
+        
+        #clear serial buffer to remove junk and noise
+        ser.flushInput()
+        
+        #read buffer until cr/lf
+        serLine = ser.readline().strip()
+        # catch exception on invalid char coming in: UnicodeDecodeError: 'ascii' codec can't decode byte 0xf4 in position 6: ordinal not in range(128)
+        try: 
+            serLine = serLine.decode('ascii')
+        except:
+            serLine = ""
+
+        if(serLine):
+            if serLine.find('OK 9') != -1:
+                if verbosemode:
+                    print(serLine + " = LaCrosse sensor")
+
+                # uns interessieren nur reinkommende Zeilen die mit "OK 9 " beginnen
+                
+                # 0  1 2  3   4   5   6
+                # OK 9 ID XXX XXX XXX XXX
+                # |  | |  |   |   |   |
+                # |  | |  |   |   |   --- Humidity incl. WeakBatteryFlag
+                # |  | |  |   |   |------ Temp * 10 + 1000 LSB
+                # |  | |  |   |---------- Temp * 10 + 1000 MSB
+                # |  | |  |-------------- Sensor type (1 or 2) +128 if NewBatteryFlag
+                # |  | |----------------- Sensor ID
+                # |  |------------------- fix "9"
+                # |---------------------- fix "OK"
+                
+                serLineParts = serLine.split(' ')
+                
+                #addr = serLineParts[2]
+                #addr = "{0:x}".format(int(serLineParts[2]))
+                #addr = hex((int(serLineParts[2])))
+                addr = int(serLineParts[2])
+                addrhex = "{0:x}".format(int(serLineParts[2]))
+                    
+                
+                lastUpdate = sensors_lastUpdate.get(addr, None)
+                lastTemp = sensors_lastTemp.get(addr, None)
+                lastHum = sensors_lastHum.get(addr, None)
+                
+                currentsensor_name = sensors_id_to_name.get(addr, None)
+                                                    
+                
+                # extract sensor data received from JeeLink
+                if int(serLineParts[3]) >= 128: 
+                    batt_new = 1
+                    type = int(serLineParts[3]) - 128
+                else:
+                    batt_new = 0
+                    type = int(serLineParts[3])
+                
+                temp = (int(serLineParts[4])*256 + int(serLineParts[5]) - 1000)/10.0
+                
+                if int(serLineParts[6]) >= 128: 
+                    batt_low = 1
+                    hum = int(serLineParts[6]) - 128
+                else:
+                    batt_low = 0
+                    hum = int(serLineParts[6])
+                    if hum > 100: 
+                        hum = 100
+                
+                if batt_new == 1:
+                    sensors_batteryState[addr] = 2
+                elif batt_low == 1:
+                    sensors_batteryState[addr] = 1
+                else:
+                    sensors_batteryState[addr] = 0
+                
+                lastValues_lastIndex = sensors_lastValues_lastIndex.get(addr, None)
+                
+                if sensors_lastValues_temp.get(addr, None) == None:
+                    sensors_lastValues_temp[addr] = [None] * average_value_steps
+                
+                if sensors_lastValues_hum.get(addr, None) == None:
+                    sensors_lastValues_hum[addr] = [None] * average_value_steps
+                
+                data_okay = False
+                
+                if sensors_lastReceivedValue_temp.get(addr, None) == None:
+                    # this is the first time we receive from that sensor in that session
+                    if verbosemode: print("first received from sensor",str(addr))
+                    sensors_lastReceivedValue_temp[addr] = temp
+                    sensors_lastReceivedTime[addr] = int(time.time())
+                    #lastValues_lastIndex = 0
+                else:
+                    lastValue = sensors_lastReceivedValue_temp.get(addr, None)
+                    
+                    max_off_value = float(config['sensors'].get('max_off_value'))
+                    ignore_off_value_timeout = int(config['sensors'].get('ignore_off_value_timeout'))
+                    
+                    if lastValue != None and ((temp >= (lastValue - max_off_value) and temp <= (lastValue + max_off_value)) or ((int(time.time()) - sensors_lastReceivedTime.get(addr)) > ignore_off_value_timeout)):   # discard off values
+                        sensors_lastReceivedValue_temp[addr] = temp
+                        sensors_lastReceivedTime[addr] = int(time.time())
+                        #print("Last Value=",lastValue,"currValue=",temp)
+                        data_okay = True
+                    else:
+                        if verbosemode: print("skipped sensor reading - Last Value=",lastValue,"currValue=",temp)
+                        
+                
+                if data_okay:
+                    if lastValues_lastIndex == None:
+                        lastValues_lastIndex = 0
+                    elif lastValues_lastIndex == (average_value_steps - 1):
+                        lastValues_lastIndex = 0
+                    else:
+                        lastValues_lastIndex += 1
+                    if verbosemode: print("lastValues_lastIndex =", lastValues_lastIndex)
+                    
+                    sensors_lastValues_lastIndex[addr] = lastValues_lastIndex
+                    sensors_lastValues_temp[addr][lastValues_lastIndex] = temp
+                    sensors_lastValues_hum[addr][lastValues_lastIndex] = hum
+                    
+                    sensors_lastUpdate[addr] = int(time.time())
+                    
+                    if verbosemode: print("sensors_lastValues_temp =", sensors_lastValues_temp[addr])
+                    
+                
+                if currentsensor_name is None: 
+                    if batt_new == 1 and not sensors_new_alreadyNotified.get('addr', False):
+                        notifystr = "NEW sensor with ID " + str(addr)
+                        mqttc.publish(mqtt_topic_prefix+"/" + mqtt_subtopic_notify, notifystr, qos=0, retain=False)
+                        
+                        if not os.path.exists(log_path+'/new'):
+                            os.makedirs(log_path+'/new')
+                            
+                        fname = log_path + '/new/' + str(addr)
+                        if not os.path.exists(fname):
+                            try:
+                                touch(fname)
+                            except:
+                                pass
+                        
+                        try:
+                            f = open(log_path+'/'+logfile_new_sensors, 'a')
+                            f.write(str(datetime.now())+": "+notifystr+"\n")
+                            f.close()
+                        except:
+                            # guat dann hoit ned...
+                            pass
+                            
+                        sensors_new_alreadyNotified[addr] = True
+                    else:
+                        if not os.path.exists(log_path+'/unknown'):
+                            os.makedirs(log_path+'/unknown')
+                        fname = log_path + '/unknown/' + str(addr)
+                        if not os.path.exists(fname):
+                            try:
+                                touch(fname)
+                            except:
+                                pass
+                    
+                    if verbosemode:
+                        print("unknown sensor ID " + str(addr))
+                                    
+                if verbosemode:
+                    print("addr: " + str(addr) + " = 0x" + str(addrhex) + "   batt_new: " + str(batt_new) + "   type: " + str(type) + "   batt_low: " + str(batt_low) + "   temp: " + str(temp) + "   hum: " + str(hum) + "   Name: " + str(currentsensor_name))
+                    print()
+                
+                #if senddata:
+                #    sensors_lastUpdate[str(addr)] = int(time.time())
+                #    sensors_lastTemp[str(addr)] = temp
+                #    sensors_lastHum[str(addr)] = hum
+                #    
+                #    isAtemp = False
+                #    if int(currentsensor_idx) == atemp_sensor_idx:
+                #        atemp1 = temp
+                #        ahum1 = hum
+                #        isAtemp = True
+                #    elif int(currentsensor_idx) == atemp_sensor_idx_2:
+                #        atemp2 = temp
+                #        ahum2 = hum
+                #        isAtemp = True
+                #    
+                #    if isAtemp:
+                #        if atemp1 <= atemp2:
+                #            atemp = atemp1
+                #            ahum = ahum1
+                #        else:
+                #            atemp = atemp2
+                #            ahum = ahum2
+                #        
+                #        if atemp < 61 and ahum < 101:
+                #            if atemp != atemp_last or ahum != ahum_last or ((time.time() - atemphum_lastUpdate) > aTempHumPublishInterval):
+                #                atemphum_lastUpdate = time.time()
+                #                atemp_last = atemp
+                #                ahum_last = ahum
+                #                mqttc.publish(mqtt_topic_atemp, str(atemp), qos=0, retain=True)
+                #                mqttc.publish(mqtt_topic_ahum, str(ahum), qos=0, retain=True)
+                #                mqttc.publish(mqtt_topic_atemphum_lastUpdate, strftime("%Y-%m-%d %H:%M:%S", localtime()), qos=0, retain=False)
+                #    
+                #    domoticz_json = "{\"idx\":" + str(currentsensor_idx) + ",\"nvalue\":0,\"svalue\":\"" + str(temp) + ";" + str(hum) + ";1\"}"
+                #    #if verbosemode:
+                #    #    print(domoticz_json)
+                #    mqttc.publish(mqtt_topic_domoticz_in, domoticz_json, qos=0, retain=False)
+                #    
+                #    mqttc.publish(mqtt_topic_prefix+"/"+str(currentsensor_name)+"/temperature", str(temp), qos=0, retain=False)
+                #    mqttc.publish(mqtt_topic_prefix+"/"+str(currentsensor_name)+"/humidity", str(hum), qos=0, retain=False)
+                #    mqttc.publish(mqtt_topic_prefix+"/"+str(currentsensor_name)+"/battery", str(batterystate), qos=0, retain=False)
+                #    mqttc.publish(mqtt_topic_prefix+"/"+str(currentsensor_name)+"/lastUpdate", strftime("%Y-%m-%d %H:%M:%S", localtime()), qos=0, retain=False)
+                #    mqttc.publish(mqtt_topic_prefix+"/"+str(currentsensor_name)+"/availability", "available", qos=0, retain=False)
+                #    
+                #    lacrosse_json = "{\"temperature\":" + str(temp) + ", \"humidity\":" + str(hum) + ", \"battery\":\"" + str(batterystate) + "\"}"
+                #    mqttc.publish(mqtt_topic_prefix+"/"+str(currentsensor_name)+"/json", lacrosse_json, qos=0, retain=False)
+                #    
+                #    tmptext = str(temp) + "° " + str(hum) + "%"
+                #    mqttc.publish(mqtt_topic_prefix+"/"+str(currentsensor_name)+"/TempHumText", tmptext, qos=0, retain=False)
+                #    
+                #    if verbosemode:
+                #        print("MQTT published")
+                #
+                #    try:
+                #        touch("/tmp/jeelink2mqtt_running")
+                #    except:
+                #        # guat dann ned...
+                #        pass
+                #
+                #else:
+                #    if verbosemode:
+                #        if currentsensor_name is None: 
+                #            print("MQTT published")
+                #        else:
+                #            print("MQTT publishing surpressed (interval not expired)")
+                #
+                #
+                #if verbosemode:
+                #    print("\n")
+        
+        
+        # publish on MQTT on set interval
+        publishNow = False
+        if (int(time.time()) - lastPublishTime) > int(config['sensors'].get('publish_interval')):
+            if lastPublishTime == 0:
+                lastPublishTime = int(time.time())
+            else:
+                publishNow = True
+                lastPublishTime = int(time.time())
+                
+                #for sensor in 
+                #sensors_lastReceivedTime[str(addr)]
+            
+        # store to InfluxDB on set interval
+        storeNow = False
+        if (int(time.time()) - lastStoreTime) > int(config['sensors'].get('store_interval')):
+            if lastStoreTime == 0:
+                lastStoreTime = int(time.time())
+            else:
+                storeNow = True
+                lastStoreTime = int(time.time())
+        
+        if publishNow or storeNow:
+            #print("available sensor data: ", sensors_lastUpdate)
+            for id in sensors_lastUpdate:
+            
+                if (time.time() - sensors_lastUpdate[id]) < sensordata_maxage:
+                    s_name = sensors_id_to_name.get(id, None)
+                    if verbosemode: print("current data available for:", id, s_name)
+                    
+                    sensorDataComplete = True
+                    
+                    sum_temp=0
+                    for val in sensors_lastValues_temp[id]:
+                        if val is None:
+                            sensorDataComplete = False
+                        else:
+                            sum_temp = sum_temp + val
+                            
+                    sum_hum=0
+                    for val in sensors_lastValues_hum[id]:
+                        if val is None:
+                            sensorDataComplete = False
+                        else:
+                            sum_hum = sum_hum + val
+                    
+                    if sensorDataComplete:
+                        s_currAvgTemp = round(sum_temp / len(sensors_lastValues_temp[id]), 1)
+                        s_currAvgHum = int(sum_hum / len(sensors_lastValues_hum[id]))
+                        sensors_lastAvgValue_temp[id] = s_currAvgTemp
+                        sensors_lastAvgValue_hum[id] = s_currAvgHum
+                        if verbosemode: print("s_currAvgTemp =", s_currAvgTemp, "s_currAvgHum =", s_currAvgHum)
+                    else:
+                        if verbosemode: print("s_currAvgTemp/s_currAvgHum: not yet enough readings available")
+                    
+                    if sensorDataComplete:
+                        
+                        if sensors_batteryState.get(id,None) == 2:
+                            s_battNew = True
+                            s_battState = "NEW"
+                        elif sensors_batteryState.get(id, None) == 1:
+                            s_battNew = False
+                            s_battState = "LOW"
+                        elif sensors_batteryState.get(id, None) == 0:
+                            s_battNew = False
+                            s_battState = "OK"
+                        
+                        
+                        if s_name is not None:
+                            s_domIdx = sensors_yaml[s_name].get('DomoticzIdx', None)
+                            s_topic_temp = sensors_yaml[s_name].get('Topic_Temp', None)
+                            s_topic_hum = sensors_yaml[s_name].get('Topic_Hum', None)
+                            s_influxInstance = sensors_yaml[s_name].get('InfluxDB_Instance', None)
+                            s_isOutsideTempSensor = sensors_yaml[s_name].get('isOutsideTempSensor', None)
+                            
+                            if s_domIdx is not None and publishNow:
+                                domoticz_json = "{\"idx\":" + str(s_domIdx) + ",\"nvalue\":0,\"svalue\":\"" + str(s_currAvgTemp) + ";" + str(s_currAvgHum) + ";1\"}"
+                                if verbosemode:
+                                    print("Domoticz JSON:", domoticz_json)
+                                mqttc.publish(mqtt_topic_domoticz_in, domoticz_json, qos=0, retain=False)
+                                
+                            if s_topic_temp is not None and len(s_topic_temp)>5 and publishNow:
+                                if verbosemode: print("publishing temp on ", s_topic_temp)
+                                mqttc.publish(s_topic_temp, str(s_currAvgTemp), qos=0, retain=False)
+                                
+                            if s_topic_hum is not None and len(s_topic_hum)>5 and publishNow:
+                                if verbosemode: print("publishing hum on ", s_topic_temp)
+                                mqttc.publish(s_topic_hum, str(s_currAvgHum), qos=0, retain=False)
+                            
+                            if publishNow:
+                                mqttc.publish(mqtt_topic_prefix+"/"+ s_name +"/temperature", str(s_currAvgTemp), qos=0, retain=False)
+                                mqttc.publish(mqtt_topic_prefix+"/"+ s_name +"/humidity", str(s_currAvgHum), qos=0, retain=False)
+                                mqttc.publish(mqtt_topic_prefix+"/"+ s_name +"/battery", s_battState, qos=0, retain=False)
+                                mqttc.publish(mqtt_topic_prefix+"/"+ s_name +"/availability", "available", qos=0, retain=False)
+                                mqttc.publish(mqtt_topic_prefix+"/"+ s_name +"/lastUpdate", strftime("%Y-%m-%d %H:%M:%S", localtime()), qos=0, retain=False)
+                                
+                                lacrosse_json = "{\"temperature\":" + str(s_currAvgTemp) + ", \"humidity\":" + str(s_currAvgHum) + ", \"battery\":\"" + str(s_battState) + "\"}"
+                                mqttc.publish(mqtt_topic_prefix+"/"+ s_name +"/json", lacrosse_json, qos=0, retain=False)
+                                
+                                tmptext = str(s_currAvgTemp) + "° " + str(s_currAvgHum) + "%"
+                                mqttc.publish(mqtt_topic_prefix+"/"+ s_name +"/TempHumText", tmptext, qos=0, retain=False)
+                                
+                            if s_influxInstance is not None and storeNow:
+                                ### write to InfluxDB here
+                                
+                                if s_influxInstance in influxdb_yaml:
+                                    influx_measurement = influxdb_yaml[s_influxInstance].get('measurement', None)
+                                
+                                    t_utc = datetime.utcnow()
+                                    t_str = t_utc.isoformat() + 'Z'
+                                    
+                                    if influx_measurement is not None:
+                                        influx_fieldnames = influxdb_yaml[s_influxInstance].get('fieldnames', None)
+                                        if influx_fieldnames == None:
+                                            influx_fieldname_temperature = influx_default_fieldname_temperature
+                                            influx_fieldname_humidity = influx_default_fieldname_humidity
+                                        else:
+                                            if influxdb_yaml[s_influxInstance]['fieldnames'].get('temperature', None) != None:
+                                                influx_fieldname_temperature = influxdb_yaml[s_influxInstance]['fieldnames'].get('temperature')
+                                            else:
+                                                influx_fieldname_temperature = influx_default_fieldname_temperature
+                                            
+                                            if influxdb_yaml[s_influxInstance]['fieldnames'].get('humidity', None) != None:
+                                                influx_fieldname_humidity = influxdb_yaml[s_influxInstance]['fieldnames'].get('humidity')
+                                            else:
+                                                influx_fieldname_humidity = influx_default_fieldname_humidity
+                                                
+                                        influx_datatypes = influxdb_yaml[s_influxInstance].get('datatypes', None)
+                                        if influx_datatypes == None:
+                                            influx_datatype_temperature = influx_default_datatype_temperature
+                                            influx_datatype_humidity = influx_default_datatype_humidity
+                                        else:
+                                            if influxdb_yaml[s_influxInstance]['datatypes'].get('temperature', None) != None:
+                                                tmpdt = influxdb_yaml[s_influxInstance]['datatypes'].get('temperature')
+                                                if tmpdt == 'float' or tmpdt == 'int':
+                                                    influx_datatype_temperature = tmpdt
+                                                else:
+                                                    influx_datatype_temperature = influx_default_datatype_temperature
+                                            else:
+                                                influx_datatype_temperature = influx_default_datatype_temperature
+                                            
+                                            if influxdb_yaml[s_influxInstance]['datatypes'].get('humidity', None) != None:
+                                                tmpdt = influxdb_yaml[s_influxInstance]['datatypes'].get('humidity')
+                                                if tmpdt == 'float' or tmpdt == 'int':
+                                                    influx_datatype_humidity = tmpdt
+                                                else:
+                                                    influx_datatype_humidity = influx_default_datatype_humidity
+                                            else:
+                                                influx_datatype_humidity = influx_default_datatype_humidity
+                                                
+                                        if influx_datatype_temperature == 'int':
+                                            influx_value_temp = int(s_currAvgTemp)
+                                        else:
+                                            influx_value_temp = float(s_currAvgTemp)
+                                        
+                                        if influx_datatype_humidity == 'int':
+                                            influx_value_hum = int(s_currAvgHum)
+                                        else:
+                                            influx_value_hum = float(s_currAvgHum)
+                                        
+                                        influx_json = [
+                                            {
+                                                'measurement': influx_measurement,
+                                                'tags': {
+                                                    'sensor': s_name
+                                                },
+                                                'time': t_str,
+                                                'fields': {
+                                                    influx_fieldname_temperature: influx_value_temp,
+                                                    influx_fieldname_humidity: influx_value_hum
+                                                }
+                                            }
+                                        ]
+                                        
+                                        try:
+                                            if verbosemode: print("write to InfluxDB...")
+                                            influxclient[s_influxInstance].write_points(influx_json)
+                                            if verbosemode: print("DONE!")
+                                        except Exception as e:
+                                            print("Error writing to InfluxDB")
+                                            print(e)
+                                else: 
+                                    if verbosemode:
+                                        print("Error: invalid InfluxDB instance '" + s_influxInstance + "' configured for sensor '" + s_name + "'")
+                                
+                            
+                        else: # this is an unknown sensor
+                            if publishNow:
+                                if s_battNew:
+                                    mqttc.publish(mqtt_topic_prefix+"/NewUnknownSensor/"+str(id)+"/temperature", str(s_currAvgTemp), qos=0, retain=False)
+                                    mqttc.publish(mqtt_topic_prefix+"/NewUnknownSensor/"+str(id)+"/humidity", str(s_currAvgHum), qos=0, retain=False)
+                                else:
+                                    mqttc.publish(mqtt_topic_prefix+"/UnknownSensor/"+str(id)+"/temperature", str(s_currAvgTemp), qos=0, retain=False)
+                                    mqttc.publish(mqtt_topic_prefix+"/UnknownSensor/"+str(id)+"/humidity", str(s_currAvgHum), qos=0, retain=False)
+                                    mqttc.publish(mqtt_topic_prefix+"/UnknownSensor/"+str(id)+"/battery", s_battState, qos=0, retain=False)
+                    if verbosemode: print()
+        
+        # outside sensors
+        if (int(time.time()) - outSens_lastRun) > outSens_interval:
+            outSens_lastRun = int(time.time())
+            
+            sum_out_sensors_temp = 0
+            sum_out_sensors_temp_min = 100
+            sum_out_sensors_temp_max = -50
+            sum_out_sensors_hum = 0
+            sum_out_sensors_hum_min = 100
+            sum_out_sensors_hum_max = -50
+            count_used_out_sensors = 0
+            
+            for id in sensors_outside_sensors:
+                lupd = sensors_lastUpdate.get(id, None)
+                tdiff = 0
+                if lupd is not None:
+                    tdiff = int(time.time()) - lupd
+                    
+                if lupd is not None and (tdiff < sensordata_maxage):
+                    tmpval_t = sensors_lastAvgValue_temp.get(id, None)
+                    tmpval_h = sensors_lastAvgValue_hum.get(id, None)
+                    if tmpval_t is not None and tmpval_h is not None:
+                        sum_out_sensors_temp = sum_out_sensors_temp + tmpval_t
+                        sum_out_sensors_hum = sum_out_sensors_hum + tmpval_h
+                        
+                        if tmpval_t < sum_out_sensors_temp_min:
+                            sum_out_sensors_temp_min = tmpval_t
+                        if tmpval_t < sum_out_sensors_hum_min:
+                            sum_out_sensors_hum_min = tmpval_h
+                        
+                        if tmpval_t > sum_out_sensors_temp_max:
+                            sum_out_sensors_temp_max = tmpval_t
+                        if tmpval_t > sum_out_sensors_hum_max:
+                            sum_out_sensors_hum_max = tmpval_h                        
+                        
+                        count_used_out_sensors += 1
+            
+            lacrosse_json = None
+            
+            if count_used_out_sensors > 0:
+                out_temp_avg = round(sum_out_sensors_temp / count_used_out_sensors, 1)
+                out_hum_avg = int(round(sum_out_sensors_hum / count_used_out_sensors))
+                
+                mqttc.publish(mqtt_topic_atemp, str(out_temp_avg), qos=0, retain=True)
+                mqttc.publish(mqtt_topic_ahum, str(out_hum_avg), qos=0, retain=True)
+                
+                mqttc.publish(topic_prefix_outside_temphum + '/temperature', str(out_temp_avg), qos=0, retain=True)
+                mqttc.publish(topic_prefix_outside_temphum + '/humidity', str(out_hum_avg), qos=0, retain=True)
+                mqttc.publish(topic_prefix_outside_temphum + '/lastUpdate', strftime("%Y-%m-%d %H:%M:%S", localtime()), qos=0, retain=True)
+                                                
+                tmptext = str(out_temp_avg) + "° " + str(out_hum_avg) + "%"
+                mqttc.publish(topic_prefix_outside_temphum + "/TempHumText", tmptext, qos=0, retain=False)
+            
+                lacrosse_json = "{\"temperature\":" + str(out_temp_avg) + ", \"humidity\":" + str(out_hum_avg) + "\", \"usedSensors\":" + str(count_used_out_sensors) + "}"
+            
+            min = 100
+            max = 100
+            if count_used_out_sensors > 1:
+                mqttc.publish(topic_prefix_outside_temphum + '/usedSensors', str(count_used_out_sensors), qos=0, retain=True)
+                
+                lacrosse_json = "{\"temperature\":" + str(out_temp_avg) + ", \"humidity\":" + str(out_hum_avg) + ", \"usedSensors\":" + str(count_used_out_sensors)
+                
+                if sum_out_sensors_temp_min < 100:
+                    mqttc.publish(topic_prefix_outside_temphum + '/min', str(sum_out_sensors_temp_min), qos=0, retain=False)
+                    lacrosse_json = lacrosse_json + ", \"temp_min\":" + str(sum_out_sensors_temp_min)
+                if sum_out_sensors_temp_max > -50:
+                    mqttc.publish(topic_prefix_outside_temphum + '/max', str(sum_out_sensors_temp_max), qos=0, retain=False)
+                    lacrosse_json = lacrosse_json + ", \"temp_max\":" + str(sum_out_sensors_temp_max)
+                if sum_out_sensors_hum_min < 100:
+                    mqttc.publish(topic_prefix_outside_temphum + '/min', str(sum_out_sensors_hum_min), qos=0, retain=False)
+                    lacrosse_json = lacrosse_json + ", \"hum_min\":" + str(sum_out_sensors_hum_min)
+                if sum_out_sensors_hum_max > -50:
+                    mqttc.publish(topic_prefix_outside_temphum + '/max', str(sum_out_sensors_hum_max), qos=0, retain=False)
+                    lacrosse_json = lacrosse_json + ", \"hum_max\":" + str(sum_out_sensors_hum_max)
+                
+                lacrosse_json = lacrosse_json + "}"
+            
+            if lacrosse_json is not None:
+                mqttc.publish(topic_prefix_outside_temphum + "/json", lacrosse_json, qos=0, retain=False)
+                
+        
+        # handle outdated sensor values once a minute
+        if (int(time.time()) - checkLastUpdateInterval_lastRun) > checkLastUpdateInterval:
+            checkLastUpdateInterval_lastRun = int(time.time())
+            #print("check lastUpdate")
+            for key in sensors_yaml:
+                #print(key, '->', sensors_yaml[key])
+                #print("Sensor name:", key)
+                sensorId = sensors_yaml[key].get('LaCrosseID', None)
+                if sensorId is not None:
+                    lupd = sensors_lastUpdate.get(sensorId, None)
+                    tdiff = 0
+                    if lupd is not None:
+                        tdiff = int(time.time()) - lupd
+                        
+                    if lupd is None or (tdiff > sensordata_maxage):
+                        
+                        mqttc.publish(mqtt_topic_prefix+"/"+ key +"/availability", "unavailable", qos=0, retain=False)
+                        notifystr = "received no data from sensor '" + key + "' with ID " + str(sensorId) + " for " + str(tdiff) + "s"
+                        mqttc.publish(mqtt_topic_prefix+"/" + mqtt_subtopic_notify, notifystr, qos=0, retain=False)
+            
+
+except KeyboardInterrupt:
+    print('\n')
+    exit()

+ 13 - 0
jeelinklog/jeelinklog.service

@@ -0,0 +1,13 @@
+[Unit]
+Description=JeeLinkLog
+StartLimitInterval=0
+
+[Service]
+Type=simple
+Restart=always
+RestartSec=1
+ExecStart=/home/pi/jeelinklog/jeelinklog.py
+User=pi
+
+[Install]
+WantedBy=multi-user.target

+ 26 - 0
jeelinklog/jeelinklog_influxdb.yml

@@ -0,0 +1,26 @@
+wetter:
+  host: localhost
+  port: 8086
+  #username: jeelinklog
+  #password: 
+  database: Wetter
+  measurement: TempHum
+  fieldnames:
+    temperature: temp
+    humidity: hum
+  datatypes:
+    temperature: float
+    humidity: int
+test:
+  host: localhost
+  port: 8086
+  #username: jeelinklog
+  #password: 
+  database: test
+  measurement: TempHum
+  fieldnames:
+    temperature: temp
+    humidity: hum
+  datatypes:
+    temperature: float
+    humidity: int

+ 44 - 0
jeelinklog/jeelinklog_sensors.yml

@@ -0,0 +1,44 @@
+Garten:
+  LaCrosseID: 18
+  DomoticzIdx: 94
+  #Topic_Temp: "Test/Atemp"
+  #Topic_Hum: "Test/Ahum"
+  InfluxDB_Instance: wetter
+  isOutsideTempSensor: true
+Parkplatz:
+  LaCrosseID: 30
+  DomoticzIdx: 113
+  #Topic_Temp: ""
+  #Topic_Hum: ""
+  InfluxDB_Instance: wetter
+  isOutsideTempSensor: true
+Stiegenhaus:
+  LaCrosseID: 40
+  DomoticzIdx: 264
+  #Topic_Temp: ""
+  #Topic_Hum: ""
+  InfluxDB_Instance: KS61
+T5-Arbeitszimmer:
+  LaCrosseID: 45
+  DomoticzIdx: 1
+  #Topic_Temp: ""
+  #Topic_Hum: ""
+  InfluxDB_Instance: KS61T5
+T5-Bad:
+  LaCrosseID: 19
+  DomoticzIdx: 4
+  #Topic_Temp: ""
+  #Topic_Hum: ""
+  InfluxDB_Instance: KS61T5
+T5-Balkon:
+  LaCrosseID: 22
+  DomoticzIdx: 88
+  #Topic_Temp: ""
+  #Topic_Hum: ""
+  InfluxDB_Instance: wetter
+T5-Kueche:
+  LaCrosseID: 55
+  DomoticzIdx: 6
+  #Topic_Temp: ""
+  #Topic_Hum: ""
+  InfluxDB_Instance: KS61T5