Skip to content

Commit

Permalink
v3, added integrated MQTT support
Browse files Browse the repository at this point in the history
  • Loading branch information
JsBergbau authored May 1, 2021
1 parent bb78e3c commit e358a82
Show file tree
Hide file tree
Showing 6 changed files with 358 additions and 135 deletions.
130 changes: 120 additions & 10 deletions LYWSD03MMC.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
#!/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

print("---------------------------------------------")
print("MiTemperature2 / ATC Thermometer version 3.0")
print("---------------------------------------------")

from bluepy import btle
import argparse
import os
Expand All @@ -14,6 +18,7 @@
import traceback
import math
import logging
import json

@dataclass
class Measurement:
Expand All @@ -37,6 +42,22 @@ def __eq__(self, other): #rssi may be different, so exclude it from comparison
#globalBatteryLevel=0
previousMeasurements={}
identicalCounters={}
MQTTClient=None
MQTTTopic=None
receiver=None
subtopics=None
mqttJSONDisabled=False

def myMQTTPublish(topic,jsonMessage):
global subtopics
if len(subtopics) > 0:
messageDict = json.loads(jsonMessage)
for subtopic in subtopics:
print("Topic:",subtopic)
MQTTClient.publish(topic + "/" + subtopic,messageDict[subtopic],0)
if not mqttJSONDisabled:
MQTTClient.publish(topic,jsonMessage,1)


def signal_handler(sig, frame):
if args.atc:
Expand Down Expand Up @@ -70,6 +91,8 @@ def thread_SendingData():
global previousMeasurements
global measurements
path = os.path.dirname(os.path.abspath(__file__))


while True:
try:
mea = measurements.popleft()
Expand Down Expand Up @@ -201,11 +224,11 @@ def handleNotification(self, cHandle, data):
measurement.humidity = humidity
measurement.voltage = voltage
measurement.sensorname = args.name
if args.battery:
#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)
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:
Expand All @@ -220,6 +243,14 @@ def handleNotification(self, cHandle, data):

if(args.callback):
measurements.append(measurement)


if(args.mqttconfigfile):
if measurement.calibratedHumidity == 0:
measurement.calibratedHumidity = measurement.humidity
jsonString=buildJSONString(measurement)
myMQTTPublish(topic,jsonString)
#MQTTClient.publish(MQTTTopic,jsonString,1)


except Exception as e:
Expand All @@ -238,13 +269,30 @@ def connect():
p.withDelegate(MyDelegate("abc"))
return p

def buildJSONString(measurement):
jsonstr = '{"temperature": ' + str(measurement.temperature) + ', "humidity": ' + str(measurement.humidity) + ', "voltage": ' + str(measurement.voltage) \
+ ', "calibratedHumidity": ' + str(measurement.calibratedHumidity) + ', "battery": ' + str(measurement.battery) \
+ ', "timestamp": '+ str(measurement.timestamp) +', "sensor": "' + measurement.sensorname + '", "rssi": ' + str(measurement.rssi) \
+ ', "receiver": "' + receiver + '"}'
return jsonstr

def MQTTOnConnect(client, userdata, flags, rc):
print("MQTT connected with result code "+str(rc))

def MQTTOnPublish(client,userdata,mid):
print("MQTT published, Client:",client," Userdata:",userdata," mid:", mid)

def MQTTOnDisconnect(client, userdata,rc):
print("MQTT disconnected, Client:", client, "Userdata:", userdata, "RC:", rc)

# Main loop --------
parser=argparse.ArgumentParser(allow_abbrev=False)
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, in ATC-Mode: Get battery level from device", 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("--interface","-i", help="Specifiy the interface number to use, e.g. 1 for hci1", metavar='N', type=int, default=0)
parser.add_argument("--unreachable-count","-urc", help="Exit after N unsuccessful connection tries", metavar='N', type=int, default=0)
parser.add_argument("--mqttconfigfile","-mcf", help="specify a configurationfile for MQTT-Broker")


rounding = parser.add_argument_group("Rounding and debouncing")
Expand Down Expand Up @@ -276,6 +324,57 @@ def connect():


args=parser.parse_args()

if args.devicelistfile or args.mqttconfigfile:
import configparser

if args.mqttconfigfile:
try:
import paho.mqtt.client as mqtt
except:
print("Please install MQTT-Library via 'pip/pip3 install paho-mqtt'")
exit(1)
if not os.path.exists(args.mqttconfigfile):
print ("Error MQTT config file '",args.mqttconfigfile,"' not found")
os._exit(1)
mqttConfig = configparser.ConfigParser()
# print(mqttConfig.sections())
mqttConfig.read(args.mqttconfigfile)
broker = mqttConfig["MQTT"]["broker"]
port = int(mqttConfig["MQTT"]["port"])
username = mqttConfig["MQTT"]["username"]
password = mqttConfig["MQTT"]["password"]
MQTTTopic = mqttConfig["MQTT"]["topic"]
lastwill = mqttConfig["MQTT"]["lastwill"]
lwt = mqttConfig["MQTT"]["lwt"]
clientid=mqttConfig["MQTT"]["clientid"]
receiver=mqttConfig["MQTT"]["receivername"]
subtopics=mqttConfig["MQTT"]["subtopics"]
if len(subtopics) > 0:
subtopics=subtopics.split(",")
if "nojson" in subtopics:
subtopics.remove("nojson")
mqttJSONDisabled=True

if len(receiver) == 0:
import socket
receiver=socket.gethostname()

client = mqtt.Client(clientid)
client.on_connect = MQTTOnConnect
client.on_publish = MQTTOnPublish
client.on_disconnect = MQTTOnDisconnect
client.reconnect_delay_set(min_delay=1,max_delay=60)
client.loop_start()
client.username_pw_set(username,password)
if len(lwt) > 0:
print("Using lastwill with topic:",lwt,"and message:",lastwill)
client.will_set(lwt,lastwill,qos=1)

client.connect_async(broker,port)
MQTTClient=client


if args.device:
if re.match("[0-9a-fA-F]{2}([:]?)[0-9a-fA-F]{2}(\\1[0-9a-fA-F]{2}){4}$",args.device):
adress=args.device
Expand Down Expand Up @@ -307,7 +406,6 @@ def connect():
p=btle.Peripheral()
cnt=0


connected=False
#logging.basicConfig(level=logging.DEBUG)
logging.basicConfig(level=logging.ERROR)
Expand Down Expand Up @@ -411,7 +509,7 @@ def connect():
advCounter=dict()
sensors = dict()
if args.devicelistfile:
import configparser
#import configparser
if not os.path.exists(args.devicelistfile):
print ("Error specified device list file '",args.devicelistfile,"' not found")
os._exit(1)
Expand Down Expand Up @@ -485,15 +583,16 @@ def le_advertise_packet_handler(mac, adv_type, data, rssi):
print ("Battery voltage:", batteryVoltage,"V")
print ("RSSI:", rssi, "dBm")

if args.battery:
batteryPercent = int(atcData_str[18:20], 16)
print ("Battery:", batteryPercent,"%")
measurement.battery = batteryPercent
#if args.battery:
batteryPercent = int(atcData_str[18:20], 16)
print ("Battery:", batteryPercent,"%")
measurement.battery = batteryPercent
measurement.humidity = humidity
measurement.temperature = temperature
measurement.voltage = batteryVoltage
measurement.rssi = rssi

currentMQTTTopic = MQTTTopic
if mac in sensors:
try:
measurement.sensorname = sensors[mac]["sensorname"]
Expand All @@ -505,10 +604,21 @@ def le_advertise_packet_handler(mac, adv_type, data, rssi):
elif "humidityOffset" in sensors[mac]:
measurement.humidity = humidity + int(sensors[mac]["humidityOffset"])
print ("Humidity calibrated (offset calibration): ", measurement.humidity)
if "topic" in sensors[mac]:
currentMQTTTopic=sensors[mac]["topic"]
else:
measurement.sensorname = mac

if measurement.calibratedHumidity == 0:
measurement.calibratedHumidity = measurement.humidity

if(args.callback):
measurements.append(measurement)
if(args.mqttconfigfile):
jsonString=buildJSONString(measurement)
myMQTTPublish(currentMQTTTopic,jsonString)
#MQTTClient.publish(currentMQTTTopic,jsonString,1)

#print("Length:", len(measurements))
print("")

Expand Down
1 change: 1 addition & 0 deletions Node-RED flows Callback mode.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
[{"id":"7792f3e3.90fa54","type":"debug","z":"892ece47.eea0d","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1030,"y":1460,"wires":[]},{"id":"95cb1a53.33c6e","type":"http in","z":"892ece47.eea0d","name":"","url":"/MiTemperature2Input","method":"post","upload":false,"swaggerDoc":"","x":180,"y":1620,"wires":[["2039f2f0.1132a6","9c3ed4d1.6fdfa"]]},{"id":"2039f2f0.1132a6","type":"http response","z":"892ece47.eea0d","name":"","statusCode":"","headers":{},"x":510,"y":1680,"wires":[]},{"id":"9c3ed4d1.6fdfa","type":"change","z":"892ece47.eea0d","name":"sensorlist","rules":[{"t":"set","p":"sensors","pt":"msg","to":"{\"info\":{\"info1\":\"MAC Adresses must be in UPPERCASE otherwise sensor won't be found by the script\",\"info2\":\"now all available options are listet. If offset1, offset2, calpoint1 and calpoint2 are given 2Point calibration is used instead of humidityOffset.\",\"info3\":\"Note options are case sensitive\",\"sensorname\":\"Specify an easy readable name\",\"humidityoffset\":-5,\"offset1\":-10,\"offset2\":10,\"calpoint1\":33,\"calpoint2\":75},\"BD:AD:CA:1F:4D:12\":{\"sensorname\":\"Living Room\",\"offset1\":-2,\"offset2\":2,\"calpoint1\":33,\"calpoint2\":75}}","tot":"json"},{"t":"set","p":"processOnlyListedSensors","pt":"msg","to":"true","tot":"bool"}],"action":"","property":"","from":"","to":"","reg":false,"x":520,"y":1620,"wires":[["5ea7697d.8ef96"]]},{"id":"5ea7697d.8ef96","type":"function","z":"892ece47.eea0d","name":"Process and calibrate sensors","func":"processOnlyListedSensors = msg.processOnlyListedSensors || false;\n\nfunction calibrateHumidity2Points(humidity, offset1, offset2, calpoint1, calpoint2)\n{\n p1y=calpoint1\n\tp2y=calpoint2\n\tp1x=p1y - offset1\n\tp2x=p2y - offset2\n\tm = (p1y - p2y) * 1.0 / (p1x - p2x)\n\tb = p2y - m * p2x //would be more efficient to do this calculations only once\n\thumidityCalibrated=m*humidity + b\n\tif (humidityCalibrated > 100 ) //with correct calibration this should not happen\n\t{\n\t humidityCalibrated = 100\n\t}\n\telse if (humidityCalibrated < 0)\n\t{\n\t\thumidityCalibrated = 0\n\t}\n\thumidityCalibrated=Math.round(humidityCalibrated,0)\n\treturn humidityCalibrated\n}\n\n//\"A4:C1:38:69:E2:7C\"\nmsg.topic = msg.payload.sensor;\n\nif (msg.payload.sensor in msg.sensors )\n{\n sensor = msg.payload.sensor;\n sensordaten = msg.sensors[sensor];\n msg.payload.sensor = sensordaten.sensorname;\n msg.topic = msg.payload.sensor;\n if (\"offset1\" in sensordaten && \"offset2\" in sensordaten && \"calpoint1\" in sensordaten && \"calpoint2\" in sensordaten)\n {\n msg.payload.humidity = calibrateHumidity2Points(msg.payload.humidity, sensordaten.offset1, sensordaten.offset2, sensordaten.calpoint1,sensordaten.calpoint2);\n }\n else if (\"humidityOffset\" in sensordaten)\n {\n msg.payload.humidity = msg.payload.humidity + sensordaten.humidityOffset\n }\n \n return msg;\n}\nelse if (! processOnlyListedSensors)\n{\n return msg;\n}\n//drop message because only listed sensors should be processed here","outputs":1,"noerr":0,"initialize":"","finalize":"","x":790,"y":1620,"wires":[["7792f3e3.90fa54","bf91ceb1.25764","90123f90.4d9d68","638a88d4.074aa8"]]},{"id":"bf91ceb1.25764","type":"change","z":"892ece47.eea0d","name":"Process for influxdb","rules":[{"t":"set","p":"measurement","pt":"msg","to":"tempMeasurement","tot":"str"},{"t":"set","p":"precision","pt":"msg","to":"s","tot":"str"},{"t":"set","p":"payload","pt":"msg","to":"[\t{\t \"Temperature\" : $.payload.temperature,\t \"humidity\" : $.payload.humidity,\t \"voltage\": $.payload.voltage,\t \"time\" : $floor($.payload.timestamp/25)*25\t},\t{\t \"Sensorname\": $.topic\t}\t]","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":1120,"y":1620,"wires":[["a438ce61.05b658"]]},{"id":"4128ee0d.2701d8","type":"comment","z":"892ece47.eea0d","name":"Node-Red input interface for LYWSD03MMC sensors via Callback","info":"","x":280,"y":1440,"wires":[]},{"id":"a438ce61.05b658","type":"influxdb out","z":"892ece47.eea0d","influxdb":"f1b4eaef.a4fb88","name":"","measurement":"","precision":"","retentionPolicy":"","x":1450,"y":1620,"wires":[]},{"id":"8a07fb58.2a3da8","type":"comment","z":"892ece47.eea0d","name":"Change here for your sensorlist","info":"If you want to output all your sensors, not only from your sensorlist, set \"processOnlyListedSensors\" to \"false\"","x":590,"y":1580,"wires":[]},{"id":"7e5b3940.65cf3","type":"comment","z":"892ece47.eea0d","name":"Change here your measurement name","info":"The time for influx chunked into 25s segments, ideal for an advertisment intervall of 10s in ATC firmware. With storing the data every 25s, almost every timestamp is stored. This leads to RLE compression of the timestamp thus saving a lot of space in influxdb.\nWith an interval of 20 seconds in tests it occured quite often, that timestamp slots were not filled and thus no RLE compression can be used.","x":1170,"y":1580,"wires":[]},{"id":"638a88d4.074aa8","type":"switch","z":"892ece47.eea0d","name":"Filter sensor","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"Living Room","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":1090,"y":1820,"wires":[["78bc1bab.99eb6c"]]},{"id":"78bc1bab.99eb6c","type":"debug","z":"892ece47.eea0d","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1410,"y":1820,"wires":[]},{"id":"90123f90.4d9d68","type":"switch","z":"892ece47.eea0d","name":"Filter receiver","property":"req.ip","propertyType":"msg","rules":[{"t":"eq","v":"192.168.178.14","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":1100,"y":1760,"wires":[["d5118881.e987d8"]]},{"id":"d5118881.e987d8","type":"debug","z":"892ece47.eea0d","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1410,"y":1760,"wires":[]},{"id":"36855753.1717e8","type":"comment","z":"892ece47.eea0d","name":"Debug / informational part","info":"You can filter here for output of the values of different sensors\n\nAnd you can filter which device sent the input in case you have multiple receivers.","x":1130,"y":1700,"wires":[]},{"id":"6e9028cd.588e6","type":"exec","z":"892ece47.eea0d","command":"/home/pi/MiTemperature2/LYWSD03MMC.py","addpay":true,"append":"","useSpawn":"true","timer":"","oldrc":false,"name":"","x":930,"y":1200,"wires":[["b2f91979.64578"],[],[]]},{"id":"37a90846.8eb728","type":"inject","z":"892ece47.eea0d","name":"Start LYWSD03MMC.py in ATC Mode","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"--atc --watchdogtimer 5 --callback sendToNodeRed.sh --rssi","payloadType":"str","x":230,"y":1140,"wires":[["5c352b22.37b81c"]]},{"id":"b2f91979.64578","type":"debug","z":"892ece47.eea0d","name":"Check if everything is correct running","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1330,"y":1180,"wires":[]},{"id":"a12549ac.15243","type":"comment","z":"892ece47.eea0d","name":"Execute LYWSD03MMC.py directly in Node-RED with Callback mode","info":"Of course you can still run it via normal commandline / cronjob / ...\n","x":290,"y":1080,"wires":[]},{"id":"32097f29.8c395","type":"inject","z":"892ece47.eea0d","name":"Stop Process","props":[{"p":"kill","v":"","vt":"date"},{"p":"reset","v":"","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":150,"y":1200,"wires":[["5c352b22.37b81c","6e9028cd.588e6"]]},{"id":"5c352b22.37b81c","type":"trigger","z":"892ece47.eea0d","name":"Ensure only one instance","op1":"","op2":"0","op1type":"pay","op2type":"str","duration":"0","extend":false,"overrideDelay":false,"units":"ms","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":570,"y":1140,"wires":[["6e9028cd.588e6"]]}]
Loading

0 comments on commit e358a82

Please sign in to comment.