Skip to content

fix(web): use configured currency minor unit in Rates chart legend#4288

Open
chalfontchubby wants to merge 5 commits into
springfall2008:mainfrom
chalfontchubby:fix/simple-issues-batch
Open

fix(web): use configured currency minor unit in Rates chart legend#4288
chalfontchubby wants to merge 5 commits into
springfall2008:mainfrom
chalfontchubby:fix/simple-issues-batch

Conversation

@chalfontchubby

@chalfontchubby chalfontchubby commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

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 configured currency_symbols, so non-GBP users (e.g. the reporter's NZD setup, configured as $c) saw p/kWh in the chart legend even though the surrounding chart title/axis already correctly used self.currency_symbols[1].

Per the maintainer's own scoping comment on the issue: fix limited to web.py's chart rendering. Left config.py's CONFIG_ITEMS units and output.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 ValueError surfaces as unhelpful "Error: Exception raised"

Inverter.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 - 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_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 crashes the whole plan. Added test_charge_window_no_source_configured alongside 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_minute rather than charge_start_time and 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 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 - 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: the afterDraw plugin now reads chart.data.datasets[0].data[0] for the percentage and the MD_DATA global 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 from chart.data/MD_DATA rather 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.

chalfontchubby and others added 5 commits July 23, 2026 09:15
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>
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>
@chalfontchubby

Copy link
Copy Markdown
Collaborator Author

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.

Comment thread apps/predbat/inverter.py
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

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

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?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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.

  1. self.rest_api is configured, but self.rest_data is 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.
  2. Neither self.rest_api nor charge_start_time is 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?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 hardcoded p/kWh.
  • Inverter status: replace a bare raise ValueError with the existing “safe defaults + retry next update” path.
  • Metrics dashboard: Chart.js afterDraw plugin reads live values from chart.data / MD_DATA so 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).

Comment on lines +25 to +39
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
Comment on lines +18 to +23
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.
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.

3 participants