-
Notifications
You must be signed in to change notification settings - Fork 14
/
LYWSD03MMC.py
391 lines (340 loc) · 17.1 KB
/
LYWSD03MMC.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
378
379
380
381
382
383
384
385
386
387
388
389
390
391
#!/usr/bin/python3 -u
#!/home/openhabian/Python3/Python-3.7.4/python -u
#-u to unbuffer output. Otherwise when calling with nohup or redirecting output things are printed very lately or would even mixup
from bluepy import btle
import argparse
import os
import re
from dataclasses import dataclass
from collections import deque
import threading
import time
import signal
import traceback
import math
import logging
try:
import paho.mqtt.publish as publish
except:
print('You do not have mqtt installed, which is needed to talk to the broker.')
print('Install using sudo pip3 install paho-mqtt')
base_topic = "blemqtt/" # MQTT Topic
@dataclass
class Measurement:
temperature: float
humidity: int
voltage: float
calibratedHumidity: int = 0
battery: int = 0
timestamp: int = 0
def __eq__(self, other):
if self.temperature == other.temperature and self.humidity == other.humidity and self.calibratedHumidity == other.calibratedHumidity and self.battery == other.battery and self.voltage == other.voltage:
return True
else:
return False
measurements=deque()
#globalBatteryLevel=0
previousMeasurement=Measurement(0,0,0,0,0,0)
identicalCounter=0
def signal_handler(sig, frame):
os._exit(0)
def watchDog_Thread():
global unconnectedTime
global connected
global pid
while True:
logging.debug("watchdog_Thread")
logging.debug("unconnectedTime : " + str(unconnectedTime))
logging.debug("connected : " + str(connected))
logging.debug("pid : " + str(pid))
now = int(time.time())
if (unconnectedTime is not None) and ((now - unconnectedTime) > 60): #could also check connected is False, but this is more fault proof
pstree=os.popen("pstree -p " + str(pid)).read() #we want to kill only bluepy from our own process tree, because other python scripts have there own bluepy-helper process
logging.debug("PSTree: " + pstree)
try:
bluepypid=re.findall(r'bluepy-helper\((.*)\)',pstree)[0] #Store the bluepypid, to kill it later
except IndexError: #Should not happen since we're now connected
logging.debug("Couldn't find pid of bluepy-helper")
os.system("kill " + bluepypid)
logging.debug("Killed bluepy with pid: " + str(bluepypid))
unconnectedTime = now #reset unconnectedTime to prevent multiple killings in a row
time.sleep(5)
def thread_SendingData():
global previousMeasurement
global measurements
path = os.path.dirname(os.path.abspath(__file__))
while True:
try:
mea = measurements.popleft()
if (mea == previousMeasurement and identicalCounter < args.skipidentical): #only send data when it has changed or X identical data has been skipped, ~10 pakets per minute, 50 pakets --> writing at least every 5 minutes
print("Measurements are identical don't send data\n")
identicalCounter+=1
continue
identicalCounter=0
fmt = "sensorname,temperature,humidity,voltage" #don't try to seperate by semicolon ';' os.system will use that as command seperator
params = args.name + " " + str(mea.temperature) + " " + str(mea.humidity) + " " + str(mea.voltage)
if (args.TwoPointCalibration or args.offset): #would be more efficient to generate fmt only once
fmt +=",humidityCalibrated"
params += " " + str(mea.calibratedHumidity)
if (args.battery):
fmt +=",batteryLevel"
params += " " + str(mea.battery)
params += " " + str(mea.timestamp)
fmt +=",timestamp"
cmd = path + "/" + args.callback + " " + fmt + " " + params
print(cmd)
ret = os.system(cmd)
if (ret != 0):
measurements.appendleft(mea) #put the measurement back
print ("Data couln't be send to Callback, retrying...")
time.sleep(5) #wait before trying again
else: #data was sent
previousMeasurement=Measurement(mea.temperature,mea.humidity,mea.voltage,mea.calibratedHumidity,mea.battery,0) #using copy or deepcopy requires implementation in the class definition
except IndexError:
#print("Keine Daten")
time.sleep(1)
except Exception as e:
print(e)
print(traceback.format_exc())
mode="round"
class MyDelegate(btle.DefaultDelegate):
def __init__(self, params):
btle.DefaultDelegate.__init__(self)
# ... initialise here
def handleNotification(self, cHandle, data):
global measurements
try:
measurement = Measurement(0,0,0,0,0,0)
measurement.timestamp = int(time.time())
temp=int.from_bytes(data[0:2],byteorder='little',signed=True)/100
#print("Temp received: " + str(temp))
if args.round:
#print("Temperatur unrounded: " + str(temp
if args.debounce:
global mode
temp*=10
intpart = math.floor(temp)
fracpart = round(temp - intpart,1)
#print("Fracpart: " + str(fracpart))
if fracpart >= 0.7:
mode="ceil"
elif fracpart <= 0.2: #either 0.8 and 0.3 or 0.7 and 0.2 for best even distribution
mode="trunc"
#print("Modus: " + mode)
if mode=="trunc": #only a few times
temp=math.trunc(temp)
elif mode=="ceil":
temp=math.ceil(temp)
else:
temp=round(temp,0)
temp /=10.
#print("Debounced temp: " + str(temp))
else:
temp=round(temp,1)
humidity=int.from_bytes(data[2:3],byteorder='little')
print("Temperature: " + str(temp))
print("Humidity: " + str(humidity))
voltage=int.from_bytes(data[3:5],byteorder='little') / 1000.
print("Battery voltage:",voltage)
measurement.temperature = temp
measurement.humidity = humidity
measurement.voltage = voltage
if args.battery:
#measurement.battery = globalBatteryLevel
batteryLevel = min(int(round((voltage - 2.1),2) * 100), 100) #3.1 or above --> 100% 2.1 --> 0 %
measurement.battery = batteryLevel
print("Battery level:",batteryLevel)
if args.offset:
humidityCalibrated = humidity + args.offset
print("Calibrated humidity: " + str(humidityCalibrated))
measurement.calibratedHumidity = humidityCalibrated
if args.TwoPointCalibration:
offset1=args.offset1
offset2=args.offset2
p1y=args.calpoint1
p2y=args.calpoint2
p1x=p1y - offset1
p2x=p2y - offset2
m = (p1y - p2y) * 1.0 / (p1x - p2x) # y=mx+b
#b = (p1x * p2y - p2x * p1y) * 1.0 / (p1y - p2y)
b = p2y - m * p2x #would be more efficient to do this calculations only once
humidityCalibrated=m*humidity + b
if (humidityCalibrated > 100 ): #with correct calibration this should not happen
humidityCalibrated = 100
elif (humidityCalibrated < 0):
humidityCalibrated = 0
humidityCalibrated=int(round(humidityCalibrated,0))
print("Calibrated humidity: " + str(humidityCalibrated))
measurement.calibratedHumidity = humidityCalibrated
measurements.append(measurement)
except Exception as e:
print("Fehler")
print(e)
print(traceback.format_exc())
# Initialisation -------
def connect():
p = btle.Peripheral(adress)
val=b'\x01\x00'
p.writeCharacteristic(0x0038,val,True) #enable notifications of Temperature, Humidity and Battery voltage
p.writeCharacteristic(0x0046,b'\xf4\x01\x00',True)
p.withDelegate(MyDelegate("abc"))
return p
# roll around to the next device address
def set_address():
global address_ctr, addresses, adress
address_ctr += 1
if address_ctr > len(addresses):
address_ctr = 0
adress = addresses[address_ctr]
# Main loop --------
parser=argparse.ArgumentParser()
parser.add_argument("--device","-d", help="Set the device MAC-Address in format AA:BB:CC:DD:EE:FF",metavar='AA:BB:CC:DD:EE:FF')
parser.add_argument("--battery","-b", help="Get estimated battery level", metavar='', type=int, nargs='?', const=1)
parser.add_argument("--count","-c", help="Read/Receive N measurements and then exit script", metavar='N', type=int)
parser.add_argument("--delay","-del", help="Delay between taking readings from each device", metavar='N', type=int)
rounding = parser.add_argument_group("Rounding and debouncing")
rounding.add_argument("--round","-r", help="Round temperature to one decimal place",action='store_true')
rounding.add_argument("--debounce","-deb", help="Enable this option to get more stable temperature values, requires -r option",action='store_true')
offsetgroup = parser.add_argument_group("Offset calibration mode")
offsetgroup.add_argument("--offset","-o", help="Enter an offset to the reported humidity value",type=int)
complexCalibrationGroup=parser.add_argument_group("2 Point Calibration")
complexCalibrationGroup.add_argument("--TwoPointCalibration","-2p", help="Use complex calibration mode. All arguments below are required",action='store_true')
complexCalibrationGroup.add_argument("--calpoint1","-p1", help="Enter the first calibration point",type=int)
complexCalibrationGroup.add_argument("--offset1","-o1", help="Enter the offset for the first calibration point",type=int)
complexCalibrationGroup.add_argument("--calpoint2","-p2", help="Enter the second calibration point",type=int)
complexCalibrationGroup.add_argument("--offset2","-o2", help="Enter the offset for the second calibration point",type=int)
callbackgroup = parser.add_argument_group("Callback related functions")
callbackgroup.add_argument("--callback","-call", help="Pass the path to a program/script that will be called on each new measurement")
callbackgroup.add_argument("--name","-n", help="Give this sensor a name reported to the callback script")
callbackgroup.add_argument("--skipidentical","-skip", help="N consecutive identical measurements won't be reported to callbackfunction",metavar='N', type=int, default=0)
mqttgroup = parser.add_argument_group("MQTT related functions")
mqttgroup.add_argument("--mqtt","-m", help="IP address, or name, of MQTT server")
args=parser.parse_args()
if args.device:
print('args.device', args.device)
addresses = args.device.split(',')
print(addresses, len(addresses))
for address in addresses:
if not re.match("[0-9a-fA-F]{2}([:]?)[0-9a-fA-F]{2}(\\1[0-9a-fA-F]{2}){4}$",address):
print("Please specify device MAC-Address in format AA:BB:CC:DD:EE:FF")
os._exit(1)
address_ctr = 1000
set_address()
else:
parser.print_help()
os._exit(1)
if args.TwoPointCalibration:
if(not(args.calpoint1 and args.offset1 and args.calpoint2 and args.offset2)):
print("In 2 Point calibration you have to enter 4 points")
os._exit(1)
elif(args.offset):
print("Offset calibration and 2 Point calibration can't be used together")
os._exit(1)
if not args.name:
args.name = args.device
if args.mqtt:
hostname = args.mqtt
else:
print ('You must set the MQTT hostname - e.g. -m 192.168.0.99')
os._exit(1)
if args.delay:
delay = args.delay
print ('Delay set to {} seconds'.format(delay))
else:
delay = 30
print ('No delay set. Defaulting to 30 seconds')
p=btle.Peripheral()
cnt=0
if args.callback:
dataThread = threading.Thread(target=thread_SendingData)
dataThread.start()
signal.signal(signal.SIGINT, signal_handler)
connected=False
#logging.basicConfig(level=logging.DEBUG)
logging.basicConfig(level=logging.ERROR)
logging.debug("Debug: Starting script...")
pid=os.getpid()
bluepypid=None
unconnectedTime=None
watchdogThread = threading.Thread(target=watchDog_Thread)
watchdogThread.start()
logging.debug("watchdogThread startet")
while True:
try:
if not connected:
#Bluepy sometimes hangs and makes it even impossible to connect with gatttool as long it is running
#on every new connection a new bluepy-helper is called
#we now make sure that the old one is really terminated. Even if it hangs a simple kill signal was sufficient to terminate it
# if bluepypid is not None:
# os.system("kill " + bluepypid)
# print("Killed possibly remaining bluepy-helper")
# else:
# print("bluepy-helper couldn't be determined, killing not allowed")
print("Trying to connect to " + adress)
p=connect()
# logging.debug("Own PID: " + str(pid))
# pstree=os.popen("pstree -p " + str(pid)).read() #we want to kill only bluepy from our own process tree, because other python scripts have there own bluepy-helper process
# logging.debug("PSTree: " + pstree)
# try:
# bluepypid=re.findall(r'bluepy-helper\((.*)\)',pstree)[0] #Store the bluepypid, to kill it later
# except IndexError: #Should not happen since we're now connected
# logging.debug("Couldn't find pid of bluepy-helper")
connected=True
unconnectedTime=None
# if args.battery:
# if(cnt % args.battery == 0):
# print("Warning the battery option is deprecated, Aqara device always reports 99 % battery")
# batt=p.readCharacteristic(0x001b)
# batt=int.from_bytes(batt,byteorder="little")
# print("Battery-Level: " + str(batt))
# globalBatteryLevel = batt
if p.waitForNotifications(2000):
# handleNotification() was called
cnt += 1
if args.count is not None and cnt >= args.count:
print(str(args.count) + " measurements collected. Exiting in a moment.")
p.disconnect()
time.sleep(5)
#It seems that sometimes bluepy-helper remains and thus prevents a reconnection, so we try killing our own bluepy-helper
pstree=os.popen("pstree -p " + str(pid)).read() #we want to kill only bluepy from our own process tree, because other python scripts have there own bluepy-helper process
bluepypid=0
try:
bluepypid=re.findall(r'bluepy-helper\((.*)\)',pstree)[0] #Store the bluepypid, to kill it later
except IndexError: #Should normally occur because we're disconnected
logging.debug("Couldn't find pid of bluepy-helper")
if bluepypid is not 0:
os.system("kill " + bluepypid)
logging.debug("Killed bluepy with pid: " + str(bluepypid))
##############################################################
# changes added
# send mqtt
topic = base_topic + adress.replace(':','')
print ('Topic',topic)
print('measurements', measurements)
print(measurements[0].temperature)
measurement = measurements[0]
message = '{"temperature": "' + str(measurement.temperature) + '", '
message = message + '"humidity": "' + str(measurement.humidity) + '", '
message = message + '"batt": "' + str(measurement.battery) + '", '
message = message + '"voltage": "' + str(measurement.voltage) + '"}'
print('Publishing ' + message)
publish.single(topic, message, hostname=hostname)
cnt = 0 # reset the counter - do not exit
measurements.clear() # clear the measurements array or it will continue to grow
set_address() # roll round to the next address in the array
time.sleep(delay) # delay between reading each device
# os._exit(0)
# end of changes
##############################################################
print("")
continue
except Exception as e:
print("Connection lost")
if connected is True: #First connection abort after connected
unconnectedTime=int(time.time())
connected=False
time.sleep(1)
logging.debug(e)
logging.debug(traceback.format_exc())
print ("Waiting...")
# Perhaps do something else here