122 lines
4.8 KiB
Python
122 lines
4.8 KiB
Python
import aiomqtt
|
|
import json
|
|
import asyncio
|
|
from datetime import datetime
|
|
from time import time
|
|
from pathlib import Path
|
|
from collections import namedtuple
|
|
from typing import Dict
|
|
from Cryptodome.Cipher import AES
|
|
|
|
BtleMeasurement = namedtuple("BtleMeasurement", ["time", "tracker", "address", "rssi", "tx_power"])
|
|
BtleDeviceMeasurement = namedtuple("BtleDeviceMeasurement", ["time", "device", "tracker", "rssi", "tx_power"])
|
|
|
|
|
|
class KnownRoomCsvLogger:
|
|
"""Logs known room measurements to be used later as training data for classifier"""
|
|
|
|
def __init__(self, csv_file: Path):
|
|
self.known_room = None
|
|
|
|
if csv_file.exists():
|
|
self.csv_file_handle = open(csv_file, "a")
|
|
else:
|
|
self.csv_file_handle = open(csv_file, "w")
|
|
print(f"#time,device,tracker,rssi,tx_power,real_room", file=csv_file)
|
|
|
|
def update_known_room(self, known_room: str):
|
|
self.known_room = known_room
|
|
|
|
def report_measure(self, m: BtleDeviceMeasurement):
|
|
ignore_rooms = ("keins", "?", "none", "unknown")
|
|
if self.known_room is None or self.known_room in ignore_rooms:
|
|
return
|
|
print(
|
|
f"{m.time},{m.device},{m.tracker},{m.rssi},{m.tx_power},{self.known_room}",
|
|
file=self.csv_file_handle,
|
|
)
|
|
|
|
|
|
class DeviceDecoder:
|
|
|
|
def __init__(self, irk_to_devicename: Dict[str, str], address_to_name: Dict[str, str]):
|
|
"irk_to_devicename is dict with irk as a hex string, mapping to device name"
|
|
self.irk_to_devicename = {bytes.fromhex(k): v for k, v in irk_to_devicename.items()}
|
|
self.address_to_name = address_to_name
|
|
|
|
def _resolve_rpa(rpa: bytes, irk: bytes) -> bool:
|
|
"""Compares the random address rpa to an irk (secret key) and return True if it matches"""
|
|
assert len(rpa) == 6
|
|
assert len(irk) == 16
|
|
|
|
key = irk
|
|
plain_text = b"\x00" * 16
|
|
plain_text = bytearray(plain_text)
|
|
plain_text[15] = rpa[3]
|
|
plain_text[14] = rpa[4]
|
|
plain_text[13] = rpa[5]
|
|
plain_text = bytes(plain_text)
|
|
|
|
cipher = AES.new(key, AES.MODE_ECB)
|
|
cipher_text = cipher.encrypt(plain_text)
|
|
return cipher_text[15] == rpa[0] and cipher_text[14] == rpa[1] and cipher_text[13] == rpa[2]
|
|
|
|
def _addr_to_bytes(addr: str) -> bytes:
|
|
"""Converts a bluetooth mac address string with semicolons to bytes"""
|
|
str_without_colons = addr.replace(":", "")
|
|
bytearr = bytearray.fromhex(str_without_colons)
|
|
bytearr.reverse()
|
|
return bytes(bytearr)
|
|
|
|
def addr_to_bytes(addr: str) -> bytes:
|
|
"""Converts a bluetooth mac address string with semicolons to bytes"""
|
|
str_without_colons = addr.replace(":", "")
|
|
bytearr = bytearray.fromhex(str_without_colons)
|
|
bytearr.reverse()
|
|
return bytes(bytearr)
|
|
|
|
def decode(self, addr: str):
|
|
"""addr is a bluetooth address as a string e.g. 4d:24:12:12:34:10"""
|
|
for irk, name in self.irk_to_devicename.items():
|
|
if self.resolve_rpa(self.addr_to_bytes(addr), irk):
|
|
return name
|
|
return self.addr_to_name.get(addr, None)
|
|
|
|
|
|
server = "homeassistant.fritz.box"
|
|
username = "my_btmonitor"
|
|
password = "8aBIAC14jaKKbla"
|
|
|
|
|
|
async def collect_data_from_mqtt_into_csv():
|
|
now = datetime.now()
|
|
with open(f"logfile_{now}.csv", "w") as csv_file:
|
|
print(f"# time,device,room,rssi,tx_power,real_room", file=csv_file)
|
|
async with aiomqtt.Client(hostname=server, username=username, password=password) as client:
|
|
real_room = "?"
|
|
await client.subscribe("my_btmonitor/#")
|
|
async for message in client.messages:
|
|
current_time = time()
|
|
topic = message.topic
|
|
if topic.value == "my_btmonitor/real_room":
|
|
print(f"Changing real room from {real_room} to {message.payload}")
|
|
real_room = message.payload.decode()
|
|
else:
|
|
splitted_topic = message.topic.value.split("/")
|
|
if splitted_topic[0] == "my_btmonitor" and splitted_topic[1] == "devices":
|
|
device = splitted_topic[2]
|
|
room = splitted_topic[3]
|
|
msg_json = json.loads(message.payload)
|
|
rssi = msg_json.get("rssi", -1)
|
|
tx_power = msg_json.get("tx_power", -1)
|
|
if real_room is not None and real_room != "keins":
|
|
print(
|
|
f"{current_time},{device},{room},{rssi},{tx_power},{real_room}",
|
|
file=csv_file,
|
|
)
|
|
print(f"{current_time},{device},{room},{rssi},{tx_power},{real_room}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(collect_data_from_mqtt_into_csv())
|