refactor(integration): typed runtime data via PriceHawkData (Phase 7 PR-1)#85
Merged
Conversation
…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>
This was referenced May 23, 2026
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
hass.data[DOMAIN][entry_id]withentry.runtime_data: PriceHawkData. Introducescustom_components/pricehawk/data.pywith the dataclass +PriceHawkConfigEntry = ConfigEntry[PriceHawkData]type alias.async_unload_entry—async_unload_platformsruns FIRST; coordinator teardown only on success.hass.config_entries.async_entries(DOMAIN)(was relying onhass.datawhich this PR removes).entry.runtime_dataon every call instead of closing over the setup-scopecoordinatorlocal. 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
custom_components/pricehawk/data.pycustom_components/pricehawk/__init__.pycustom_components/pricehawk/sensor.pyentry.runtime_datawith mypy-safeassertnarrowingtests/test_runtime_data.pyCHANGELOG.md[Unreleased] > ChangedentryDECISIONS.mdNo new dependencies. No user-facing changes. No public API or entity-id changes.
Out of scope
manifest.jsonquality_scale: silverflip (Plan 08-05 / PR-9)coordinator.pymypy errors at lines 295/1538/1570 — pre-existing baseline, boundary-protected by this PR's planAudit trail
Enterprise audit applied before APPLY — see
.paul/phases/07-foundation/07-01-AUDIT.mdfor 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 passedpytest --tb=short -q→ 815 passed (was 808 baseline; +7 new)grep -rn "hass.data\[DOMAIN\]\|hass.data.get(DOMAIN)\|hass.data.setdefault(DOMAIN" custom_components/pricehawk/→ 0grep -rn 'hasattr(entry, "runtime_data")' custom_components/pricehawk/→ 0coordinator.py— boundary-protected; zero new errors)OptionsFlowWithReloadsmoke (Configure → save no-op options → confirm reload cycle clean → confirmrank_alternativesservice still resolves)Related
🤖 Generated with Claude Code