Virtual presence sensors for Home Assistant — inspired by the Aqara Presence Soft Sensor concept, but fully transparent, fully local, and fully configurable.
Instead of relying on a single sensor, HA Soft Presence combines multiple signals (mmWave, PIR, door contacts, media players, workstations, lights, …) using a weighted score engine and a 6-state machine to produce a stable, automation-ready occupancy decision per room.
- 🏠 8 entities per room — binary occupancy, score, confidence, reason, plus 4 optional AI advisory entities
- 🔀 Sensor fusion — mmWave, PIR, BLE/ESPresense, media player, workstation, lights, door contacts, locks
- 🔄 6-state machine —
clear → possible_entry → occupied → clear_pending → clear - 📊 Hysteresis — separate occupied/clear thresholds, no flickering
- 🌙 Sleep mode — configurable entities raise the clear threshold at night
- ⏱️ Configurable timeout per room before going clear
- 💬 Reason string — human-readable explanation, e.g.
"mmWave active + Media playing" - 📡 HA Events — fires
ha_soft_presence_state_changedon every transition - 🛠️ Service calls —
force_occupied,force_clear,reset_overrideper room - 🤖 Optional AI advisory — via Gemini, Ollama, OpenAI — off by default, fully local
- 📍 Area auto-fill — entity selectors pre-filled from matching HA area on setup
- 🖥️ Config UI — full multi-step setup, no YAML required
- 🌍 11 languages — EN, DE, FR, ES, IT, NL, PL, PT, SV, RU, BG
HA Soft Presence is available in the HACS default store.
- Open HACS → search for HA Soft Presence (or click the button above)
- Install HA Soft Presence
- Restart Home Assistant
- Copy
custom_components/ha_soft_presence/to your HA config directory - Restart Home Assistant
- Click the button above or go to Settings → Devices & Services → Add Integration
- Search for HA Soft Presence
- Follow the 5-step wizard:
- Step 1 — Room name, has door, transit room, disable door-entry check
- Step 2 — mmWave & PIR sensors (pre-filled from matching HA area)
- Step 3 — Context sensors: doors, windows, locks, media, lights, switches, workstation (pre-filled)
- Step 4 — Thresholds, timeout, sleep mode entities
- Step 5 — Optional AI advisory
Repeat to add more rooms.
Tip — an entity missing from a picker? Several steps filter by
device_class(PIR →motion/occupancy, doors →door, windows →window). Many contact and motion sensors ship with a generic or wrong class, so they won't show up. Open the entity's settings and set Show as to the matching type (Door, Window, …) — it then appears in the right step.
Entity settings → Show as (here: Anzeigen als → Tür)
| Entity | Example | Description |
|---|---|---|
binary_sensor.{room}_presence_soft |
binary_sensor.wohnzimmer_presence_soft |
on = occupied, off = clear |
sensor.{room}_presence_score |
sensor.wohnzimmer_presence_score |
Score 0–100 |
sensor.{room}_presence_confidence |
sensor.wohnzimmer_presence_confidence |
high / medium / low |
sensor.{room}_presence_reason |
sensor.wohnzimmer_presence_reason |
"mmWave active + Media playing" |
| Entity | Example | Description |
|---|---|---|
binary_sensor.{room}_presence_llm |
binary_sensor.wohnzimmer_presence_llm |
AI occupancy estimate |
sensor.{room}_presence_llm_score |
sensor.wohnzimmer_presence_llm_score |
AI score 0–100 |
sensor.{room}_presence_llm_confidence |
sensor.wohnzimmer_presence_llm_confidence |
AI confidence |
sensor.{room}_presence_llm_reason |
sensor.wohnzimmer_presence_llm_reason |
AI explanation |
presence_score: 78
confidence: high
reason: "mmWave active + Media playing"
active_sources:
- mmwave
- media_playing
state_machine: occupied
last_positive_signal: "2026-04-24T22:33:00+00:00"
last_positive_reason: "mmWave active + Media playing" # what last triggered OCCUPIED
last_positive_sources:
- mmwave
- media_playing
timeout_remaining: 210
room_name: Wohnzimmer
manual_override: null # "occupied" | "clear" | null
sleep_mode_active: false
sensors: # diagnostic — all configured sensors with current HA state
mmwave:
binary_sensor.bedroom_mmwave: "on"
pir:
binary_sensor.bedroom_pir: "off"| Signal | Weight | Notes |
|---|---|---|
| mmWave active | +80 | Strongest signal — detects still persons |
| Person-count sensor | +80 | Camera/people-counter; value > 0 counts as presence |
| PIR active | +35 | Real-time motion |
| PIR recent (decay) | up to +15 | Fades over 5 min after PIR goes off |
| Workstation active | +35 | Binary sensor or power > 10 W |
| Media playing | +30 | Media player in "playing" state |
| Media paused | +15 | |
| Light manually on | +20 | Any configured light/switch on |
| Lock recently unlocked | up to +15 | Decays over 10 min |
| Door recently opened | up to +10 | Decays over 5 min |
Score ≥
occupied_threshold(default 50) → OCCUPIED Score ≤clear_threshold(default 20) → start no-presence timeout Between thresholds → hold current state (hysteresis) Sleep mode active →clear_thresholdreplaced bysleep_clear_threshold(default 5)
CLEAR
↓ (score enters hysteresis zone)
POSSIBLE_ENTRY
↓ (score ≥ occupied_threshold)
OCCUPIED
↓ (score ≤ clear_threshold, after min_hold_time)
CLEAR_PENDING ←→ OCCUPIED (if score recovers)
↓ (timeout expires, no contradicting signal)
CLEAR
Note
A door closing alone does not trigger CLEAR — it is a context signal, not proof of absence.
| Category | What to select |
|---|---|
| mmWave | DFRobot SEN0395, FP2, LD2410, EP1, Everything Presence One |
| PIR | Any binary_sensor with device class motion or occupancy |
| Person-count | Numeric sensor from a camera / people-counter — value > 0 triggers presence |
| Door contacts | binary_sensor with device class door |
| Window contacts | binary_sensor with device class window |
| Locks | Any lock entity |
| Media players | Any media_player entity |
| Lights | Any light entity |
| Switches | Any switch entity |
| Workstation | binary_sensor (on/off) or sensor with device class power (W, active when > 10 W) |
Configure one or more entities (e.g. input_boolean.sleepmode_bedroom, group.sleepmode_all) as sleep mode indicators. When any of them is on, the clear threshold is replaced by the much lower sleep mode clear threshold (default: 5).
This means the room stays occupied as long as bed sensors, PIR, or any other signal is above 5 — preventing false "clear" states while sleeping.
The sleep_mode_active attribute on the binary sensor reflects the current state.
On every occupied ↔ clear transition, HA Soft Presence fires:
event_type: ha_soft_presence_state_changed
event_data:
room_name: Wohnzimmer
room_slug: wohnzimmer
entry_id: abc123
occupied: true
score: 78
confidence: high
reason: "mmWave active"
state_machine: occupied
manual_override: nullUse in automations:
triggers:
- trigger: event
event_type: ha_soft_presence_state_changed
event_data:
room_slug: wohnzimmer
occupied: true| Service | Description |
|---|---|
ha_soft_presence.force_occupied |
Override room to occupied, ignore sensor signals |
ha_soft_presence.force_clear |
Override room to clear, ignore sensor signals |
ha_soft_presence.reset_override |
Remove override, return to automatic detection |
ha_soft_presence.reload_all |
Reload all HA Soft Presence config entries at once |
force_occupied, force_clear, and reset_override take the entity_id of the room's binary_sensor.*_presence_soft entity.
actions:
- action: ha_soft_presence.force_occupied
data:
entity_id: binary_sensor.arbeitszimmer_presence_softWhen enabled, an AI analysis runs once on startup and then whenever new sensor events are recorded (respecting the configured minimum interval, default 5 min).
Note
The AI receives only anonymised data: event type + age in seconds. No names, no images, no raw sensor values are sent externally.
Supported providers: any HA conversation agent (Gemini, Ollama, OpenAI, …)
AI entities show "Waiting for evaluation" until the first response arrives.
- 🚪 Door closing ≠ vacant — door is context only, not proof of absence
- 🚶 Door-validated fast clear — if the door didn't open since the room became occupied, nobody could have left → 30 s timeout instead of the full no-presence timeout
- 📡 mmWave is highest-weight and overrides PIR
- ⚡
occupiedis set fast;clearis set slowly (timeout + hysteresis) - 🛏️ Bed sensors → use the PIR sensor slots (binary on/off)
- 🌙 Sleep mode keeps the room occupied longer with minimal configuration
- 🔒 All processing is local — nothing leaves HA unless AI is explicitly enabled
| Feature | Status | Notes |
|---|---|---|
| ESPresense / BLE | ✅ done | One sensor per tracked device; state = room name string, matched against room slug |
| Person-count sensors | ✅ done | Numeric camera / people-counter sensors; value > 0 → +80 score |
| Camera / Frigate support | planned | Frigate person-detection binary sensors; own score weight distinct from PIR |
| Camera snapshot + Vision AI | planned | Send camera snapshot to vision-capable AI; opt-in, privacy-first |
| Batch AI evaluation | ✅ done | Single LLM call per agent covering all rooms — minimises API requests |
| Direct HTTP AI provider | ✅ done | Call MiniMax, Groq, or any OpenAI-compatible /chat/completions API directly without an HA conversation agent. Per-room provider toggle + base URL / key / model; rooms sharing a backend still batch into one call. Handles reasoning-model output (<think> / code fences) |
| Room-level aggregation | planned | "Anyone home on floor 1?" aggregating multiple rooms |
HA Quality Scale — iot_class |
✅ done | local_push is correct — coordinator is primarily event-driven via async_track_state_change_event |
HA Quality Scale — diagnostics.py |
✅ done | Download Diagnostics button on integration device page; dumps config, entity state, and internal coordinator state (lock-in, clear-pending, event log) |
| HA Quality Scale — Tests | planned | pytest-homeassistant-custom-component test suite |
| HA Quality Scale — Repairs / Issues | ✅ done | Three actionable repair issues: no presence sensors, has_door without contact, missing entities |
| HA Quality Scale — Entity unique_id | ✅ done | Config entry unique_id set to room slug; prevents duplicate room entries |
| Sleep mode | ✅ done | Configurable entities raise clear threshold when active |
| HA Events | ✅ done | ha_soft_presence_state_changed on every transition |
| Service calls | ✅ done | force_occupied, force_clear, reset_override |
| Area auto-fill | ✅ done | Entity selectors pre-filled from matching HA area |
| AI initial evaluation | ✅ done | AI runs once on startup |
| Multi-language (11 languages) | ✅ done | EN, DE, FR, ES, IT, NL, PL, PT, SV, RU, BG |
| Options flow | ✅ done | Full reconfiguration without deleting the integration |
MIT — free to use, no warranty.