The single hardest automation to get right in my Home Assistant install isn’t a complicated dashboard or a fancy energy-balancing routine. It’s “is anyone home?”. Three years and three full rewrites later, my presence file is 326 lines and I still don’t fully trust it. But it’s gotten reliable enough that I’m willing to write down what works and — more importantly — what didn’t.
This isn’t a beginner walkthrough. It’s a postmortem on three iterations and the specific failure modes that broke each one.
What “presence detection” actually means
When people say “presence detection” in HA they usually mean one of three things, and conflating them is the source of about half the bugs:
- Person tracking. Where is each individual person, right now? GPS-driven, comes from the HA companion app on a phone.
- Home occupancy. Is anyone home at all? Boolean. Drives “lock the doors when nobody’s here” and “turn off all the lights when the house is empty.”
- Room occupancy. Is the kitchen occupied? Driven by motion sensors, mmWave radar, BLE beacons. Drives “turn the lights on when I walk in.”
This post is about the second one — home occupancy — because that’s the one I kept getting wrong. Person tracking via the companion app is mostly a solved problem (it works as well as your phone’s GPS works, and it works fine for everything I do with it). Room occupancy is its own rabbit hole, mostly about sensor placement.
The hard part is collapsing two people’s individual locations into a single “house occupied” boolean and using that to drive automations that don’t fire at the wrong time.
Iteration 1: Naive person.charles.state == 'home'
The first version was the obvious one. I had a binary_sensor.home_occupied template that returned true if either my person or my partner’s person was in the home zone:
template:
- binary_sensor:
- name: "Home Occupied"
state: >
{{ is_state('person.charles', 'home') or
is_state('person.partner', 'home') }}
This broke in three different ways within the first month:
Failure mode 1: Phone GPS jitter. The companion app would briefly report not_home for ten or twenty seconds when GPS lock got fuzzy — typically when I was inside the house and my phone was on the kitchen counter. The boolean would flip to false, an automation would fire (“nobody’s home, turn off the lights”), the lights would go dark while I was standing under them, then GPS would re-acquire and the boolean would flip back. By that point my partner was annoyed.
Failure mode 2: Welcome-home firing on departure. I had a “welcome home” automation that triggered on state changed from not_home to home. It would also fire when I drove past the house to the highway — the GPS bounced through the home zone for thirty seconds and triggered the automation. Lights came on. Doors unlocked. I wasn’t home.
Failure mode 3: Battery-saver mode killing updates. If my partner’s phone went into battery saver, location updates would slow to a crawl. The boolean stayed home for hours after she’d left, because nothing was telling HA she’d moved.
I spent a few weeks bandaging these — debounce delays, “for: 5 minutes” conditions, longer state checks — and it kept finding new edges to fail on. The fundamental problem was that I was using a transient phone GPS reading as the source of truth for an ambient state.
Iteration 2: Home mode as a manual switch
I went the other direction next: a manual input_select.home_mode with three states (Home, Away, Sleep), changed only by explicit triggers. The companion app tells me about location, but it doesn’t get to set the home mode. Setting Away happens when both phones have been outside the home zone for a sustained period; setting Home happens when at least one of us crosses the home zone boundary heading inward.
input_select:
home_mode:
name: Home Mode
options:
- Home
- Away
- Sleep
initial: Home
icon: mdi:home
This was a big improvement. Automations now condition on input_select.home_mode instead of raw person.X.state, and the input_select only changes when an automation explicitly sets it. That means GPS jitter no longer flips the world out from under me — it might briefly cause a person sensor to say not_home, but unless that state persists, no mode change happens.
Most of the file is the logic that decides when to flip modes:
- id: 'auto_away_mode'
alias: Auto Away Mode
triggers:
- trigger: state
entity_id:
- person.charles
- person.partner
to: 'not_home'
for:
minutes: 10
conditions:
# Both people must be away
- condition: template
value_template: >
{{ not is_state('person.charles', 'home') and
not is_state('person.partner', 'home') }}
# Don't flip if already Away
- condition: not
conditions:
- condition: state
entity_id: input_select.home_mode
state: 'Away'
actions:
- action: input_select.select_option
target:
entity_id: input_select.home_mode
data:
option: 'Away'
Three things matter here:
for: 10 minuteson the trigger — both people must have been away for ten consecutive minutes before mode flips. That’s the GPS-jitter killer. A spurious twenty-secondnot_homedoesn’t propagate.- The template condition double-checks both people at action time, not trigger time. If my partner left at 9 AM and I leave at 11 AM, my
not_hometrigger fires; the template confirms she’s also still away; mode flips. If she came back at 10:55 AM, the template fails and mode doesn’t flip even though my trigger fired. - The “don’t flip if already X” guard prevents the same automation from re-firing every time a trigger reaches the threshold.
mode: singlewould also handle this, but the explicit guard makes the intent obvious to anyone reading the YAML six months later — including me.
Failure mode 4: The zone-bounce on departure
The pattern that took the longest to find was this: I have a zone.close_to_home that’s a wider geofence around the house, and zone.home for the actual property. The welcome-home automation triggered on entering close_to_home. The departure path normally crosses close_to_home first, then home. But on the way back from a long trip, GPS sometimes reports an out-of-order sequence: the phone briefly registers as inside home before close_to_home, depending on which tower the GPS is fixing against.
That used to fire welcome_home_actions_charles on departure too, because close_to_home got an enter event after a brief leave-then-re-enter flicker. The fix was the home_mode condition: welcome-home only fires when mode is currently Away. If we’re leaving — mode is Home, geofence flickers, condition fails, nothing happens. If we’re arriving — mode is Away, geofence triggers, condition passes, action fires.
- id: 'welcome_home_actions_charles'
alias: Welcome Home Actions - Charles
triggers:
- trigger: zone
entity_id: person.charles
zone: zone.close_to_home
event: enter
conditions:
# Must actually be away (not just leaving the house)
- condition: state
entity_id: input_select.home_mode
state: 'Away'
actions:
- action: notify.mobile_app_charles_phone
data:
title: "Welcome home"
message: "Tap to open garage / unlock front / turn on lamp"
data:
actions:
- action: "OPEN_GARAGE"
title: "Open garage"
- action: "UNLOCK_FRONT"
title: "Unlock front door"
- action: "TURN_LAMP_ON"
title: "Turn on lamp"
The notification has actionable buttons. Tapping “Open garage” opens the garage and unlocks the deadbolt; tapping “Unlock front door” unlocks the front lock; tapping “Turn on lamp” turns on a living-room light. All three handled by a separate welcome_home_action_handler automation that listens for mobile_app_notification_action events.
I went with action buttons rather than auto-opening the garage on geofence enter, partly because of the false-trigger history above and partly because the failure mode of “garage opens when the wrong person is driving past with my phone” is much worse than “I have to tap a button when I get home.”
Failure mode 5: Battery-saver and the dead-phone problem
for: 10 minutes solved GPS jitter, but it didn’t solve “what happens when a phone stops reporting at all.” If my partner’s phone goes into deep sleep, her person.partner state never changes. If she actually leaves and her phone never reports it, the auto-away automation never fires.
The bandage I have for this is home_occupied_sync, which fires on a different signal entirely — interior motion sensor activity. The logic is roughly: if no motion has been detected anywhere inside the house for a long time (currently 90 minutes), and the active mode is still Home, force a re-evaluation. If both people are not_home at that point, flip to Away.
This is a backstop, not a primary signal. It catches the case where GPS was wrong about home/away status and the house sat empty for an hour and a half. The 90-minute threshold is long enough to avoid firing during a midday nap — the only time you might be home without triggering motion sensors for that long.
I’d love to replace this with something more reliable, like a presence-detection mmWave sensor in the living room, but I haven’t gotten around to it.
What I’d build differently
Three things I’d do different on a fresh install:
- Start with
input_select.home_modefrom day one. Don’t condition automations on rawperson.X.stateever. GPS jitter and battery saver are real and they will hurt you. - Use
for:aggressively. Anything triggered by GPS state or zone enter/leave needs afor:of at least a few minutes, sometimes longer. The cost is responsiveness; the benefit is sanity. - Build the failure case first. Before the welcome-home automation, write the “I’m not home” automation. Before the away-mode flip, write the unit test (mentally if not literally) of every weird path: deep sleep, GPS bounce, brief disconnect, both phones losing signal at once, etc.
When it’s worth the investment
I spent probably 30 hours total across the three iterations, and it’s not done — I expect to put another five or ten hours in over the next year. That’s a lot of time for “is anyone home.” The reason it’s worth it is that home-occupancy is the single condition that drives the most other automations: lighting, climate, security, alerts, energy management. If it’s wrong, every downstream automation is wrong with it. If it’s right, you stop thinking about it and start trusting the rest of the stack.
The 326-line presence file is by far the largest single automation file in my install. It’s not that complicated line by line. It’s that the edge cases are real, and most of the lines exist to handle edges that took weeks to find.