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:
360
docs/HOME_ASSISTANT.md
Normal file
360
docs/HOME_ASSISTANT.md
Normal 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
|
||||
```
|
||||
Reference in New Issue
Block a user