Repository navigation
fix/drone alert agl gate - #68
Merged
Merged
Conversation
The max_alt_agl_ft gate read canonical.alt_baro_ft exclusively. Remote ID has no barometric altimeter, so a drone track's alt_baro_ft is always None and the gate returned False unconditionally — making every AGL-bounded rule unsatisfiable for drones. Both drone rules we document (drone_nearby, drone_conflict) carry max_alt_agl_ft, so in practice they could only ever trip on manned aircraft, which is what the live drone_nearby/active topic shows. Resolve AGL from the most direct source instead. Drones broadcast true height-above-takeoff in the Location message, which is better than the MSL-minus-watchpoint-elevation approximation we use for aircraft, so prefer it verbatim rather than reconstructing AGL from alt_geom_ft. alt_geom_ft backs up a drone whose Location message omitted AGL. Manned aircraft stay strictly baro-only. Geometric altitude is ellipsoid-referenced, so folding it into the MSL path would shift AGL by the local geoid separation and quietly move empirically tuned thresholds — out of scope for a drone fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Running the service against the live feeds and broker from a workstation is the only way to exercise a fix end-to-end without tagging a release: the add-on image is FROM the published service image, so a local add-on build still layers over old code. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The two files disagreed on drone_nearby: config.example.yaml said max_distance_nm 0.5 / max_alt_agl_ft 1000, automations.example.yaml said 0.34 / 1640 for the same rule name. Both distance bounds are tighter than a drone sitting 0.72 nm out, so a working setup looks broken. That failure is silent and expensive to diagnose: a too-tight bound doesn't error, it just never matches, which reads exactly like "no drone was detected" — it cost a full debugging session alongside the AGL bug. Remote ID reception range caps how far these can ever fire anyway, so a generous distance costs little and a tight one costs a lot. Go wide by default and tell people to measure before tightening. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
When a drone alert doesn't fire there are three indistinguishable causes: the feed is empty, the transmitter is sending Basic ID only (no Location message, so no position and no altitude — correctly matches nothing), or a rule threshold excludes it. All three look like "no drone detected". This prints what the feed actually carries per poll and labels each drone ALERTABLE vs BASIC-ID-ONLY, which separates cause two from the others immediately, and prints the real distance/AGL for sizing thresholds against. It is the tool that caught the too-tight example bounds. Read-only, stdlib only, no service dependency — it talks to the feed directly so it works whether or not ha-airspace is running. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Home Assistant sets up retained binary_sensor discovery before sensor discovery on reconnect, so an alert entity reaches `on` while sensor.airspace_nearest_drone is still `unavailable`. An automation that triggers on the alert but guards on nearest_drone therefore fails every restart-triggered edge, and the trace reads `failed_conditions` at condition/0 — indistinguishable from the alert never firing. The shipped examples are each internally consistent (#4 triggers on and reads nearest_drone; #6 triggers on and reads the alert entity), so this only bites someone who mixes #6's trigger into #4. That is an easy and natural mistake to make, and it is invisible until you pull the trace, so name it explicitly next to the trigger it applies to. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Patch: the AGL fix is a behavior correction within the existing rule grammar — no config schema change, no payload contract change, so schema_version is untouched. Manned-aircraft matching is byte-identical, so nobody's tuned thresholds move. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The default URL pinned a specific RFC1918 host, which is both wrong for anyone else and someone's real internal addressing in a public repo. Default to localhost (correct when run on the dump3411 box) and take the URL from argv or RID_URL otherwise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What was wrong
max_alt_agl_ftresolved height above ground fromcanonical.alt_baro_ftalone.Remote ID carries no barometric altitude, so a drone track's
alt_baro_ftisalways
nulland the gate returned false unconditionally — every AGL-boundedalert rule was unsatisfiable for drones.
Both drone rules in our own docs (
drone_conflict,drone_nearby) carrymax_alt_agl_ft, so in practice they could only ever fire on manned aircraft.That is what was happening in the field:
drone_nearbywason, triggered by ahelicopter.
It failed in the worst possible way — silently. No error, no log line, no
unhealthy receiver. Drones appeared correctly in
drone_countandnearest_dronethe entire time, so the pipeline looked healthy and only thealert never came.
The fix
AGL now resolves most-direct-first:
drone.agl_ft— Remote ID broadcasts true height above takeoff, which isbetter than the MSL-minus-watchpoint-elevation approximation we use for
aircraft.
alt_geom_ft— for a drone whose Location message omits AGL.alt_baro_ftminus ground elevation — unchanged for manned aircraft.Manned-aircraft matching is byte-identical. Geometric altitude is
ellipsoid-referenced, so folding it into the MSL path would shift AGL by the
local geoid separation and quietly move empirically tuned thresholds. Left
deliberately out of scope.
Verification
Reproduced against
tests/fixtures/remoteid_basic.json, then confirmedend-to-end on a live Remote ID feed: a drone at 0.72 nm and 279 ft AGL, with
alt_baro_ft: null, now matchesdrone_nearby, publishes the alert, flips theHome Assistant
binary_sensor, and fires the notification automation. The sametrack matched nothing before the fix.
Six regression tests added, including one pinning unchanged manned-aircraft
behavior.
Also in this PR
config.example.yamlanddocs/automations.example.yamlgave different values for the samedrone_nearbyrule (0.5/1000vs0.34/1640), and both distance boundsexcluded a drone under a nautical mile out. A too-tight bound fails silently —
the same class of problem as the bug itself.
scripts/watch_rid.py— prints what the feed actually carries and labelseach drone
ALERTABLEvsBASIC-ID-ONLY, so "transmitter sent no position"is distinguishable from "threshold excluded it".
sensor.airspace_nearest_drone. On reconnect Home Assistant sets up theretained
binary_sensordiscovery first, so the alert entity reachesonwhile
nearest_droneis stillunavailable; the condition then fails everyrestart-triggered edge and looks exactly like the alert never firing.
version-guardCI job, whichneeds
pyproject.tomlandaddon/config.yamlto agree and be ahead of thelast tag.
Reviewer notes
schema_versionisuntouched.
in the published release.
🤖 Generated with Claude Code