đ Handbuch
Dokumentation fĂŒr BLE Presence v1.0
Voraussetzungen
Master (Raspberry Pi)
- Raspberry Pi Zero 2W / 3/4/5
- DietPi OS (Trixie)
- LAN/WLAN-Zugang
Scanner
- ESP32-WROOM, ESP32-S3
- Stromversorgung via USB oder Lötpins
- WLAN-Zugang
Zu trackende GerÀte
Jedes BLE-GerĂ€t, das regelmĂ€Ăig Advertisements sendet â z.B. BLE-Tags, Shelly-GerĂ€te, iBeacons. Protokolle: BLE/Eddystone/BTHome/IRK.
Button-Events funktionieren aktuell nur mit BTHome Devices, z.B. Shelly BLU Button.
Aufgrund der wechselnden MAC-Adresse und properitÀren Protokollen eignen sich keine Apple AirTags und AirTag-kompatible GerÀte, oder Samsung Tags.
iPhone / Android
Ăber das auslesen des Identity Resolving Key (IRK) können Telefone mit random MAC-Adrersse auch getrackt werden.
Hierzu ist zu sagen das die meisten aktuellen Telefone aggressive Energiespar-Modi nutzen und bei ausgeschaltetem Display oft keine Signale gesndet werden.
Empfehlung: Shelly BLU Button, Feasycom Eddystone Devices.
Installation
1. DietPi installieren
In den folgenden Links steht, wie man DietPi installiert:
Die nachfolgenden Dateien helfen dir, DietPi schnell und einfach fĂŒr den ersten Start zu konfigurieren:
dietpi.txt â Hier ist soweit alles vorkonfiguriert fĂŒr den normalen Gebrauch.
dietpi-wifi.txt â Hier mĂŒsst ihr, wenn ihr WLAN nutzen wollt, eure SSID (WLAN-Name) und WLAN-Passwort eintragen. GroĂ-/Kleinschreibung beachten!
Beide Dateien liegen auf der SD-Karte im Boot-Verzeichnis, auf das ihr ĂŒber z.B. den Windows Explorer zugreifen könnt.
2. DietPi erster Start
Nach dem Einstecken braucht der Pi ein paar Minuten, bis er per SSH erreichbar ist.
Um uns mit dem Pi zu verbinden, nutzen wir SSH. Ich empfehle PuTTY, das es HIER als Portable-Version gibt.
In PuTTY tragen wir den Hostname oder die IP ein und drĂŒcken âOpen".
FĂŒr den ersten Login nutzt ihr:
login as: root
root@master's password: dietpi
DietPi fÀngt mit der Grundinstallation an.
Ihr werdet aufgefordert, die Passwörter fĂŒr die User root und dietpi zu Ă€ndern. Nutzt sichere Passwörter, auch wenn alles nur im internen Netz lĂ€uft.
Die serial/UART-Konsole könnt ihr abgeschaltet lassen. âNO" wĂ€hlen.
Da wir ĂŒber die dietpi.txt schon alles vorkonfiguriert haben, kannst du hier ohne Ănderung auf âInstall" und âSelect" gehen.
Kurz noch bestÀtigen und es wird alles eingerichtet. Das kann je nach Hardware ein paar Minuten dauern.
Wenn ihr weder den Hostname noch die IP kennt, nutzt Angry IP Scanner Portable
3. Master installieren
Einfach die untere Zeile kopieren und einfĂŒgen, das war es schon:
cd / && rm -rf install.* && wget http://ble-presence.de/download/install.sh && bash install.sh | tee /install.log
Nach dem Reboot ist die Web-OberflÀche mit admin/admin unter eurer IP erreichbar.
NĂ€chste Schritte
- Web-Interface öffnen:
http://[raspberry-ip] - Master Konfiguration durchfĂŒhren / MQTT konfigurieren
- ESP32 einrichten (min. 1 Scanner)
- Unter âGerĂ€te" â âStart Distributed Scan" einen ersten Scan starten
- Erkannte BLE-GerÀte benennen und per Alias zuordnen
Wichtig ist als erstes der MQTT Broker, ohne den geht fast nichts.
Alles andere könnt ihr auch spÀter noch einrichten.
GerÀte-Katalog
Hier findest du eine Ăbersicht kompatibler BLE-GerĂ€te, die mit BLE Presence getestet wurden. Die Tabelle wird laufend erweitert.
| Hersteller | Bezeichnung | Formfaktor | Protokolle | UUID konfig. | Button | Batterie | Lebensdauer | Bewertung | Bezugsquelle | Preis ca. |
|---|---|---|---|---|---|---|---|---|---|---|
| Shelly | BLU Button | Button | BTHome | â | â | â CR2032 | ~1 Jahr | âââââ | shelly.com | ~10 ⏠|
| Shelly | BLU Door/Window | Sensor | BTHome | â | â | â CR2032 | ~1 Jahr | ââââ | shelly.com | ~15 ⏠|
| Feasycom | FSC-BP108 | Tag | Eddystone / iBeacon | â | â | â CR2477 | ~2â3 Jahre | âââââ | feasycom.com | ~8 ⏠|
| Feasycom | FSC-BP104 | Tag | Eddystone / iBeacon | â | â | â CR2032 | ~1 Jahr | ââââ | feasycom.com | ~5 ⏠|
| Android Handy | Beacon Scope App | Smartphone | iBeacon / Eddystone / AltBeacon | â | â | â | â | âââ | Google Play | Kostenlos |
Feasycom-Tags werden ab Werk mit einer Standard-UUID ausgeliefert. Diese muss vor der Nutzung ĂŒber die Feasycom-App geĂ€ndert werden, da sonst alle Tags die gleiche Kennung senden und nicht unterscheidbar sind.
GerÀte mit konfigurierbarer UUID sind ideal, da sie eine feste Kennung senden. GerÀte mit wechselnder MAC-Adresse (z.B. AirTags) funktionieren nicht mit BLE Presence.
Eddystone / UUID
Eddystone ist ein von Google entwickeltes BLE-Beacon-Protokoll. BLE Presence unterstĂŒtzt Eddystone-UID, das eine feste Kennung bestehend aus Namespace (10 Bytes) und Instance (6 Bytes) sendet.
Aufbau einer Eddystone-UID
| Feld | LĂ€nge | Beschreibung |
|---|---|---|
Namespace |
10 Bytes | Identifiziert die Gruppe/das System (z.B. alle Tags einer Wohnung) |
Instance |
6 Bytes | Identifiziert das einzelne GerÀt innerhalb des Namespace |
Konfiguration am Tag
- Tag per Hersteller-App verbinden
- Eddystone-UID-Frame aktivieren
- Namespace einheitlich fĂŒr alle Tags setzen (z.B.
0x00112233445566778899) - Instance pro Tag eindeutig vergeben (z.B.
0x000000000001,0x000000000002, âŠ) - Sende-Intervall einstellen (empfohlen: 500â1000 ms)
Ein kĂŒrzeres Sende-Intervall verbessert die Erkennung, reduziert aber die Batterielebensdauer. 1000 ms ist ein guter Kompromiss.
iBeacon UUID
iBeacon ist Apples BLE-Beacon-Protokoll. Es verwendet eine dreiteilige Kennung aus UUID, Major und Minor.
Aufbau einer iBeacon-Kennung
| Feld | LĂ€nge | Beschreibung |
|---|---|---|
UUID |
16 Bytes | Identifiziert die Anwendung/das System |
Major |
2 Bytes | Gruppe, z.B. Etage oder Bereich (0â65535) |
Minor |
2 Bytes | Einzelnes GerĂ€t innerhalb der Gruppe (0â65535) |
Konfiguration am Tag
- Tag per App verbinden
- iBeacon-Frame aktivieren
- UUID fĂŒr alle Tags gleich setzen (z.B.
12345678-1234-1234-1234-123456789ABC) - Major z.B. pro Etage vergeben (
1= EG,2= OG) - Minor pro GerÀt eindeutig vergeben (
1,2,3, âŠ) - Sende-Intervall einstellen (empfohlen: 500â1000 ms)
Die UUID/Namespace muss in BLE Presence nicht separat hinterlegt werden â das System erkennt die GerĂ€te automatisch anhand ihrer Advertisement-Daten beim Discovery-Scan.
Android App
FĂŒr die Einrichtung und Konfiguration von BLE-Tags wird die jeweilige Hersteller-App benötigt.
Hersteller-Apps
| Hersteller | App | Funktionen |
|---|---|---|
| Feasycom | Feasycom Tool | UUID/Namespace konfigurieren, Sende-Intervall, TX Power, Protokoll wÀhlen |
| Shelly | Shelly Smart Control | Button-Aktionen konfigurieren, Firmware-Updates |
UUID konfigurieren
Bei GerĂ€ten mit konfigurierbarer UUID (z.B. Feasycom-Tags) wird die UUID ĂŒber die Hersteller-App geĂ€ndert:
- Hersteller-App installieren und Bluetooth aktivieren
- Tag in der App suchen und verbinden
- UUID, Major/Minor (iBeacon) oder Namespace/Instance (Eddystone) setzen
- Ănderungen speichern â das GerĂ€t sendet ab sofort mit der neuen Kennung
Feasycom-Tags mĂŒssen vor der Nutzung unbedingt per Feasycom-App mit einer eigenen UUID konfiguriert werden. Ab Werk senden alle Tags die gleiche Standard-UUID!
Android Handy als Beacon
Mit der App Beacon Scope kann jedes Android-Handy (ab Android 5.0) selbst als BLE-Beacon genutzt werden. Das Handy simuliert dann ein iBeacon-, Eddystone- oder AltBeacon-Signal und wird von BLE Presence wie ein normaler Tag erkannt.
- Beacon Scope aus dem Google Play Store installieren
- App öffnen und unter Transmit einen neuen Beacon erstellen
- Protokoll wÀhlen (iBeacon, Eddystone oder AltBeacon)
- UUID/Namespace konfigurieren und Beacon starten
- Die App sendet auch im Hintergrund, bis man den Beacon manuell stoppt
Beacon Scope kann auch als BLE-Scanner genutzt werden, um alle Beacons in der NĂ€he anzuzeigen â praktisch zum Testen und PrĂŒfen der SignalstĂ€rke (RSSI) vor der Einrichtung.
Ein Handy als Beacon verbraucht deutlich mehr Akku als dedizierte Tags. AuĂerdem wird kein Batterie-Level an BLE Presence gemeldet. FĂŒr dauerhaften Einsatz sind echte BLE-Tags die bessere Wahl.
Apple
Content...
Lizenz
BLE Presence verwendet ein Lizenzsystem auf Basis von Ed25519-signierten SchlĂŒsseln. Jeder Lizenz-Key ist kryptografisch an eine bestimmte Hardware-ID gebunden und kann nicht auf andere GerĂ€te ĂŒbertragen werden.
Sinn der Lizenz ist interne VerschlĂŒsselungen zu generieren um das System jetzt und vor allem in noch kommenden Versionen abzusichern.
Lizenztypen
| Typ | GĂŒltigkeit | Beschreibung |
|---|---|---|
| FULL | Unbegrenzt | Vollversion nach Kauf. Alle Features dauerhaft freigeschaltet. |
| DEV | Unbegrenzt | Entwickler-Lizenz, wird nur intern vergeben. |
Das System lÀuft weiterhin, aber die MQTT- und Home-Assistant-Integration ist deaktiviert. Lokale BLE-Scans funktionieren, die Daten werden jedoch nicht an externe Systeme weitergeleitet.
Hardware-ID
Bei der Installation wird automatisch eine eindeutige Hardware-ID fĂŒr den Raspberry Pi erzeugt. Sie hat das Format:
BLE-XXXX-XXXX
Die Hardware-ID findest du an folgenden Stellen:
- Im Web-Interface unter
Konfiguration â Master â Lizenz - Auf der Kommandozeile:
cat /var/www/html/ble/data/hwid.txt
Die ID basiert auf den Hardware-Eigenschaften des Raspberry Pi und bleibt bei Neuinstallation gleich, solange dieselbe Hardware verwendet wird.
Die Hardware-ID kann im Web-Interface per Klick in die Zwischenablage kopiert werden. Du brauchst sie fĂŒr die Trial-Anforderung und den Lizenzkauf.
Lizenz erstellen
- Ăffne den Shop: ble-presence.de/shop.php
- Trage deine Hardware-ID und E-Mail-Adresse ein
- Der Key wird direkt auf der Seite angezeigt und kann kopiert oder als Datei heruntergeladen werden
- ZusÀtzlich erhÀltst du den Key per E-Mail
Der Key ist eine lange Zeichenkette. Nutze am besten die Kopier- oder Download-Funktion auf der Erfolgsseite, um Tippfehler zu vermeiden.
Lizenz aktivieren
Nach Erhalt des Lizenz-Keys wird dieser im Web-Interface des Masters aktiviert:
- Ăffne das Web-Interface:
http://[raspberry-ip] - Gehe zu
Konfiguration â Master â Lizenz - Im Bereich âLizenz" siehst du deine Hardware-ID und den aktuellen Status
- FĂŒge den Lizenz-Key in das Eingabefeld ein
- Klicke auf âAktivieren"
Nach erfolgreicher Aktivierung wechselt der Status und die MQTT-Integration wird sofort freigeschaltet. Ein Neustart der Services ist nicht nötig.
Der Lizenz-Key ist an die angezeigte Hardware-ID gebunden. Ein Key, der fĂŒr eine andere Hardware-ID erzeugt wurde, wird abgelehnt.
Lizenzstatus
Der aktuelle Lizenzstatus wird im Web-Interface unter Konfiguration â Lizenz als farbiger Badge angezeigt:
| Badge | Bedeutung |
|---|---|
| đą LIZENZIERT | Vollversion aktiv, alle Features freigeschaltet |
| đ” DEVELOPER | Entwickler-Lizenz, unbegrenzt gĂŒltig |
| â« KEINE LIZENZ | Kein Key eingegeben â MQTT deaktiviert |
Lizenz deaktivieren
Falls du den Key entfernen möchtest (z.B. bei Hardware-Wechsel), kannst du ĂŒber den âDeaktivieren"-Button im Lizenz-Bereich die aktive Lizenz löschen. Die MQTT-Integration wird daraufhin wieder gesperrt.
Troubleshooting Lizenz
| Problem | Lösung |
|---|---|
| Key wird abgelehnt | Hardware-ID prĂŒfen â stimmt sie mit dem Key ĂŒberein? Key erneut kopieren (keine Leerzeichen, vollstĂ€ndig). |
| MQTT trotz Lizenz inaktiv | PrĂŒfe ob der MQTT-Broker konfiguriert ist: Konfiguration â MQTT. Lizenzstatus in den Logs prĂŒfen: sudo journalctl -u ble_aggregator -n 50 |
| Hardware-ID geÀndert | Die ID Àndert sich nur bei einem Wechsel der Hardware. In diesem Fall einen neuen Key anfordern. |
Web-Konfiguration Master
Die gesamte Konfiguration erfolgt ĂŒber Konfiguration > Master im Web-Interface. Ănderungen werden in INI-Dateien gespeichert und die entsprechenden Services automatisch neu gestartet.
Konfigurationsgruppen
| Gruppe | Beschreibung |
|---|---|
| âïž Main | MQTT-Broker, Topics, Home Assistant |
| đïž Aggregator | Timeouts, RSSI-Filter, Button-Events, Statistik, Tracking |
| đĄ Scanner | Scan-Daemon, Cron-Jobs, Logging |
| đĄ Logging | Debug Logging |
MQTT Einstellungen
| Parameter | Default | Beschreibung |
|---|---|---|
base_topic |
presence | Basis-Topic fĂŒr alle MQTT-Nachrichten |
scanner_name |
master | Name dieses Scanners |
broker |
localhost | MQTT Broker IP-Adresse |
port |
1883 | MQTT Port |
username |
MQTT Benutzername | |
password |
MQTT Passwort | |
websocket_port |
9001 | MQTT Websocket Port |
Habt ihr schon einen Broker, dann könnt ihr diesen auch nutzen.
Einfach den Haken bei "Lokalen Mosquitto Broker aktivieren & konfigurieren?" entfernen.
Alle Daten gelten dann fĂŒr den externen Broker.
Aggregator
Der Aggregator sammelt Daten aller Scanner, bestimmt den besten Scanner pro GerÀt und publiziert das aggregierte Ergebnis.
Aggregator Einstellungen
| Parameter | Default | Beschreibung |
|---|---|---|
device_timeout |
120 | Sekunden bis ein GerÀt als offline gilt |
scanner_timeout |
180 | Sekunden bis ein Scanner als offline gilt |
cleanup_interval |
60 | Wie oft auf Timeouts geprĂŒft wird |
prefer_stronger_rssi |
false | Immer den stÀrksten Scanner verwenden |
device_timeout + cleanup_interval dauern. Beispiel: 120 + 60 = max. 180 Sekunden.
Aggregator Button Events
| Parameter | Default | Beschreibung |
|---|---|---|
event_mode |
immediate / collect | Sendet Button-Event sofort, oder wartet collect_time (0.3 Sek.) und sendet vom stÀrksten Scanner |
deduplication_timeout |
1.0 | Sekunden bis der nÀchste Button-Event des gleichen GerÀts weitergegeben wird. |
Aggregator UDP Export
| Parameter | Default | Beschreibung |
|---|---|---|
enabled |
Sollen UDP-Nachrichten genutzt werden, oder nur MQTT. | |
host |
IP des NetzwerkgerÀts, das die UDP-Nachricht bekommen soll, z.B. MiniServer. | |
port |
Port der vom EmpfĂ€nger fĂŒr die Nachricht genutzt wird. |
Statistik
| Parameter | Default | Beschreibung |
|---|---|---|
enabled |
Sollen die Statistiken aufgezeichnet werden? | |
directory |
/var/www/html/ble/statistics | In diesem Verzeichnis werden die Daten gesammelt. Nur in begrĂŒndeten FĂ€llen Ă€ndern! |
cleanup_hours |
24 | Alle X Stunden werden alte Daten gelöscht. (Anwesenheit Àlter 30 Tage / Batterie Àlter 2 Jahre) |
Scanner
Hier kann eingestellt werden ob der Master auch ein Scanner sein soll, und Optionen fĂŒr alle Scanner eingestellt werden.
Scan Daemon Steuerung
| Parameter | Default | Beschreibung |
|---|---|---|
Starten / Stoppen |
gestoppt | Hier kann der Scanner des Masters aktiviert werden. Dann scannt der Master auch nach Devices. (Kein Batterie-Scan) |
Allgemein
| Parameter | Default | Beschreibung |
|---|---|---|
report_offline_battery |
nein | Wenn ein GerĂ€t offline ist, wĂŒrde bei Ja 0% Batterie zurĂŒck kommen. Bei Nein behĂ€lt er den zuletzt gesehenen Wert. |
Discovery / Scan
| Parameter | Default | Beschreibung |
|---|---|---|
timeout |
15 | So lange sucht er GerĂ€te im Discovery-Modus und meldet sie danach zurĂŒck. |
Zeitplan
| Parameter | Default | Beschreibung |
|---|---|---|
battery_scan_time |
3:00 | Um diese Uhrzeit startet der automatische Batterie-Scan. Es ist eine Chain: Es ermittelt immer nur ein Scanner die Daten. Wenn dieser fertig ist, startet der nÀchste. |
UDP Einstellungen
| Parameter | Default | Beschreibung |
|---|---|---|
enabled |
Hier wird von jedem Scanner einzeln an UDP gesendet, nicht ĂŒber den Aggregator. (Viele Daten!) | |
host |
IP des NetzwerkgerÀts, das die UDP-Nachricht bekommen soll, z.B. MiniServer. | |
port |
Port der vom EmpfĂ€nger fĂŒr die Nachricht genutzt wird. |
Logging
Das System kann Daten sammeln. Um nicht unnötig viele Logs zu schreiben kann man hier das Level einstellen.
Logging Einstellungen
| Parameter | Default | Beschreibung |
|---|---|---|
log_level |
WARNING | Level des System-Log |
console_level |
INFO | Level der Konsolen Ausgabe |
bleak_level |
WARNING | Level des Bleak-Stack fĂŒr Bluetooth (Wenn Master als Scanner arbeitet) |
GerÀte verwalten
Unter GerÀte werden alle bekannten BLE-Devices aufgelistet. Hier kann man:
- Alias vergeben â GerĂ€te erhalten einen lesbaren Namen statt MAC-Adresse
- Batterie scannen â Manueller oder automatischer Battery-Level-Check
- MQTT Monitoring â Live-Status und RSSI pro GerĂ€t
- GerĂ€t löschen â Entfernt das GerĂ€t aus der Datenbank
Distributed Scan
Ein koordinierter Scan ĂŒber alle verbundenen Scanner (Master + ESP32 Clients). Findet neue BLE-GerĂ€te im gesamten Haus gleichzeitig.
ESP32 Hardware
Folgende ESP32-Varianten werden unterstĂŒtzt:
| Hardware | Chip | RAM | Hinweis |
|---|---|---|---|
| ESP32 DevKit / WROOM | ESP32-WROOM-32 | ~60KB frei | GĂŒnstiger Standard |
| ESP32-D1 Mini | ESP32-WROOM-32 | ~60KB frei | Kompakter Formfaktor |
| ESP32-S3 ohne PSRam | ESP32-S3 | ~70KB frei | Empfohlen â mehr RAM fĂŒr BLE |
| Xiao ESP32-S3 mit PSRam | ESP32-S3 | ~135KB frei | Sehr kompakt |
| Olimex ESP32-POE (LAN8720) | ESP32-WROOM-32 | ~95KB frei | LAN-POE |
| WT32-ETH01 (LAN8720) | ESP32-WROOM-32 | ~95KB frei | LAN |
| Olimex ESP32-GATEWAY (LAN8710) | ESP32-WROOM-32 | ~95KB frei | LAN |
| Waveshare ESP32-S3-ETH (W5500) | ESP32-S3 | ~135KB frei | LAN / POE möglich |
| LilyGO T-ETH-Lite S3 (W5500) | ESP32-S3 | ~135KB frei | LAN / POE möglich |
Firmware flashen
Erstinstallation (Full-Version)
- OPTION 1
- Ăber den ESP Web Tools Flasher
- Dieses Tool lÀuft nur auf Microsoft Edge, Chrome oder Chrome basierenden Browsern
- Mit dem ESP Flash Tool flashen
- OPTION 2
- Firmware unter
WebUI â System â Udapte Firmware Downloadsherunterladen (Full-Version) - ESP32 per USB an den Computer anschlieĂen
- Mit dem ESP Flash Tool flashen
OTA Updates
ESP32-Scanner können remote aktualisiert werden:
Vom Scanner selbst
- Scanner-WebUI öffnen â
Updateklicken - Update-Firmware (.bin) hochladen
- Scanner startet automatisch neu
Zentral vom Master
System â Scanner Firmware-Statusöffnen- Veraltete Scanner werden markiert (gelbe Legende)
- Per Klick auf âUpdate" wird die neue Firmware ĂŒbertragen
ESP32 Einrichtung
- Nach dem Flashen öffnet der ESP32 einen WLAN-Hotspot - NAME: ESP32-BLE-XXXXXX
- Mit dem Hotspot verbinden - Passwort: 12345678 und Setup-Seite öffnen
- WLAN-Zugangsdaten, Master-URL und Scanner-Name (muss einzigartig sein) konfigurieren
- Feste-IP vergeben wenn gewĂŒnscht, und speichern
- Nach dem speichern startet der ESP32
- Am Master WebUI unter
Konfiguartion â ESP32-Clientsden Scanner anlegen. - Name muss gleich der Scanner ID sein.
- API-Key kopieren, speichern, und den Key im WebUI des Scanner eintragen.
ESP32 Web-UI
Jeder ESP32 Scanner hat ein eigenes Web-Interface unter seiner IP-Adresse mit:
- System-Ăbersicht (IP, RAM, Uptime, Temperatur, RSSI)
- GerÀte-Tabelle mit Status, RSSI und Batterie
- Aktionen: Battery Scan, Device Sync, Logs, Monitor, Update, Reboot
Installieren
Im WebUI des Shelly -> Scripts -> Create Script
- Script-Name vergeben
- Skript aus der nĂ€chsten Sektion kopieren und einfĂŒgen
- API-Key erstellen Master -> WebUI -> Konfiguration -> Shelly Scanner.
- IP vom Master und den API Key im Skript eintragen.
- Skript speichern und starten
Im Shelly WebUI -> Scripts den Schalter "run on startup" aktivieren.
So startet das Skript bei jedem Start des Shelly automatisch.
Skript
Hier das Skript fĂŒr den Shelly als Scanner.
- Device-Scan -> scannt known Devices UUID / MAC
- Discover-Scan -> scannt alle BLE Devices in der nĂ€he und meldet sie zurĂŒck an den Master
- Batterie-Scan -> Eddystone und BTHome
- Button-Event -> BTHome Button press / double_press / tripple_press / long_press
- Auto-Kalibration -> min. 3 BLE-Presence ESP32-Scanner mĂŒssen aktiv sein, die ĂŒber nehmen dann die Kalibrierung des Shelly mit.
Welche Funktionen werden unterstĂŒtzt
- IRK Device-Scan -> IRK wie z.B. beim iPhone funktionieren aufgrund fehlender EntschlĂŒsselungs-Bibliothken nicht.
- Batterie-Scan BLE -> Der Batteriescan von reinen BLE GerĂ€ten wie Gigset-G-Tag funktionieren aufgrund fehlender EntschlĂŒsselungs-Bibliothken nicht.
Welche Funktionen werden NICHT unterstĂŒtzt
let C = {
name: "Scanner - Name",
host: "IP -> Master",
api_key: "API-KEY -> Master WEBUI",
scan_interval: 5,
hb_interval: 10,
offline_after: 30,
debug: false
};
let VERSION="1.0.0";
let BTHOME="fcd2";
let EDDYSTONE="feaa";
let CALIB_UUID_PREFIX="B1E0CA11";
let _P="/shelly/";
let _H="0123456789abcdef";
let _s3="\x04\x05\x0A\x42\x4B\x4C";
let _s4="\x3E\x4D\x4E\x4F\x50";
let _s2="\x02\x03\x06\x07\x08\x0B\x0C\x0E\x12\x13\x14\x3D\x40\x41\x43\x45\x46\x47\x48\x49\x4A\x51\x52";
function objSize(id){
let c=String.fromCharCode(id);
if(_s3.indexOf(c)>=0)return 3;
if(_s4.indexOf(c)>=0)return 4;
if(_s2.indexOf(c)>=0)return 2;
if(id<=0x53)return 1;
return -1;
}
let known={};
let knownUuid={};
let discMode=false;
let discDevs={};
let discTimer=null;
let devs={};
let pending=[];
let ready=false;
let scanning=false;
let bootTs=0;
let lastPid={};
let calibBuf={};
let batchTimer=null;
let hbTimer=null;
let pollTimer=null;
let retryTimer=null;
let offlineInitTimer=null;
function ts(){return Math.floor(Date.now()/1000);}
function mem(tag){
Shelly.call("Sys.GetStatus",{},function(r){
if(r)print("[MEM] "+tag+" free="+r.ram_free);
});
}
function macKey(a){return a.split(":").join("").toLowerCase();}
function toHex(b){return _H[(b>>4)&0xF]+_H[b&0xF];}
function url(p){return "http://"+C.host+":8099"+p;}
function post(p,body,cb){
if(C.api_key)body.api_key=C.api_key;
Shelly.call("HTTP.Request",{
method:"POST",url:url(p),body:JSON.stringify(body),
content_type:"application/json",timeout:5
},function(r,ec){
if(ec!==0||!r||!r.body){if(cb)cb(null);return;}
try{if(cb)cb(JSON.parse(r.body));}catch(e){if(cb)cb(null);}
});
}
function get(p,cb){
let u=url(p);
if(C.api_key)u+=(u.indexOf("?")>=0?"&":"?")+"api_key="+C.api_key;
Shelly.call("HTTP.Request",{
method:"GET",url:u,timeout:5
},function(r,ec){
if(ec!==0||!r||!r.body){if(cb)cb(null);return;}
try{if(cb)cb(JSON.parse(r.body));}catch(e){if(cb)cb(null);}
});
}
function parseBTHome(raw){
if(!raw||raw.length<2)return null;
let result={pid:-1,battery:-1,button:-1};
if(raw.charCodeAt(0)&0x01)return null;
let i=1;
while(i=raw.length)break;
i+=1+raw.charCodeAt(i);
continue;
}
let size=objSize(id);
if(size<0||i+size>raw.length)break;
if(id===0x00||id===0x01||id===0x3A){
let val=0;
for(let b=0;b100)return 100;
return pct;
}
function parseIBeacon(advData){
if(!advData)return null;
let raw=BLE.GAP.ParseManufacturerData(advData);
if(!raw||raw.length<25)return null;
if(raw.charCodeAt(0)!==0x4C||raw.charCodeAt(1)!==0x00)return null;
if(raw.charCodeAt(2)!==0x02||raw.charCodeAt(3)!==0x15)return null;
let u="";
for(let i=4;i<20;i++){
u+=toHex(raw.charCodeAt(i));
if(i===7||i===9||i===11||i===13)u+="-";
}
let txp=raw.charCodeAt(24);
if(txp>127)txp-=256;
return {uuid:u,major:(raw.charCodeAt(20)<<8)|raw.charCodeAt(21),minor:(raw.charCodeAt(22)<<8)|raw.charCodeAt(23),txpower:txp};
}
function isCalibBeacon(ib){
if(!ib||!ib.uuid)return false;
return ib.uuid.substring(0,8).toUpperCase()===CALIB_UUID_PREFIX;
}
function handleCalibBeacon(ib,rssi){
let key=ib.major+":"+ib.minor;
let e=calibBuf[key];
if(!e){e={s:0,c:0,t:ib.txpower};calibBuf[key]=e;}
e.s+=rssi;
e.c++;
}
function sendCalibration(){
let readings=[];
for(let key in calibBuf){
let e=calibBuf[key];
if(e.c===0)continue;
let parts=key.split(":");
let avg=Math.round(e.s/e.c);
readings.push({major:parseInt(parts[0]),minor:parseInt(parts[1]),rssi:avg,distance:Math.round(Math.pow(10,(e.t-avg)/25)*100)/100});
}
calibBuf={};
if(readings.length===0)return;
if(C.debug)print("[CAL] "+readings.length);
post(_P+"calibration",{scanner:C.name,readings:readings},null);
}
function sendButtonEvent(mac,event,rssi,pid){
let names=["none","press","double_press","triple_press","long_press"];
let name=(event>=1&&event<=4)?names[event]:"unknown";
if(C.debug)print("[BTN] "+mac+" -> "+name+" pid="+JSON.stringify(pid));
post(_P+"event",{scanner:C.name,mac:mac,event:name,rssi:rssi,pid:pid,timestamp:ts()},null);
}
function loadKnown(macs){
known={};
knownUuid={};
if(!macs)return;
for(let i=0;i0)knownUuid[m]=true;
}
}
function register(){
print("[REG] Registering...");
let info=Shelly.getDeviceInfo();
let wifi=Shelly.getComponentStatus("wifi");
post(_P+"register",{
scanner:C.name,
ip:(wifi&&wifi.sta_ip)?wifi.sta_ip:"0.0.0.0",
model:info?(info.model||"Shelly"):"Shelly",
firmware:info?(info.fw_id||"?"):"?",
scan_interval:C.scan_interval
},function(d){
if(!d||!d.success){
print("[REG] Failed, retry 10s");
if(retryTimer)Timer.clear(retryTimer);
retryTimer=Timer.set(10000,false,register);
return;
}
loadKnown(d.known_macs);
if(C.debug){
let cM=0,cU=0;
for(let k in known)cM++;
for(let k in knownUuid)cU++;
print("[REG] OK: "+cM+"M "+cU+"U");
}else{
print("[REG] OK");
}
mem("post-reg");
if(d.scan_interval)C.scan_interval=d.scan_interval;
ready=true;
startScan();
startTimers();
});
}
function startScan(){
if(scanning)return;
BLE.Scanner.Subscribe(onScan);
BLE.Scanner.Start({duration_ms:-1,active:true});
scanning=true;
}
function onScan(ev,res){
if(ev!==BLE.Scanner.SCAN_RESULT||!res||!res.addr)return;
if(discMode){
let mac=res.addr.toUpperCase();
let ib=parseIBeacon(res.advData);
let dk=ib?ib.uuid:macKey(mac);
if(!discDevs[dk]||res.rssi>discDevs[dk].r){
let entry={m:mac,n:res.local_name||"",r:res.rssi,tp:"mac"};
if(ib){
entry.tp="ibeacon";
entry.iu=ib.uuid.toUpperCase();
entry.ima=ib.major;
entry.imi=ib.minor;
}else if(res.service_data&&res.service_data[BTHOME]){
entry.tp="bthome";
}
discDevs[dk]=entry;
}
return;
}
let ib=parseIBeacon(res.advData);
if(ib&&isCalibBeacon(ib)){
handleCalibBeacon(ib,res.rssi);
return;
}
let mac=res.addr.toUpperCase();
let mk=macKey(mac);
let isMac=known[mk]===true;
let isUuid=false;
let dk=mk;
if(!isMac){
if(ib&&knownUuid[ib.uuid]){
isUuid=true;
dk=ib.uuid;
}else{
return;
}
}
let t=ts();
let d=devs[dk];
if(!d){
d={o:false,s:0,r:-100,m:mac,t:0,b:-1,u:isUuid,ui:""};
devs[dk]=d;
}
d.s=t;
d.r=res.rssi;
d.m=mac;
if(isUuid&&ib)d.ui=ib.uuid;
let bth=null;
if(res.service_data&&res.service_data[BTHOME]){
bth=parseBTHome(res.service_data[BTHOME]);
}
if(bth&&bth.pid>=0){
if(lastPid[dk]===bth.pid)return;
lastPid[dk]=bth.pid;
}
if(bth&&bth.battery>=0&&bth.battery<=100)d.b=bth.battery;
if(d.b<0&&res.service_data&&res.service_data[EDDYSTONE]){
let eB=parseEddystoneBatt(res.service_data[EDDYSTONE]);
if(eB>=0)d.b=eB;
}
if(bth&&bth.button>=1){
sendButtonEvent(mac,bth.button,res.rssi,bth.pid);
}
let wasOff=!d.o;
d.o=true;
let scan={address:mac,rssi:res.rssi,is_online:1,local_name:""};
if(d.b>=0)scan.battery=d.b;
if(isUuid&&ib){
scan.ibeacon_uuid=ib.uuid;
scan.ibeacon_major=ib.major;
scan.ibeacon_minor=ib.minor;
scan.ibeacon_txpower=ib.txpower;
}
if(wasOff){
if(C.debug){
let l=isUuid?dk.substring(0,8)+"...":mac;
print("[ON] "+l+" R:"+res.rssi);
}
if(pending.length<20)pending.push(scan);
d.t=t;
sendBatch();
}else if(t-d.t>=C.scan_interval){
if(pending.length<20)pending.push(scan);
d.t=t;
}
}
function checkOffline(){
let t=ts();
let ct=C.offline_after*3;
for(let mk in devs){
let d=devs[mk];
let age=t-d.s;
if(d.o&&age>=C.offline_after){
d.o=false;
d.t=t;
if(C.debug){
let l=d.u?d.ui.substring(0,8)+"...":d.m;
print("[OFF] "+l);
}
let os={address:d.m,rssi:-100,is_online:0,local_name:""};
if(d.u&&d.ui)os.ibeacon_uuid=d.ui;
if(pending.length<20)pending.push(os);
}
if(!d.o&&age>=ct){
delete devs[mk];
delete lastPid[mk];
}
}
}
function sendBatch(){
if(!ready)return;
checkOffline();
if(pending.length===0)return;
let scans=pending;
pending=[];
if(C.debug)print("[TX] "+scans.length);
post(_P+"scan",{scanner:C.name,scans:scans,discovery:false},function(d){
if(!d)print("[ERR] Batch failed");
});
}
function heartbeat(){
if(!ready)return;
sendCalibration();
Shelly.call("Sys.GetStatus",{},function(sr){
let hb={scanner:C.name,uptime:ts()-bootTs};
if(sr&&sr.ram_free){
hb.free_mem=sr.ram_free;
if(C.debug)print("[MEM] hb free="+sr.ram_free);
}
post(_P+"heartbeat",hb,function(d){
if(!d||!d.success){
print("[ERR] HB failed");
ready=false;
if(retryTimer)Timer.clear(retryTimer);
retryTimer=Timer.set(5000,false,register);
}
});
});
}
function offlineInit(){
offlineInitTimer=null;
let cnt=0;
for(let mk in known){
if(!devs[mk]){
if(pending.length<20){
pending.push({address:mk,rssi:-100,is_online:0,local_name:""});
cnt++;
}
}
}
for(let uu in knownUuid){
if(!devs[uu]){
let c="";
for(let j=0;j0)sendBatch();
}
function startTimers(){
if(batchTimer)Timer.clear(batchTimer);
batchTimer=Timer.set(C.scan_interval*1000,true,sendBatch);
if(hbTimer)Timer.clear(hbTimer);
hbTimer=Timer.set(C.hb_interval*1000,true,heartbeat);
if(offlineInitTimer)Timer.clear(offlineInitTimer);
offlineInitTimer=Timer.set(C.offline_after*1000,false,offlineInit);
if(pollTimer)Timer.clear(pollTimer);
pollTimer=Timer.set(300000,true,pollDevices);
}
function startDiscovery(duration){
if(discMode)return "already running";
if(!duration||duration<5)duration=15;
discMode=true;
discDevs={};
if(discTimer)Timer.clear(discTimer);
discTimer=Timer.set(duration*1000,false,discEnd);
return "started";
}
function discEnd(){
discMode=false;
discTimer=null;
sendDiscoveryResults();
}
function sendDiscoveryResults(){
let devices=[];
for(let k in discDevs){
let dd=discDevs[k];
let entry={mac:dd.m,name:dd.n,rssi:dd.r,type:dd.tp};
if(dd.iu){
entry.ibeacon_uuid=dd.iu;
entry.ibeacon_major=dd.ima;
entry.ibeacon_minor=dd.imi;
}
devices.push(entry);
}
discDevs={};
let dUrl="http://"+C.host+"/ble/api.php?action=report_discovery";
if(C.api_key)dUrl+="&api_key="+C.api_key;
Shelly.call("HTTP.Request",{
method:"POST",url:dUrl,
body:JSON.stringify({client_id:C.name,timestamp:ts(),devices:devices}),
content_type:"application/json",timeout:15
},function(r,ec){
if(ec!==0||!r||r.code!==200)print("[DISC] Send failed");
});
}
function pollDevices(){
get(_P+"devices?scanner="+C.name,function(d){
if(!d||!d.success||!d.known_macs)return;
loadKnown(d.known_macs);
});
}
function status(){
let kon=0,koff=0,kn=0,ku=0;
for(let mk in devs){if(devs[mk].o)kon++;else koff++;}
for(let k in known)kn++;
for(let k in knownUuid)ku++;
return JSON.stringify({ready:ready,known_mac:kn,known_uuid:ku,online:kon,offline:koff,uptime:ts()-bootTs,pending:pending.length,discovery:discMode});
}
bootTs=ts();
print("=== BLE Presence Shelly v"+VERSION+" ===");
print("[BLE] "+C.name+" -> "+C.host+(C.api_key?" [API]":" [NO KEY]"));
mem("boot");
retryTimer=Timer.set(3000,false,register); Installieren
Content...
Grundriss einrichten
Unter Position â Grundriss Editor wird der Grundriss konfiguriert:
- Grundriss hochladen â PNG oder JPG des Grundrisses
- MaĂstab kalibrieren â Pixel pro Meter einstellen (z.B. 68 px/m). Wichtig fĂŒr korrekte Distanzberechnung!
- Zonen zeichnen â RĂ€ume als farbige Polygone auf dem Grundriss markieren
- Etagen verwalten â Mehrere Stockwerke mit eigenem Grundriss
Scanner platzieren
Im Grundriss-Editor werden Scanner per Drag & Drop auf dem Plan positioniert. Die Position bestimmt die Trilateration:
- Scanner möglichst gleichmĂ€Ăig verteilen
- Mindestens 3 Scanner fĂŒr Trilateration empfohlen
- WĂ€nde und Hindernisse beeinflussen die RSSI-Werte
- Scanner auf verschiedenen Etagen getrennt platzieren
Anti-Flapping
Das Anti-Flapping-System verhindert, dass GerÀte stÀndig zwischen RÀumen hin- und herspringen.
| Parameter | Default | Beschreibung |
|---|---|---|
enabled |
true | Master-Schalter: Aktiviert Positionierung komplett |
antiflapping |
true | Anti-Flapping mit Hysterese |
hysteresis |
8 | dB â neuer Scanner muss so viel besser sein |
stability_time |
30 | Sekunden â Kandidat muss so lange besser bleiben |
rssi_history |
5 | Anzahl RSSI-Werte fĂŒr Rolling Average |
Wie es funktioniert
- FĂŒr jedes GerĂ€t wird der aktuelle âbeste Scanner" (=Raum) gespeichert
- Ein neuer Scanner wird erst dann Kandidat, wenn er den aktuellen um
hysteresisdB ĂŒbertrifft - Der Kandidat muss fĂŒr
stability_timeSekunden konstant besser bleiben - Erst dann wird der Raum tatsÀchlich gewechselt
- Ein Kandidat bekommt 3 Fehlversuche, bevor er verworfen wird
Live Map
Die Live Map unter Position zeigt alle GerÀte in Echtzeit auf dem Grundriss:
- đą GrĂŒne Punkte â Position per Trilateration berechnet
- đĄ Gelbe Punkte â Fallback: Position beim stĂ€rksten Scanner
- đ” Blaue Punkte â Scanner-Positionen
Die Karte aktualisiert sich ĂŒber MQTT WebSocket in Echtzeit. GerĂ€te bewegen sich mit CSS-Transitions sanft ĂŒber den Plan.
Loxone
Anbindung an Loxone ĂŒber UDP-Export:
- In der Konfiguration
Aggregator UDP Exportaktivieren - Ziel-IP und Port des Loxone Miniservers eintragen
- In Loxone einen UDP-Eingang anlegen
- Im WebUI â GerĂ€te auf das Symbol klicken.
Hier findet ihr die Befehlserkennung fĂŒr den UDP Eingang.
Home Assistant
BLE Presence integriert sich ĂŒber MQTT Auto-Discovery in Home Assistant:
- GerÀte erscheinen automatisch als Sensor-EntitÀten
- Status (online/offline), Raum, Batterie und RSSI als Attribute
- FĂŒr Automatisierungen nutzbar (z.B. âWenn Dieter im Wohnzimmer â Licht an")
# Beispiel-Automation (Home Assistant YAML)
automation:
- trigger:
platform: state
entity_id: sensor.ble_dieter
to: "Wohnzimmer"
action:
service: light.turn_on
target:
entity_id: light.wohnzimmer API
Es können alle Daten auch ĂŒber den API-Server im Json Format ausgelesen werden.
| URL | Beschreibung |
|---|---|
http://raspberrypi:8099/ |
Alles |
http://raspberrypi:8099/stats |
Statistik |
http://raspberrypi:8099/devices |
Alle GerÀte |
http://raspberrypi:8099/online |
Nur Online |
http://raspberrypi:8099/scanners |
Scanner |
http://raspberrypi:8099/device/AA:BB:CC:DD:EE:FF o. UUID |
Device einzeln |
http://raspberrypi:8099/calibration/auto |
Werte der Autokalibrierung |
MQTT Topics
BLE Presence verwendet folgende Topic-Struktur (Beispiel mit base_topic = presence):
| Topic | Beschreibung |
|---|---|
presence/master/discovery |
Scan-Ergebnisse des Masters |
presence/[scanner]/discovery |
Scan-Ergebnisse eines Scanners |
presence/aggregated/discovery |
Aggregierte Ergebnisse (bester Scanner pro GerÀt) |
presence/aggregated/positioning |
Berechnete Positionen (Trilateration) |
presence/aggregated/scanner/+/status |
Scanner Online/Offline Status |
Aggregated Discovery Payload
{
"devices": [
{
"mac": "DC:0D:30:23:07:69",
"alias": "Dieter",
"rssi": -64,
"scanner": "west",
"battery": 100,
"status": "online",
"room": "Wohnzimmer",
"last_seen": "2026-02-06T12:45:00"
}
]
} Statistiken
Unter Statistik werden Anwesenheits- und Batteriedaten aufgezeichnet:
- Anwesenheits-Timeline â Online/Offline-Phasen als Gantt-Chart. Zeigt Dauer und zugeordneten Scanner.
- Batterie-Verlauf â Graphen ĂŒber 7 Tage bis 2 Jahre. Min/Max/Aktuell-Werte.
Logs & Debugging
Logs können ĂŒber die Web-UI oder direkt via Journal eingesehen werden:
# Aggregator Logs
sudo journalctl -u ble_aggregator -f
# Scan Daemon Logs
sudo journalctl -u ble_tool -f
# ESP32 WebSerial Monitor
# Im ESP32-WebUI: "Monitor" Button
Log-Level
In der Konfiguration unter Logging einstellbar:
| Level | Beschreibung |
|---|---|
DEBUG |
Alle Details (sehr viel Output) |
INFO |
Normaler Betrieb (empfohlen) |
WARNING |
Nur Warnungen und Fehler |
ERROR |
Nur Fehler |
System Updates
Unter WebUI â System â Update wird die installierte Version mit der verfĂŒgbaren verglichen. Updates können direkt ĂŒber das Web-UI eingespielt werden.
ESP32 Updates
Unter WebUI â System â Update wird die installierte Version der Scanner mit der verfĂŒgbaren verglichen. Updates können per Remote-Update ĂŒber das Web-UI eingespielt werden.
Troubleshooting
Dashboard zeigt âNicht verbunden"
- PrĂŒfe ob Mosquitto lĂ€uft:
sudo systemctl status mosquitto - WebSocket-Port (9001) muss in der Mosquitto-Konfiguration aktiv sein
- Browser-Konsole auf WebSocket-Fehler prĂŒfen
- Im Browser
http://scanner-ip/logsprĂŒfen
GerÀte werden nicht erkannt
- BLE muss am Raspberry Pi aktiv sein:
hciconfig hci0 up - Scan Daemon muss laufen:
sudo systemctl status ble_tool - RSSI-Threshold in der Konfiguration prĂŒfen (Default: -99 dBm)
- Browser-Konsole auf WebSocket-Fehler prĂŒfen
- Im Browser
http://scanner-ip/logsprĂŒfen
ESP32 verbindet sich nicht
- WLAN-Credentials im ESP32-Setup prĂŒfen
- Master-URL korrekt? (z.B.
http://192.168.136.120/ble) - API Key am Master und ESP32 identisch?
- ESP32 Serial Monitor via WebSerial prĂŒfen
- Im Browser
http://scanner-ip/logsprĂŒfen
GerÀte springen zwischen RÀumen
- Anti-Flapping aktivieren (
RoomTracking â antiflapping = true) - Hysterese erhöhen (8â10 dB)
- Stability Time erhöhen (30+ Sekunden)
- Scanner besser verteilen â ideal: nicht zu nah beieinander
Hoher RAM-Verbrauch am ESP32
- ESP32-S3 hat mehr freien RAM (~90 KB vs ~60 KB)
- GerÀteliste reduzieren (nur benötigte GerÀte tracken)
- Firmware-Version prĂŒfen â neuere Versionen sind optimiert
Die meisten Probleme lassen sich ĂŒber die Logs diagnostizieren.
Im Zweifel
log_level = DEBUG setzen und die Ausgabe prĂŒfen.