Compare commits
No commits in common. "9092f084818152cfa178c634762206687aa238bb" and "fb6f10891d02dc33912dbde84e0d6b7d3b4cc647" have entirely different histories.
9092f08481
...
fb6f10891d
|
@ -5,4 +5,4 @@ ve_*
|
||||||
/music
|
/music
|
||||||
__pycache__
|
__pycache__
|
||||||
/roles/pi-squeezeserver/backup
|
/roles/pi-squeezeserver/backup
|
||||||
venv*
|
venv
|
|
@ -1,8 +1,5 @@
|
||||||
{
|
{
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
}
|
12
full.yml
12
full.yml
|
@ -41,21 +41,13 @@
|
||||||
- pi-disable-onboard-bluetooth
|
- pi-disable-onboard-bluetooth
|
||||||
- bluetooth-monitor
|
- bluetooth-monitor
|
||||||
|
|
||||||
- hosts: musicmouse
|
|
||||||
roles:
|
|
||||||
- pi-standard-setup
|
|
||||||
- pi-hifiberry-amp
|
|
||||||
- pi-musicmouse
|
|
||||||
- pi-squeezelite-custom
|
|
||||||
- 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,16 +12,12 @@ 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
|
||||||
|
@ -29,7 +25,6 @@ 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
|
||||||
|
@ -37,7 +32,6 @@ 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
|
||||||
|
@ -46,15 +40,12 @@ 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
|
my_btmonitor_pl0: 68 # default is 73 - increase to make more sensitve (i.e. lower distances)
|
||||||
musicmouse:
|
musicmouse:
|
||||||
squeezelite_name: MusicMouse
|
squeezelite_name: MusicMouse
|
||||||
shairport_name: MusicMouse
|
shairport_name: MusicMouse
|
||||||
alsa_card_name: 1
|
alsa_card_name: 0
|
||||||
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
|
||||||
|
@ -65,12 +56,7 @@ all:
|
||||||
server:
|
server:
|
||||||
sensor_room_name: Arbeitszimmer
|
sensor_room_name: Arbeitszimmer
|
||||||
sensor_room_name_ascii: arbeitszimmer
|
sensor_room_name_ascii: arbeitszimmer
|
||||||
my_bt_monitor_watchdog_seconds: 600
|
my_btmonitor_pl0: 78
|
||||||
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
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
function dot -w git -d "Manages dotfiles"
|
|
||||||
git --git-dir=$HOME/.dot --work-tree=$HOME $argv
|
|
||||||
end
|
|
|
@ -1,47 +0,0 @@
|
||||||
---
|
|
||||||
#
|
|
||||||
- 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,11 +0,0 @@
|
||||||
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" ]
|
|
|
@ -1,95 +0,0 @@
|
||||||
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))
|
|
File diff suppressed because one or more lines are too long
|
@ -1,309 +0,0 @@
|
||||||
#!/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))
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +0,0 @@
|
||||||
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
|
|
File diff suppressed because it is too large
Load Diff
|
@ -4,12 +4,13 @@ from bleak import BleakScanner
|
||||||
from bleak.assigned_numbers import AdvertisementDataType
|
from bleak.assigned_numbers import AdvertisementDataType
|
||||||
from bleak.backends.bluezdbus.advertisement_monitor import OrPattern
|
from bleak.backends.bluezdbus.advertisement_monitor import OrPattern
|
||||||
from bleak.backends.bluezdbus.scanner import BlueZScannerArgs
|
from bleak.backends.bluezdbus.scanner import BlueZScannerArgs
|
||||||
|
from Cryptodome.Cipher import AES
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import asyncio_mqtt
|
import asyncio_mqtt
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from typing import Dict
|
||||||
import os
|
import collections
|
||||||
import time
|
import numpy as np
|
||||||
|
|
||||||
# ------------------- Config ----------------------------------------------------------------
|
# ------------------- Config ----------------------------------------------------------------
|
||||||
|
|
||||||
|
@ -20,51 +21,116 @@ config = {
|
||||||
"password": "{{my_btmonitor_mqtt_password}}",
|
"password": "{{my_btmonitor_mqtt_password}}",
|
||||||
"room": "{{sensor_room_name_ascii}}"
|
"room": "{{sensor_room_name_ascii}}"
|
||||||
},
|
},
|
||||||
"watchdog_seconds": {{my_bt_monitor_watchdog_seconds | default(None)}},
|
"irk_to_devicename": {
|
||||||
"restart_ble_interface": {{my_btmonitor_restart_ble_interface | default(None)}},
|
"aa67542b82c0e05d65c27fb7e313aba5": "martins_apple_watch",
|
||||||
|
"840e3892644c1ebd1594a9069c14ce0d" : "martins_iphone",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stop_event = asyncio.Event()
|
# --------------------------------------------------------------------------------------------
|
||||||
time_last_package_received = datetime.now()
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
|
||||||
async def on_device_found_callback(mqtt_client, room, device, advertising_data):
|
def addr_to_bytes(addr:str) -> bytes:
|
||||||
global time_last_package_received
|
"""Converts a bluetooth mac address string with semicolons to bytes"""
|
||||||
time_last_package_received = datetime.now()
|
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=73):
|
||||||
|
"""
|
||||||
|
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 = 3.5
|
||||||
|
return 10**((tx_power - rssi - pl0) / (10 * n))
|
||||||
|
|
||||||
|
|
||||||
|
def smooth(y, box_pts):
|
||||||
|
box = np.ones(box_pts)/box_pts
|
||||||
|
y_smooth = np.convolve(y, box, mode='same')
|
||||||
|
return y_smooth
|
||||||
|
|
||||||
|
WINDOW_SIZE = 6
|
||||||
|
last_values = collections.deque(maxlen=WINDOW_SIZE)
|
||||||
|
def filter_distance(dist: float):
|
||||||
|
global last_values
|
||||||
|
last_values.append(dist)
|
||||||
|
return smooth(np.array(last_values), 12)[-1]
|
||||||
|
|
||||||
|
|
||||||
|
async def on_device_found_callback(irks, mqtt_client, room, device, advertising_data):
|
||||||
|
decoded_device_id = decode_address(device.address, irks)
|
||||||
rssi = advertising_data.rssi
|
rssi = advertising_data.rssi
|
||||||
tx_power = advertising_data.tx_power
|
tx_power = advertising_data.tx_power
|
||||||
if tx_power is not None and rssi is not None:
|
if decoded_device_id and tx_power is not None and rssi is not None:
|
||||||
topic = f"my_btmonitor/raw_measurements/{room}"
|
topic = f"my_btmonitor/devices/{decoded_device_id}/{room}"
|
||||||
data = {"address": device.address,
|
distance = estimate_distance(rssi, tx_power, {{my_btmonitor_pl0 | default('73')}} )
|
||||||
|
filtered_distance = filter_distance(distance)
|
||||||
|
data = {"id": decoded_device_id,
|
||||||
|
"name": decoded_device_id,
|
||||||
"rssi": rssi,
|
"rssi": rssi,
|
||||||
"tx_power": tx_power}
|
"tx_power": tx_power,
|
||||||
try:
|
"distance": filtered_distance,
|
||||||
|
"unfiltered_distance": distance,
|
||||||
|
}
|
||||||
await mqtt_client.publish(topic, json.dumps(data).encode())
|
await mqtt_client.publish(topic, json.dumps(data).encode())
|
||||||
except Exception:
|
#print(data)
|
||||||
print("Probably mqtt isn't running - exit whole script and let systemd restart it")
|
|
||||||
exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
async def watchdog():
|
async def main():
|
||||||
global time_last_package_received
|
stop_event = asyncio.Event()
|
||||||
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']
|
mqtt_conf = config['mqtt']
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
async with asyncio_mqtt.Client(hostname=mqtt_conf["hostname"],
|
async with asyncio_mqtt.Client(hostname=mqtt_conf["hostname"],
|
||||||
username=mqtt_conf["username"],
|
username=mqtt_conf["username"],
|
||||||
password=mqtt_conf['password']) as mqtt_client:
|
password=mqtt_conf['password']) as mqtt_client:
|
||||||
cb = partial(on_device_found_callback, mqtt_client, mqtt_conf['room'])
|
cb = partial(on_device_found_callback, config['irk_to_devicename'], mqtt_client, mqtt_conf['room'])
|
||||||
active_scan = True
|
active_scan = True
|
||||||
if active_scan:
|
if active_scan:
|
||||||
async with BleakScanner(cb) as scanner:
|
async with BleakScanner(cb) as scanner:
|
||||||
|
@ -80,18 +146,4 @@ async def ble_scan():
|
||||||
print("Error", e)
|
print("Error", e)
|
||||||
print("Starting again")
|
print("Starting again")
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
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())
|
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
---
|
||||||
|
- 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
|
|
@ -1,40 +0,0 @@
|
||||||
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"]
|
|
||||||
|
|
|
@ -1,11 +0,0 @@
|
||||||
[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
|
|
|
@ -1,15 +0,0 @@
|
||||||
[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
|
|
|
@ -1,38 +0,0 @@
|
||||||
---
|
|
||||||
- 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
|
|
10
todo.md
10
todo.md
|
@ -23,13 +23,3 @@ 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
|
|
13
working.yml
13
working.yml
|
@ -18,16 +18,3 @@
|
||||||
- pi-disable-onboard-bluetooth
|
- pi-disable-onboard-bluetooth
|
||||||
- bluetooth-monitor
|
- 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
|
|
Loading…
Reference in New Issue