Optimizer: expose forecasted highest/lowest battery SOC (#29564)
This commit is contained in:
parent
b76af6101d
commit
7e41160acf
23 changed files with 242 additions and 129 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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({});
|
||||
|
|
|
|||
|
|
@ -131,13 +131,13 @@
|
|||
#subline
|
||||
>
|
||||
<div
|
||||
v-if="batteryForecastEmpty"
|
||||
v-if="batteryForecastLowest"
|
||||
class="d-flex align-items-center mb-2"
|
||||
>
|
||||
<ForecastMessage :message="batteryForecastEmpty" />
|
||||
<ForecastMessage :point="batteryForecastLowest" />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="batteryForecastFull"
|
||||
v-else-if="batteryForecastHighest"
|
||||
class="d-none d-md-block mb-2"
|
||||
>
|
||||
|
||||
|
|
@ -272,13 +272,13 @@
|
|||
#subline
|
||||
>
|
||||
<div
|
||||
v-if="batteryForecastFull"
|
||||
v-if="batteryForecastHighest"
|
||||
class="d-flex align-items-center mb-2"
|
||||
>
|
||||
<ForecastMessage :message="batteryForecastFull" />
|
||||
<ForecastMessage :point="batteryForecastHighest" high />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="batteryForecastEmpty"
|
||||
v-else-if="batteryForecastLowest"
|
||||
class="d-none d-md-block mb-2"
|
||||
>
|
||||
|
||||
|
|
@ -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 });
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,43 @@
|
|||
<template>
|
||||
<router-link to="/optimize" class="root" @click.stop>
|
||||
{{ $t("main.energyflow.forecast") }} <span class="message">{{ message }}</span>
|
||||
{{ label }}: <span class="message">{{ message }}</span>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import formatter from "@/mixins/formatter";
|
||||
import type { BatteryForecastPoint } from "@/types/evcc";
|
||||
|
||||
export default defineComponent({
|
||||
name: "ForecastMessage",
|
||||
mixins: [formatter],
|
||||
props: {
|
||||
message: { type: String, required: true },
|
||||
point: { type: Object as PropType<BatteryForecastPoint>, required: true },
|
||||
high: { type: Boolean, default: false },
|
||||
},
|
||||
computed: {
|
||||
label(): string {
|
||||
if (this.point.limit) {
|
||||
return this.$t("main.energyflow.forecast");
|
||||
}
|
||||
return this.$t(
|
||||
this.high
|
||||
? "main.energyflow.batteryForecastNextHigh"
|
||||
: "main.energyflow.batteryForecastNextLow"
|
||||
);
|
||||
},
|
||||
message(): string {
|
||||
const time = this.fmtAbsoluteDate(new Date(this.point.time));
|
||||
if (this.point.limit) {
|
||||
const key = this.high
|
||||
? "main.energyflow.batteryForecastFull"
|
||||
: "main.energyflow.batteryForecastEmpty";
|
||||
return this.$t(key, { time });
|
||||
}
|
||||
const soc = this.fmtPercentage(this.point.soc);
|
||||
return `${time} (${soc})`;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -590,8 +590,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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
})
|
||||
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 })
|
||||
|
||||
return fullSlot, emptySlot
|
||||
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 high, low
|
||||
}
|
||||
|
||||
func (site *Site) loadpointRequest(lp loadpoint.API, minLen int, firstSlotDuration time.Duration, grid api.Rates) (optimizer.BatteryConfig, batteryDetail) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
{
|
||||
"never empty",
|
||||
[]float32{100, 100},
|
||||
[]float32{100, 100},
|
||||
0, zero,
|
||||
"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},
|
||||
},
|
||||
{
|
||||
"first full then empty",
|
||||
[]float32{100, 0},
|
||||
[]float32{100, 0},
|
||||
0, 1,
|
||||
"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 finally empty",
|
||||
[]float32{100, 100, 0},
|
||||
[]float32{100, 0, 0},
|
||||
0, 2,
|
||||
"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 empty then full",
|
||||
[]float32{0, 100},
|
||||
[]float32{0, 100},
|
||||
1, 0,
|
||||
"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 finally full",
|
||||
[]float32{0, 100, 100},
|
||||
[]float32{0, 0, 100},
|
||||
2, 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 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")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1192,7 +1192,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",
|
||||
|
|
|
|||
|
|
@ -1170,13 +1170,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",
|
||||
|
|
|
|||
|
|
@ -1170,13 +1170,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",
|
||||
|
|
|
|||
|
|
@ -1191,7 +1191,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",
|
||||
|
|
|
|||
|
|
@ -1191,7 +1191,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": "Import depuis le réseau",
|
||||
"homePower": "Consommation",
|
||||
|
|
|
|||
|
|
@ -1169,7 +1169,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",
|
||||
|
|
|
|||
|
|
@ -1181,7 +1181,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": "Import vum Netz",
|
||||
"homePower": "Verbrauch",
|
||||
|
|
|
|||
|
|
@ -1192,7 +1192,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",
|
||||
|
|
|
|||
|
|
@ -1190,7 +1190,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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1181,7 +1181,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": "Importação da rede",
|
||||
"homePower": "Consumo",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1193,7 +1193,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",
|
||||
|
|
|
|||
|
|
@ -1002,7 +1002,7 @@
|
|||
"batteryGridChargeLimit": "கிரிட் சார்சிங்: எப்போது",
|
||||
"batteryHold": "மின்கலம் (பூட்டபட்டது)",
|
||||
"batteryTooltip": "{energy} ({total}) இன் {soc}",
|
||||
"forecast": "முன்னறிவிப்பு: ",
|
||||
"forecast": "முன்னறிவிப்பு",
|
||||
"forecastTooltip": "முன்னறிவிப்பு: இன்று சூரிய விளைவாக்கம்",
|
||||
"gridImport": "கட்டம் பயன்பாடு",
|
||||
"homePower": "நுகர்வு",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue