fix(web): use configured currency minor unit in Rates chart legend#4288
fix(web): use configured currency minor unit in Rates chart legend#4288chalfontchubby wants to merge 5 commits into
Conversation
The Rates chart's "Hourly"/"Today" series legends were hardcoded to "p/kWh" (pence) regardless of the configured currency_symbols, so non-GBP users (e.g. NZD, configured as "$c") saw "p/kWh" in the chart legend even though the surrounding chart title/axis already correctly used self.currency_symbols[1]. Fixes springfall2008#4153. Added a test verifying the series-name formatting follows currency_symbols for £p/$c/€c - get_chart() itself requires a fully computed plan before reaching this branch, so the test mirrors the formatting logic directly rather than standing up that heavier machinery (matching this file's existing "Loading..." early-return gate and the lack of any content assertions in test_web_if.py's integration tests). 🤖 Implemented with Claude Code, disclosed per repo convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…e is configured update_status() raised a bare `raise ValueError` (no message) when neither REST data nor a charge_start_time/charge_end_time config value was available at all - the exception carried no context, so it bubbled up to the main loop and surfaced to the user as the unhelpful status "Error: Exception raised", discarding the actually-useful message already logged just before it. This was also inconsistent with the very next branch, which handles a configured-but-unusable value (None) gracefully: logs a Warn, sets safe defaults, and retries next update rather than raising. Fixes springfall2008#4179. Rather than duplicate that recovery logic, this branch now sets charge_start_time/charge_end_time to None and falls through into the existing graceful handling below it, so a temporarily-empty inverter feed (the actual production trigger reported - GivEnergy cloud "no devices", Fox returning no data) no longer hard-crashes plan calculation. The specific "neither REST, charge_start_time nor charge_start_hour are set" detail is still logged for diagnosis; the shared downstream handler owns the status entity so it isn't immediately overwritten. Added a test (test_charge_window_no_source_configured) alongside the two existing sibling tests for the related None-value cases, verifying the same safe defaults are set and no exception propagates. 🤖 Implemented with Claude Code, disclosed per repo convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ed at all Follow-up to the previous commit's runtime fix. self.rest_api (the givtcp_rest config, GE-only) is a static fact known once at Inverter construction; self.rest_data is refreshed live on every update_status() call and can legitimately be transiently empty (a REST fetch hiccup, or before the first poll on a fresh start) - that's the case the previous commit's graceful runtime handling now covers. But if neither self.rest_api nor charge_start_time is configured at all, that's a permanent setup gap, not a transient one - so it's worth surfacing once, clearly, at Inverter setup, rather than only discovering it lazily (and repeatedly, every update cycle) via the runtime path. charge_start_time is confirmed as the shared config key across every inverter brand's apps.yaml template (GivEnergy, LuxPower, Sofar, Solar Assistant, etc.), not GE-specific, so the check generalises correctly. 🤖 Implemented with Claude Code, disclosed per repo convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…configured at all" This reverts commit 6218983.
The battery SoC doughnut chart's center text (percentage and kWh figures) is drawn by a Chart.js afterDraw plugin registered once in mdInitSOCChart. That plugin closed over the pct/d values from the moment the chart was first created. On each 30s refresh, mdUpdateSOCChart only mutates the existing chart's data array and calls .update() - it never recreates the chart (mdRenderAll only calls mdInitSOCChart again if mdSocChart doesn't exist yet) - so the doughnut ring's fill proportion updated correctly but the on-screen number text stayed frozen at the first-load reading, while every other card on the page (Health, Cost, API, Solar) re-renders fresh from the latest data on every refresh. Fixes springfall2008#4151. The afterDraw plugin now reads chart.data.datasets[0].data[0] for the percentage (already correctly updated by mdUpdateSOCChart) and the MD_DATA global for the kWh figures (already correctly reassigned on every refresh), instead of the stale closed-over values. No JS execution/browser test infra exists in this codebase, so this can't run the embedded JavaScript and check actual canvas output. Added a structural test instead, checking the generated JS source itself reads from chart.data/MD_DATA rather than the closure parameters, to catch a regression back to the same bug shape. 🤖 Implemented with Claude Code, disclosed per repo convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Note: I bundled three small, independent fixes into this one PR since none of them touch overlapping code. Happy to split any of these out into their own PR if that's preferred for review - just let me know. |
| charge_start_time = time_string_to_stamp(self.base.get_arg("charge_start_time", index=self.id)) | ||
| charge_end_time = time_string_to_stamp(self.base.get_arg("charge_end_time", index=self.id)) | ||
| else: | ||
| # Neither a data source nor a fallback config value is available - log the specific |
There was a problem hiding this comment.
I'm not sure of the rationale here, this is detecting when the inverter has not been configured (charge_start_time is not set at all in the args). The only time this is temporary is when you depend on auto-config of a cloud component, but why pretend to make a plan when you can't actually deliver it?
There was a problem hiding this comment.
That's a fair challenge, and I think it points at something the fix as written glossed over: the else branch actually covers two different situations, not one.
self.rest_apiis configured, butself.rest_datais empty this cycle - e.g. the original bug report's actual trigger, GivEnergy cloud returning "no devices" or Fox returning no data on a fresh start. This genuinely is transient - the data source is legitimate, it just hasn't successfully returned anything yet.- Neither
self.rest_apinorcharge_start_timeis configured at all - this is the case you're pointing at, and you're right that it's not temporary. Retrying every cycle forever doesn't fix a config gap that isn't going to close itself.
The fix collapsed both into the same "safe defaults + retry" path, which is defensible for (1) but not really for (2).
Given that raising here crashes the whole plan calculation (not just this one inverter) - which was the actual harm in the original report - I don't think going back to a hard raise for case (2) is right either. But I take your point that silently retrying with a "will retry next update" message is misleading when there's nothing to wait for. Would something like: keep the graceful retry for case (1), but for case (2) still avoid crashing the plan while making the status persistently loud (an ongoing Error-level status, not a message implying transience) so it doesn't get lost in the log - does that match what you had in mind, or would you rather it fail harder for that specific case?
There was a problem hiding this comment.
Pull request overview
This PR bundles three small fixes across the Web UI and inverter status handling: (1) make the Rates chart legend respect the configured currency minor unit, (2) avoid a hard-crash in Inverter.update_status() when no charge-window source is available, and (3) fix the Metrics dashboard SoC doughnut center text not refreshing over time.
Changes:
- Web Rates chart: legend series names now use
self.currency_symbols[1](minor unit) instead of hardcodedp/kWh. - Inverter status: replace a bare
raise ValueErrorwith the existing “safe defaults + retry next update” path. - Metrics dashboard: Chart.js
afterDrawplugin reads live values fromchart.data/MD_DATAso the on-canvas center text updates on refresh; adds regression tests.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/predbat/web.py | Updates Rates chart series legend labels to use configured currency minor unit. |
| apps/predbat/inverter.py | Stops raising a bare ValueError when no charge-window source is available; falls back to existing retry/safe-defaults logic. |
| apps/predbat/web_metrics_dashboard.py | Fixes SoC doughnut center text refresh by reading live chart/global data in the plugin. |
| apps/predbat/unit_test.py | Registers two new targeted tests in the suite registry. |
| apps/predbat/tests/test_web_chart_currency.py | Adds a test intended to validate currency minor-unit formatting for Rates chart series names. |
| apps/predbat/tests/test_metrics_dashboard_soc_refresh.py | Adds a regression test ensuring generated JS reads live values (not stale closure values). |
| apps/predbat/tests/test_inverter.py | Adds a test for “no charge window source configured” behavior (no exception + safe defaults). |
| print("**** test_rates_chart_series_names_use_currency_symbol ****") | ||
|
|
||
| for currency_symbols, expected_minor in [("£p", "p"), ("$c", "c"), ("€c", "c")]: | ||
| my_predbat.currency_symbols = currency_symbols | ||
| hourly_name = "Hourly {}/kWh".format(my_predbat.currency_symbols[1]) | ||
| today_name = "Today {}/kWh".format(my_predbat.currency_symbols[1]) | ||
|
|
||
| assert hourly_name == "Hourly {}/kWh".format(expected_minor), f"Expected 'Hourly {expected_minor}/kWh', got '{hourly_name}'" | ||
| assert today_name == "Today {}/kWh".format(expected_minor), f"Expected 'Today {expected_minor}/kWh', got '{today_name}'" | ||
| if expected_minor != "p": | ||
| assert "p/kWh" not in hourly_name and "p/kWh" not in today_name, f"Series name should not be hardcoded to pence for currency_symbols={currency_symbols}" | ||
|
|
||
| print("✓ Rates chart series names correctly follow currency_symbols[1] (£p, $c, €c all verified)") | ||
| print("✓ Test passed") | ||
| return False |
| def test_rates_chart_series_names_use_currency_symbol(my_predbat): | ||
| """ | ||
| Directly mirrors the series-name formatting used in WebInterface.get_chart's "Rates" | ||
| branch (web.py) - get_chart itself requires a fully computed plan (soc_kw_best populated) | ||
| before it reaches this branch, so this tests the formatting logic directly rather than | ||
| standing up that heavier machinery. |
Summary
A small batch of independent, simple fixes - each is its own commit.
#4153 - Rates chart legend hardcoded to "p/kWh"
The Rates chart's "Hourly"/"Today" series legend names were hardcoded to
p/kWh(pence) regardless of the user's configuredcurrency_symbols, so non-GBP users (e.g. the reporter's NZD setup, configured as$c) sawp/kWhin the chart legend even though the surrounding chart title/axis already correctly usedself.currency_symbols[1].Per the maintainer's own scoping comment on the issue: fix limited to
web.py's chart rendering. Leftconfig.py's CONFIG_ITEMS units andoutput.py's HA entity attribute names untouched, since changing those affects existing HA entity definitions/history for all users.apps/predbat/web.py: series names now use"{}/kWh".format(self.currency_symbols[1]), matching the pattern already used elsewhere in the same file.apps/predbat/tests/test_web_chart_currency.py: new test verifying the formatting for£p/$c/€c.#4179 - bare
raise ValueErrorsurfaces as unhelpful "Error: Exception raised"Inverter.update_status()raised a bareraise ValueError(no message) when neither REST data nor acharge_start_time/charge_end_timeconfig value was available - the exception carried no context, bubbling up as the unhelpful status "Error: Exception raised" and hard-crashing plan calculation, inconsistent with the very next branch which handles a configured-but-unusable value gracefully (safe defaults + retry next update).Matches the issue's suggested "Option 2": the branch now sets
charge_start_time/charge_end_timetoNoneand falls through into the existing graceful handling below it, so a temporarily-empty inverter feed (the actual production trigger reported - GivEnergy cloud "no devices", Fox returning no data) no longer crashes the whole plan. Addedtest_charge_window_no_source_configuredalongside the existing sibling tests for the related None-value cases.(A follow-up commit adding a setup-time warning for "no source configured at all" was tried and reverted - it would have produced false positives for Ginlong Solis-type inverters, which use
charge_start_hour/charge_start_minuterather thancharge_start_timeand have no REST API. That inconsistency already exists in the pre-existing error message text and looks like it needs separate, more careful investigation rather than bundling into this fix.)#4151 - Metrics dashboard battery SoC doesn't update when page left displayed
The battery SoC doughnut chart's center text (percentage and kWh figures) is drawn by a Chart.js
afterDrawplugin registered once inmdInitSOCChart. That plugin closed over thepct/dvalues from the moment the chart was first created. On each 30s refresh,mdUpdateSOCChartonly mutates the existing chart's data array and calls.update()- it never recreates the chart - so the doughnut ring's fill proportion updated correctly but the on-screen number text stayed frozen at the first-load reading, while every other card on the page re-renders fresh from the latest data on every refresh.apps/predbat/web_metrics_dashboard.py: theafterDrawplugin now readschart.data.datasets[0].data[0]for the percentage and theMD_DATAglobal for the kWh figures, instead of the stale closed-over values.apps/predbat/tests/test_metrics_dashboard_soc_refresh.py: no JS execution/browser test infra exists in this codebase, so this checks the generated JS source itself reads fromchart.data/MD_DATArather than the closure parameters, to catch a regression back to the same bug shape.Test plan
./run_all --test web_chart_currency/./run_all --test inverter/./run_all --test metrics_dashboard_soc_refresh- new tests pass./run_all --quick- full quick suite passes (606 tests, 0 failures)./run_pre_commit- clean (ruff, black, cspell, markdownlint) after each commit🤖 Implemented with Claude Code, disclosed per repo convention.