Skip to content

release: v3.1.0-rc.6 - #4

Merged
Pascal-ZeGerman merged 170 commits into
mainfrom
release/v3.1.0-rc.6
May 5, 2026
Merged

release: v3.1.0-rc.6#4
Pascal-ZeGerman merged 170 commits into
mainfrom
release/v3.1.0-rc.6

Conversation

@Pascal-ZeGerman

Copy link
Copy Markdown
Owner

Summary

Release candidate: v3.1.0-rc.6

This PR contains 165 commits across 82 files (+6 760 / -394 lines) since v3.1.0-rc.5. It bundles all review-cycle fixes (phases 01–30) plus new feature work from phases 26–30.


Review-Cycle Fixes

Phases Key fixes
01–05 Clear last_error on OutsideNYCError/NoSegmentFoundError; fix time_window_start/end attrs to use next_window; replace extractall with per-member extract (TOCTOU); lazy asyncio.Lock; RuntimeError guard in retry loop
05–09 Add VERSION=3.1.0 to const.py; re-export IndexNotFoundError/SODA exceptions from __init__; import _NEAR_INTERSECTION_THRESHOLD_FT from confidence.py; fix async check_for_updates; tzinfo=NYC_TZ on all CleaningWindow fixtures
10–14 Narrow .gitignore to allow graph.json.zst for HA component; sync vendored pipeline.py with Stage 4 suspension; supplement asp_pids with dead-end ASP segments; setup_method/teardown_method for TestStreetGraphLoad; remove spurious async from build_index()
15–17 Fix normalize.py idempotency + dead code; fix lettered-avenue expansion (AVE E/N/S/W); audit script fixes (queens_coverage, audit_queens_coverage.py); add suffix-expansion and idempotency tests; fix Flushing→Jamaica entry
20–24 Fix _REASON_PATTERN regex; narrow resolution_reason Literal; add ha_nyc311 to emergency branch in merge.py; reorder FALLBACK_2026 Islamic New Year chronologically; fix notification dedup; _get_now() in lead-time gate; now=_get_now() in compute_schedule
25–29 Remove dead CONF_DEBUG_ENABLED; retire ASPDebugModeSensor; async_on_remove deregistration on sensors/binary sensor; async_remove_update_callback; ZipSlip path-traversal validation before extractall; fix blocking I/O in SpatialIndex._load() via asyncio.to_thread; implement ASPDebugModeSwitch
30 Fix suspension poll test; move borough attr to unconditional metadata group; replace weekly_schedule=None with WeeklySchedule(windows=()); async_on_remove on both sensor classes; fix field-count docstrings

New Features (Phases 26–30)

Feature Description
SpatialIndex.query_radius() Query all segments within a configurable radius (Phase 26-01)
Parking-area options step New parking_area config flow step; radius/area constants (Phase 26-02)
Sign-cache pre-seeding materialize_cached_records() helper + coordinator pre-seed on startup (Phase 26-03)
HA diagnostics export async_get_config_entry_diagnostics with redaction of secrets (Phase 27-02)
DIAG-04 diagnostic sensors 4 sensor classes: car name, VIN, raw segment, raw schedule (Phase 27-03)
ImportError repair lifecycle Guard + repair issue for missing gps2asp library (Phase 27-04)
UX copy strings Sync strings.jsontranslations/en.json; em-dash; American English (Phase 28)
ASPDebugModeSwitch entity Replaces ASPDebugModeSensor; WARNING-level log upgrades (Phase 29)
ResolutionResult diagnostic fields segment_id, borough, distance_ft, confidence_score (Phase 30-01)
ASPDebugResult diagnostic fields Thread diagnostic fields through all classmethods (Phase 30-02)
Coordinator borough mapping _BOROUGH_NAMES + thread borough/diagnostic fields through ASPParkingData (Phase 30-03)
Sensor diagnostic attributes Expose borough + 3 diagnostic attrs on resolved-street + next-move sensors (Phase 30-04)

Testing

  • 165 commits include dedicated test commits for every feature phase
  • autouse reset_street_graph fixture added to conftest; setup_method/teardown_method for singleton isolation
  • tzinfo=NYC_TZ applied to all CleaningWindow datetime fixtures
  • Stale and broken tests from phases 22–24 fixed in final commit

Tagging

After merge, tag v3.1.0-rc.6 to trigger the release automation.

🤖 Generated with Claude Code

Pascal-ZeGerman and others added 30 commits April 30, 2026 23:34
- Add tests/test_spatial_index_radius.py with seven tests locking the contract
- Tests cover: within-radius results, tight-vs-loose subset, zero-radius -> [],
  far-from-NYC -> [], closest-first sort, SegmentCandidate field shape, and
  RuntimeError when index not loaded
- All seven tests currently FAIL with AttributeError (RED state) — implementation
  lands in next commit
Add bounded-radius helper to enumerate every NYC street segment whose
centerline is within radius_ft of a State Plane (x, y) point. Returns
SegmentCandidate list sorted closest-first.

This is the geometry primitive the Phase 26 coordinator pre-seeder calls
to enumerate segments inside the user's configured parking area (AREA-02).

- Uses rtree intersection() with the (x±r, y±r) bounding box, then filters
  by exact point.distance(geometry) <= radius_ft (matches the canonical
  "Pattern 3" from the phase research).
- Returns [] (does NOT raise) when no segments fall within radius — the
  Plan 03 pre-seeder relies on this contract for empty-area handling.
- Method body is a near-verbatim clone of nearest() with two changes:
  (a) intersection() bbox query in place of nearest((x,y,x,y), n), and
  (b) no NoSegmentFoundError raise on empty result.
- nearest() is unchanged; existing callers and tests are unaffected.
- Seven new tests in tests/test_spatial_index_radius.py now pass (RED -> GREEN).
Copy the new query_radius() method byte-for-byte from
src/gps2asp/resolver/spatial_index.py into the HA-vendored mirror at
custom_components/asp_parking/gps2asp/resolver/spatial_index.py so the
Home Assistant integration sees the same public API.

- Method body is byte-identical to source (verified via diff of the
  function range — empty diff). No asyncio.to_thread wrapping needed
  because intersection(), wkt.loads(), and Point.distance() are all
  synchronous (matches the existing nearest() pattern in both files).
- Vendored library stays HA-free: no `from homeassistant` imports added.
- nearest(), __init__, get(), reset(), _load() are all unchanged.
- CONF_PARKING_LAT / DEFAULT_PARKING_LAT (None)
- CONF_PARKING_LON / DEFAULT_PARKING_LON (None)
- CONF_PARKING_RADIUS / DEFAULT_PARKING_RADIUS (500 m, D-06)
- All three keys are optional (D-07); None defaults follow Phase 24 debug pattern
Four ha_integration tests covering AREA-01:
- parking_area step renders after init (chain wiring)
- empty submission does NOT write CONF_PARKING_* keys (D-07)
- valid lat/lon/radius round-trip persists with correct types
- init step preserves pre-existing parking keys across saves (D-09)

All four currently fail (RED) — init step still calls async_create_entry
directly; parking_area step does not yet exist.
…(AREA-01)

Implements the new ASPParkingOptionsFlow.async_step_parking_area with three
NumberSelector fields (lat / lon / radius) using the Phase 25 conditional-default
pattern (default only set when key already present in entry.options) so the form
never crashes on first open.

- Init step now stores its built options dict in self._options and chains
  via 'return await self.async_step_parking_area()' (D-08).
- Init carry-forward tuple extended with CONF_PARKING_LAT/LON/RADIUS so a
  no-change save through init alone preserves parking values (D-09).
- parking_area save handler clears keys on blank submission and only persists
  the radius default when at least one of lat/lon is also being persisted
  (avoids the voluptuous default leaking radius=500 on a fully-blank submit).
- Both translations/en.json and strings.json gain the parking_area step block.

Deviation from plan (Rule 3 — auto-fix blocking issue): the documented
step=0.000001 for lat/lon NumberSelectors causes a NumberSelectorConfig
validation failure in HA 2026.2.3 (schema requires step >= 0.001 or the
literal 'any'). The existing async_step_debug uses the same broken value but
is bypassed (Phase 25 commit 64fbf6d) so it never crashes in production.
This step is reachable, so step='any' is used here. The async_step_debug
pattern is left untouched (out of scope for this plan).

Tests: tests/test_options_flow.py — 4 ha_integration tests now pass GREEN.
…hed_records helper

- Add public materialize_cached_records() helper to gps2asp.signs (source + vendored mirror, byte-identical bodies) so the HA cache hit path produces a SignRetrievalResult without coupling to private _deduplicate.
- Coordinator: load CONF_PARKING_LAT/LON/RADIUS from entry.options; spawn _async_preseed_cache as a lifecycle-tied background task via entry.async_create_background_task; register periodic cache rebuild on refresh_interval.
- Cache key: (on_street, from_street, to_street, side_of_street); value: list of raw SODA records.
- _async_resolve_pipeline checks cache before retrieve_signs; cache miss does NOT write back (D-04).
- Add module-level _legal_sides_for() helper to enumerate compass sides per segment nominaldir.
- D-07 fallbacks: silent skip + WARN on missing config or OutsideNYCError.
Nine async unit tests cover the Phase 26 sign-cache plumbing:
- pre-seed early-returns when parking area is unconfigured
- pre-seed converts WGS84 to State Plane and queries SpatialIndex.query_radius with radius_m * 3.28084 ft
- pre-seed populates _sign_cache keyed by (on, from, to, side) for both legal sides per segment
- OutsideNYCError swallowed; cache stays empty + WARN logged (D-07)
- resolve pipeline cache-hit short-circuits retrieve_signs
- resolve pipeline cache-miss calls retrieve_signs as before
- cache miss does NOT write back (D-04)
- periodic rebuild clears the cache and re-spawns the pre-seed task
- spawn path uses entry.async_create_background_task with name='asp_parking_preseed' (lifecycle-tied; D-03)
…cache

Import name_variants from the signs normalize module and use the SODA-normalized
name (index [0]) for build_block_query() calls in the preseed loop. Cache keys
remain the original CSCL names to match what the resolution result produces.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mirror the HA-bundled copy's double-checked locking pattern: add
_lock: ClassVar[asyncio.Lock] = asyncio.Lock() and wrap the _load()
call in 'async with cls._lock: if cls._instance is None: ...' so
concurrent awaiters cannot both trigger a double load.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace independent lat/lon persistence with an atomic pair check:
only write CONF_PARKING_LAT, CONF_PARKING_LON, and CONF_PARKING_RADIUS
to entry.options when both lat and lon are non-None. If either is missing
remove all three keys, fully disabling the feature rather than leaving
a semantically invalid half-configured state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove self._sign_cache = {} from _async_periodic_cache_rebuild. The
preseed coroutine builds a local new_cache dict and performs an atomic
swap (self._sign_cache = new_cache) at completion, so wiping the live
cache before the rebuild finishes creates a window where all lookups
miss and fall through to live SODA calls — defeating the cache entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Switch from new=AsyncMock(return_value=...) to new_callable=AsyncMock so
the 'as mock_retrieve' alias refers to the proper MagicMock wrapper, not
the AsyncMock object itself. Set mock_retrieve.return_value inside the
with block before awaiting the pipeline so the alias and assertion are
unambiguously correct.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add nyc311_entity and nyc311_api_key to options.step.init.data and
data_description in strings.json, matching the fields declared in
en.json that the options flow renders. The parking_area step was
already present; only the init step data keys were stale.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The test test_periodic_rebuild_clears_and_respawns was asserting the
old (incorrect) pre-clear behavior. Update it to verify the new correct
behavior: the live cache object is preserved during the rebuild window,
not wiped before the preseed task completes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace bare 3.28084 literal in coordinator.py with a named module-level
constant _METRES_TO_FEET and update test_coordinator_cache.py to import
and use the same constant instead of repeating the magic number.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Extend the docstring for test_query_radius_returns_empty_list_for_zero_radius
to explain that the empty result is guaranteed by the implementation's
distance_ft <= 0.0 filter, not solely by libspatialindex degenerate-box
behaviour, making the test's intent clear and the contract explicit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eve_signs

Remove the module-level import of the private symbol _find_best_covering_span
from both signs/__init__.py files (src library and HA vendored mirror).
Move the import inside the Level 4 block of retrieve_signs() so the symbol
is not exposed on the package's public namespace while still being used
internally. StreetGraph remains at module level as it is not private.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Asserts top-level keys {config, state, last_resolve, last_error}
- Asserts five sensitive option keys redact to '**REDACTED**'
- Asserts non-sensitive options pass through unchanged
- Asserts datetime fields serialise as ISO 8601 strings

All four tests fail with ModuleNotFoundError on
custom_components.asp_parking.diagnostics — the intended RED state for
Wave 0. Plan 02 will create the module and turn these GREEN.
- Asserts ImportError logs an actionable ERROR mentioning gps2asp + 'reinstall via HACS'
- Asserts ImportError creates 'gps2asp_import_error' repair issue (severity=ERROR, is_fixable=False)
- Asserts a successful setup auto-dismisses any pre-seeded repair issue (D-07)

All three tests fail because __init__.py does not yet wrap async_setup_entry
with a try/except for ImportError nor call ir.async_create_issue /
async_delete_issue. Plan 04 will wire that up and turn these GREEN.

Imports follow Pitfall #1 mitigation: use homeassistant.helpers.issue_registry
(legacy components.repairs path is intentionally avoided).

Deviation: DOMAIN moved to module-top import (Rule 3 — blocking issue). Inside
the helper function the import was failing because pytest's enable_custom_integrations
fixture invalidates the cached custom_components loader cache. Module-top import
matches the working pattern used in tests/test_options_flow.py:13-22.
- Four pure-Python helpers replicate the four new diagnostic sensor
  native_value properties (confidence_score, soda_level, last_resolved,
  last_error) and their corresponding tests pass on commit (no production
  dependency).
- A fifth ``@pytest.mark.ha_integration`` test imports the four sensor
  classes from custom_components.asp_parking.sensor; it FAILS until Plan 03
  ships those classes — the RED gate for DIAG-04's import-surface contract.

Pattern matches the file's existing convention of replicating sensor logic
locally instead of importing the HA-bound coordinator module.
Adds 27-01-SUMMARY.md documenting the 8 RED tests + 4 GREEN helper tests
authored across tests/test_diagnostics.py, tests/test_repair_issue.py, and
tests/test_ha_integration.py. Includes the full RED-state matrix, three
auto-fixed deviations (Rule 3 — blocking), forward pointers to Plans 02-04,
and self-check confirmation that all artifacts are present.
- Add ASPConfidenceScoreSensor surfacing coordinator.data.confidence_score (MEASUREMENT, mdi:gauge)
- Add ASPSODALevelSensor surfacing coordinator.data.soda_level (MEASUREMENT, mdi:layers-search)
- Add ASPLastResolvedSensor surfacing coordinator.data.last_resolved as ISO string (mdi:clock-check)
- Add ASPLastErrorSensor surfacing coordinator.data.last_error (mdi:alert-circle-outline)
- Register all four in async_setup_entry alongside existing diagnostic sensors
- Update module docstring: 6 -> 10 diagnostic sensors
- All 4 inherit from _ASPDiagnosticSensor (auto-gets EntityCategory.DIAGNOSTIC, device_info, update callback)
- test_diag04_sensor_classes_exist now GREEN (was RED in Wave 0)
- 5/5 DIAG-04 tests passing in tests/test_ha_integration.py
- Add custom_components/asp_parking/diagnostics.py
- async_get_config_entry_diagnostics returns {config, state, last_resolve, last_error}
- TO_REDACT covers parking_lat, parking_lon, debug_lat, debug_lon, nyc311_api_key
- async_redact_data wraps entry.options for D-03 GPS/credential masking
- Defensive getattr(entry, 'runtime_data', None) handles 'Setup failed' state
- Datetime fields serialised to ISO 8601 strings
- state section explicitly EXCLUDES last_lat/last_lon/_sign_cache (D-04, T-27-04, T-27-07)

Turns all 4 DIAG-01 tests GREEN:
  test_diagnostics_shape
  test_diagnostics_redacts_lat_lon
  test_diagnostics_passthrough
  test_state_section_iso_datetime

Mitigates STRIDE T-27-04 (real-time GPS leak), T-27-05 (config GPS leak),
T-27-06 (API key leak), T-27-07 (sign cache home block leak), T-27-08
(setup-failed crash). T-27-09 (entity_id) accepted per D-02.
- Add 4 entries (confidence_score, soda_level, last_resolved, last_error) to entity.sensor block in translations/en.json
- Add same 4 entries to entity.sensor block in strings.json (Pitfall #7 mitigation: kept in lock-step)
- Display names: 'Confidence Score', 'SODA Level', 'Last Resolved', 'Last Error'
- Both files remain valid JSON; trailing comma added to debug_mode (en.json) and next_move_time (strings.json) since they are no longer the last sibling
- Add 27-02-SUMMARY.md documenting DIAG-01 implementation
- All 4 tests/test_diagnostics.py tests GREEN
- TO_REDACT locked to {parking_lat, parking_lon, debug_lat, debug_lon, nyc311_api_key}
- All STRIDE mitigations confirmed: T-27-04..08 mitigated, T-27-09 accepted
- Zero deviations from plan instructions
- No regressions in full-suite run
Plan 27-03 SUMMARY documenting:
- 2 task commits (5a43db1, df6bf0e)
- 4 new diagnostic sensor classes wired through async_setup_entry
- Display names added to both translations/en.json and strings.json
- 5/5 DIAG-04 tests GREEN
- Threat-model dispositions T-27-10..T-27-13 confirmed
- No regressions (372 passed; 2 pre-existing Wave-0-documented failures unchanged)
Pascal-ZeGerman and others added 20 commits May 4, 2026 16:25
…_near_centerline_above_threshold_returns_nonzero
- test_suspension: set cal._loaded=True and add source='holiday' to match
  SuspensionInfo.source field added in a later phase
- test_diagnostics: notify_service is now redacted (CONF_NOTIFY_SERVICE in
  TO_REDACT); last_resolved/last_error_time/last_error moved to last_resolve
  and last_error sections in the restructured diagnostics output
- test_ha_integration TestNotificationLogic: add _get_now to SimpleNamespace
  mock (coordinator calls self._get_now() since phase 24 CR-02)
- prospect_heights.json: replace broken Vanderbilt coord (40.6774,-73.9694)
  with verified (40.6770,-73.9690) that resolves to VANDERBILT AVE E

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
"""
try:
self._entity_update_callbacks.remove(cb)
except ValueError:
Comment thread tests/test_audit_script.py Fixed
Comment thread tests/test_audit_script.py Fixed
Comment thread tests/test_coordinator_cache.py Fixed
Comment thread tests/test_coordinator_debug_logs.py Fixed
Comment thread tests/test_debug_switch.py Fixed
Pascal-ZeGerman and others added 2 commits May 4, 2026 22:22
Both actions were pinned to versions that no longer resolve:
- hacs/action@v1.3.4 → v2
- home-assistant/actions/hassfest@v4 → master

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- hacs.yml: bump action to @main (v1.3.4 and v2 no longer resolve)
- hassfest: reorder manifest.json keys (domain, name, then alphabetical)
- ruff: auto-fix 61 errors, manually fix 18 remaining:
  - Add SegmentCandidate to __all__ in both resolver __init__.py files
  - Add noqa: E402 to deferred suspension imports (avoid circular import)
  - Move imports above function in test_ha_integration.py
  - Move NYC_TZ assignment after imports in test_resolve_asp.py
  - Add noqa: E402 to late pathlib import in test_ha_integration.py
- pytest: add pytest.importorskip("geopandas") to test_build_index.py
  and test_graph_filter.py to skip when build deps are not installed
- ruff format: reformat 89 files to comply with format check

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

# Named constants for spatial search and ambiguity classification
_MAX_SNAP_DISTANCE_FT: float = 164.0 # ~50m: maximum snap radius for spatial index
_MAX_SNAP_DISTANCE_FT: float = 164.0 # ~50m: maximum snap radius for spatial index
# Named constants for spatial search and ambiguity classification
_MAX_SNAP_DISTANCE_FT: float = 164.0 # ~50m: maximum snap radius for spatial index
_NEAR_INTERSECTION_THRESHOLD_FT: float = 30.0 # ~10m: block-face ambiguity zone
_MAX_SNAP_DISTANCE_FT: float = 164.0 # ~50m: maximum snap radius for spatial index
Pascal-ZeGerman and others added 3 commits May 4, 2026 22:55
Both tests were hitting the real network before reaching the patched
coordinator, causing RuntimeError from pytest's DNS guard. Patching
_async_ensure_index makes the tests exercise the ImportError path only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- pyproject.toml: add [tool.mypy] overrides to suppress
  import-untyped for shapely and rtree (no bundled stubs)
- suspension/merge.py: narrow resolution_reason annotation from str
  to Literal["suspended_holiday","suspended_emergency"] to match
  the ScheduleFound/ASPActiveNow field type
- signs/client.py: inline exc.response.status_code into logger call
  (removes loop-scope variable that conflicted with post-loop
  status_code: int | None = None declaration)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously this mypy step was skipped because src/gps2asp/ errors caused
early CI failure. Now that those are fixed, the step runs and reveals
pre-existing type issues. Fixed:

- pyproject.toml: add gps2asp/gps2asp.* to ignore_missing_imports
  (avoids type identity conflict between vendored and installed copies)
- coordinator.py: add asyncio import; properly annotate _preseed_task
  and _unsub_cache_rebuild; silence callback arg-type mismatches with
  HA's EventStateChangedData; remove spurious await on sync
  async_cancel(); add None guards for _nyc311_client and
  _holiday_calendar before use
- config_flow.py: annotate options as dict[str, Any] to accept
  mixed-type values

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@Pascal-ZeGerman
Pascal-ZeGerman merged commit 6e588b5 into main May 5, 2026
13 checks passed
Pascal-ZeGerman added a commit that referenced this pull request May 6, 2026
…r load() (#3)

* test(26-01): add failing tests for SpatialIndex.query_radius()

- Add tests/test_spatial_index_radius.py with seven tests locking the contract
- Tests cover: within-radius results, tight-vs-loose subset, zero-radius -> [],
  far-from-NYC -> [], closest-first sort, SegmentCandidate field shape, and
  RuntimeError when index not loaded
- All seven tests currently FAIL with AttributeError (RED state) — implementation
  lands in next commit

* feat(26-01): add SpatialIndex.query_radius()

Add bounded-radius helper to enumerate every NYC street segment whose
centerline is within radius_ft of a State Plane (x, y) point. Returns
SegmentCandidate list sorted closest-first.

This is the geometry primitive the Phase 26 coordinator pre-seeder calls
to enumerate segments inside the user's configured parking area (AREA-02).

- Uses rtree intersection() with the (x±r, y±r) bounding box, then filters
  by exact point.distance(geometry) <= radius_ft (matches the canonical
  "Pattern 3" from the phase research).
- Returns [] (does NOT raise) when no segments fall within radius — the
  Plan 03 pre-seeder relies on this contract for empty-area handling.
- Method body is a near-verbatim clone of nearest() with two changes:
  (a) intersection() bbox query in place of nearest((x,y,x,y), n), and
  (b) no NoSegmentFoundError raise on empty result.
- nearest() is unchanged; existing callers and tests are unaffected.
- Seven new tests in tests/test_spatial_index_radius.py now pass (RED -> GREEN).

* feat(26-01): mirror query_radius() into HA-vendored spatial_index.py

Copy the new query_radius() method byte-for-byte from
src/gps2asp/resolver/spatial_index.py into the HA-vendored mirror at
custom_components/asp_parking/gps2asp/resolver/spatial_index.py so the
Home Assistant integration sees the same public API.

- Method body is byte-identical to source (verified via diff of the
  function range — empty diff). No asyncio.to_thread wrapping needed
  because intersection(), wkt.loads(), and Point.distance() are all
  synchronous (matches the existing nearest() pattern in both files).
- Vendored library stays HA-free: no `from homeassistant` imports added.
- nearest(), __init__, get(), reset(), _load() are all unchanged.

* feat(26-02): add parking-area constants for AREA-01 options step

- CONF_PARKING_LAT / DEFAULT_PARKING_LAT (None)
- CONF_PARKING_LON / DEFAULT_PARKING_LON (None)
- CONF_PARKING_RADIUS / DEFAULT_PARKING_RADIUS (500 m, D-06)
- All three keys are optional (D-07); None defaults follow Phase 24 debug pattern

* test(26-02): add failing tests for parking_area options step (AREA-01)

Four ha_integration tests covering AREA-01:
- parking_area step renders after init (chain wiring)
- empty submission does NOT write CONF_PARKING_* keys (D-07)
- valid lat/lon/radius round-trip persists with correct types
- init step preserves pre-existing parking keys across saves (D-09)

All four currently fail (RED) — init step still calls async_create_entry
directly; parking_area step does not yet exist.

* feat(26-02): add parking_area options step + chain init→parking_area (AREA-01)

Implements the new ASPParkingOptionsFlow.async_step_parking_area with three
NumberSelector fields (lat / lon / radius) using the Phase 25 conditional-default
pattern (default only set when key already present in entry.options) so the form
never crashes on first open.

- Init step now stores its built options dict in self._options and chains
  via 'return await self.async_step_parking_area()' (D-08).
- Init carry-forward tuple extended with CONF_PARKING_LAT/LON/RADIUS so a
  no-change save through init alone preserves parking values (D-09).
- parking_area save handler clears keys on blank submission and only persists
  the radius default when at least one of lat/lon is also being persisted
  (avoids the voluptuous default leaking radius=500 on a fully-blank submit).
- Both translations/en.json and strings.json gain the parking_area step block.

Deviation from plan (Rule 3 — auto-fix blocking issue): the documented
step=0.000001 for lat/lon NumberSelectors causes a NumberSelectorConfig
validation failure in HA 2026.2.3 (schema requires step >= 0.001 or the
literal 'any'). The existing async_step_debug uses the same broken value but
is bypassed (Phase 25 commit 64fbf6d) so it never crashes in production.
This step is reachable, so step='any' is used here. The async_step_debug
pattern is left untouched (out of scope for this plan).

Tests: tests/test_options_flow.py — 4 ha_integration tests now pass GREEN.

* feat(26-03): add sign cache pre-seed to coordinator + materialize_cached_records helper

- Add public materialize_cached_records() helper to gps2asp.signs (source + vendored mirror, byte-identical bodies) so the HA cache hit path produces a SignRetrievalResult without coupling to private _deduplicate.
- Coordinator: load CONF_PARKING_LAT/LON/RADIUS from entry.options; spawn _async_preseed_cache as a lifecycle-tied background task via entry.async_create_background_task; register periodic cache rebuild on refresh_interval.
- Cache key: (on_street, from_street, to_street, side_of_street); value: list of raw SODA records.
- _async_resolve_pipeline checks cache before retrieve_signs; cache miss does NOT write back (D-04).
- Add module-level _legal_sides_for() helper to enumerate compass sides per segment nominaldir.
- D-07 fallbacks: silent skip + WARN on missing config or OutsideNYCError.

* test(26-03): cover coordinator sign-cache lifecycle (AREA-02)

Nine async unit tests cover the Phase 26 sign-cache plumbing:
- pre-seed early-returns when parking area is unconfigured
- pre-seed converts WGS84 to State Plane and queries SpatialIndex.query_radius with radius_m * 3.28084 ft
- pre-seed populates _sign_cache keyed by (on, from, to, side) for both legal sides per segment
- OutsideNYCError swallowed; cache stays empty + WARN logged (D-07)
- resolve pipeline cache-hit short-circuits retrieve_signs
- resolve pipeline cache-miss calls retrieve_signs as before
- cache miss does NOT write back (D-04)
- periodic rebuild clears the cache and re-spawns the pre-seed task
- spawn path uses entry.async_create_background_task with name='asp_parking_preseed' (lifecycle-tied; D-03)

* fix(26-CR-01): normalize CSCL names to SODA format in _async_preseed_cache

Import name_variants from the signs normalize module and use the SODA-normalized
name (index [0]) for build_block_query() calls in the preseed loop. Cache keys
remain the original CSCL names to match what the resolution result produces.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(26-CR-02): add asyncio.Lock to SpatialIndex.get() in src library

Mirror the HA-bundled copy's double-checked locking pattern: add
_lock: ClassVar[asyncio.Lock] = asyncio.Lock() and wrap the _load()
call in 'async with cls._lock: if cls._instance is None: ...' so
concurrent awaiters cannot both trigger a double load.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(26-WR-01): require lat+lon pair before writing parking options

Replace independent lat/lon persistence with an atomic pair check:
only write CONF_PARKING_LAT, CONF_PARKING_LON, and CONF_PARKING_RADIUS
to entry.options when both lat and lon are non-None. If either is missing
remove all three keys, fully disabling the feature rather than leaving
a semantically invalid half-configured state.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(26-WR-02): remove premature _sign_cache clear in periodic rebuild

Remove self._sign_cache = {} from _async_periodic_cache_rebuild. The
preseed coroutine builds a local new_cache dict and performs an atomic
swap (self._sign_cache = new_cache) at completion, so wiping the live
cache before the rebuild finishes creates a window where all lookups
miss and fall through to live SODA calls — defeating the cache entirely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(26-WR-03): fix patch() as-alias pattern in cache miss test

Switch from new=AsyncMock(return_value=...) to new_callable=AsyncMock so
the 'as mock_retrieve' alias refers to the proper MagicMock wrapper, not
the AsyncMock object itself. Set mock_retrieve.return_value inside the
with block before awaiting the pipeline so the alias and assertion are
unambiguously correct.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(26-WR-04): sync strings.json options schema with en.json

Add nyc311_entity and nyc311_api_key to options.step.init.data and
data_description in strings.json, matching the fields declared in
en.json that the options flow renders. The parking_area step was
already present; only the init step data keys were stale.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(26-WR-02): update test to assert cache is preserved during rebuild

The test test_periodic_rebuild_clears_and_respawns was asserting the
old (incorrect) pre-clear behavior. Update it to verify the new correct
behavior: the live cache object is preserved during the rebuild window,
not wiped before the preseed task completes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(26-IN-01): extract _METRES_TO_FEET constant in coordinator

Replace bare 3.28084 literal in coordinator.py with a named module-level
constant _METRES_TO_FEET and update test_coordinator_cache.py to import
and use the same constant instead of repeating the magic number.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(26-IN-02): document zero-radius degenerate-box contract in test

Extend the docstring for test_query_radius_returns_empty_list_for_zero_radius
to explain that the empty result is guaranteed by the implementation's
distance_ft <= 0.0 filter, not solely by libspatialindex degenerate-box
behaviour, making the test's intent clear and the contract explicit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(26-IN-03): move _find_best_covering_span to local import in retrieve_signs

Remove the module-level import of the private symbol _find_best_covering_span
from both signs/__init__.py files (src library and HA vendored mirror).
Move the import inside the Level 4 block of retrieve_signs() so the symbol
is not exposed on the package's public namespace while still being used
internally. StreetGraph remains at module level as it is not private.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(27-01): add 4 failing DIAG-01 tests for diagnostics export shape

- Asserts top-level keys {config, state, last_resolve, last_error}
- Asserts five sensitive option keys redact to '**REDACTED**'
- Asserts non-sensitive options pass through unchanged
- Asserts datetime fields serialise as ISO 8601 strings

All four tests fail with ModuleNotFoundError on
custom_components.asp_parking.diagnostics — the intended RED state for
Wave 0. Plan 02 will create the module and turn these GREEN.

* test(27-01): add 3 failing DIAG-02/03 tests for repair issue lifecycle

- Asserts ImportError logs an actionable ERROR mentioning gps2asp + 'reinstall via HACS'
- Asserts ImportError creates 'gps2asp_import_error' repair issue (severity=ERROR, is_fixable=False)
- Asserts a successful setup auto-dismisses any pre-seeded repair issue (D-07)

All three tests fail because __init__.py does not yet wrap async_setup_entry
with a try/except for ImportError nor call ir.async_create_issue /
async_delete_issue. Plan 04 will wire that up and turn these GREEN.

Imports follow Pitfall #1 mitigation: use homeassistant.helpers.issue_registry
(legacy components.repairs path is intentionally avoided).

Deviation: DOMAIN moved to module-top import (Rule 3 — blocking issue). Inside
the helper function the import was failing because pytest's enable_custom_integrations
fixture invalidates the cached custom_components loader cache. Module-top import
matches the working pattern used in tests/test_options_flow.py:13-22.

* test(27-01): append 4 DIAG-04 helper tests + 1 RED import-surface test

- Four pure-Python helpers replicate the four new diagnostic sensor
  native_value properties (confidence_score, soda_level, last_resolved,
  last_error) and their corresponding tests pass on commit (no production
  dependency).
- A fifth ``@pytest.mark.ha_integration`` test imports the four sensor
  classes from custom_components.asp_parking.sensor; it FAILS until Plan 03
  ships those classes — the RED gate for DIAG-04's import-surface contract.

Pattern matches the file's existing convention of replicating sensor logic
locally instead of importing the HA-bound coordinator module.

* docs(27-01): complete Wave 0 diagnostics test scaffolding plan

Adds 27-01-SUMMARY.md documenting the 8 RED tests + 4 GREEN helper tests
authored across tests/test_diagnostics.py, tests/test_repair_issue.py, and
tests/test_ha_integration.py. Includes the full RED-state matrix, three
auto-fixed deviations (Rule 3 — blocking), forward pointers to Plans 02-04,
and self-check confirmation that all artifacts are present.

* feat(27-03): add 4 DIAG-04 diagnostic sensor classes + register them

- Add ASPConfidenceScoreSensor surfacing coordinator.data.confidence_score (MEASUREMENT, mdi:gauge)
- Add ASPSODALevelSensor surfacing coordinator.data.soda_level (MEASUREMENT, mdi:layers-search)
- Add ASPLastResolvedSensor surfacing coordinator.data.last_resolved as ISO string (mdi:clock-check)
- Add ASPLastErrorSensor surfacing coordinator.data.last_error (mdi:alert-circle-outline)
- Register all four in async_setup_entry alongside existing diagnostic sensors
- Update module docstring: 6 -> 10 diagnostic sensors
- All 4 inherit from _ASPDiagnosticSensor (auto-gets EntityCategory.DIAGNOSTIC, device_info, update callback)
- test_diag04_sensor_classes_exist now GREEN (was RED in Wave 0)
- 5/5 DIAG-04 tests passing in tests/test_ha_integration.py

* feat(27-02): implement HA diagnostics export with redaction

- Add custom_components/asp_parking/diagnostics.py
- async_get_config_entry_diagnostics returns {config, state, last_resolve, last_error}
- TO_REDACT covers parking_lat, parking_lon, debug_lat, debug_lon, nyc311_api_key
- async_redact_data wraps entry.options for D-03 GPS/credential masking
- Defensive getattr(entry, 'runtime_data', None) handles 'Setup failed' state
- Datetime fields serialised to ISO 8601 strings
- state section explicitly EXCLUDES last_lat/last_lon/_sign_cache (D-04, T-27-04, T-27-07)

Turns all 4 DIAG-01 tests GREEN:
  test_diagnostics_shape
  test_diagnostics_redacts_lat_lon
  test_diagnostics_passthrough
  test_state_section_iso_datetime

Mitigates STRIDE T-27-04 (real-time GPS leak), T-27-05 (config GPS leak),
T-27-06 (API key leak), T-27-07 (sign cache home block leak), T-27-08
(setup-failed crash). T-27-09 (entity_id) accepted per D-02.

* feat(27-03): add DIAG-04 entity display names to translations

- Add 4 entries (confidence_score, soda_level, last_resolved, last_error) to entity.sensor block in translations/en.json
- Add same 4 entries to entity.sensor block in strings.json (Pitfall #7 mitigation: kept in lock-step)
- Display names: 'Confidence Score', 'SODA Level', 'Last Resolved', 'Last Error'
- Both files remain valid JSON; trailing comma added to debug_mode (en.json) and next_move_time (strings.json) since they are no longer the last sibling

* docs(27-02): complete HA diagnostics export plan

- Add 27-02-SUMMARY.md documenting DIAG-01 implementation
- All 4 tests/test_diagnostics.py tests GREEN
- TO_REDACT locked to {parking_lat, parking_lon, debug_lat, debug_lon, nyc311_api_key}
- All STRIDE mitigations confirmed: T-27-04..08 mitigated, T-27-09 accepted
- Zero deviations from plan instructions
- No regressions in full-suite run

* docs(27-03): complete DIAG-04 diagnostic sensors plan

Plan 27-03 SUMMARY documenting:
- 2 task commits (5a43db1, df6bf0e)
- 4 new diagnostic sensor classes wired through async_setup_entry
- Display names added to both translations/en.json and strings.json
- 5/5 DIAG-04 tests GREEN
- Threat-model dispositions T-27-10..T-27-13 confirmed
- No regressions (372 passed; 2 pre-existing Wave-0-documented failures unchanged)

* chore: sync worktree to expected base 1098c31

* feat(27-04): add ImportError guard + repair lifecycle to async_setup_entry

- Import homeassistant.helpers.issue_registry as ir (NOT broken components.repairs)
- Add _IMPORT_ERROR_ISSUE_ID = 'gps2asp_import_error' module constant
- Auto-dismiss stale repair issue at top of async_setup_entry (D-07)
- Wrap ASPParkingCoordinator instantiation in try/except ImportError (D-06)
- On ImportError: log actionable message, create repair issue, raise ConfigEntryNotReady
- All 3 DIAG-02/03 tests in tests/test_repair_issue.py turn GREEN

* feat(27-04): add 'issues' translation block for gps2asp_import_error

- Add top-level 'issues.gps2asp_import_error' to strings.json (title + description)
- Add identical block to translations/en.json (HA reads this at runtime; Pitfall #7)
- Title and description match in both files
- Plan 03 entity.sensor entries (confidence_score, soda_level, last_resolved, last_error) preserved

* docs(27-04): complete ImportError repair-issue lifecycle plan

* fix(28-01): remove personal notify.mobile_app_yourphone example from en.json

- Replace notify_service data_description with generic copy (D-08)
- Removes user-specific 'mobile_app_yourphone' substring; HACS-safe
- Single-line edit; JSON structure preserved

* fix(28-01): sync strings.json with translations/en.json

- Drop dead config.step.vehicle (no matching step_id in config_flow.py)
- Replace VW-era user-step copy with generic 'Select Vehicle' + device_tracker
- Add config.step.api_keys (NYC 311) and config.error blocks
- Add options.step.init/debug descriptions + options.error block
- Strip unit suffixes from data labels; units now live in data_description
- Add 7 missing entity.sensor translation keys (car_name, vin, latitude,
  longitude, resolved_street, resolution_status, debug_mode)
- Apply D-08 generic notify_service copy (matches en.json post-Task-1)

After this commit, strings.json and translations/en.json are byte-identical
(cmp -s reports MATCH). Closes UX-01..UX-04.

* docs(28-01): complete UX copy strings plan

- Sync strings.json with translations/en.json (byte-equivalent)
- Strip personal notify.mobile_app_yourphone example from both files
- SUMMARY.md captures decisions D-01..D-09 and pre-existing
  test_is_suspended_holiday failure deferred to a follow-up plan

Closes UX-01..UX-04.

* fix(28): WR-02 replace double-hyphen with em dash in UI copy

* fix(28): WR-01 WR-03 normalize parking_radius to American English and soften env-var guidance

* feat(29-01): add switch to PLATFORMS in const.py

- Append "switch" to PLATFORMS list so HA platform discovery imports
  the new switch.py and calls its async_setup_entry
- Pre-req for ASPDebugModeSwitch entity (Phase 29 / DBG-01)

* refactor(29-02): retire ASPDebugModeSensor (D-07)

- Remove ASPDebugModeSensor class definition from sensor.py
- Drop ASPDebugModeSensor from async_setup_entry list (12 → 11 entities)
- Update module docstring to list 9 diagnostic sensors (was 10)

Replaced by switch.asp_parking_debug_mode entity from Plan 29-01
which now owns the canonical debug-mode control surface.

* test(29-01): add failing tests for coordinator debug-log + alias contract

- async_update_listeners() public alias must exist (D-03)
- async_start must initialize _debug_enabled = False unconditionally (D-02)
- Main-loop OutsideNYCError handler must log WARNING (D-10, D-13)
- Main-loop NoSegment/Ambiguous handler must log WARNING (D-11, D-13)
- Pre-seeder OutsideNYCError WARNING must remain unchanged (D-12)
- CONF_DEBUG_ENABLED / DEFAULT_DEBUG_ENABLED imports must be dropped

* refactor(29-02): scope async_step_debug to overrides only (D-04, D-06)

- Remove CONF_DEBUG_ENABLED form field + user_input write from
  async_step_debug (master toggle now lives on the switch entity)
- Remove CONF_SUPPRESS_NOTIFICATIONS form field + user_input write
- Drop unused DEFAULT_DEBUG_ENABLED and DEFAULT_SUPPRESS_NOTIFICATIONS
  imports from .const

The async_step_init carry-forward block is preserved verbatim
(D-06): both CONF_DEBUG_ENABLED and CONF_SUPPRESS_NOTIFICATIONS still
ride through entry.options on re-save so existing installs do not
lose persisted values.

* i18n(29-02): rename debug step + add switch translation key (D-05, D-08, D-09)

- Rename options.step.debug.title to "GPS & Time Overrides" (D-05)
- Restate description to focus on overrides only
- Drop debug_enabled and suppress_notifications data + data_description
  keys (debug step is now overrides-only per D-04)
- Remove entity.sensor.debug_mode key (D-08): sensor was retired in
  Task 1 and replaced by switch.asp_parking_debug_mode
- Add entity.switch.debug_switch with name "Debug Mode" (D-09)
- Apply identical edits to strings.json and translations/en.json so
  they remain byte-equivalent (Phase 28 invariant)

* feat(29-01): D-02/D-03/D-10/D-11/D-13 coordinator debug refactor + WARNING upgrades

- async_start now sets self._debug_enabled = False unconditionally on
  every HA restart (D-02). Drops the legacy entry.options.get(CONF_DEBUG_ENABLED)
  read; switch.py becomes the sole runtime setter for the in-memory flag.
- Drops CONF_DEBUG_ENABLED and DEFAULT_DEBUG_ENABLED imports (now unused).
- Adds public async_update_listeners() alias delegating to the existing
  private _async_notify_entities (D-03 / PATTERNS.md note 2). The new
  switch platform calls this after mutating _debug_enabled.
- Upgrades two main-loop logger.info calls to logger.warning with
  actionable user-facing messages (D-10, D-11, D-13):
    - OutsideNYCError handler now warns with 'check that your device
      tracker is reporting a valid NYC location'.
    - NoSegmentFoundError/AmbiguousResolutionError handler now warns
      with 'check that your device tracker is reporting accurate
      coordinates within a mapped NYC street'.
- Pre-seeder OutsideNYCError WARNING (Phase 26) is left unchanged (D-12).
- Refines RED test expectations after observing the existing __init__
  uses an annotated form (self._debug_enabled: bool = False).

* test(29-01): add failing tests for ASPDebugModeSwitch contract

Specifies the full DBG-01 entity contract before implementation:
- unique_id pattern, translation_key, icon, has_entity_name, entity_category
- initial is_on reflects coordinator._debug_enabled
- async_turn_on/off mutate coordinator._debug_enabled and call
  coordinator.async_update_listeners() (D-01, D-03)
- async_turn_on/off do NOT mutate entry.options (D-01)
- extra_state_attributes returns exactly {debug_lat, debug_lon, debug_datetime}
  with no suppress_notifications (D-09)
- async_added_to_hass registers async_write_ha_state with the coordinator
- DeviceInfo groups the switch under the existing ASP Parking Monitor device
- async_setup_entry adds exactly one switch entity from entry.runtime_data

* docs(29-02): complete debug step + ASPDebugModeSensor retirement plan

- 29-02-SUMMARY.md captures the three task commits (sensor, config_flow,
  i18n), confirms async_step_init carry-forward block was preserved
  byte-identically (D-06), and confirms strings.json/translations/en.json
  remain byte-equivalent (Phase 28 invariant).
- deferred-items.md logs a pre-existing test_ha_integration baseline
  failure unrelated to this plan (deselected during verification).

Closes DBG-02 portion of Phase 29.

* feat(29-01): implement ASPDebugModeSwitch entity (DBG-01 / D-01 D-03 D-09)

- New custom_components/asp_parking/switch.py exposes ASPDebugModeSwitch,
  a writable HA SwitchEntity that toggles coordinator._debug_enabled
  in-memory (no entry.options write per D-01).
- async_setup_entry pulls the coordinator from entry.runtime_data and
  registers exactly one switch entity.
- async_turn_on / async_turn_off mutate the coordinator flag and call
  the new public coordinator.async_update_listeners() alias to push the
  state change to all registered entities (D-03).
- extra_state_attributes mirrors the retired ASPDebugModeSensor's three
  GPS/time override fields (debug_lat, debug_lon, debug_datetime) and
  intentionally excludes the suppress_notifications flag (D-09).
- Entity is EntityCategory.DIAGNOSTIC with translation_key 'debug_switch'
  and icon mdi:bug; unique_id follows the existing
  f'{entry.entry_id}_{suffix}' convention.
- Replaces the 5-click options-flow toggle from Phase 24 with a one-tap
  dashboard control. Debug mode resets to False on every HA restart by
  design (D-02; coordinator change in the previous commit).

* docs(29-01): complete debug switch + WARNING logs plan

SUMMARY.md captures the full execution of Phase 29-01 (DBG-01 + DBG-03):
- Three tasks executed atomically with TDD red/green pairs (Tasks 2 + 3)
- 23 new unit tests cover the switch + coordinator contract
- Three Rule 1 deviations documented (over-fitted plan acceptance grep
  patterns, all corrected without functional impact)
- Two pre-existing test failures (test_is_suspended_holiday and
  TestSuspensionPoll::test_suspension_poll_does_not_require_gps_coordinates)
  reproduced against base commit and deferred (see deferred-items.md)
- requirements-completed: [DBG-01, DBG-03]; DBG-02 lands in Plan 29-02

* fix(29): WR-01 remove CONF_DEBUG_ENABLED carry-forward in config_flow

Remove CONF_DEBUG_ENABLED from the async_step_init carry-forward loop
and from the import block. The coordinator unconditionally sets
_debug_enabled = False on async_start (D-02) and never reads this key
from options, so writing it forward is misleading.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(29): WR-02 remove dead if self._debug_enabled block in async_start

_debug_enabled is set unconditionally to False at async_start (D-02),
making the if self._debug_enabled guard at line 307 permanently False.
Remove the entire dead block to eliminate the maintenance hazard of a
developer believing the startup log is still functional.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(29): WR-03 add async_remove_update_callback and deregister on remove

Add a public async_remove_update_callback() method to the coordinator
and clear _entity_update_callbacks in async_stop() to prevent stale
closures accumulating across integration reloads. Wire the removal into
ASPDebugModeSwitch.async_added_to_hass() via async_on_remove() so the
callback is cleaned up automatically when the entity is removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(29): WR-04 preserve suppress_notifications in async_step_debug

The debug step form omits the BooleanSelector for suppress_notifications.
Without an explicit carry-forward, submitting the debug step would silently
drop suppress_notifications from entry.options, causing unwanted push
notifications during debug sessions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(29): IN-01 remove dead CONF_DEBUG_ENABLED and DEFAULT_DEBUG_ENABLED from const.py

Both constants are unreferenced after Phase 29: coordinator no longer reads
CONF_DEBUG_ENABLED (D-02), config_flow.py no longer imports it (WR-01 fix),
and no production code references DEFAULT_DEBUG_ENABLED. Remove both to
eliminate dead definitions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(29): IN-02 add maintainer comment to test_coordinator_debug_logs.py

Document that these tests inspect coordinator.py source text directly
and that log message reformatting requires updating assertion strings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(30-01): add failing tests for ResolutionResult diagnostic fields

- 4 new tests covering borocode, perpendicular_distance_ft, street_width_ft, segment_id
- Test 1: default None values when constructed without new fields
- Test 2: explicit values round-trip
- Test 3: resolve_segment populates fields from best candidate (mocked SpatialIndex)
- Test 4: vendored mirror parity for ResolutionResult shape
- RED gate: AttributeError on first test (no .borocode attribute yet)

* feat(30-01): add diagnostic fields to ResolutionResult and populate in resolve_segment

- ResolutionResult gains 4 optional fields (D-05): borocode, perpendicular_distance_ft,
  street_width_ft, segment_id — all default None for backwards compatibility (D-04)
- resolve_segment populates all 4 from best.borocode, round(perp_distance, 2),
  effective_width, best.segment_id (D-06) — values already in scope
- Vendored mirror under custom_components/asp_parking/gps2asp/resolver/ kept
  byte-identical for models.py; mirror __init__.py extends call body identically
  while preserving the existing relative-import block (D-15)
- Test fixture geometry tweaked (200ft segment + 10ft perp offset) so
  resolve_segment runs to the success branch instead of tripping the
  near-centerline / near-intersection ambiguity guards

* docs(30-01): complete ResolutionResult diagnostic fields plan

- Add SUMMARY.md documenting RED/GREEN cycle, mirror parity, and the
  one Rule-3 deviation (test fixture geometry tweak so resolve_segment
  reaches the success branch instead of tripping the near-centerline
  ambiguity guard).
- Add deferred-items.md noting the pre-existing
  test_suspension::test_is_suspended_holiday failure (verified out of scope
  via git stash; Plan 30-01 does not touch suspension code).

* test(30-02): add failing tests for ASPDebugResult diagnostic field threading

- Add tests/test_asp_debug_result_extended_fields.py with 6 tests covering
  the 4 new diagnostic fields (borocode, perpendicular_distance_ft,
  street_width_ft, segment_id) per D-04, D-07, D-08, D-15.
- Test 1: ASPDebugResult exposes the 4 fields as top-level attributes.
- Test 2: from_resolution() reads them from the ResolutionResult argument.
- Test 3: from_resolution() forwards None values intact when resolution
  fields are None.
- Test 4: from_error() sets all 4 fields to None.
- Test 5 (negative): ASPResult does NOT gain the 4 fields (D-08).
- Test 6: vendored mirror exposes the same 4 fields (D-15 parity check).

RED gate confirmed: 5/6 fail with AttributeError on ASPDebugResult.borocode
or TypeError when constructing with the new kwargs; the negative ASPResult
test passes correctly.

* feat(30-02): add diagnostic fields to ASPDebugResult and thread through classmethods

- ASPDebugResult gains 4 new top-level optional fields (D-07): borocode,
  perpendicular_distance_ft, street_width_ft, segment_id — exposed
  separately from the nested resolution field so coordinator/sensor
  callers do not have to unwrap the nested object.
- Fields default to None to satisfy frozen-dataclass field-ordering
  rules (existing fields are positional/no-default; new fields appended
  with defaults).
- from_resolution() classmethod now reads all 4 fields off the
  ResolutionResult argument and threads them onto the result (D-07).
- from_error() classmethod sets all 4 fields to None on the
  resolution-failure path (D-04, D-07).
- ASPResult (lean variant) is intentionally untouched (D-08) — these
  fields are debug-only.
- Vendored mirror under custom_components/asp_parking/gps2asp/api_models.py
  is byte-for-byte identical to src (D-15) — diff returns no output.
- Docstring extended with the 4 new attribute descriptions.

GREEN gate: all 6 tests in tests/test_asp_debug_result_extended_fields.py
pass; full fast suite green except the pre-existing
test_suspension::test_is_suspended_holiday failure already documented in
the phase deferred-items.md.

* docs(30-02): complete ASPDebugResult diagnostic fields plan

- Add 30-02-SUMMARY.md documenting RED/GREEN cycle, ASPDebugResult
  signature change (13 → 17 fields), classmethod call-body extensions,
  ASPResult-unchanged confirmation (D-08), mirror byte-identity
  confirmation (D-15), and the one Rule-3 deviation (PYTHONPATH override
  required for pytest to import the worktree's src/gps2asp/ instead of
  the editable install pointing at the project root).
- No deferred-items.md entry needed; the pre-existing test_suspension
  failure is already tracked from Plan 30-01.

* test(30-03): add failing tests for coordinator borough mapping and field population

- 8 new tests in tests/test_coordinator_borough_fields.py (RED gate for Plan 30-03)
- Covers: _BOROUGH_NAMES constant existence (Test 1), ASPParkingData
  field declaration (Test 2), success-branch borocode->name mapping for
  Brooklyn/Manhattan (Tests 3-4), unmapped borocode and None borocode
  coalesce (Tests 5-6), error-branch field reset for OutsideNYCError and
  NoSegmentFoundError (Tests 7-8).
- Tests fail at collection (ImportError on _BOROUGH_NAMES) — confirms RED
  gate before any implementation work.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(30-03): add _BOROUGH_NAMES and thread diagnostic fields through ASPParkingData

- New module-level constant _BOROUGH_NAMES maps CSCL borocode str ('1'..'5')
  to human-readable borough name (D-12).
- ASPParkingData gains 4 optional diagnostic fields with None defaults
  (D-10): borough, distance_ft, street_width_ft, segment_id.
- _async_resolve_pipeline() success branch populates all 4 fields from the
  ResolutionResult, using _BOROUGH_NAMES.get(resolution.borocode or '') so
  None or unmapped borocodes coalesce safely to borough=None (D-09, D-11).
- OutsideNYCError and NoSegmentFoundError/AmbiguousResolutionError handlers
  reset all 4 new fields to None to avoid stale data leaking across
  resolutions (mirrors existing soda_level=0 reset pattern).
- Generic Exception handler intentionally untouched: it never resets
  soda_level either (last-known-state fallback for unexpected errors).
- All 8 RED tests now pass (GREEN); fast suite shows zero new regressions
  (only the pre-existing test_suspension::test_is_suspended_holiday
  failure remains, deferred since Plan 30-01).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs(30-03): complete coordinator borough mapping and diagnostic field plan

- Add Phase 30 Plan 03 SUMMARY documenting the _BOROUGH_NAMES constant,
  ASPParkingData 4-field extension (borough/distance_ft/street_width_ft/
  segment_id), success-branch population block, and OutsideNYCError +
  NoSegmentFoundError reset blocks.
- Records test count delta (+8), TDD gate compliance (RED 3a51eb3 → GREEN
  ce32de4), and confirms resolve() call site is unchanged per D-09.
- Self-check PASSED: 8 tests pass, fast suite shows zero new regressions
  (only pre-existing test_suspension::test_is_suspended_holiday remains,
  deferred since Plan 30-01).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(30-04): expose borough and 3 diagnostic attributes on resolved-street + next-move sensors

- ASPResolvedStreetSensor.extra_state_attributes now returns 8 keys total
  (4 existing: from_street, to_street, side_of_street, confidence_score
  + 4 new: borough, distance_ft, street_width_ft, segment_id) read from
  coordinator.data per D-13.
- Return type widened to dict[str, str | float | int | None] (segment_id is int).
- ASPNextMoveTimeSensor.extra_state_attributes['borough'] is now populated
  from data.borough; the prior hardcoded None placeholder and
  'Not in current pipeline output' comment are removed (D-14).
- Test helper sensor_extra_attributes() in test_ha_integration.py mirrors
  the production change at line 178.
- Local ASPParkingData mirror gains the 4 Phase 30 diagnostic fields so the
  helper read of data.borough resolves (Rule 3 — blocking for the helper).
- Two new focused HA tests cover both sensor branches and use the vendored
  ScheduleFound class (the sensor imports it from custom_components/.../gps2asp/,
  not from src/gps2asp/, so isinstance against the canonical class would not
  match).

* docs(30-04): complete sensor diagnostic attributes plan with SUMMARY

* docs(30-04): add SUMMARY.md for sensor diagnostic attributes plan

* fix(30): WR-01 fix suspension poll test to assert self._get_now().date()

The test was asserting for 'datetime.now(NYC_TZ).date()' which does not
exist in coordinator.py — the coordinator uses self._get_now().date().
Updated assertion to match actual implementation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(30): WR-02 move borough attr to unconditional metadata group in sensor

borough was silently absent when special_state was set or schedule_result
was None. Moved attrs["borough"] = data.borough to the Metadata group so
it is always present (None when unresolved), consistent with confidence_score,
sign_count and soda_level. Updated sensor_extra_attributes test helper to match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(30): WR-03 replace weekly_schedule=None with WeeklySchedule(windows=())

The VendoredScheduleFound fixture was constructed with weekly_schedule=None
which violates the non-Optional WeeklySchedule type. Fixed by importing and
using VendoredWeeklySchedule(windows=()) instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(30): WR-04 add async_on_remove deregistration to both sensor classes

_ASPDiagnosticSensor and ASPNextMoveTimeSensor registered callbacks via
async_add_update_callback but never deregistered them. Added async_on_remove
to both async_added_to_hass methods so stale callbacks do not accumulate
when entities are removed during integration reload.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(30): IN-01 clarify SegmentCandidate.borocode docstring to string type

The SegmentCandidate docstring implied integer values for borocode while the
field is declared as str. Updated both src/ and vendored copies to say
'Borough code as string' with quoted values.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(30): IN-02 update ASPDebugResult docstring field count from 13 to 17

Phase 30 added four new fields (borocode, perpendicular_distance_ft,
street_width_ft, segment_id), making the total 17. Updated the module
docstring in both src/ and vendored copies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(30): IN-03 remove fragile field-count claim from _make_segment_candidate docstring

The '13 required fields' claim would silently become stale if SegmentCandidate
gains or loses fields. Replaced with a count-independent description.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(25-29): CR-01 fix blocking I/O in src spatial_index._load() via asyncio.to_thread

* fix(25-29): CR-02 add async_on_remove deregistration to ASPActiveNowBinarySensor

* fix(25-29): CR-03 add ZipSlip path-traversal validation before extractall

* fix(25-29): WR-01 pop _DOWNLOAD_TASK_KEY from hass.data after download completes

* fix(25-29): WR-02 dismiss asp_parking_index_error notification at start of _async_ensure_index

* fix(25-29): WR-03 simplify options flow API key clear logic from elif to else

* fix(25-29): WR-04 skip empty Level-1 results in cache pre-seeder to allow L2/L3/L4 fallback

* fix(25-29): WR-05 add missing Phase 30 resolution fields to test mocks in test_coordinator_cache.py

* fix(25-29): WR-06 reset CONF_SUPPRESS_NOTIFICATIONS to False instead of carrying forward

* fix(25-29): IN-01 remove duplicate last_resolved/last_error/last_error_time from state section in diagnostics

* fix(25-29): IN-02 add self.hass guard to ASPCarNameSensor and ASPVINSensor native_value

* fix(25-29): IN-03 document source-text inspection limitation with IN-03 note in test docstrings

* fix(20): CR-01 add ha_nyc311 to emergency branch in merge.py

* fix(20): WR-01 narrow resolution_reason Literal to assigned values only

* fix(20): WR-02 fix _REASON_PATTERN regex to not require trailing whitespace

* fix(20): IN-01 remove unused field import from schedule/models.py

* fix(20): IN-02 reorder FALLBACK_2026 Islamic New Year to chronological position

* fix(21): CR-01 catch ValueError from response.json() to preserve fail-open contract

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(21): WR-02 fix misleading retry-in log message on final attempt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(21): WR-03 normalize empty-string api_key to None before env var fallback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(21): IN-01 add NYC311AuthError to suspension package __all__

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(22): CR-01 apply suspension in binary sensor extra_state_attributes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(22): WR-01 rename _suppress_notifications to _debug_suppress_notifications

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(22): WR-02 default API key field to empty string in options flow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(22): WR-03 use .get() for CONF_NYC311_ENTITY option access

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(22): WR-04 add last_notified_window to test ASPParkingData mirror

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(22): WR-05 add TestNotificationLogic test class

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(22): IN-01 replace %-I strftime with portable lstrip("0") approach

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(22): IN-03 import CONF_NYC311_API_KEY from const in test instead of redefining

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(23): CR-01 fix _async_initial_311_fetch to allow 311 call when bridge is unavailable

* fix(23): WR-01 add info log when bridge off overrides holiday suspension at startup

* fix(23): WR-02 fix _bridge_state_to_info attributes type annotation to Mapping

* fix(23): WR-03 add TestNyc311Bridge test class with 7 bridge path tests

* fix(24): CR-01 pass now=_get_now() to compute_schedule for debug datetime support

* fix(24): CR-02 use _get_now() in notification lead-time gate

* fix(24): CR-03 delete dead async_step_debug from options flow

* fix(24): WR-01 fix notification dedup to compare scheduling-identity fields only

* fix(24): WR-02 use debug coordinates in async_force_resolve and _async_periodic_refresh when no real GPS

* fix(24): WR-03 add CONF_NOTIFY_SERVICE to diagnostics redaction set

* fix(24): IN-02 add suppress_notifications label to translations debug step

* fix(15): CR-01 return early from lettered-avenue expansion to prevent AVE E/N/S/W -> AVENUE EAST/NORTH/SOUTH/WEST

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(15): WR-01 remove unused ASPDebugResult and AmbiguousResolutionError imports from audit script

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(15): WR-02 skip L3 diagnostic for failed-resolution rows with empty on_street

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(15): IN-01 add regression tests for AVE E/N/S/W Brooklyn lettered avenues

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(15): IN-02 iterate all soda_level keys in print_report instead of hardcoded list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(17): CR-01 WR-01 WR-02 IN-01 IN-02 normalize.py idempotency + docstring + dead code

- 17-CR-01: extend _LETTERED_AVE_RE to match AVENUE [A-Z] so normalize_to_soda
  is idempotent for already-expanded SODA names (AVENUE E -> AVENUE E, not
  AVENUE EAST); prevents broken cross-street matching in Brooklyn lettered avenues
- 15-WR-02: after Step 3 expands directional suffix, re-check second-to-last
  token for suffix abbreviation (handles OCEAN AVE E -> OCEAN AVENUE EAST)
- 15-IN-01/17-IN-02: remove dead stripped_rest = rest.lstrip() / if stripped_rest
  guard in Step 1; rest cannot have leading spaces after whitespace collapse
- 17-WR-01: update normalize_to_soda() docstring to list Step 0 in expansion
  order section with examples for AVE E and AVENUE E
- 17-IN-01: update _LETTERED_AVE_RE comment to mention Brooklyn lettered avenues
  (AVE E, N, S, W) in addition to Manhattan East Village (AVE A-D)

Both src and custom_components copies updated; byte-for-byte identical.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(15-16): WR-01 WR-02 IN-04 / WR-01 WR-03 IN-02 audit_queens_coverage.py fixes

- 15-WR-01: move desc = loc["description"] inside try block so a missing
  "description" key is handled by the same except handler as lat/lon
- 15-IN-04: add encoding="utf-8" to open(fixture_path) for cross-platform safety
- 16-WR-01: replace soda_level==0 check with isinstance(result.sign_result,
  NoMatchFound) to skip L3 diagnostics for NoASPSigns (found but no ASP),
  avoiding unnecessary extra SODA API calls; import NoMatchFound added
- 16-WR-03: log exceptions in diagnose_l3 before returning [] so operator
  can distinguish network failure from legitimate no-span result
- 16-IN-02: fix mixed pre-init + .get() pattern for counts dict: use
  counts[level] += 1 when key exists, otherwise create new key explicitly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(16-17): CR-01 WR-02 IN-01 / WR-02 geocode_fixtures.py safety + logging fixes

- 16-CR-01: abort write when all geocodes fail -- add guard before opening
  output file; exits with error rather than silently writing empty [] and
  destroying existing fixture data
- 16-WR-02: guard against null geometry in GeoSearch response by checking
  feature.get("geometry") and geometry.get("coordinates") before access;
  prints actionable warning instead of opaque TypeError
- 16-IN-01: add assertion that _BOROUGH_ADDRESSES and _BOROUGH_LABELS keys
  are in sync at module load time; catches new-borough omissions immediately
- 17-WR-02: log returned geocoded label after successful geocode so operator
  can spot mis-geocoded addresses during fixture generation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(15-17): IN-02 CR-01 add missing suffix expansion and idempotency tests

- 15-IN-02: add 6 tests for suffix expansions that had no coverage:
  CT (COURT), PKWY (PARKWAY), EXPY (EXPRESSWAY), HWY (HIGHWAY),
  SQ (SQUARE), CIR (CIRCLE)
- 17-CR-01: add 4 regression tests for normalize_to_soda idempotency with
  already-expanded AVENUE [E|N|S|W] forms (must not produce AVENUE EAST etc.)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(16): WR-04 correct queens_coverage.json entry #4 from Flushing to Jamaica

Entry index 3 (source address "109-35 168th Street, Jamaica, NY") had
geocoded to "35-35 168 STREET, Flushing" (lat 40.763968, lon -73.797592).
Replace with Jamaica-area coordinates (lat 40.7018, lon -73.8045) and
update description to "109-35 168 STREET, Jamaica, NY, USA" so the fixture
correctly represents Jamaica neighborhood coverage.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(15): IN-03 add test_audit_script.py for print_report() logic

Adds unit tests for the non-trivial logic in audit_queens_coverage.py:
- Level count accumulation per soda_level
- L1+2 percentage computation
- Error row counting (separate from level counts)
- Zero-division guard for empty results list
- Fixture name in header output
- Unexpected soda_level surfaced in output (not silently dropped)

No network access required; tests use synthetic results dicts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(10-14): CR-01 sync vendored pipeline.py with Stage 4 suspension annotation

* fix(10-14): WR-01 narrow .gitignore to allow graph.json.zst for HA component

* fix(10-14): WR-02 import _filter_2hop_neighborhood from build_index instead of duplicating

* fix(10-14): WR-03 remove spurious async from build_index() and asyncio.run() wrapper

* fix(10-14): WR-04 add missing assertion to test_graph_get_is_singleton

* fix(10-14): WR-05 supplement asp_pids with dead-end ASP segments missing from intersection_index

* fix(10-14): IN-01 update NoMatchFound docstring to say four fallback levels

* fix(10-14): IN-02 update ASPDebugResult soda_level docstring to include level 4

* fix(10-14): IN-03 add setup_method/teardown_method to TestStreetGraphLoad for singleton isolation

* fix(10-14): IN-04 record graph.json.zst size and segment count in build_info.json

* fix(10-14): IN-05 update README Known Limitations with Phase 11/14 coverage and percentages

* fix(05-09): CR-01 add tzinfo=NYC_TZ to all CleaningWindow datetime fixtures

* fix(05-09): WR-01 IN-04 remove async from check_for_updates and drop asyncio import

* fix(05-09): WR-02 add IndexNotFoundError to docstring and re-export SODA/index exceptions from __init__

* fix(05-09): WR-03 add VERSION=3.1.0 to const.py and use it for sw_version in sensor device_info

* fix(05-09): WR-04 import _NEAR_INTERSECTION_THRESHOLD_FT from confidence.py, remove duplicate definition

* fix(05-09): IN-01 rename test_near_centerline_below_threshold to test_near_centerline_above_threshold_returns_nonzero

* fix(05-09): IN-02 correct docstrings in TestIsConfident to say default threshold is 0.33 not 0.6

* fix(05-09): IN-03 correct build command in run_pipeline.py docstring to python scripts/build_index.py

* fix(05-09): IN-05 clarify materialize_cached_records docstring: caller must pre-filter BROOM signs

* fix(01-05): CR-01 replace assert with explicit RuntimeError guard in retry loop

* fix(01-05): CR-02 lazy asyncio.Lock initialization to avoid pre-event-loop creation

* fix(01-05): WR-01 acknowledge nominaldir unused in determine_side docstring and body

* fix(01-05): WR-02 remove configure_logging call in test, use caplog.at_level only

* fix(01-05): WR-03 add autouse reset_street_graph fixture to conftest.py

* fix(01-05): WR-04 document cross==0 centerline edge case in determine_side

* fix(01-05): WR-05 use next_window for time_window_start/end sensor attributes

* fix(01-05): WR-06 replace extractall with per-member extract to eliminate TOCTOU

* fix(01-05): WR-07 clear last_error on OutsideNYCError and NoSegmentFoundError

* fix(01-05): IN-01 correct docstring and log message to 8-calendar-day lookahead

* fix(01-05): IN-02 document source_sign vs source_signs provenance difference in merge.py

* test: fix stale and broken tests from phases 22-24

- test_suspension: set cal._loaded=True and add source='holiday' to match
  SuspensionInfo.source field added in a later phase
- test_diagnostics: notify_service is now redacted (CONF_NOTIFY_SERVICE in
  TO_REDACT); last_resolved/last_error_time/last_error moved to last_resolve
  and last_error sections in the restructured diagnostics output
- test_ha_integration TestNotificationLogic: add _get_now to SimpleNamespace
  mock (coordinator calls self._get_now() since phase 24 CR-02)
- prospect_heights.json: replace broken Vanderbilt coord (40.6774,-73.9694)
  with verified (40.6770,-73.9690) that resolves to VANDERBILT AVE E

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(suspension): ensure HolidayCalendar.is_suspended() works before explicit load()

https://claude.ai/code/session_01UjqY6jvvrEVtpWJ4QA2Gho

* test(suspension): fix test_is_suspended_normal and add before-load warning test

- Add explicit source='none' to test_is_suspended_normal assertion to verify
  the loaded non-holiday path (not the coincidentally-matching unloaded path)
- Add test_is_suspended_before_load_warns to assert the warning fires and
  source='none' is returned when is_suspended() is called before load()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: bump hacs/action to @main and hassfest to @master

* test: skip build-dep tests when geopandas not installed

* fix(ruff): auto-fix unused imports, f-strings, and reformat

* fix(ruff): add SegmentCandidate to __all__, noqa E402 on deferred imports, exclude .claude worktree

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(codeql): remove unused _MAX_SNAP_DISTANCE_FT, add comment to empty except

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): manifest key order, mypy type errors, repair issue test setup

- manifest.json: sort keys alphabetically after domain/name (hassfest)
- pyproject.toml: add [tool.mypy] overrides for shapely/rtree stubs
- suspension/merge.py: narrow resolution_reason to Literal type
- signs/client.py: inline status_code to fix no-redef mypy error
- test_repair_issue.py: patch _async_ensure_index in import-error tests
  so setup reaches ASPParkingCoordinator before DNS is blocked in CI

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ruff): reformat signs/client.py after ternary restructure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(mypy): resolve custom_components/asp_parking type errors

- Add asyncio import; type _preseed_task/unsub_cache_rebuild properly
- Guard _nyc311_client.fetch_status() and _holiday_calendar.is_suspended()
  with None checks (were reachable when clients not configured)
- Suppress arg-type on async_track_state_change_event handlers (HA stubs
  use EventStateChangedData but handlers use generic Event)
- Suppress misc on Debouncer.async_cancel() (stubs type it as non-async)
- Suppress assignment on config_flow options dict (mixed str/numeric values)
- Add gps2asp.* mypy override (installed without py.typed marker)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
@Pascal-ZeGerman
Pascal-ZeGerman deleted the release/v3.1.0-rc.6 branch May 7, 2026 14:47
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