Skip to content

Add location reporting as a device_tracker - #46

Open
jlclemmons wants to merge 2 commits into
muniter:masterfrom
jlclemmons:gps-location
Open

Add location reporting as a device_tracker#46
jlclemmons wants to merge 2 commits into
muniter:masterfrom
jlclemmons:gps-location

Conversation

@jlclemmons

Copy link
Copy Markdown

Adds optional location reporting: a GeoClue2 client that posts fixes to the existing update_location webhook, which makes Home Assistant create a device_tracker for the machine so it can be used for presence and zones. This is the "roaming laptops" direction from the README's to-do list, though it does not attempt the remote/local instance part.

  • Off unless a location section sets "enabled": true.
  • No new dependencies — dbus_next is already required for notifications.
  • Existing configuration files keep working: the section and every option in it are defaulted, so a config written before this change still validates.

How well it works depends on the machine

Worth being upfront, since the answer is very different per device:

  • A machine with a GNSS receiver (a Linux phone, or a laptop with a WWAN module exposing GPS) gets fixes accurate to a few metres.
  • A typical desktop or laptop has no GNSS, so GeoClue falls back to a wifi lookup — usually tens to hundreds of metres, and dependent on the surrounding access points being in the database it queries. For a machine that never moves that may be enough for a home/away zone, but it can be wrong by a street, and it is not a replacement for a phone's tracker.

max_accuracy defaults to 500m for that reason: it rejects cell-tower-grade fixes without discarding the wifi positioning a desktop depends on. Devices with GNSS can set it far tighter.

Design notes

Two things that are less obvious than they look, both documented in the commit and the README:

  • The heartbeat re-reads GeoClue rather than resending the last payload. update_location carries no timestamp, so Home Assistant stamps whatever arrives as current. Replaying a cached fix therefore asserts a stale position indefinitely while looking freshly updated. Re-reading costs nothing, since GeoClue serves the last fix its source produced rather than powering up a receiver to answer. It exists at all because DistanceThreshold means a stationary device stops emitting entirely, which is when a lost device most needs to report.
  • Fixes are dropped when they are worse than max_accuracy or older than max_age, and nothing is sent when there is no usable fix. RequestedAccuracyLevel = EXACT is only a hint about which source GeoClue may use, not a floor on what it delivers, so coarse fixes still arrive and would otherwise be plotted as the device's position. A tracker that goes stale is honest; one that is confidently in the wrong place is not.

Similarly, distance_threshold defaults to a fairly tight 30m because the last emitted fix can be a threshold's worth of distance behind where the device actually came to rest.

Testing

  • Running on a FuriPhone FLX1s+ (FuriOS/Phosh, geoclue 2.7.1 with the hybris GNSS source) reporting to Home Assistant 2026.7 over HTTPS, currently producing 3m fixes.
  • halinuxcompanion/test_location.py adds 15 tests covering config defaults and the reporting rules, including that the heartbeat reports the current fix rather than a stale one. They drive the event loop directly instead of using a pytest-asyncio marker, since that plugin is in requirements.txt but not in the tox environment, where a marked test would silently skip.
  • Full suite passes (21 tests), and pycodestyle --max-line-length=120 is clean across the package.

Happy to adjust any of the defaults, or to gate the feature differently, if you would rather it were shaped another way.


Developed with AI assistance (Claude); the commits carry a Co-Authored-By trailer to that effect. Everything above has been reviewed and tested on real hardware.

jlclemmons and others added 2 commits July 30, 2026 17:58
The companion registers sensors but never reports position, so Home
Assistant has no device_tracker for the machine and cannot use it for
presence or zones.

Add a location module that holds a GeoClue2 client and posts each fix to the
existing update_location webhook. Home Assistant creates the device_tracker
from the first update, so no registration step is needed. It is off unless a
"location" section enables it, and adds no dependency: dbus_next is already
required for notifications.

Notes on the implementation:

- The accuracy level requested is EXACT. Lower levels rely on wifi/cell
  lookups, and where those databases have no coverage GeoClue returns no
  location at all rather than a coarse one, so anything less can silently
  never produce a fix. EXACT is only a hint about which source GeoClue may
  use, though, not a floor on what it delivers.
- Updates are driven by GeoClue's DistanceThreshold rather than a timer,
  since a GNSS fix is expensive on battery. That means the last emitted fix
  can be a threshold's worth of distance behind where the device came to
  rest, so the default is a fairly tight 30m.
- A stationary device stops emitting entirely, which is exactly when a lost
  device still needs to report, so a heartbeat reports the position when
  nothing has moved. It re-reads the client's current location rather than
  resending the last payload: update_location carries no timestamp, so Home
  Assistant stamps whatever arrives as current, and replaying a cached fix
  would assert a stale position indefinitely while looking fresh. Re-reading
  costs nothing, as GeoClue serves the last fix its source produced rather
  than powering up a receiver to answer.
- For the same reason, fixes are dropped when they are less accurate than
  max_accuracy or older than max_age, and nothing is sent at all when there
  is no usable fix. A tracker that goes stale is honest; one that is
  confidently in the wrong place is not.
- Altitude, speed and course are only included when non-zero. GeoClue
  reports 0.0 for values its source cannot determine, and Home Assistant
  would otherwise render those as real readings.
- The desktop_id must match an installed .desktop file: GeoClue's agent
  authorizes by that id, and an unknown one leaves the client started but
  never receiving a fix.

- The config section and every knob in it carry pydantic defaults rather
  than being merely Optional. Pydantic treats Optional without a default as
  required-but-nullable, so without this an existing configuration file, which
  has no location section at all, would stop validating and the application
  would refuse to start.

The README is explicit that the quality of this depends entirely on what the
machine can position with. A device with a GNSS receiver gets metres; a
desktop without one falls back to a wifi lookup that can be wrong by a
street. Hence max_accuracy defaulting to 500m, which rejects cell-tower-grade
fixes without discarding the wifi positioning such a machine depends on.

Tested on a FuriPhone FLX1s+ running FuriOS (Phosh, geoclue 2.7.1 with the
hybris GNSS source), reporting to Home Assistant 2026.7 over HTTPS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cover what the module decides rather than what GeoClue does: which fixes are
worth asserting as the device's position, and what the heartbeat reports.

The heartbeat cases are the ones worth having. Reporting the current fix
instead of replaying the last payload is the difference between a tracker
that converges on where the device stopped and one that asserts a stale
position forever, and neither is visible in Home Assistant, which stamps
whatever arrives as current either way.

The tests drive the event loop themselves rather than taking a marker from
pytest-asyncio: it is in requirements.txt but not in the tox environment, so
a marker would pass in CI and silently skip under tox.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant