Skip to content

Fix availability false-unavailable + add curb calibration endpoint-trim retry - #44

Merged
Pascal-ZeGerman merged 11 commits into
mainfrom
ship/availability-backstop-and-curb-calibration
Sep 11, 2026
Merged

Pascal-ZeGerman merged 11 commits into
mainfrom
ship/availability-backstop-and-curb-calibration

Conversation

@Pascal-ZeGerman

Copy link
Copy Markdown
Owner

Summary

  • Close gaps in the 7-day staleness backstop and tracker-health fast path so sensors don't falsely flip unavailable during ordinary parking, then simplify the watchdog/gate code and test stubs
  • Add endpoint_trim_ft and a fallback-retry to curb calibration, and wire the endpoint-trim retry into the index build
  • Doc updates: numbered reconfigure walkthrough, Configure vs Reconfigure clarification, docs/ landing page

Test plan

  • pytest tests/test_curb_calibration.py tests/test_sensor_availability_backstop.py tests/test_gps_pipeline_health_binary_sensor.py tests/test_build_index.py tests/test_ha_integration.py (192 passed, run during code review)
  • Manual smoke test in a running HA instance

🤖 Generated with Claude Code

Pascal-ZeGerman and others added 9 commits August 27, 2026 10:16
Fixes the root Pages URL 404 (only docs/demo and docs/explorer had
index.html). Splits the two pages by audience: demo for everyone,
explorer for maintainers/technical users. README's Live Demo section
now leads with the hosted URLs instead of local-only instructions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VQxcp5EeadKNjM5u7WAR2r
…e behavior

The device tracker picker only lives under Reconfigure (three-dot menu),
not Configure (gear icon) — easy to miss and led to confusion after the
3.3.0-rc4 reconfigure flow shipped. Also clarify that VIN/Car Name are
generic device_tracker attribute reads, not tied to any specific vehicle
brand's integration.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NVTaYxJNE3ANndgwg7LJPt
…backstop + tracker health)

The sensor flipped to unavailable after 8h of device_tracker silence even
though the resolved curb position was still correct. `last_gps_update` only
advances on a real state_changed event, and the periodic heartbeat re-resolves
from cached coordinates without touching it -- so a parked car (telematics
asleep on ignition-off, or a Shortcut-posted tracker that only fires on phone
movement) tripped the gate on every overnight park.

Silence and failure are now treated as different things:

- stale_timeout becomes a 168h (7-day) backstop for "no location event at all"
  rather than a freshness SLA. 7 days matches the NYC ASP recurrence cycle, so
  a car untouched for a week is ordinary rather than exceptional.
- New coordinator.tracker_unavailable reads the tracker's live HA state; an
  explicit unavailable/unknown is a real integration failure (dead API, expired
  token) and marks entities unavailable immediately. Read live rather than
  cached because a tracker already unavailable at HA start never fires an event.
  A MISSING entity is deliberately not a failure -- that would flap the sensor
  on every restart for manually-posted trackers.
- async_migrate_entry rewrites the shadowing legacy stale_timeout=8 stored in
  existing entries; without it the new default would reach nobody. Only the
  exact legacy value is touched.
- _async_on_gps_update pushes an entity refresh on a tracker-health transition
  so availability re-evaluates at once instead of at the next pipeline run.

The GPS-pipeline-health binary sensor also consults tracker_unavailable, since
it shares stale_timeout and would otherwise stay ON for a week after the source
integration died.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The prior table-only version made users read the whole Configure/Reconfigure
comparison before finding out which menu to click. Lead with a literal
numbered click path for the common case (swapping the GPS source device
tracker), then keep the table as a reference for what Configure covers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NVTaYxJNE3ANndgwg7LJPt
…rb calibration

- ENDPOINT_TRIM_FT = 25.0 fixed module constant (spike 008 backtested value)
- derive_segment_calibration() gains endpoint_trim_ft (default 0.0 = no-op)
- derive_segment_calibration_with_endpoint_retry(): untrimmed-first, retry once
  with the 25 ft trim only on failure, return the untrimmed baseline if both fail
…antics

- Synthetic corner-flare fixture reproduces the spike 008 mechanism offline:
  spread_n ~16.98 / spread_s ~17.70 untrimmed (rejected), c=+1.0 width=34.0
  spreads ~0 with the 25 ft trim (calibrated)
- TestEndpointTrim: constant, 0.0 default is a no-op, corner rejection,
  trim rescue, degenerate short-block collapses safely
- TestEndpointRetry: passes-untrimmed / rescued-by-trim / fails-both branches
  plus the zero-trim opt-out
- _derive_segment_fields calls derive_segment_calibration_with_endpoint_retry;
  the roadbed cross-check still runs on whatever comes back
- docstrings + inline comment name the retry and note the two-gate pipeline
- TestCurbCalibrationEndpointTrim: corner-contaminated segment (~17 ft spreads
  untrimmed) now reaches segments.json calibrated with c=+1.0, width=34.0
…fast path

The prior fix (e5a9a93) added a tracker-health fast path but only wired it
into 2 of 15 entities and left the GPS-stale watchdog notification on the
old full-stale_timeout wait, so a genuinely broken tracker still took up
to 7 days to alert the user instead of firing immediately.

- Watchdog notification now shares the same tracker-health fast path as
  entity availability, firing immediately instead of waiting stale_timeout.
- Detect a deleted/renamed device_tracker via the entity registry
  (tracker_registered/tracker_failed), distinct from "not started yet",
  so a removed entity also triggers the immediate alert.
- Consolidate the duplicated three-gate availability policy into
  coordinator.gps_data_available, shared by ASPNextMoveTimeSensor,
  ASPGpsPipelineHealthBinarySensor, ASPActiveNowBinarySensor, and the
  pipeline-derived diagnostic sensors (latitude/longitude/resolved
  street/resolution status/confidence/SODA level/last resolved); tracker
  name/VIN sensors gate on tracker health alone via a lighter mixin.
- Coalesce the v1->v2 config-entry migration into a single
  async_update_entry call instead of two.
- Extract tracker-health-transition handling out of _async_on_gps_update
  into its own method.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Follow-up cleanup pass on 8ac5637 (reuse/simplification/efficiency/altitude):

- Extract _gps_watchdog_arm_normal from _gps_watchdog_rearm so a real GPS
  location fix (which already proves the tracker isn't unavailable) arms
  the backstop directly instead of re-deriving tracker_failed, including
  an unnecessary entity-registry lookup on every location update.
- Merge the two near-duplicate coordinator test stubs in
  test_sensor_availability_backstop.py into one _StubCoordinator.
- Replace the hand-rolled gps_data_available reimplementation in
  test_gps_pipeline_health_binary_sensor.py's stub with a rebind of the
  real property, matching the sibling test file's pattern.
- Document why gps_data_available intentionally omits tracker_failed
  (entity availability stays lenient on a missing entity to avoid
  reboot-flapping; the one-shot watchdog notification does not need to)
  and the three-tier availability-gating convention for diagnostic
  sensors, so both read as deliberate design rather than oversights.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment thread tests/test_build_index.py

def _patch_downloads(self, monkeypatch):
monkeypatch.setattr(
build_index, "_download_cscl_geojson", lambda: self._make_cscl_gdf()
Comment thread tests/test_build_index.py Fixed
CI's ruff-format-check job was failing on these 3 files.
Comment thread tests/test_build_index.py
monkeypatch.setattr(
build_index, "_download_cscl_geojson", lambda: self._make_cscl_gdf()
)
monkeypatch.setattr(build_index, "_fetch_asp_signs", lambda: set())
new_state's None-ness isn't narrowed by has_gps_location alone
since has_location() isn't a TypeGuard.

if not has_gps_location:
return
assert new_state is not None # has_gps_location implies this
@Pascal-ZeGerman
Pascal-ZeGerman merged commit 7bdb0df into main Sep 11, 2026
17 checks passed
@Pascal-ZeGerman
Pascal-ZeGerman deleted the ship/availability-backstop-and-curb-calibration branch September 11, 2026 16:59
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.

2 participants