Först incheckning av scripten

This commit is contained in:
2020-12-30 13:42:22 +00:00
commit 6940fc1a10
6 changed files with 1017 additions and 0 deletions

515
LYWSD03MMC.py Executable file
View File

@@ -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 <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)

421
bluetooth_utils.py Normal file
View File

@@ -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("<BHHBB", SCAN_TYPE_PASSIVE, interval, window,
own_bdaddr_type, filter_policy)
bluez.hci_send_cmd(sock, OGF_LE_CTL, OCF_LE_SET_SCAN_PARAMETERS, cmd_pkt)
print("scan params: interval=%.3fms window=%.3fms own_bdaddr=%s "
"whitelist=%s" %
(interval * 0.625, window * 0.625,
'public' if own_bdaddr_type == LE_PUBLIC_ADDRESS else 'random',
'yes' if filter_policy in (FILTER_POLICY_SCAN_WHITELIST,
FILTER_POLICY_SCAN_AND_CONN_WHITELIST)
else 'no'))
cmd_pkt = struct.pack("<BB", SCAN_ENABLE, SCAN_FILTER_DUPLICATES if filter_duplicates else 0x00)
bluez.hci_send_cmd(sock, OGF_LE_CTL, OCF_LE_SET_SCAN_ENABLE, cmd_pkt)
def disable_le_scan(sock):
"""
Disable LE scan.
:param sock: A bluetooth HCI socket (retrieved using the
``hci_open_dev`` PyBluez function).
"""
print("Disable LE scan")
cmd_pkt = struct.pack("<BB", SCAN_DISABLE, 0x00)
bluez.hci_send_cmd(sock, OGF_LE_CTL, OCF_LE_SET_SCAN_ENABLE, cmd_pkt)
def start_le_advertising(sock, min_interval=1000, max_interval=1000,
adv_type=ADV_NONCONN_IND, data=()):
"""
Start LE advertising.
:param sock: A bluetooth HCI socket (retrieved using the
``hci_open_dev`` PyBluez function).
:param min_interval: Minimum advertising interval.
:param max_interval: Maximum advertising interval.
:param adv_type: Advertisement type (``ADV_NONCONN_IND`` by default).
:param data: The advertisement data (maximum of 31 bytes).
:type data: iterable
"""
own_bdaddr_type = 0
direct_bdaddr_type = 0
direct_bdaddr = (0,) * 6
chan_map = 0x07 # All channels: 37, 38, 39
filter = 0
struct_params = [min_interval, max_interval, adv_type, own_bdaddr_type,
direct_bdaddr_type]
struct_params.extend(direct_bdaddr)
struct_params.extend((chan_map, filter))
cmd_pkt = struct.pack("<HHBBB6BBB", *struct_params)
bluez.hci_send_cmd(sock, OGF_LE_CTL, OCF_LE_SET_ADVERTISING_PARAMETERS,
cmd_pkt)
cmd_pkt = struct.pack("<B", 0x01)
bluez.hci_send_cmd(sock, OGF_LE_CTL, OCF_LE_SET_ADVERTISE_ENABLE, cmd_pkt)
data_length = len(data)
if data_length > 31:
raise ValueError("data is too long (%d but max is 31 bytes)",
data_length)
cmd_pkt = struct.pack("<B%dB" % data_length, data_length, *data)
bluez.hci_send_cmd(sock, OGF_LE_CTL, OCF_LE_SET_ADVERTISING_DATA, cmd_pkt)
print("Advertising started data_length=%d data=%r" % (data_length, data))
def stop_le_advertising(sock):
"""
Stop LE advertising.
:param sock: A bluetooth HCI socket (retrieved using the
``hci_open_dev`` PyBluez function).
"""
cmd_pkt = struct.pack("<B", 0x00)
bluez.hci_send_cmd(sock, OGF_LE_CTL, OCF_LE_SET_ADVERTISE_ENABLE, cmd_pkt)
print("Advertising stopped")
def parse_le_advertising_events(sock, mac_addr=None, packet_length=None,
handler=None, debug=False):
"""
Parse and report LE advertisements.
This is a blocking call, an infinite loop is started and the
given handler will be called each time a new LE advertisement packet
is detected and corresponds to the given filters.
.. note:: The :func:`.start_le_advertising` function must be
called before calling this function.
:param sock: A bluetooth HCI socket (retrieved using the
``hci_open_dev`` PyBluez function).
:param mac_addr: list of filtered mac address representations
(uppercase, with ':' separators).
If not specified, the LE advertisement of any device will be reported.
Example: mac_addr=('00:2A:5F:FF:25:11', 'DA:FF:12:33:66:12')
:type mac_addr: ``list`` of ``string``
:param packet_length: Filter a specific length of LE advertisement packet.
:type packet_length: ``int``
:param handler: Handler that will be called each time a LE advertisement
packet is available (in accordance with the ``mac_addr``
and ``packet_length`` filters).
:type handler: ``callable`` taking 4 parameters:
mac (``str``), adv_type (``int``), data (``bytes``) and rssi (``int``)
:param debug: Enable debug prints.
:type debug: ``bool``
"""
if not debug and handler is None:
raise ValueError("You must either enable debug or give a handler !")
old_filter = sock.getsockopt(bluez.SOL_HCI, bluez.HCI_FILTER, 14)
flt = bluez.hci_filter_new()
bluez.hci_filter_set_ptype(flt, bluez.HCI_EVENT_PKT)
# bluez.hci_filter_all_events(flt)
bluez.hci_filter_set_event(flt, LE_META_EVENT)
sock.setsockopt(bluez.SOL_HCI, bluez.HCI_FILTER, flt)
print("socket filter set to ptype=HCI_EVENT_PKT event=LE_META_EVENT")
print("Listening ...")
try:
while True:
pkt = full_pkt = sock.recv(255)
ptype, event, plen = struct.unpack("BBB", pkt[:3])
if event != LE_META_EVENT:
# Should never occur because we filtered with this type of event
print("Not a LE_META_EVENT !")
continue
sub_event, = struct.unpack("B", pkt[3:4])
if sub_event != EVT_LE_ADVERTISING_REPORT:
if debug:
print("Not a EVT_LE_ADVERTISING_REPORT !")
continue
pkt = pkt[4:]
adv_type = struct.unpack("b", pkt[1:2])[0]
mac_addr_str = bluez.ba2str(pkt[3:9])
if packet_length and plen != packet_length:
# ignore this packet
if debug:
print("packet with non-matching length: mac=%s adv_type=%02x plen=%s" %
(mac_addr_str, adv_type, plen))
print(raw_packet_to_str(pkt))
continue
data = pkt[9:-1]
rssi = struct.unpack("b", full_pkt[len(full_pkt)-1:len(full_pkt)])[0]
if mac_addr and mac_addr_str not in mac_addr:
if debug:
print("packet with non-matching mac %s adv_type=%02x data=%s RSSI=%s" %
(mac_addr_str, adv_type, raw_packet_to_str(data), rssi))
continue
if debug:
print("LE advertisement: mac=%s adv_type=%02x data=%s RSSI=%d" %
(mac_addr_str, adv_type, raw_packet_to_str(data), rssi))
if handler is not None:
try:
handler(mac_addr_str, adv_type, data, rssi)
except Exception as e:
print('Exception when calling handler with a BLE advertising event: %r' % (e,))
import traceback
traceback.print_exc()
except KeyboardInterrupt:
print("\nRestore previous socket filter")
sock.setsockopt(bluez.SOL_HCI, bluez.HCI_FILTER, old_filter)
raise
"""
def hci_le_add_white_list(int dd, const bdaddr_t *bdaddr, uint8_t type, int to)
{
struct hci_request {
uint16_t ogf;
uint16_t ocf;
int event;
void *cparam;
int clen;
void *rparam;
int rlen;
};
struct hci_request rq;
le_add_device_to_white_list_cp cp;
uint8_t status;
memset(&cp, 0, sizeof(cp));
cp.bdaddr_type = type;
bacpy(&cp.bdaddr, bdaddr);
memset(&rq, 0, sizeof(rq));
rq.ogf = OGF_LE_CTL;
rq.ocf = OCF_LE_ADD_DEVICE_TO_WHITE_LIST;
rq.cparam = &cp;
rq.clen = LE_ADD_DEVICE_TO_WHITE_LIST_CP_SIZE;
rq.rparam = &status;
rq.rlen = 1;
if (hci_send_req(dd, &rq, to) < 0)
return -1;
if (status) {
errno = EIO;
return -1;
}
return 0;
}"""

20
sendToMQTT.sh Executable file
View File

@@ -0,0 +1,20 @@
#!/bin/bash
#This script is provided by Chiunownow https://github.com/Chiunownow
#Thank you very much for providing this script
#This script is
#use e.g with that script: MySensor.sh
#!/bin/bash
#DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
#$DIR/LYWSD03MMC.py -d <device> -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"

11
sensorer.ini Normal file
View File

@@ -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

5
sensorer.sh Executable file
View File

@@ -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

45
temperatur_koksfonstret.py Executable file
View File

@@ -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] <GPIO pin number>')
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)