Files
rf-mapper/docs/HOME_ASSISTANT.md
User 7cc7c47805 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>
2026-02-01 03:31:02 +01:00

361 lines
11 KiB
Markdown

# 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
```