feat(providers): add OpenElectricity wholesale-price client (Phase 7 PR-2)#86
Merged
Conversation
…PR-2) Standalone async client for OpenElectricity v4 — not yet wired into the coordinator or config flow (that's PR-2 part 2 / Plan 07-02b). Foundation for Phase 8 Silver-compliance handlers and Phase 9 external-statistics integration. * ``custom_components/pricehawk/providers/openelectricity.py`` — new module exposing ``OpenElectricityPriceSource`` async client + ``WholesalePrice`` frozen dataclass. CC BY-NC 4.0 attribution baked into every result per research §1.3. * ``manifest.json:requirements`` pinned to ``openelectricity>=0.10.1,<0.11`` — minor-bounded per the SDK README's "currently under active development" notice. Audit invariants applied (07-02-AUDIT.md): * M1 — API key never appears in ``__repr__`` output or log messages. ``_scrub()`` filters the full key plus its 8-char prefix from any text passed to ``_LOGGER.warning``. ``test_log_scrubs_api_key_from_sdk_error`` uses ``caplog`` to verify against a leaky SDK exception. * M2 — ``asyncio.timeout(_FETCH_TIMEOUT_SECONDS=30.0)`` bounds every SDK call. Hung connection → ``None`` + WARNING, never an HA-wide stall. * M3 — Missing ``openelectricity`` install raises ``ConfigEntryNotReady`` (HA retries setup), not ``ImportError`` (permanent crash). Lazy import inside the async method. * S1 — Auth-error detection is belt-and-braces: message-string match plus exception-class-name probe. Defensive against the SDK's active-development error-message format drift. * S2 — HTTP 429 rate-limit has a distinct branch: ``None`` + WARNING saying "rate-limited", last-good cache preserved, no spurious ``ConfigEntryAuthFailed``. * S5 — Module is intentionally NOT exported from ``providers/__init__.py``. ``OpenElectricityPriceSource`` is a wholesale-price source, not a ``Provider`` Protocol implementation. Consumers import directly. Implementation diverged from plan pseudo-code: real ``openelectricity==0.10.1`` ``TimeSeriesResponse`` is nested three levels deep (``data`` → ``results`` → ``data``), not flat. ``_extract_latest_for_region`` walks the nested structure and returns tz-aware UTC. We skip ``response.to_records()`` because it emits naive-local timestamps. 12 contract tests (``tests/test_openelectricity_provider.py``) cover AC-1 through AC-7e: happy-path NEM, happy-path WEM, attribution-verbatim, 401 mapping, generic-error cache preservation, empty-data, invalid region, empty API key, timeout, missing SDK, 429 rate limit, repr redaction, log scrub. Test suite: 815 → 827 passed. ``tests/conftest.py`` gained one stub: ``ConfigEntryAuthFailed`` (was only ``ConfigEntryNotReady`` before). Necessary because the new module is the first user of the auth-failed exception in this project. Decisions D-P7-5 through D-P7-8 logged to DECISIONS.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 20, 2026
7 tasks
This was referenced May 23, 2026
Artic0din
added a commit
that referenced
this pull request
May 23, 2026
…CI (#143) Four findings from a 2026-05-23 @claude retro-review of merged PRs. - providers/openelectricity.py: ``_is_auth_error`` no longer matches the bare word ``forbidden`` in exception messages — TLS/corporate-proxy errors with that word were mis-classified as ``ConfigEntryAuthFailed``, prompting users to re-enter their API key when the real problem is connectivity. Auth detection now keys on auth-specific message tokens (401, unauthorized, invalid api key); the class-name check still catches genuine ``*Forbidden*`` exception types. New regression test test_tls_error_with_forbidden_word_is_not_auth_failure. (Retro #86) - tests/test_tariff_engine_hypothesis.py: three property tests used ``if k <= threshold: return`` to early-exit out-of-range draws, which Hypothesis treats as PASSED rather than discarded. With max_examples=200 and ~75% of draws hitting the early return, the effective constrained-region coverage was ~50 examples per invariant instead of 200. Switched to ``assume(k > threshold)`` / ``assume(k < threshold)`` / ``assume(k >= threshold)`` so Hypothesis discards and regenerates until 200 valid examples are spent in the interesting region. Tolerance also aligned to 1e-9 across invariants 2 + 3 (single bounded-float multiplications have round-trip error < ~4e-12, so the tight value is safe). (Retro #102) - .github/workflows/dual-loop-review.yml: dropped ``develop`` from the pull_request branches filter — ``gh api repos/.../branches`` confirms no ``develop`` branch exists; it was a stale alias from the template that silently never matched. Filter now ``[main, dev]``. (Retro #103) PR #84 review noted no issues (after_dependencies fix verified correct); no change here for that PR. Full test suite: 1119 passing locally.
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
custom_components/pricehawk/providers/openelectricity.pyfor OpenElectricity v4 wholesale-price queries.openelectricity>=0.10.1,<0.11in manifest (minor-bounded per the SDK's "active development" notice).Phase 7 / PR-2 of the v2 Silver Compliance + OpenElectricity roadmap. Consumers in 07-02b + Phase 8 reauth/diagnostics handlers will pull
OpenElectricityPriceSourceoffentry.runtime_data(introduced in PR-1).Scope
custom_components/pricehawk/providers/openelectricity.pycustom_components/pricehawk/manifest.jsontests/test_openelectricity_provider.pytests/conftest.pyConfigEntryAuthFailedstubCHANGELOG.md[Unreleased] > AddedentryDECISIONS.mdAudit-required invariants (07-02-AUDIT.md)
__repr__or logs__repr__redaction +_scrub()filter +test_log_scrubs_api_key_from_sdk_errorviacaplogasyncio.timeout(_FETCH_TIMEOUT_SECONDS=30.0)ConfigEntryNotReadytry/except ImportErrorinside the async method0.10.1(re-checked at APPLY time before staging)AuthFailedproviders/__init__.pyexclusionImplementation note — plan pseudo-code diverged from real SDK
Plan template assumed a flat
response.datashape. Realopenelectricity==0.10.1TimeSeriesResponseis nested 3 levels deep (data→results→data). Caught via Context7 cross-verification + venv introspection BEFORE implementation._extract_latest_for_regionwalks the nested structure and returns tz-aware UTC. We skipresponse.to_records()because it emits naive-local timestamps.Out of scope (deferred)
coordinator.pypre-existing mypy errors (boundary-protected — Phase 8/11 cleanup)Test plan
ruff check custom_components/pricehawk/providers/openelectricity.py tests/test_openelectricity_provider.py→ all checks passedpytest --tb=short -q→ 827 passed (was 815; +12 new tests)mypy custom_components/pricehawk/providers/openelectricity.py --ignore-missing-imports→ clean (pre-existingcoordinator.pybaseline preserved)_LOGGERreferences toself._api_key_FETCH_TIMEOUT_SECONDSandasyncio.timeoutused at the SDK call siteConfigEntryNotReadyimported and raised on missing SDKopenelectricity/OpenElectricity*references inproviders/__init__.py🤖 Generated with Claude Code