ansible/roles/bluetooth-monitor/other/data_collector.py

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