Repository: evcc-io/evcc
Pull request #18168 (2025-01-10 16:50:53)

add maxacpower
bei einigen Hybrid-WR fehlte noch `maxacpower`
der PR hat aber keinen Anspruch auf Vollständigkeit.
Da ich die ganzen Geräte nicht kenne, habe ich mich auf die beschränkt, die aktive Batteriesteuerung haben.

andig: Den Parameter brauchts ja nur wenn die AC Leistung kleiner als die mögliche DC Leistung ist. Wir sollten den nur hinzufügen wenn die Geräte entsprechende Limits haben. 

/cc @premultiply 
VolkerK62: Ich dachte, das ist dann der Fall, wenn auf dem Dach mehr installiert ist, als der WR kann.

 
premultiply: Es geht nur um den ausgangsseitigen Wechselrichterpfad.

Die interne DC-Summenleistung von PV zu Batterie + AC kann größer sein als der AC-Pfad alleine zulässt.
In dem Fall "sieht" evcc nur die Summe bzw. insbesondere die Batterieladeleistung, die wir aber nicht nutzen können da der AC-Pfad schon voll gesättigt ist.
Dieser Parameter begrenzt dann die scheinbar verfügbare Leistung auf den tatsächlich nutzbaren Wert.
VolkerK62: @premultiply  Also kann der Fall bei jedem Hybrid-WR eintreten?

premultiply: Das kann man so pauschal nicht sagen.
Aber vermutlich bei den meisten.

Kann aber grundsätzlich nur passieren wenn die angeschlossene Modulleistung (kWp) größer ist als die Scheinleistung (kVA) des WR.

Patch:
diff --git a/Makefile b/Makefile
index 832337dd..cbc3b03a 100644
--- a/Makefile
+++ b/Makefile
@@ -10,7 +10,7 @@ VERSION := $(if $(TAG_NAME),$(TAG_NAME),$(SHA))
 BUILD_DATE := $(shell date -u '+%Y-%m-%d_%H:%M:%S')
 BUILD_TAGS := -tags=release
 TESLA_CLIENT_ID := ${TESLA_CLIENT_ID}
-LD_FLAGS := -X github.com/evcc-io/evcc/server.Version=$(VERSION) -X github.com/evcc-io/evcc/server.Commit=$(COMMIT) -X github.com/evcc-io/evcc/vehicle/tesla.TESLA_CLIENT_ID=$(TESLA_CLIENT_ID) -s -w
+LD_FLAGS := -X github.com/evcc-io/evcc/server.Version=$(VERSION) -X github.com/evcc-io/evcc/server.Commit=$(COMMIT) -s -w
 BUILD_ARGS := -trimpath -ldflags='$(LD_FLAGS)'
 
 # docker
diff --git a/assets/js/utils/energyOptions.js b/assets/js/utils/energyOptions.js
index 4e6a7d52..5da29908 100644
--- a/assets/js/utils/energyOptions.js
+++ b/assets/js/utils/energyOptions.js
@@ -1,6 +1,7 @@
 import { POWER_UNIT } from "../mixins/formatter";
 
 export function optionStep(maxEnergy) {
+  if (maxEnergy < 0.1) return 0.005;
   if (maxEnergy < 1) return 0.05;
   if (maxEnergy < 2) return 0.1;
   if (maxEnergy < 5) return 0.25;
diff --git a/assets/js/views/Config.vue b/assets/js/views/Config.vue
index 1ac69a62..916ef768 100644
--- a/assets/js/views/Config.vue
+++ b/assets/js/views/Config.vue
@@ -186,7 +186,7 @@
 							>
 								<template #icon><NotificationIcon /></template>
 								<template #tags>
-									<DeviceTags :tags="yamlTags('messaging')" />
+									<DeviceTags :tags="messagingTags" />
 								</template>
 							</DeviceCard>
 							<DeviceCard
@@ -210,7 +210,7 @@
 							>
 								<template #icon><EebusIcon /></template>
 								<template #tags>
-									<DeviceTags :tags="yamlTags('eebus')" />
+									<DeviceTags :tags="eebusTags" />
 								</template>
 							</DeviceCard>
 							<DeviceCard
@@ -224,7 +224,7 @@
 								<template #tags>
 									<DeviceTags
 										v-if="circuits.length == 0"
-										:tags="yamlTags('circuits')"
+										:tags="{ configured: { value: false } }"
 									/>
 									<template
 										v-for="(circuit, idx) in circuits"
@@ -249,7 +249,7 @@
 							>
 								<template #icon><ModbusProxyIcon /></template>
 								<template #tags>
-									<DeviceTags :tags="yamlTags('modbusproxy')" />
+									<DeviceTags :tags="modbusproxyTags" />
 								</template>
 							</DeviceCard>
 							<DeviceCard
@@ -390,12 +390,6 @@ export default {
 			site: { grid: "", pv: [], battery: [] },
 			deviceValueTimeout: undefined,
 			deviceValues: {},
-			yamlConfigState: {
-				messaging: false,
-				eebus: false,
-				circuits: false,
-				modbusproxy: false,
-			},
 		};
 	},
 	computed: {
@@ -462,6 +456,19 @@ export default {
 			}
 			return result;
 		},
+		eebusTags() {
+			return { configured: { value: store.state?.eebus || false } };
+		},
+		modbusproxyTags() {
+			const config = store.state?.modbusproxy || [];
+			if (config.length > 0) {
+				return { amount: { value: config.length } };
+			}
+			return { configured: { value: false } };
+		},
+		messagingTags() {
+			return { configured: { value: store.state?.messaging || false } };
+		},
 	},
 	watch: {
 		offline() {
@@ -484,7 +491,6 @@ export default {
 			await this.loadCircuits();
 			await this.loadDirty();
 			await this.updateValues();
-			await this.updateYamlConfigState();
 		},
 		async loadDirty() {
 			const response = await api.get("/config/dirty");
@@ -567,7 +573,6 @@ export default {
 		},
 		yamlChanged() {
 			this.loadDirty();
-			this.updateYamlConfigState();
 		},
 		addMeterToSite(type, name) {
 			if (type === "grid") {
@@ -636,16 +641,6 @@ export default {
 				console.error(`modal ${id} not found`);
 			}
 		},
-		updateYamlConfigState() {
-			const keys = Object.keys(this.yamlConfigState);
-			keys.forEach(async (key) => {
-				const res = await api.get(`/config/${key}`);
-				this.yamlConfigState[key] = !!res.data.result;
-			});
-		},
-		yamlTags(key) {
-			return { configured: { value: this.yamlConfigState[key] } };
-		},
 		circuitTags(circuit) {
 			const data = store.state?.circuits[circuit.name] || {};
 			const result = {};
diff --git a/charger/mypv.go b/charger/mypv.go
index db9da992..e20d7e40 100644
--- a/charger/mypv.go
+++ b/charger/mypv.go
@@ -24,6 +24,7 @@ import (
 	"time"
 
 	"github.com/evcc-io/evcc/api"
+	"github.com/evcc-io/evcc/core/loadpoint"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/modbus"
 	"github.com/evcc-io/evcc/util/sponsor"
@@ -33,8 +34,10 @@ import (
 type MyPv struct {
 	log     *util.Logger
 	conn    *modbus.Connection
+	lp      loadpoint.API
 	power   uint32
 	statusC uint16
+	enabled bool
 }
 
 const (
@@ -151,7 +154,11 @@ func (wb *MyPv) Enabled() (bool, error) {
 		return false, err
 	}
 
-	return binary.BigEndian.Uint16(b) > 0, nil
+	if binary.BigEndian.Uint16(b) == 0 {
+		wb.enabled = false
+	}
+
+	return wb.enabled, nil
 }
 
 func (wb *MyPv) setPower(power uint16) error {
@@ -169,7 +176,12 @@ func (wb *MyPv) Enable(enable bool) error {
 		power = uint16(atomic.LoadUint32(&wb.power))
 	}
 
-	return wb.setPower(power)
+	res := wb.setPower(power)
+	if res == nil {
+		wb.enabled = enable
+	}
+
+	return res
 }
 
 // MaxCurrent implements the api.Charger interface
@@ -181,7 +193,13 @@ var _ api.ChargerEx = (*MyPv)(nil)
 
 // MaxCurrentMillis implements the api.ChargerEx interface
 func (wb *MyPv) MaxCurrentMillis(current float64) error {
-	power := uint16(230 * current)
+	phases := 1
+	if wb.lp != nil {
+		if p := wb.lp.GetPhases(); p != 0 {
+			phases = p
+		}
+	}
+	power := uint16(voltage * current * float64(phases))
 
 	err := wb.setPower(power)
 	if err == nil {
@@ -226,3 +244,10 @@ func (wb *MyPv) GetLimitSoc() (int64, error) {
 
 	return int64(binary.BigEndian.Uint16(b)) / 10, nil
 }
+
+var _ loadpoint.Controller = (*MyPv)(nil)
+
+// LoadpointControl implements loadpoint.Controller
+func (wb *MyPv) LoadpointControl(lp loadpoint.API) {
+	wb.lp = lp
+}
diff --git a/charger/ocpp/cp_setup.go b/charger/ocpp/cp_setup.go
index f8590236..44488d4a 100644
--- a/charger/ocpp/cp_setup.go
+++ b/charger/ocpp/cp_setup.go
@@ -125,11 +125,10 @@ func (cp *CP) Setup(meterValues string, meterInterval time.Duration) error {
 
 	// configure measurands
 	if meterValues != "" {
-		if err := cp.ChangeConfigurationRequest(KeyMeterValuesSampledData, meterValues); err == nil || meterValues == "disable" {
-			cp.meterValuesSample = meterValues
-		} else {
+		if err := cp.ChangeConfigurationRequest(KeyMeterValuesSampledData, meterValues); err != nil {
 			cp.log.WARN.Printf("failed configuring %s: %v", KeyMeterValuesSampledData, err)
 		}
+		cp.meterValuesSample = meterValues
 	}
 
 	// trigger initial meter values
diff --git a/cmd/migrate.go b/cmd/migrate.go
index 28486742..d384e4be 100644
--- a/cmd/migrate.go
+++ b/cmd/migrate.go
@@ -79,7 +79,7 @@ func runMigrate(cmd *cobra.Command, args []string) {
 	log.DEBUG.Println("- eebus")
 	if reset {
 		settings.Delete(keys.EEBus)
-	} else if conf.EEBus.URI != "" {
+	} else if conf.EEBus.Configured() {
 		_ = settings.SetYaml(keys.EEBus, conf.EEBus)
 	}
 
diff --git a/cmd/root.go b/cmd/root.go
index bd331f5c..92f329a5 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -180,7 +180,7 @@ func runRoot(cmd *cobra.Command, args []string) {
 
 	// setup modbus proxy
 	if err == nil {
-		err = wrapErrorWithClass(ClassModbusProxy, configureModbusProxy(conf.ModbusProxy))
+		err = wrapErrorWithClass(ClassModbusProxy, configureModbusProxy(&conf.ModbusProxy))
 	}
 
 	// setup site and loadpoints
@@ -209,14 +209,6 @@ func runRoot(cmd *cobra.Command, args []string) {
 
 	// remove previous fatal startup errors
 	valueChan <- util.Param{Key: keys.Fatal, Val: nil}
-	// publish initial settings
-	valueChan <- util.Param{Key: keys.Interval, Val: conf.Interval}
-	valueChan <- util.Param{Key: keys.Network, Val: conf.Network}
-	valueChan <- util.Param{Key: keys.Mqtt, Val: conf.Mqtt}
-	valueChan <- util.Param{Key: keys.Influx, Val: conf.Influx}
-	valueChan <- util.Param{Key: keys.Hems, Val: conf.HEMS}
-	// TODO
-	valueChan <- util.Param{Key: keys.Sponsor, Val: sponsor.Status()}
 
 	// setup mqtt publisher
 	if err == nil && conf.Mqtt.Broker != "" {
@@ -234,16 +226,27 @@ func runRoot(cmd *cobra.Command, args []string) {
 
 	// start HEMS server
 	if err == nil {
-		err = wrapErrorWithClass(ClassHEMS, configureHEMS(conf.HEMS, site, httpd))
+		err = wrapErrorWithClass(ClassHEMS, configureHEMS(&conf.HEMS, site, httpd))
 	}
 
 	// setup messaging
 	var pushChan chan push.Event
 	if err == nil {
-		pushChan, err = configureMessengers(conf.Messaging, site.Vehicles(), valueChan, cache)
+		pushChan, err = configureMessengers(&conf.Messaging, site.Vehicles(), valueChan, cache)
 		err = wrapErrorWithClass(ClassMessenger, err)
 	}
 
+	// publish initial settings
+	valueChan <- util.Param{Key: keys.EEBus, Val: conf.EEBus.Configured()}
+	valueChan <- util.Param{Key: keys.Hems, Val: conf.HEMS}
+	valueChan <- util.Param{Key: keys.Influx, Val: conf.Influx}
+	valueChan <- util.Param{Key: keys.Interval, Val: conf.Interval}
+	valueChan <- util.Param{Key: keys.Messaging, Val: pushChan != nil}
+	valueChan <- util.Param{Key: keys.ModbusProxy, Val: conf.ModbusProxy}
+	valueChan <- util.Param{Key: keys.Mqtt, Val: conf.Mqtt}
+	valueChan <- util.Param{Key: keys.Network, Val: conf.Network}
+	valueChan <- util.Param{Key: keys.Sponsor, Val: sponsor.Status()}
+
 	// run shutdown functions on stop
 	var once sync.Once
 	stopC := make(chan struct{})
diff --git a/cmd/setup.go b/cmd/setup.go
index 89cae4c4..356ea613 100644
--- a/cmd/setup.go
+++ b/cmd/setup.go
@@ -491,7 +491,7 @@ func configureEnvironment(cmd *cobra.Command, conf *globalconfig.All) (err error
 
 	// setup EEBus server
 	if err == nil {
-		err = wrapErrorWithClass(ClassEEBus, configureEEBus(conf.EEBus))
+		err = wrapErrorWithClass(ClassEEBus, configureEEBus(&conf.EEBus))
 	}
 
 	// setup javascript VMs
@@ -559,14 +559,6 @@ func configureInflux(conf *globalconfig.Influx) (*server.Influx, error) {
 		return nil, nil
 	}
 
-	// TODO remove yaml file
-	// // migrate settings
-	// if !settings.Exists(keys.Influx) {
-	// 	if err := settings.SetJson(keys.Influx, conf); err != nil {
-	// 		return nil, err
-	// 	}
-	// }
-
 	influx := server.NewInfluxClient(
 		conf.URL,
 		conf.Token,
@@ -587,13 +579,6 @@ func configureMqtt(conf *globalconfig.Mqtt) error {
 		if err := settings.Json(keys.Mqtt, &conf); err != nil {
 			return err
 		}
-
-		// TODO remove yaml file
-		// } else {
-		// 	// migrate settings & write defaults
-		// 	if err := settings.SetJson(keys.Mqtt, conf); err != nil {
-		// 		return err
-		// 	}
 	}
 
 	if conf.Broker == "" {
@@ -645,7 +630,7 @@ func configureGo(conf []globalconfig.Go) error {
 }
 
 // setup HEMS
-func configureHEMS(conf globalconfig.Hems, site *core.Site, httpd *server.HTTPd) error {
+func configureHEMS(conf *globalconfig.Hems, site *core.Site, httpd *server.HTTPd) error {
 	// migrate settings
 	if settings.Exists(keys.Hems) {
 		if err := settings.Yaml(keys.Hems, new(map[string]any), &conf); err != nil {
@@ -657,14 +642,6 @@ func configureHEMS(conf globalconfig.Hems, site *core.Site, httpd *server.HTTPd)
 		return nil
 	}
 
-	// TODO remove yaml file
-	// // migrate settings
-	// if !settings.Exists(keys.Hems) {
-	// 	if err := settings.SetYaml(keys.Hems, conf); err != nil {
-	// 		return err
-	// 	}
-	// }
-
 	hems, err := hems.NewFromConfig(context.TODO(), conf.Type, conf.Other, site, httpd)
 	if err != nil {
 		return fmt.Errorf("failed configuring hems: %w", err)
@@ -681,10 +658,6 @@ func networkSettings(conf *globalconfig.Network) error {
 		return settings.Json(keys.Network, &conf)
 	}
 
-	// TODO remove yaml file
-	// // migrate settings
-	// return settings.SetJson(keys.Network, conf)
-
 	return nil
 }
 
@@ -703,7 +676,7 @@ func configureMDNS(conf globalconfig.Network) error {
 }
 
 // setup EEBus
-func configureEEBus(conf eebus.Config) error {
+func configureEEBus(conf *eebus.Config) error {
 	// migrate settings
 	if settings.Exists(keys.EEBus) {
 		if err := settings.Yaml(keys.EEBus, new(map[string]any), &conf); err != nil {
@@ -715,16 +688,8 @@ func configureEEBus(conf eebus.Config) error {
 		return nil
 	}
 
-	// TODO remove yaml file
-	// // migrate settings
-	// if !settings.Exists(keys.EEBus) {
-	// 	if err := settings.SetYaml(keys.EEBus, conf); err != nil {
-	// 		return err
-	// 	}
-	// }
-
 	var err error
-	if eebus.Instance, err = eebus.NewServer(conf); err != nil {
+	if eebus.Instance, err = eebus.NewServer(*conf); err != nil {
 		return fmt.Errorf("failed configuring eebus: %w", err)
 	}
 
@@ -735,18 +700,12 @@ func configureEEBus(conf eebus.Config) error {
 }
 
 // setup messaging
-func configureMessengers(conf globalconfig.Messaging, vehicles push.Vehicles, valueChan chan<- util.Param, cache *util.Cache) (chan push.Event, error) {
+func configureMessengers(conf *globalconfig.Messaging, vehicles push.Vehicles, valueChan chan<- util.Param, cache *util.Cache) (chan push.Event, error) {
 	// migrate settings
 	if settings.Exists(keys.Messaging) {
 		if err := settings.Yaml(keys.Messaging, new(map[string]any), &conf); err != nil {
 			return nil, err
 		}
-
-		// TODO remove yaml file
-		// } else if len(conf.Services)+len(conf.Events) > 0 {
-		// 	if err := settings.SetYaml(keys.Messaging, conf); err != nil {
-		// 		return nil, err
-		// 	}
 	}
 
 	messageChan := make(chan push.Event, 1)
@@ -807,12 +766,6 @@ func configureTariffs(conf globalconfig.Tariffs) (*tariff.Tariffs, error) {
 		if err := settings.Yaml(keys.Tariffs, new(map[string]any), &conf); err != nil {
 			return nil, err
 		}
-
-		// TODO remove yaml file
-		// } else if conf.Grid.Type != "" || conf.FeedIn.Type != "" || conf.Co2.Type != "" || conf.Planner.Type != "" {
-		// 	if err := settings.SetYaml(keys.Tariffs, conf); err != nil {
-		// 		return nil, err
-		// 	}
 	}
 
 	tariffs := tariff.Tariffs{
@@ -853,21 +806,15 @@ func configureDevices(conf globalconfig.All) error {
 	return nil
 }
 
-func configureModbusProxy(conf []globalconfig.ModbusProxy) error {
+func configureModbusProxy(conf *[]globalconfig.ModbusProxy) error {
 	// migrate settings
 	if settings.Exists(keys.ModbusProxy) {
-		if err := settings.Yaml(keys.ModbusProxy, new([]map[string]any), &conf); err != nil {
+		if err := settings.Yaml(keys.ModbusProxy, new([]map[string]any), conf); err != nil {
 			return err
 		}
-
-		// TODO remove yaml file
-		// } else if len(conf) > 0 {
-		// 	if err := settings.SetYaml(keys.ModbusProxy, conf); err != nil {
-		// 		return err
-		// 	}
 	}
 
-	for _, cfg := range conf {
+	for _, cfg := range *conf {
 		var mode modbus.ReadOnlyMode
 		mode, err := modbus.ReadOnlyModeString(cfg.ReadOnly)
 		if err != nil {
diff --git a/core/loadpoint.go b/core/loadpoint.go
index 906a4c39..8c720ac8 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -623,6 +623,17 @@ func (lp *Loadpoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
 	lp.publish(keys.EnableDelay, lp.Enable.Delay)
 	lp.publish(keys.DisableDelay, lp.Disable.Delay)
 
+	if phases := lp.getChargerPhysicalPhases(); phases != 0 {
+		if lp.configuredPhases != phases && lp.configuredPhases != 0 {
+			lp.log.WARN.Printf("configured phases %d do not match physical phases %d", lp.configuredPhases, phases)
+		}
+		lp.phases = phases
+		lp.configuredPhases = phases
+		lp.publish(keys.ChargerPhysicalPhases, phases)
+	} else {
+		lp.publish(keys.ChargerPhysicalPhases, nil)
+	}
+
 	lp.publish(keys.PhasesConfigured, lp.configuredPhases)
 	lp.publish(keys.ChargerPhases1p3p, lp.hasPhaseSwitching())
 	lp.publish(keys.PhasesEnabled, lp.phases)
@@ -630,12 +641,6 @@ func (lp *Loadpoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
 	lp.publishTimer(phaseTimer, 0, timerInactive)
 	lp.publishTimer(pvTimer, 0, timerInactive)
 
-	if phases := lp.getChargerPhysicalPhases(); phases != 0 {
-		lp.publish(keys.ChargerPhysicalPhases, phases)
-	} else {
-		lp.publish(keys.ChargerPhysicalPhases, nil)
-	}
-
 	// charger features
 	for _, f := range []api.Feature{api.IntegratedDevice, api.Heating} {
 		lp.publishChargerFeature(f)
diff --git a/core/loadpoint_effective.go b/core/loadpoint_effective.go
index 7f16f5f9..716f7f4e 100644
--- a/core/loadpoint_effective.go
+++ b/core/loadpoint_effective.go
@@ -1,7 +1,7 @@
 package core
 
 import (
-	"sort"
+	"slices"
 	"time"
 
 	"github.com/evcc-io/evcc/api"
@@ -31,20 +31,39 @@ func (lp *Loadpoint) EffectivePriority() int {
 	return lp.GetPriority()
 }
 
+type plan struct {
+	Id    int
+	Start time.Time // last possible start time
+	End   time.Time // user-selected finish time
+	Soc   int
+}
+
+func (lp *Loadpoint) nextActivePlan(maxPower float64, plans []plan) *plan {
+	for i, p := range plans {
+		requiredDuration := lp.GetPlanRequiredDuration(float64(p.Soc), maxPower)
+		plans[i].Start = p.End.Add(-requiredDuration)
+	}
+
+	// sort plans by start time
+	slices.SortStableFunc(plans, func(i, j plan) int {
+		return i.Start.Compare(j.Start)
+	})
+
+	if len(plans) > 0 {
+		return &plans[0]
+	}
+
+	return nil
+}
+
 // nextVehiclePlan returns the next vehicle plan time, soc and id
 func (lp *Loadpoint) nextVehiclePlan() (time.Time, int, int) {
 	if v := lp.GetVehicle(); v != nil {
-		type plan struct {
-			Time time.Time
-			Soc  int
-			Id   int
-		}
-
 		var plans []plan
 
 		// static plan
 		if planTime, soc := vehicle.Settings(lp.log, v).GetPlanSoc(); soc != 0 {
-			plans = append(plans, plan{Id: 1, Soc: soc, Time: planTime})
+			plans = append(plans, plan{Id: 1, Soc: soc, End: planTime})
 		}
 
 		// repeating plans
@@ -59,16 +78,12 @@ func (lp *Loadpoint) nextVehiclePlan() (time.Time, int, int) {
 				continue
 			}
 
-			plans = append(plans, plan{Id: index + 2, Soc: rp.Soc, Time: time})
+			plans = append(plans, plan{Id: index + 2, Soc: rp.Soc, End: time})
 		}
 
-		// sort plans by time
-		sort.Slice(plans, func(i, j int) bool {
-			return plans[i].Time.Before(plans[j].Time)
-		})
-
-		if len(plans) > 0 {
-			return plans[0].Time, plans[0].Soc, plans[0].Id
+		// calculate earliest required plan start
+		if plan := lp.nextActivePlan(lp.EffectiveMaxPower(), plans); plan != nil {
+			return plan.End, plan.Soc, plan.Id
 		}
 	}
 	return time.Time{}, 0, 0
diff --git a/core/loadpoint_phases.go b/core/loadpoint_phases.go
index 6a8d81f1..fc8d3f46 100644
--- a/core/loadpoint_phases.go
+++ b/core/loadpoint_phases.go
@@ -76,9 +76,9 @@ func (lp *Loadpoint) ActivePhases() int {
 
 // minActivePhases returns the minimum number of active phases for the loadpoint.
 func (lp *Loadpoint) minActivePhases() int {
-	lp.Lock()
+	lp.RLock()
 	configuredPhases := lp.configuredPhases
-	lp.Unlock()
+	lp.RUnlock()
 
 	// 1p3p supported or limit 1p
 	if lp.hasPhaseSwitching() || configuredPhases == 1 {
@@ -102,9 +102,9 @@ func (lp *Loadpoint) maxActivePhases() int {
 
 	// if 1p3p supported then assume configured limit or 3p
 	if lp.hasPhaseSwitching() {
-		lp.Lock()
+		lp.RLock()
 		physical = lp.configuredPhases
-		lp.Unlock()
+		lp.RUnlock()
 	}
 
 	return min(expect(vehicle), expect(physical), expect(measured), expect(charger))
diff --git a/i18n/da.toml b/i18n/da.toml
index e057d38d..1d505f7b 100644
--- a/i18n/da.toml
+++ b/i18n/da.toml
@@ -1,5 +1,5 @@
 [batterySettings]
-batteryLevel = "Batteriniveau"
+batteryLevel = "Batteri-niveau"
 capacity = "{energy} af {total}"
 control = "Batteri indstillinger"
 discharge = "Forhindrer afladning ved planlagt opladning og i hurtig mode"
@@ -43,6 +43,7 @@ labelResidualPower = "Tilbageværende effekt"
 title = "kontrol adfærd"
 
 [config.deviceValue]
+amount = "Mængde"
 broker = "Broker"
 bucket = "Bucket"
 capacity = "Kapacitet"
@@ -421,6 +422,8 @@ weekdays = "Dag"
 battery = "Batteri"
 batteryCharge = "Batteriet oplades"
 batteryDischarge = "Batteriet aflades"
+batteryGridChargeActive = "Oplader fra elnettet"
+batteryGridChargeLimit = "Oplader fra elnettet når"
 batteryHold = "Batteri (låst)"
 batteryTooltip = "{energi} af {total} ({soc})"
 gridImport = "Forbrug fra elnet"
diff --git a/i18n/de.toml b/i18n/de.toml
index ab44b7bf..7b614764 100644
--- a/i18n/de.toml
+++ b/i18n/de.toml
@@ -40,6 +40,7 @@ labelResidualPower = "Residualleistung"
 title = "Regelverhalten"
 
 [config.deviceValue]
+amount = "Anzahl"
 broker = "Broker"
 bucket = "Bucket"
 capacity = "Kapazität"
diff --git a/i18n/en.toml b/i18n/en.toml
index 6e289b9b..26ea338b 100644
--- a/i18n/en.toml
+++ b/i18n/en.toml
@@ -40,6 +40,7 @@ labelResidualPower = "Residual power"
 title = "Control behavior"
 
 [config.deviceValue]
+amount = "Amount"
 broker = "Broker"
 bucket = "Bucket"
 capacity = "Capacity"
diff --git a/i18n/fi.toml b/i18n/fi.toml
index e6056a3f..b11f095c 100644
--- a/i18n/fi.toml
+++ b/i18n/fi.toml
@@ -261,7 +261,7 @@ validateSave = "Vahvista ja tallenna"
 [footer.community]
 greenEnergy = "Aurinkoenergiaa"
 greenEnergySub1 = "ladattu evcc:llä"
-greenEnergySub2 = "Lokakuusta 2022 lähtien"
+greenEnergySub2 = "lokakuusta 2022 lähtien"
 greenShare = "Aurinkoenergian osuus"
 greenShareSub1 = "on peräisin"
 greenShareSub2 = "aurinkoenergiasta ja akkuvarastoinnista"
@@ -271,13 +271,13 @@ powerSub2 = "lataa..."
 tabTitle = "Yhteisö"
 
 [footer.savings]
-co2Saved = "{value} tallennettu"
+co2Saved = "{value} säästetty"
 co2Title = "CO₂ Päästöt"
 configurePriceCo2 = "Opi kuinka voit muokata hinta- ja CO₂ arvoja."
 footerLong = "{percent} aurinkoenergiaa"
 footerShort = "{percent} aurinkoenergiaa"
 modalTitle = "Latausenergian yhteenveto"
-moneySaved = "{value} tallennettu"
+moneySaved = "{value} säästetty"
 percentGrid = "{grid} kWh verkosta"
 percentSelf = "{self} kWh auringosta"
 percentTitle = "Aurinkoenergia"
diff --git a/i18n/fr.toml b/i18n/fr.toml
index 62412a76..bce3afb3 100644
--- a/i18n/fr.toml
+++ b/i18n/fr.toml
@@ -43,6 +43,7 @@ labelResidualPower = "Puissance résiduelle"
 title = "Comportement du contrôle"
 
 [config.deviceValue]
+amount = "Montant"
 broker = "Courtier"
 bucket = "Bucket"
 capacity = "Capacité"
@@ -426,6 +427,8 @@ weekdays = "Jours"
 battery = "Batterie"
 batteryCharge = "Batterie en charge"
 batteryDischarge = "Décharge de la batterie"
+batteryGridChargeActive = "charge réseau active"
+batteryGridChargeLimit = "charge réseau quand"
 batteryHold = "Batterie (verrouillée)"
 batteryTooltip = "{energy} sur {total} ({soc})"
 gridImport = "Utilisation du réseau"
diff --git a/i18n/hr.toml b/i18n/hr.toml
index 9c9c7afa..befe283f 100644
--- a/i18n/hr.toml
+++ b/i18n/hr.toml
@@ -418,6 +418,8 @@ weekdays = "Dani"
 battery = "Baterija"
 batteryCharge = "Punjenje baterije"
 batteryDischarge = "Pražnjenje baterije"
+batteryGridChargeActive = "aktivno punjenje putem mreže"
+batteryGridChargeLimit = "punjene putem mreže kad"
 batteryHold = "Baterija (zaključano)"
 batteryTooltip = "{energy} od {total} ({soc})"
 gridImport = "Upotreba mreže"
diff --git a/i18n/hu.toml b/i18n/hu.toml
index 3cb54677..6e23ce66 100644
--- a/i18n/hu.toml
+++ b/i18n/hu.toml
@@ -408,6 +408,8 @@ weekdays = "Napok"
 battery = "Battery"
 batteryCharge = "Energiatároló töltés"
 batteryDischarge = "Energiatároló kisütés"
+batteryGridChargeActive = "hálózatból töltés aktív"
+batteryGridChargeLimit = "hálózatból töltés ha"
 batteryHold = "Energiatároló (lezárva)"
 batteryTooltip = "{energy} / {total} ({soc})"
 gridImport = "Hálózatból import"
diff --git a/i18n/it.toml b/i18n/it.toml
index 92d924f8..9e22451d 100644
--- a/i18n/it.toml
+++ b/i18n/it.toml
@@ -33,7 +33,7 @@ description = "Garantisce che la somma dei carichi connessi ad un circuito non s
 title = "Gestione del Carico"
 
 [config.control]
-description = "Normalmente le impostazioni di base vanno bene. Modifica solo se sai cosa stai facendo."
+description = "Normalmente le impostazioni di default vanno bene. Modificale solo se sai cosa stai facendo."
 descriptionInterval = "Intervallo ciclo di aggiornamento in secondi. Definisce quanto spesso evcc rileva i dati, modifica la potenza di carica e aggiorna l'interfaccia utente. Intervalli ridotti (<30s) possono causare oscillazioni e comportamenti indesiderati."
 descriptionResidualPower = "Modifica la soglia di corrente per il ciclo di controllo. Se hai una batteria è consigliabile impostare un valore minimo di 100W, in questo modo la batteria riceverà una priorità rispetto all'uso della rete elettrica"
 labelInterval = "Intervallo di aggiornamento"
@@ -408,7 +408,7 @@ remove = "Rimuovi"
 repeating = "ricorrente"
 repeatingPlans = "Piani ricorrenti"
 selectAll = "Seleziona tutti"
-time = "Tempo"
+time = "Ora"
 title = "Piano"
 titleMinSoc = "Carica minima"
 titleTargetCharge = "Partenza"
@@ -507,8 +507,8 @@ descriptionEnergy = "Fino a quando dovrebbero essere {targetEnergy} caricati nel
 descriptionSoc = "Quando dovrebbe essere caricato il veicolo al {targetSoc}?"
 inactiveLabel = "Tempo target"
 modalTitle = "Imposta orario target"
-nextPlan = "“Prossimo piano\""
-notReachableInTime = "L'obbiettivo sarà raggiunto con un ritardo di {overrun}."
+nextPlan = "Prossimo piano"
+notReachableInTime = "L'obiettivo sarà raggiunto con un ritardo di {overrun}."
 onlyInPvMode = "Il piano di ricarica funziona solo in modalità Solare."
 planDuration = "Tempo di carica"
 planPeriodLabel = "Periodo"
diff --git a/i18n/lb.toml b/i18n/lb.toml
index 3c4a87f7..723a8eee 100644
--- a/i18n/lb.toml
+++ b/i18n/lb.toml
@@ -40,6 +40,7 @@ labelResidualPower = "Residuell Leeschtung"
 title = "Kontrollverhalen"
 
 [config.deviceValue]
+amount = "Unzahl"
 broker = "Broker"
 bucket = "Eemer"
 capacity = "Capacitéit"
@@ -419,6 +420,8 @@ weekdays = "Deeg"
 battery = "Batterie"
 batteryCharge = "Batterie oplueden"
 batteryDischarge = "Batterie entlueden"
+batteryGridChargeActive = "Netzlueden aktiv"
+batteryGridChargeLimit = "Netzluede wann"
 batteryHold = "Batterie (gespäert)"
 batteryTooltip = "{energy} vun {total} ({soc})"
 gridImport = "Verbrauch vum Stroumnetz"
diff --git a/i18n/lt.toml b/i18n/lt.toml
index 4dc18dc0..0bc16b00 100644
--- a/i18n/lt.toml
+++ b/i18n/lt.toml
@@ -43,6 +43,7 @@ labelResidualPower = "Likutinė galia"
 title = "Valdymo elgesys"
 
 [config.deviceValue]
+amount = "Kiekis"
 broker = "Broker sistema"
 bucket = "Bucket"
 capacity = "Talpa"
@@ -426,6 +427,8 @@ weekdays = "Dienos"
 battery = "Kaupiklis"
 batteryCharge = "Kaupiklis įkraunamas"
 batteryDischarge = "Kaupiklis iškraunamas"
+batteryGridChargeActive = "įkraunama iš tinklo"
+batteryGridChargeLimit = "įkrauti iš tinklo, kai"
 batteryHold = "Kaupiklis (užblokuotas)"
 batteryTooltip = "{energy} iš {total} ({soc})"
 gridImport = "Iš tinklo"
diff --git a/i18n/nl.toml b/i18n/nl.toml
index f33b818e..288c4899 100644
--- a/i18n/nl.toml
+++ b/i18n/nl.toml
@@ -402,14 +402,17 @@ vehicles = "Parking"
 
 [main.chargingPlan]
 active = "Actief"
+addRepeatingPlan = "Herhalend plan toevoegen"
 arrivalTab = "Aankomst"
 day = "Dag"
 departureTab = "Vertrek"
 goal = "Hoeveelheid"
 modalTitle = "Laad Plan"
 none = "geen"
+planNumber = "Plan {number}"
 remove = "Verwijderen"
 repeating = "herhalend"
+repeatingPlans = "Herhalende plannen"
 selectAll = "Alles selectreren"
 time = "Tijd"
 title = "Plan"
@@ -417,12 +420,14 @@ titleMinSoc = "Min lading"
 titleTargetCharge = "Vertrek"
 unsavedChanges = "Wijzigingen nog niet opgeslagen. Nu toepassen?"
 update = "Toepassen"
-weekdays = "Weekdagen"
+weekdays = "Dagen"
 
 [main.energyflow]
 battery = "Batterij"
 batteryCharge = "Batterij opladen"
 batteryDischarge = "Batterij ontladen"
+batteryGridChargeActive = "net laden actief"
+batteryGridChargeLimit = "net laden wanneer"
 batteryHold = "Batterij (aanhouden)"
 batteryTooltip = "{energy} van {total} ({soc})"
 gridImport = "Netafname"
@@ -512,6 +517,7 @@ descriptionEnergy = "Tegen wanneer moet {targetEnergy} in het voertuig geladen z
 descriptionSoc = "Wanneer moet het voertuig geladen zijn naar {targetSoc}?"
 inactiveLabel = "Ingestelde tijd"
 modalTitle = "Stel tijd in"
+nextPlan = "Volgend plan"
 notReachableInTime = "Laaddoel zal {overrun} later bereikt zijn."
 onlyInPvMode = "Laadplan werkt alleen in PV modus"
 planDuration = "Oplaadtijd"
@@ -588,6 +594,7 @@ scale3p = "Wordt verhoogd naar laden op 3 fasen."
 targetChargeActive = "Laadplan actief. Geschatte beëindiging over {duration}."
 targetChargePlanned = "Laadplan begint over {duration}."
 targetChargeWaitForVehicle = "Laadplan gereed. Wachten op voertuig..."
+unknown = ""
 vehicleLimit = "Voertuiglimiet."
 vehicleLimitReached = "Voertuiglimiet bereikt."
 waitForVehicle = "Klaar. Wachten op voertuig..."
diff --git a/i18n/pt.toml b/i18n/pt.toml
index a512b1a7..f3b9eaba 100644
--- a/i18n/pt.toml
+++ b/i18n/pt.toml
@@ -43,6 +43,7 @@ labelResidualPower = "Poder real"
 title = "Comportamento de controle"
 
 [config.deviceValue]
+amount = "Quantia"
 broker = "Corretor"
 bucket = "Balde"
 capacity = "Capacidade"
@@ -422,6 +423,8 @@ weekdays = "Dias"
 battery = "Bateria"
 batteryCharge = "Carrego de bateria"
 batteryDischarge = "Descarrego de bateria"
+batteryGridChargeActive = "carga da rede ativa"
+batteryGridChargeLimit = "carga da rede quando"
 batteryHold = "Bateria (suspensa)"
 batteryTooltip = "{energy} de {total} ({soc})"
 gridImport = "Consumo de rede"
diff --git a/i18n/sl.toml b/i18n/sl.toml
index 5e8bcb37..071b8693 100644
--- a/i18n/sl.toml
+++ b/i18n/sl.toml
@@ -397,19 +397,25 @@ vehicles = "Parkiranje"
 
 [main.chargingPlan]
 active = "Aktivno"
+addRepeatingPlan = "Dodaj ponavljajoči načrt"
 arrivalTab = "Prihod"
 day = "Dan"
 departureTab = "Odhod"
 goal = "Cilj polnjenja"
 modalTitle = "Načrt polnjenja"
 none = "brez"
+planNumber = "Načrt {number}"
 remove = "Odstrani"
+repeating = "ponavljajoči"
+repeatingPlans = "Ponavljajoči načrti"
+selectAll = "Izberi vse"
 time = "Čas"
 title = "Načrt"
 titleMinSoc = "Min. polnjenje"
 titleTargetCharge = "Odhod"
 unsavedChanges = "Obstajajo neshranjene spremembe. Želite uporabiti zdaj?"
 update = "Uporabi"
+weekdays = "Dnevi"
 
 [main.energyflow]
 battery = "Baterija"
diff --git a/i18n/sv.toml b/i18n/sv.toml
index d16e0142..eacae2d2 100644
--- a/i18n/sv.toml
+++ b/i18n/sv.toml
@@ -43,6 +43,7 @@ labelResidualPower = "Kvarvarande effekt"
 title = "Kontrollera beteende"
 
 [config.deviceValue]
+amount = "Belopp"
 broker = "Broker"
 bucket = "Bucket"
 capacity = "Kapacitet"
@@ -421,6 +422,8 @@ weekdays = "Dagar"
 battery = "Batteri"
 batteryCharge = "Batteri laddas"
 batteryDischarge = "Batteri laddas ur"
+batteryGridChargeActive = "nätladdning aktiv"
+batteryGridChargeLimit = "nätladdning när"
 batteryHold = "Batteri (låst)"
 batteryTooltip = "{energy} av {total} ({soc})"
 gridImport = "Import från elnät"
diff --git a/server/http_site_handler.go b/server/http_site_handler.go
index 84a326bc..731f4456 100644
--- a/server/http_site_handler.go
+++ b/server/http_site_handler.go
@@ -241,6 +241,7 @@ func healthHandler(site site.API) http.HandlerFunc {
 			return
 		}
 
+		w.Header().Set("Content-Type", "text/plain")
 		w.WriteHeader(http.StatusOK)
 		fmt.Fprintln(w, "OK")
 	}
diff --git a/templates/definition/charger/ocpp-fronius-wattpilot.yaml b/templates/definition/charger/ocpp-fronius-wattpilot.yaml
index b296f4ee..172bee7f 100644
--- a/templates/definition/charger/ocpp-fronius-wattpilot.yaml
+++ b/templates/definition/charger/ocpp-fronius-wattpilot.yaml
@@ -7,12 +7,12 @@ requirements:
   description:
     de: |
       Hinweis zur Konfiguration des Wattpilot mittels solar.wattpilot app: das Symbol neben dem 
-      Beispiel-OCPP-Server "wss://<username>:<password>@ocpp.cloud.fronius.com/<UID>" dient zur Konfiguration. 
-      Hier muss der evcc URL "ws://<IP-vom-evcc-host>:8887/ eingetragen sein und "Füge SN zur URL" aktiviert werden.
+      Beispiel-OCPP-Server `wss://<username>:<password>@ocpp.cloud.fronius.com/<UID>` dient zur Konfiguration. 
+      Hier muss der evcc URL `ws://<evcc-address>:8887/` eingetragen sein und "Füge SN zur URL" aktiviert werden.
     en: |
       Note on configuring the Wattpilot using the solar.wattpilot app: The symbol next to the 
-      example OCPP server "wss://<username>:<password>@ocpp.cloud.fronius.com/<UID>" is used for configuration.
-      The evcc URL "ws://<IP-of-the-evcc-host>:8887/" must be entered here and "Add SN to URL" must be activated.
+      example OCPP server `wss://<username>:<password>@ocpp.cloud.fronius.com/<UID>` is used for configuration.
+      The evcc URL `ws://<evcc-address>:8887/` must be entered here and "Add SN to URL" must be activated.
   evcc: ["sponsorship", "skiptest"]
 params:
   - preset: ocpp
diff --git a/templates/definition/meter/alpha-ess-smile.yaml b/templates/definition/meter/alpha-ess-smile.yaml
index 564caa1a..9569c7a5 100644
--- a/templates/definition/meter/alpha-ess-smile.yaml
+++ b/templates/definition/meter/alpha-ess-smile.yaml
@@ -28,6 +28,7 @@ params:
     default: 95
     type: int
     advanced: true
+  - name: maxacpower
 render: |
   type: custom
   {{- if eq .usage "grid" }}
@@ -123,6 +124,7 @@ render: |
       type: holding
       decode: uint32
     scale: 0.01
+  maxacpower: {{ .maxacpower }}
   {{- end }}
   {{- if eq .usage "battery" }}
   power:
diff --git a/templates/definition/meter/deye-hybrid-3p.yaml b/templates/definition/meter/deye-hybrid-3p.yaml
index 2b2c09ef..cd423a82 100644
--- a/templates/definition/meter/deye-hybrid-3p.yaml
+++ b/templates/definition/meter/deye-hybrid-3p.yaml
@@ -26,6 +26,7 @@ params:
     type: int
     default: 95
     advanced: true
+  - name: maxacpower
 render: |
   type: custom
   {{- if eq .usage "grid" }}
@@ -103,6 +104,7 @@ render: |
       type: holding
       decode: uint32s
     scale: 0.1
+  maxacpower: {{ .maxacpower }}
   {{- end }}
   {{- if eq .usage "battery" }}
   power:
diff --git a/templates/definition/meter/deye-hybrid-hp3.yaml b/templates/definition/meter/deye-hybrid-hp3.yaml
index fb5dcd0e..08b73c0b 100644
--- a/templates/definition/meter/deye-hybrid-hp3.yaml
+++ b/templates/definition/meter/deye-hybrid-hp3.yaml
@@ -25,6 +25,7 @@ params:
     type: int
     default: 95
     advanced: true
+  - name: maxacpower
 render: |
   type: custom
   {{- if eq .usage "grid" }}
@@ -106,6 +107,7 @@ render: |
       type: holding
       decode: uint32s
     scale: 0.1
+  maxacpower: {{ .maxacpower }}
   {{- end }}
   {{- if eq .usage "battery" }}
   power:
diff --git a/templates/definition/meter/enphase.yaml b/templates/definition/meter/enphase.yaml
index 7d22b15a..300e0e6b 100644
--- a/templates/definition/meter/enphase.yaml
+++ b/templates/definition/meter/enphase.yaml
@@ -39,7 +39,7 @@ render: |
         password: {{ .token }}
       insecure: true
       {{- end }}
-      jq: if .consumption[] | select(.measurementType == "net-consumption").activeCount >= 1 then .consumption[] | select(.measurementType == "net-consumption").lines[0].rmsCurrent else 0 end
+      jq: if (( .consumption[] | select(.measurementType == "net-consumption").activeCount >= 1 ) and ( .consumption[] | select(.measurementType == "net-consumption").lines | length >= 1 )) then .consumption[] | select(.measurementType == "net-consumption").lines[0].rmsCurrent else 0 end
     - source: http
       uri: http://{{ .host }}/production.json?details=1
       {{- if .token }}
@@ -48,7 +48,7 @@ render: |
         password: {{ .token }}
       insecure: true
       {{- end }}
-      jq: if .consumption[] | select(.measurementType == "net-consumption").activeCount >= 1 then .consumption[] | select(.measurementType == "net-consumption").lines[1].rmsCurrent else 0 end
+      jq: if (( .consumption[] | select(.measurementType == "net-consumption").activeCount >= 1 ) and ( .consumption[] | select(.measurementType == "net-consumption").lines | length >= 2 )) then .consumption[] | select(.measurementType == "net-consumption").lines[1].rmsCurrent else 0 end
     - source: http
       uri: http://{{ .host }}/production.json?details=1
       {{- if .token }}
@@ -57,7 +57,7 @@ render: |
         password: {{ .token }}
       insecure: true
       {{- end }}
-      jq: if .consumption[] | select(.measurementType == "net-consumption").activeCount >= 1 then .consumption[] | select(.measurementType == "net-consumption").lines[2].rmsCurrent else 0 end
+      jq: if (( .consumption[] | select(.measurementType == "net-consumption").activeCount >= 1 ) and ( .consumption[] | select(.measurementType == "net-consumption").lines | length >= 3 )) then .consumption[] | select(.measurementType == "net-consumption").lines[2].rmsCurrent else 0 end
   {{- end }}
   {{- if eq .usage "pv" }}
   power:
@@ -90,7 +90,7 @@ render: |
         password: {{ .token }}
       insecure: true
       {{- end }}
-      jq: if .production[] | select(.measurementType == "production").activeCount >= 1 then .production[] | select(.measurementType == "production").lines[0].rmsCurrent else 0 end
+      jq: if (( .production[] | select(.measurementType == "production").activeCount >= 1 ) and ( .production[] | select(.measurementType == "production").lines | length >= 1 )) then .production[] | select(.measurementType == "production").lines[0].rmsCurrent else 0 end
     - source: http
       uri: http://{{ .host }}/production.json?details=1
       {{- if .token }}
@@ -99,7 +99,7 @@ render: |
         password: {{ .token }}
       insecure: true
       {{- end }}
-      jq: if .production[] | select(.measurementType == "production").activeCount >= 1 then .production[] | select(.measurementType == "production").lines[1].rmsCurrent else 0 end
+      jq: if (( .production[] | select(.measurementType == "production").activeCount >= 1 ) and ( .production[] | select(.measurementType == "production").lines | length >= 2 )) then .production[] | select(.measurementType == "production").lines[1].rmsCurrent else 0 end
     - source: http
       uri: http://{{ .host }}/production.json?details=1
       {{- if .token }}
@@ -108,7 +108,7 @@ render: |
         password: {{ .token }}
       insecure: true
       {{- end }}
-      jq: if .production[] | select(.measurementType == "production").activeCount >= 1 then .production[] | select(.measurementType == "production").lines[2].rmsCurrent else 0 end
+      jq: if (( .production[] | select(.measurementType == "production").activeCount >= 1 ) and ( .production[] | select(.measurementType == "production").lines | length >= 3 )) then .production[] | select(.measurementType == "production").lines[2].rmsCurrent else 0 end
   {{- end }}
   {{- if eq .usage "battery" }}
   power:
diff --git a/templates/definition/meter/fox-ess-h3.yaml b/templates/definition/meter/fox-ess-h3.yaml
index e9ddc36d..2429279e 100644
--- a/templates/definition/meter/fox-ess-h3.yaml
+++ b/templates/definition/meter/fox-ess-h3.yaml
@@ -19,6 +19,7 @@ params:
   - name: maxsoc
     type: int
     advanced: true
+  - name: maxacpower
 render: |
   type: custom
   {{- if eq .usage "grid" }}
@@ -71,6 +72,7 @@ render: |
         address: 31005 # PV2
         type: holding
         decode: int16
+  maxacpower: {{ .maxacpower }}
   {{- end }}
   {{- if eq .usage "battery" }}
   power:
diff --git a/templates/definition/meter/huawei-smartlogger.yaml b/templates/definition/meter/huawei-smartlogger.yaml
index ffafd986..55da9550 100644
--- a/templates/definition/meter/huawei-smartlogger.yaml
+++ b/templates/definition/meter/huawei-smartlogger.yaml
@@ -18,6 +18,7 @@ params:
     default: 15s
   - name: capacity
     advanced: true
+  - name: maxacpower
 render: |
   type: custom
   {{- if eq .usage "grid" }}
@@ -84,6 +85,7 @@ render: |
       type: holding
       decode: uint32
     scale: 0.1
+  maxacpower: {{ .maxacpower }}
   {{- end }}
   {{- if eq .usage "battery" }}
   power:
diff --git a/templates/definition/meter/huawei-sun2000.yaml b/templates/definition/meter/huawei-sun2000.yaml
index a51725c9..7ecfc350 100644
--- a/templates/definition/meter/huawei-sun2000.yaml
+++ b/templates/definition/meter/huawei-sun2000.yaml
@@ -32,6 +32,7 @@ params:
     advanced: true
   - name: capacity
     advanced: true
+  - name: maxacpower
   - name: forceaccharging
     default: false
     advanced: true
@@ -110,6 +111,7 @@ render: |
       type: holding
       decode: uint32nan
     scale: 0.01
+  maxacpower: {{ .maxacpower }}
   {{- end }}
   {{- if eq .usage "battery" }}
   power:
diff --git a/templates/definition/meter/sofarsolar-g3.yaml b/templates/definition/meter/sofarsolar-g3.yaml
index baac1cd6..95798cd6 100644
--- a/templates/definition/meter/sofarsolar-g3.yaml
+++ b/templates/definition/meter/sofarsolar-g3.yaml
@@ -29,6 +29,7 @@ params:
     deprecated: true
   - name: capacity
     advanced: true
+  - name: maxacpower
   - name: defaultmode
     help:
       de: Standardmodus für die aktive Batteriesteuerung. Gültige Werte sind 0 (Eigenbedarfsmodus), 1 (Nutzungszeitmodus), 2 (Zeitmodus), 4 (Peak-shaving Modus)
@@ -125,6 +126,7 @@ render: |
       type: holding
       decode: uint32
     scale: 0.1
+  maxacpower: {{ .maxacpower }}
   {{- end }}
   {{- if eq .usage "battery" }}
   power:
diff --git a/templates/definition/meter/solaredge-inverter.yaml b/templates/definition/meter/solaredge-inverter.yaml
index 3bf5f179..fba5924b 100644
--- a/templates/definition/meter/solaredge-inverter.yaml
+++ b/templates/definition/meter/solaredge-inverter.yaml
@@ -102,4 +102,23 @@ render: |
       - 101:WH
       - 103:WH
     scale: 0.001
+  currents:
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      timeout: {{ .timeout }}
+      value:
+        - 101:AphA
+        - 103:AphA
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      timeout: {{ .timeout }}
+      value:
+        - 101:AphB
+        - 103:AphB
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      timeout: {{ .timeout }}
+      value:
+        - 101:AphC
+        - 103:AphC
   {{- end }}
diff --git a/templates/definition/meter/solarmax-maxstorage.yaml b/templates/definition/meter/solarmax-maxstorage.yaml
index 7bf178ad..d9858b65 100644
--- a/templates/definition/meter/solarmax-maxstorage.yaml
+++ b/templates/definition/meter/solarmax-maxstorage.yaml
@@ -17,6 +17,7 @@ params:
     id: 1
   - name: capacity
     advanced: true
+  - name: maxacpower
   # battery control
   - name: watchdog
     type: duration
@@ -41,6 +42,7 @@ render: |
       address: 110 # PV-Leistung MAX.STORAGE
       type: input
       decode: int32s
+  maxacpower: {{ .maxacpower }}
   {{- end }}
   {{- if eq .usage "battery" }}
     source: modbus
diff --git a/templates/definition/meter/sungrow-hybrid.yaml b/templates/definition/meter/sungrow-hybrid.yaml
index 32fcd8ad..af21190d 100644
--- a/templates/definition/meter/sungrow-hybrid.yaml
+++ b/templates/definition/meter/sungrow-hybrid.yaml
@@ -28,6 +28,7 @@ render: |
   power:
     source: modbus
     {{- include "modbus" . | indent 2 }}
+    timeout: {{ .timeout }}
     register:
       type: input
       address: 13009 # Export power
@@ -92,6 +93,7 @@ render: |
   power:
     source: modbus
     {{- include "modbus" . | indent 2 }}
+    timeout: {{ .timeout }}
     register:
       address: 5016 # Total DC power
       type: input
@@ -114,6 +116,7 @@ render: |
       abs:
         source: modbus
         {{- include "modbus" . | indent 6 }}
+        timeout: {{ .timeout }}
         register:
           type: input
           address: 13021 # Battery power
@@ -163,7 +166,6 @@ render: |
           set:
             source: modbus
             {{- include "modbus" . | indent 10 }}
-            timeout: {{ .timeout }}
             register:
               address: 13049 # EMS mode selection
               type: writesingle
@@ -173,7 +175,6 @@ render: |
           set:
             source: modbus
             {{- include "modbus" . | indent 10 }}
-            timeout: {{ .timeout }}
             register:
               address: 13050 # Charge/discharge command
               type: writesingle
@@ -187,7 +188,6 @@ render: |
           set:
             source: modbus
             {{- include "modbus" . | indent 10 }}
-            timeout: {{ .timeout }}
             register:
               address: 13049 # EMS mode selection
               type: writesingle
@@ -197,7 +197,6 @@ render: |
           set:
             source: modbus
             {{- include "modbus" . | indent 10 }}
-            timeout: {{ .timeout }}
             register:
               address: 13050 # Charge/discharge command
               type: writesingle
@@ -211,7 +210,6 @@ render: |
           set:
             source: modbus
             {{- include "modbus" . | indent 10 }}
-            timeout: {{ .timeout }}
             register:
               address: 13049 # EMS mode
               type: writesingle
@@ -221,7 +219,6 @@ render: |
           set:
             source: modbus
             {{- include "modbus" . | indent 10 }}
-            timeout: {{ .timeout }}
             register:
               address: 13050 # Charge/discharge command
               type: writesingle
@@ -232,7 +229,6 @@ render: |
           set:
             source: modbus
             {{- include "modbus" . | indent 10 }}
-            timeout: {{ .timeout }}
             register:
               address: 13051 # Charge power
               type: writesingle
@@ -246,7 +242,6 @@ render: |
             config:
               source: modbus
               {{- include "modbus" . | indent 12 }}
-              timeout: {{ .timeout }}
               register:
                 address: 5627 # BDC rated power
                 type: input
@@ -258,7 +253,6 @@ render: |
             config:
               source: modbus
               {{- include "modbus" . | indent 12 }}
-              timeout: {{ .timeout }}
               register:
                 address: 13051 # Charge/discharge power
                 type: writesingle
diff --git a/templates/definition/meter/victron-energy.yaml b/templates/definition/meter/victron-energy.yaml
index a046bd1b..2eeb1590 100644
--- a/templates/definition/meter/victron-energy.yaml
+++ b/templates/definition/meter/victron-energy.yaml
@@ -17,6 +17,7 @@ params:
     default: 502
   - name: capacity
     advanced: true
+  - name: maxacpower
   - name: meterid # grid meter VRM instance
     type: int
     usages: ["grid"]
@@ -138,6 +139,7 @@ render: |
         address: 850 # DC pv power
         type: input
         decode: uint16
+  maxacpower: {{ .maxacpower }}
   {{- end }}
   {{- if eq .usage "battery" }}
   power:
diff --git a/templates/definition/vehicle/tesla.yaml b/templates/definition/vehicle/tesla.yaml
index 7c2bc7c2..0cc19467 100644
--- a/templates/definition/vehicle/tesla.yaml
+++ b/templates/definition/vehicle/tesla.yaml
@@ -6,7 +6,7 @@ requirements:
   description:
     de: |
       Tesla bietet eine offizielle, aber kostenpflichtige Fahrzeug-API an.
-      Für private Nutzung kannst du dir einen [Tesla Developer Account](https://developer.tesla.com/) erstellen und erhältst ein monatliches API-Guthaben von $10. Das ist für die gängigen evcc-Anwendungsfälle in der Regel ausreichend.
+      Für private Nutzung kannst du dir einen [Tesla Developer Account](https://developer.tesla.com/) erstellen und erhältst ein monatliches API-Guthaben von 10$. Das ist für die gängigen evcc-Anwendungsfälle in der Regel ausreichend.
 
       Die Anleitung von [myteslamate.com](https://www.myteslamate.com/tesla-api-application-registration/) erklärt den Prozess und generiert dir kostenfrei die für evcc benötigten Access- und Refresh-Token.
       Mit diesem Tokenpaar und deiner im Tesla Developer Account erstellten Client ID kann evcc direkt mit der Tesla API kommunizieren. Dein verbrauchtes Guthaben kannst du bei Tesla einsehen.
@@ -14,7 +14,7 @@ requirements:
       Für die Nutzung des "Tesla Wall Connectors" benötigst du einen öffentlichen Command-Proxy-Server. [myteslamate.com](https://app.myteslamate.com/) stellt diesen Dienst für 12€/Jahr bereit. Konfiguriere dafür bei myteslamate.com die Command-Berechtigungen und trage das Proxy-Token hier ein. Start-, Stopp- und Stromstärken-Kommandos werden über diesen Proxy an Tesla geschickt.
     en: |
       Tesla offers an official, but paid vehicle API.
-      For private use, you can create a [Tesla Developer Account](https://developer.tesla.com/) and receive a monthly API credit of $10. This is usually sufficient for the common evcc use cases.
+      For private use, you can create a [Tesla Developer Account](https://developer.tesla.com/) and receive a monthly API credit of 10$. This is usually sufficient for the common evcc use cases.
 
       The [myteslamate.com](https://www.myteslamate.com/tesla-api-application-registration/) guide explains the process and generates a free Access and Refresh Token. With this token pair and your Client ID created in the Tesla Developer Account, evcc can directly communicate with the Tesla API. You can see your used credit at Tesla.
 
@@ -27,17 +27,16 @@ params:
       en: Client ID of your [Tesla Developer App](https://developer.tesla.com/dashboard).
       de: Kunden ID deiner [Tesla Developer App](https://developer.tesla.com/dashboard).
     required: true
-    advanced: true
   - name: accessToken
     help:
-      en: From [myteslamate.com](https://app.myteslamate.com/).
-      de: Von [myteslamate.com](https://app.myteslamate.com/).
+      en: from [myteslamate.com](https://app.myteslamate.com/).
+      de: von [myteslamate.com](https://app.myteslamate.com/).
     required: true
     mask: true
   - name: refreshToken
     help:
-      en: From [myteslamate.com](https://app.myteslamate.com/).
-      de: Von [myteslamate.com](https://app.myteslamate.com/).
+      en: from [myteslamate.com](https://app.myteslamate.com/).
+      de: von [myteslamate.com](https://app.myteslamate.com/).
     required: true
     mask: true
   - name: vin
diff --git a/vehicle/smart/hello/provider.go b/vehicle/smart/hello/provider.go
index 115aad4f..36abcfcd 100644
--- a/vehicle/smart/hello/provider.go
+++ b/vehicle/smart/hello/provider.go
@@ -35,7 +35,7 @@ var _ api.ChargeState = (*Provider)(nil)
 // Status implements the api.ChargeState interface
 func (v *Provider) Status() (api.ChargeStatus, error) {
 	res, err := v.statusG()
-	switch res.AdditionalVehicleStatus.ElectricVehicleStatus.ChargerState {
+	switch res.AdditionalVehicleStatus.ElectricVehicleStatus.StatusOfChargerConnection {
 	case 1, 3:
 		return api.StatusB, err
 	case 2:
diff --git a/vehicle/tesla/identity.go b/vehicle/tesla/identity.go
index 8f49b431..8640f2ba 100644
--- a/vehicle/tesla/identity.go
+++ b/vehicle/tesla/identity.go
@@ -34,7 +34,8 @@ func OAuth2Config(id, secret string) *oauth2.Config {
 type Identity struct {
 	oauth2.TokenSource
 	mu      sync.Mutex
-	rts     oauth2.TokenSource
+	log     *util.Logger
+	oc      *oauth2.Config
 	subject string
 }
 
@@ -59,6 +60,8 @@ func NewIdentity(log *util.Logger, oc *oauth2.Config, token *oauth2.Token) (oaut
 	}
 
 	v := &Identity{
+		log:     log,
+		oc:      oc,
 		subject: claims.Subject,
 	}
 
@@ -82,10 +85,6 @@ func NewIdentity(log *util.Logger, oc *oauth2.Config, token *oauth2.Token) (oaut
 
 	v.TokenSource = oauth.RefreshTokenSource(token, v)
 
-	// refresh token source
-	ctx := context.WithValue(context.Background(), oauth2.HTTPClient, request.NewClient(log))
-	v.rts = oc.TokenSource(ctx, token)
-
 	// add instance
 	addInstance(claims.Subject, v)
 
@@ -100,7 +99,9 @@ func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) {
 	v.mu.Lock()
 	defer v.mu.Unlock()
 
-	token, err := v.rts.Token()
+	// refresh token source
+	ctx := context.WithValue(context.Background(), oauth2.HTTPClient, request.NewClient(v.log))
+	token, err := v.oc.TokenSource(ctx, token).Token()
 	if err != nil {
 		return nil, err
 	}


Test patch:
diff --git a/core/loadpoint_effective_test.go b/core/loadpoint_effective_test.go
index f2165478..80c41e70 100644
--- a/core/loadpoint_effective_test.go
+++ b/core/loadpoint_effective_test.go
@@ -2,10 +2,13 @@ package core
 
 import (
 	"testing"
+	"time"
 
+	"github.com/benbjohnson/clock"
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 	"go.uber.org/mock/gomock"
 )
 
@@ -68,3 +71,37 @@ func TestEffectiveMinMaxCurrent(t *testing.T) {
 		assert.Equal(t, tc.effectiveMax, lp.effectiveMaxCurrent(), "max")
 	}
 }
+
+func TestNextPlan(t *testing.T) {
+	clock := clock.NewMock()
+
+	ctrl := gomock.NewController(t)
+	lp := NewLoadpoint(util.NewLogger("foo"), nil)
+	lp.charger = api.NewMockCharger(ctrl)
+
+	for _, tc := range []struct {
+		planId int
+		plans  []plan
+	}{
+		{1, []plan{
+			{Id: 1, End: clock.Now().Add(8 * time.Hour), Soc: 10},
+			{Id: 2, End: clock.Now().Add(10 * time.Hour), Soc: 10},
+		}},
+		{1, []plan{
+			{Id: 1, End: clock.Now().Add(8 * time.Hour), Soc: 20},
+			{Id: 2, End: clock.Now().Add(9 * time.Hour), Soc: 20},
+		}},
+		{2, []plan{
+			{Id: 2, End: clock.Now().Add(8 * time.Hour), Soc: 20},
+			{Id: 1, End: clock.Now().Add(9 * time.Hour), Soc: 20},
+		}},
+		{2, []plan{
+			{Id: 1, End: clock.Now().Add(8 * time.Hour), Soc: 10},
+			{Id: 2, End: clock.Now().Add(10 * time.Hour), Soc: 60},
+		}},
+	} {
+		res := lp.nextActivePlan(1e4, tc.plans)
+		require.NotNil(t, res)
+		assert.Equal(t, tc.planId, res.Id)
+	}
+}
diff --git a/tests/plan.spec.js b/tests/plan.spec.js
index e07ecf4c..ffe1eb58 100644
--- a/tests/plan.spec.js
+++ b/tests/plan.spec.js
@@ -497,6 +497,7 @@ test.describe("repeating", async () => {
     const plan1 = modal.getByTestId("plan-entry").nth(0);
     await plan1.getByTestId("static-plan-day").selectOption({ index: 1 });
     await plan1.getByTestId("static-plan-time").fill("09:30");
+    await plan1.getByTestId("static-plan-soc").selectOption("80%");
     await plan1.getByTestId("static-plan-active").click();
 
     // add repeating plan for tomorrow