jeelink2mqtt.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. #!/usr/bin/python3 -u
  2. # -*- coding: utf-8 -*-
  3. #
  4. import serial
  5. import time
  6. from time import localtime, strftime
  7. import os
  8. import sys
  9. import paho.mqtt.client as mqtt
  10. #import json
  11. #import math
  12. #import numpy as np
  13. #import httplib
  14. serialport = '/dev/serial/by-id/usb-FTDI_FT232R_USB_UART_AL01MYTF-if00-port0'
  15. serialbaud = 57600
  16. mqtt_server = "mqtt.lan"
  17. mqtt_port = 1883
  18. mqtt_user = ""
  19. mqtt_password = ""
  20. mqtt_topic_prefix = "LaCrosse"
  21. mqtt_topic_atemp = "wetter/atemp"
  22. mqtt_topic_ahum = "wetter/ahum"
  23. mqtt_topic_atemphum_lastUpdate = "wetter/atemphum_lastUpdate"
  24. mqtt_topic_domoticz_in = "domoticz/in"
  25. verbosemode = False
  26. sensors_conf_file = "/home/pi/jeelink_sensors.csv"
  27. sensordata_maxage = 300
  28. minUpdateInterval = 60
  29. aTempHumPublishInterval = 60
  30. override_updateinterval_on_change = False
  31. atemp_sensor_idx = 94
  32. atemp_sensor_idx_2 = 113
  33. if len(sys.argv) > 1 and str(sys.argv[1]) == "-v":
  34. verbosemode = True
  35. def touch(fname, times=None):
  36. with open(fname, 'a'):
  37. os.utime(fname, times)
  38. def on_connect(client, userdata, flags, rc):
  39. if verbosemode:
  40. print("MQTT connected with result code " + str(rc) + "\n")
  41. #client.subscribe("wetter/atemp")
  42. def on_disconnect(client, userdata, rc):
  43. if rc != 0:
  44. print("Unexpected MQTT disconnection. Will auto-reconnect\n")
  45. #def on_message(client, userdata, msg):
  46. # #print(msg.topic + " " + str(msg.payload))
  47. # global atemp
  48. # atemp = msg.payload
  49. # dont edit below
  50. #starting values only..
  51. atemp = 61
  52. ahum = 101
  53. atemp1 = 61
  54. ahum1 = 101
  55. atemp2 = 61
  56. ahum2 = 101
  57. atemp_last = 61
  58. ahum_last = 101
  59. checkLastUpdateInterval = 60
  60. checkLastUpdateInterval_lastRun = 0
  61. sensors = {}
  62. sensors_idx = {}
  63. sensors_lastTemp = {}
  64. sensors_lastHum = {}
  65. sensors_lastUpdate = {}
  66. sensors_unavailable = {}
  67. if verbosemode:
  68. print("JeeLink2MQTT by Flo Kra")
  69. print("=======================================================================")
  70. print("loading sensors assignment: ")
  71. with open(sensors_conf_file, "r") as sensorscsv:
  72. for line in sensorscsv:
  73. if line.find('ID,DomoticzIdx,Name') == -1:
  74. # csv file header filtern
  75. line = line.strip('\r')
  76. line = line.strip('\n')
  77. parts = line.split(',')
  78. sensorId = parts[0]
  79. domoticzIdx = parts[1]
  80. sensorName = parts[2]
  81. sensors[str(sensorId)] = str(sensorName)
  82. sensors_idx[str(sensorId)] = str(domoticzIdx)
  83. sensors_lastUpdate[str(sensorId)] = 0
  84. sensors_unavailable[str(sensorId)] = 1 #will be overwritten when first value is received
  85. if verbosemode:
  86. idhex = "{0:x}".format(int(sensorId))
  87. print("Sensor " + sensorId + " = 0x" + str(idhex) + ", Idx = " + str(domoticzIdx) + ", Name = '" + sensorName + "'")
  88. if verbosemode:
  89. print("\n")
  90. mqttc = mqtt.Client()
  91. mqttc.on_connect = on_connect
  92. mqttc.on_disconnect = on_disconnect
  93. ##mqttc.on_message = on_message
  94. if mqtt_user != "" and mqtt_password != "":
  95. mqttc.username_pw_set(mqtt_user, mqtt_password)
  96. mqttc.connect(mqtt_server, mqtt_port, 60)
  97. mqttc.loop_start()
  98. #mqttc.loop_forever()
  99. ser = serial.Serial(port=serialport,
  100. baudrate = serialbaud,
  101. parity=serial.PARITY_NONE,
  102. stopbits=serial.STOPBITS_ONE,
  103. bytesize=serial.EIGHTBITS,
  104. timeout=1)
  105. #sensors = {'4':'Arbeitszimmer','16':'AussenGarten','60':'AussenParkplatz','50':'Bad','39':'Balkon','55':'Kueche','40':'Schlafzimmer'}
  106. #sensors_idx = {'4':'1','16':'94','60':'113','50':'4','39':'88','55':'6','40':'3'}
  107. #if verbosemode:
  108. # print(sensors)
  109. # print(sensors_idx)
  110. checkLastUpdateInterval_lastRun = time.time() # first check after 1 min
  111. try:
  112. while True:
  113. msg_was_sent = 0
  114. #clear serial buffer to remove junk and noise
  115. ser.flushInput()
  116. #read buffer until cr/lf
  117. serLine = ser.readline().strip()
  118. # catch exception on invalid char coming in: UnicodeDecodeError: 'ascii' codec can't decode byte 0xf4 in position 6: ordinal not in range(128)
  119. try:
  120. serLine = serLine.decode('ascii')
  121. except:
  122. serLine = ""
  123. if(serLine):
  124. if serLine.find('OK 9') != -1:
  125. if verbosemode:
  126. print(serLine + " = LaCrosse sensor")
  127. # uns interessieren nur reinkommende Zeilen die mit "OK 9 " beginnen
  128. # 0 1 2 3 4 5 6
  129. # OK 9 ID XXX XXX XXX XXX
  130. # | | | | | | |
  131. # | | | | | | --- Humidity incl. WeakBatteryFlag
  132. # | | | | | |------ Temp * 10 + 1000 LSB
  133. # | | | | |---------- Temp * 10 + 1000 MSB
  134. # | | | |-------------- Sensor type (1 or 2) +128 if NewBatteryFlag
  135. # | | |----------------- Sensor ID
  136. # | |------------------- fix "9"
  137. # |---------------------- fix "OK"
  138. serLineParts = serLine.split(' ')
  139. #addr = serLineParts[2]
  140. #addr = "{0:x}".format(int(serLineParts[2]))
  141. #addr = hex((int(serLineParts[2])))
  142. addr = int(serLineParts[2])
  143. addrhex = "{0:x}".format(int(serLineParts[2]))
  144. lastUpdate = sensors_lastUpdate.get(str(addr), None)
  145. lastTemp = sensors_lastTemp.get(str(addr), None)
  146. lastHum = sensors_lastHum.get(str(addr), None)
  147. currentsensor_idx = sensors_idx.get(str(addr),None)
  148. currentsensor_name = sensors.get(str(addr), None)
  149. if int(serLineParts[3]) >= 128:
  150. batt_new = 1
  151. type = int(serLineParts[3]) - 128
  152. else:
  153. batt_new = 0
  154. type = int(serLineParts[3])
  155. temp = (int(serLineParts[4])*256 + int(serLineParts[5]) - 1000)/10.0
  156. if int(serLineParts[6]) >= 128:
  157. batt_low = 1
  158. hum = int(serLineParts[6]) - 128
  159. else:
  160. batt_low = 0
  161. hum = int(serLineParts[6])
  162. if hum > 100:
  163. hum = 100
  164. if batt_low == 0:
  165. batterystate = "ok"
  166. else:
  167. batterystate = "low"
  168. senddata = False
  169. if currentsensor_idx is not None:
  170. if override_updateinterval_on_change:
  171. if lastTemp != temp or lastHum != hum:
  172. senddata = True
  173. if verbosemode:
  174. print("override interval (value changed): " + str(temp) + " != " + str(lastTemp) + " " + str(hum) + " != " + str(lastHum))
  175. if lastUpdate is not None:
  176. timediff = int(time.time()) - lastUpdate
  177. if timediff >= minUpdateInterval:
  178. senddata = True
  179. elif sensors_unavailable[str(addr)] == 1:
  180. senddata = True
  181. else:
  182. senddata = True
  183. sensors_unavailable[str(addr)] = 0
  184. #print(sensors_unavailable)
  185. if currentsensor_name is None:
  186. if batt_new == 1:
  187. fname = '/home/pi/logs/jeelink_unknown_new_sensor_' + str(addr)
  188. else:
  189. fname = '/home/pi/logs/jeelink_unknown_sensor_' + str(addr)
  190. if not os.path.isfile(fname):
  191. try:
  192. touch(fname)
  193. except:
  194. # guat dann hoit ned...
  195. pass
  196. temp = (int(serLineParts[4])*256 + int(serLineParts[5]) - 1000)/10.0
  197. if verbosemode:
  198. print("unknown sensor ID " + str(addr))
  199. mqttc.publish(mqtt_topic_prefix+"/UnknownSensor/"+str(addr)+"/temperature", str(temp), qos=0, retain=False)
  200. mqttc.publish(mqtt_topic_prefix+"/UnknownSensor/"+str(addr)+"/humidity", str(hum), qos=0, retain=False)
  201. mqttc.publish(mqtt_topic_prefix+"/UnknownSensor/"+str(addr)+"/battNew", str(batt_new), qos=0, retain=False)
  202. if verbosemode:
  203. 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))
  204. if senddata:
  205. sensors_lastUpdate[str(addr)] = int(time.time())
  206. sensors_lastTemp[str(addr)] = temp
  207. sensors_lastHum[str(addr)] = hum
  208. isAtemp = False
  209. if int(currentsensor_idx) == atemp_sensor_idx:
  210. atemp1 = temp
  211. ahum1 = hum
  212. isAtemp = True
  213. elif int(currentsensor_idx) == atemp_sensor_idx_2:
  214. atemp2 = temp
  215. ahum2 = hum
  216. isAtemp = True
  217. if isAtemp:
  218. if atemp1 <= atemp2:
  219. atemp = atemp1
  220. ahum = ahum1
  221. else:
  222. atemp = atemp2
  223. ahum = ahum2
  224. if atemp < 61 and ahum < 101:
  225. if atemp != atemp_last or ahum != ahum_last or ((time.time() - atemphum_lastUpdate) > aTempHumPublishInterval):
  226. atemphum_lastUpdate = time.time()
  227. atemp_last = atemp
  228. ahum_last = ahum
  229. mqttc.publish(mqtt_topic_atemp, str(atemp), qos=0, retain=True)
  230. mqttc.publish(mqtt_topic_ahum, str(ahum), qos=0, retain=True)
  231. mqttc.publish(mqtt_topic_atemphum_lastUpdate, strftime("%Y-%m-%d %H:%M:%S", localtime()), qos=0, retain=False)
  232. domoticz_json = "{\"idx\":" + str(currentsensor_idx) + ",\"nvalue\":0,\"svalue\":\"" + str(temp) + ";" + str(hum) + ";1\"}"
  233. #if verbosemode:
  234. # print(domoticz_json)
  235. mqttc.publish(mqtt_topic_domoticz_in, domoticz_json, qos=0, retain=False)
  236. mqttc.publish(mqtt_topic_prefix+"/"+str(currentsensor_name)+"/temperature", str(temp), qos=0, retain=False)
  237. mqttc.publish(mqtt_topic_prefix+"/"+str(currentsensor_name)+"/humidity", str(hum), qos=0, retain=False)
  238. mqttc.publish(mqtt_topic_prefix+"/"+str(currentsensor_name)+"/battery", str(batterystate), qos=0, retain=False)
  239. mqttc.publish(mqtt_topic_prefix+"/"+str(currentsensor_name)+"/lastUpdate", strftime("%Y-%m-%d %H:%M:%S", localtime()), qos=0, retain=False)
  240. mqttc.publish(mqtt_topic_prefix+"/"+str(currentsensor_name)+"/availability", "available", qos=0, retain=False)
  241. lacrosse_json = "{\"temperature\":" + str(temp) + ", \"humidity\":" + str(hum) + ", \"battery\":\"" + str(batterystate) + "\"}"
  242. mqttc.publish(mqtt_topic_prefix+"/"+str(currentsensor_name)+"/json", lacrosse_json, qos=0, retain=False)
  243. tmptext = str(temp) + "° " + str(hum) + "%"
  244. mqttc.publish(mqtt_topic_prefix+"/"+str(currentsensor_name)+"/TempHumText", tmptext, qos=0, retain=False)
  245. if verbosemode:
  246. print("MQTT published")
  247. try:
  248. touch("/tmp/jeelink2mqtt_running")
  249. except:
  250. # guat dann ned...
  251. pass
  252. else:
  253. if verbosemode:
  254. if currentsensor_name is None:
  255. print("MQTT published")
  256. else:
  257. print("MQTT publishing surpressed (interval not expired)")
  258. if verbosemode:
  259. print("\n")
  260. # handle outdated sensor values once a minute
  261. if (time.time() - checkLastUpdateInterval_lastRun) > checkLastUpdateInterval:
  262. checkLastUpdateInterval_lastRun = time.time()
  263. #print("check lastUpdate")
  264. for key in sensors_lastUpdate:
  265. #print(key, '->', sensors_lastUpdate[key], '->', sensors[key])
  266. if (time.time() - sensors_lastUpdate[key]) > sensordata_maxage:
  267. if verbosemode:
  268. print(sensors[key], ' outd ->')
  269. sensors_unavailable[key] = 1
  270. mqttc.publish(mqtt_topic_prefix+"/"+str(sensors[key])+"/availability", "unavailable", qos=0, retain=False)
  271. except KeyboardInterrupt:
  272. print('\n')
  273. exit()