Skip to content

Commit a6b07aa

Browse files
authored
v4 supported for encrypted format
Added support for encrypted format Added support for custom format Ensured mqtt.conf file compability with old versions Added troubleshooting for connection issues
1 parent 08983ae commit a6b07aa

File tree

4 files changed

+303
-85
lines changed

4 files changed

+303
-85
lines changed

LYWSD03MMC.py

Lines changed: 162 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
#!/usr/bin/python3 -u
2-
#!/home/openhabian/Python3/Python-3.7.4/python -u
32
#-u to unbuffer output. Otherwise when calling with nohup or redirecting output things are printed very lately or would even mixup
43

54
print("---------------------------------------------")
6-
print("MiTemperature2 / ATC Thermometer version 3.1")
5+
print("MiTemperature2 / ATC Thermometer version 4.0")
76
print("---------------------------------------------")
87

8+
readme="""
9+
10+
Please read README.md in this folder. Latest version is available at https://github.com/JsBergbau/MiTemperature2#readme
11+
This file explains very detailed about the usage and covers everything you need to know as user.
12+
13+
"""
14+
15+
print(readme)
16+
17+
918
from bluepy import btle
1019
import argparse
1120
import os
@@ -22,6 +31,7 @@
2231
import requests
2332
import ssl
2433

34+
2535
@dataclass
2636
class Measurement:
2737
temperature: float
@@ -309,7 +319,7 @@ def MQTTOnDisconnect(client, userdata,rc):
309319
print("MQTT disconnected, Client:", client, "Userdata:", userdata, "RC:", rc)
310320

311321
# Main loop --------
312-
parser=argparse.ArgumentParser(allow_abbrev=False)
322+
parser=argparse.ArgumentParser(allow_abbrev=False,epilog=readme)
313323
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')
314324
parser.add_argument("--battery","-b", help="Get estimated battery level, in ATC-Mode: Get battery level from device", metavar='', type=int, nargs='?', const=1)
315325
parser.add_argument("--count","-c", help="Read/Receive N measurements and then exit script", metavar='N', type=int)
@@ -319,7 +329,7 @@ def MQTTOnDisconnect(client, userdata,rc):
319329

320330

321331
rounding = parser.add_argument_group("Rounding and debouncing")
322-
rounding.add_argument("--round","-r", help="Round temperature to one decimal place",action='store_true')
332+
rounding.add_argument("--round","-r", help="Round temperature to one decimal place (and in ATC mode humidity to whole numbers)",action='store_true')
323333
rounding.add_argument("--debounce","-deb", help="Enable this option to get more stable temperature values, requires -r option",action='store_true')
324334

325335
offsetgroup = parser.add_argument_group("Offset calibration mode")
@@ -368,12 +378,12 @@ def MQTTOnDisconnect(client, userdata,rc):
368378
port = int(mqttConfig["MQTT"]["port"])
369379

370380
# MQTTS parameters
371-
tls = int(mqttConfig["MQTT"]["tls"])
372-
cacerts = mqttConfig["MQTT"]["cacerts"] if mqttConfig["MQTT"]["cacerts"] else None
373-
certificate = mqttConfig["MQTT"]["certificate"] if mqttConfig["MQTT"]["certificate"] else None
374-
certificate_key = mqttConfig["MQTT"]["certificate_key"] if mqttConfig["MQTT"]["certificate_key"] else None
375-
insecure = int(mqttConfig["MQTT"]["insecure"])
376-
381+
tls = int(mqttConfig["MQTT"]["tls"]) if "tls" in mqttConfig["MQTT"] else 0
382+
if tls != 0:
383+
cacerts = mqttConfig["MQTT"]["cacerts"] if mqttConfig["MQTT"]["cacerts"] else None
384+
certificate = mqttConfig["MQTT"]["certificate"] if mqttConfig["MQTT"]["certificate"] else None
385+
certificate_key = mqttConfig["MQTT"]["certificate_key"] if mqttConfig["MQTT"]["certificate_key"] else None
386+
insecure = int(mqttConfig["MQTT"]["insecure"])
377387
username = mqttConfig["MQTT"]["username"]
378388
password = mqttConfig["MQTT"]["password"]
379389
MQTTTopic = mqttConfig["MQTT"]["topic"]
@@ -402,7 +412,6 @@ def MQTTOnDisconnect(client, userdata,rc):
402412
if len(lwt) > 0:
403413
print("Using lastwill with topic:",lwt,"and message:",lastwill)
404414
client.will_set(lwt,lastwill,qos=1)
405-
406415
# MQTTS parameters
407416
if tls:
408417
client.tls_set(cacerts, certificate, certificate_key, cert_reqs=ssl.CERT_REQUIRED, tls_version=ssl.PROTOCOL_TLS, ciphers=None)
@@ -411,6 +420,7 @@ def MQTTOnDisconnect(client, userdata,rc):
411420
client.connect_async(broker,port)
412421
MQTTClient=client
413422

423+
414424
if args.device:
415425
if re.match("[0-9a-fA-F]{2}([:]?)[0-9a-fA-F]{2}(\\1[0-9a-fA-F]{2}){4}$",args.device):
416426
adress=args.device
@@ -530,19 +540,21 @@ def MQTTOnDisconnect(client, userdata,rc):
530540
print("----------------------------")
531541
print("In this mode all devices within reach are read out, unless a devicelistfile and --onlydevicelist is specified.")
532542
print("Also --name Argument is ignored, if you require names, please use --devicelistfile.")
533-
print("In this mode rounding and debouncing are not available, since ATC firmware sends out only one decimal place.")
543+
print("In this mode debouncing is not available. Rounding option will round humidity and temperature to one decimal place.")
534544
print("ATC mode usually requires root rights. If you want to use it with normal user rights, \nplease execute \"sudo setcap cap_net_raw,cap_net_admin+eip $(eval readlink -f `which python3`)\"")
535545
print("You have to redo this step if you upgrade your python version.")
536546
print("----------------------------")
537547

538548
import sys
539549
import bluetooth._bluetooth as bluez
550+
import cryptoFunctions
540551

541552
from bluetooth_utils import (toggle_device,
542553
enable_le_scan, parse_le_advertising_events,
543554
disable_le_scan, raw_packet_to_str)
544555

545-
advCounter=dict()
556+
advCounter=dict()
557+
#encryptedPacketStore=dict()
546558
sensors = dict()
547559
if args.devicelistfile:
548560
#import configparser
@@ -557,6 +569,15 @@ def MQTTOnDisconnect(client, userdata,rc):
557569
sensorsnew[key.upper()] = sensors[key]
558570
sensors = sensorsnew
559571

572+
#loop through sensors to generate key
573+
sensorsnew=sensors
574+
for sensor in sensors:
575+
if "decryption" in sensors[sensor]:
576+
if sensors[sensor]["decryption"][0] == "k":
577+
sensorsnew[sensor]["key"] = sensors[sensor]["decryption"][1:]
578+
#print(sensorsnew[sensor]["key"])
579+
sensors = sensorsnew
580+
560581
if args.onlydevicelist and not args.devicelistfile:
561582
print("Error: --onlydevicelist requires --devicelistfile <devicelistfile>")
562583
os._exit(1)
@@ -581,83 +602,152 @@ def le_advertise_packet_handler(mac, adv_type, data, rssi):
581602
lastBLEPaketReceived = time.time()
582603
lastBLEPaketReceived = time.time()
583604
data_str = raw_packet_to_str(data)
584-
preeamble = "10161a18"
605+
preeamble = "161a18"
585606
paketStart = data_str.find(preeamble)
586607
offset = paketStart + len(preeamble)
587-
#print("reveived BLE packet")+
588-
atcData_str = data_str[offset:offset+26]
608+
atcData_str = data_str[offset:offset+26] #if shorter will just be shorter then 13 Bytes
609+
atcData_str = data_str[offset:] #if shorter will just be shorter then 13 Bytes
610+
customFormat_str = data_str[offset:offset+29]
589611
ATCPaketMAC = atcData_str[0:12].upper()
590612
macStr = mac.replace(":","").upper()
591613
atcIdentifier = data_str[(offset-4):offset].upper()
592614

593-
if(atcIdentifier == "1A18" and ATCPaketMAC == macStr) and not args.onlydevicelist or (atcIdentifier == "1A18" and mac in sensors) and len(atcData_str) == 26: #only Data from ATC devices, double checked
594-
advNumber = atcData_str[-2:]
615+
# if (atcIdentifier == "1A18" ) and mac == "A4:C1:38:92:E3:BD" : #debug
616+
# print("BLE packet: %s %02x %s %d" % (mac, adv_type, data_str, rssi))
617+
# print("raw:",data_str)
618+
619+
620+
batteryVoltage=None
621+
if(atcIdentifier == "1A18" ) and not args.onlydevicelist or (atcIdentifier == "1A18" and mac in sensors) and (len(atcData_str) == 26 or len(atcData_str) == 16 or len(atcData_str) == 22): #only Data from ATC devices
622+
global measurements
623+
measurement = Measurement(0,0,0,0,0,0,0,0)
624+
if len(atcData_str) == 30: #custom format, next-to-last ist adv number
625+
advNumber = atcData_str[-4:-2]
626+
else:
627+
advNumber = atcData_str[-2:] #last data in paket is adv number
628+
595629
if macStr in advCounter:
596630
lastAdvNumber = advCounter[macStr]
597631
else:
598632
lastAdvNumber = None
599633
if lastAdvNumber == None or lastAdvNumber != advNumber:
600-
advCounter[macStr] = advNumber
601-
print("BLE packet: %s %02x %s %d" % (mac, adv_type, data_str, rssi))
602-
#print("AdvNumber: ", advNumber)
603-
#temp = data_str[22:26].encode('utf-8')
604-
#temperature = int.from_bytes(bytearray.fromhex(data_str[22:26]),byteorder='big') / 10.
605-
global measurements
606-
measurement = Measurement(0,0,0,0,0,0,0,0)
607-
if args.influxdb == 1:
608-
measurement.timestamp = int((time.time() // 10) * 10)
609-
else:
610-
measurement.timestamp = int(time.time())
611634

635+
if len(atcData_str) == 26: #ATC1441 Format
636+
#print("atc14441") #debug
637+
advCounter[macStr] = advNumber
638+
print("BLE packet: %s %02x %s %d" % (mac, adv_type, data_str, rssi))
639+
#print("AdvNumber: ", advNumber)
640+
#temp = data_str[22:26].encode('utf-8')
641+
#temperature = int.from_bytes(bytearray.fromhex(data_str[22:26]),byteorder='big') / 10.
642+
#temperature = int(data_str[22:26],16) / 10.
643+
temperature = int.from_bytes(bytearray.fromhex(atcData_str[12:16]),byteorder='big',signed=True) / 10.
644+
# print("Temperature: ", temperature)
645+
humidity = int(atcData_str[16:18], 16)
646+
# print("Humidity: ", humidity)
647+
batteryVoltage = int(atcData_str[20:24], 16) / 1000
648+
# print ("Battery voltage:", batteryVoltage,"V")
649+
# print ("RSSI:", rssi, "dBm")
650+
651+
#if args.battery:
652+
batteryPercent = int(atcData_str[18:20], 16)
653+
#print ("Battery:", batteryPercent,"%")
654+
655+
elif len(atcData_str) == 30: #custom format
656+
#print("custom:", atcData_str)
657+
print("BLE packet: %s %02x %s %d" % (mac, adv_type, data_str, rssi))
658+
temperature = int.from_bytes(bytearray.fromhex(atcData_str[12:16]),byteorder='little',signed=True) / 100.
659+
humidity = int.from_bytes(bytearray.fromhex(atcData_str[16:20]),byteorder='little',signed=False) / 100.
660+
batteryVoltage = int.from_bytes(bytearray.fromhex(atcData_str[20:24]),byteorder='little',signed=False) / 1000.
661+
batteryPercent = int.from_bytes(bytearray.fromhex(atcData_str[24:26]),byteorder='little',signed=False)
662+
663+
664+
665+
elif len(atcData_str) == 22 or len(atcData_str) == 16: #encrypted: length 22/11 Bytes on custom format, 16/8 Bytes on ATC1441 Format
666+
#print("enc") # debug
667+
#if macStr in encryptedPacketStore:
668+
if macStr in advCounter:
669+
lastData = advCounter[macStr]
670+
else:
671+
lastData = None
672+
673+
if lastData == None or lastData != atcData_str:
674+
print("Encrypted BLE packet: %s %02x %s %d, length: %d" % (mac, adv_type, data_str, rssi, len(atcData_str)/2))
675+
if mac in sensors and "key" in sensors[mac]:
676+
bindkey = bytes.fromhex(sensors[mac]["key"])
677+
macReversed=""
678+
for x in range(-1,-len(macStr),-2):
679+
macReversed += macStr[x-1] + macStr[x]
680+
macReversed = bytes.fromhex(macReversed.lower())
681+
#print("New encrypted format, MAC:" , macStr, "Reversed: ", macReversed)
682+
lengthHex=data_str[offset-8:offset-6]
683+
#lengthHex="0b"
684+
ret = cryptoFunctions.decrypt_aes_ccm(bindkey,macReversed,bytes.fromhex(lengthHex + "161a18" + atcData_str))
685+
if ret == None: #Error decrypting
686+
print("\n")
687+
return
688+
#temperature, humidity, batteryPercent = cryptoFunctions.decrypt_aes_ccm(bindkey,macReversed,bytes.fromhex(lengthHex + "161a18" + atcData_str))
689+
temperature, humidity, batteryPercent = ret
690+
else:
691+
print("Warning: No key provided for sensor:", mac,"\n")
692+
return
693+
else: #no fitting paket
694+
return
695+
696+
else: #Packet is just repeated
697+
return
698+
699+
if args.influxdb == 1:
700+
measurement.timestamp = int((time.time() // 10) * 10)
701+
else:
702+
measurement.timestamp = int(time.time())
612703

613-
#temperature = int(data_str[22:26],16) / 10.
614-
temperature = int.from_bytes(bytearray.fromhex(atcData_str[12:16]),byteorder='big',signed=True) / 10.
615-
print("Temperature: ", temperature)
616-
humidity = int(atcData_str[16:18], 16)
617-
print("Humidity: ", humidity)
618-
batteryVoltage = int(atcData_str[20:24], 16) / 1000
704+
if args.round:
705+
temperature=round(temperature,1)
706+
humidity=round(humidity,1)
707+
708+
measurement.battery = batteryPercent
709+
measurement.humidity = humidity
710+
measurement.temperature = temperature
711+
measurement.voltage = batteryVoltage if batteryVoltage != None else 0
712+
measurement.rssi = rssi
713+
714+
print("Temperature: ", temperature)
715+
print("Humidity: ", humidity)
716+
if batteryVoltage != None:
619717
print ("Battery voltage:", batteryVoltage,"V")
620-
print ("RSSI:", rssi, "dBm")
621-
622-
#if args.battery:
623-
batteryPercent = int(atcData_str[18:20], 16)
624-
print ("Battery:", batteryPercent,"%")
625-
measurement.battery = batteryPercent
626-
measurement.humidity = humidity
627-
measurement.temperature = temperature
628-
measurement.voltage = batteryVoltage
629-
measurement.rssi = rssi
630-
631-
currentMQTTTopic = MQTTTopic
632-
if mac in sensors:
633-
try:
634-
measurement.sensorname = sensors[mac]["sensorname"]
635-
except:
636-
measurement.sensorname = mac
637-
if "offset1" in sensors[mac] and "offset2" in sensors[mac] and "calpoint1" in sensors[mac] and "calpoint2" in sensors[mac]:
638-
measurement.humidity = calibrateHumidity2Points(humidity,int(sensors[mac]["offset1"]),int(sensors[mac]["offset2"]),int(sensors[mac]["calpoint1"]),int(sensors[mac]["calpoint2"]))
639-
print ("Humidity calibrated (2 points calibration): ", measurement.humidity)
640-
elif "humidityOffset" in sensors[mac]:
641-
measurement.humidity = humidity + int(sensors[mac]["humidityOffset"])
642-
print ("Humidity calibrated (offset calibration): ", measurement.humidity)
643-
if "topic" in sensors[mac]:
644-
currentMQTTTopic=sensors[mac]["topic"]
645-
else:
718+
print ("RSSI:", rssi, "dBm")
719+
print ("Battery:", batteryPercent,"%")
720+
721+
currentMQTTTopic = MQTTTopic
722+
if mac in sensors:
723+
try:
724+
measurement.sensorname = sensors[mac]["sensorname"]
725+
except:
646726
measurement.sensorname = mac
647-
648-
if measurement.calibratedHumidity == 0:
649-
measurement.calibratedHumidity = measurement.humidity
727+
if "offset1" in sensors[mac] and "offset2" in sensors[mac] and "calpoint1" in sensors[mac] and "calpoint2" in sensors[mac]:
728+
measurement.humidity = calibrateHumidity2Points(humidity,int(sensors[mac]["offset1"]),int(sensors[mac]["offset2"]),int(sensors[mac]["calpoint1"]),int(sensors[mac]["calpoint2"]))
729+
print ("Humidity calibrated (2 points calibration): ", measurement.humidity)
730+
elif "humidityOffset" in sensors[mac]:
731+
measurement.humidity = humidity + int(sensors[mac]["humidityOffset"])
732+
print ("Humidity calibrated (offset calibration): ", measurement.humidity)
733+
if "topic" in sensors[mac]:
734+
currentMQTTTopic=sensors[mac]["topic"]
735+
else:
736+
measurement.sensorname = mac
737+
738+
if measurement.calibratedHumidity == 0:
739+
measurement.calibratedHumidity = measurement.humidity
650740

651-
if args.callback or args.httpcallback:
652-
measurements.append(measurement)
741+
if args.callback or args.httpcallback:
742+
measurements.append(measurement)
653743

654-
if args.mqttconfigfile:
655-
jsonString=buildJSONString(measurement)
656-
myMQTTPublish(currentMQTTTopic,jsonString)
657-
#MQTTClient.publish(currentMQTTTopic,jsonString,1)
744+
if args.mqttconfigfile:
745+
jsonString=buildJSONString(measurement)
746+
myMQTTPublish(currentMQTTTopic,jsonString)
747+
#MQTTClient.publish(currentMQTTTopic,jsonString,1)
658748

659-
#print("Length:", len(measurements))
660-
print("")
749+
#print("Length:", len(measurements))
750+
print("")
661751

662752
if args.watchdogtimer:
663753
keepingLEScanRunningThread = threading.Thread(target=keepingLEScanRunning)

0 commit comments

Comments
 (0)