Skip to content

Commit 223ae76

Browse files
committed
feat: Android widget support for smart mode redesign
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.
1 parent 3a58d31 commit 223ae76

32 files changed

Lines changed: 168 additions & 20 deletions

File tree

scripts/build-widget-strings.mts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -198,13 +198,12 @@ for (const [key, src] of Object.entries(KEYS)) {
198198
translations[key] = byLocale;
199199
}
200200

201-
for (const [iosKey, frozen] of Object.entries(FROZEN)) {
202-
const localizations: Record<string, unknown> = {};
201+
for (const [key, frozen] of Object.entries(FROZEN)) {
202+
const byLocale: Record<string, string> = {};
203203
for (const locale of locales) {
204-
const value = frozen[locale] ?? frozen[locale.split("-")[0]] ?? frozen[SOURCE_LANG];
205-
localizations[locale] = { stringUnit: { state: "translated", value } };
204+
byLocale[locale] = frozen[locale] ?? frozen[locale.split("-")[0]] ?? frozen[SOURCE_LANG];
206205
}
207-
strings[iosKey] = { extractionState: "manual", localizations };
206+
translations[key] = byLocale;
208207
}
209208

210209
if (missing.length) {

targets/android-widget/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,15 @@ Done:
7474
already resizable (`resizeMode="horizontal|vertical"`); this just makes the
7575
wider layout actually render something different once resized. The forecast
7676
widgets don't have an iOS size-variant precedent, so they stay single-size.
77+
- **Smart mode redesign** (mirrors iOS's `Loadpoint.swift`/#246): `Loadpoint`
78+
gained `alwaysCharge`/`chargerFeatureContinuous`. Its presence detects
79+
smart-mode servers (`off/smart/now`, with per-device-class labels - e.g.
80+
continuous heat pumps get Normal/Boost, switchable devices get On) vs. old
81+
servers (`off/pv/minpv/now`, unchanged). `modeChipLabel()` appends a
82+
read-only "∞" to the Smart chip when Always charge is on/once - no toggle in
83+
the widget, matching iOS. `widget.mode.pv`/`widget.mode.minpv` stay in the
84+
strings script as frozen (non-Weblate) translations since evcc removed them
85+
from its own i18n once the redesign shipped.
7786

7887
Not done yet (follow-ups for parity with iOS): none currently tracked -
7988
remaining gaps (reload button, deep links, spline chart smoothing) are

targets/android-widget/kotlin/ApiClient.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,13 @@ data class Loadpoint(
100100
val sessionEnergy: Double?,
101101
val chargedEnergy: Double?,
102102
val mode: String?,
103+
val alwaysCharge: String?,
103104
val charging: Boolean,
104105
val connected: Boolean,
105106
val enabled: Boolean,
106107
val chargerFeatureHeating: Boolean,
107108
val chargerFeatureSwitchDevice: Boolean,
109+
val chargerFeatureContinuous: Boolean,
108110
val ui: LoadpointUi?,
109111
) {
110112
companion object {
@@ -124,11 +126,13 @@ data class Loadpoint(
124126
sessionEnergy = d("sessionEnergy"),
125127
chargedEnergy = d("chargedEnergy"),
126128
mode = o.optString("mode").takeIf { it.isNotEmpty() },
129+
alwaysCharge = if (o.has("alwaysCharge") && !o.isNull("alwaysCharge")) o.optString("alwaysCharge") else null,
127130
charging = o.optBoolean("charging", false),
128131
connected = o.optBoolean("connected", false),
129132
enabled = o.optBoolean("enabled", false),
130133
chargerFeatureHeating = o.optBoolean("chargerFeatureHeating", false),
131134
chargerFeatureSwitchDevice = o.optBoolean("chargerFeatureSwitchDevice", false),
135+
chargerFeatureContinuous = o.optBoolean("chargerFeatureContinuous", false),
132136
ui = ui,
133137
)
134138
}.getOrNull()

targets/android-widget/kotlin/LoadpointWidget.kt

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,38 @@ private val WIDE_SIZE = DpSize(340.dp, 110.dp)
6060
// Row measurements are unreliable right at a boundary on some launchers, so
6161
// the switch-over threshold sits well below WIDE_SIZE's width.
6262
private val WIDE_THRESHOLD = 260.dp
63-
// visible (not private) so the config activities can reuse them for previews
64-
fun modeLabel(context: Context, mode: String): String = when (mode) {
65-
"off" -> context.getString(R.string.widget_mode_off)
66-
"pv" -> context.getString(R.string.widget_mode_pv)
67-
"minpv" -> context.getString(R.string.widget_mode_minpv)
68-
"now" -> context.getString(R.string.widget_mode_now)
69-
else -> mode
63+
// visible (not private) so the config activities can reuse them for previews.
64+
// mirrors Loadpoint.swift's `item()` closure: on smart-mode servers (detected
65+
// by the presence of `alwaysCharge`) the same raw mode can carry a
66+
// device-class-specific label (off->Normal, now->Boost/On) for continuous
67+
// heat pumps / switchable devices.
68+
fun modeLabel(context: Context, lp: Loadpoint, mode: String): String {
69+
val smartModeServer = lp.alwaysCharge != null
70+
if (smartModeServer) {
71+
if (mode == "off" && lp.chargerFeatureContinuous) return context.getString(R.string.widget_mode_normal)
72+
if (mode == "now") {
73+
if (lp.chargerFeatureContinuous) return context.getString(R.string.widget_mode_boost)
74+
if (lp.chargerFeatureSwitchDevice) return context.getString(R.string.widget_mode_on)
75+
}
76+
}
77+
return when (mode) {
78+
"off" -> context.getString(R.string.widget_mode_off)
79+
"smart" -> context.getString(R.string.widget_mode_smart)
80+
"pv" -> context.getString(R.string.widget_mode_pv)
81+
"minpv" -> context.getString(R.string.widget_mode_minpv)
82+
"now" -> context.getString(R.string.widget_mode_now)
83+
else -> mode
84+
}
85+
}
86+
87+
fun alwaysChargeActive(lp: Loadpoint): Boolean = lp.alwaysCharge == "on" || lp.alwaysCharge == "once"
88+
89+
// label plus a read-only "∞" marker on the Smart chip when Always charge is
90+
// on/once (mirrors the SF Symbol "infinity" shown next to Smart in
91+
// LoadpointViews.swift; no toggle in the widget for now, matches iOS)
92+
fun modeChipLabel(context: Context, lp: Loadpoint, mode: String): String {
93+
val label = modeLabel(context, lp, mode)
94+
return if (mode == "smart" && alwaysChargeActive(lp)) "$label" else label
7095
}
7196

7297
enum class LpStatus(val active: Boolean) {
@@ -133,8 +158,11 @@ fun title(context: Context, lp: Loadpoint): String {
133158
return vt.ifEmpty { lp.title ?: context.getString(R.string.widget_loadpoint_name) }
134159
}
135160

136-
fun modes(lp: Loadpoint): List<String> =
137-
if (lp.chargerFeatureSwitchDevice) listOf("off", "pv", "now") else listOf("off", "pv", "minpv", "now")
161+
fun modes(lp: Loadpoint): List<String> = when {
162+
lp.alwaysCharge != null -> listOf("off", "smart", "now")
163+
lp.chargerFeatureSwitchDevice -> listOf("off", "pv", "now")
164+
else -> listOf("off", "pv", "minpv", "now")
165+
}
138166

139167
class LoadpointWidget : GlanceAppWidget() {
140168
override val sizeMode = SizeMode.Responsive(setOf(SMALL_SIZE, WIDE_SIZE))
@@ -266,7 +294,7 @@ class LoadpointWidget : GlanceAppWidget() {
266294
Row {
267295
modes(state.lp).forEachIndexed { i, mode ->
268296
if (i > 0) Spacer(GlanceModifier.width(4.dp))
269-
ModeChip(context, mode = mode, current = state.lp.mode, serverId = state.serverId, lpIndex = state.lpIndex)
297+
ModeChip(context, lp = state.lp, mode = mode, serverId = state.serverId, lpIndex = state.lpIndex)
270298
}
271299
}
272300
}
@@ -278,7 +306,7 @@ class LoadpointWidget : GlanceAppWidget() {
278306
modes(state.lp).forEachIndexed { i, mode ->
279307
if (i > 0) Spacer(GlanceModifier.height(6.dp))
280308
ModeChip(
281-
context, mode = mode, current = state.lp.mode, serverId = state.serverId, lpIndex = state.lpIndex,
309+
context, lp = state.lp, mode = mode, serverId = state.serverId, lpIndex = state.lpIndex,
282310
modifier = GlanceModifier.fillMaxWidth().defaultWeight(),
283311
)
284312
}
@@ -287,13 +315,13 @@ class LoadpointWidget : GlanceAppWidget() {
287315
@Composable
288316
private fun ModeChip(
289317
context: Context,
318+
lp: Loadpoint,
290319
mode: String,
291-
current: String?,
292320
serverId: String,
293321
lpIndex: Int,
294322
modifier: GlanceModifier = GlanceModifier,
295323
) {
296-
val selected = mode == current
324+
val selected = mode == lp.mode
297325
Box(
298326
modifier = modifier
299327
.background(if (selected) modeSelectedBackground else modeUnselectedBackground)
@@ -311,7 +339,7 @@ class LoadpointWidget : GlanceAppWidget() {
311339
contentAlignment = Alignment.Center,
312340
) {
313341
Text(
314-
text = modeLabel(context, mode),
342+
text = modeChipLabel(context, lp, mode),
315343
style = modeChipStyle.copy(color = if (selected) modeSelectedText else modeUnselectedText),
316344
)
317345
}

targets/android-widget/kotlin/WidgetPreview.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ object WidgetPreview {
136136
val chipsRow = LinearLayout(context).apply { orientation = LinearLayout.HORIZONTAL }
137137
modes(lp).forEachIndexed { i, mode ->
138138
if (i > 0) chipsRow.addView(View(context).apply { layoutParams = LinearLayout.LayoutParams(d(4), 1) })
139-
chipsRow.addView(chip(context, modeLabel(context, mode), mode == lp.mode, dark))
139+
chipsRow.addView(chip(context, modeChipLabel(context, lp, mode), mode == lp.mode, dark))
140140
}
141141
root.addView(chipsRow)
142142

targets/android-widget/res/values-b+ar/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,14 @@
3030
<string name="widget_lpstatus_disconnected">Disconnected.</string>
3131
<string name="widget_lpstatus_finished">Finished.</string>
3232
<string name="widget_lpstatus_waitForVehicle">Ready. Waiting for vehicle…</string>
33+
<string name="widget_mode_boost">Boost</string>
3334
<string name="widget_mode_minpv">Min+Solar</string>
35+
<string name="widget_mode_normal">Normal</string>
3436
<string name="widget_mode_now">Fast</string>
3537
<string name="widget_mode_off">Off</string>
38+
<string name="widget_mode_on">On</string>
3639
<string name="widget_mode_pv">Solar</string>
40+
<string name="widget_mode_smart">Smart</string>
3741
<string name="widget_name_co2">CO₂</string>
3842
<string name="widget_name_feedin">Grid export price</string>
3943
<string name="widget_name_price">Grid import price</string>

targets/android-widget/res/values-b+bs/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,14 @@
3030
<string name="widget_lpstatus_disconnected">Disconnected.</string>
3131
<string name="widget_lpstatus_finished">Finished.</string>
3232
<string name="widget_lpstatus_waitForVehicle">Ready. Waiting for vehicle…</string>
33+
<string name="widget_mode_boost">Boost</string>
3334
<string name="widget_mode_minpv">Min+Solar</string>
35+
<string name="widget_mode_normal">Normal</string>
3436
<string name="widget_mode_now">Fast</string>
3537
<string name="widget_mode_off">Off</string>
38+
<string name="widget_mode_on">On</string>
3639
<string name="widget_mode_pv">Solar</string>
40+
<string name="widget_mode_smart">Smart</string>
3741
<string name="widget_name_co2">CO₂</string>
3842
<string name="widget_name_feedin">Grid export price</string>
3943
<string name="widget_name_price">Grid import price</string>

targets/android-widget/res/values-b+cs/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,14 @@
3030
<string name="widget_lpstatus_disconnected">Odpojeno.</string>
3131
<string name="widget_lpstatus_finished">Dokončeno.</string>
3232
<string name="widget_lpstatus_waitForVehicle">Připraveno. Čekám na vozidlo…</string>
33+
<string name="widget_mode_boost">Boost</string>
3334
<string name="widget_mode_minpv">Min+Solar</string>
35+
<string name="widget_mode_normal">Normal</string>
3436
<string name="widget_mode_now">Rychlé</string>
3537
<string name="widget_mode_off">Vypnuto</string>
38+
<string name="widget_mode_on">On</string>
3639
<string name="widget_mode_pv">Solár</string>
40+
<string name="widget_mode_smart">Chytrý</string>
3741
<string name="widget_name_co2">CO₂</string>
3842
<string name="widget_name_feedin">Grid export price</string>
3943
<string name="widget_name_price">Grid import price</string>

targets/android-widget/res/values-b+da/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,14 @@
3030
<string name="widget_lpstatus_disconnected">Afbrudt.</string>
3131
<string name="widget_lpstatus_finished">Færdig.</string>
3232
<string name="widget_lpstatus_waitForVehicle">Parat. Venter på køretøj…</string>
33+
<string name="widget_mode_boost">Boost</string>
3334
<string name="widget_mode_minpv">Min+Sol</string>
35+
<string name="widget_mode_normal">Normal</string>
3436
<string name="widget_mode_now">Hurtig</string>
3537
<string name="widget_mode_off">Fra</string>
38+
<string name="widget_mode_on">On</string>
3639
<string name="widget_mode_pv">Sol</string>
40+
<string name="widget_mode_smart">Smart</string>
3741
<string name="widget_name_co2">CO₂</string>
3842
<string name="widget_name_feedin">Grid export price</string>
3943
<string name="widget_name_price">Grid import price</string>

targets/android-widget/res/values-b+de/strings.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,14 @@
3030
<string name="widget_lpstatus_disconnected">Nicht verbunden.</string>
3131
<string name="widget_lpstatus_finished">Abgeschlossen.</string>
3232
<string name="widget_lpstatus_waitForVehicle">Ladebereit. Warte auf Fahrzeug …</string>
33+
<string name="widget_mode_boost">Boost</string>
3334
<string name="widget_mode_minpv">Min+PV</string>
35+
<string name="widget_mode_normal">Normal</string>
3436
<string name="widget_mode_now">Schnell</string>
3537
<string name="widget_mode_off">Aus</string>
38+
<string name="widget_mode_on">An</string>
3639
<string name="widget_mode_pv">PV</string>
40+
<string name="widget_mode_smart">Smart</string>
3741
<string name="widget_name_co2">CO₂</string>
3842
<string name="widget_name_feedin">Einspeisevergütung</string>
3943
<string name="widget_name_price">Netzbezugspreis</string>

0 commit comments

Comments
 (0)