Skip to content

refactor(integration): typed runtime data via PriceHawkData (Phase 7 PR-1)#85

Merged
Artic0din merged 1 commit into
devfrom
feat/typed-runtime-data
May 20, 2026
Merged

refactor(integration): typed runtime data via PriceHawkData (Phase 7 PR-1)#85
Artic0din merged 1 commit into
devfrom
feat/typed-runtime-data

Conversation

@Artic0din
Copy link
Copy Markdown
Owner

Summary

  • Replaces hass.data[DOMAIN][entry_id] with entry.runtime_data: PriceHawkData. Introduces custom_components/pricehawk/data.py with the dataclass + PriceHawkConfigEntry = ConfigEntry[PriceHawkData] type alias.
  • Reorders async_unload_entryasync_unload_platforms runs FIRST; coordinator teardown only on success.
  • Multi-entry service deregistration now uses hass.config_entries.async_entries(DOMAIN) (was relying on hass.data which this PR removes).
  • Fixes a pre-existing latent bug — service handlers now re-resolve the coordinator from entry.runtime_data on every call instead of closing over the setup-scope coordinator local. Without this, OptionsFlowWithReload (HA 2026.3+) leaves registered handlers pointing at the dead coordinator after a reload.

Phase 7 / PR-1 of the v2 Silver Compliance + OpenElectricity roadmap. Foundation for Phase 8 reauth/reconfigure/diagnostics handlers — they all consume entry.runtime_data.

Scope

File Change Lines
custom_components/pricehawk/data.py new 21
custom_components/pricehawk/__init__.py typed signatures + runtime_data write + reordered unload + service-handler closure refactor + multi-entry sentinel 173 → 209
custom_components/pricehawk/sensor.py reader uses entry.runtime_data with mypy-safe assert narrowing 940 → 950
tests/test_runtime_data.py new — 7 contract tests 330
CHANGELOG.md [Unreleased] > Changed entry +4
DECISIONS.md D-P7-1 through D-P7-4 logged +28

No new dependencies. No user-facing changes. No public API or entity-id changes.

Out of scope

  • OpenElectricity provider (Plan 07-02 / PR-2)
  • NEMWeb DISPATCH fallback (Plan 07-03 / PR-3)
  • API-key-gated comparator pricing (Plan 07-04 / PR-4)
  • Silver-compliance handlers — reauth / reconfigure / diagnostics / repairs (Phase 8)
  • manifest.json quality_scale: silver flip (Plan 08-05 / PR-9)
  • coordinator.py mypy errors at lines 295/1538/1570 — pre-existing baseline, boundary-protected by this PR's plan

Audit trail

Enterprise audit applied before APPLY — see .paul/phases/07-foundation/07-01-AUDIT.md for the 6-section reviewer record (verdict: conditionally acceptable; 5 must-have + 6 strongly-recommended findings applied inline; 1 deferred to Phase 11). Audit Gaps #1, #2, #4, #10 all bundled into this PR.

Test plan

  • ruff check custom_components/pricehawk/ tests/test_runtime_data.py → all checks passed
  • pytest --tb=short -q815 passed (was 808 baseline; +7 new)
  • grep -rn "hass.data\[DOMAIN\]\|hass.data.get(DOMAIN)\|hass.data.setdefault(DOMAIN" custom_components/pricehawk/ → 0
  • grep -rn 'hasattr(entry, "runtime_data")' custom_components/pricehawk/ → 0
  • Mypy baseline preserved (6 pre-existing errors in coordinator.py — boundary-protected; zero new errors)
  • Manual HA reload smoke (deploy to HA box → restart → reload integration → confirm sensors populate)
  • Manual multi-entry smoke (add second PriceHawk entry → unload first → confirm services still respond → unload second → confirm services unregister)
  • Manual OptionsFlowWithReload smoke (Configure → save no-op options → confirm reload cycle clean → confirm rank_alternatives service still resolves)

Related

🤖 Generated with Claude Code

…PR-1)

Retire ``hass.data[DOMAIN][entry_id]`` for coordinator storage. Introduce
``custom_components/pricehawk/data.py`` with ``PriceHawkData`` dataclass and
``PriceHawkConfigEntry = ConfigEntry[PriceHawkData]`` type alias. The coordinator
now lives on ``entry.runtime_data``.

Bundled architectural fixes surfaced by the migration:

* ``async_unload_entry`` reordered: ``async_unload_platforms`` runs FIRST. On
  failure, coordinator + runtime_data are left intact so HA can retry. Previously
  the coordinator was torn down before checking whether platform-unload succeeded.

* Multi-entry service deregistration is now sourced from
  ``hass.config_entries.async_entries(DOMAIN)`` with explicit filter of the
  unloading entry. The previous sentinel (``if not hass.data.get(DOMAIN)``)
  became unreachable garbage after ``hass.data[DOMAIN]`` was removed —
  production-breaking for any user with two PriceHawk entries.

* Service handlers (``analyze_csv``, ``backfill_history``, ``rank_alternatives``)
  now re-resolve the coordinator via ``_resolve_coordinator()`` on every
  invocation, instead of closing over the setup-scope ``coordinator`` local.
  Pre-existing latent bug: ``OptionsFlowWithReload`` (HA 2026.3+) replaces the
  entry's coordinator on options-save reload, but a closure-captured handler
  kept pointing at the dead instance. Now version-safe across reloads.

* ``sensor.py`` uses ``assert data is not None`` to narrow ``Optional[PriceHawkData]``
  for mypy and loud-fail any test fixture that violates the platform-setup
  lifecycle.

No user-facing change. Foundation for Phase 8 Silver-compliance handlers
(reauth, reconfigure, diagnostics) which all consume ``entry.runtime_data``.

7 regression tests added in ``tests/test_runtime_data.py``:
* ``test_setup_writes_runtime_data``
* ``test_unload_runs_platform_unload_first``
* ``test_unload_does_not_touch_hass_data``
* ``test_multi_entry_service_lifecycle``
* ``test_options_flow_reload_cycle``
* ``test_service_handlers_resolve_fresh_coordinator``
* ``test_no_legacy_hass_data_reads`` (static grep against legacy patterns)

Test suite: 808 → 815 passed.

Decisions logged: D-P7-1 (typed-entry alias), D-P7-2 (handler re-resolution),
D-P7-3 (unload reorder), D-P7-4 (multi-entry sentinel).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Artic0din Artic0din merged commit bdf6f74 into dev May 20, 2026
2 checks passed
@Artic0din Artic0din deleted the feat/typed-runtime-data branch May 22, 2026 05:54
Artic0din added a commit that referenced this pull request May 23, 2026
… Jinja (#145)

Two real bugs surfaced by a Copilot-CLI retro-review of the 22 merged PRs
that the prior @claude batch couldn't reach (OIDC workflow-validation
against stale main).

statistics.py — external_statistic_id now sanitizes via regex:
  CDR-derived provider_ids carry the plan id verbatim
  (e.g. ``agl_AGL-CDR-N0001`` for AGL via CDR), so `.lower()` alone
  left hyphens that the recorder's ``[a-z0-9_]+`` regex silently
  rejected. Every CDR user's dual-write would fail and the Energy
  Dashboard would never receive their cost data — same silent-failure
  class #107 fixed for uppercase ULIDs. Added
  ``_STATISTIC_ID_OBJECT_SAFE`` compiled regex that coerces ANY
  character outside ``[a-z0-9_]`` to underscore. New regression test
  ``test_cdr_plan_id_with_hyphens_is_sanitized`` + adjusted
  ``test_entry_id_sliced_to_8_chars`` for the post-sanitization shape.
  Surfaced by retro-review of PRs #93, #95.

blueprints — variables block replaces !input inside Jinja:
  ``daily_7pm_summary.yaml`` and ``wholesale_spike_alert.yaml`` had
  templates like ``{{ states(!input today_cost_sensor) }}``. ``!input``
  is a YAML tag (resolved at YAML parse time) and is invalid inside
  Jinja ``{{ }}`` — Jinja parses ``!`` as an invalid operator and the
  template never renders. Replaced with the standard HA pattern: a
  ``variables:`` block at action level that binds blueprint inputs as
  Jinja identifiers (``today_cost_entity: !input today_cost_sensor``
  etc.), then ``{{ states(today_cost_entity) }}`` in the message.
  Surfaced by retro-review of PRs #99, #100.

22 Copilot reviews ran (PRs #85, #87-#101, #104, #105, #108-#111).
Most findings were false positives — Copilot flagged ``ServiceValidationError``
and ``async_items`` as broken HA APIs (they're both fine), and several
findings duplicated bugs already fixed by codex P0/P1 work
(token-in-URL #109, DWT reset #109). Findings library + triage notes
archived in ``.planning/copilot-retro/``.

Full test suite: 1120 passing.
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