Skip to content

fix/drone alert agl gate - #68

Merged
ifnull merged 7 commits into
mainfrom
fix/drone-alert-agl-gate
Sep 16, 2026
Merged

ifnull merged 7 commits into
mainfrom
fix/drone-alert-agl-gate

Conversation

@ifnull

@ifnull ifnull commented Sep 16, 2026 •

Copy link
Copy Markdown
Owner

What was wrong

max_alt_agl_ft resolved height above ground from canonical.alt_baro_ft alone.
Remote ID carries no barometric altitude, so a drone track's alt_baro_ft is
always null and the gate returned false unconditionally — every AGL-bounded
alert rule was unsatisfiable for drones
.

Both drone rules in our own docs (drone_conflict, drone_nearby) carry
max_alt_agl_ft, so in practice they could only ever fire on manned aircraft.
That is what was happening in the field: drone_nearby was on, triggered by a
helicopter.

It failed in the worst possible way — silently. No error, no log line, no
unhealthy receiver. Drones appeared correctly in drone_count and
nearest_drone the entire time, so the pipeline looked healthy and only the
alert never came.

The fix

AGL now resolves most-direct-first:

  1. drone.agl_ft — Remote ID broadcasts true height above takeoff, which is
    better than the MSL-minus-watchpoint-elevation approximation we use for
    aircraft.
  2. alt_geom_ft — for a drone whose Location message omits AGL.
  3. alt_baro_ft minus 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 confirmed
end-to-end on a live Remote ID feed: a drone at 0.72 nm and 279 ft AGL, with
alt_baro_ft: null, now matches drone_nearby, publishes the alert, flips the
Home Assistant binary_sensor, and fires the notification automation. The same
track matched nothing before the fix.

Six regression tests added, including one pinning unchanged manned-aircraft
behavior.

Also in this PR

  • Example thresholds widened and reconciled. config.example.yaml and
    docs/automations.example.yaml gave different values for the same
    drone_nearby rule (0.5/1000 vs 0.34/1640), and both distance bounds
    excluded 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 labels
    each drone ALERTABLE vs BASIC-ID-ONLY, so "transmitter sent no position"
    is distinguishable from "threshold excluded it".
  • Docs warning — an automation triggered by an alert entity must not read
    sensor.airspace_nearest_drone. On reconnect Home Assistant sets up the
    retained binary_sensor discovery first, so the alert entity reaches on
    while nearest_drone is still unavailable; the condition then fails every
    restart-triggered edge and looks exactly like the alert never firing.
  • Version bump to 1.1.6 — required by the version-guard CI job, which
    needs pyproject.toml and addon/config.yaml to agree and be ahead of the
    last tag.

Reviewer notes

  • No config schema change and no payload contract change — schema_version is
    untouched.
  • Until this is tagged and the add-on image updated, drone alerts remain broken
    in the published release.

🤖 Generated with Claude Code

ifnull and others added 7 commits September 15, 2026 21:33
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>
@ifnull
ifnull merged commit 25098b7 into main Sep 16, 2026
3 checks passed
@ifnull
ifnull deleted the fix/drone-alert-agl-gate branch September 16, 2026 05:04
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