Skip to content

fix(coordinator): rebuild explanation every tick (closes empty-bullets cache)#148

Merged
Artic0din merged 1 commit into
devfrom
fix/explanation-per-tick
May 24, 2026
Merged

fix(coordinator): rebuild explanation every tick (closes empty-bullets cache)#148
Artic0din merged 1 commit into
devfrom
fix/explanation-per-tick

Conversation

@Artic0din

Copy link
Copy Markdown
Owner

Second post-deploy UAT hotpatch. Even after beta.3 fixed NEMWeb, winner_explanation.bullets stayed []. Root cause: explanation cached at midnight when NEMWeb was still broken; never rebuilt during the day.

Fix

Moved build_explanation from the midnight-only rollover branch into the per-tick _async_update_data body, gated on non-empty self._providers. Explanation now reflects current provider snapshot every 30s.

Test plan

  • pytest — 1130 passing
  • ruff check — clean
  • Post-merge tag v1.6.0-beta.4 → HACS beta refresh → bullets populate within ~30s of update

…s cache)

Live UAT 2026-05-24: even after beta.3 fixed the NEMWeb regex and the
AEMO spot rate started flowing, the Best Provider winner_explanation
attribute still showed bullets=[]. Root cause: build_explanation only
ran inside the midnight daily-rollover branch in _async_update_data,
not on every tick.

Sequence that exposed it:
  1. Midnight: NEMWeb broken (beta.2 deployed late morning), DWT
     provider has no _last_price, providers["dwt_aemo_direct"].extras
     has wholesale_price=None, _dwt_won_bullets skips every conditional,
     returns []. explanation.to_dict() caches bullets=[].
  2. Late morning: NEMWeb fixed by beta.3, AEMO fetch starts succeeding,
     DWT's _last_price gets populated, today_cost climbs from $1.10 to
     $5.04 across ticks. Best Rate sensor reads 549.13 c/kWh — real
     wholesale spike data flowing.
  3. winner_explanation.bullets still []. Explanation built at midnight
     was never rebuilt because the rollover branch is the only caller.

Fix: rebuild explanation on every coordinator tick after the per-
provider update loop, gated on self._providers being non-empty (else
min() over empty dict raises ValueError). The explanation now reflects
the *current* provider snapshot every 30s. Cost is a handful of dict
comprehensions per tick — negligible.

Manifest bumped to 1.6.0-beta.4. Silver-checklist version assertion
updated to match.

Full test suite: 1130 passing.
@Artic0din Artic0din merged commit 24d38f9 into dev May 24, 2026
1 of 2 checks passed
@Artic0din Artic0din deleted the fix/explanation-per-tick branch May 24, 2026 01:13

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review

This pull request bumps the version to 1.6.0-beta.4 and updates the coordinator to rebuild the winner explanation on every 30-second tick, ensuring the dashboard reflects current provider data. Feedback indicates that the new logic is duplicated with the existing daily rollover section, which could prematurely overwrite the previous day's summary. Furthermore, the changelog incorrectly states the logic was moved when it was actually added, leaving the redundant code in place.

Comment on lines +1164 to +1175
if self._providers:
avg_spot = None
if self._amber and self._amber.import_kwh_today > 0:
avg_spot = (
self._amber.import_cost_today_c
/ self._amber.import_kwh_today
)
explanation = build_explanation(
self._build_providers_block(),
avg_amber_spot_c_kwh=avg_spot,
)
self._last_explanation = explanation.to_dict()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

high

The logic for building the winner explanation is now duplicated with the existing block in the daily rollover section (lines 1075-1085).

Because this new block runs every 30 seconds, it will immediately overwrite the captured "Why X won" explanation for the previous day with a new explanation for the current day as soon as the rollover tick completes. This effectively makes the end-of-day summary for the previous day inaccessible to the user.

If the intention is to provide a live explanation for the current day (as suggested by the PR description), the redundant block in the rollover section should be removed to avoid unnecessary execution and semantic confusion at the day boundary.

Comment thread CHANGELOG.md

### Fixed

- **`build_explanation` now runs every coordinator tick.** Moved the call out of the daily-rollover branch into `_async_update_data` after the per-provider tick, gated on `self._providers` being non-empty. The explanation now reflects the *current* provider snapshot every 30s, not whatever the rollover branch saw at midnight. Cost is a handful of dict comprehensions per tick — pays for itself the moment a user opens the dashboard before the next midnight. (`coordinator.py:1145-1166`)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The changelog states that the call was moved out of the daily-rollover branch, but the current implementation in coordinator.py actually adds a second call while leaving the original one intact at lines 1075-1085. This discrepancy confirms that the intended cleanup of the rollover branch was missed in the code changes.

Artic0din added a commit that referenced this pull request May 24, 2026
…#153)

* fix(stack): four gemini-review findings I missed across beta.4-beta.8

Ryan caught that I'd been merging today's PRs with --auto without reading
gemini-code-assist reviews. These are the legitimate findings from those
reviews, fixed in one batch.

#148 (gemini): per-tick build_explanation duplicates the rollover branch.
  Beta.4 added a per-tick rebuild but left the daily-rollover rebuild in
  place. At midnight the rollover branch built the "yesterday's summary"
  snapshot, then the per-tick rebuild a few seconds later clobbered it
  with the post-reset all-zeros snapshot. Per-tick rebuild fires on the
  midnight tick too, so the rollover one was redundant AND actively
  harmful. Removed.

#147 (gemini): _pick_latest_dispatch_file lex-sort is case-sensitive.
  _FILE_RE uses re.IGNORECASE — could capture mixed-case filenames.
  sorted(matches)[-1] then compared Unicode codepoints; uppercase sorts
  before lowercase. A hypothetical mixed-case listing would put an older
  uppercase file last after sort. NEMWeb today is all uppercase but the
  contract should match the regex's tolerance. key=str.upper normalises.

#150 (gemini): rebuild_engine never re-reads _grid_power_entity.
  The options→data fallback added in beta.6 fires once at __init__ and
  never refreshes. Users editing the grid sensor via options-flow saw
  the integration silently keep pointing at the prior entity until HA
  restart. rebuild_engine is the options-update path — now re-resolves.

#152 (gemini): reset_today service silently succeeds with no entries.
  Silver action-exceptions rule: service handlers should raise when they
  cannot perform the action. A user with all entries in failed-load
  state would see "Successfully executed pricehawk.reset_today" and
  observe no dashboard change. Now raises HomeAssistantError telling
  them to reload the integration.

PR #146 (export-credit bullet) and PR #149 (loop optimization) are real
findings too but lower priority — left as future work.

Manifest bumped to 1.6.0-beta.9. Silver-checklist version assertion
updated to match.

Full test suite: 1140 passing.

* fix(coordinator): drop duplicate _grid_power_entity overwrite in rebuild_engine

Per gemini review of PR #153: the fallback I added at the TOP of
rebuild_engine was being silently negated by a duplicate options-only
assignment at the END of the same function. Removed the second
assignment; kept a comment marker so this isn't accidentally reintroduced.

Bug effect: rebuild_engine fired on every options-flow save would
overwrite my fix with the empty-string default if entry.options didn't
have CONF_GRID_POWER_SENSOR. Same regression as the original bug
beta.6 was meant to fix.

NOTE: gemini also flagged that rebuild_engine creates new provider
instances on every options update, losing today's daily accumulators
(import_kwh_today, import_cost_today_c, etc.). That's a real concern
but a bigger refactor — left for a follow-up PR.
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