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 @@ 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",