Skip to content

Phase 3.1 prep — EME refdata2 registry + brand disambiguation#54

Merged
Artic0din merged 2 commits into
phase-3-multi-planfrom
phase-3-1-prep-eme-refdata2
May 16, 2026
Merged

Phase 3.1 prep — EME refdata2 registry + brand disambiguation#54
Artic0din merged 2 commits into
phase-3-multi-planfrom
phase-3-1-prep-eme-refdata2

Conversation

@Artic0din
Copy link
Copy Markdown
Owner

@Artic0din Artic0din commented May 16, 2026

Summary

PriceHawk's retailer list comes from a public registry of who supports the Consumer Data Right (CDR) energy API. This PR switches the registry source from a community-maintained list (78 retailers) to the official Energy Made Easy refdata service (117 retailers) and fixes a bug where multiple retailers sharing one address couldn't be told apart.

What was broken

  • We only knew about 78 of Australia's 117 CDR-enrolled retailers. Users on smaller brands (Indigo, Sonnen, RAA Energy, Cooperative Power, MYOB Energy, etc.) couldn't pick their actual retailer from the dropdown.
  • 14 retailers share one CDR address with up to 6 sibling brands (Energy Locals hosts ARCLINE / RAA / Cooperative / Indigo / Sonnen / iO; OVO hosts MYOB + OVO + OVO CTM). When the wizard asked the address for a plan list, the server didn't know which brand to return, so users got the wrong retailer's plans.
  • The old registry had 2 known wrong addresses (ARCLINE and iO Energy pointed at addresses that didn't exist) which caused those retailers to silently fail.

What this fixes

  • 117 retailers now appear in the picker.
  • Each retailer carries a brand discriminator so the wizard appends ?brand=arcline (or cooperative, raa etc) and gets the correct plan list back.
  • 2 known address bugs disappear because EME provides correct addresses.
  • Hardened against future EME bugs: a known upstream trailing-space bug on cdrBrand is now stripped; malformed payloads fall back to the bundled offline snapshot instead of crashing the wizard.

Test plan

  • pytest — 658 passing (18 new for this PR)
    • EME envelope parsing (single org, fallback to orgName, missing-cdrCode skip, trailing-whitespace strip)
    • Shared-base-URI disambiguation (Energy Locals hosting 3 brands → distinct cdr_brand, same base_uri)
    • Baked-in registry health (file exists, parses, contains big-4 retailers, cdr_brand populated)
    • Async fetch: happy path, non-200, network error, malformed body all behave correctly
    • cdr_client: ?brand=<x> appended when set on plan list + plan detail, omitted when None
  • ruff check — clean on all changed files

Changes

Registry rewrite (cdr/registry.py)

  • Single source: EME refdata2 (api.energymadeeasy.gov.au/refdata2?keys=organisations,thirdParties)
  • RetailerEndpoint adds cdr_brand: str | None field
  • Parser strips trailing whitespace on cdrBrand
  • Parser failures raise CdrUnavailable so get_registry() falls through to baked-in
  • jxeeno parser, fetch helper, and live URL removed

HTTP client (cdr/cdr_client.py)

  • fetch_plan_list(brand=...) appends ?brand=<x> when set
  • fetch_plan_detail(brand=...) appends ?brand=<x> when set
  • Backwards-compatible: omit brand= and behavior is unchanged

Config flow (config_flow.py)

  • All 4 plan-fetch call sites pass retailer.cdr_brand
  • Docstring updated to reference EME

User-facing strings

  • Error message updated to mention EME instead of jxeeno (strings.json, translations/en.json)

Data

  • cdr/data/eme_refdata.json — 86KB EME snapshot (117 orgs, 72 brokers)
  • cdr/data/cdr_endpoints.json deleted (legacy jxeeno snapshot)

Why

This is the foundation for Phase 3.1 (multi-plan ranking). Without cdr_brand we'd recommend the wrong plan to anyone on a shared-base-URI brand. Without 117-retailer coverage, we'd silently exclude ~40 retailers from the comparison. Both are correctness bugs that have to be fixed before ranking math runs against arbitrary user-selected plans.

Breaking Changes

None for users — the wizard UX is unchanged.

For code:

  • LIVE_REGISTRY_URL_JXEENO, _parse_jxeeno_entries, parse_entries_for_test, baked_in_jxeeno_path_for_test removed. No internal consumers, but if any external code imported the legacy names they will break.
  • cdr/data/cdr_endpoints.json removed.

Files Changed

  • CHANGELOG.md[Unreleased] section
  • custom_components/pricehawk/cdr/cdr_client.pybrand= param on both fetchers
  • custom_components/pricehawk/cdr/registry.py — full rewrite, EME-only
  • custom_components/pricehawk/cdr/data/eme_refdata.json — new 86KB snapshot
  • custom_components/pricehawk/cdr/data/cdr_endpoints.json — deleted
  • custom_components/pricehawk/config_flow.py — 4 call sites pass retailer.cdr_brand
  • custom_components/pricehawk/strings.json — error message
  • custom_components/pricehawk/translations/en.json — error message
  • tests/test_cdr_registry.py — rewritten for EME shape, +9 new tests
  • tests/test_cdr_client.py — +4 brand-param tests

Stacked on #28. Retarget to dev after #28 merges.

🤖 Generated with Claude Code

Summary by Sourcery

Switch the CDR retailer registry to the official Energy Made Easy (EME) refdata2 source and plumb brand discriminators through to plan fetches to correctly handle retailers sharing a base URI.

New Features:

  • Carry a CDR brand discriminator on retailer endpoints and propagate it to plan list/detail requests to disambiguate shared-base-URI retailers.

Enhancements:

  • Replace the legacy jxeeno-based retailer registry with the EME refdata2 API, including a baked-in offline snapshot and more robust envelope parsing and error handling.
  • Update the CDR config flow to reference the new EME registry and to pass retailer brand discriminators into plan selection steps.
  • Adjust retailer registry tests and CDR client tests to cover the EME schema, offline snapshot health, error fallbacks, and brand query parameter behavior.

Documentation:

  • Refresh changelog and user-facing error strings to reference the EME registry instead of the retired jxeeno source.

Chores:

  • Add an EME refdata2 snapshot JSON file to the repository and remove the legacy jxeeno snapshot file.

Summary of Changes

Key Changes

  • Registry source migration: Replaced community-maintained CDR retailer registry (jxeeno) with official Energy Made Easy (EME) refdata2 source, now fetching from api.energymadeeasy.gov.au/refdata2?keys=organisations,thirdParties
  • Brand discriminator added: New cdr_brand: str | None field in RetailerEndpoint to disambiguate retailers sharing the same base CDR URI
  • CDR client updates: Extended fetch_plan_list() and fetch_plan_detail() to accept optional brand parameter, which appends ?brand=<value> to CDR requests when provided
  • Config flow enhancement: Updated plan selection steps in both main config flow and options flow to pass retailer's cdr_brand to CDR API calls (four call sites total)
  • Baked-in snapshot: Added offline EME snapshot at cdr/data/eme_refdata.json containing 117 organizations as fallback when live fetch fails
  • Parser improvements: Strips trailing whitespace from cdrBrand field; catches and logs parse failures as CdrUnavailable to ensure graceful fallback to offline snapshot
  • Error messaging: Updated user-facing error strings in strings.json and translations/en.json to reference EME service instead of jxeeno/GitHub
  • Test coverage: Added 18 new tests covering EME parsing, shared base-URI disambiguation, fallback behavior, and brand query parameter composition; 658 tests passing overall
  • Legacy cleanup: Removed legacy cdr/data/cdr_endpoints.json and jxeeno snapshot references

Breaking Changes

None for end users. Some internal implementation details and legacy filenames have changed, which may affect external imports.

Files Changed

File Added Removed Notes
CHANGELOG.md 30 0 Documents Phase 3.1 changes
custom_components/pricehawk/cdr/cdr_client.py 22 8 Added brand parameter to fetch functions
custom_components/pricehawk/cdr/data/cdr_endpoints.json 0 1045 Legacy jxeeno registry removed
custom_components/pricehawk/cdr/data/eme_refdata.json 2325 0 New EME baked-in snapshot (117 organizations)
custom_components/pricehawk/cdr/registry.py 91 56 Switched to EME parsing, added cdr_brand field
custom_components/pricehawk/config_flow.py 13 7 Pass brand to CDR API calls at 4 locations
custom_components/pricehawk/strings.json 1 1 Updated error message reference
custom_components/pricehawk/translations/en.json 1 1 Updated translation for EME service
tests/test_cdr_client.py 65 0 Added brand parameter composition tests
tests/test_cdr_registry.py 185 73 Rewrote test suite for EME refdata2 format
Total 2733 1191 Net +1542 lines

Review Change Stack

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 16, 2026

Reviewer's Guide

Replaces the legacy jxeeno-based CDR registry with Energy Made Easy (EME) refdata2 as the sole retailer source, adds a brand discriminator to registry and HTTP client so shared-base-URI retailers can be disambiguated, wires this through the config flow, and hardens live-registry fetching with an EME snapshot fallback and expanded tests.

File-Level Changes

Change Details Files
Switch retailer registry from jxeeno JSON to EME refdata2 with baked-in snapshot and new parsing logic.
  • Change baked-in registry path to eme_refdata.json and live URL to api.energymadeeasy.gov.au/refdata2
  • Introduce EME-specific parsing (_parse_eme_entries) that reads organisations, builds base URIs from cdrCode, normalises cdrBrand, and maps logo paths to full URLs
  • Extend RetailerEndpoint with cdr_brand, update load_baked_in/get_registry/fetch_live to use EME format and treat malformed live payloads as CdrUnavailable
  • Expose parse_eme_for_test and baked_in_path_for_test for tests; remove jxeeno-specific helpers and legacy baked-in cdr_endpoints.json
custom_components/pricehawk/cdr/registry.py
custom_components/pricehawk/cdr/data/eme_refdata.json
custom_components/pricehawk/cdr/data/cdr_endpoints.json
tests/test_cdr_registry.py
Propagate brand discriminator through HTTP client and config flow so plan list/detail calls disambiguate shared-base-URI retailers.
  • Add optional brand parameter to fetch_plan_list/fetch_plan_detail and append ?brand=<cdr_brand> when provided
  • Update all plan-fetch call sites in config_flow to pass retailer.cdr_brand for both list and detail requests
  • Add tests to assert brand query param presence/absence and correct URL composition for list/detail calls
custom_components/pricehawk/cdr/cdr_client.py
custom_components/pricehawk/config_flow.py
tests/test_cdr_client.py
Update user-facing docs/strings and changelog to describe the new registry source and removal of legacy data.
  • Document EME refdata2 registry, cdr_brand field, and brand-aware plan fetches in CHANGELOG
  • Update error/help strings to reference EME instead of jxeeno
  • Add/remove corresponding translation and data files
CHANGELOG.md
custom_components/pricehawk/strings.json
custom_components/pricehawk/translations/en.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 16, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (2)
  • main
  • develop

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: bbc4f321-93f8-484e-971a-cb1351f3272e

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch phase-3-1-prep-eme-refdata2
  • 🛠️ scrub-secrets
  • 🛠️ no-hardcoded-rates
  • 🛠️ amber-api-limits
  • 🛠️ dashboard-protocol-safety

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In fetch_plan_detail you unconditionally append ?brand=..., which will break if base_url is ever given with its own query string; consider using urllib.parse.urlparse/urlunparse or urllib.parse.parse_qs to merge query parameters safely instead of manual concatenation.
  • The baked‑in registry tests assert len(orgs) > 50/len(endpoints) > 50; tying tests to approximate current registry size can make them brittle over time, so it may be more robust to assert the presence of specific expected orgIds/brands or invariants (e.g. that shared‑base‑URI brands retain distinct cdr_brand) instead of a numeric threshold.
  • You now have two very similar HTTP mocking helpers in the tests (_mock_session and _mock_session_capturing); consider consolidating them into a single, parameterized helper to reduce duplication and keep future test changes (e.g. adding headers or timeouts) in one place.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `fetch_plan_detail` you unconditionally append `?brand=...`, which will break if `base_url` is ever given with its own query string; consider using `urllib.parse.urlparse/urlunparse` or `urllib.parse.parse_qs` to merge query parameters safely instead of manual concatenation.
- The baked‑in registry tests assert `len(orgs) > 50`/`len(endpoints) > 50`; tying tests to approximate current registry size can make them brittle over time, so it may be more robust to assert the presence of specific expected orgIds/brands or invariants (e.g. that shared‑base‑URI brands retain distinct `cdr_brand`) instead of a numeric threshold.
- You now have two very similar HTTP mocking helpers in the tests (`_mock_session` and `_mock_session_capturing`); consider consolidating them into a single, parameterized helper to reduce duplication and keep future test changes (e.g. adding headers or timeouts) in one place.

## Individual Comments

### Comment 1
<location path="custom_components/pricehawk/cdr/registry.py" line_range="118-123" />
<code_context>
+        display = o.get("tradingName") or o.get("orgName")
+        if not (cdr_code and display):
             continue
+        logo_path = o.get("logo")
+        logo_uri = (
+            f"{_EME_LOGO_PREFIX}{logo_path}"
+            if isinstance(logo_path, str) and logo_path.startswith("/")
+            else logo_path
+        )
         out.append(
             RetailerEndpoint(
-                brand_id=str(bid),
-                brand_name=str(brand),
-                base_uri=str(base).rstrip("/"),
-                logo_uri=e.get("logoUri"),
-                abn=e.get("abn"),
-                last_updated=e.get("lastUpdated"),
+                brand_id=str(org_id),
+                brand_name=str(display),
+                base_uri=_EME_BASE_URI_TEMPLATE.format(cdr_code=cdr_code),
+                logo_uri=logo_uri,
+                abn=str(o.get("abn")) if o.get("abn") else None,
+                last_updated=None,  # EME envelope has no per-row mtime
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Normalize `logo_uri` to a string or `None` instead of passing through arbitrary types.

Currently `logo_uri` is a `https://energymadeeasy.gov.au/...` string only when `logo_path` is a string starting with `/`; otherwise we pass through whatever truthy value `logo_path` is. If upstream changes `logo` to a non-string (e.g. dict/list), we’ll store that in `RetailerEndpoint.logo_uri` despite its type being `str | None`. Consider normalising non-string values to `None` (or `str(...)`) so downstream consumers can safely rely on the declared type.

```suggestion
        logo_path = o.get("logo")
        if isinstance(logo_path, str) and logo_path:
            logo_uri = (
                f"{_EME_LOGO_PREFIX}{logo_path}"
                if logo_path.startswith("/")
                else logo_path
            )
        else:
            logo_uri = None
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread custom_components/pricehawk/cdr/registry.py Outdated
Artic0din added a commit that referenced this pull request May 16, 2026
CR-driven correctness + security fixes on PR #28 plus v1.5.0 release
artefacts (manifest version, CHANGELOG section).

Correctness
- cdr_client.fetch_plan_list now dedups by planId — republish-boundary
  page-flips no longer double-count plans.
- cdr_client.fetch_plan_list: 404 from the LIST endpoint raises
  CdrAPIError (bad URL / proxy regression), not CdrPlanNotFound which
  is reserved for stale planId on the detail endpoint.
- coordinator.daily_rollover skips saving_month_aud accumulation when
  Amber is not configured (was computing fake savings against $0).
- streaming.from_dict only restores _last_update when the stored state
  belongs to today — restoring yesterday's timestamp produced a
  synthetic delta on the first tick of a new day → over-counted
  energy/cost.
- providers/cdr_plan guards float(dailySupplyCharge) against malformed
  CDR values — defaults to $0/day instead of crashing provider setup.
- vpp_rebate.parse_rule uses safe_int for batteries_enrolled; garbage
  option values no-op the credit instead of aborting parser dispatch.

Wizard
- Removed dead "Skip CDR — enter rates manually" sentinel from the
  install-flow retailer + plan-select dropdowns. With manual entry
  deleted in Phase 3.0f the skip handlers re-entered the same step,
  creating an infinite loop. Options-flow keeps a Cancel sentinel
  (legitimate escape back to init menu).
- Deleted async_step_cdr_override and CONF_CDR_OVERRIDE_JSON — the
  step was wired into strings/const but never routed from any other
  step, so it was unreachable dead code. Helpers _deep_merge_dict +
  _parse_override_json + their tests removed too.
- strings.json + translations/en.json: removed "skip CDR" / "enter
  rates manually" copy from cdr_retailer, cdr_plan_select, cdr_error
  step descriptions and cdr_empty_unavailable error.

Release scope
- manifest.json: 1.4.0-beta.2 → 1.5.0-beta.1.
- CHANGELOG: full [1.5.0-beta.1] section documenting CDR-native engine,
  8 retailer parsers, opt-in fields, streaming evaluator, wizard
  rewrite, and all the carried-over fixes.

Known limitations (tracked, not fixed in this commit)
- tiered_fit PERIOD cap multiplies by distinct days in slots. Correct
  for full billing-period evals (the common case); under-credits
  partial windows, over-credits cross-period windows. Proper fix needs
  electricityContract.billingPeriod parsing — deferred. Documented
  inline in tiered_fit.py.

Test plan
- 633 tests pass (was 658 on PR #54 — PR #28 branch doesn't have the
  registry-EME changes yet).
- ruff clean on all modified files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 7-35: The changelog entries currently only live under "##
[Unreleased]"; add a concrete released section header (e.g. "## [x.y.z] -
YYYY-MM-DD") and either move or duplicate the listed "Changed", "Added", and
"Removed" subsections under that new version header while preserving the Keep a
Changelog formatting and the exact bullet text (retailer registry switched to
EME refdata2, RetailerEndpoint.cdr_brand, fetch_plan_list/fetch_plan_detail
brand parameter, baked-in snapshot path, and removed cdr_endpoints.json) so the
PR contains a new versioned release section alongside Unreleased.

In `@custom_components/pricehawk/cdr/registry.py`:
- Around line 115-117: The display value taken from o.get("tradingName") or
o.get("orgName") should be trimmed of leading/trailing whitespace before use and
before persisting as brand_name; update the code path around the display
variable (the lines that compute display and where brand_name is assigned) to
call .strip() on the chosen string (and guard against None), and apply the same
trimming to the other occurrence mentioned (the block around lines that set
brand_name at 127-128) so upstream names with trailing spaces are normalized
before slugging/storage.
- Around line 111-114: The code assumes o.get("cdrCode")/o.get("cdrBrand") are
strings and calls .strip(), which can raise AttributeError or other exceptions
and currently only some exception types are converted to CdrUnavailable; update
get_registry() so access/parsing of cdr_code and cdr_brand (the lines assigning
cdr_code and cdr_brand) are defensive: wrap the retrieval and .strip() calls in
a small try/except that catches AttributeError (or more generally Exception) and
converts any parsing failure into raising CdrUnavailable (the same fallback used
elsewhere), ensuring malformed payloads cannot crash get_registry(); apply the
same defensive pattern to the similar parsing block around lines 165-171.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 02a1eed6-96f8-474c-b665-641f9e8e770d

📥 Commits

Reviewing files that changed from the base of the PR and between cddc4fb and 00b7784.

📒 Files selected for processing (10)
  • CHANGELOG.md
  • custom_components/pricehawk/cdr/cdr_client.py
  • custom_components/pricehawk/cdr/data/cdr_endpoints.json
  • custom_components/pricehawk/cdr/data/eme_refdata.json
  • custom_components/pricehawk/cdr/registry.py
  • custom_components/pricehawk/config_flow.py
  • custom_components/pricehawk/strings.json
  • custom_components/pricehawk/translations/en.json
  • tests/test_cdr_client.py
  • tests/test_cdr_registry.py
💤 Files with no reviewable changes (1)
  • custom_components/pricehawk/cdr/data/cdr_endpoints.json
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
**/*.md

⚙️ CodeRabbit configuration file

**/*.md: Verify: no broken links, code examples match actual implementation, version numbers are current, no TODO left unfixed.

Files:

  • CHANGELOG.md
**/CHANGELOG.md

⚙️ CodeRabbit configuration file

**/CHANGELOG.md: Entries MUST follow Keep a Changelog format. New version section MUST be present for this PR's changes.

Files:

  • CHANGELOG.md
**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.py: Use async/await for all I/O operations
NEVER hardcode tokens, API keys, or credentials in any file — use HA config entry storage
State restore MUST validate storage version before loading
from_dict() methods MUST receive an explicit HA-timezone date — no date.today() fallback

Files:

  • tests/test_cdr_client.py
  • custom_components/pricehawk/cdr/cdr_client.py
  • custom_components/pricehawk/config_flow.py
  • custom_components/pricehawk/cdr/registry.py
  • tests/test_cdr_registry.py

⚙️ CodeRabbit configuration file

**/*.py: Check for: type hints on all public functions, no bare except:, SQL injection risks, missing input sanitisation, secrets not in code, Flask Blueprint structure respected, APScheduler job error handling.

Files:

  • tests/test_cdr_client.py
  • custom_components/pricehawk/cdr/cdr_client.py
  • custom_components/pricehawk/config_flow.py
  • custom_components/pricehawk/cdr/registry.py
  • tests/test_cdr_registry.py
**/test*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Tariff rate calculation changes require edge case tests (negative rates, midnight boundaries, empty windows)

Files:

  • tests/test_cdr_client.py
  • tests/test_cdr_registry.py
🔇 Additional comments (5)
custom_components/pricehawk/strings.json (1)

228-228: LGTM!

custom_components/pricehawk/translations/en.json (1)

228-228: LGTM!

custom_components/pricehawk/config_flow.py (1)

1330-1332: LGTM!

Also applies to: 1525-1527, 1548-1550, 2147-2149, 2171-2173

custom_components/pricehawk/cdr/cdr_client.py (1)

62-63: LGTM!

Also applies to: 70-73, 78-86, 108-110, 117-119, 121-123

tests/test_cdr_client.py (1)

191-212: LGTM!

Also applies to: 214-248

Comment thread CHANGELOG.md Outdated
Comment thread custom_components/pricehawk/cdr/registry.py Outdated
Comment thread custom_components/pricehawk/cdr/registry.py Outdated
…ation

What was broken
- jxeeno community registry covered 78 retailers vs the AER's full
  117. It also published 2 known wrong base URIs (ARCLINE → /arcline/
  instead of /energy-locals/; iO Energy → /io-energy/ instead of
  /energy-locals/). Plans hosted on shared base URIs (Energy Locals
  hosts 7 brands; OVO hosts 3) had no way to disambiguate which
  brand's plans were being requested.

What this fixes
- Switches the live registry source to EME refdata2 (117 orgs).
- Ships the EME snapshot baked in as the offline fallback.
- Drops jxeeno entirely — two unreliable sources are not better than
  one good source with an offline cache.
- Adds the cdrBrand discriminator on every RetailerEndpoint and
  threads it through cdr_client as an optional ?brand= query param.
  Shared-base-URI plans (ARCLINE, RAA, Cooperative, Indigo, Sonnen,
  iO, MYOB, OVO CTM, Sunswitch, etc.) are now correctly identified.
- Hardens the parser: trailing-whitespace bug in upstream EME
  cdrBrand fields (Amber, Aurora, Brighte) is stripped. Malformed
  payloads raise CdrUnavailable so the wizard falls back to baked-in
  rather than crashing.

Test plan
- 658 tests pass (added 18 new for EME parsing, baked-in health,
  shared-base-URI disambiguation, malformed-body fallback, and
  brand= query-string composition).
- Ruff clean on all changed files.

Why
- Required prep for Phase 3.1 multi-plan ranking. Without cdrBrand,
  the wizard can't distinguish two brands hosted on the same CDR
  base URI, so picking the wrong one returns the wrong plan list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Artic0din Artic0din force-pushed the phase-3-1-prep-eme-refdata2 branch from 00b7784 to 8d3d9b5 Compare May 16, 2026 09:32
@Artic0din
Copy link
Copy Markdown
Owner Author

@coderabbitai re-review please — all 3 round-1 findings fixed in 8d3d9b5: CHANGELOG folded into [1.5.0-beta.1] versioned section, defensive _safe_str helper added for non-string payloads + AttributeError caught in fallback, display name whitespace stripped. 647 tests pass.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 16, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Get the latest PR state and commit info
gh pr view 54 --repo Artic0din/ha-pricehawk --json commits,title,state | jq '.commits[-3:]'

# Get the diff for commit 8d3d9b5
gh api repos/Artic0din/ha-pricehawk/commits/8d3d9b5 --jq '.files[] | {filename: .filename, patch: .patch}'

Length of output: 188308


@Artic0din All three round-1 findings confirmed fixed in 8d3d9b5:

Fix 1 — CHANGELOG versioned: Additions land inside the [1.5.0-beta.1] block (adjacent to the existing "Universal CDR wizard" entry); removal documented under ### Removed. ✅

Fix 2 — _safe_str + AttributeError:

  • _safe_str correctly returns "" for non-strings (int cdrCode, None tradingName) and strips whitespace. ✅
  • Applied to all three fields (cdrCode, cdrBrand, display name) in _parse_eme_entries. ✅
  • fetch_live parse block now catches (ValueError, TypeError, KeyError, AttributeError)CdrUnavailable. ✅
  • test_non_string_fields_safely_skipped covers both the int-code and None-name cases. ✅

Fix 3 — Display name whitespace stripped: _safe_str applied to both tradingName and orgName before the falsy guard, validated by test_strips_trailing_whitespace_in_display_name. ✅

The rest of the PR (brand query-param threading through cdr_client/config_flow, shared-base-URI disambiguation, EME snapshot, test coverage) looks solid. No new issues.

[approve]

Previously `logo_uri` could be passed through as-is when EME shipped a
non-string `logo` value (dict, int, list). RetailerEndpoint.logo_uri is
typed `str | None` so downstream consumers expect that contract.

Now: `isinstance(logo_path, str) and logo_path` gates the assignment;
anything else (None, dict, empty string) becomes None.

Test: tests/test_cdr_registry.py::test_logo_uri_normalised_to_str_or_none
covers dict, empty string, None, absolute URL, relative path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Artic0din Artic0din merged commit b810240 into phase-3-multi-plan May 16, 2026
13 checks passed
@Artic0din Artic0din deleted the phase-3-1-prep-eme-refdata2 branch May 16, 2026 12:38
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