Compare commits
6 Commits
7501ef18a4
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 7c70221723 | |||
|
|
9092f08481 | ||
|
|
e9ec94a5f8 | ||
|
|
fe744b2285 | ||
|
|
ffeee72652 | ||
|
|
fb6f10891d |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,3 +5,4 @@ ve_*
|
|||||||
/music
|
/music
|
||||||
__pycache__
|
__pycache__
|
||||||
/roles/pi-squeezeserver/backup
|
/roles/pi-squeezeserver/backup
|
||||||
|
venv*
|
||||||
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"python.linting.pylintEnabled": false,
|
"python.linting.pylintEnabled": false,
|
||||||
"python.linting.enabled": true,
|
"python.linting.enabled": true,
|
||||||
"python.linting.flake8Enabled": true
|
"python.linting.flake8Enabled": true,
|
||||||
|
"files.associations": {
|
||||||
|
"*.yaml": "home-assistant"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
48
full.yml
48
full.yml
@@ -9,6 +9,7 @@
|
|||||||
# - pi-lirc
|
# - pi-lirc
|
||||||
# - pi-sispmctl
|
# - pi-sispmctl
|
||||||
#
|
#
|
||||||
|
|
||||||
- hosts: musikserverwohnzimmeroben
|
- hosts: musikserverwohnzimmeroben
|
||||||
roles:
|
roles:
|
||||||
- pi-standard-setup
|
- pi-standard-setup
|
||||||
@@ -16,34 +17,45 @@
|
|||||||
- pi-squeezelite-custom
|
- pi-squeezelite-custom
|
||||||
- pi-shairport
|
- pi-shairport
|
||||||
- pi-irserver
|
- pi-irserver
|
||||||
- pi-dhtsensor
|
#- pi-dhtsensor
|
||||||
- pi-squeezeserver
|
- pi-squeezeserver
|
||||||
|
|
||||||
|
- hosts: kitchenpi
|
||||||
|
roles:
|
||||||
|
- pi-standard-setup
|
||||||
|
- pi-hifiberry-amp
|
||||||
|
- pi-squeezelite-custom
|
||||||
|
- pi-shairport
|
||||||
|
- pi-lirc
|
||||||
|
- pi-dhtsensor
|
||||||
|
- pi-disable-onboard-bluetooth
|
||||||
|
- bluetooth-monitor
|
||||||
|
|
||||||
#- hosts: kitchenpi
|
- hosts: bedroompi
|
||||||
# roles:
|
roles:
|
||||||
# - pi-standard-setup
|
- pi-standard-setup
|
||||||
# - pi-hifiberry-amp
|
- pi-squeezelite-custom
|
||||||
# - pi-squeezelite-custom
|
- pi-shairport
|
||||||
# - pi-shairport
|
- pi-lirc
|
||||||
# - pi-lirc
|
- pi-dhtsensor
|
||||||
# - pi-dhtsensor
|
- pi-disable-onboard-bluetooth
|
||||||
|
- bluetooth-monitor
|
||||||
|
|
||||||
#- hosts: bedroompi
|
- hosts: musicmouse
|
||||||
# roles:
|
roles:
|
||||||
# - pi-standard-setup
|
- pi-standard-setup
|
||||||
# - pi-squeezelite-custom
|
- pi-hifiberry-amp
|
||||||
# - pi-shairport
|
- pi-musicmouse
|
||||||
# - pi-lirc
|
- pi-squeezelite-custom
|
||||||
# - pi-dhtsensor
|
- pi-shairport
|
||||||
|
- pi-lirc
|
||||||
|
- bluetooth-monitor
|
||||||
|
|
||||||
#- hosts: octopi
|
#- hosts: octopi
|
||||||
# roles:
|
# roles:
|
||||||
# - pi-dhtsensor
|
# - pi-dhtsensor
|
||||||
|
|
||||||
|
|
||||||
#- hosts: newrpi
|
#- hosts: newrpi
|
||||||
# roles:
|
# roles:
|
||||||
# - pi-standard-setup
|
# - pi-standard-setup
|
||||||
# - pi-lirc
|
# - pi-lirc
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,16 @@ all:
|
|||||||
sensor_room_name_ascii: prusaprinter
|
sensor_room_name_ascii: prusaprinter
|
||||||
sensor_room_name: prusaprinter
|
sensor_room_name: prusaprinter
|
||||||
dht_pin: 26
|
dht_pin: 26
|
||||||
|
main_user: root
|
||||||
bedroompi:
|
bedroompi:
|
||||||
squeezelite_name: BedroomPi
|
squeezelite_name: BedroomPi
|
||||||
shairport_name: BedroomPi
|
shairport_name: BedroomPi
|
||||||
alsa_card_name: Codec
|
alsa_card_name: Codec
|
||||||
sensor_room_name_ascii: schlafzimmer
|
sensor_room_name_ascii: schlafzimmer
|
||||||
sensor_room_name: Schlafzimmer
|
sensor_room_name: Schlafzimmer
|
||||||
|
my_bt_monitor_watchdog_seconds: 600
|
||||||
|
my_btmonitor_restart_ble_interface: hci0
|
||||||
|
main_user: root
|
||||||
kitchenpi:
|
kitchenpi:
|
||||||
squeezelite_name: KitchenPi
|
squeezelite_name: KitchenPi
|
||||||
shairport_name: KitchenPi
|
shairport_name: KitchenPi
|
||||||
@@ -25,6 +29,7 @@ all:
|
|||||||
sensor_room_name_ascii: kueche
|
sensor_room_name_ascii: kueche
|
||||||
sensor_room_name: Küche
|
sensor_room_name: Küche
|
||||||
hifiberry_overlay: hifiberry-amp
|
hifiberry_overlay: hifiberry-amp
|
||||||
|
main_user: root
|
||||||
esszimmerradio: # oben, eltern
|
esszimmerradio: # oben, eltern
|
||||||
squeezelite_name: Esszimmer
|
squeezelite_name: Esszimmer
|
||||||
shairport_name: _Oben_Esszimmer
|
shairport_name: _Oben_Esszimmer
|
||||||
@@ -32,6 +37,7 @@ all:
|
|||||||
squeezeserver: 192.168.178.100
|
squeezeserver: 192.168.178.100
|
||||||
configure_wifi: true
|
configure_wifi: true
|
||||||
alsa_card_name: 1
|
alsa_card_name: 1
|
||||||
|
main_user: root
|
||||||
musikserverwohnzimmeroben: # oben, eltern
|
musikserverwohnzimmeroben: # oben, eltern
|
||||||
squeezelite_name: Wohnzimmer
|
squeezelite_name: Wohnzimmer
|
||||||
shairport_name: _Oben_Wohnzimmer
|
shairport_name: _Oben_Wohnzimmer
|
||||||
@@ -40,11 +46,15 @@ all:
|
|||||||
sensor_room_name_ascii: wohnzimmeroben
|
sensor_room_name_ascii: wohnzimmeroben
|
||||||
sensor_room_name: WohnzimmerOben
|
sensor_room_name: WohnzimmerOben
|
||||||
hifiberry_overlay: hifiberry-dacplus
|
hifiberry_overlay: hifiberry-dacplus
|
||||||
|
main_user: root
|
||||||
musicmouse:
|
musicmouse:
|
||||||
squeezelite_name: MusicMouse
|
squeezelite_name: MusicMouse
|
||||||
shairport_name: MusicMouse
|
shairport_name: MusicMouse
|
||||||
alsa_card_name: 0
|
alsa_card_name: 1
|
||||||
hifiberry_overlay: hifiberry-dacplus
|
hifiberry_overlay: hifiberry-dacplus
|
||||||
|
sensor_room_name: Kinderzimmer
|
||||||
|
sensor_room_name_ascii: kinderzimmer
|
||||||
|
main_user: root
|
||||||
newrpi:
|
newrpi:
|
||||||
squeezelite_name: MyTestRaspberry
|
squeezelite_name: MyTestRaspberry
|
||||||
shairport_name: MyTestRaspberry
|
shairport_name: MyTestRaspberry
|
||||||
@@ -52,6 +62,15 @@ all:
|
|||||||
sensor_room_name_ascii: testraum
|
sensor_room_name_ascii: testraum
|
||||||
sensor_room_name: Test Raum
|
sensor_room_name: Test Raum
|
||||||
heatingpi:
|
heatingpi:
|
||||||
|
server:
|
||||||
|
sensor_room_name: Arbeitszimmer
|
||||||
|
sensor_room_name_ascii: arbeitszimmer
|
||||||
|
my_bt_monitor_watchdog_seconds: 600
|
||||||
|
my_btmonitor_restart_ble_interface: hci0
|
||||||
|
main_user: core
|
||||||
|
homeassistant:
|
||||||
|
sensor_room_name: Anschlussraum
|
||||||
|
sensor_room_name_ascii: anschlussraum
|
||||||
vars:
|
vars:
|
||||||
ansible_user: root
|
ansible_user: root
|
||||||
ansible_python_interpreter: /usr/bin/python3
|
ansible_python_interpreter: /usr/bin/python3
|
||||||
@@ -61,3 +80,5 @@ all:
|
|||||||
home_assistant_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkM2QxYjAwYjkxZjY0MWVhYjA4YmZhMDYwYTg3YjRhNyIsImlhdCI6MTcwNDI3MDU5MSwiZXhwIjoyMDE5NjMwNTkxfQ.dzvejgEQd9hf-Yftzd7NkR5pv76GaLFczeOy-a2pa1o
|
home_assistant_token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkM2QxYjAwYjkxZjY0MWVhYjA4YmZhMDYwYTg3YjRhNyIsImlhdCI6MTcwNDI3MDU5MSwiZXhwIjoyMDE5NjMwNTkxfQ.dzvejgEQd9hf-Yftzd7NkR5pv76GaLFczeOy-a2pa1o
|
||||||
configure_wifi: false
|
configure_wifi: false
|
||||||
wifi_ssid: BauerWLAN
|
wifi_ssid: BauerWLAN
|
||||||
|
my_btmonitor_mqtt_username: my_btmonitor
|
||||||
|
my_btmonitor_mqtt_password: 8aBIAC14jaKKbla
|
||||||
|
|||||||
3
roles/better-shell-env/tasks/files/dot.fish
Normal file
3
roles/better-shell-env/tasks/files/dot.fish
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
function dot -w git -d "Manages dotfiles"
|
||||||
|
git --git-dir=$HOME/.dot --work-tree=$HOME $argv
|
||||||
|
end
|
||||||
47
roles/better-shell-env/tasks/main.yml
Normal file
47
roles/better-shell-env/tasks/main.yml
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
#
|
||||||
|
- name: Install packages
|
||||||
|
apt:
|
||||||
|
name:
|
||||||
|
- bat
|
||||||
|
- broot
|
||||||
|
- duf
|
||||||
|
- fd-find
|
||||||
|
- fish
|
||||||
|
- fzf
|
||||||
|
- git
|
||||||
|
- glances
|
||||||
|
- lsd
|
||||||
|
- neovim
|
||||||
|
- ripgrep
|
||||||
|
- tmux
|
||||||
|
- zoxide
|
||||||
|
|
||||||
|
# TODO: Dust, btm
|
||||||
|
|
||||||
|
block:
|
||||||
|
become: true
|
||||||
|
become_user: {{main_user}}
|
||||||
|
- name: Create bin folder
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: ~/bin
|
||||||
|
state: directory
|
||||||
|
mode: '0755'
|
||||||
|
|
||||||
|
# Oh-my-fish
|
||||||
|
- name: get oh-my-fish repo
|
||||||
|
git:
|
||||||
|
repo: 'https://github.com/oh-my-fish/oh-my-fish.git'
|
||||||
|
dest: ~/.local/share/git/oh-my-fish
|
||||||
|
- name: install oh-my-fish
|
||||||
|
shell:
|
||||||
|
cmd: "bin/install --offline --noninteractive"
|
||||||
|
executable: /usr/bin/fish
|
||||||
|
chdir: ~/.local/share/git/oh-my-fish
|
||||||
|
creates:
|
||||||
|
- ~/.local/share/omf
|
||||||
|
- ~/.config/omf
|
||||||
|
- copy:
|
||||||
|
content: "bobthefish"
|
||||||
|
dst: "~/.config/omf/theme"
|
||||||
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from bleak import BleakScanner
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
stop_event = asyncio.Event()
|
|
||||||
|
|
||||||
# TODO: add something that calls stop_event.set()
|
|
||||||
|
|
||||||
def callback(device, advertising_data):
|
|
||||||
print("device", device.address, "advertising_data", advertising_data)
|
|
||||||
# TODO: do something with incoming data
|
|
||||||
pass
|
|
||||||
|
|
||||||
async with BleakScanner(callback, scanning_mode="active") as scanner:
|
|
||||||
# Important! Wait for an event to trigger stop, otherwise scanner
|
|
||||||
# will stop immediately.
|
|
||||||
await stop_event.wait()
|
|
||||||
|
|
||||||
# scanner stops when block exits
|
|
||||||
...
|
|
||||||
|
|
||||||
asyncio.run(main())
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
import logging
|
|
||||||
from bleak import BleakClient, BleakScanner
|
|
||||||
from bleak.assigned_numbers import AdvertisementDataType
|
|
||||||
from bleak.backends.bluezdbus.advertisement_monitor import OrPattern
|
|
||||||
from bleak.backends.bluezdbus.scanner import BlueZScannerArgs
|
|
||||||
|
|
||||||
async def scan():
|
|
||||||
args = BlueZScannerArgs(
|
|
||||||
or_patterns=[OrPattern(0, AdvertisementDataType.MANUFACTURER_SPECIFIC_DATA, b"\x10\x05")]
|
|
||||||
)
|
|
||||||
|
|
||||||
async with BleakScanner(bluez=args, scanning_mode="passive") as scanner:
|
|
||||||
async for _, advertisement_data in scanner.advertisement_data():
|
|
||||||
mfr_data = advertisement_data.manufacturer_data
|
|
||||||
if mfr_data.get(0x02e1):
|
|
||||||
logging.info("scan(): found correct device: %s", mfr_data)
|
|
||||||
else:
|
|
||||||
logging.info("scan(): this should never happen: %s", mfr_data)
|
|
||||||
|
|
||||||
async def connect():
|
|
||||||
device1 = await BleakScanner.find_device_by_address("01:B6:EC:10:CB:8F")
|
|
||||||
|
|
||||||
async with BleakClient(device1):
|
|
||||||
logging.info("connect(): connected to device")
|
|
||||||
await asyncio.sleep(60)
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
logging.info("main(): starting scan")
|
|
||||||
asyncio.create_task(scan())
|
|
||||||
await asyncio.sleep(30)
|
|
||||||
|
|
||||||
logging.basicConfig(
|
|
||||||
level=logging.INFO,
|
|
||||||
format="%(asctime)-15s %(name)-8s %(levelname)s: %(message)s",
|
|
||||||
)
|
|
||||||
|
|
||||||
asyncio.run(main())
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from bleak import BleakScanner
|
|
||||||
from bleak.assigned_numbers import AdvertisementDataType
|
|
||||||
from bleak.backends.bluezdbus.advertisement_monitor import OrPattern
|
|
||||||
from bleak.backends.bluezdbus.scanner import BlueZScannerArgs
|
|
||||||
from Crypto.Cipher import AES
|
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
irks = {
|
|
||||||
"aa67542b82c0e05d65c27fb7e313aba5": "martins_apple_watch",
|
|
||||||
"840e3892644c1ebd1594a9069c14ce0d" : "martins_iphone",
|
|
||||||
}
|
|
||||||
|
|
||||||
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 decode_address(addr: str, irks: Dict[str, str]):
|
|
||||||
"""
|
|
||||||
addr is a bluetooth address as a string e.g. 4d:24:12:12:34:10
|
|
||||||
irks is dict with irk as a hex string, mapping to device name
|
|
||||||
"""
|
|
||||||
for irk, name in irks.items():
|
|
||||||
if resolve_rpa(addr_to_bytes(addr), bytes.fromhex(irk)):
|
|
||||||
return name
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def estimate_distance(rssi, tx_power, pl0=54):
|
|
||||||
"""
|
|
||||||
RSSI in dBm
|
|
||||||
txPower is a transmitter parameter that calculated according to its physic layer and antenna in dBm
|
|
||||||
Return value in meter
|
|
||||||
|
|
||||||
You should calculate "PL0" in calibration stage:
|
|
||||||
PL0 = txPower - RSSI; // When distance is distance0 (distance0 = 1m or more)
|
|
||||||
|
|
||||||
SO, RSSI will be calculated by below formula:
|
|
||||||
RSSI = txPower - PL0 - 10 * n * log(distance/distance0) - G(t)
|
|
||||||
G(t) ~= 0 //This parameter is the main challenge in achiving to more accuracy.
|
|
||||||
n = 2 (Path Loss Exponent, in the free space is 2)
|
|
||||||
distance0 = 1 (m)
|
|
||||||
distance = 10 ^ ((txPower - RSSI - PL0 ) / (10 * n))
|
|
||||||
|
|
||||||
Read more details:
|
|
||||||
https://en.wikipedia.org/wiki/Log-distance_path_loss_model
|
|
||||||
"""
|
|
||||||
n = 2.7
|
|
||||||
return 10**(( tx_power - rssi - pl0) / (10 * n))
|
|
||||||
|
|
||||||
|
|
||||||
def callback(device, advertising_data):
|
|
||||||
print(device.address)
|
|
||||||
decoded = decode_address(device.address, irks)
|
|
||||||
if decoded:
|
|
||||||
print(f"{decoded} @ {advertising_data.rssi} distance {estimate_distance(advertising_data.rssi, advertising_data.tx_power)}")
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
# Scan for
|
|
||||||
args = BlueZScannerArgs(
|
|
||||||
or_patterns=[OrPattern(0, AdvertisementDataType.MANUFACTURER_SPECIFIC_DATA, b"\x10\x05")]
|
|
||||||
)
|
|
||||||
stop_event = asyncio.Event()
|
|
||||||
|
|
||||||
#async with BleakScanner(bluez=args, scanning_mode="passive") as scanner:
|
|
||||||
async with BleakScanner(callback) as scanner:
|
|
||||||
await stop_event.wait()
|
|
||||||
|
|
||||||
asyncio.run(main())
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import asyncio
|
|
||||||
from bleak import BleakScanner
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
devices = await BleakScanner.discover()
|
|
||||||
while True:
|
|
||||||
for d in devices:
|
|
||||||
print(d)
|
|
||||||
|
|
||||||
asyncio.run(main())
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
"""Scan for iBeacons.
|
|
||||||
Copyright (c) 2022 Koen Vervloesem
|
|
||||||
SPDX-License-Identifier: MIT
|
|
||||||
"""
|
|
||||||
import asyncio
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from construct import Array, Byte, Const, Int8sl, Int16ub, Struct
|
|
||||||
from construct.core import ConstError
|
|
||||||
|
|
||||||
from bleak import BleakScanner
|
|
||||||
from bleak.backends.device import BLEDevice
|
|
||||||
from bleak.backends.scanner import AdvertisementData
|
|
||||||
|
|
||||||
ibeacon_format = Struct(
|
|
||||||
"type_length" / Const(b"\x02\x15"),
|
|
||||||
"uuid" / Array(16, Byte),
|
|
||||||
"major" / Int16ub,
|
|
||||||
"minor" / Int16ub,
|
|
||||||
"power" / Int8sl,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def device_found(
|
|
||||||
device: BLEDevice, advertisement_data: AdvertisementData
|
|
||||||
):
|
|
||||||
"""Decode iBeacon."""
|
|
||||||
try:
|
|
||||||
apple_data = advertisement_data.manufacturer_data[0x004C]
|
|
||||||
print("apple data", apple_data)
|
|
||||||
ibeacon = ibeacon_format.parse(apple_data)
|
|
||||||
uuid = UUID(bytes=bytes(ibeacon.uuid))
|
|
||||||
print(f"UUID : {uuid}")
|
|
||||||
print(f"Major : {ibeacon.major}")
|
|
||||||
print(f"Minor : {ibeacon.minor}")
|
|
||||||
print(f"TX power : {ibeacon.power} dBm")
|
|
||||||
print(f"RSSI : {device.rssi} dBm")
|
|
||||||
print(47 * "-")
|
|
||||||
except KeyError:
|
|
||||||
# Apple company ID (0x004c) not found
|
|
||||||
pass
|
|
||||||
except ConstError:
|
|
||||||
# No iBeacon (type 0x02 and length 0x15)
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
|
||||||
"""Scan for devices."""
|
|
||||||
scanner = BleakScanner()
|
|
||||||
scanner.register_detection_callback(device_found)
|
|
||||||
|
|
||||||
while True:
|
|
||||||
await scanner.start()
|
|
||||||
await asyncio.sleep(1.0)
|
|
||||||
await scanner.stop()
|
|
||||||
|
|
||||||
|
|
||||||
asyncio.run(main())
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
devices:
|
|
||||||
martins_iphone: 840e3892644c1ebd1594a9069c14ce0d
|
|
||||||
martins_apple_watch: aa67542b82c0e05d65c27fb7e313aba5
|
|
||||||
131
roles/bluetooth-monitor/files/filter.cpp
Normal file
131
roles/bluetooth-monitor/files/filter.cpp
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
#include <cmath>
|
||||||
|
#include <vector>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
using real_t = double;
|
||||||
|
|
||||||
|
static constexpr real_t SPIKE_THRESHOLD = 1.0f; // Threshold for spike detection
|
||||||
|
static constexpr int NUM_READINGS = 12; // Number of readings to keep track of
|
||||||
|
|
||||||
|
|
||||||
|
class FilteredDistance {
|
||||||
|
public:
|
||||||
|
FilteredDistance(real_t minCutoff = 1e-1f, real_t beta = 1e-3, real_t dcutoff = 5e-3);
|
||||||
|
void addMeasurement(real_t dist, real_t time_now_in_seconds);
|
||||||
|
const real_t getMedianDistance() const;
|
||||||
|
const real_t getDistance() const;
|
||||||
|
const real_t getVariance() const;
|
||||||
|
|
||||||
|
bool hasValue() const { return lastTime != 0; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
real_t minCutoff;
|
||||||
|
real_t beta;
|
||||||
|
real_t dcutoff;
|
||||||
|
real_t x, dx;
|
||||||
|
real_t lastDist;
|
||||||
|
real_t lastTime;
|
||||||
|
|
||||||
|
real_t getAlpha(real_t cutoff, real_t dT);
|
||||||
|
|
||||||
|
real_t readings[NUM_READINGS]; // Array to store readings
|
||||||
|
int readIndex; // Current position in the array
|
||||||
|
real_t total; // Total of the readings
|
||||||
|
real_t totalSquared; // Total of the squared readings
|
||||||
|
|
||||||
|
void initSpike(real_t dist);
|
||||||
|
real_t removeSpike(real_t dist);
|
||||||
|
};
|
||||||
|
|
||||||
|
FilteredDistance::FilteredDistance(real_t minCutoff, real_t beta, real_t dcutoff)
|
||||||
|
: minCutoff(minCutoff), beta(beta), dcutoff(dcutoff), x(0), dx(0), lastDist(0), lastTime(-1), total(0), totalSquared(0), readIndex(0) {
|
||||||
|
}
|
||||||
|
|
||||||
|
void FilteredDistance::initSpike(real_t dist) {
|
||||||
|
for (size_t i = 0; i < NUM_READINGS; i++) {
|
||||||
|
readings[i] = dist;
|
||||||
|
}
|
||||||
|
total = dist * NUM_READINGS;
|
||||||
|
totalSquared = dist * dist * NUM_READINGS; // Initialize sum of squared distances
|
||||||
|
}
|
||||||
|
|
||||||
|
real_t FilteredDistance::removeSpike(real_t dist) {
|
||||||
|
total -= readings[readIndex]; // Subtract the last reading
|
||||||
|
totalSquared -= readings[readIndex] * readings[readIndex]; // Subtract the square of the last reading
|
||||||
|
|
||||||
|
readings[readIndex] = dist; // Read the sensor
|
||||||
|
total += readings[readIndex]; // Add the reading to the total
|
||||||
|
totalSquared += readings[readIndex] * readings[readIndex]; // Add the square of the reading
|
||||||
|
|
||||||
|
readIndex = (readIndex + 1) % NUM_READINGS; // Advance to the next position in the array
|
||||||
|
|
||||||
|
auto average = total / static_cast<real_t>(NUM_READINGS); // Calculate the average
|
||||||
|
|
||||||
|
if (std::fabs(dist - average) > SPIKE_THRESHOLD)
|
||||||
|
return average; // Spike detected, use the average as the filtered value
|
||||||
|
|
||||||
|
return dist; // No spike, return the new value
|
||||||
|
}
|
||||||
|
|
||||||
|
void FilteredDistance::addMeasurement(real_t dist, real_t time_now_in_seconds) {
|
||||||
|
const bool initialized = lastTime >= 0;
|
||||||
|
const real_t elapsed = time_now_in_seconds - lastTime;
|
||||||
|
lastTime = time_now_in_seconds;
|
||||||
|
|
||||||
|
if (!initialized) {
|
||||||
|
x = dist; // Set initial filter state to the first reading
|
||||||
|
dx = 0; // Initial derivative is unknown, so we set it to zero
|
||||||
|
lastDist = dist;
|
||||||
|
initSpike(dist);
|
||||||
|
} else {
|
||||||
|
real_t dT = std::max(elapsed, real_t(0.05)); // Convert microseconds to seconds, enforce a minimum dT
|
||||||
|
const real_t alpha = getAlpha(minCutoff, dT);
|
||||||
|
const real_t dAlpha = getAlpha(dcutoff, dT);
|
||||||
|
dist = removeSpike(dist);
|
||||||
|
x += alpha * (dist - x);
|
||||||
|
dx = dAlpha * ((dist - lastDist) / dT);
|
||||||
|
lastDist = x + beta * dx;
|
||||||
|
std::cout << "alpha=" << alpha <<
|
||||||
|
" dAlpha=" << dAlpha <<
|
||||||
|
" dist=" << dist <<
|
||||||
|
" x=" << x <<
|
||||||
|
" dx=" << dx <<
|
||||||
|
" lastDist=" << lastDist <<
|
||||||
|
std::endl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const real_t FilteredDistance::getDistance() const {
|
||||||
|
return lastDist;
|
||||||
|
}
|
||||||
|
|
||||||
|
real_t FilteredDistance::getAlpha(real_t cutoff, real_t dT) {
|
||||||
|
real_t tau = 1.0f / (2 * M_PI * cutoff);
|
||||||
|
return 1.0f / (1.0f + tau / dT);
|
||||||
|
}
|
||||||
|
|
||||||
|
const real_t FilteredDistance::getVariance() const {
|
||||||
|
auto mean = total / static_cast<real_t>(NUM_READINGS);
|
||||||
|
auto meanOfSquares = totalSquared / static_cast<real_t>(NUM_READINGS);
|
||||||
|
auto variance = meanOfSquares - (mean * mean); // Variance formula: E(X^2) - (E(X))^2
|
||||||
|
if (variance < 0.0f) return 0.0f;
|
||||||
|
return variance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int main(int argc, char**argv)
|
||||||
|
{
|
||||||
|
FilteredDistance f;
|
||||||
|
std::vector<real_t> values = {1.5, 2.9, 5.3, 15.1, 1.5, 2.5, 1.5, 2.9, 5.3, 15.1};
|
||||||
|
|
||||||
|
real_t time = 0.0;
|
||||||
|
//std::cout << " result_cpp = [";
|
||||||
|
for(int i=0; i < 1; ++i)
|
||||||
|
for(auto value : values) {
|
||||||
|
f.addMeasurement(value, time);
|
||||||
|
time += 1.0;
|
||||||
|
//std::cout << f.getDistance() << ", ";
|
||||||
|
}
|
||||||
|
//std::cout << "]" << std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
91
roles/bluetooth-monitor/files/filter.py
Normal file
91
roles/bluetooth-monitor/files/filter.py
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
#from time import time
|
||||||
|
import math
|
||||||
|
from scipy import signal
|
||||||
|
|
||||||
|
# Taken from ESPresense C++ code
|
||||||
|
class FilteredDistance:
|
||||||
|
NUM_READINGS = 100
|
||||||
|
SPIKE_THRESHOLD = 1.0
|
||||||
|
|
||||||
|
def __init__(self, min_cutoff : float = 1e-1, beta : float = 1e-3, dcutoff : float = 5e-3):
|
||||||
|
self.min_cutoff = min_cutoff
|
||||||
|
self.beta = beta
|
||||||
|
self.dcutoff = dcutoff
|
||||||
|
self.x = 0
|
||||||
|
self.dx = 0
|
||||||
|
self.last_dist = 0
|
||||||
|
self.last_time = -1.0
|
||||||
|
self.total = 0
|
||||||
|
self.read_index = 0
|
||||||
|
self.readings = []
|
||||||
|
|
||||||
|
def _init_spike(self, dist : float):
|
||||||
|
self.readings = [dist] * self.NUM_READINGS
|
||||||
|
self.total = sum(self.readings)
|
||||||
|
|
||||||
|
def _remove_spike(self, dist: float):
|
||||||
|
self.total -= self.readings[self.read_index]
|
||||||
|
|
||||||
|
self.readings[self.read_index] = dist
|
||||||
|
|
||||||
|
self.total += dist
|
||||||
|
|
||||||
|
self.read_index = (self.read_index + 1) % self.NUM_READINGS
|
||||||
|
average = self.total / self.NUM_READINGS
|
||||||
|
if abs(dist - average) > self.SPIKE_THRESHOLD:
|
||||||
|
return average # spike detected
|
||||||
|
else:
|
||||||
|
return dist
|
||||||
|
|
||||||
|
def _get_alpha(self, cutoff : float, dT : float):
|
||||||
|
tau = 1 / (2 * math.pi * cutoff)
|
||||||
|
return 1 / (1 + tau / dT)
|
||||||
|
|
||||||
|
def add_measurement(self, dist : float, time_now_in_seconds: float):
|
||||||
|
initialized = (self.last_time >= 0.0)
|
||||||
|
elapsed = time_now_in_seconds - self.last_time
|
||||||
|
self.last_time = time_now_in_seconds
|
||||||
|
if not initialized:
|
||||||
|
self.x = dist
|
||||||
|
self.dx = 0
|
||||||
|
self.last_dist = dist
|
||||||
|
self._init_spike(dist)
|
||||||
|
else:
|
||||||
|
dT = max(elapsed, 0.05)
|
||||||
|
alpha = self._get_alpha(self.min_cutoff, dT)
|
||||||
|
d_alpha = self._get_alpha(self.dcutoff, dT)
|
||||||
|
dist = self._remove_spike(dist)
|
||||||
|
self.x += alpha * (dist - self.x)
|
||||||
|
self.dx = d_alpha * ((dist - self.last_dist) / dT)
|
||||||
|
self.last_dist = self.x + self.beta * self.dx
|
||||||
|
#print(f"{alpha=} {d_alpha=} {dist=} {self.x=} {self.dx=} {self.last_dist=}")
|
||||||
|
|
||||||
|
def get_distance(self):
|
||||||
|
return self.last_dist
|
||||||
|
|
||||||
|
def run_test(times, values, **kwargs):
|
||||||
|
f = FilteredDistance(**kwargs)
|
||||||
|
result = []
|
||||||
|
for t, value in zip(times, values):
|
||||||
|
f.add_measurement(value, t)
|
||||||
|
result.append(f.get_distance())
|
||||||
|
return result
|
||||||
|
|
||||||
|
def smooth(y, box_pts):
|
||||||
|
box = np.ones(box_pts)/box_pts
|
||||||
|
y_smooth = np.convolve(y, box, mode='same')
|
||||||
|
return y_smooth
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import numpy as np
|
||||||
|
values = np.array([1] * 20 + [2, 4, 6, 7, 10, 16, 10, 13, 16, 24, 13] + [1] * 20 )
|
||||||
|
|
||||||
|
times = np.arange(0, len(values)) * 10
|
||||||
|
result_default = run_test(times, values)
|
||||||
|
result_beta1 = smooth(values, 6)
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
plt.plot(times, values, label="raw")
|
||||||
|
#plt.plot(times, result_default, marker="o", label="filtered")
|
||||||
|
plt.plot(times, result_beta1, marker='x', label="altered")
|
||||||
|
plt.legend()
|
||||||
|
plt.show()
|
||||||
BIN
roles/bluetooth-monitor/files/filtered
Executable file
BIN
roles/bluetooth-monitor/files/filtered
Executable file
Binary file not shown.
11
roles/bluetooth-monitor/files/my_btmonitor.service
Normal file
11
roles/bluetooth-monitor/files/my_btmonitor.service
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=My Bluetooth monitor
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
Restart=always
|
||||||
|
ExecStart=/usr/bin/my_btmonitor
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
11
roles/bluetooth-monitor/other/Dockerfile
Normal file
11
roles/bluetooth-monitor/other/Dockerfile
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
FROM python:3
|
||||||
|
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
COPY requirements.txt ./
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY bt_monitor_server.py .
|
||||||
|
COPY training_data.csv .
|
||||||
|
|
||||||
|
CMD [ "python", "./bt_monitor_server.py" ]
|
||||||
95
roles/bluetooth-monitor/other/analysis.py
Normal file
95
roles/bluetooth-monitor/other/analysis.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
from copy import copy
|
||||||
|
from sklearn.model_selection import cross_val_score
|
||||||
|
from sklearn import svm
|
||||||
|
from sklearn.neural_network import MLPClassifier
|
||||||
|
from sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from sklearn.model_selection import train_test_split
|
||||||
|
|
||||||
|
def load_measurements(csv_file: Path):
|
||||||
|
def cleanup_column_name(col_name: str):
|
||||||
|
clean_name = col_name.replace('#', '').strip()
|
||||||
|
if clean_name == 'room':
|
||||||
|
return 'tracker'
|
||||||
|
return clean_name
|
||||||
|
|
||||||
|
df = pd.read_csv(str(csv_file))
|
||||||
|
|
||||||
|
# String cleanup in column names and room names
|
||||||
|
df = df.rename(columns=cleanup_column_name)
|
||||||
|
df.applymap(lambda x: x.strip() if isinstance(x, str) else x)
|
||||||
|
|
||||||
|
df['tracker'] = df['tracker'].astype("category")
|
||||||
|
df['real_room'] = df['real_room'].astype("category")
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
FAR_AWAY_FEATURE_VALUE = 1
|
||||||
|
def get_feature_value(rssi, tx_power):
|
||||||
|
MIN_RSSI = -90
|
||||||
|
MAX_TRANSFORMED_RSSI = 40
|
||||||
|
v = tx_power - rssi - MAX_TRANSFORMED_RSSI
|
||||||
|
if v < 0:
|
||||||
|
v = 0
|
||||||
|
return v / (-MIN_RSSI)
|
||||||
|
|
||||||
|
|
||||||
|
def make_training_data(df: pd.DataFrame, device_to_map):
|
||||||
|
idx_to_tracker = dict(enumerate(df['tracker'].cat.categories ))
|
||||||
|
tracker_to_idx = {v: k for k, v in idx_to_tracker.items()}
|
||||||
|
idx_to_room = dict(enumerate(df['real_room'].cat.categories ))
|
||||||
|
room_to_idx = {v: k for k, v in idx_to_room.items()}
|
||||||
|
|
||||||
|
last_real_room = None
|
||||||
|
start_time = None
|
||||||
|
current_feature = [FAR_AWAY_FEATURE_VALUE] * len(idx_to_tracker)
|
||||||
|
|
||||||
|
features = []
|
||||||
|
labels = []
|
||||||
|
|
||||||
|
# Feature vectors - rssi column for each room
|
||||||
|
for i, row in df.iterrows():
|
||||||
|
time, device, tracker, rssi, tx_power, real_room = row
|
||||||
|
if device != device_to_map:
|
||||||
|
continue
|
||||||
|
if last_real_room != real_room:
|
||||||
|
start_time = time
|
||||||
|
last_real_room = real_room
|
||||||
|
|
||||||
|
tracker_idx = tracker_to_idx[tracker]
|
||||||
|
current_feature[tracker_idx] = get_feature_value(rssi, tx_power)
|
||||||
|
if time - start_time > 20:
|
||||||
|
features.append(copy(current_feature))
|
||||||
|
labels.append(room_to_idx[real_room])
|
||||||
|
|
||||||
|
return np.array(features), np.array(labels)
|
||||||
|
|
||||||
|
def train(features, labels, classes):
|
||||||
|
clf = svm.SVC(kernel='rbf')
|
||||||
|
print("Training")
|
||||||
|
scores = cross_val_score(clf, features, labels, cv=5)
|
||||||
|
print(scores)
|
||||||
|
print("%0.2f accuracy with a standard deviation of %0.2f" % (scores.mean(), scores.std()))
|
||||||
|
|
||||||
|
X_train, X_test, y_train, y_test = train_test_split(features, labels, random_state=0)
|
||||||
|
clf.fit(X_train, y_train)
|
||||||
|
cm = confusion_matrix(clf.predict(X_test), y_test)
|
||||||
|
print(cm)
|
||||||
|
print(classes)
|
||||||
|
disp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=classes)
|
||||||
|
disp.plot()
|
||||||
|
plt.show()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
csv_path = Path("/home/martin/code/ansible/roles/bluetooth-monitor/other/collected.csv")
|
||||||
|
df = load_measurements(csv_path)
|
||||||
|
features, labels = make_training_data(df, "martins_apple_watch")
|
||||||
|
print(np.unique(labels))
|
||||||
|
print(features.shape, labels.shape)
|
||||||
|
train(features, labels, list(df['real_room'].dtype.categories))
|
||||||
383
roles/bluetooth-monitor/other/bt_monitor_analyze.ipynb
Normal file
383
roles/bluetooth-monitor/other/bt_monitor_analyze.ipynb
Normal file
File diff suppressed because one or more lines are too long
309
roles/bluetooth-monitor/other/bt_monitor_server.py
Executable file
309
roles/bluetooth-monitor/other/bt_monitor_server.py
Executable file
@@ -0,0 +1,309 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import os
|
||||||
|
import aiomqtt
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
from time import time
|
||||||
|
from pathlib import Path
|
||||||
|
from collections import namedtuple, defaultdict, deque
|
||||||
|
from typing import Dict, Optional, List
|
||||||
|
from Crypto.Cipher import AES
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
import logging
|
||||||
|
from sklearn import svm
|
||||||
|
from sklearn.model_selection import cross_val_score
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
BtleMeasurement = namedtuple("BtleMeasurement", ["time", "tracker", "address", "rssi", "tx_power"])
|
||||||
|
BtleDeviceMeasurement = namedtuple("BtleDeviceMeasurement", ["time", "device", "tracker", "rssi", "tx_power"])
|
||||||
|
MqttInfo = namedtuple("MqttInfo", ["server", "username", "password"])
|
||||||
|
|
||||||
|
# ------------------------------------------------------- DECODING -------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class DeviceDecoder:
|
||||||
|
"""Decode bluetooth addresses - either simple ones (just address to name) or random changing ones like Apple devices using irk keys"""
|
||||||
|
|
||||||
|
def __init__(self, irk_to_devicename: Dict[str, str], address_to_name: Dict[str, str]):
|
||||||
|
"""
|
||||||
|
address_to_name: dictionary from bt address as string separated by ":" to a device name
|
||||||
|
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 decode(self, addr: str) -> Optional[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 DeviceDecoder._resolve_rpa(DeviceDecoder._addr_to_bytes(addr), irk):
|
||||||
|
return name
|
||||||
|
return self.address_to_name.get(addr, None)
|
||||||
|
|
||||||
|
def __call__(self, m: BtleMeasurement) -> Optional[BtleDeviceMeasurement]:
|
||||||
|
decoded_device_name = self.decode(m.address)
|
||||||
|
if not decoded_device_name:
|
||||||
|
return None
|
||||||
|
return BtleDeviceMeasurement(m.time, decoded_device_name, m.tracker, m.rssi, m.tx_power)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------- MACHINE LEARNING ----------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
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,known_room", file=csv_file)
|
||||||
|
|
||||||
|
def update_known_room(self, known_room: str):
|
||||||
|
if known_room != self.known_room:
|
||||||
|
logging.info(f"Updating known_room {self.known_room} -> {known_room}")
|
||||||
|
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
|
||||||
|
logging.info(f"Appending to training set: {m}")
|
||||||
|
print(
|
||||||
|
f"{m.time},{m.device},{m.tracker},{m.rssi},{m.tx_power},{self.known_room}",
|
||||||
|
file=self.csv_file_handle,)
|
||||||
|
|
||||||
|
|
||||||
|
class RunningFeatureVector:
|
||||||
|
FAR_AWAY_FEATURE_VALUE = 1
|
||||||
|
MIN_TIME_UNTIL_PREDICTION = 40 # wait until every reachable tracker detected the device
|
||||||
|
TIME_TO_DELETE_IF_NOT_SEEN = 30 # if device wasn't spotted for this time period, the measure is set to inf
|
||||||
|
|
||||||
|
def __init__(self, trackers: List[str]):
|
||||||
|
self.trackers = trackers
|
||||||
|
self.feature_vecs_per_device = defaultdict(lambda: [self.FAR_AWAY_FEATURE_VALUE] * len(trackers))
|
||||||
|
self.last_measurements = deque()
|
||||||
|
self.tracker_name_to_idx = {name: i for i, name in enumerate(trackers)}
|
||||||
|
self.start_time = None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_feature_value(rssi, tx_power):
|
||||||
|
"""Transforms rssi and tx power into a value between 0 and 1, where 0 is close and 1 is far away"""
|
||||||
|
MIN_RSSI = -90
|
||||||
|
MAX_TRANSFORMED_RSSI = 40
|
||||||
|
v = tx_power - rssi - MAX_TRANSFORMED_RSSI
|
||||||
|
if v < 0:
|
||||||
|
v = 0
|
||||||
|
return v / (-MIN_RSSI)
|
||||||
|
|
||||||
|
def add_measurement(self, new_measurement: BtleDeviceMeasurement):
|
||||||
|
if self.start_time is None:
|
||||||
|
self.start_time = new_measurement.time
|
||||||
|
|
||||||
|
self.last_measurements.append(new_measurement)
|
||||||
|
while len(self.last_measurements) > 0 and new_measurement.time - self.last_measurements[0].time > self.TIME_TO_DELETE_IF_NOT_SEEN:
|
||||||
|
self.last_measurements.popleft()
|
||||||
|
|
||||||
|
feature_vec = [self.FAR_AWAY_FEATURE_VALUE] * len(self.trackers)
|
||||||
|
for m in self.last_measurements:
|
||||||
|
if m.device == new_measurement.device:
|
||||||
|
tracker_idx = self.tracker_name_to_idx[m.tracker]
|
||||||
|
feature_vec[tracker_idx] = self._get_feature_value(m.rssi, m.tx_power)
|
||||||
|
return feature_vec if new_measurement.time - self.start_time > self.MIN_TIME_UNTIL_PREDICTION else None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def training_data_from_df(df: pd.DataFrame, device_to_train: str):
|
||||||
|
"""Returns a feature matrix (num_measurement, num_trackers) and a label vector (both numeric) to be used in scikit learn"""
|
||||||
|
trackers = list(df["tracker"].cat.categories)
|
||||||
|
idx_to_room = dict(enumerate(df["known_room"].cat.categories))
|
||||||
|
room_to_idx = {v: k for k, v in idx_to_room.items()}
|
||||||
|
|
||||||
|
last_known_room = None
|
||||||
|
|
||||||
|
features = []
|
||||||
|
labels = []
|
||||||
|
|
||||||
|
feature_accumulator = RunningFeatureVector(trackers)
|
||||||
|
|
||||||
|
# Feature vectors - rssi column for each room
|
||||||
|
for i, row in df.iterrows():
|
||||||
|
time, device, tracker, rssi, tx_power, known_room = row
|
||||||
|
m = BtleDeviceMeasurement(time, device, tracker, rssi, tx_power)
|
||||||
|
if device != device_to_train:
|
||||||
|
continue
|
||||||
|
if last_known_room != known_room:
|
||||||
|
feature_accumulator = RunningFeatureVector(trackers) # reset for new room
|
||||||
|
last_known_room = known_room
|
||||||
|
feature_vec = feature_accumulator.add_measurement(m)
|
||||||
|
if feature_vec is not None:
|
||||||
|
features.append(feature_vec)
|
||||||
|
labels.append(room_to_idx[known_room])
|
||||||
|
|
||||||
|
return np.array(features), np.array(labels)
|
||||||
|
|
||||||
|
|
||||||
|
def load_measurements_from_csv(csv_file: Path) -> pd.DataFrame:
|
||||||
|
"""Load csv with training data into dataframe"""
|
||||||
|
|
||||||
|
def cleanup_column_name(col_name: str):
|
||||||
|
return col_name.replace("#", "").strip()
|
||||||
|
|
||||||
|
df = pd.read_csv(str(csv_file))
|
||||||
|
|
||||||
|
# String cleanup in column names and room names
|
||||||
|
df = df.rename(columns=cleanup_column_name)
|
||||||
|
df.map(lambda x: x.strip() if isinstance(x, str) else x)
|
||||||
|
|
||||||
|
df["tracker"] = df["tracker"].astype("category")
|
||||||
|
df["known_room"] = df["known_room"].astype("category")
|
||||||
|
df['device'] = df['device'].astype("category")
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
async def send_discovery_messages(mqtt_client, device_names):
|
||||||
|
for device_name in device_names:
|
||||||
|
topic = f"homeassistant/sensor/my_btmonitor/{device_name}/config"
|
||||||
|
msg = {
|
||||||
|
"name": device_name,
|
||||||
|
"state_topic": f"my_btmonitor/ml/{device_name}",
|
||||||
|
"expire_after": 30,
|
||||||
|
"unique_id": device_name,
|
||||||
|
}
|
||||||
|
await mqtt_client.publish(topic, json.dumps(msg).encode(), retain=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def async_main(
|
||||||
|
mqtt_info: MqttInfo,
|
||||||
|
trackers: List[str],
|
||||||
|
devices: List[str],
|
||||||
|
classifier,
|
||||||
|
device_decoder: DeviceDecoder,
|
||||||
|
training_data_logger: KnownRoomCsvLogger,
|
||||||
|
):
|
||||||
|
current_rooms = defaultdict(lambda: "unknown")
|
||||||
|
feature_accumulator = RunningFeatureVector(trackers)
|
||||||
|
async with aiomqtt.Client(
|
||||||
|
hostname=mqtt_info.server, username=mqtt_info.username, password=mqtt_info.password
|
||||||
|
) as client:
|
||||||
|
await send_discovery_messages(client, devices)
|
||||||
|
await client.subscribe("my_btmonitor/#")
|
||||||
|
async for message in client.messages:
|
||||||
|
current_time = time()
|
||||||
|
topic = message.topic
|
||||||
|
if topic.value == "my_btmonitor/known_room":
|
||||||
|
training_data_logger.update_known_room(message.payload.decode())
|
||||||
|
else:
|
||||||
|
splitted_topic = message.topic.value.split("/")
|
||||||
|
if splitted_topic[0] == "my_btmonitor" and splitted_topic[1] == "raw_measurements":
|
||||||
|
msg_json = json.loads(message.payload)
|
||||||
|
measurement = BtleMeasurement(
|
||||||
|
time=current_time,
|
||||||
|
tracker=splitted_topic[2],
|
||||||
|
address=msg_json["address"],
|
||||||
|
rssi=msg_json["rssi"],
|
||||||
|
tx_power=msg_json.get("tx_power", 0),
|
||||||
|
)
|
||||||
|
logging.debug(f"Got Measurement {measurement}")
|
||||||
|
m = device_decoder(measurement)
|
||||||
|
if m is not None:
|
||||||
|
logging.info(f"Decoded Measurement {m}")
|
||||||
|
training_data_logger.report_measure(m)
|
||||||
|
feature_vec =feature_accumulator.add_measurement(m)
|
||||||
|
if feature_vec:
|
||||||
|
feature_str={tracker : value for tracker, value in zip(trackers, feature_vec)}
|
||||||
|
logging.info(f"Features: {feature_str}")
|
||||||
|
if feature_vec is not None and classifier is not None:
|
||||||
|
room = classifier(m.device, feature_vec)
|
||||||
|
if room != current_rooms[m.device]:
|
||||||
|
logging.info(f"{m.device} moved room {current_rooms[m.device]} to {room}")
|
||||||
|
current_rooms[m.device] = room
|
||||||
|
await client.publish(f"my_btmonitor/ml/{m.device}", room.encode())
|
||||||
|
|
||||||
|
async def async_main_with_restart(
|
||||||
|
mqtt_info: MqttInfo,
|
||||||
|
trackers: List[str],
|
||||||
|
devices: List[str],
|
||||||
|
classifier,
|
||||||
|
device_decoder: DeviceDecoder,
|
||||||
|
training_data_logger: KnownRoomCsvLogger,
|
||||||
|
):
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
await async_main(mqtt_info, trackers, devices, classifier, device_decoder, training_data_logger)
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
print("restarting...")
|
||||||
|
|
||||||
|
def get_classification_func(training_df: pd.DataFrame, log_classifier_scores=True):
|
||||||
|
devices_to_track = list(training_df["device"].unique())
|
||||||
|
classifiers = {}
|
||||||
|
rooms = list(training_df["known_room"].dtype.categories)
|
||||||
|
for device_to_track in devices_to_track:
|
||||||
|
features, labels = training_data_from_df(training_df, device_to_track)
|
||||||
|
clf = svm.SVC(kernel="rbf")
|
||||||
|
logging.info(f"Computing cross validation score for {device_to_track}")
|
||||||
|
if log_classifier_scores:
|
||||||
|
scores = cross_val_score(clf, features, labels, cv=5)
|
||||||
|
logging.info(" %0.2f accuracy with a standard deviation of %0.2f" % (scores.mean(), scores.std()))
|
||||||
|
|
||||||
|
logging.info(f"Training SVM classifier for {device_to_track}")
|
||||||
|
clf.fit(features, labels)
|
||||||
|
classifiers[device_to_track] = clf
|
||||||
|
|
||||||
|
def classify(device_name, feature_vec):
|
||||||
|
room_idx = classifiers[device_name].predict([feature_vec])[0]
|
||||||
|
return rooms[room_idx]
|
||||||
|
|
||||||
|
return classify
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
mqtt_info = MqttInfo(server="homeassistant.fritz.box", username="my_btmonitor", password="8aBIAC14jaKKbla")
|
||||||
|
# Dict with bt addresses as strings to device name
|
||||||
|
address_to_name = {}
|
||||||
|
# Devices with random addresses - need irk key
|
||||||
|
irk_to_devicename = {
|
||||||
|
"aa67542b82c0e05d65c27fb7e313aba5": "martins_apple_watch",
|
||||||
|
"840e3892644c1ebd1594a9069c14ce0d": "martins_iphone",
|
||||||
|
}
|
||||||
|
script_path = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
data_file = Path(script_path) / Path("training_data.csv")
|
||||||
|
training_df = load_measurements_from_csv(data_file)
|
||||||
|
classification_func = get_classification_func(training_df)
|
||||||
|
training_data_logger = KnownRoomCsvLogger(data_file)
|
||||||
|
device_decoder = DeviceDecoder(irk_to_devicename, address_to_name)
|
||||||
|
trackers = list(training_df["tracker"].cat.categories)
|
||||||
|
devices = list(training_df['device'].cat.categories)
|
||||||
|
asyncio.run(async_main_with_restart(mqtt_info, trackers, devices, classification_func, device_decoder, training_data_logger))
|
||||||
27855
roles/bluetooth-monitor/other/collected_backup.csv
Normal file
27855
roles/bluetooth-monitor/other/collected_backup.csv
Normal file
File diff suppressed because it is too large
Load Diff
7
roles/bluetooth-monitor/other/requirements.txt
Normal file
7
roles/bluetooth-monitor/other/requirements.txt
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
aiomqtt==2.0.0
|
||||||
|
numpy==1.26.4
|
||||||
|
pandas==2.2.1
|
||||||
|
pycryptodome==3.20.0
|
||||||
|
scikit-learn==1.4.1.post1
|
||||||
|
scipy==1.12.0
|
||||||
|
typing_extensions==4.10.0
|
||||||
27855
roles/bluetooth-monitor/other/training_data.csv
Normal file
27855
roles/bluetooth-monitor/other/training_data.csv
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,22 @@
|
|||||||
- name: Apt install bluez and firmware
|
- name: Apt install bluez, firmware and Python requirements
|
||||||
apt:
|
apt:
|
||||||
name:
|
name:
|
||||||
- bluez
|
- bluez
|
||||||
- bluez-firmware
|
- bluez-firmware
|
||||||
- firmware-realtek
|
- firmware-realtek
|
||||||
- firmware-realtek-rtl8723cs-bt
|
- firmware-realtek-rtl8723cs-bt
|
||||||
|
- python3-pycryptodome
|
||||||
|
- python3-bleak
|
||||||
|
- python3-asyncio-mqtt
|
||||||
|
- python3-numpy
|
||||||
|
- name: Copy monitor script
|
||||||
|
template: src=my_btmonitor.py dest=/usr/bin/my_btmonitor owner=root mode=u+rwx
|
||||||
|
- name: Install systemd service file
|
||||||
|
copy: src=my_btmonitor.service dest=/etc/systemd/system/
|
||||||
|
- name: Add script to autostart and start now
|
||||||
|
systemd: name=my_btmonitor state=restarted enabled=yes daemon_reload=yes
|
||||||
|
#- name: Add to sysdweb
|
||||||
|
# include_role:
|
||||||
|
# name: pi-sysdweb
|
||||||
|
# vars:
|
||||||
|
# sysdweb_name: my_btmonitor
|
||||||
97
roles/bluetooth-monitor/templates/my_btmonitor.py
Normal file
97
roles/bluetooth-monitor/templates/my_btmonitor.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import asyncio
|
||||||
|
from bleak import BleakScanner
|
||||||
|
from bleak.assigned_numbers import AdvertisementDataType
|
||||||
|
from bleak.backends.bluezdbus.advertisement_monitor import OrPattern
|
||||||
|
from bleak.backends.bluezdbus.scanner import BlueZScannerArgs
|
||||||
|
from functools import partial
|
||||||
|
import asyncio_mqtt
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
# ------------------- Config ----------------------------------------------------------------
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"mqtt": {
|
||||||
|
"hostname": "homeassistant.fritz.box",
|
||||||
|
"username": "{{my_btmonitor_mqtt_username}}",
|
||||||
|
"password": "{{my_btmonitor_mqtt_password}}",
|
||||||
|
"room": "{{sensor_room_name_ascii}}"
|
||||||
|
},
|
||||||
|
"watchdog_seconds": {{my_bt_monitor_watchdog_seconds | default(None)}},
|
||||||
|
"restart_ble_interface": {{my_btmonitor_restart_ble_interface | default(None)}},
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_event = asyncio.Event()
|
||||||
|
time_last_package_received = datetime.now()
|
||||||
|
|
||||||
|
|
||||||
|
async def on_device_found_callback(mqtt_client, room, device, advertising_data):
|
||||||
|
global time_last_package_received
|
||||||
|
time_last_package_received = datetime.now()
|
||||||
|
rssi = advertising_data.rssi
|
||||||
|
tx_power = advertising_data.tx_power
|
||||||
|
if tx_power is not None and rssi is not None:
|
||||||
|
topic = f"my_btmonitor/raw_measurements/{room}"
|
||||||
|
data = {"address": device.address,
|
||||||
|
"rssi": rssi,
|
||||||
|
"tx_power": tx_power}
|
||||||
|
try:
|
||||||
|
await mqtt_client.publish(topic, json.dumps(data).encode())
|
||||||
|
except Exception:
|
||||||
|
print("Probably mqtt isn't running - exit whole script and let systemd restart it")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
async def watchdog():
|
||||||
|
global time_last_package_received
|
||||||
|
timeout = config["watchdog_seconds"]
|
||||||
|
if not timeout or timeout <= 0:
|
||||||
|
return
|
||||||
|
while True:
|
||||||
|
restart = (datetime.now() - time_last_package_received).seconds > timeout
|
||||||
|
if restart:
|
||||||
|
stop_event.set()
|
||||||
|
await asyncio.sleep(60)
|
||||||
|
|
||||||
|
|
||||||
|
async def ble_scan():
|
||||||
|
mqtt_conf = config['mqtt']
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
async with asyncio_mqtt.Client(hostname=mqtt_conf["hostname"],
|
||||||
|
username=mqtt_conf["username"],
|
||||||
|
password=mqtt_conf['password']) as mqtt_client:
|
||||||
|
cb = partial(on_device_found_callback, mqtt_client, mqtt_conf['room'])
|
||||||
|
active_scan = True
|
||||||
|
if active_scan:
|
||||||
|
async with BleakScanner(cb) as scanner:
|
||||||
|
await stop_event.wait()
|
||||||
|
else:
|
||||||
|
# Doesn't work, because of the strange or_patters
|
||||||
|
args = BlueZScannerArgs(
|
||||||
|
or_patterns=[OrPattern(0, AdvertisementDataType.MANUFACTURER_SPECIFIC_DATA, b"\x00\x4c")]
|
||||||
|
)
|
||||||
|
async with BleakScanner(cb, bluez=args, scanning_mode="passive") as scanner:
|
||||||
|
await stop_event.wait()
|
||||||
|
except Exception as e:
|
||||||
|
print("Error", e)
|
||||||
|
print("Starting again")
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
await asyncio.gather(ble_scan(), watchdog())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
restart_interface = config["restart_ble_interface"]
|
||||||
|
if restart_interface:
|
||||||
|
print(f"Restarting {restart_interface}")
|
||||||
|
os.system(f"hciconfig {restart_interface} down")
|
||||||
|
time.sleep(3)
|
||||||
|
os.system(f"hciconfig {restart_interface} up")
|
||||||
|
time.sleep(3)
|
||||||
|
print("Done")
|
||||||
|
asyncio.run(main())
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
---
|
|
||||||
- name: Install packages
|
|
||||||
apt:
|
|
||||||
name:
|
|
||||||
- bat
|
|
||||||
- fish
|
|
||||||
- fzf
|
|
||||||
- fd-find
|
|
||||||
- ripgrep
|
|
||||||
- lsd
|
|
||||||
- zoxide
|
|
||||||
|
|
||||||
- name: Check if oh-my-fish is installed
|
|
||||||
stat:
|
|
||||||
path: '/etc/omf.installed'
|
|
||||||
register: omf
|
|
||||||
|
|
||||||
- name: Download omf installer
|
|
||||||
get_url:
|
|
||||||
url: https://raw.githubusercontent.com/oh-my-fish/oh-my-fish/master/bin/install
|
|
||||||
|
|
||||||
- name: Execute omf installer
|
|
||||||
shell: /usr/bin/fish /tmp/install --noninteractive
|
|
||||||
|
|
||||||
- name: Execute omf installer
|
|
||||||
shell: /usr/bin/fish -c omf install
|
|
||||||
|
|
||||||
- name: Remove the omf installer
|
|
||||||
file:
|
|
||||||
path: /tmp/install
|
|
||||||
state: absent
|
|
||||||
|
|
||||||
- name: Mark oh-my-fish installed with /etc/omf.installed
|
|
||||||
file:
|
|
||||||
path: /etc/omf.installed
|
|
||||||
state: touch
|
|
||||||
4
roles/pi-disable-onboard-bluetooth/handlers/main.yml
Normal file
4
roles/pi-disable-onboard-bluetooth/handlers/main.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
- name: reboot
|
||||||
|
reboot:
|
||||||
|
|
||||||
7
roles/pi-disable-onboard-bluetooth/tasks/main.yml
Normal file
7
roles/pi-disable-onboard-bluetooth/tasks/main.yml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
- name: Deactivate onboard bluetooth
|
||||||
|
lineinfile:
|
||||||
|
path: /boot/firmware/config.txt
|
||||||
|
regexp: "^#?dtoverlay=disable-bt"
|
||||||
|
line: "dtoverlay=disable-bt"
|
||||||
|
notify: reboot
|
||||||
Binary file not shown.
@@ -7,3 +7,11 @@ make arm_noccfgcc
|
|||||||
|
|
||||||
-> linker errors with libzip
|
-> linker errors with libzip
|
||||||
-> installed libzip-dev and added lzip to linker parameters
|
-> installed libzip-dev and added lzip to linker parameters
|
||||||
|
|
||||||
|
|
||||||
|
mkdir tmp
|
||||||
|
cd tmp
|
||||||
|
tar xf ../irserver-src.tar.gz
|
||||||
|
mkdir arm
|
||||||
|
# in makefile add: LDFLAGS = -lzip
|
||||||
|
make irserver_arm_noccf
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=IR server for IR remotes
|
Description=IR server for IR remotes
|
||||||
After=multi-user.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
|
|||||||
BIN
roles/pi-irserver/files/irserver_old
Executable file
BIN
roles/pi-irserver/files/irserver_old
Executable file
Binary file not shown.
@@ -1 +1,2 @@
|
|||||||
include "hauppauge.conf"
|
include "hauppauge.conf"
|
||||||
|
include "small_led_remote.conf"
|
||||||
350
roles/pi-lirc/files/small_led_remote.conf
Normal file
350
roles/pi-lirc/files/small_led_remote.conf
Normal file
@@ -0,0 +1,350 @@
|
|||||||
|
|
||||||
|
begin remote
|
||||||
|
|
||||||
|
name small_led_remote
|
||||||
|
flags RAW_CODES|CONST_LENGTH
|
||||||
|
eps 30
|
||||||
|
aeps 100
|
||||||
|
|
||||||
|
gap 108076
|
||||||
|
|
||||||
|
begin raw_codes
|
||||||
|
|
||||||
|
name ON
|
||||||
|
9257 4452 619 534 611 510
|
||||||
|
637 509 611 535 611 510
|
||||||
|
663 485 609 535 635 486
|
||||||
|
635 1637 640 1610 613 1636
|
||||||
|
641 1637 637 485 663 1611
|
||||||
|
615 1637 613 1637 666 1612
|
||||||
|
639 1612 615 537 608 509
|
||||||
|
661 486 609 535 636 485
|
||||||
|
637 510 609 535 611 510
|
||||||
|
662 1628 599 1636 640 1612
|
||||||
|
642 1637 639 1613 615 1637
|
||||||
|
642
|
||||||
|
|
||||||
|
name OFF
|
||||||
|
9068 4464 602 539 597 511
|
||||||
|
625 511 598 537 599 511
|
||||||
|
625 512 597 537 599 513
|
||||||
|
624 1643 599 1640 604 1640
|
||||||
|
630 1641 603 511 626 1642
|
||||||
|
603 1641 627 1618 630 511
|
||||||
|
599 1641 632 512 599 538
|
||||||
|
598 512 626 511 599 539
|
||||||
|
598 512 625 1641 603 511
|
||||||
|
626 1642 604 1641 603 1641
|
||||||
|
655 1617 603 1641 604 1640
|
||||||
|
629
|
||||||
|
|
||||||
|
name RED
|
||||||
|
9237 4461 602 547 597 519
|
||||||
|
625 521 597 545 600 518
|
||||||
|
626 520 598 547 596 521
|
||||||
|
624 1646 603 1646 602 1645
|
||||||
|
631 1647 601 524 621 1645
|
||||||
|
605 1645 604 1647 629 522
|
||||||
|
595 548 596 1645 604 550
|
||||||
|
594 525 620 520 597 548
|
||||||
|
596 523 622 1649 601 1647
|
||||||
|
602 547 599 1643 606 1644
|
||||||
|
631 1645 604 1645 604 1645
|
||||||
|
631
|
||||||
|
|
||||||
|
name GREEN
|
||||||
|
9103 4437 624 513 622 487
|
||||||
|
648 487 622 512 622 487
|
||||||
|
648 487 622 513 621 487
|
||||||
|
652 1616 621 1619 623 1617
|
||||||
|
651 1617 624 486 649 1618
|
||||||
|
624 1616 624 1617 651 1617
|
||||||
|
625 486 649 1618 624 486
|
||||||
|
649 486 622 512 623 486
|
||||||
|
648 486 622 512 623 1617
|
||||||
|
625 511 623 1618 623 1618
|
||||||
|
651 1617 624 1618 625 1617
|
||||||
|
651
|
||||||
|
|
||||||
|
|
||||||
|
name BLUE
|
||||||
|
9024 4501 609 524 613 494
|
||||||
|
638 498 610 522 611 498
|
||||||
|
634 501 610 523 613 496
|
||||||
|
635 1632 563 1680 560 1680
|
||||||
|
640 1629 609 499 675 1594
|
||||||
|
646 1596 620 1622 647 488
|
||||||
|
619 1621 647 1621 620 489
|
||||||
|
645 490 618 517 613 495
|
||||||
|
640 494 562 1679 589 547
|
||||||
|
562 572 564 1677 563 1679
|
||||||
|
589 1678 610 1632 608 1634
|
||||||
|
641
|
||||||
|
|
||||||
|
name WHITE
|
||||||
|
9083 4471 590 547 586 523
|
||||||
|
614 522 587 548 588 520
|
||||||
|
614 521 590 545 588 520
|
||||||
|
616 1650 591 1651 590 1652
|
||||||
|
618 1650 591 521 615 1649
|
||||||
|
592 1651 592 1649 618 1650
|
||||||
|
591 1650 591 1651 616 521
|
||||||
|
588 547 586 523 614 524
|
||||||
|
584 550 586 520 615 520
|
||||||
|
587 547 587 1652 589 1655
|
||||||
|
616 1650 589 1653 591 1655
|
||||||
|
615
|
||||||
|
|
||||||
|
name 1_ORANGE
|
||||||
|
9076 4445 562 581 600 513
|
||||||
|
618 515 595 530 554 553
|
||||||
|
623 514 596 532 600 509
|
||||||
|
581 1683 604 1637 601 1637
|
||||||
|
633 1636 604 502 584 1710
|
||||||
|
577 1638 555 1686 580 563
|
||||||
|
544 584 598 512 619 1638
|
||||||
|
556 563 571 563 543 592
|
||||||
|
588 510 623 1637 603 1636
|
||||||
|
556 1687 580 559 548 1682
|
||||||
|
584 1686 555 1684 556 1684
|
||||||
|
583
|
||||||
|
|
||||||
|
name 4_ORANGE
|
||||||
|
9158 4444 620 517 621 490
|
||||||
|
648 490 620 517 621 490
|
||||||
|
647 491 620 517 621 490
|
||||||
|
647 1624 620 1622 623 1622
|
||||||
|
651 1622 624 491 647 1622
|
||||||
|
623 1621 623 1622 650 491
|
||||||
|
620 518 620 1622 622 1623
|
||||||
|
648 493 619 519 619 493
|
||||||
|
621 518 593 1647 629 1644
|
||||||
|
596 517 621 516 593 1648
|
||||||
|
624 1646 598 1647 597 1647
|
||||||
|
625
|
||||||
|
|
||||||
|
name 7_YELLOW
|
||||||
|
9092 4468 593 543 592 518
|
||||||
|
619 517 592 544 593 515
|
||||||
|
621 518 592 543 592 518
|
||||||
|
620 1647 593 1650 596 1648
|
||||||
|
621 1647 596 517 619 1647
|
||||||
|
601 1642 595 1648 623 516
|
||||||
|
593 543 593 517 620 516
|
||||||
|
592 1649 621 518 591 544
|
||||||
|
594 516 619 1647 595 1662
|
||||||
|
582 1646 622 1647 596 517
|
||||||
|
618 1647 595 1648 595 1649
|
||||||
|
621
|
||||||
|
|
||||||
|
name STAR_YELLOW
|
||||||
|
9012 4468 589 544 587 518
|
||||||
|
613 518 588 543 586 519
|
||||||
|
615 516 587 544 586 522
|
||||||
|
608 1651 588 1650 588 1650
|
||||||
|
613 1650 588 520 612 1649
|
||||||
|
588 1649 588 1650 614 520
|
||||||
|
587 547 583 1649 588 543
|
||||||
|
589 1650 588 545 587 517
|
||||||
|
613 518 587 1649 614 1651
|
||||||
|
586 518 613 1650 589 517
|
||||||
|
611 1652 587 1650 588 1650
|
||||||
|
613
|
||||||
|
|
||||||
|
name 2_GREEN
|
||||||
|
9022 4497 569 566 561 546
|
||||||
|
587 546 561 573 561 547
|
||||||
|
587 547 561 571 562 546
|
||||||
|
589 1679 562 1677 564 1677
|
||||||
|
591 1678 564 546 588 1679
|
||||||
|
563 1678 564 1678 639 1631
|
||||||
|
565 546 588 546 561 1678
|
||||||
|
638 497 610 525 604 501
|
||||||
|
637 495 613 521 560 1679
|
||||||
|
563 1678 590 549 609 1626
|
||||||
|
589 1678 606 1636 561 1678
|
||||||
|
590
|
||||||
|
|
||||||
|
|
||||||
|
name 8_LIGHT_BLUE
|
||||||
|
9062 4473 589 548 587 521
|
||||||
|
615 520 586 548 588 519
|
||||||
|
614 521 586 547 587 520
|
||||||
|
615 1650 588 1653 589 1651
|
||||||
|
616 1651 588 522 612 1653
|
||||||
|
585 1655 536 1704 608 1659
|
||||||
|
537 572 561 572 533 600
|
||||||
|
546 1691 536 599 534 573
|
||||||
|
560 573 533 600 531 1706
|
||||||
|
581 1659 612 1654 586 522
|
||||||
|
609 1655 587 1653 585 1653
|
||||||
|
615
|
||||||
|
|
||||||
|
|
||||||
|
name 0_LIGHT_BLUE
|
||||||
|
9001 4491 564 569 563 543
|
||||||
|
589 542 563 569 563 543
|
||||||
|
589 542 563 569 563 542
|
||||||
|
589 1674 565 1674 565 1674
|
||||||
|
590 1674 564 543 588 1675
|
||||||
|
562 1676 535 1704 560 1703
|
||||||
|
535 572 560 1704 535 572
|
||||||
|
559 1703 535 574 557 572
|
||||||
|
533 599 533 580 552 1703
|
||||||
|
535 570 561 1703 535 572
|
||||||
|
560 1703 535 1703 535 1703
|
||||||
|
561
|
||||||
|
|
||||||
|
name 3_LIGHT_BLUE
|
||||||
|
9031 4460 596 536 596 510
|
||||||
|
622 510 595 537 594 510
|
||||||
|
622 510 618 514 593 511
|
||||||
|
621 1643 595 1643 596 1643
|
||||||
|
621 1643 595 519 612 1644
|
||||||
|
595 1643 595 1644 622 512
|
||||||
|
593 1645 620 514 591 1644
|
||||||
|
622 513 592 539 593 513
|
||||||
|
619 513 592 1644 621 513
|
||||||
|
592 1643 622 512 593 1643
|
||||||
|
622 1642 597 1642 597 1642
|
||||||
|
623
|
||||||
|
|
||||||
|
name 6_PURPLE
|
||||||
|
9015 4472 589 545 586 519
|
||||||
|
611 521 589 543 584 522
|
||||||
|
611 526 577 549 585 519
|
||||||
|
614 1650 588 1651 590 1649
|
||||||
|
614 1651 588 519 613 1649
|
||||||
|
590 1648 586 1654 613 521
|
||||||
|
584 1653 613 1652 586 1654
|
||||||
|
586 547 586 520 613 520
|
||||||
|
584 547 585 1652 588 545
|
||||||
|
585 521 616 516 586 1652
|
||||||
|
610 1655 586 1651 588 1651
|
||||||
|
614
|
||||||
|
|
||||||
|
name HASH_PINK
|
||||||
|
9048 4457 600 538 594 508
|
||||||
|
625 508 598 534 598 509
|
||||||
|
625 508 598 535 598 508
|
||||||
|
624 1638 600 1639 600 1640
|
||||||
|
627 1638 600 508 624 1641
|
||||||
|
599 1638 600 1639 626 509
|
||||||
|
597 1639 626 1640 600 509
|
||||||
|
623 1639 599 509 624 509
|
||||||
|
597 535 597 1639 600 535
|
||||||
|
597 509 623 1639 600 509
|
||||||
|
623 1639 600 1639 599 1640
|
||||||
|
626
|
||||||
|
|
||||||
|
name 5_BLUE
|
||||||
|
9065 4404 618 505 619 505
|
||||||
|
618 506 620 505 618 505
|
||||||
|
620 505 619 505 621 503
|
||||||
|
619 1623 620 1622 620 1621
|
||||||
|
620 1622 620 504 619 1623
|
||||||
|
620 1623 620 1623 617 1626
|
||||||
|
619 505 619 1623 620 1622
|
||||||
|
620 505 619 505 614 510
|
||||||
|
618 506 618 505 616 1624
|
||||||
|
619 504 620 504 619 1622
|
||||||
|
619 1623 618 1624 618 1623
|
||||||
|
617
|
||||||
|
name 9_PINK
|
||||||
|
9047 4402 616 507 618 504
|
||||||
|
619 504 618 505 618 505
|
||||||
|
615 508 618 504 619 504
|
||||||
|
618 1622 618 1622 619 1622
|
||||||
|
619 1624 618 504 618 1623
|
||||||
|
618 1623 619 1622 619 505
|
||||||
|
618 1623 619 504 618 505
|
||||||
|
620 1622 618 505 619 505
|
||||||
|
619 502 619 1622 619 506
|
||||||
|
619 1621 620 1623 618 504
|
||||||
|
619 1622 619 1622 617 1624
|
||||||
|
619
|
||||||
|
|
||||||
|
name FLASH
|
||||||
|
9066 4406 637 486 638 485
|
||||||
|
640 484 637 487 637 485
|
||||||
|
637 486 638 485 635 488
|
||||||
|
641 1602 640 1601 633 1608
|
||||||
|
634 1608 631 492 633 1609
|
||||||
|
633 1609 634 1607 631 1609
|
||||||
|
631 1610 630 493 632 1609
|
||||||
|
638 485 631 491 636 487
|
||||||
|
637 487 635 487 638 486
|
||||||
|
630 1611 638 484 632 1609
|
||||||
|
641 1599 637 1604 629 1612
|
||||||
|
638
|
||||||
|
|
||||||
|
name BRIGHTNESS_DOWN
|
||||||
|
9059 4403 619 504 619 505
|
||||||
|
619 506 618 505 618 505
|
||||||
|
619 504 618 505 619 504
|
||||||
|
620 1623 617 1624 618 1624
|
||||||
|
619 1622 619 505 619 1623
|
||||||
|
618 1623 619 1623 619 1623
|
||||||
|
619 505 619 505 619 505
|
||||||
|
617 507 619 503 619 504
|
||||||
|
619 506 616 507 616 1625
|
||||||
|
618 1624 619 1623 618 1624
|
||||||
|
618 1625 618 1623 619 1623
|
||||||
|
618
|
||||||
|
name BRIGHTNESS_UP
|
||||||
|
9055 4404 618 504 619 504
|
||||||
|
619 504 619 505 618 505
|
||||||
|
618 505 617 506 618 505
|
||||||
|
618 1623 619 1622 619 1624
|
||||||
|
618 1623 619 505 618 1622
|
||||||
|
619 1623 616 1625 619 505
|
||||||
|
618 504 619 505 619 504
|
||||||
|
616 507 618 505 619 504
|
||||||
|
618 505 618 1623 616 1627
|
||||||
|
615 1625 618 1623 619 1623
|
||||||
|
618 1624 618 1622 619 1622
|
||||||
|
618
|
||||||
|
name STROBE
|
||||||
|
9052 4403 618 504 619 504
|
||||||
|
618 506 615 507 618 504
|
||||||
|
619 504 619 504 619 504
|
||||||
|
619 1622 617 1624 619 1622
|
||||||
|
618 1623 619 504 617 1624
|
||||||
|
619 1622 619 1623 619 1622
|
||||||
|
619 1622 619 1622 618 1622
|
||||||
|
620 504 619 505 618 505
|
||||||
|
614 509 618 505 614 509
|
||||||
|
618 504 619 507 614 1625
|
||||||
|
619 1623 616 1625 619 1622
|
||||||
|
619
|
||||||
|
name FADE
|
||||||
|
9040 4403 616 504 618 504
|
||||||
|
620 503 618 504 618 505
|
||||||
|
617 505 616 504 618 504
|
||||||
|
618 1621 617 1624 614 1625
|
||||||
|
617 1621 618 504 614 1625
|
||||||
|
617 1622 618 1622 617 1623
|
||||||
|
617 1622 618 504 618 504
|
||||||
|
617 1623 617 504 615 507
|
||||||
|
616 505 618 503 618 504
|
||||||
|
618 1622 618 1622 617 505
|
||||||
|
618 1623 617 1623 617 1622
|
||||||
|
617
|
||||||
|
name SMOOTH
|
||||||
|
9040 4401 619 504 618 504
|
||||||
|
618 504 617 505 618 503
|
||||||
|
618 503 618 503 617 505
|
||||||
|
617 1623 616 1623 618 1623
|
||||||
|
616 1624 617 505 617 1622
|
||||||
|
617 1622 616 1624 618 1621
|
||||||
|
617 1623 614 1623 619 503
|
||||||
|
617 1622 618 504 618 504
|
||||||
|
617 504 618 504 617 505
|
||||||
|
617 505 616 1623 618 504
|
||||||
|
617 1623 616 1622 618 1621
|
||||||
|
618
|
||||||
|
|
||||||
|
|
||||||
|
end raw_codes
|
||||||
|
|
||||||
|
end remote
|
||||||
40
roles/pi-musicmouse/files/config.yml
Normal file
40
roles/pi-musicmouse/files/config.yml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
general:
|
||||||
|
alsa_device: softvol_effects
|
||||||
|
mqtt:
|
||||||
|
user: musicmouse
|
||||||
|
password: KNLEFLZF94yA6Zhj141
|
||||||
|
server: homeassistant
|
||||||
|
hass_url: https://ha.bauer.tech
|
||||||
|
hass_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiI4N2ExMzM2ZmQ4ZWQ0ZDgzOWZhMjU3NmZjYTg1NWQ1ZiIsImlhdCI6MTcwNDAyOTUyOSwiZXhwIjoyMDE5Mzg5NTI5fQ.vx9L5bTSey98uc8TwodShaEXSMr-hjXugPsNviR_fEw"
|
||||||
|
button_leds_brightness: 0.5
|
||||||
|
volume_increment: 5
|
||||||
|
min_volume: 23
|
||||||
|
max_volume: 70
|
||||||
|
serial_port: "/dev/ttyUSB0"
|
||||||
|
|
||||||
|
figures:
|
||||||
|
elefant:
|
||||||
|
id: "88041174e9"
|
||||||
|
colors: ["#ffff00", "#00c8ff", "#094b46", "#c20099"]
|
||||||
|
fuchs:
|
||||||
|
id: "8804ce7230"
|
||||||
|
colors: ["#F4D35E", "#F95738", "#F95738", "#083d77"]
|
||||||
|
eule:
|
||||||
|
id: "88040d71f0"
|
||||||
|
colors: ["#e5a200", "#f8e300", "w33", "w99"]
|
||||||
|
omnom:
|
||||||
|
id: "88043c6ede"
|
||||||
|
colors: ["#005102", "#fec800", "#005102", "#3bc405"]
|
||||||
|
eichhoernchen:
|
||||||
|
id: "88040b78ff"
|
||||||
|
colors: ["#ff0ada", "#4BC6B9", "#69045a", "#4BC6B9"]
|
||||||
|
hund:
|
||||||
|
id: "8804bc7444"
|
||||||
|
colors: ["#ffff00", "#00c8ff", "#094b46", "#c20099"]
|
||||||
|
hase:
|
||||||
|
id: "88044670ba"
|
||||||
|
colors: ["#ffff00", "#00c8ff", "#094b46", "#c20099"]
|
||||||
|
schneemann:
|
||||||
|
id: "88043f71c2"
|
||||||
|
colors: ["#ff0ada", "#4BC6B9", "#69045a", "#4BC6B9"]
|
||||||
|
|
||||||
11
roles/pi-musicmouse/files/musicmouse.service
Normal file
11
roles/pi-musicmouse/files/musicmouse.service
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=MusicMouse Player
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
Restart=always
|
||||||
|
ExecStart=/opt/musicmouse_venv/bin/python /opt/musicmouse/espmusicmouse/host_driver/main.py /media/musicmouse/
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
15
roles/pi-musicmouse/files/smb.conf
Normal file
15
roles/pi-musicmouse/files/smb.conf
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[global]
|
||||||
|
workgroup = WORKGROUP
|
||||||
|
logging = syslog@1
|
||||||
|
server role = standalone server
|
||||||
|
obey pam restrictions = no
|
||||||
|
unix password sync = no
|
||||||
|
|
||||||
|
|
||||||
|
[MusicMouse]
|
||||||
|
browseable = yes
|
||||||
|
path = /media/musicmouse
|
||||||
|
guest ok = no
|
||||||
|
writeable = yes
|
||||||
|
create mask = 666
|
||||||
|
force create mode = 666
|
||||||
38
roles/pi-musicmouse/tasks/main.yml
Normal file
38
roles/pi-musicmouse/tasks/main.yml
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
- name: Packages
|
||||||
|
apt:
|
||||||
|
name:
|
||||||
|
- python3
|
||||||
|
- python3-pip
|
||||||
|
- python3-vlc # also in venv - but this installs vlc + deps
|
||||||
|
- samba
|
||||||
|
- name: Checkout Musicmouse repo
|
||||||
|
ansible.builtin.git:
|
||||||
|
repo: 'ssh://git@git.bauer.tech:2222/martin/musicmouse.git'
|
||||||
|
dest: '/opt/musicmouse'
|
||||||
|
version: 'release/1.1'
|
||||||
|
accept_hostkey: true
|
||||||
|
- name: Create and update virtual env
|
||||||
|
ansible.builtin.pip:
|
||||||
|
requirements: /opt/musicmouse/espmusicmouse/host_driver/requirements.txt
|
||||||
|
virtualenv: /opt/musicmouse_venv
|
||||||
|
virtualenv_command: "/usr/bin/python3 -m venv"
|
||||||
|
- name: Create media directory
|
||||||
|
file:
|
||||||
|
path: /media/musicmouse
|
||||||
|
state: directory
|
||||||
|
- name: Install config file
|
||||||
|
copy: src=config.yml dest=/media/musicmouse/config.yml
|
||||||
|
- name: Install systemd service file
|
||||||
|
copy: src=musicmouse.service dest=/etc/systemd/system/
|
||||||
|
- name: Add script to autostart and start now
|
||||||
|
systemd: name=musicmouse state=restarted enabled=yes daemon_reload=yes
|
||||||
|
- name: Samba setup
|
||||||
|
copy: src=smb.conf dest=/etc/samba/
|
||||||
|
- name: Restart samba
|
||||||
|
systemd: name=smbd state=restarted enabled=yes
|
||||||
|
# manual steps:
|
||||||
|
# - set samba passwords with smbpasswd
|
||||||
|
# - copy music into /media/musicmouse (or via samba share)
|
||||||
|
# - upload host driver?
|
||||||
|
# - manual patching of hassclient necessary :( loop has to be passed in, to not use running_event_loop
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
- libflac-dev
|
- libflac-dev
|
||||||
- libfaad2
|
- libfaad2
|
||||||
- libmad0
|
- libmad0
|
||||||
- perl-openssl-abi-1.1
|
- perl-openssl-defaults
|
||||||
- libnet-ssleay-perl
|
- libnet-ssleay-perl
|
||||||
- libio-socket-ssl-perl
|
- libio-socket-ssl-perl
|
||||||
- nasm
|
- nasm
|
||||||
|
|||||||
@@ -6,4 +6,5 @@
|
|||||||
- server-exthdd-mount
|
- server-exthdd-mount
|
||||||
- server-nfs
|
- server-nfs
|
||||||
- server-link-aggregation
|
- server-link-aggregation
|
||||||
|
- bluetooth-monitor
|
||||||
|
|
||||||
|
|||||||
10
todo.md
10
todo.md
@@ -23,3 +23,13 @@ dpkg-buildpackage -b -uc
|
|||||||
[General]
|
[General]
|
||||||
Discoverable=false
|
Discoverable=false
|
||||||
Alias=bla
|
Alias=bla
|
||||||
|
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
- fish
|
||||||
|
- apt install fish
|
||||||
|
- oh my fish
|
||||||
|
- fish config
|
||||||
|
- ripgrep (apt install ripgrep)
|
||||||
|
- fd (apt install fd-find)
|
||||||
|
- apt install neovim tmux fd-find ripgrep lsd broot fzf tmux glances
|
||||||
33
working.yml
Normal file
33
working.yml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
- hosts: server
|
||||||
|
roles:
|
||||||
|
- bluetooth-monitor
|
||||||
|
|
||||||
|
- hosts: musikserverwohnzimmeroben
|
||||||
|
roles:
|
||||||
|
- bluetooth-monitor
|
||||||
|
|
||||||
|
- hosts: kitchenpi
|
||||||
|
roles:
|
||||||
|
- pi-disable-onboard-bluetooth
|
||||||
|
- bluetooth-monitor
|
||||||
|
|
||||||
|
- hosts: bedroompi
|
||||||
|
roles:
|
||||||
|
- pi-disable-onboard-bluetooth
|
||||||
|
- bluetooth-monitor
|
||||||
|
|
||||||
|
- hosts: musicmouse
|
||||||
|
roles:
|
||||||
|
- pi-standard-setup
|
||||||
|
- pi-hifiberry-amp
|
||||||
|
- pi-musicmouse
|
||||||
|
- pi-squeezelite-custom
|
||||||
|
- pi-shairport
|
||||||
|
- pi-lirc
|
||||||
|
- bluetooth-monitor
|
||||||
|
|
||||||
|
- hosts: homeassistant
|
||||||
|
roles:
|
||||||
|
- bluetooth-monitor
|
||||||
Reference in New Issue
Block a user