diff --git a/AGENTS.md b/AGENTS.md
index f7a467b3a49..aaff844dc55 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -183,6 +183,7 @@ Deep documentation on specific subsystems is available in `docs/agents/`. Load w
- Use placeholders for dynamic content: `{soc}`, `{duration}`, `{value}`
- Prefer context-specific keys over generic ones
- Test with German translations (20-40% longer text)
+- Keep separators and trailing punctuation (`: `, `…`, `—`) in the template, not in the translation value.
### Testing
diff --git a/assets/js/components/Energyflow/Energyflow.stories.ts b/assets/js/components/Energyflow/Energyflow.stories.ts
index 05695887c8c..16627e032f0 100644
--- a/assets/js/components/Energyflow/Energyflow.stories.ts
+++ b/assets/js/components/Energyflow/Energyflow.stories.ts
@@ -148,7 +148,7 @@ BatteryForecastDischarging.args = {
...batteryBase,
gridPower: 500,
homePower: 1800,
- battery: bat(1300, 62, { full: null, empty: hoursFromNow(0.4) }),
+ battery: bat(1300, 62, { lowest: { soc: 0, time: hoursFromNow(0.4), limit: true } }),
} as any;
export const BatteryForecastCharging = Template.bind({});
@@ -156,7 +156,7 @@ BatteryForecastCharging.args = {
...batteryBase,
pvPower: 6000,
gridPower: -1000,
- battery: bat(-4200, 45, { full: hoursFromNow(2.5), empty: null }),
+ battery: bat(-4200, 45, { highest: { soc: 100, time: hoursFromNow(2.5), limit: true } }),
} as any;
export const BatteryForecastBoth = Template.bind({});
@@ -164,7 +164,21 @@ BatteryForecastBoth.args = {
...batteryBase,
pvPower: 3000,
gridPower: -500,
- battery: bat(-1700, 70, { full: hoursFromNow(2), empty: hoursFromNow(36) }),
+ battery: bat(-1700, 70, {
+ highest: { soc: 100, time: hoursFromNow(2), limit: true },
+ lowest: { soc: 0, time: hoursFromNow(36), limit: true },
+ }),
+} as any;
+
+export const BatteryForecastSocExtremes = Template.bind({});
+BatteryForecastSocExtremes.args = {
+ ...batteryBase,
+ gridPower: 200,
+ homePower: 1500,
+ battery: bat(1300, 95, {
+ highest: { soc: 100, time: hoursFromNow(20), limit: true },
+ lowest: { soc: 34, time: hoursFromNow(8) },
+ }),
} as any;
export const BatteryForecastMulti = Template.bind({});
@@ -173,7 +187,7 @@ BatteryForecastMulti.args = {
pvPower: 8000,
gridPower: -1000,
homePower: 1000,
- battery: bat(-6000, 40, { full: hoursFromNow(26), empty: null }, [
+ battery: bat(-6000, 40, { highest: { soc: 100, time: hoursFromNow(26), limit: true } }, [
dev("Powerwall", -3500, 35),
dev("BYD", -2500, 47),
]),
@@ -187,7 +201,7 @@ BatteryForecastGridChargeLimit.args = {
batteryGridChargeLimit: 0.15,
smartCostType: "price",
currency: CURRENCY.EUR,
- battery: bat(-3700, 50, { full: hoursFromNow(1.5), empty: null }),
+ battery: bat(-3700, 50, { highest: { soc: 100, time: hoursFromNow(1.5), limit: true } }),
} as any;
export const BatteryAndGrid = Template.bind({});
diff --git a/assets/js/components/Energyflow/Energyflow.vue b/assets/js/components/Energyflow/Energyflow.vue
index 832a5369a03..e7f789d79b0 100644
--- a/assets/js/components/Energyflow/Energyflow.vue
+++ b/assets/js/components/Energyflow/Energyflow.vue
@@ -131,13 +131,13 @@
#subline
>
@@ -272,13 +272,13 @@
#subline
>
-
+
@@ -350,6 +350,7 @@ import { defineComponent, type PropType } from "vue";
import {
SMART_COST_TYPE,
type Battery,
+ type BatteryForecastPoint,
type Meter,
type CURRENCY,
type Forecast,
@@ -583,14 +584,14 @@ export default defineComponent({
consumers() {
return [...this.aux, ...this.ext];
},
- batteryForecastFull(): string | undefined {
- return this.fmtForecast(this.battery?.forecast, true);
+ batteryForecastHighest(): BatteryForecastPoint | undefined {
+ return this.battery?.forecast?.highest;
},
- batteryForecastEmpty(): string | undefined {
- return this.fmtForecast(this.battery?.forecast, false);
+ batteryForecastLowest(): BatteryForecastPoint | undefined {
+ return this.battery?.forecast?.lowest;
},
batteryForecastExists(): boolean {
- return !!(this.batteryForecastEmpty || this.batteryForecastFull);
+ return !!(this.batteryForecastHighest || this.batteryForecastLowest);
},
},
watch: {
@@ -687,18 +688,6 @@ export default defineComponent({
genericConsumerTitle(index: number) {
return `${this.$t("config.devices.consumer")} #${index + 1}`;
},
- fmtForecast(
- forecast: { full?: string | null; empty?: string | null } | undefined,
- full: boolean
- ): string | undefined {
- const isoString = full ? forecast?.full : forecast?.empty;
- if (!isoString) return undefined;
- const time = this.fmtAbsoluteDate(new Date(isoString));
- const key = full
- ? "main.energyflow.batteryForecastFull"
- : "main.energyflow.batteryForecastEmpty";
- return this.$t(key, { time });
- },
},
});
diff --git a/assets/js/components/Energyflow/ForecastMessage.vue b/assets/js/components/Energyflow/ForecastMessage.vue
index e2592843132..fda166b879f 100644
--- a/assets/js/components/Energyflow/ForecastMessage.vue
+++ b/assets/js/components/Energyflow/ForecastMessage.vue
@@ -1,16 +1,43 @@
- {{ $t("main.energyflow.forecast") }} {{ message }}
+ {{ label }}: {{ message }}
diff --git a/assets/js/types/evcc.ts b/assets/js/types/evcc.ts
index 9377684ac2d..4788df0acf6 100644
--- a/assets/js/types/evcc.ts
+++ b/assets/js/types/evcc.ts
@@ -589,8 +589,14 @@ export interface Meter {
}
export interface BatteryForecast {
- full: string | null; // ISO 8601 datetime
- empty: string | null; // ISO 8601 datetime
+ highest?: BatteryForecastPoint;
+ lowest?: BatteryForecastPoint;
+}
+
+export interface BatteryForecastPoint {
+ soc: number; // percent
+ time: string; // ISO 8601 datetime
+ limit?: boolean; // true when SMax (highest) or SMin (lowest) boundary reached
}
export interface Battery {
diff --git a/core/site_optimizer.go b/core/site_optimizer.go
index 85036ca2014..c9655e5fd78 100644
--- a/core/site_optimizer.go
+++ b/core/site_optimizer.go
@@ -292,51 +292,76 @@ func (site *Site) addBatteryForecastTotals(req []optimizer.BatteryConfig, resp [
return nil
}
- now := time.Now().Round(tariff.SlotDuration)
- fullSlot, emptySlot := site.batteryForecastFullAndEmptySlots(req, resp)
-
- const zero = -1
- if fullSlot == zero && emptySlot == zero {
+ high, low := batteryForecastSocExtremes(req, resp)
+ if high == nil && low == nil {
return nil
}
- var res types.BatteryForecast
- if fullSlot != zero {
- if ts := now.Add(time.Duration(fullSlot) * tariff.SlotDuration); ts.After(time.Now()) {
- res.Full = new(ts)
+ cutoff := time.Now()
+ now := cutoff.Round(tariff.SlotDuration)
+ point := func(p *batteryForecastSlot) *types.BatteryForecastPoint {
+ if p == nil {
+ return nil
}
- }
- if emptySlot != zero {
- if ts := now.Add(time.Duration(emptySlot) * tariff.SlotDuration); ts.After(time.Now()) {
- res.Empty = new(ts)
+ ts := now.Add(time.Duration(p.slot) * tariff.SlotDuration)
+ if !ts.After(cutoff) {
+ return nil
}
+ return &types.BatteryForecastPoint{Soc: p.soc, Time: ts, Limit: p.limit}
}
+ res := types.BatteryForecast{
+ Highest: point(high),
+ Lowest: point(low),
+ }
+ if res.Highest == nil && res.Lowest == nil {
+ return nil
+ }
return &res
}
-func (site *Site) batteryForecastFullAndEmptySlots(req []optimizer.BatteryConfig, resp []optimizer.BatteryResult) (int, int) {
- matchSlot := func(fun func(soc float32, bat optimizer.BatteryConfig) bool) int {
- NEXT:
- for i := range resp[0].StateOfCharge {
- for batIdx := range req {
- if !fun(resp[batIdx].StateOfCharge[i], req[batIdx]) {
- continue NEXT
- }
- }
- return i
- }
- return -1
- }
+type batteryForecastSlot struct {
+ slot int
+ soc float64 // percent
+ limit bool // true when SMax (highest) or SMin (lowest) boundary reached
+}
- fullSlot := matchSlot(func(soc float32, bat optimizer.BatteryConfig) bool {
- return soc >= bat.SMax
- })
- emptySlot := matchSlot(func(soc float32, bat optimizer.BatteryConfig) bool {
- return soc <= bat.SMin
+// batteryForecastSocExtremes returns the highest and lowest aggregate SOC
+// points across home batteries (SCapacity > 0) over the forecast horizon.
+// The Limit flag indicates whether the SOC reached the configured SMax (for
+// the highest point) or SMin (for the lowest point) boundary - in which case
+// the battery is forecasted to become fully charged or empty.
+// Returns nil for either point when no home battery is present.
+func batteryForecastSocExtremes(req []optimizer.BatteryConfig, resp []optimizer.BatteryResult) (*batteryForecastSlot, *batteryForecastSlot) {
+ homeIndices := lo.FilterMap(req, func(b optimizer.BatteryConfig, i int) (int, bool) {
+ return i, b.SCapacity > 0
})
+ if len(homeIndices) == 0 || len(resp) == 0 {
+ return nil, nil
+ }
+
+ totalCapacity := lo.SumBy(homeIndices, func(i int) float32 { return req[i].SCapacity })
+ totalSMax := lo.SumBy(homeIndices, func(i int) float32 { return req[i].SMax })
+ totalSMin := lo.SumBy(homeIndices, func(i int) float32 { return req[i].SMin })
+
+ var high, low *batteryForecastSlot
+ for i := range resp[homeIndices[0]].StateOfCharge {
+ sum := lo.SumBy(homeIndices, func(idx int) float32 { return resp[idx].StateOfCharge[i] })
+ soc := float64(sum/totalCapacity) * 100
+ fullReached := totalSMax > 0 && sum >= totalSMax
+ emptyReached := sum <= totalSMin
+
+ // first slot at SMax wins for highest
+ if high == nil || (!high.limit && (soc > high.soc || fullReached)) {
+ high = &batteryForecastSlot{slot: i, soc: soc, limit: fullReached}
+ }
+ // first slot at SMin wins for lowest
+ if low == nil || (!low.limit && (soc < low.soc || emptyReached)) {
+ low = &batteryForecastSlot{slot: i, soc: soc, limit: emptyReached}
+ }
+ }
- return fullSlot, emptySlot
+ return high, low
}
func (site *Site) loadpointRequest(lp loadpoint.API, minLen int, firstSlotDuration time.Duration, grid api.Rates) (optimizer.BatteryConfig, batteryDetail) {
diff --git a/core/site_optimizer_test.go b/core/site_optimizer_test.go
index 42f17b5215b..9c50919853c 100644
--- a/core/site_optimizer_test.go
+++ b/core/site_optimizer_test.go
@@ -46,67 +46,105 @@ func TestAsTimestamps(t *testing.T) {
}, got)
}
-func TestBatteryForecastTotals(t *testing.T) {
- site := new(Site)
-
- req := []optimizer.BatteryConfig{
- {SMax: 80},
- {SMax: 80},
- }
-
- const zero = -1
-
+func TestBatteryForecastSocExtremes(t *testing.T) {
for _, tc := range []struct {
- name string
- bat1, bat2 []float32
- full, empty int
+ name string
+ req []optimizer.BatteryConfig
+ soc [][]float32
+ high, low *batteryForecastSlot
}{
{
- "never full",
- []float32{0, 0},
- []float32{0, 0},
- zero, 0,
+ "no home battery",
+ []optimizer.BatteryConfig{{SMax: 80}}, // SCapacity unset → vehicle
+ [][]float32{{1000, 2000}},
+ nil, nil,
+ },
+ {
+ "single home battery rising — reaches full",
+ []optimizer.BatteryConfig{{SCapacity: 1000, SMax: 1000}},
+ [][]float32{{200, 500, 1000}},
+ &batteryForecastSlot{slot: 2, soc: 100, limit: true},
+ &batteryForecastSlot{slot: 0, soc: 20, limit: false},
},
{
- "never empty",
- []float32{100, 100},
- []float32{100, 100},
- 0, zero,
+ "single home battery falling — reaches empty",
+ []optimizer.BatteryConfig{{SCapacity: 1000, SMax: 1000}},
+ [][]float32{{900, 500, 0}},
+ &batteryForecastSlot{slot: 0, soc: 90, limit: false},
+ &batteryForecastSlot{slot: 2, soc: 0, limit: true},
},
{
- "first full then empty",
- []float32{100, 0},
- []float32{100, 0},
- 0, 1,
+ "single home battery — local extremes (no limit reached)",
+ []optimizer.BatteryConfig{{SCapacity: 1000, SMax: 900, SMin: 100}},
+ [][]float32{{500, 800, 200}},
+ &batteryForecastSlot{slot: 1, soc: 80, limit: false},
+ &batteryForecastSlot{slot: 2, soc: 20, limit: false},
},
{
- "first full finally empty",
- []float32{100, 100, 0},
- []float32{100, 0, 0},
- 0, 2,
+ "two home batteries aggregated",
+ []optimizer.BatteryConfig{
+ {SCapacity: 1000, SMax: 1000},
+ {SCapacity: 1000, SMax: 1000},
+ },
+ [][]float32{
+ {200, 400, 1000},
+ {800, 400, 1000},
+ },
+ &batteryForecastSlot{slot: 2, soc: 100, limit: true},
+ &batteryForecastSlot{slot: 1, soc: 40, limit: false},
},
{
- "first empty then full",
- []float32{0, 100},
- []float32{0, 100},
- 1, 0,
+ "vehicle and home battery — vehicle ignored",
+ []optimizer.BatteryConfig{
+ {SMax: 80}, // vehicle
+ {SCapacity: 1000, SMax: 1000}, // home
+ },
+ [][]float32{
+ {0, 0, 80},
+ {200, 500, 900},
+ },
+ &batteryForecastSlot{slot: 2, soc: 90, limit: false},
+ &batteryForecastSlot{slot: 0, soc: 20, limit: false},
},
{
- "first empty finally full",
- []float32{0, 100, 100},
- []float32{0, 0, 100},
- 2, 0,
+ "first slot at SMax wins for highest",
+ []optimizer.BatteryConfig{{SCapacity: 1000, SMax: 1000}},
+ [][]float32{{1000, 1000, 500}},
+ &batteryForecastSlot{slot: 0, soc: 100, limit: true},
+ &batteryForecastSlot{slot: 2, soc: 50, limit: false},
+ },
+ {
+ "near SMax is not full",
+ []optimizer.BatteryConfig{{SCapacity: 1000, SMax: 1000}},
+ [][]float32{{500, 999, 800}},
+ &batteryForecastSlot{slot: 1, soc: 99.9, limit: false},
+ &batteryForecastSlot{slot: 0, soc: 50, limit: false},
},
} {
t.Run(tc.name, func(t *testing.T) {
- resp := []optimizer.BatteryResult{
- {StateOfCharge: tc.bat1},
- {StateOfCharge: tc.bat2},
+ resp := make([]optimizer.BatteryResult, len(tc.soc))
+ for i, s := range tc.soc {
+ resp[i] = optimizer.BatteryResult{StateOfCharge: s}
}
- full, empty := site.batteryForecastFullAndEmptySlots(req, resp)
- assert.Equal(t, tc.full, full, "full")
- assert.Equal(t, tc.empty, empty, "empty")
+ high, low := batteryForecastSocExtremes(tc.req, resp)
+
+ if tc.high == nil {
+ assert.Nil(t, high, "high")
+ } else {
+ require.NotNil(t, high, "high")
+ assert.Equal(t, tc.high.slot, high.slot, "high.slot")
+ assert.InDelta(t, tc.high.soc, high.soc, 1e-3, "high.soc")
+ assert.Equal(t, tc.high.limit, high.limit, "high.limit")
+ }
+ if tc.low == nil {
+ assert.Nil(t, low, "low")
+ } else {
+ require.NotNil(t, low, "low")
+ assert.Equal(t, tc.low.slot, low.slot, "low.slot")
+ assert.InDelta(t, tc.low.soc, low.soc, 1e-3, "low.soc")
+ assert.Equal(t, tc.low.limit, low.limit, "low.limit")
+ }
})
}
}
diff --git a/core/types/types.go b/core/types/types.go
index 5c38828f0eb..ad233bd824d 100644
--- a/core/types/types.go
+++ b/core/types/types.go
@@ -21,8 +21,17 @@ type Measurement struct {
}
type BatteryForecast struct {
- Full *time.Time `json:"full"`
- Empty *time.Time `json:"empty"`
+ Highest *BatteryForecastPoint `json:"highest,omitempty"`
+ Lowest *BatteryForecastPoint `json:"lowest,omitempty"`
+}
+
+// BatteryForecastPoint describes an extreme SOC point in the battery forecast.
+// Limit indicates whether the configured SMax (for Highest) or SMin (for Lowest)
+// boundary was reached, i.e. the battery becomes fully charged or empty.
+type BatteryForecastPoint struct {
+ Soc float64 `json:"soc"`
+ Time time.Time `json:"time"`
+ Limit bool `json:"limit,omitempty"`
}
var _ api.TitleDescriber = (*Measurement)(nil)
diff --git a/i18n/da.json b/i18n/da.json
index 6ec225ed33c..efe7f9f49ec 100644
--- a/i18n/da.json
+++ b/i18n/da.json
@@ -1171,7 +1171,7 @@
"batteryGridChargeLimit": "Opladning fra elnettet: når",
"batteryHold": "Batteri (låst)",
"batteryTooltip": "{energy} af {total} ({soc})",
- "forecast": "Prognose: ",
+ "forecast": "Prognose",
"forecastTooltip": "prognose: resterende solenergi produktion i dag",
"gridImport": "Import fra elnet",
"homePower": "Forbrug",
diff --git a/i18n/de.json b/i18n/de.json
index 7c5023e6bc0..a870aee1158 100644
--- a/i18n/de.json
+++ b/i18n/de.json
@@ -1169,13 +1169,15 @@
"battery": "Batterie",
"batteryCharge": "Batterie laden",
"batteryDischarge": "Batterie entladen",
- "batteryForecastEmpty": "leer {time}",
- "batteryForecastFull": "voll {time}",
+ "batteryForecastEmpty": "Leer ab {time}",
+ "batteryForecastFull": "Voll ab {time}",
+ "batteryForecastNextHigh": "Nächstes Hoch",
+ "batteryForecastNextLow": "Nächstes Tief",
"batteryGridChargeActive": "Netzladen: aktiv",
"batteryGridChargeLimit": "Netzladen: wenn",
"batteryHold": "Batterie (gesperrt)",
"batteryTooltip": "{energy} von {total} ({soc})",
- "forecast": "Vorhersage: ",
+ "forecast": "Vorhersage",
"forecastTooltip": "Vorhersage: verbleibende PV-Produktion heute",
"gridImport": "Netzbezug",
"homePower": "Verbrauch",
diff --git a/i18n/en.json b/i18n/en.json
index f0500fab652..4f16c615584 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -1169,13 +1169,15 @@
"battery": "Battery",
"batteryCharge": "Battery charging",
"batteryDischarge": "Battery discharging",
- "batteryForecastEmpty": "empty {time}",
- "batteryForecastFull": "full {time}",
+ "batteryForecastEmpty": "Empty from {time}",
+ "batteryForecastFull": "Full from {time}",
+ "batteryForecastNextHigh": "Next high",
+ "batteryForecastNextLow": "Next low",
"batteryGridChargeActive": "Grid charging: active",
"batteryGridChargeLimit": "Grid charging: when",
"batteryHold": "Battery (locked)",
"batteryTooltip": "{energy} of {total} ({soc})",
- "forecast": "Forecast: ",
+ "forecast": "Forecast",
"forecastTooltip": "forecast: remaining solar production today",
"gridImport": "Grid import",
"homePower": "Consumption",
diff --git a/i18n/fi.json b/i18n/fi.json
index cf32811c0ff..f7ff675bf45 100644
--- a/i18n/fi.json
+++ b/i18n/fi.json
@@ -1170,7 +1170,7 @@
"batteryGridChargeLimit": "Milloin ladataan verkosta",
"batteryHold": "Akku (lukittu)",
"batteryTooltip": "{energy} / {total} ({soc})",
- "forecast": "Ennuste: ",
+ "forecast": "Ennuste",
"forecastTooltip": "ennuste: jäljellä oleva aurinkotuotanto tänään",
"gridImport": "Kulutus sähköverkosta",
"homePower": "Kodin sähkönkulutus",
diff --git a/i18n/fr.json b/i18n/fr.json
index 40649f24611..53c283b2307 100644
--- a/i18n/fr.json
+++ b/i18n/fr.json
@@ -1131,7 +1131,7 @@
"batteryGridChargeLimit": "Charge réseau : quand",
"batteryHold": "Batterie (verrouillée)",
"batteryTooltip": "{energy} sur {total} ({soc})",
- "forecast": "Prévisions : ",
+ "forecast": "Prévisions",
"forecastTooltip": "prévision : production solaire restante aujourd’hui",
"gridImport": "Utilisation du réseau",
"homePower": "Consommation",
diff --git a/i18n/it.json b/i18n/it.json
index 940edfa9ae5..6ff7be90e14 100644
--- a/i18n/it.json
+++ b/i18n/it.json
@@ -1123,7 +1123,7 @@
"batteryGridChargeLimit": "Ricarica dalla rete: quando",
"batteryHold": "Batteria (locked)",
"batteryTooltip": "{energy} di {total} ({soc})",
- "forecast": "Previsioni: ",
+ "forecast": "Previsioni",
"forecastTooltip": "previsioni: produzione solare rimanente per oggi",
"gridImport": "Uso della rete",
"homePower": "Consumo",
diff --git a/i18n/lb.json b/i18n/lb.json
index 27f3c437bf8..403bbe97ba6 100644
--- a/i18n/lb.json
+++ b/i18n/lb.json
@@ -1130,7 +1130,7 @@
"batteryGridChargeLimit": "Opluede vum Netz: wann",
"batteryHold": "Batterie (gespäert)",
"batteryTooltip": "{energy} vun {total} ({soc})",
- "forecast": "Viraussoen: ",
+ "forecast": "Viraussoen",
"forecastTooltip": "Prognose: Rescht Solarproduktioun fir haut",
"gridImport": "Verbrauch vum Stroumnetz",
"homePower": "Verbrauch",
diff --git a/i18n/lt.json b/i18n/lt.json
index 09adec2915e..759fbe8daa0 100644
--- a/i18n/lt.json
+++ b/i18n/lt.json
@@ -1171,7 +1171,7 @@
"batteryGridChargeLimit": "Įkrauti iš tinklo: kai",
"batteryHold": "Kaupiklis (užblokuotas)",
"batteryTooltip": "{energy} iš {total} ({soc})",
- "forecast": "Prognozė: ",
+ "forecast": "Prognozė",
"forecastTooltip": "prognozė: likusi saulės energijos gamyba šiandien",
"gridImport": "Iš tinklo",
"homePower": "Namo suvartojimas",
diff --git a/i18n/nl.json b/i18n/nl.json
index 6e843cbf2c0..6252380f1a6 100644
--- a/i18n/nl.json
+++ b/i18n/nl.json
@@ -1160,7 +1160,7 @@
"batteryGridChargeLimit": "Net laden: wanneer",
"batteryHold": "Batterij (geblokkeerd)",
"batteryTooltip": "{energy} van {total} ({soc})",
- "forecast": "Voorspelling: ",
+ "forecast": "Voorspelling",
"forecastTooltip": "voorspelling: resterende zonne-energieproductie voor vandaag",
"gridImport": "Netafname",
"homePower": "Consumptie",
diff --git a/i18n/pl.json b/i18n/pl.json
index 40a3a7c5ac1..618cbd104b1 100644
--- a/i18n/pl.json
+++ b/i18n/pl.json
@@ -1090,7 +1090,7 @@
"batteryGridChargeLimit": "Ładowanie z sieci: gdy",
"batteryHold": "Magazyn energii (chroniony)",
"batteryTooltip": "{energy} z {total} ({soc})",
- "forecast": "Prognoza: ",
+ "forecast": "Prognoza",
"forecastTooltip": "prognoza: pozostała na dziś produkcja energii słonecznej",
"gridImport": "Z sieci",
"homePower": "Konsumpcja",
diff --git a/i18n/pt.json b/i18n/pt.json
index 7f654842ace..1652967a719 100644
--- a/i18n/pt.json
+++ b/i18n/pt.json
@@ -1130,7 +1130,7 @@
"batteryGridChargeLimit": "Carga da rede: quando",
"batteryHold": "Bateria (suspensa)",
"batteryTooltip": "{energy} de {total} ({soc})",
- "forecast": "Previsões: ",
+ "forecast": "Previsões",
"forecastTooltip": "previsão: produção solar restante de hoje",
"gridImport": "Consumo da Rede",
"homePower": "Consumo",
diff --git a/i18n/ro.json b/i18n/ro.json
index a6d6ab08bab..64414b5034d 100644
--- a/i18n/ro.json
+++ b/i18n/ro.json
@@ -1084,7 +1084,7 @@
"batteryGridChargeLimit": "Încărcare din rețea: când",
"batteryHold": "Baterie (inchisa)",
"batteryTooltip": "{energy} din {total} ({soc})",
- "forecast": "Prognoză: ",
+ "forecast": "Prognoză",
"forecastTooltip": "prognoză: producția solară rămasă astăzi",
"gridImport": "Folosirea rețelei",
"homePower": "Consum",
diff --git a/i18n/sv.json b/i18n/sv.json
index a274321e96b..e91d83198da 100644
--- a/i18n/sv.json
+++ b/i18n/sv.json
@@ -1172,7 +1172,7 @@
"batteryGridChargeLimit": "Nätladdning: när",
"batteryHold": "Batteri (låst)",
"batteryTooltip": "{energy} av {total} ({soc})",
- "forecast": "Prognos: ",
+ "forecast": "Prognos",
"forecastTooltip": "Prognos: återstående solproduktion idag",
"gridImport": "Import från elnät",
"homePower": "Konsumtion",
diff --git a/i18n/ta.json b/i18n/ta.json
index c2e32cb0373..8476ec20bd5 100644
--- a/i18n/ta.json
+++ b/i18n/ta.json
@@ -1002,7 +1002,7 @@
"batteryGridChargeLimit": "கிரிட் சார்சிங்: எப்போது",
"batteryHold": "மின்கலம் (பூட்டபட்டது)",
"batteryTooltip": "{energy} ({total}) இன் {soc}",
- "forecast": "முன்னறிவிப்பு: ",
+ "forecast": "முன்னறிவிப்பு",
"forecastTooltip": "முன்னறிவிப்பு: இன்று சூரிய விளைவாக்கம்",
"gridImport": "கட்டம் பயன்பாடு",
"homePower": "நுகர்வு",
diff --git a/i18n/tr.json b/i18n/tr.json
index 52f20a9c8ca..8bd426a37e4 100644
--- a/i18n/tr.json
+++ b/i18n/tr.json
@@ -1130,7 +1130,7 @@
"batteryGridChargeLimit": "Şebekeden doldurma: şayet",
"batteryHold": "Batarya (kilitli)",
"batteryTooltip": "{total} ({soc})'ın {energy}'ı",
- "forecast": "Tahmin: ",
+ "forecast": "Tahmin",
"forecastTooltip": "“öngörü: bugün kalan güneşden üreti̇m”",
"gridImport": "Şebeke kullanımı",
"homePower": "Tüketim",