From 6940fc1a105ab9ad9e1ae8f33cc4fa74eeb4ec77 Mon Sep 17 00:00:00 2001 From: Fredrik Wahlberg Date: Wed, 30 Dec 2020 13:42:22 +0000 Subject: [PATCH] =?UTF-8?q?F=C3=B6rst=20incheckning=20av=20scripten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- LYWSD03MMC.py | 515 +++++++++++++++++++++++++++++++++++++ bluetooth_utils.py | 421 ++++++++++++++++++++++++++++++ sendToMQTT.sh | 20 ++ sensorer.ini | 11 + sensorer.sh | 5 + temperatur_koksfonstret.py | 45 ++++ 6 files changed, 1017 insertions(+) create mode 100755 LYWSD03MMC.py create mode 100644 bluetooth_utils.py create mode 100755 sendToMQTT.sh create mode 100644 sensorer.ini create mode 100755 sensorer.sh create mode 100755 temperatur_koksfonstret.py diff --git a/LYWSD03MMC.py b/LYWSD03MMC.py new file mode 100755 index 0000000..d7c3bf7 --- /dev/null +++ b/LYWSD03MMC.py @@ -0,0 +1,515 @@ +#!/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 + +@dataclass +class Measurement: + temperature: float + humidity: int + voltage: float + calibratedHumidity: int = 0 + battery: int = 0 + timestamp: int = 0 + sensorname: str = "" + rssi: int = 0 + + def __eq__(self, other): #rssi may be different + 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 and self.sensorname == other.sensorname: + return True + else: + return False + +measurements=deque() +#globalBatteryLevel=0 +previousMeasurement=Measurement(0,0,0,0,0,0,0,0) +identicalCounter=0 + +def signal_handler(sig, frame): + if args.atc: + disable_le_scan(sock) + 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 + if ' ' in mea.sensorname: + sensorname = '"' + mea.sensorname + '"' + else: + sensorname = mea.sensorname + params = sensorname + " " + 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) + if (args.rssi): + fmt +=",rssi" + params += " " + str(mea.rssi) + 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()) + +sock = None #from ATC +lastBLEPaketReceived = 0 +BLERestartCounter = 1 +def keepingLEScanRunning(): #LE-Scanning gets disabled sometimes, especially if you have a lot of BLE connections, this thread periodically enables BLE scanning again + global BLERestartCounter + while True: + time.sleep(1) + now = time.time() + if now - lastBLEPaketReceived > args.watchdogtimer: + print("Watchdog: Did not receive any BLE Paket within", int(now - lastBLEPaketReceived), "s. Restarting BLE scan. Count:", BLERestartCounter) + disable_le_scan(sock) + enable_le_scan(sock, filter_duplicates=False) + BLERestartCounter += 1 + print("") + time.sleep(5) #give some time to take effect + + +def calibrateHumidity2Points(humidity, offset1, offset2, calpoint1, calpoint2): + #offset1=args.offset1 + #offset2=args.offset2 + #p1y=args.calpoint1 + #p2y=args.calpoint2 + p1y=calpoint1 + p2y=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)) + return humidityCalibrated + + +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,0,0) + if args.influxdb == 1: + measurement.timestamp = int((time.time() // 10) * 10) + else: + 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,"V") + measurement.temperature = temp + measurement.humidity = humidity + measurement.voltage = voltage + measurement.sensorname = args.name + 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: + humidityCalibrated= calibrateHumidity2Points(humidity,args.offset1,args.offset2, args.calpoint1, args.calpoint2) + print("Calibrated humidity: " + str(humidityCalibrated)) + measurement.calibratedHumidity = humidityCalibrated + + if(args.callback): + measurements.append(measurement) + + + except Exception as e: + print("Fehler") + print(e) + print(traceback.format_exc()) + +# Initialisation ------- + +def connect(): + #print("Interface: " + str(args.interface)) + p = btle.Peripheral(adress,iface=args.interface) + 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 + +# 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", 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) + + +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 arguments") +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) +callbackgroup.add_argument("--influxdb","-infl", help="Optimize for writing data to influxdb,1 timestamp optimization, 2 integer optimization",metavar='N', type=int, default=0) + +atcgroup = parser.add_argument_group("ATC mode related arguments") +atcgroup.add_argument("--atc","-a", help="Read the data of devices with custom ATC firmware flashed",action='store_true') +atcgroup.add_argument("--watchdogtimer","-wdt",metavar='X', type=int, help="Re-enable scanning after not receiving any BLE packet after X seconds") +atcgroup.add_argument("--devicelistfile","-df",help="Specify a device list file giving further details to devices") +atcgroup.add_argument("--onlydevicelist","-odl", help="Only read devices which are in the device list file",action='store_true') +atcgroup.add_argument("--rssi","-rs", help="Report RSSI via callback",action='store_true') + + +args=parser.parse_args() +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 + else: + print("Please specify device MAC-Address in format AA:BB:CC:DD:EE:FF") + os._exit(1) +elif not args.atc: + 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.callback: + dataThread = threading.Thread(target=thread_SendingData) + dataThread.start() + +signal.signal(signal.SIGINT, signal_handler) + +if args.device: + + p=btle.Peripheral() + cnt=0 + + + connected=False + #logging.basicConfig(level=logging.DEBUG) + logging.basicConfig(level=logging.ERROR) + logging.debug("Debug: Starting script...") + pid=os.getpid() + bluepypid=None + unconnectedTime=None + connectionLostCounter=0 + + watchdogThread = threading.Thread(target=watchDog_Thread) + watchdogThread.start() + logging.debug("watchdogThread started") + + 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 != 0: + os.system("kill " + bluepypid) + logging.debug("Killed bluepy with pid: " + str(bluepypid)) + os._exit(0) + print("") + continue + except Exception as e: + print("Connection lost") + connectionLostCounter +=1 + if connected is True: #First connection abort after connected + unconnectedTime=int(time.time()) + connected=False + if args.unreachable_count != 0 and connectionLostCounter >= args.unreachable_count: + print("Maximum numbers of unsuccessful connections reaches, exiting") + os._exit(0) + time.sleep(1) + logging.debug(e) + logging.debug(traceback.format_exc()) + + print ("Waiting...") + # Perhaps do something else here + +elif args.atc: + print("Script started in ATC Mode") + print("----------------------------") + print("In this mode all devices within reach are read out, unless a namefile and --namefileonlydevices is specified.") + print("Also --name Argument is ignored, if you require names, please use --namefile.") + print("In this mode rounding and debouncing are not available, since ATC firmware sends out only one decimal place.") + 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`)\"") + print("You have to redo this step if you upgrade your python version.") + print("----------------------------") + + import sys + import bluetooth._bluetooth as bluez + + from bluetooth_utils import (toggle_device, + enable_le_scan, parse_le_advertising_events, + disable_le_scan, raw_packet_to_str) + + advCounter=dict() + sensors = dict() + if args.devicelistfile: + import configparser + if not os.path.exists(args.devicelistfile): + print ("Error specified device list file '",args.devicelistfile,"' not found") + os._exit(1) + sensors = configparser.ConfigParser() + sensors.read(args.devicelistfile) + + if args.onlydevicelist and not args.devicelistfile: + print("Error: --onlydevicelist requires --devicelistfile ") + os._exit(1) + + dev_id = args.interface # the bluetooth device is hci0 + toggle_device(dev_id, True) + + try: + sock = bluez.hci_open_dev(dev_id) + except: + print("Cannot open bluetooth device %i" % dev_id) + raise + + enable_le_scan(sock, filter_duplicates=False) + + try: + prev_data = None + + def le_advertise_packet_handler(mac, adv_type, data, rssi): + global lastBLEPaketReceived + if args.watchdogtimer: + lastBLEPaketReceived = time.time() + lastBLEPaketReceived = time.time() + #print("reveived BLE packet") + data_str = raw_packet_to_str(data) + ATCPaketMAC = data_str[10:22].upper() + macStr = mac.replace(":","").upper() + atcIdentifier = data_str[6:10].upper() + if(atcIdentifier == "1A18" and ATCPaketMAC == macStr) and not args.onlydevicelist or (atcIdentifier == "1A18" and mac in sensors): #only Data from ATC devices, double checked + advNumber = data_str[-2:] + if macStr in advCounter: + lastAdvNumber = advCounter[macStr] + else: + lastAdvNumber = None + if lastAdvNumber == None or lastAdvNumber != advNumber: + advCounter[macStr] = advNumber + print("BLE packet: %s %02x %s %d" % (mac, adv_type, data_str, rssi)) + #print("AdvNumber: ", advNumber) + #temp = data_str[22:26].encode('utf-8') + #temperature = int.from_bytes(bytearray.fromhex(data_str[22:26]),byteorder='big') / 10. + global measurements + measurement = Measurement(0,0,0,0,0,0,0,0) + if args.influxdb == 1: + measurement.timestamp = int((time.time() // 10) * 10) + else: + measurement.timestamp = int(time.time()) + + + #temperature = int(data_str[22:26],16) / 10. + temperature = int.from_bytes(bytearray.fromhex(data_str[22:26]),byteorder='big',signed=True) / 10. + print("Temperature: ", temperature) + humidity = int(data_str[26:28], 16) + print("Humidity: ", humidity) + batteryVoltage = int(data_str[30:34], 16) / 1000 + print ("Battery voltage:", batteryVoltage,"V") + print ("RSSI:", rssi, "dBm") + + if args.battery: + batteryPercent = int(data_str[28:30], 16) + print ("Battery:", batteryPercent,"%") + measurement.battery = batteryPercent + measurement.humidity = humidity + measurement.temperature = temperature + measurement.voltage = batteryVoltage + measurement.rssi = rssi + + if mac in sensors: + try: + measurement.sensorname = sensors[mac]["sensorname"] + except: + measurement.sensorname = mac + if "offset1" in sensors[mac] and "offset2" in sensors[mac] and "calpoint1" in sensors[mac] and "calpoint2" in sensors[mac]: + measurement.humidity = calibrateHumidity2Points(humidity,int(sensors[mac]["offset1"]),int(sensors[mac]["offset2"]),int(sensors[mac]["calpoint1"]),int(sensors[mac]["calpoint2"])) + print ("Humidity calibrated (2 points calibration): ", measurement.humidity) + elif "humidityOffset" in sensors[mac]: + measurement.humidity = humidity + int(sensors[mac]["humidityOffset"]) + print ("Humidity calibrated (offset calibration): ", measurement.humidity) + else: + measurement.sensorname = mac + if(args.callback): + measurements.append(measurement) + #print("Length:", len(measurements)) + print("") + + if args.watchdogtimer: + keepingLEScanRunningThread = threading.Thread(target=keepingLEScanRunning) + keepingLEScanRunningThread.start() + logging.debug("keepingLEScanRunningThread started") + + + + # Blocking call (the given handler will be called each time a new LE + # advertisement packet is detected) + parse_le_advertising_events(sock, + handler=le_advertise_packet_handler, + debug=False) + except KeyboardInterrupt: + disable_le_scan(sock) diff --git a/bluetooth_utils.py b/bluetooth_utils.py new file mode 100644 index 0000000..529599a --- /dev/null +++ b/bluetooth_utils.py @@ -0,0 +1,421 @@ +# -*- coding: utf-8 -*- +# This file is from https://github.com/colin-guyon/py-bluetooth-utils +# published under MIT License + +# MIT License + +# Copyright (c) 2020 Colin GUYON + +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +""" +Module containing some bluetooth utility functions (linux only). + +It either uses HCI commands using PyBluez, or does ioctl calls like it's +done in Bluez tools such as hciconfig. + +Main functions: + - toggle_device : enable or disable a bluetooth device + - set_scan : set scan type on a device ("noscan", "iscan", "pscan", "piscan") + - enable/disable_le_scan : enable BLE scanning + - parse_le_advertising_events : parse and read BLE advertisements packets + - start/stop_le_advertising : advertise custom data using BLE + +Bluez : http://www.bluez.org/ +PyBluez : http://karulis.github.io/pybluez/ + +The module was in particular inspired from 'iBeacon-Scanner-' +https://github.com/switchdoclabs/iBeacon-Scanner-/blob/master/blescan.py +and sometimes directly from the Bluez sources. +""" + +from __future__ import absolute_import +import sys +import struct +import fcntl +import array +import socket +from errno import EALREADY + +# import PyBluez +import bluetooth._bluetooth as bluez + +__all__ = ('toggle_device', 'set_scan', + 'enable_le_scan', 'disable_le_scan', 'parse_le_advertising_events', + 'start_le_advertising', 'stop_le_advertising', + 'raw_packet_to_str') + +LE_META_EVENT = 0x3E +LE_PUBLIC_ADDRESS = 0x00 +LE_RANDOM_ADDRESS = 0x01 + +OGF_LE_CTL = 0x08 +OCF_LE_SET_SCAN_PARAMETERS = 0x000B +OCF_LE_SET_SCAN_ENABLE = 0x000C +OCF_LE_CREATE_CONN = 0x000D +OCF_LE_SET_ADVERTISING_PARAMETERS = 0x0006 +OCF_LE_SET_ADVERTISE_ENABLE = 0x000A +OCF_LE_SET_ADVERTISING_DATA = 0x0008 + +SCAN_TYPE_PASSIVE = 0x00 +SCAN_FILTER_DUPLICATES = 0x01 +SCAN_DISABLE = 0x00 +SCAN_ENABLE = 0x01 + +# sub-events of LE_META_EVENT +EVT_LE_CONN_COMPLETE = 0x01 +EVT_LE_ADVERTISING_REPORT = 0x02 +EVT_LE_CONN_UPDATE_COMPLETE = 0x03 +EVT_LE_READ_REMOTE_USED_FEATURES_COMPLETE = 0x04 + +# Advertisement event types +ADV_IND = 0x00 +ADV_DIRECT_IND = 0x01 +ADV_SCAN_IND = 0x02 +ADV_NONCONN_IND = 0x03 +ADV_SCAN_RSP = 0x04 + +# Allow Scan Request from Any, Connect Request from Any +FILTER_POLICY_NO_WHITELIST = 0x00 +# Allow Scan Request from White List Only, Connect Request from Any +FILTER_POLICY_SCAN_WHITELIST = 0x01 +# Allow Scan Request from Any, Connect Request from White List Only +FILTER_POLICY_CONN_WHITELIST = 0x02 +# Allow Scan Request from White List Only, Connect Request from White List Only +FILTER_POLICY_SCAN_AND_CONN_WHITELIST = 0x03 + + +def toggle_device(dev_id, enable): + """ + Power ON or OFF a bluetooth device. + + :param dev_id: Device id. + :type dev_id: ``int`` + :param enable: Whether to enable of disable the device. + :type enable: ``bool`` + """ + hci_sock = socket.socket(socket.AF_BLUETOOTH, + socket.SOCK_RAW, + socket.BTPROTO_HCI) + print("Power %s bluetooth device %d" % ('ON' if enable else 'OFF', dev_id)) + # di = struct.pack("HbBIBBIIIHHHH10I", dev_id, *((0,) * 22)) + # fcntl.ioctl(hci_sock.fileno(), bluez.HCIGETDEVINFO, di) + req_str = struct.pack("H", dev_id) + request = array.array("b", req_str) + try: + fcntl.ioctl(hci_sock.fileno(), + bluez.HCIDEVUP if enable else bluez.HCIDEVDOWN, + request[0]) + except IOError as e: + if e.errno == EALREADY: + print("Bluetooth device %d is already %s" % ( + dev_id, 'enabled' if enable else 'disabled')) + else: + raise + finally: + hci_sock.close() + + +# Types of bluetooth scan +SCAN_DISABLED = 0x00 +SCAN_INQUIRY = 0x01 +SCAN_PAGE = 0x02 + + +def set_scan(dev_id, scan_type): + """ + Set scan type on a given bluetooth device. + + :param dev_id: Device id. + :type dev_id: ``int`` + :param scan_type: One of + ``'noscan'`` + ``'iscan'`` + ``'pscan'`` + ``'piscan'`` + :type scan_type: ``str`` + """ + hci_sock = socket.socket(socket.AF_BLUETOOTH, + socket.SOCK_RAW, + socket.BTPROTO_HCI) + if scan_type == "noscan": + dev_opt = SCAN_DISABLED + elif scan_type == "iscan": + dev_opt = SCAN_INQUIRY + elif scan_type == "pscan": + dev_opt = SCAN_PAGE + elif scan_type == "piscan": + dev_opt = SCAN_PAGE | SCAN_INQUIRY + else: + raise ValueError("Unknown scan type %r" % scan_type) + + req_str = struct.pack("HI", dev_id, dev_opt) + print("Set scan type %r to bluetooth device %d" % (scan_type, dev_id)) + try: + fcntl.ioctl(hci_sock.fileno(), bluez.HCISETSCAN, req_str) + finally: + hci_sock.close() + + +def raw_packet_to_str(pkt): + """ + Returns the string representation of a raw HCI packet. + """ + if sys.version_info > (3, 0): + return ''.join('%02x' % struct.unpack("B", bytes([x]))[0] for x in pkt) + else: + return ''.join('%02x' % struct.unpack("B", x)[0] for x in pkt) + + +def enable_le_scan(sock, interval=0x0800, window=0x0800, + filter_policy=FILTER_POLICY_NO_WHITELIST, + filter_duplicates=True): + """ + Enable LE passive scan (with filtering of duplicate packets enabled). + + :param sock: A bluetooth HCI socket (retrieved using the + ``hci_open_dev`` PyBluez function). + :param interval: Scan interval. + :param window: Scan window (must be less or equal than given interval). + :param filter_policy: One of + ``FILTER_POLICY_NO_WHITELIST`` (default value) + ``FILTER_POLICY_SCAN_WHITELIST`` + ``FILTER_POLICY_CONN_WHITELIST`` + ``FILTER_POLICY_SCAN_AND_CONN_WHITELIST`` + + .. note:: Scan interval and window are to multiply by 0.625 ms to + get the real time duration. + """ + print("Enable LE scan") + own_bdaddr_type = LE_PUBLIC_ADDRESS # does not work with LE_RANDOM_ADDRESS + cmd_pkt = struct.pack(" 31: + raise ValueError("data is too long (%d but max is 31 bytes)", + data_length) + cmd_pkt = struct.pack("/dev/null 2>&1 && pwd )" +#$DIR/LYWSD03MMC.py -d -b 1000 -r --debounce --skipidentical 50 --name MySensor --callback sendToMQTT + +#mosquitto_pub -h mqtt.host -t "MiTemperature2/$2/temp" -u mqtt.username -P mqtt.passwd -i "mibridge" -m "$3" +#mosquitto_pub -h mqtt.host -t "MiTemperature2/$2/humidity" -u mqtt.username -P mqtt.passwd -i "mibridge" -m "$4" +#mosquitto_pub -h mqtt.host -t "MiTemperature2/$2/batteryvoltage" -u mqtt.username -P mqtt.passwd -i "mibridge" -m "$5" +#mosquitto_pub -h mqtt.host -t "MiTemperature2/$2/batterylevel" -u mqtt.username -P mqtt.passwd -i "mibridge" -m "$6" + +mosquitto_pub -h 192.168.0.114 -t "MiTemperature2/$2/temp" -u hasse -P casablanca -i "mibridge" -m "$3" +mosquitto_pub -h 192.168.0.114 -t "MiTemperature2/$2/humidity" -u hasse -P casablanca -i "mibridge" -m "$4" +mosquitto_pub -h 192.168.0.114 -t "MiTemperature2/$2/batteryvoltage" -u hasse -P casablanca -i "mibridge" -m "$5" +mosquitto_pub -h 192.168.0.114 -t "MiTemperature2/$2/batterylevel" -u hasse -P casablanca -i "mibridge" -m "$6" diff --git a/sensorer.ini b/sensorer.ini new file mode 100644 index 0000000..95dd88c --- /dev/null +++ b/sensorer.ini @@ -0,0 +1,11 @@ +[A4:C1:38:98:7B:B6] +sensorname=mi_temp_1 + +[A4:C1:38:29:03:0D] +sensorname=mi_temp_2 + +[A4:C1:38:62:CA:83] +sensorname=mi_temp_3 + +[A4:C1:38:D5:EA:63] +sensorname=mi_temp_4 diff --git a/sensorer.sh b/sensorer.sh new file mode 100755 index 0000000..c3f499d --- /dev/null +++ b/sensorer.sh @@ -0,0 +1,5 @@ +#!/bin/bash + + +tmux new-session -d -s sensorer '/home/pi/LYWSD03MMC.py -a -wdt 5 --devicelistfile sensorer.ini --callback sendToMQTT.sh' +tmux detach -s sensorer diff --git a/temperatur_koksfonstret.py b/temperatur_koksfonstret.py new file mode 100755 index 0000000..abbc7f8 --- /dev/null +++ b/temperatur_koksfonstret.py @@ -0,0 +1,45 @@ +#!/usr/bin/python3 +# Copyright (c) 2014 Adafruit Industries +# Author: Tony DiCola + +import sys +import Adafruit_DHT +import paho.mqtt.client as mqtt + +mqttserver = "192.168.0.114" +client = mqtt.Client("koksfonstret") +client.connect(mqttserver) + +# Parse command line parameters. +sensor_args = { '11': Adafruit_DHT.DHT11, + '22': Adafruit_DHT.DHT22, + '2302': Adafruit_DHT.AM2302 } +if len(sys.argv) == 3 and sys.argv[1] in sensor_args: + sensor = sensor_args[sys.argv[1]] + pin = sys.argv[2] +else: + print('Usage: sudo ./Adafruit_DHT.py [11|22|2302] ') + print('Example: sudo ./Adafruit_DHT.py 2302 4 - Read from an AM2302 connected to GPIO pin #4') + sys.exit(1) + + +# Try to grab a sensor reading. Use the read_retry method which will retry up +# to 15 times to get a sensor reading (waiting 2 seconds between each retry). +humidity, temperature = Adafruit_DHT.read_retry(sensor, pin) + +# Un-comment the line below to convert the temperature to Fahrenheit. +# temperature = temperature * 9/5.0 + 32 + +# Note that sometimes you won't get a reading and +# the results will be null (because Linux can't +# guarantee the timing of calls to read the sensor). +# If this happens try again! + +if humidity is not None and temperature is not None: + print('Temp={0:0.1f}* Humidity={1:0.1f}%'.format(temperature, humidity)) + client.publish("casablanca/koksfonstret/temperature", temperature) + client.publish("casablanca/koksfonstret/humidity", humidity) + +else: + print('Failed to get reading. Try again!') + sys.exit(1)