Browsers apply much more aggressive heuristic caching to a linked
stylesheet than to the dynamically-rendered HTML that references it, so a
CSS-only deploy could silently leave an already-open browser serving the
old styles until a hard refresh -- exactly what made today's dashboard
polish changes look broken while testing, until forcing a fresh fetch.
Appends a content-hash query param computed once at startup.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Period summary and battery stats now render as an icon-led, responsive
stat grid (.stat-grid) instead of a flat flex row.
- Generation/load/import/export stats get a real inline sparkline from the
same daily series already fetched for the bar chart. Self-consumption,
solar coverage and net revenue get a direction-aware trend badge against
an equal-length prior period (new `offset_days` param on
/api/usage/summary shifts the trailing window back for this comparison).
- Live flow diagram: static "conduit" wires now stay visible underneath
the animated overlay, flow lines redrawn as glowing round-dot particles
(was a plain dashed line) whose speed now scales with the actual kW
flowing, and each node's ring lights up in that flow's color while active.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Chart colors now resolve from the app's own CSS tokens instead of
hardcoded hex, so they stay in sync with the rest of the theme.
- Unified all three range pickers (TOU, battery, generation chart) on the
same <select> pattern, replacing the one-off pill-toggle buttons.
- Added shared .card-header/.card-title/.stat-row/.stat-label/.stat-value
utility classes so cards share consistent typography instead of
scattered inline styles.
- Added a hero strip showing today's net cost/revenue up front, with a
dollar-delta comparison against the recent 7-day daily average, so the
one number that matters most isn't buried mid-page.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
(end - start).days truncates instead of rounding, so "all time" (which
starts at the earliest reading's exact timestamp, rarely local midnight)
could report fewer range_days than days_with_data -- reads backwards
("5 of 4 days have data"). Round the duration up instead; day-aligned
presets and custom ranges are unaffected since they're already exact
multiples of 24h.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Extends the battery card's range beyond Today/3 days/This week to Last 30
days, Last 365 days, All time, and a user-picked custom start/end date.
Since battery telemetry is only ever captured on the live "today" poll and
never backfilled, a long or all-time range typically has real gaps -- the
card now shows how many of the requested days actually have battery data
rather than implying continuous history.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New card (hidden entirely for solar-only plants) shows: an SOC curve over
Today/Last 3 days/This week, and an estimate of what the battery actually
saved -- discharged energy valued at the import rate it avoided, minus
charged energy valued at the feed-in credit given up, both matched against
the current plan's own time-of-use windows.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Previously only showed import cost by tariff period; now also shows the
feed-in/export credit components alongside it, plus a net (usage cost
minus export credit) total.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sungrow's OAuth server keeps only one active token session per (app,
account), not per plant -- with Kallangur and McDowall each managing their
own independent token against the same account, whichever one refreshed
last was silently invalidating the other's refresh token. Both instances
now share one token file (a common bind-mounted volume) instead of each
holding their own.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Cost modeling can't tell you whether you actually qualify for a plan --
several top-ranked results turned out to require things like buying
solar equipment through a specific installer partner, or being a new
customer only. AER's data states these restrictions explicitly in most
cases; surface them as modeling notes (and a badge in the comparison
table) so they're visible before considering a switch, rather than
only showing up when someone tries to sign up.
Doesn't catch every restriction -- some plans (e.g. concession/seniors
offers) carry no eligibility block in AER's data at all despite an
implied real-world restriction, so absence of the badge isn't a
guarantee of eligibility.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
AER's Consumer Data Right unitPrice fields are GST-exclusive, confirmed
by comparing a real, currently-effective EnergyAustralia contract's own
GST-inclusive rates against this plan's cached AER data: usage rates and
the supply charge all matched the cached value multiplied by exactly
1.1 (four independent figures). A real AGL bill confirmed the same
convention separately (excl-GST line items, GST added back as one
combined amount at the end). This understated every one of the ~700+
cached comparison plans' costs by ~10% relative to manually-entered
current plans (which were correctly GST-inclusive, taken from real
bills), biasing every "savings vs current" figure across the board.
Feed-in/solar buy-back rates are the exception -- confirmed GST-free in
that same contract's own terms -- so the multiplier applies only to
usage rates and the supply charge.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Confirmed against Sungrow's own docs and live-tested against a real
battery plant: SOC and charge/discharge power live at the *device*
level (getDeviceRealTimeData against the hybrid inverter's ps_key,
device_type 14), not in the plant-aggregate points used elsewhere in
this client -- querying them via the plant-level endpoint just returns
null. Battery status is fetched once per live "today" poll (not
backfilled historically) and stored in three new nullable columns,
self-migrated on startup since there's no Alembic here.
Also fixes the dashboard's self-consumption line to derive from
load - import rather than generation - export, which overstated
"solar -> house" for battery systems (a chunk of that gap was going
into charging, not the house). The live flow diagram now shows a
Battery node (SOC + charge/discharge) that stays hidden entirely for
solar-only plants like Kallangur.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New /api/usage/live endpoint derives instantaneous-ish kW from the most
recent interval reading, shown as a solar -> house/grid flow diagram
that updates every 30s and flips direction/color between exporting and
importing. /api/usage/summary now also reports net revenue for the
selected period, reusing the existing costing engine against the
current plan.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Settings always showed the full "how to connect" steps and code-paste
form for oauth2 mode regardless of connection status, which read as
broken even when the token was live and polling successfully. Collapse
it behind a "Reconnect" toggle once connected.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
FastAPI app that pulls interval usage from Sungrow iSolarCloud (OAuth2),
caches AER Consumer Data Right electricity plan data, and compares actual
metered usage against every available plan to estimate real annual cost
(TOU, tiered feed-in, and flat tariffs).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>