Skip to content

Commit f7c6a48

Browse files
authored
Add support for Qingping/Cleargrass advertisements and fix support for custom format (#105)
* Added HTTP callback support * Add support for Qingping/ClearGrass advertisements * Fix support for custom format * Qingping/ClearGrass now tested with CGG1 and CGDK2 * Restored commented out code requested by JsBergbau * Fixed missing influxdb timestamp code for ATC mode * Renamed ATC to Passive mode and improved README * Improved Qingping docs in README a bit further * Updated version to 5.0
1 parent 4686505 commit f7c6a48

File tree

2 files changed

+177
-161
lines changed

2 files changed

+177
-161
lines changed

LYWSD03MMC.py

Lines changed: 101 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#-u to unbuffer output. Otherwise when calling with nohup or redirecting output things are printed very lately or would even mixup
33

44
print("---------------------------------------------")
5-
print("MiTemperature2 / ATC Thermometer version 4.0")
5+
print("MiTemperature2 / ATC Thermometer version 5.0")
66
print("---------------------------------------------")
77

88
readme="""
@@ -45,8 +45,8 @@ class Measurement:
4545

4646
def __eq__(self, other): #rssi may be different, so exclude it from comparison
4747
if self.temperature == other.temperature and self.humidity == other.humidity and self.calibratedHumidity == other.calibratedHumidity and self.battery == other.battery and self.sensorname == other.sensorname:
48-
#in atc mode also exclude voltage as it changes often due to frequent measurements
49-
return True if args.atc else (self.voltage == other.voltage)
48+
#in passive mode also exclude voltage as it changes often due to frequent measurements
49+
return True if args.passive else (self.voltage == other.voltage)
5050
else:
5151
return False
5252

@@ -72,7 +72,7 @@ def myMQTTPublish(topic,jsonMessage):
7272

7373

7474
def signal_handler(sig, frame):
75-
if args.atc:
75+
if args.passive:
7676
disable_le_scan(sock)
7777
os._exit(0)
7878

@@ -321,15 +321,15 @@ def MQTTOnDisconnect(client, userdata,rc):
321321
# Main loop --------
322322
parser=argparse.ArgumentParser(allow_abbrev=False,epilog=readme)
323323
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')
324-
parser.add_argument("--battery","-b", help="Get estimated battery level, in ATC-Mode: Get battery level from device", metavar='', type=int, nargs='?', const=1)
324+
parser.add_argument("--battery","-b", help="Get estimated battery level, in passive mode: Get battery level from device", metavar='', type=int, nargs='?', const=1)
325325
parser.add_argument("--count","-c", help="Read/Receive N measurements and then exit script", metavar='N', type=int)
326326
parser.add_argument("--interface","-i", help="Specifiy the interface number to use, e.g. 1 for hci1", metavar='N', type=int, default=0)
327327
parser.add_argument("--unreachable-count","-urc", help="Exit after N unsuccessful connection tries", metavar='N', type=int, default=0)
328328
parser.add_argument("--mqttconfigfile","-mcf", help="specify a configurationfile for MQTT-Broker")
329329

330330

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

335335
offsetgroup = parser.add_argument_group("Offset calibration mode")
@@ -349,12 +349,12 @@ def MQTTOnDisconnect(client, userdata,rc):
349349
callbackgroup.add_argument("--skipidentical","-skip", help="N consecutive identical measurements won't be reported to callbackfunction",metavar='N', type=int, default=0)
350350
callbackgroup.add_argument("--influxdb","-infl", help="Optimize for writing data to influxdb,1 timestamp optimization, 2 integer optimization",metavar='N', type=int, default=0)
351351

352-
atcgroup = parser.add_argument_group("ATC mode related arguments")
353-
atcgroup.add_argument("--atc","-a", help="Read the data of devices with custom ATC firmware flashed, use --battery to get battery level additionaly in percent",action='store_true')
354-
atcgroup.add_argument("--watchdogtimer","-wdt",metavar='X', type=int, help="Re-enable scanning after not receiving any BLE packet after X seconds")
355-
atcgroup.add_argument("--devicelistfile","-df",help="Specify a device list file giving further details to devices")
356-
atcgroup.add_argument("--onlydevicelist","-odl", help="Only read devices which are in the device list file",action='store_true')
357-
atcgroup.add_argument("--rssi","-rs", help="Report RSSI via callback",action='store_true')
352+
passivegroup = parser.add_argument_group("Passive mode related arguments")
353+
passivegroup.add_argument("--passive","-p","--atc","-a", help="Read the data of devices based on BLE advertisements, use --battery to get battery level additionaly in percent",action='store_true')
354+
passivegroup.add_argument("--watchdogtimer","-wdt",metavar='X', type=int, help="Re-enable scanning after not receiving any BLE packet after X seconds")
355+
passivegroup.add_argument("--devicelistfile","-df",help="Specify a device list file giving further details to devices")
356+
passivegroup.add_argument("--onlydevicelist","-odl", help="Only read devices which are in the device list file",action='store_true')
357+
passivegroup.add_argument("--rssi","-rs", help="Report RSSI via callback",action='store_true')
358358

359359

360360
args=parser.parse_args()
@@ -427,7 +427,7 @@ def MQTTOnDisconnect(client, userdata,rc):
427427
else:
428428
print("Please specify device MAC-Address in format AA:BB:CC:DD:EE:FF")
429429
os._exit(1)
430-
elif not args.atc:
430+
elif not args.passive:
431431
parser.print_help()
432432
os._exit(1)
433433

@@ -536,13 +536,13 @@ def MQTTOnDisconnect(client, userdata,rc):
536536
print ("Waiting...")
537537
# Perhaps do something else here
538538

539-
elif args.atc:
540-
print("Script started in ATC Mode")
541-
print("----------------------------")
539+
elif args.passive:
540+
print("Script started in passive mode")
541+
print("------------------------------")
542542
print("In this mode all devices within reach are read out, unless a devicelistfile and --onlydevicelist is specified.")
543543
print("Also --name Argument is ignored, if you require names, please use --devicelistfile.")
544544
print("In this mode debouncing is not available. Rounding option will round humidity and temperature to one decimal place.")
545-
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`)\"")
545+
print("Passive 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`)\"")
546546
print("You have to redo this step if you upgrade your python version.")
547547
print("----------------------------")
548548

@@ -597,82 +597,54 @@ def MQTTOnDisconnect(client, userdata,rc):
597597
try:
598598
prev_data = None
599599

600-
def le_advertise_packet_handler(mac, adv_type, data, rssi):
601-
global lastBLEPaketReceived
602-
if args.watchdogtimer:
603-
lastBLEPaketReceived = time.time()
604-
lastBLEPaketReceived = time.time()
605-
data_str = raw_packet_to_str(data)
600+
def decode_data_atc(mac, adv_type, data_str, rssi, measurement):
606601
preeamble = "161a18"
607602
paketStart = data_str.find(preeamble)
608603
offset = paketStart + len(preeamble)
609-
atcData_str = data_str[offset:offset+26] #if shorter will just be shorter then 13 Bytes
610-
atcData_str = data_str[offset:] #if shorter will just be shorter then 13 Bytes
611-
customFormat_str = data_str[offset:offset+29]
612-
ATCPaketMAC = atcData_str[0:12].upper()
613-
macStr = mac.replace(":","").upper()
614-
atcIdentifier = data_str[(offset-4):offset].upper()
615-
616-
# if (atcIdentifier == "1A18" ) and mac == "A4:C1:38:92:E3:BD" : #debug
617-
# print("BLE packet: %s %02x %s %d" % (mac, adv_type, data_str, rssi))
618-
# print("raw:",data_str)
619-
604+
strippedData_str = data_str[offset:offset+26] #if shorter will just be shorter then 13 Bytes
605+
strippedData_str = data_str[offset:] #if shorter will just be shorter then 13 Bytes
606+
macStr = mac.replace(":","").upper()
607+
dataIdentifier = data_str[(offset-4):offset].upper()
620608

621609
batteryVoltage=None
622-
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
623-
global measurements
624-
measurement = Measurement(0,0,0,0,0,0,0,0)
625-
if len(atcData_str) == 30: #custom format, next-to-last ist adv number
626-
advNumber = atcData_str[-4:-2]
627-
else:
628-
advNumber = atcData_str[-2:] #last data in paket is adv number
629610

611+
if(dataIdentifier == "1A18") and not args.onlydevicelist or (dataIdentifier == "1A18" and mac in sensors) and (len(strippedData_str) in (16, 22, 26, 30)): #only Data from ATC devices
612+
if len(strippedData_str) == 30: #custom format, next-to-last ist adv number
613+
advNumber = strippedData_str[-4:-2]
614+
else:
615+
advNumber = strippedData_str[-2:] #last data in paket is adv number
630616
if macStr in advCounter:
631617
lastAdvNumber = advCounter[macStr]
632618
else:
633619
lastAdvNumber = None
634620
if lastAdvNumber == None or lastAdvNumber != advNumber:
635621

636-
if len(atcData_str) == 26: #ATC1441 Format
637-
#print("atc14441") #debug
622+
if len(strippedData_str) == 26: #ATC1441 Format
623+
print("BLE packet - ATC1441: %s %02x %s %d" % (mac, adv_type, data_str, rssi))
638624
advCounter[macStr] = advNumber
639-
print("BLE packet: %s %02x %s %d" % (mac, adv_type, data_str, rssi))
640-
#print("AdvNumber: ", advNumber)
641-
#temp = data_str[22:26].encode('utf-8')
642-
#temperature = int.from_bytes(bytearray.fromhex(data_str[22:26]),byteorder='big') / 10.
643-
#temperature = int(data_str[22:26],16) / 10.
644-
temperature = int.from_bytes(bytearray.fromhex(atcData_str[12:16]),byteorder='big',signed=True) / 10.
645-
# print("Temperature: ", temperature)
646-
humidity = int(atcData_str[16:18], 16)
647-
# print("Humidity: ", humidity)
648-
batteryVoltage = int(atcData_str[20:24], 16) / 1000
649-
# print ("Battery voltage:", batteryVoltage,"V")
650-
# print ("RSSI:", rssi, "dBm")
651-
652-
#if args.battery:
653-
batteryPercent = int(atcData_str[18:20], 16)
654-
#print ("Battery:", batteryPercent,"%")
655-
656-
elif len(atcData_str) == 30: #custom format
657-
#print("custom:", atcData_str)
658-
print("BLE packet: %s %02x %s %d" % (mac, adv_type, data_str, rssi))
659-
temperature = int.from_bytes(bytearray.fromhex(atcData_str[12:16]),byteorder='little',signed=True) / 100.
660-
humidity = int.from_bytes(bytearray.fromhex(atcData_str[16:20]),byteorder='little',signed=False) / 100.
661-
batteryVoltage = int.from_bytes(bytearray.fromhex(atcData_str[20:24]),byteorder='little',signed=False) / 1000.
662-
batteryPercent = int.from_bytes(bytearray.fromhex(atcData_str[24:26]),byteorder='little',signed=False)
663-
664-
665-
666-
elif len(atcData_str) == 22 or len(atcData_str) == 16: #encrypted: length 22/11 Bytes on custom format, 16/8 Bytes on ATC1441 Format
667-
#print("enc") # debug
668-
#if macStr in encryptedPacketStore:
625+
#temperature = int(data_str[12:16],16) / 10. # this method fails for negative temperatures
626+
temperature = int.from_bytes(bytearray.fromhex(strippedData_str[12:16]),byteorder='big',signed=True) / 10.
627+
humidity = int(strippedData_str[16:18], 16)
628+
batteryVoltage = int(strippedData_str[20:24], 16) / 1000
629+
batteryPercent = int(strippedData_str[18:20], 16)
630+
631+
elif len(strippedData_str) == 30: #Custom format
632+
print("BLE packet - Custom: %s %02x %s %d" % (mac, adv_type, data_str, rssi))
633+
advCounter[macStr] = advNumber
634+
temperature = int.from_bytes(bytearray.fromhex(strippedData_str[12:16]),byteorder='little',signed=True) / 100.
635+
humidity = int.from_bytes(bytearray.fromhex(strippedData_str[16:20]),byteorder='little',signed=False) / 100.
636+
batteryVoltage = int.from_bytes(bytearray.fromhex(strippedData_str[20:24]),byteorder='little',signed=False) / 1000.
637+
batteryPercent = int.from_bytes(bytearray.fromhex(strippedData_str[24:26]),byteorder='little',signed=False)
638+
639+
elif len(strippedData_str) == 22 or len(strippedData_str) == 16: #encrypted: length 22/11 Bytes on custom format, 16/8 Bytes on ATC1441 Format
669640
if macStr in advCounter:
670641
lastData = advCounter[macStr]
671642
else:
672643
lastData = None
673644

674-
if lastData == None or lastData != atcData_str:
675-
print("Encrypted BLE packet: %s %02x %s %d, length: %d" % (mac, adv_type, data_str, rssi, len(atcData_str)/2))
645+
if lastData == None or lastData != strippedData_str:
646+
print("BLE packet - Encrypted: %s %02x %s %d, length: %d" % (mac, adv_type, data_str, rssi, len(strippedData_str)/2))
647+
advCounter[macStr] = strippedData_str
676648
if mac in sensors and "key" in sensors[mac]:
677649
bindkey = bytes.fromhex(sensors[mac]["key"])
678650
macReversed=""
@@ -682,11 +654,11 @@ def le_advertise_packet_handler(mac, adv_type, data, rssi):
682654
#print("New encrypted format, MAC:" , macStr, "Reversed: ", macReversed)
683655
lengthHex=data_str[offset-8:offset-6]
684656
#lengthHex="0b"
685-
ret = cryptoFunctions.decrypt_aes_ccm(bindkey,macReversed,bytes.fromhex(lengthHex + "161a18" + atcData_str))
657+
ret = cryptoFunctions.decrypt_aes_ccm(bindkey,macReversed,bytes.fromhex(lengthHex + "161a18" + strippedData_str))
686658
if ret == None: #Error decrypting
687659
print("\n")
688660
return
689-
#temperature, humidity, batteryPercent = cryptoFunctions.decrypt_aes_ccm(bindkey,macReversed,bytes.fromhex(lengthHex + "161a18" + atcData_str))
661+
#temperature, humidity, batteryPercent = cryptoFunctions.decrypt_aes_ccm(bindkey,macReversed,bytes.fromhex(lengthHex + "161a18" + strippedData_str))
690662
temperature, humidity, batteryPercent = ret
691663
else:
692664
print("Warning: No key provided for sensor:", mac,"\n")
@@ -697,27 +669,65 @@ def le_advertise_packet_handler(mac, adv_type, data, rssi):
697669
else: #Packet is just repeated
698670
return
699671

672+
measurement.battery = batteryPercent
673+
measurement.humidity = humidity
674+
measurement.temperature = temperature
675+
measurement.voltage = batteryVoltage if batteryVoltage != None else 0
676+
measurement.rssi = rssi
677+
return measurement
678+
679+
# Tested with Qingping CGG1 and CGDK2
680+
def decode_data_qingping(mac, adv_type, data_str, rssi, measurement):
681+
preeamble = "cdfd88"
682+
paketStart = data_str.find(preeamble)
683+
offset = paketStart + len(preeamble)
684+
strippedData_str = data_str[offset:offset+32]
685+
macStr = mac.replace(":","").upper()
686+
dataIdentifier = data_str[(offset-2):offset].upper()
687+
688+
if(dataIdentifier == "88") and not args.onlydevicelist or (dataIdentifier == "88" and mac in sensors) and len(strippedData_str) == 32:
689+
print("BLE packet - Qingping: %s %02x %s %d" % (mac, adv_type, data_str, rssi))
690+
temperature = int.from_bytes(bytearray.fromhex(strippedData_str[18:22]),byteorder='little',signed=True) / 10.
691+
humidity = int.from_bytes(bytearray.fromhex(strippedData_str[22:26]),byteorder='little',signed=True) / 10.
692+
batteryPercent = int(strippedData_str[30:32], 16)
693+
694+
measurement.battery = batteryPercent
695+
measurement.humidity = humidity
696+
measurement.temperature = temperature
697+
measurement.rssi = rssi
698+
return measurement
699+
700+
def le_advertise_packet_handler(mac, adv_type, data, rssi):
701+
global lastBLEPaketReceived
702+
if args.watchdogtimer:
703+
lastBLEPaketReceived = time.time()
704+
lastBLEPaketReceived = time.time()
705+
data_str = raw_packet_to_str(data)
706+
707+
global measurements
708+
measurement = Measurement(0,0,0,0,0,0,0,0)
709+
measurement = (
710+
decode_data_atc(mac, adv_type, data_str, rssi, measurement)
711+
or
712+
decode_data_qingping(mac, adv_type, data_str, rssi, measurement)
713+
)
714+
715+
if measurement:
700716
if args.influxdb == 1:
701717
measurement.timestamp = int((time.time() // 10) * 10)
702718
else:
703719
measurement.timestamp = int(time.time())
704720

705721
if args.round:
706-
temperature=round(temperature,1)
707-
humidity=round(humidity,1)
708-
709-
measurement.battery = batteryPercent
710-
measurement.humidity = humidity
711-
measurement.temperature = temperature
712-
measurement.voltage = batteryVoltage if batteryVoltage != None else 0
713-
measurement.rssi = rssi
722+
measurement.temperature=round(measurement.temperature,1)
723+
measurement.humidity=round(measurement.humidity,1)
714724

715-
print("Temperature: ", temperature)
716-
print("Humidity: ", humidity)
717-
if batteryVoltage != None:
718-
print ("Battery voltage:", batteryVoltage,"V")
725+
print("Temperature: ", measurement.temperature)
726+
print("Humidity: ", measurement.humidity)
727+
if measurement.voltage != None:
728+
print ("Battery voltage:", measurement.voltage,"V")
719729
print ("RSSI:", rssi, "dBm")
720-
print ("Battery:", batteryPercent,"%")
730+
print ("Battery:", measurement.battery,"%")
721731

722732
currentMQTTTopic = MQTTTopic
723733
if mac in sensors:
@@ -748,7 +758,7 @@ def le_advertise_packet_handler(mac, adv_type, data, rssi):
748758
#MQTTClient.publish(currentMQTTTopic,jsonString,1)
749759

750760
#print("Length:", len(measurements))
751-
print("")
761+
print("")
752762

753763
if args.watchdogtimer:
754764
keepingLEScanRunningThread = threading.Thread(target=keepingLEScanRunning)

0 commit comments

Comments
 (0)