diff --git a/CHANGELOG.md b/CHANGELOG.md index 65cf7d5..13d1ea1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ## [Unreleased] +### Fixed (retro-review of #107) + +Two follow-ups from a Claude retro-review of the live-UAT bug-fix PR: + +- **Stale inline comment in `_pick_latest_dispatch_file`.** Comment still claimed `_LEGACY` was part of every filename — the very assumption #107 corrected. Updated to describe both shapes plus why lexical sort still works. (`aemo_api.py:119-122`) +- **`provider_id` not lowercased in `external_statistic_id`.** All current provider IDs (`amber`, `globird`, `dwt_aemo_direct`) are lowercase so the gap was latent, but a future provider with mixed/upper case would re-trigger the same silent `Invalid statistic_id` failure #107 fixed for `entry_id`. Belt-and-suspenders `.lower()` on the `provider_id` segment + regression test (`DWT_AEMO_Direct` form). (`statistics.py:46`) + ### CI - Codecov upload: bump `codecov/codecov-action` v4 → v5 (per Context7 sweep), rename `file:` → `files:` for the v5 input contract, add explicit `token: ${{ secrets.CODECOV_TOKEN }}` reference, and set `fail_ci_if_error: false` so a codecov flake never breaks CI. Token itself is configured via GitHub repo secret `CODECOV_TOKEN`, never in the codebase. diff --git a/custom_components/pricehawk/aemo_api.py b/custom_components/pricehawk/aemo_api.py index ec48d11..87d06f7 100644 --- a/custom_components/pricehawk/aemo_api.py +++ b/custom_components/pricehawk/aemo_api.py @@ -116,8 +116,10 @@ def _pick_latest_dispatch_file(html: str) -> str | None: matches = _FILE_RE.findall(html) if not matches: return None - # Filenames are PUBLIC_DISPATCHIS_YYYYMMDDHHMM_..._LEGACY.zip. - # Lexical sort correctly puts the most recent timestamp last. + # Filenames are PUBLIC_DISPATCHIS_YYYYMMDDHHMM_NNN[_LEGACY].zip. + # The `_LEGACY` suffix was dropped from the NEMWeb directory listing in May 2026; + # the regex accepts both shapes. Lexical sort still puts the most recent timestamp + # last because the YYYYMMDDHHMM prefix sits at a fixed position regardless of shape. return sorted(matches)[-1] diff --git a/custom_components/pricehawk/statistics.py b/custom_components/pricehawk/statistics.py index a38bce4..19a4ef8 100644 --- a/custom_components/pricehawk/statistics.py +++ b/custom_components/pricehawk/statistics.py @@ -43,7 +43,7 @@ def external_statistic_id(entry_id: str, provider_id: str) -> str: backfill with "Invalid statistic_id". Lowercase the entry-id slice. Live UAT 2026-05-23. """ - return f"{DOMAIN}:cost_{entry_id[:8].lower()}_{provider_id}" + return f"{DOMAIN}:cost_{entry_id[:8].lower()}_{provider_id.lower()}" def _metadata_for(entry_id: str, provider_id: str) -> StatisticMetaData: diff --git a/tests/test_external_statistics.py b/tests/test_external_statistics.py index 5574df8..3af5910 100644 --- a/tests/test_external_statistics.py +++ b/tests/test_external_statistics.py @@ -58,6 +58,18 @@ def test_distinct_per_entry(self): b = external_statistic_id("entry-BBB", "amber") assert a != b + def test_provider_id_lowercased_for_ha_recorder_contract(self): + """Belt-and-suspenders: a future provider whose id is not all-lowercase + (e.g. ``DWT_AEMO_Direct``) must still produce a valid recorder object_id. + Regression guard from #107 retro-review. + """ + sid = external_statistic_id("01KS83AKB2TN6G0BT9TAC1EMN9", "DWT_AEMO_Direct") + _, object_id = sid.split(":", 1) + assert object_id == object_id.lower(), ( + f"object_id {object_id!r} must be lowercase per HA recorder contract" + ) + assert sid == "pricehawk:cost_01ks83ak_dwt_aemo_direct" + def test_id_is_lowercase_for_ha_recorder_contract(self): """HA's recorder validates statistic_id as ``:`` where ``object_id`` must match ``[a-z0-9_]+``. HA's ULID entry_ids