feat: add Home Assistant integration and improve CLI/UI

Home Assistant Integration:
- New homeassistant.py module with webhook support
- Webhooks for scan results, new devices, and device departures
- Absence detection with configurable timeout
- Documentation in docs/HOME_ASSISTANT.md

CLI Improvements:
- Replace 'web' command with start/stop/restart/status
- Background daemon mode with PID file management
- Foreground mode for debugging (--foreground)

Web UI Enhancements:
- Improved device list styling and layout
- Better floor assignment UI
- Enhanced map visualization

Documentation:
- Add CHANGELOG.md
- Add docs/API.md with full endpoint reference
- Add docs/CHEATSHEET.md for quick reference
- Update project documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
User
2026-02-01 03:31:02 +01:00
parent 0ffd220022
commit 7cc7c47805
16 changed files with 2704 additions and 108 deletions

750
docs/API.md Normal file
View File

@@ -0,0 +1,750 @@
# RF Mapper API Reference
REST API documentation for RF Mapper web interface.
**Base URL:** `http://localhost:5000`
---
## Scanning
### Trigger Scan
Performs WiFi and/or Bluetooth scan.
```
POST /api/scan
```
**Request Body:**
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `location` | string | `"web_scan"` | Location label for the scan |
| `lat` | number | config value | GPS latitude |
| `lon` | number | config value | GPS longitude |
| `scan_wifi` | boolean | `true` | Include WiFi networks |
| `scan_bluetooth` | boolean | `true` | Include Bluetooth devices |
**Example:**
```bash
curl -X POST http://localhost:5000/api/scan \
-H "Content-Type: application/json" \
-d '{"location": "kitchen", "scan_wifi": true, "scan_bluetooth": true}'
```
**Response:**
```json
{
"timestamp": "2026-02-01T14:30:00.123456",
"location": "kitchen",
"gps": {"lat": 50.8585, "lon": 4.3978},
"wifi_networks": [
{
"ssid": "HomeNetwork",
"bssid": "AA:BB:CC:DD:EE:FF",
"rssi": -45,
"channel": 6,
"frequency": 2437,
"encryption": "WPA2",
"manufacturer": "Cisco",
"estimated_distance_m": 3.5,
"signal_quality": "Excellent",
"floor": null,
"height_m": null
}
],
"bluetooth_devices": [
{
"address": "11:22:33:44:55:66",
"name": "iPhone",
"rssi": -60,
"device_class": "Phone",
"device_type": "Smartphone",
"manufacturer": "Apple",
"estimated_distance_m": 5.2,
"signal_quality": "Good",
"floor": null,
"height_m": null
}
]
}
```
---
### Quick Bluetooth Scan
Fast BLE-only scan for real-time tracking (uses bleak library).
```
POST /api/scan/bt
```
**Response:** Same as `/api/scan` but only `bluetooth_devices` populated.
---
### Get Latest Scan
Retrieve the most recent scan results.
```
GET /api/latest
```
**Response:** Same format as `/api/scan` response.
---
### List Scans
List saved scan files (most recent 50).
```
GET /api/scans
```
**Response:**
```json
[
{
"filename": "scan_20260201_143000_kitchen.json",
"timestamp": "2026-02-01T14:30:00",
"location": "kitchen",
"wifi_count": 12,
"bt_count": 5
}
]
```
---
### Get Specific Scan
Retrieve a specific scan by filename.
```
GET /api/scans/<filename>
```
**Example:**
```bash
curl http://localhost:5000/api/scans/scan_20260201_143000_kitchen.json
```
---
## Position & Configuration
### GPS Position
Get or set current GPS position.
```
GET /api/position
POST /api/position
```
**POST Body:**
```json
{"lat": 50.8585, "lon": 4.3978}
```
**Response:**
```json
{"lat": 50.8585, "lon": 4.3978}
```
---
### Configuration
Get or update application configuration.
```
GET /api/config
POST /api/config
```
**POST Body:**
| Field | Type | Description |
|-------|------|-------------|
| `gps.latitude` | number | GPS latitude |
| `gps.longitude` | number | GPS longitude |
| `scanner.path_loss_exponent` | number | Distance calculation parameter |
| `save` | boolean | Persist changes to config file |
**Example:**
```bash
curl -X POST http://localhost:5000/api/config \
-H "Content-Type: application/json" \
-d '{"gps": {"latitude": 50.85, "longitude": 4.39}, "save": true}'
```
**Response:**
```json
{
"gps": {"latitude": 50.85, "longitude": 4.39},
"web": {"host": "0.0.0.0", "port": 5000},
"scanner": {
"wifi_interface": "wlan0",
"bt_scan_timeout": 10,
"path_loss_exponent": 2.5
},
"config_file": "/home/user/git/rf-mapper/config.yaml"
}
```
---
### Building Configuration
Get or update building configuration for 3D visualization.
```
GET /api/building
POST /api/building
```
**POST Body:**
| Field | Type | Description |
|-------|------|-------------|
| `enabled` | boolean | Enable 3D building view |
| `name` | string | Building name |
| `floors` | integer | Number of floors |
| `floor_height_m` | number | Height per floor (meters) |
| `ground_floor_number` | integer | Ground floor number (0 or 1) |
| `current_floor` | integer | Scanner's current floor |
| `save` | boolean | Persist changes |
**Response:**
```json
{
"enabled": true,
"name": "Home",
"floors": 3,
"floor_height_m": 3.0,
"ground_floor_number": 0,
"current_floor": 1
}
```
---
## Device Management
### Set Device Floor
Assign a floor to a device (persists in database).
```
POST /api/device/<device_id>/floor
```
**Parameters:**
- `device_id`: BSSID (WiFi) or address (Bluetooth)
**Request Body:**
```json
{"floor": 2, "height_m": 6.0}
```
**Response:**
```json
{
"status": "updated",
"device_id": "AA:BB:CC:DD:EE:FF",
"device_type": "wifi",
"floor": 2,
"height_m": 6.0
}
```
---
### Set Device Distance
Override estimated distance for a device.
```
POST /api/device/<device_id>/distance
```
**Request Body:**
```json
{"distance": 5.5}
```
---
### Get All Floor Assignments
Retrieve all saved floor assignments.
```
GET /api/device/floors
```
**Response:**
```json
{
"AA:BB:CC:DD:EE:FF": 2,
"11:22:33:44:55:66": 1
}
```
---
## Bluetooth Identification
### Identify Single Device
Get detailed info about a Bluetooth device.
```
GET /api/bluetooth/identify/<address>
```
**Example:**
```bash
curl http://localhost:5000/api/bluetooth/identify/11:22:33:44:55:66
```
---
### Batch Identify
Identify multiple devices (max 10).
```
POST /api/bluetooth/identify
```
**Request Body:**
```json
{"addresses": ["11:22:33:44:55:66", "AA:BB:CC:DD:EE:FF"]}
```
---
## Auto-Scan
### Get Status
```
GET /api/autoscan
```
**Response:**
```json
{
"enabled": true,
"running": true,
"interval_minutes": 5,
"location_label": "auto_scan",
"scan_wifi": true,
"scan_bluetooth": true,
"last_scan_time": "2026-02-01T14:30:00",
"last_scan_result": {"timestamp": "...", "wifi_count": 12, "bt_count": 5},
"scan_count": 42
}
```
---
### Start Auto-Scan
```
POST /api/autoscan/start
```
**Request Body:**
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `interval_minutes` | integer | 5 | Scan interval |
| `location_label` | string | `"auto_scan"` | Location label |
| `scan_wifi` | boolean | `true` | Include WiFi |
| `scan_bluetooth` | boolean | `true` | Include Bluetooth |
| `save` | boolean | `false` | Persist to config |
---
### Stop Auto-Scan
```
POST /api/autoscan/stop
```
**Request Body:**
```json
{"save": true}
```
---
### Update Settings
```
POST /api/autoscan/settings
```
**Request Body:**
```json
{"interval_minutes": 10, "location_label": "living_room", "save": true}
```
---
## Historical Data API
All history endpoints require database to be enabled in config.
### List Devices
Get all tracked devices with statistics.
```
GET /api/history/devices
```
**Query Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `type` | string | Filter by `wifi` or `bluetooth` |
| `since` | string | ISO timestamp filter |
| `limit` | integer | Max results (default: 100) |
**Response:**
```json
{
"devices": [
{
"device_id": "AA:BB:CC:DD:EE:FF",
"device_type": "wifi",
"name": "HomeNetwork",
"ssid": "HomeNetwork",
"manufacturer": "Cisco",
"first_seen": "2026-01-15T10:00:00",
"last_seen": "2026-02-01T14:30:00",
"total_observations": 500,
"custom_label": "Main Router",
"is_favorite": 1,
"assigned_floor": 1
}
],
"count": 1
}
```
---
### Get Device Details
```
GET /api/history/devices/<device_id>
```
**Response:**
```json
{
"device": { ... },
"stats": {
"avg_rssi": -55.3,
"min_rssi": -75,
"max_rssi": -40,
"avg_distance_m": 4.2,
"min_distance_m": 1.5,
"max_distance_m": 12.0,
"total_observations": 500
}
}
```
---
### Get RSSI History
Time series RSSI data for a device.
```
GET /api/history/devices/<device_id>/rssi
```
**Query Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `since` | string | ISO timestamp filter |
| `limit` | integer | Max results (default: 1000) |
**Response:**
```json
{
"device_id": "AA:BB:CC:DD:EE:FF",
"observations": [
{"timestamp": "2026-02-01T14:30:00", "rssi": -55, "distance_m": 4.2, "floor": 1}
],
"count": 100
}
```
---
### Get Activity Pattern
Hourly/daily activity pattern for a device.
```
GET /api/history/devices/<device_id>/activity
```
**Query Parameters:**
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `days` | integer | 7 | Analysis period |
**Response:**
```json
{
"device_id": "AA:BB:CC:DD:EE:FF",
"period_days": 7,
"hourly_pattern": {
"8": {"count": 50, "avg_rssi": -55.0},
"9": {"count": 45, "avg_rssi": -52.3}
},
"daily_pattern": {
"0": 120,
"1": 115
}
}
```
---
### Set Device Label
```
POST /api/history/devices/<device_id>/label
```
**Request Body:**
```json
{"label": "Living Room TV"}
```
---
### Toggle Favorite
```
POST /api/history/devices/<device_id>/favorite
```
**Request Body:**
```json
{"favorite": true}
```
---
### Get Movement Events
```
GET /api/history/movement
```
**Query Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `device_id` | string | Filter by device |
| `since` | string | ISO timestamp filter |
| `limit` | integer | Max results (default: 100) |
**Response:**
```json
{
"events": [
{
"id": 1,
"device_id": "11:22:33:44:55:66",
"timestamp": "2026-02-01T14:30:00",
"rssi_delta": 10,
"distance_delta_m": -2.5,
"direction": "approaching",
"velocity_m_s": 0.5
}
],
"count": 1
}
```
---
### Get Alerts
```
GET /api/history/alerts
```
**Query Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `acknowledged` | boolean | Filter by acknowledged status |
| `type` | string | Filter by type: `new_device`, `device_absent`, `rssi_threshold` |
| `limit` | integer | Max results (default: 50) |
**Response:**
```json
{
"alerts": [
{
"id": 1,
"alert_type": "new_device",
"device_id": "11:22:33:44:55:66",
"timestamp": "2026-02-01T14:30:00",
"message": "New Bluetooth device detected: iPhone (Apple)",
"acknowledged": 0
}
],
"count": 1
}
```
---
### Acknowledge Alert
```
POST /api/history/alerts/<alert_id>/acknowledge
```
---
### Get Activity Summary
```
GET /api/history/activity
```
**Query Parameters:**
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `hours` | integer | 24 | Period in hours |
**Response:**
```json
{
"period_hours": 24,
"since": "2026-01-31T14:30:00",
"active_wifi_devices": 12,
"active_bt_devices": 8,
"total_observations": 2500,
"movement_events": 15,
"new_devices": 2,
"scan_count": 288
}
```
---
### Get Database Stats
```
GET /api/history/stats
```
**Response:**
```json
{
"total_devices": 45,
"total_observations": 50000,
"total_scans": 1200,
"total_movement_events": 300,
"unread_alerts": 5,
"database_size_bytes": 5242880,
"database_size_mb": 5.0
}
```
---
### Manual Cleanup
Trigger data cleanup (removes old records).
```
POST /api/history/cleanup
```
**Request Body:**
```json
{"retention_days": 30}
```
**Response:**
```json
{
"retention_days": 30,
"cutoff": "2026-01-02T14:30:00",
"cleaned_at": "2026-02-01T14:30:00"
}
```
---
## Error Responses
All endpoints return errors in this format:
```json
{"error": "Error message here"}
```
**Common HTTP Status Codes:**
| Code | Description |
|------|-------------|
| 200 | Success |
| 400 | Bad request (missing/invalid parameters) |
| 404 | Resource not found |
| 500 | Internal server error |
| 503 | Database not enabled |

277
docs/CHEATSHEET.md Normal file
View File

@@ -0,0 +1,277 @@
# RF Mapper Cheatsheet
Quick reference for RF Mapper commands and configuration.
---
## CLI Commands
| Command | Description |
|---------|-------------|
| `rf-mapper` | Interactive scan mode |
| `rf-mapper scan` | Run scan with defaults |
| `rf-mapper scan -l kitchen` | Scan with location label |
| `rf-mapper scan --no-bt` | WiFi only |
| `rf-mapper scan --no-wifi` | Bluetooth only |
| `rf-mapper visualize` | ASCII radar display |
| `rf-mapper analyze` | RF environment analysis |
| `rf-mapper list` | List saved scans |
| `rf-mapper start` | Start web server (background) |
| `rf-mapper stop` | Stop web server |
| `rf-mapper restart` | Restart web server |
| `rf-mapper status` | Check if server is running |
| `rf-mapper config` | Show configuration |
---
## Web Server
```bash
# Lifecycle
rf-mapper start # Start (background daemon)
rf-mapper stop # Stop
rf-mapper restart # Restart
rf-mapper status # Check if running
# Start options
rf-mapper start -f # Foreground mode
rf-mapper start -H 127.0.0.1 # Bind to localhost only
rf-mapper start -p 8080 # Custom port
rf-mapper start --debug # Debug mode (requires -f)
rf-mapper start --profile-requests # Per-request profiling
rf-mapper start --log-requests # Request logging
```
---
## Configuration Commands
```bash
# Show current config
rf-mapper config
# Set GPS coordinates
rf-mapper config --set-gps 50.8585 4.3978 --save
```
---
## Profiling
```bash
rf-mapper --profile scan # CPU profiling
rf-mapper --profile-memory scan # Memory profiling
rf-mapper --profile --profile-output scan.prof scan
```
---
## Common API Calls
```bash
# Trigger scan
curl -X POST http://localhost:5000/api/scan \
-H "Content-Type: application/json" \
-d '{"location": "office"}'
# Get latest scan
curl http://localhost:5000/api/latest
# Quick BT scan (real-time tracking)
curl -X POST http://localhost:5000/api/scan/bt
# List scans
curl http://localhost:5000/api/scans
# Get auto-scan status
curl http://localhost:5000/api/autoscan
# Start auto-scan
curl -X POST http://localhost:5000/api/autoscan/start \
-H "Content-Type: application/json" \
-d '{"interval_minutes": 5}'
# Stop auto-scan
curl -X POST http://localhost:5000/api/autoscan/stop
# Set device floor
curl -X POST http://localhost:5000/api/device/AA:BB:CC:DD:EE:FF/floor \
-H "Content-Type: application/json" \
-d '{"floor": 2}'
# Get all floor assignments
curl http://localhost:5000/api/device/floors
# Get device history
curl "http://localhost:5000/api/history/devices?type=bluetooth&limit=20"
# Get RSSI history for device
curl http://localhost:5000/api/history/devices/AA:BB:CC:DD:EE:FF/rssi
# Get database stats
curl http://localhost:5000/api/history/stats
```
---
## Config File Locations
Checked in order:
1. `./config.yaml` (project directory)
2. `~/.config/rf-mapper/config.yaml`
3. `/etc/rf-mapper/config.yaml`
---
## Environment Variables
| Variable | Description |
|----------|-------------|
| `RF_MAPPER_LAT` | Override GPS latitude |
| `RF_MAPPER_LON` | Override GPS longitude |
| `RF_MAPPER_HOST` | Override web server host |
| `RF_MAPPER_PORT` | Override web server port |
| `HA_TOKEN` | Home Assistant API token |
| `HA_URL` | Home Assistant URL |
---
## Key Config Options
```yaml
# GPS position
gps:
latitude: 50.8585
longitude: 4.3978
# Web server
web:
host: "0.0.0.0"
port: 5000
debug: false
# Scanner settings
scanner:
wifi_interface: "wlan0"
bt_scan_timeout: 10
path_loss_exponent: 2.5 # 2.0=open, 2.5=indoor, 3.5=walls
auto_identify_bluetooth: true
# Data storage
data:
directory: "data"
max_scans: 100
# Database
database:
enabled: true
filename: "devices.db"
retention_days: 30
auto_cleanup: true
# Building (3D view)
building:
enabled: true
floors: 3
floor_height_m: 3.0
current_floor: 1
```
---
## Data Locations
| Path | Content |
|------|---------|
| `data/` | Scan JSON files |
| `data/devices.db` | SQLite database |
| `data/profiles/` | Request profiles |
| `data/logs/` | Request logs |
---
## Troubleshooting
### Permission denied (WiFi/BT)
```bash
sudo rf-mapper scan
```
### No Bluetooth adapter
```bash
sudo systemctl start bluetooth
sudo hciconfig hci0 up
```
### No RSSI from Bluetooth
The app uses `bleak` library for BLE scanning. Ensure:
- BlueZ service running: `systemctl status bluetooth`
- D-Bus available
- Bluetooth adapter powered on
### Check WiFi interface
```bash
iw dev # List interfaces
sudo iw dev wlan0 scan # Test scan
```
### Database issues
```bash
# Check database
sqlite3 data/devices.db ".tables"
sqlite3 data/devices.db "SELECT COUNT(*) FROM devices"
# Manual cleanup
curl -X POST http://localhost:5000/api/history/cleanup \
-H "Content-Type: application/json" \
-d '{"retention_days": 7}'
```
---
## Web Interface Views
| View | Description | Best For |
|------|-------------|----------|
| Radar | Distance rings | Quick overview |
| World Map | Leaflet 2D | Geographic context |
| 3D Map | MapLibre GL | Building/floor view |
### Keyboard Shortcuts (Web UI)
| Key | Action |
|-----|--------|
| `R` | Refresh scan |
| `1` | Radar view |
| `2` | World map view |
| `3` | 3D map view |
---
## Signal Quality Reference
| RSSI Range | Quality | Est. Distance |
|------------|---------|---------------|
| -30 to -50 | Excellent | < 2m |
| -50 to -60 | Good | 2-5m |
| -60 to -70 | Fair | 5-10m |
| -70 to -80 | Weak | 10-20m |
| < -80 | Poor | > 20m |
---
## Path Loss Exponent Guide
| Value | Environment |
|-------|-------------|
| 2.0 | Free space (outdoor) |
| 2.5 | Light indoor (open plan) |
| 3.0 | Normal indoor |
| 3.5 | Dense indoor (walls) |
| 4.0+ | Heavy obstructions |

360
docs/HOME_ASSISTANT.md Normal file
View File

@@ -0,0 +1,360 @@
# Home Assistant Integration
RF Mapper integrates with Home Assistant using webhooks. RF Mapper sends events to HA, and HA automations handle the logic.
## Features
| Feature | Description | HA Entity Type |
|---------|-------------|----------------|
| Device presence | Track WiFi/BT devices | `device_tracker.rf_*` |
| New device alerts | Notify on unknown device | `automation` trigger |
| Departure alerts | Notify when device leaves | `automation` trigger |
| Sensor entities | Device count, nearest distance | `sensor.rf_*` |
| Multi-scanner | Room-level presence detection | per-scanner sensors |
## Architecture
### Single Scanner
```
RF Mapper Home Assistant
------------------------------------------------------
[Scan] ----webhook----> /api/webhook/rf_mapper_scan
|-- automation: update sensors
|-- automation: device_tracker.see
[New Device] --webhook--> /api/webhook/rf_mapper_new_device
|-- automation: notify
[Device Gone] --webhook-> /api/webhook/rf_mapper_device_gone
|-- automation: notify
```
### Multi-Scanner Setup
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Pi #1 │ │ Pi #2 │ │ Pi #3 │
│ scanner: │ │ scanner: │ │ scanner: │
│ id: living│ │ id: kitchen│ │ id: bedroom│
│ floor: 0 │ │ floor: 0 │ │ floor: 1 │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└───────────────────┼───────────────────┘
┌───────────────┐
│ Home Assistant │
│ - Track nearest│
│ scanner/device│
│ - Room presence │
└───────────────┘
```
## RF Mapper Configuration
Enable webhooks in `config.yaml`:
```yaml
# Scanner identity (for multi-scanner support)
scanner:
id: "living_room" # Unique scanner ID
name: "Living Room Scanner" # Human-readable name
latitude: 50.8584 # Scanner position (optional, falls back to gps.latitude)
longitude: 4.3976 # Scanner position (optional, falls back to gps.longitude)
floor: 0 # Scanner's floor (optional, falls back to building.current_floor)
home_assistant:
enabled: true
url: "http://192.168.129.10:8123"
webhook_scan: "rf_mapper_scan"
webhook_new_device: "rf_mapper_new_device"
webhook_device_gone: "rf_mapper_device_gone"
device_timeout_minutes: 5
```
### Scanner Identity Settings
| Setting | Description |
|---------|-------------|
| `scanner.id` | Unique scanner identifier (auto-generated from hostname if empty) |
| `scanner.name` | Human-readable display name (defaults to id) |
| `scanner.latitude` | Scanner latitude (falls back to `gps.latitude`) |
| `scanner.longitude` | Scanner longitude (falls back to `gps.longitude`) |
| `scanner.floor` | Scanner's floor number (falls back to `building.current_floor`) |
### Home Assistant Settings
| Setting | Description |
|---------|-------------|
| `enabled` | Enable/disable HA integration |
| `url` | Home Assistant URL (no trailing slash) |
| `webhook_scan` | Webhook ID for scan results |
| `webhook_new_device` | Webhook ID for new device alerts |
| `webhook_device_gone` | Webhook ID for departure alerts |
| `device_timeout_minutes` | Minutes before device is considered departed |
## Home Assistant Setup
### 1. Automations (`automations.yaml`)
```yaml
# Process scan results - update device trackers with scanner info
- alias: "RF Mapper - Update Device Trackers"
trigger:
- platform: webhook
webhook_id: rf_mapper_scan
action:
- repeat:
for_each: "{{ trigger.json.devices }}"
sequence:
- service: device_tracker.see
data:
dev_id: "rf_{{ repeat.item.id | replace(':', '_') }}"
source_type: "{{ 'bluetooth' if ':' in repeat.item.id else 'router' }}"
attributes:
friendly_name: "{{ repeat.item.name }}"
rssi: "{{ repeat.item.rssi }}"
distance_m: "{{ repeat.item.distance }}"
floor: "{{ repeat.item.floor }}"
scanner_id: "{{ trigger.json.scanner.id }}"
scanner_name: "{{ trigger.json.scanner.name }}"
# New device notification (includes which scanner detected it)
- alias: "RF Mapper - New Device Alert"
trigger:
- platform: webhook
webhook_id: rf_mapper_new_device
action:
- service: notify.persistent_notification
data:
title: "New Device Detected"
message: >
{{ trigger.json.device_type }}: {{ trigger.json.name }}
({{ trigger.json.device_id }})
detected by {{ trigger.json.scanner.name | default('unknown scanner') }}
# Device departure notification (includes last scanner)
- alias: "RF Mapper - Device Left"
trigger:
- platform: webhook
webhook_id: rf_mapper_device_gone
action:
- service: notify.persistent_notification
data:
title: "Device Left"
message: >
{{ trigger.json.name }} last seen {{ trigger.json.last_seen }}
at {{ trigger.json.last_scanner.name | default('unknown location') }}
```
### 2. Sensor Templates (`configuration.yaml`)
```yaml
template:
- trigger:
- platform: webhook
webhook_id: rf_mapper_scan
sensor:
# Per-scanner device count sensor
- name: "RF Scanner {{ trigger.json.scanner.id }} Device Count"
unique_id: "rf_scanner_{{ trigger.json.scanner.id }}_count"
state: "{{ trigger.json.device_count }}"
icon: mdi:bluetooth
attributes:
scanner_id: "{{ trigger.json.scanner.id }}"
scanner_name: "{{ trigger.json.scanner.name }}"
scanner_floor: "{{ trigger.json.scanner.floor }}"
# Per-scanner nearest device sensor
- name: "RF Scanner {{ trigger.json.scanner.id }} Nearest"
unique_id: "rf_scanner_{{ trigger.json.scanner.id }}_nearest"
state: "{{ trigger.json.devices | map(attribute='distance') | min | round(1) if trigger.json.devices else 'none' }}"
unit_of_measurement: "m"
icon: mdi:map-marker-distance
attributes:
scanner_id: "{{ trigger.json.scanner.id }}"
```
## Webhook Payload Reference
### Scan Results (`rf_mapper_scan`)
```json
{
"timestamp": "2026-02-01T12:34:56.789",
"scan_type": "bluetooth",
"scanner": {
"id": "living_room",
"name": "Living Room Scanner",
"latitude": 50.8584,
"longitude": 4.3976,
"floor": 0
},
"scanner_floor": 0,
"device_count": 5,
"devices": [
{
"id": "AA:BB:CC:DD:EE:FF",
"name": "iPhone",
"rssi": -65,
"distance": 3.2,
"floor": 0
}
]
}
```
| Field | Description |
|-------|-------------|
| `scanner` | Full scanner identity object |
| `scanner.id` | Unique scanner identifier |
| `scanner.name` | Human-readable scanner name |
| `scanner.latitude` | Scanner GPS latitude |
| `scanner.longitude` | Scanner GPS longitude |
| `scanner.floor` | Floor where scanner is located |
| `scanner_floor` | (Deprecated) Same as `scanner.floor`, for backward compatibility |
### New Device (`rf_mapper_new_device`)
```json
{
"timestamp": "2026-02-01T12:34:56.789",
"device_id": "AA:BB:CC:DD:EE:FF",
"name": "Unknown Device",
"device_type": "bluetooth",
"manufacturer": "Apple, Inc.",
"rssi": -70,
"distance_m": 5.5,
"scanner": {
"id": "living_room",
"name": "Living Room Scanner",
"latitude": 50.8584,
"longitude": 4.3976,
"floor": 0
}
}
```
### Device Gone (`rf_mapper_device_gone`)
```json
{
"timestamp": "2026-02-01T12:34:56.789",
"device_id": "AA:BB:CC:DD:EE:FF",
"name": "iPhone",
"device_type": "bluetooth",
"last_seen": "2026-02-01T12:29:00.000",
"last_scanner": {
"id": "living_room",
"name": "Living Room Scanner",
"latitude": 50.8584,
"longitude": 4.3976,
"floor": 0
}
}
```
## Verification Steps
1. **Enable integration**: Set `home_assistant.enabled: true` in config.yaml
2. **Add HA automations**: Copy webhook automations to HA
3. **Restart RF Mapper**: `rf-mapper restart`
4. **Run scan**: Trigger BT scan in RF Mapper web UI
5. **Check HA**: Verify `device_tracker.rf_*` entities appear
6. **Test new device**: Clear device from DB, re-scan, verify notification
7. **Test departure**: Wait for timeout, verify departure notification
8. **Check sensors**: Verify `sensor.rf_mapper_*` values update
## Troubleshooting
| Issue | Solution |
|-------|----------|
| Webhooks not received | Check HA URL in config, ensure no firewall blocking |
| No device trackers | Verify automation is enabled in HA |
| Departure not triggered | Increase `device_timeout_minutes` |
| Connection timeout | Check network connectivity between RF Mapper and HA |
## Multi-Scanner Setup
Configure each scanner with a unique ID and position:
**Pi #1 (Living Room):**
```yaml
scanner:
id: "living_room"
name: "Living Room Scanner"
floor: 0
latitude: 50.8584
longitude: 4.3976
```
**Pi #2 (Kitchen):**
```yaml
scanner:
id: "kitchen"
name: "Kitchen Scanner"
floor: 0
latitude: 50.8585
longitude: 4.3978
```
**Pi #3 (Bedroom):**
```yaml
scanner:
id: "bedroom"
name: "Bedroom Scanner"
floor: 1
latitude: 50.8584
longitude: 4.3977
```
### Room Presence Automation
```yaml
# Track which room a device is in (nearest scanner)
- alias: "RF Mapper - Track Room Presence"
trigger:
- platform: webhook
webhook_id: rf_mapper_scan
action:
- repeat:
for_each: "{{ trigger.json.devices }}"
sequence:
- service: input_text.set_value
target:
entity_id: "input_text.rf_{{ repeat.item.id | replace(':', '_') }}_room"
data:
value: "{{ trigger.json.scanner.id }}"
```
## Advanced: Presence-based Automations
```yaml
# Turn on lights when specific device arrives in living room
- alias: "Welcome Home - Living Room"
trigger:
- platform: webhook
webhook_id: rf_mapper_scan
condition:
- condition: template
value_template: >
{{ trigger.json.scanner.id == 'living_room' and
trigger.json.devices | selectattr('id', 'eq', 'AA:BB:CC:DD:EE:FF') | list | count > 0 }}
action:
- service: light.turn_on
target:
entity_id: light.living_room
# Turn off lights when device leaves a room
- alias: "Room Empty Check"
trigger:
- platform: webhook
webhook_id: rf_mapper_scan
condition:
- condition: template
value_template: "{{ trigger.json.scanner.id == 'bedroom' and trigger.json.device_count == 0 }}"
action:
- service: light.turn_off
target:
entity_id: light.bedroom
```