-
Notifications
You must be signed in to change notification settings - Fork 0
/
jee2mqtt.py
executable file
·378 lines (337 loc) · 13.6 KB
/
jee2mqtt.py
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
368
369
370
371
372
373
374
375
376
377
#!/bin/python
#
# Small program to read data received by JeeLink on 868MHz. JeeLink sends ASCII data.
#
#
# TODO:
# * auto update ID after battery exchange. One sensor is left since 5 min. A new has appeared 4min ago -> is same !
#
import asyncio
import serial_asyncio
import serial, time
import paho.mqtt.client as mqtt
import logging
from logging.handlers import RotatingFileHandler
import sys
import argparse
import yaml # for cfgfile
CFG_FILE = "/etc/jee2mqtt.conf"
SERIAL_PORT = "/dev/ttyUSB0"
SERIAL_BAUDRATE = 57600
MQTT_SERVER = "192.168.0.90"
MQTT_PORT = 1883
MQTT_ONCHANGE = True # update values only on change or always
# Below is the mapping of sensor ID's to MQTT topics.
Sensors = {} # dict to map ID to MQTT topic. Get filled from CFG_FILE
# Unique is a metaclass which will only create a new instance of a class,
# if there was no other instance of this class created with same first
# argument to ctor. This is something like a singleton for classes with same
# first argument in ctor
class Unique( type ):
def __call__( cls, *args, **kwargs):
# This is called if class is instaciated ( a = Class())
# check if cache contains already an instance
if args[0] not in cls._cache:
# new instance, init and add to cache
self = cls.__new__(cls, *args, **kwargs)
cls.__init__(self, *args, **kwargs)
cls._cache[ args[0] ] = self
return cls._cache[args[0]]
def __init__(cls, name, bases, attributes):
# This is called at startup to init metaclass
super().__init__(name, bases, attributes)
cls._cache = {}
# updatable is a class to track if a value has changed and needs to be updated
# upstream to mqtt.
from dataclasses import dataclass
@dataclass
class Updatable:
value = None
isUpdated = False
# set a new value and set update flag
def set( self, new ):
if self.value != new:
self.value = new
self.isUpdated = True
# get the value
def get( self):
return self.value
# reset the update flag
def reset(self):
if MQTT_ONCHANGE == True:
self.isUpdated = False
def __str__(self):
return str(self.value)
class Sensor(metaclass=Unique):
# The Unique metaclass will ensure instances with different
# "type" in ctor will be created at first time. At second call, no new
# instance is created, but old one returned.
id=0
lastRx = 0 # timestamp when sensor was last updated
def __init__( self, id, mqttC ):
self.id = id
self.name = Sensors.get( self.id )
if not self.name:
self.name = str(self.id)
self.mqttC = mqttC
self.temp = Updatable()
self.hum = Updatable()
self.type = Updatable()
self.online = Updatable()
# function to update values of sensor and publish to mqtt
def update( self, type, temp, hum, newBat, weakBat ):
self.type.set( type )
self.temp.set( temp )
if hum == 106: # 106 is indication for no hum
self.hum.set( None )
else:
self.hum.set( hum )
self.newBat = newBat
self.weakBat = weakBat
self.lastRx = int(time.time()) # store current timestamp
self.online.set( 'on' ) # we go something -> is alive
self.mqttPub( force=False )
# check the time of last received value. In case it exceed the given
# timout, update the online status for this sensor on mqtt
def checkAlive( self, timeout ):
d = int(time.time()) - self.lastRx
if d > timeout:
log.debug( 'off :' + self.name)
self.online.set( 'off' )
self.mqttPub( force=False )
else:
log.debug( 'on : '+ self.name)
self.mqttPub( force=True ) # we're still alive, resend mqtt vaules
# publish values to mqtt server. Update will be done only if value has
# changed, or force=True
def mqttPub( self, force ):
# TODO: add recognition of server restart to update all values again-> retain=True
try:
if self.temp.isUpdated or force==True :
self.temp.reset()
self.mqttC.publish( self.name+'/temp', str(self.temp), retain=False)
# log.debug( "mqttPub: temp" )
if self.hum.isUpdated or force==True:
self.hum.reset()
self.mqttC.publish( self.name+'/hum', str(self.hum), retain=False)
# log.debug( "mqttPub: hum" )
if self.online.isUpdated or force==True:
self.online.reset()
self.mqttC.publish( self.name+'/online', str(self.online), retain=False)
# log.debug( "mqttPub: hum" )
except:
log.error( "Error, failed to upload to mosquitto")
# pretty print sensor object
def __str__( self ):
dbg = 'ID={0:>2}'.format( self.id )
dbg += ', type='.format( self.type )
dbg += ', Bat new={0} weak={1}'.format( self.newBat, self.weakBat )
dbg += ', T={0:>5}'.format( self.temp.get() )
#dbg += ', T={0}'.format( self.temp )
if self.hum.get() != None:
dbg += ', H={0}'.format( self.hum )
else:
dbg += ', H=xx'
dbg += ', {0}'.format( self.name )
return dbg
# ID2_KILL = 10
def decode( msg ):
args = msg.split()
if len(args) == 0:
if msg != b'\n': # no trace on empyt lines
log.debug( "rx unknown message. Ignoring! '" + str(msg)+"'" )
return
if args[0].startswith( b'[LaCrosseITPlusReader'):
log.debug("rx::versionreply:" +str(args) )
return
if args[0] != b'OK' or args[1] != b'9':
log.debug( "rx unknown message. Ignoring! '" + str(msg)+"'" )
return
# we're sure it's an LaCross message. Decode it..
# Protocol: http://fredboboss.free.fr/articles/tx29.php/ (only values,
# below is aligned to bytes !)
# "OK 9 210 220 230 240 250"
# "OK 9" -> static
# "210" -> comlete Sensor ID
# "220" -> Type ("1"), newBatter ("128") -> "1" or "129"
# "230" -> Temp1
# "240" -> Temp2
# "250" -> Humidity (newbat in bit8, 106 = no HUM info supported)
id = int( args[2] )
newBat = int( args[3]) >> 7
type = int( args[3] ) & 0x7f
temp = int( args[4])*256+int(args[5] )
temp = (float(temp)-1000) /10
weakBat = int( args[6]) >> 7
hum = int( args[6] ) & 0x7f
a = Sensor( id, mqttC)
# if a.id == 2:
# global ID2_KILL
# if not ID2_KILL:
# log.debug("SKIP SENSOR FOR OFFLINE TEST")
# return
# ID2_KILL -= 1
a.update( type, temp, hum, newBat, weakBat)
log.info( a )
# Asyncio Class to capture serial port handling.
class serial:
timeoutRx = 600 # timeout in sec for serial rx
online = False
lastRx = 0 # time of last serial rx
def __init__(self, port, baudrate ):
self.port = port
self.baudrate = baudrate
async def main( self ):
try:
self.reader, self.writer = await serial_asyncio.open_serial_connection(url=self.port, baudrate=self.baudrate)
except IOError:
log.error( "Can not open " + PORT )
exit("Cannot open " + PORT)
# wait till jeelink has settled to ensure init sequence will be received
time.sleep(2)
messages = [ b'1r0t0a', b'v' ]
log.debug("Start receiving ..." )
await asyncio.gather( self.send(messages), self.recv(), self.monitor() )
async def send( self, msgs):
#TODO: add queue to allow sending data from outside
for msg in msgs:
self.writer.write(msg)
log.debug(f'tx: {msg.decode().rstrip()}')
# await asyncio.sleep(0.5)
# increased to 1s to hopefully fix startup isse
await asyncio.sleep(1)
log.debug('Done sending')
async def recv( self):
while True:
msg = await self.reader.readuntil(b'\n')
# if not self.online:
self.lastRx = time.time() # save timestamp for rx
# log.debug( f'raw= {msg.rstrip().decode()}')
log.debug( 'rx::raw: ' +str(msg) )
decode( msg )
async def monitor( self ):
log.debug("monitor() started. Rx timeout=" + str(self.timeoutRx))
self.lastRx = time.time() # init timestamp for rx
while True:
d = time.time() - self.lastRx # time left since last rx
if d > self.timeoutRx: # exceed timeout ?
if self.online:
self.online=False
# TODO: ugly workaround using global mqtt object !!!
mqttC.publish( '/jeelink/online', 'off', retain=True)
log.warning("nothing rx since " + str(d) + " sec. Going offline !")
else:
if not self.online:
self.online = True
mqttC.publish( '/jeelink/online', 'on', retain=False)
log.info("received somthing ... back online !")
# check for each sensor if it is offline
for s in Sensors:
a = Sensor( s, mqttC)
a.checkAlive( self.timeoutRx )
await asyncio.sleep( self.timeoutRx / 2 )
def on_mqtt( client, userdata, msg ):
log.debug("on_mqtt: '"+msg.topic+"' -> '" + str(msg.payload) +"'")
def on_connect(client, userdata, flags, rc):
if rc == 0:
log.info("connected to mqtt server")
client.isConnected = True
else:
log.error("on_connect: failed to connect to mqtt server")
def on_subscribe( client, userdata, mid, granted_qos):
log.debug("on_subscribe:")
def on_log( client, userdata, level, buf):
None
# if "PING" not in buf:
# log.debug("on_log: " + buf)
#def onMqttConnect( client, userdata, flags, rc):
# log.info( "Connected to mqtt server")
# get name of program. Remove prefixed path and postfixed fileytpe
myName = sys.argv[0]
myName = myName[ myName.rfind('/')+1: ]
myName = myName[ : myName.find('.')]
my_parser = argparse.ArgumentParser(description='Read jeelink data from USB \
serial device and publish to MQTT server')
my_parser.add_argument('--serial-port',
required=False,
type=str,
default=SERIAL_PORT,
help='Path to serial port device')
my_parser.add_argument('--serial-baudrate',
required=False,
type=int,
default=SERIAL_BAUDRATE,
help='Baudrate for serial port')
my_parser.add_argument('--mqtt-server',
required=False,
type=str,
default=MQTT_SERVER,
help='Name or IP of MQTT server')
my_parser.add_argument('--mqtt-port',
required=False,
type=int,
default=MQTT_PORT,
help='Port of MQTT server')
my_parser.add_argument('--loglevel',
required=False,
type=str,
default="ERROR",
help='Set loglevel to [DEBUG, INFO, ..]')
my_parser.add_argument('--logfile',
required=False,
default=False,
action='store_true',
help='If provided, we log into a file')
my_parser.add_argument('--cfgfile',
required=False,
default=CFG_FILE,
action='store_true',
help='Location of the configuration file. Default='+CFG_FILE)
args = my_parser.parse_args()
print( 'Started ' + myName + ' using ' + args.serial_port + \
' with ' + str(args.serial_baudrate) )
log = logging.getLogger( __name__ )
log.setLevel( getattr(logging, args.loglevel.upper()))
fmt = logging.Formatter('%(asctime)s %(levelname)7s: %(message)s')
sh = logging.StreamHandler(sys.stdout)
sh.setFormatter( fmt )
log.addHandler( sh )
if args.logfile:
print( 'Logging to /tmp/'+myName+'.log')
rh = logging.handlers.RotatingFileHandler( '/tmp/'+myName+'.log',
maxBytes=10000000, backupCount=1 )
rh.setFormatter( fmt )
log.addHandler( rh )
# read the Sensors dict from yaml configuration file
# TODO: add serial and mqtt configuration to cfgfile
try:
with open( args.cfgfile, "r") as cfgfile:
cfg = yaml.safe_load( cfgfile )
# TODO: need some error handling !!!
Sensors = cfg['Sensors']
# swap keys and values of the dict
Sensors = {v: k for k,v in Sensors.items() }
except:
log.error("Failed to read config file ("+args.cfgfile+"!")
# create mqtt client
mqttC = mqtt.Client(client_id="jee2mqtt")
mqttC.isConnected = False
mqttC.on_connect = on_connect
mqttC.on_message = on_mqtt
mqttC.on_subscribe = on_subscribe
mqttC.on_log = on_log
mqttC.loop_start() # create rx/tx thread in background
try:
mqttC.connect( args.mqtt_server, args.mqtt_port, 10)
except:
log.error("Failed to connect to mqtt server. Retry in 2 secs ....")
while not mqttC.isConnected:
time.sleep(1) # wait till mqtt server arrives.
try:
# start serial coroutine
s = serial( args.serial_port, args.serial_baudrate )
asyncio.run( s.main() )
except KeyboardInterrupt:
time.sleep(2)
mqttC.loop_stop()
print("Terminated")