Skip to content

feat(widget): Android forecast widgets (Solar/Price/CO₂/Feed-in) - #264

Closed
webalexeu wants to merge 7 commits into
evcc-io:mainfrom
webalexeu:feat/android-widgets-forecast
Closed

webalexeu wants to merge 7 commits into
evcc-io:mainfrom
webalexeu:feat/android-widgets-forecast

Conversation

@webalexeu

Copy link
Copy Markdown
Contributor

Follow-up to #255 (split out per naltatis's review: #255 (comment))

#255 now covers the Android Loadpoint widget only. This branch is a snapshot of #255 from just before that split - it still has the from-scratch Solar/Price/CO₂/Feed-in forecast widgets, kept here for whenever the charting approach is settled (Android has no first-party charting API comparable to iOS's Swift Charts; the current implementation hand-rolls each chart to a Canvas bitmap in ChartRenderer.kt rather than relying on a library).

  • ForecastWidget.kt / ChartRenderer.kt - the four forecast widgets, Y-axis + step/step-area/area chart modes, per-type color
  • ForecastWidgetConfigActivity.kt - server picker + live preview, Solar's "adjust to real production" toggle
  • Shares ApiClient.kt/SharedStore.kt/Theme.kt/WidgetConfig.kt with the Loadpoint widget in Android: add loadpoint widget #255

TODO

  • Rebase onto Android: add loadpoint widget #255 once merged, dropping the now-duplicated Loadpoint-widget history
  • Decide on the charting approach - hand-rolled ChartRenderer.kt (already working) vs. a library (e.g. Vico's compose-glance module, currently pre-release) - before this is ready for review

webalexeu and others added 7 commits August 27, 2026 14:09
Adds loadpoint and forecast home-screen widgets built with Jetpack
Glance: a widget configuration activity for picking server + loadpoint,
real chart rendering for forecast data, per-instance widget config with
immediate refresh on server change, and widget package name derived
from app config so fork builds can coexist with the official app
install.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
lint was failing on 3 @typescript-eslint/no-explicit-any errors in
withAndroidWidget.ts; typed the receiver/activity manifest nodes against
AndroidConfig.Manifest instead. Also brings the README status section up to
date - it still described the pipeline-spike state (one widget, no config
Activity, no instant refresh) even though all 5 widgets, per-instance config,
and instant refresh are implemented.
Ports LoadpointVM's full status/metric logic (heating, finished/
waitForVehicle, kWh fallback), status dot + color coding, a canvas-rendered
progress bar (Glance has no fractional-width modifier), chip-style mode
buttons, a two-column forecast header, a chart Y-axis + step-vs-area modes +
per-type color (previously always a flat green line), bold/colored footer
stats, and day/night theming throughout - all mirrored from LoadpointViews.swift
/ Views.swift / Theme.swift.

Both widget config Activities now fetch real data for the tapped choice and
show an actual preview of the widget (plain Views reusing the same chart/
progress-bar bitmaps, since a live Glance render would need pulling in the
full Compose UI stack) before committing via a new "Use this" button, instead
of committing immediately on tap with no preview.

Verified with expo prebuild + local assembleDebug/assembleRelease builds.
The Android widgets and their config Activities had hardcoded English text.
Extend build-widget-strings.mts (already generating the iOS .xcstrings
catalog from evcc's + this app's Weblate translations) to also emit Android
string resources under targets/android-widget/res/values(-b+<locale>)/, and
have withAndroidWidget.ts merge them into the prebuilt project's
values/strings.xml (which already has app_name etc.) instead of overwriting
it.

Replaced every hardcoded string in the widget/config-Activity Kotlin with
R.string.* lookups. The config Activities' picker/live-preview flow has no
iOS equivalent, so those strings are new additions to this app's own i18n
(en.json source + de.json, matching this repo's i18n conventions) rather
than reuses of existing evcc/app keys.

Also generalized the config plugin's package-rewrite from a single `package`
line replace to a whole-file regex, since the widget Kotlin now needs
`import io.evcc.android.R` to resolve correctly for the LOCAL-ONLY dev fork
build too (io.evcc.android.dev).

Verified with expo prebuild + local assembleDebug (confirmed the manifest
merge keeps app_name, and R resolves for the fork package).
Adds the last open follow-up from the parity work: a second widget size,
mirroring LoadpointCard's HStack { left; modeSelector } on iOS. Declares
SizeMode.Responsive(setOf(SMALL_SIZE, WIDE_SIZE)) and reads LocalSize to
branch layout - compact keeps the inline mode-chip row (deliberately kept
interactive, unlike iOS's compact size which drops to a plain-text mode
label), wide adds a vertical mode-selector column alongside. No manifest
change needed since the widget was already resizable; this just makes the
wider size render differently once a user resizes it.

Forecast widgets have no iOS size-variant precedent, so they stay single-size.

Verified with expo prebuild + local assembleDebug.
pairs with evcc-io/evcc#32490 and mirrors targets/widget/Loadpoint.swift's
handling from evcc-io#246, which this branch picked up via a rebase onto
main. Detects smart-mode servers via loadpoints[].alwaysCharge, switching the
selector to off/smart/now (with device-class labels for continuous heat pumps
and switchable devices) while old servers keep off/pv/minpv/now unchanged. A
read-only "∞" marks the Smart chip when Always charge is on/once, no toggle in
the widget yet, matching iOS.

Also fixes a bug the rebase's auto-merge introduced: the frozen pv/minpv
legacy-label loop wrote straight into `strings`, a variable that in this
branch is now built later from an intermediate `translations` map (added here
to share resolved strings between the iOS and Android generators) - it was
referencing `strings` before its declaration.
Addresses evcc-io#255#issuecomment-5317470240 (Maschga's PR review):

- LoadpointWidget gets a reload button in the title row (res/drawable/ic_reload.xml
  tinted via Glance's ColorFilter.tint(), wired to a new ReloadAction), mirroring
  iOS's ReloadIntent.
- Both widget families are now tappable end-to-end, deep-linking to the right
  app screen (evcc://loadpoint?server=&lp=, evcc://forecast?server=,
  evcc://server when unconfigured) via a shared deepLinkAction() helper -
  mirrors widgetURL in LoadpointViews.swift/Views.swift, including the same
  query param semantics the app's own router (AppContext.tsx) expects.
- loadpoint_widget_info.xml's resize bounds are now pinned to
  SizeMode.Responsive's two declared breakpoints (180-340dp wide, height
  locked at 110dp, resizeMode="horizontal" only) instead of open-ended
  horizontal|vertical; forecast_widget_info.xml drops resizeMode entirely
  since those widgets have no size-variant layout. Closes the gap where a
  launcher could hand the widget a real container bigger than any size Glance
  was told to lay content out for, leaving unfillable blank space - the
  likely cause of the "strange spacing" in Maschga's screenshot, pending
  on-device confirmation.

The mode-button highlight bug and widget-reconfigure check from the same
review are held for a live-device pass.
@webalexeu
webalexeu force-pushed the feat/android-widgets-forecast branch from c4a1ee9 to 49a6c65 Compare August 27, 2026 14:09
@webalexeu

Copy link
Copy Markdown
Contributor Author

Update on the charting-approach TODO above, before I rebase this onto #255's final (simplified) state: Vico's Glance support is no longer pre-release.

com.patrykandpatrick.vico:compose-glance (CartesianChartImage/PieChartImage composables purpose-built for Jetpack Glance app widgets) shipped its first stable release, 3.3.1, about two weeks ago on Maven Central. When this PR was drafted it genuinely was pre-release (3.2.0-next.1); that's changed since.

For context on the actual trade-off, since "hand-rolled vs. library" undersells it a bit: Glance can't host arbitrary custom drawing at all (no Canvas composable - it renders to RemoteViews), so any charting approach here, Vico included, ends up rasterizing to a Bitmap and showing it via Image, the same way ProgressBarRenderer.kt already does for the Loadpoint widget's progress strip. The real question is whether Vico's API is worth adopting as the way we produce that bitmap, not "real chart vs. fake chart."

Arguments for switching now:

  • No longer pre-release - the objection in the TODO is gone.
  • Purpose-built for exactly this (Glance chart images), likely handles edge cases (empty series, single data point, RTL, light/dark) more robustly than a hand-rolled renderer.
  • Less custom drawing code to maintain long-term.

Arguments for keeping ChartRenderer.kt as-is, at least for this PR:

  • It already works and needs no further changes to land the rebase/cleanup.
  • A new third-party charting dependency is a bigger, separate review surface for a still-draft feature - I'd rather keep this PR to "catch up with the simplification + apply the same last-known-data fix from Android: keep showing last loadpoint data through a transient fetch failure #283" as one reviewable unit, and treat a Vico swap as its own follow-up if wanted.
  • No new APK size / dependency-maintenance surface to take on right now.

I'm proceeding with the hand-rolled renderer for this rebase and will call this out again when it's ready, but wanted to flag the update in case you'd rather have me swap it in now while the code is already being touched.

@webalexeu

Copy link
Copy Markdown
Contributor Author

Closing and moving to #284

@webalexeu webalexeu closed this Sep 11, 2026
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