Skip to content

Send only the newest location fix so a stale one can't land after a newer one - #7676

Open
eseverson wants to merge 2 commits into
home-assistant:mainfrom
eseverson:fix/location-update-race
Open

eseverson wants to merge 2 commits into
home-assistant:mainfrom
eseverson:fix/location-update-race

Conversation

@eseverson

@eseverson eseverson commented Oct 22, 2026 •

Copy link
Copy Markdown

Summary

Fixes a race that could flip a person entity back out of a zone right after entering it.

How it was found. A zone-enter automation fired on arriving home, and a second automation conditioned on the person being home then saw them as away a moment later. The recorder shows the mobile_app device tracker (exact GPS, home zone radius 75 m) going home at 73 m from the zone center with 9 m accuracy, then not_home 207 ms later at 113 m with 7 m accuracy, then home again 13 s later. The second fix is farther back along the approach path, so it is an older position delivered after a newer one, not GPS noise. A similar same-second burst of three updates appeared earlier the same evening. Core 2026.10.2, Pixel 10 Pro connected to Android Auto, likely switching from cellular to home Wi-Fi at the time. The on-device location history was not captured for this event, so the attribution below comes from the code rather than from phone logs.

Cause. Location fixes from the geofence and background paths were each sent from their own coroutine, and lastLocationSend was only recorded once the HTTP call returned. Two fixes arriving within the same delivery window, most likely the geofence ENTER and a background fix of the same approach, both passed the "not latest" check and raced to the server. Whichever HTTP call finished last won, so an older fix could land after a newer one. The update_location webhook carries no fix timestamp, so Core cannot drop an out-of-order update; the app has to.

Repro. Enable zone tracking and background location, drive into a zone while the connection is changing or slow, and compare the app's location history (Settings > Companion app > Troubleshooting > Location tracking history) with the person entity's history. Expect two SENT rows within a few hundred milliseconds of each other, and the person entity sometimes ending on the older position.

Change. Sends now go through a small LocationSendQueue:

  • One send at a time per server. Fixes that arrive while a send is in flight collapse into a single pending slot that only a newer fix (by Location.time) can replace, so a backlog becomes one send of the latest position. Superseded fixes are logged as SKIPPED_NOT_LATEST in the location history.
  • Servers are independent, so a slow or unreachable server never delays updates to another one.
  • When a newer background fix replaces a queued geofence fix, it inherits the geofence's debounce bypass. Otherwise a zone transition could be collapsed and then dropped by the five second debounce, leaving nothing sent.
  • The "not latest" check now compares a fix against the last sent fix's own time instead of the wall clock of the previous send's evaluation. With sends serialized, the old comparison would have skipped a newer fix that was queued behind an in-flight send.
  • Maps written on the send path are ConcurrentHashMaps, since servers now send concurrently. LocationUpdateTrigger moves out of the companion object so the queue and its test can import it.

Impact. Location updates reach the server in timestamp order per server. Multi-server users with one unreachable server keep getting timely updates on the others. The five second debounce now applies to closely spaced background fixes as intended, where before the race let the second one through.

Checklist

  • New or updated tests have been added to cover the changes following the testing guidelines.
  • The code follows the project's code style and best_practices.
  • The changes have been thoroughly tested, and edge cases have been considered.
  • Changes are backward compatible whenever feasible. Any breaking changes are documented in the changelog for users and/or in the code for developers depending on the relevance.
  • I have read the Open Home Foundation AI Policy.

Select exactly one option that describes AI usage in this contribution:

  • I have not used AI for this contribution.
  • AI assistance was used for this contribution.
  • AI fully generated the code for this contribution, but I've reviewed and understood it before submitting and will respond without AI during review.

Any other notes

LocationSendQueueTest covers the ordering, collapse, geofence bypass carry-over, and per-server independence with a TestDispatcher and a gated fake send. The skip checks inside sendLocationUpdate are not unit tested because the manager builds its own CoroutineScope and reads the wall clock directly; making that injectable is a larger change than this fix.

…ewer one

Location fixes from the geofence and background paths were each sent from their own coroutine, and the "not latest"
check only saw a send once its HTTP call had returned. Two fixes arriving together both passed the check and raced to
the server, so an older fix could land last and flip the person entity back out of a zone it had just entered.

Sends now go through LocationSendQueue: one at a time per server, with fixes waiting behind an in-flight send
collapsing into a single pending slot that only a newer fix can replace. Superseded fixes are logged as
SKIPPED_NOT_LATEST. When a newer background fix replaces a geofence fix it inherits the geofence's debounce bypass, so
a zone transition can't be collapsed and then debounced into no send. The "not latest" check compares against the
last sent fix's own time rather than the wall clock of the previous send, which would have skipped a newer fix queued
behind an in-flight one. Servers are independent, so an unreachable server no longer delays the others. Maps written
on the send path are ConcurrentHashMaps since servers send concurrently, and LocationUpdateTrigger moves out of the
companion object so the queue and its test can import it.
Copilot AI balanced review requested due to automatic review settings October 22, 2026 08:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved timestamp-handling and zone-only history redaction issues affect location correctness and privacy.

3 open issues
What changed in this PR

This PR adds a per-server location queue to prevent overlapping updates from sending stale positions after newer ones in the Android companion app.

Changes:

  • Serializes sends per server and collapses pending fixes to the newest position.
  • Preserves geofence debounce bypass and tracks fix timestamps separately from send timing.
  • Adds tests for ordering, superseding fixes, debounce inheritance, and server independence.
File Description
app/​src/​testFull/​kotlin/​io/​homeassistant/​companion/​android/​sensors/​LocationSendQueueTest.kt Tests queue ordering and per-server behavior.
app/​src/​full/​kotlin/​io/​homeassistant/​companion/​android/​sensors/​LocationSensorManager.kt Routes location updates through the queue and adjusts send checks.
app/​src/​full/​kotlin/​io/​homeassistant/​companion/​android/​sensors/​LocationSendQueue.kt Implements serialized sends and pending-fix replacement.

Review effort: Balanced

…guard before send

The superseded-fix history row logged a null payload, which stores raw coordinates for a server configured for zone-only
location. The payload builder is now extracted from sendLocationUpdate into buildLocationPayload and the superseded path
logs the same zone-only payload the other skip rows use.

The future-timestamp check moves ahead of the queue so a future-dated fix is logged as SKIPPED_FUTURE without entering
it, where it could have displaced a valid pending fix and then been rejected at send time. It moves rather than being
duplicated: a fix cannot become future-dated while it waits.

lastSentLocationTime is recorded after the skip checks and before the network call, so an older fix queued behind an
in-flight one is still skipped when the server applies the newer update but its response is lost. lastLocationSend and
lastUpdateLocation keep updating on success only since they drive the debounce and duplicate windows.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

2 participants