Repository: evcc-io/evcc
Pull request #11766 (2024-01-19 17:26:33)

Fix Polestar api
Fix https://github.com/evcc-io/evcc/issues/11754


Patch:
diff --git a/.golangci.yml b/.golangci.yml
index 38095ae2..fc1095a9 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -19,7 +19,6 @@ linters:
     - exportloopref
     - gci
     - gofmt
-    - gofumpt
     - goimports
     - goprintffuncname
     - govet
diff --git a/README.md b/README.md
index 476464e9..c7430680 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,12 @@
 # evcc 🚘☀️
 
-[![Build Status](https://github.com/evcc-io/evcc/workflows/Build/badge.svg)](https://github.com/evcc-io/evcc/actions?query=workflow%3ABuild)
-[![Code Quality](https://goreportcard.com/badge/github.com/evcc-io/evcc)](https://goreportcard.com/report/github.com/evcc-io/evcc)
+[![Build](https://github.com/evcc-io/evcc/actions/workflows/nightly.yml/badge.svg)](https://github.com/evcc-io/evcc/actions/workflows/nightly.yml)
 [![Translation](https://hosted.weblate.org/widgets/evcc/-/evcc/svg-badge.svg)](https://hosted.weblate.org/engage/evcc/)
 [![Open in Visual Studio Code](https://img.shields.io/static/v1?logo=visualstudiocode&label=&message=Open%20in%20VS%20Code&labelColor=2c2c32&color=007acc&logoColor=007acc)](https://open.vscode.dev/evcc-io/evcc)
 [![OSS hosting by cloudsmith](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith)](https://cloudsmith.io/~evcc/packages/)
 [![Latest Version](https://img.shields.io/github/release/evcc-io/evcc.svg)](https://github.com/evcc-io/evcc/releases)
 
-evcc is an extensible EV Charge Controller with PV integration implemented in [Go][1]. Featured in [PV magazine](https://www.pv-magazine.de/2021/01/15/selbst-ist-der-groeoenlandhof-wallbox-ladesteuerung-selbst-gebaut/).
+evcc is an extensible EV Charge Controller and home energy management system. Featured in [PV magazine](https://www.pv-magazine.de/2021/01/15/selbst-ist-der-groeoenlandhof-wallbox-ladesteuerung-selbst-gebaut/).
 
 ![Screenshot](docs/screenshot.png)
 
diff --git a/api/api.go b/api/api.go
index 85ff24b5..37f907e0 100644
--- a/api/api.go
+++ b/api/api.go
@@ -107,15 +107,20 @@ type Authorizer interface {
 	Authorize(key string) error
 }
 
+// PhaseDescriber returns the number of availablephases
+type PhaseDescriber interface {
+	Phases() int
+}
+
 // Vehicle represents the EV and it's battery
 type Vehicle interface {
 	Battery
 	BatteryCapacity
 	IconDescriber
 	FeatureDescriber
+	PhaseDescriber
 	Title() string
 	SetTitle(string)
-	Phases() int
 	Identifiers() []string
 	OnIdentified() ActionConfig
 }
diff --git a/assets/js/components/Loadpoint.vue b/assets/js/components/Loadpoint.vue
index 5925dddb..5602d9f2 100644
--- a/assets/js/components/Loadpoint.vue
+++ b/assets/js/components/Loadpoint.vue
@@ -170,7 +170,8 @@ export default {
 		phases: Number,
 		phasesConfigured: Number,
 		phasesActive: Number,
-		phases1p3p: Boolean,
+		chargerPhases1p3p: Boolean,
+		chargerPhysicalPhases: Number,
 		minCurrent: Number,
 		maxCurrent: Number,
 		chargeCurrent: Number,
@@ -181,8 +182,6 @@ export default {
 		phaseRemaining: Number,
 		pvRemaining: Number,
 		pvAction: String,
-		guardRemaining: Number,
-		guardAction: String,
 		smartCostLimit: Number,
 		smartCostType: String,
 		smartCostActive: Boolean,
@@ -195,7 +194,6 @@ export default {
 			tickerHandler: null,
 			phaseRemainingInterpolated: this.phaseRemaining,
 			pvRemainingInterpolated: this.pvRemaining,
-			guardRemainingInterpolated: this.guardRemaining,
 			chargeDurationInterpolated: this.chargeDuration,
 			chargeRemainingDurationInterpolated: this.chargeRemainingDuration,
 		};
@@ -251,9 +249,6 @@ export default {
 		pvRemaining() {
 			this.pvRemainingInterpolated = this.pvRemaining;
 		},
-		guardRemaining() {
-			this.guardRemainingInterpolated = this.guardRemaining;
-		},
 		chargeDuration() {
 			this.chargeDurationInterpolated = this.chargeDuration;
 		},
@@ -275,9 +270,6 @@ export default {
 			if (this.pvRemainingInterpolated > 0) {
 				this.pvRemainingInterpolated--;
 			}
-			if (this.guardRemainingInterpolated > 0) {
-				this.guardRemainingInterpolated--;
-			}
 			if (this.chargeDurationInterpolated > 0 && this.charging) {
 				this.chargeDurationInterpolated++;
 			}
diff --git a/assets/js/components/LoadpointSettingsModal.vue b/assets/js/components/LoadpointSettingsModal.vue
index 68853330..f73eec78 100644
--- a/assets/js/components/LoadpointSettingsModal.vue
+++ b/assets/js/components/LoadpointSettingsModal.vue
@@ -26,7 +26,7 @@
 							<h4 class="d-flex align-items-center mb-3 mt-4 text-evcc">
 								{{ $t("main.loadpointSettings.currents") }}
 							</h4>
-							<div class="mb-3 row">
+							<div v-if="phasesOptions.length" class="mb-3 row">
 								<label
 									:for="formId('phases_0')"
 									class="col-sm-4 col-form-label pt-0"
@@ -34,7 +34,7 @@
 									{{ $t("main.loadpointSettings.phasesConfigured.label") }}
 								</label>
 								<div class="col-sm-8 pe-0">
-									<p v-if="!phases1p3p" class="mt-0 mb-2">
+									<p v-if="!chargerPhases1p3p" class="mt-0 mb-2">
 										<small>
 											{{
 												$t(
@@ -166,7 +166,8 @@ export default {
 		id: [String, Number],
 		phasesConfigured: Number,
 		phasesActive: Number,
-		phases1p3p: Boolean,
+		chargerPhases1p3p: Boolean,
+		chargerPhysicalPhases: Number,
 		minSoc: Number,
 		maxCurrent: Number,
 		minCurrent: Number,
@@ -182,13 +183,19 @@ export default {
 	},
 	computed: {
 		phasesOptions: function () {
-			if (this.phases1p3p) {
+			if (this.chargerPhysicalPhases == 1) {
+				// known fixed phase configuration, no settings required
+				return [];
+			}
+			if (this.chargerPhases1p3p) {
+				// automatic switching
 				return [PHASES_AUTO, PHASES_3, PHASES_1];
 			}
+			// 1p or 3p possible
 			return [PHASES_3, PHASES_1];
 		},
 		maxPower: function () {
-			if (this.phases1p3p) {
+			if (this.chargerPhases1p3p) {
 				if (this.phasesConfigured === PHASES_AUTO) {
 					return this.maxPowerPhases(3);
 				}
@@ -199,7 +206,7 @@ export default {
 			return this.fmtKw(this.maxCurrent * V * this.phasesActive);
 		},
 		minPower: function () {
-			if (this.phases1p3p) {
+			if (this.chargerPhases1p3p) {
 				if (this.phasesConfigured === PHASES_AUTO) {
 					return this.minPowerPhases(1);
 				}
diff --git a/assets/js/components/Vehicle.story.vue b/assets/js/components/Vehicle.story.vue
index b3a7f67a..04b4e776 100644
--- a/assets/js/components/Vehicle.story.vue
+++ b/assets/js/components/Vehicle.story.vue
@@ -108,9 +108,6 @@ const hoursFromNow = function (hours) {
 				:pvRemainingInterpolated="155"
 			/>
 		</Variant>
-		<Variant title="guard timer">
-			<Vehicle v-bind="state" guardAction="enable" :guardRemainingInterpolated="123" />
-		</Variant>
 		<Variant title="vehicle switch">
 			<Vehicle
 				v-bind="state"
diff --git a/assets/js/components/Vehicle.vue b/assets/js/components/Vehicle.vue
index a7933146..901dec53 100644
--- a/assets/js/components/Vehicle.vue
+++ b/assets/js/components/Vehicle.vue
@@ -95,8 +95,6 @@ export default {
 		effectivePlanSoc: Number,
 		effectivePlanTime: String,
 		enabled: Boolean,
-		guardAction: String,
-		guardRemainingInterpolated: Number,
 		heating: Boolean,
 		id: [String, Number],
 		integratedDevice: Boolean,
diff --git a/assets/js/components/VehicleStatus.vue b/assets/js/components/VehicleStatus.vue
index c0399790..9f4c7258 100644
--- a/assets/js/components/VehicleStatus.vue
+++ b/assets/js/components/VehicleStatus.vue
@@ -25,8 +25,6 @@ export default {
 		phaseRemainingInterpolated: Number,
 		pvAction: String,
 		pvRemainingInterpolated: Number,
-		guardAction: String,
-		guardRemainingInterpolated: Number,
 		targetChargeDisabled: Boolean,
 		vehicleClimaterActive: Boolean,
 		smartCostLimit: Number,
@@ -48,9 +46,6 @@ export default {
 				this.pvRemainingInterpolated > 0 && ["enable", "disable"].includes(this.pvAction)
 			);
 		},
-		guardTimerActive() {
-			return this.guardRemainingInterpolated > 0 && this.guardAction === "enable";
-		},
 		message: function () {
 			const t = (key, data) => {
 				if (this.heating) {
@@ -128,12 +123,6 @@ export default {
 				});
 			}
 
-			if (this.guardTimerActive) {
-				return t("guard", {
-					remaining: this.fmtDuration(this.guardRemainingInterpolated),
-				});
-			}
-
 			if (this.charging) {
 				return t("charging");
 			}
diff --git a/assets/js/router.js b/assets/js/router.js
index 39f375b4..373c98a3 100644
--- a/assets/js/router.js
+++ b/assets/js/router.js
@@ -1,8 +1,17 @@
 import { createRouter, createWebHashHistory } from "vue-router";
-
+import Modal from "bootstrap/js/dist/modal";
 import Main from "./views/Main.vue";
 import { ensureCurrentLocaleMessages } from "./i18n";
 
+function hideAllModals() {
+  [...document.querySelectorAll(".modal.show")].forEach((modal) => {
+    const modalInstance = Modal.getInstance(modal);
+    if (modalInstance) {
+      modalInstance.hide();
+    }
+  });
+}
+
 export default function setupRouter(i18n) {
   const router = createRouter({
     history: createWebHashHistory(),
@@ -28,5 +37,8 @@ export default function setupRouter(i18n) {
     await ensureCurrentLocaleMessages(i18n.global);
     return true;
   });
+  router.afterEach(() => {
+    hideAllModals();
+  });
   return router;
 }
diff --git a/assets/js/views/ChargingSessions.vue b/assets/js/views/ChargingSessions.vue
index e043e18b..58107b4a 100644
--- a/assets/js/views/ChargingSessions.vue
+++ b/assets/js/views/ChargingSessions.vue
@@ -41,7 +41,6 @@
 									{{ $t("sessions.date") }}
 								</th>
 								<th
-									v-if="showLoadpoints"
 									scope="col"
 									class="align-top d-none d-md-table-cell"
 									data-testid="loadpoint"
@@ -61,7 +60,6 @@
 									</CustomSelect>
 								</th>
 								<th
-									v-if="showVehicles"
 									scope="col"
 									class="align-top d-none d-md-table-cell"
 									data-testid="vehicle"
@@ -81,10 +79,7 @@
 									</CustomSelect>
 								</th>
 								<th scope="col" class="align-top d-md-none text-truncate">
-									<div
-										v-if="showLoadpoints"
-										class="d-flex flex-wrap text-truncate"
-									>
+									<div class="d-flex flex-wrap text-truncate">
 										<div class="me-2 text-truncate">
 											{{ $t("sessions.loadpoint") }}
 										</div>
@@ -103,10 +98,7 @@
 											</span>
 										</CustomSelect>
 									</div>
-									<div
-										class="text-truncate"
-										:class="{ 'd-flex flex-wrap': showLoadpoints }"
-									>
+									<div class="text-truncate d-flex flex-wrap">
 										<div class="me-2 text-truncate">
 											{{ $t("sessions.vehicle") }}
 										</div>
@@ -154,16 +146,8 @@
 								<th scope="col" class="align-top ps-0">
 									{{ $t("sessions.total") }}
 								</th>
-								<th
-									v-if="showLoadpoints"
-									scope="col"
-									class="d-none d-md-table-cell"
-								></th>
-								<th
-									v-if="showVehicles"
-									scope="col"
-									class="d-none d-md-table-cell"
-								></th>
+								<th scope="col" class="d-none d-md-table-cell"></th>
+								<th scope="col" class="d-none d-md-table-cell"></th>
 								<th scope="col" class="d-md-none"></th>
 								<th
 									v-for="column in columnsPerBreakpoint"
@@ -187,14 +171,14 @@
 								<td class="ps-0">
 									{{ fmtFullDateTime(new Date(session.created), true) }}
 								</td>
-								<td v-if="showLoadpoints" class="d-none d-md-table-cell">
+								<td class="d-none d-md-table-cell">
 									{{ session.loadpoint }}
 								</td>
-								<td v-if="showVehicles" class="d-none d-md-table-cell">
+								<td class="d-none d-md-table-cell">
 									{{ session.vehicle }}
 								</td>
 								<td class="d-md-none text-truncate">
-									<div v-if="showLoadpoints">{{ session.loadpoint }}</div>
+									<div>{{ session.loadpoint }}</div>
 									<div>{{ session.vehicle }}</div>
 								</td>
 								<td
@@ -453,20 +437,6 @@ export default {
 			}
 			return null;
 		},
-		showVehicles() {
-			return this.hasMultipleVehicles || this.vehicleFilter;
-		},
-		showLoadpoints() {
-			return this.hasMultipleLoadpoints || this.loadpointFilter;
-		},
-		hasMultipleVehicles() {
-			const vehicles = this.currentSessions.map((s) => s.vehicle);
-			return new Set(vehicles).size > 1;
-		},
-		hasMultipleLoadpoints() {
-			const loadpoints = this.currentSessions.map((s) => s.loadpoint);
-			return new Set(loadpoints).size > 1;
-		},
 		pricePerKWh() {
 			const total = this.filteredSessions
 				.filter((s) => s.price !== null)
diff --git a/charger/amperfied.go b/charger/amperfied.go
index d4457bd3..9cae1836 100644
--- a/charger/amperfied.go
+++ b/charger/amperfied.go
@@ -61,8 +61,8 @@ func init() {
 // NewAmperfiedFromConfig creates a Amperfied charger from generic config
 func NewAmperfiedFromConfig(other map[string]interface{}) (api.Charger, error) {
 	cc := struct {
-		modbus.TcpSettings
-		Phases1p3p bool
+		modbus.TcpSettings `mapstructure:",squash"`
+		Phases1p3p         bool
 	}{
 		TcpSettings: modbus.TcpSettings{
 			ID: 255,
diff --git a/charger/switchsocket.go b/charger/switchsocket.go
index 99ef1333..45b5193a 100644
--- a/charger/switchsocket.go
+++ b/charger/switchsocket.go
@@ -98,3 +98,10 @@ var _ loadpoint.Controller = (*switchSocket)(nil)
 func (c *switchSocket) LoadpointControl(lp loadpoint.API) {
 	c.lp = lp
 }
+
+var _ api.PhaseDescriber = (*switchSocket)(nil)
+
+// Phases implements the api.PhasesDescriber interface
+func (v *switchSocket) Phases() int {
+	return 1
+}
diff --git a/charger/victron.go b/charger/victron.go
index 00f72ec0..2c4ca75d 100644
--- a/charger/victron.go
+++ b/charger/victron.go
@@ -25,7 +25,6 @@ import (
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/modbus"
-	"github.com/evcc-io/evcc/util/sponsor"
 	"github.com/spf13/cast"
 )
 
@@ -102,10 +101,6 @@ func NewVictron(uri string, slaveID uint8, regs victronRegs) (api.Charger, error
 		return nil, err
 	}
 
-	if !sponsor.IsAuthorized() {
-		return nil, api.ErrSponsorRequired
-	}
-
 	log := util.NewLogger("victron")
 	conn.Logger(log.TRACE)
 
diff --git a/cmd/configure/configure.tpl b/cmd/configure/configure.tpl
index 07c85359..b3a02086 100644
--- a/cmd/configure/configure.tpl
+++ b/cmd/configure/configure.tpl
@@ -54,9 +54,6 @@ loadpoints:
   vehicle: {{ .Vehicle }}
 {{- end }}
   mode: {{ .Mode }}
-  phases: {{ .Phases }}
-  mincurrent: {{ .MinCurrent }}
-  maxcurrent: {{ .MaxCurrent }}
 {{- end }}
 {{- end }}
 
diff --git a/cmd/flags.go b/cmd/flags.go
index 5e019e33..0436d4be 100644
--- a/cmd/flags.go
+++ b/cmd/flags.go
@@ -40,6 +40,7 @@ const (
 
 	flagDigits = "digits"
 	flagDelay  = "delay"
+	flagForce  = "force"
 )
 
 func bind(cmd *cobra.Command, key string, flagName ...string) {
diff --git a/cmd/settings-get.go b/cmd/settings-get.go
index 325db4a0..12f4ecc8 100644
--- a/cmd/settings-get.go
+++ b/cmd/settings-get.go
@@ -3,9 +3,7 @@ package cmd
 import (
 	"fmt"
 	"os"
-	"os/signal"
 	"regexp"
-	"syscall"
 	"text/tabwriter"
 
 	"github.com/evcc-io/evcc/server/db/settings"
@@ -49,14 +47,4 @@ func runSettingsGet(cmd *cobra.Command, args []string) {
 		fmt.Fprintf(w, "%s:\t%s\n", s.Key, s.Value)
 	}
 	w.Flush()
-
-	// catch signals
-	go func() {
-		signalC := make(chan os.Signal, 1)
-		signal.Notify(signalC, os.Interrupt, syscall.SIGTERM)
-
-		<-signalC // wait for signal
-
-		os.Exit(1)
-	}()
 }
diff --git a/cmd/settings-set.go b/cmd/settings-set.go
new file mode 100644
index 00000000..cfdbabe2
--- /dev/null
+++ b/cmd/settings-set.go
@@ -0,0 +1,57 @@
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/AlecAivazis/survey/v2"
+	"github.com/evcc-io/evcc/server/db/settings"
+	"github.com/spf13/cobra"
+)
+
+// settingsSetCmd represents the configure command
+var settingsSetCmd = &cobra.Command{
+	Use:   "set",
+	Short: "Set configuration setting",
+	Run:   runSettingsSet,
+	Args:  cobra.ExactArgs(2),
+}
+
+func init() {
+	settingsCmd.AddCommand(settingsSetCmd)
+	settingsSetCmd.Flags().BoolP(flagForce, "f", false, "Force (no confirmation)")
+}
+
+func runSettingsSet(cmd *cobra.Command, args []string) {
+	// load config
+	if err := loadConfigFile(&conf); err != nil {
+		log.FATAL.Fatal(err)
+	}
+
+	// setup environment
+	if err := configureEnvironment(cmd, conf); err != nil {
+		log.FATAL.Fatal(err)
+	}
+
+	confirmation, _ := cmd.Flags().GetBool(flagForce)
+	if !confirmation {
+		msg := fmt.Sprintf("Set %s", args[0])
+		if val, _ := settings.String(args[0]); val != "" {
+			msg = fmt.Sprintf("Override %s (current value: %s)", args[0], val)
+		}
+
+		prompt := &survey.Confirm{
+			Message: msg,
+		}
+
+		if err := survey.AskOne(prompt, &confirmation); err != nil {
+			log.FATAL.Fatal(err)
+		}
+	}
+
+	if confirmation {
+		settings.SetString(args[0], args[1])
+	}
+
+	// wait for shutdown
+	<-shutdownDoneC()
+}
diff --git a/core/keys/loadpoint.go b/core/keys/loadpoint.go
index 2fb543d1..8fd627a4 100644
--- a/core/keys/loadpoint.go
+++ b/core/keys/loadpoint.go
@@ -14,12 +14,13 @@ const (
 	DisableThreshold = "disableThreshold"
 
 	PhasesConfigured = "phasesConfigured" // configured phases (1/3, 0 for auto on 1p3p chargers, nil for plain chargers)
-	Phases1p3p       = "phases1p3p"       // phase switcher (1p3p chargers)
 	PhasesEnabled    = "phasesEnabled"    // enabled phases (1/3)
 	PhasesActive     = "phasesActive"     // active phases as used by vehicle (1/2/3)
 
-	ChargerIcon    = "chargerIcon"    // charger icon for ui
-	ChargerFeature = "chargerFeature" // charger feature
+	ChargerIcon           = "chargerIcon"           // charger icon for ui
+	ChargerFeature        = "chargerFeature"        // charger feature
+	ChargerPhysicalPhases = "chargerPhysicalPhases" // charger phases
+	ChargerPhases1p3p     = "chargerPhases1p3p"     // phase switcher (1p3p chargers)
 
 	// loadpoint status
 	Enabled   = "enabled"   // loadpoint enabled
diff --git a/core/loadpoint.go b/core/loadpoint.go
index a46db590..d16ac59e 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -43,9 +43,6 @@ const (
 	pvEnable  = "enable"
 	pvDisable = "disable"
 
-	guardTimer  = "guard"
-	guardEnable = "enable"
-
 	phaseTimer   = "phase"
 	phaseScale1p = "scale1p"
 	phaseScale3p = "scale3p"
@@ -55,9 +52,8 @@ const (
 	minActiveCurrent = 1.0 // minimum current at which a phase is treated as active
 	minActiveVoltage = 207 // minimum voltage at which a phase is treated as active
 
-	guardGracePeriod          = 60 * time.Second // allow out of sync during this timespan
-	phaseSwitchCommandTimeout = 30 * time.Second // do not sync charger enabled/disabled state during this timespan
-	phaseSwitchDuration       = 60 * time.Second // do not measure phases during this timespan
+	chargerSwitchDuration = 60 * time.Second // allow out of sync during this timespan
+	phaseSwitchDuration   = 60 * time.Second // allow out of sync and do not measure phases during this timespan
 )
 
 // elapsed is the time an expired timer will be set to
@@ -116,12 +112,12 @@ type Loadpoint struct {
 	MeterRef        string `mapstructure:"meter"`    // Charge meter reference
 	Soc             SocConfig
 	Enable, Disable ThresholdConfig
-	GuardDuration   time.Duration // charger enable/disable minimum holding time
 
 	// TODO deprecated
-	ConfiguredPhases_ int     `mapstructure:"phases"`
-	MinCurrent_       float64 `mapstructure:"minCurrent"`
-	MaxCurrent_       float64 `mapstructure:"maxCurrent"`
+	GuardDuration_    time.Duration `mapstructure:"guardduration"` // charger enable/disable minimum holding time
+	ConfiguredPhases_ int           `mapstructure:"phases"`
+	MinCurrent_       float64       `mapstructure:"minCurrent"`
+	MaxCurrent_       float64       `mapstructure:"maxCurrent"`
 
 	minCurrent       float64 // PV mode: start current	Min+PV mode: min current
 	maxCurrent       float64 // Max allowed current. Physically ensured by the charger
@@ -134,9 +130,9 @@ type Loadpoint struct {
 	phases              int       // Charger enabled phases, guarded by mutex
 	measuredPhases      int       // Charger physically measured phases
 	chargeCurrent       float64   // Charger current limit
-	guardUpdated        time.Time // Charger enabled/disabled timestamp
 	socUpdated          time.Time // Soc updated timestamp (poll: connected)
 	vehicleDetect       time.Time // Vehicle connected timestamp
+	chargerSwitched     time.Time // Charger enabled/disabled timestamp
 	phasesSwitched      time.Time // Phase switch timestamp
 	vehicleDetectTicker *clock.Ticker
 	vehicleIdentifier   string
@@ -295,7 +291,6 @@ func NewLoadpoint(log *util.Logger, settings *Settings) *Loadpoint {
 		},
 		Enable:        ThresholdConfig{Delay: time.Minute, Threshold: 0},     // t, W
 		Disable:       ThresholdConfig{Delay: 3 * time.Minute, Threshold: 0}, // t, W
-		GuardDuration: 5 * time.Minute,
 		sessionEnergy: NewEnergyMetrics(),
 		progress:      NewProgress(0, 10),     // soc progress indicator
 		coordinator:   coordinator.NewDummy(), // dummy vehicle coordinator
@@ -595,12 +590,17 @@ func (lp *Loadpoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
 	lp.publish(keys.DisableThreshold, lp.Disable.Threshold)
 
 	lp.publish(keys.PhasesConfigured, lp.configuredPhases)
-	lp.publish(keys.Phases1p3p, lp.hasPhaseSwitching())
+	lp.publish(keys.ChargerPhases1p3p, lp.hasPhaseSwitching())
 	lp.publish(keys.PhasesEnabled, lp.phases)
 	lp.publish(keys.PhasesActive, lp.ActivePhases())
 	lp.publishTimer(phaseTimer, 0, timerInactive)
 	lp.publishTimer(pvTimer, 0, timerInactive)
-	lp.publishTimer(guardTimer, 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} {
@@ -635,7 +635,6 @@ func (lp *Loadpoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
 	// read initial charger state to prevent immediately disabling charger
 	if enabled, err := lp.charger.Enabled(); err == nil {
 		if lp.enabled = enabled; enabled {
-			lp.guardUpdated = lp.clock.Now()
 			// set defined current for use by pv mode
 			_ = lp.setLimit(lp.effectiveMinCurrent(), false)
 		}
@@ -656,7 +655,7 @@ func (lp *Loadpoint) syncCharger() error {
 		return err
 	}
 
-	if lp.guardGracePeriodElapsed() {
+	if lp.chargerUpdateCompleted() {
 		defer func() {
 			lp.enabled = enabled
 			lp.publish(keys.Enabled, lp.enabled)
@@ -666,11 +665,10 @@ func (lp *Loadpoint) syncCharger() error {
 	if !enabled && lp.charging() {
 		lp.log.WARN.Println("charger logic error: disabled but charging")
 		enabled = true // treat as enabled when charging
-		if lp.guardGracePeriodElapsed() {
+		if lp.chargerUpdateCompleted() {
 			if err := lp.charger.Enable(true); err != nil { // also enable charger to correct internal state
 				return err
 			}
-			lp.elapseGuard()
 			lp.elapsePVTimer()
 			return nil
 		}
@@ -687,10 +685,9 @@ func (lp *Loadpoint) syncCharger() error {
 
 			// smallest adjustment most PWM-Controllers can do is: 100%÷256×0,6A = 0.234A
 			if math.Abs(lp.chargeCurrent-current) > 0.23 {
-				if lp.guardGracePeriodElapsed() {
+				if lp.chargerUpdateCompleted() {
 					lp.log.WARN.Printf("charger logic error: current mismatch (got %.3gA, expected %.3gA)", current, lp.chargeCurrent)
 				}
-
 				lp.chargeCurrent = current
 				lp.bus.Publish(evChargeCurrent, lp.chargeCurrent)
 			}
@@ -699,13 +696,9 @@ func (lp *Loadpoint) syncCharger() error {
 		return nil
 	}
 
-	if enabled || lp.phaseSwitchCommandTimeoutElapsed() {
-		// ignore disabled state if vehicle was disconnected ^(lp.enabled && ^lp.connected)
-		if lp.guardGracePeriodElapsed() && lp.phaseSwitchCompleted() && (enabled || lp.connected()) {
-			lp.log.WARN.Printf("charger out of sync: expected %vd, got %vd", status[lp.enabled], status[enabled])
-			lp.elapseGuard()
-		}
-		return nil
+	// ignore disabled state if vehicle was disconnected ^(lp.enabled && ^lp.connected)
+	if lp.chargerUpdateCompleted() && lp.phaseSwitchCompleted() && (enabled || lp.connected()) {
+		lp.log.WARN.Printf("charger out of sync: expected %vd, got %vd", status[lp.enabled], status[enabled])
 	}
 
 	return nil
@@ -746,12 +739,6 @@ func (lp *Loadpoint) setLimit(chargeCurrent float64, force bool) error {
 		lp.bus.Publish(evChargeCurrent, chargeCurrent)
 	}
 
-	if lp.clock.Since(lp.guardUpdated).Truncate(time.Second) < lp.GuardDuration && !force {
-		lp.publishTimer(guardTimer, lp.GuardDuration, guardEnable)
-		return nil
-	}
-	lp.elapseGuard()
-
 	// set enabled/disabled
 	if enabled := chargeCurrent >= lp.effectiveMinCurrent(); enabled != lp.enabled {
 		if err := lp.charger.Enable(enabled); err != nil {
@@ -771,7 +758,7 @@ func (lp *Loadpoint) setLimit(chargeCurrent float64, force bool) error {
 		lp.log.DEBUG.Printf("charger %s", status[enabled])
 		lp.enabled = enabled
 		lp.publish(keys.Enabled, lp.enabled)
-		lp.guardUpdated = lp.clock.Now()
+		lp.chargerSwitched = lp.clock.Now()
 
 		lp.bus.Publish(evChargeCurrent, chargeCurrent)
 
@@ -962,8 +949,6 @@ func (lp *Loadpoint) elapsePVTimer() {
 
 	lp.pvTimer = elapsed
 	lp.publishTimer(pvTimer, 0, timerInactive)
-
-	lp.elapseGuard()
 }
 
 // resetPVTimer resets the pv enable/disable timer to disabled state
@@ -1055,7 +1040,7 @@ func (lp *Loadpoint) pvScalePhases(sitePower, minCurrent, maxCurrent float64) bo
 	// - https://github.com/evcc-io/evcc/issues/2613
 	measuredPhases := lp.getMeasuredPhases()
 	if phases > 0 && phases < measuredPhases {
-		if lp.guardGracePeriodElapsed() {
+		if lp.chargerUpdateCompleted() {
 			lp.log.WARN.Printf("ignoring inconsistent phases: %dp < %dp observed active", phases, measuredPhases)
 		}
 		lp.resetMeasuredPhases()
@@ -1134,9 +1119,6 @@ func (lp *Loadpoint) publishTimer(name string, delay time.Duration, action strin
 	if name == phaseTimer {
 		timer = lp.phaseTimer
 	}
-	if name == guardTimer {
-		timer = lp.guardUpdated
-	}
 
 	remaining := delay - lp.clock.Since(timer)
 	if remaining < 0 {
@@ -1178,9 +1160,15 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batter
 	}
 
 	if mode == api.ModePV && lp.enabled && targetCurrent < minCurrent {
+		projectedSitePower := sitePower
+		if !lp.phaseTimer.IsZero() {
+			// calculate site power after a phase switch from activePhases phases -> 1 phase
+			// notes: activePhases can be 1, 2 or 3 and phaseTimer can only be active if lp current is already at minCurrent
+			projectedSitePower -= Voltage * minCurrent * float64(activePhases-1)
+		}
 		// kick off disable sequence
-		if sitePower >= lp.Disable.Threshold && lp.phaseTimer.IsZero() {
-			lp.log.DEBUG.Printf("site power %.0fW >= %.0fW disable threshold", sitePower, lp.Disable.Threshold)
+		if projectedSitePower >= lp.Disable.Threshold {
+			lp.log.DEBUG.Printf("projected site power %.0fW >= %.0fW disable threshold", projectedSitePower, lp.Disable.Threshold)
 
 			if lp.pvTimer.IsZero() {
 				lp.log.DEBUG.Printf("pv disable timer start: %v", lp.Disable.Delay)
@@ -1460,14 +1448,6 @@ func (lp *Loadpoint) publishSocAndRange() {
 	}
 }
 
-func (lp *Loadpoint) elapseGuard() {
-	if lp.guardUpdated != elapsed {
-		lp.log.DEBUG.Print("charger: guard elapse")
-		lp.guardUpdated = elapsed
-		lp.publishTimer(guardTimer, 0, timerInactive)
-	}
-}
-
 // addTask adds a single task to the queue
 func (lp *Loadpoint) addTask(task func()) {
 	// test guard
@@ -1503,17 +1483,12 @@ func (lp *Loadpoint) stopWakeUpTimer() {
 	lp.wakeUpTimer.Stop()
 }
 
-// guardGracePeriodElapsed checks if last guard update is within guard grace period
-func (lp *Loadpoint) guardGracePeriodElapsed() bool {
-	return time.Since(lp.guardUpdated) > guardGracePeriod
-}
-
-// phaseSwitchCommandTimeoutElapsed returns true if phase switch command should be already processed by the charger
-func (lp *Loadpoint) phaseSwitchCommandTimeoutElapsed() bool {
-	return time.Since(lp.phasesSwitched) > phaseSwitchCommandTimeout
+// chargerUpdateCompleted returns true if enable command should be already processed by the charger (so we can try to sync charger and loadpoint)
+func (lp *Loadpoint) chargerUpdateCompleted() bool {
+	return time.Since(lp.chargerSwitched) > chargerSwitchDuration
 }
 
-// phaseSwitchCompleted returns true if phase switch has completed
+// phaseSwitchCompleted returns true if phase switch command should be already processed by the charger (so we can try to sync charger and loadpoint and are able to measure currents)
 func (lp *Loadpoint) phaseSwitchCompleted() bool {
 	return time.Since(lp.phasesSwitched) > phaseSwitchDuration
 }
diff --git a/core/loadpoint_phases.go b/core/loadpoint_phases.go
index b2b96d69..e665e432 100644
--- a/core/loadpoint_phases.go
+++ b/core/loadpoint_phases.go
@@ -62,8 +62,9 @@ func (lp *Loadpoint) ActivePhases() int {
 	physical := lp.GetPhases()
 	vehicle := lp.getVehiclePhases()
 	measured := lp.getMeasuredPhases()
+	charger := lp.getChargerPhysicalPhases()
 
-	active := min(expect(vehicle), expect(physical), expect(measured))
+	active := min(expect(vehicle), expect(physical), expect(measured), expect(charger))
 
 	// sanity check - we should not assume less active phases than actually measured
 	if measured > 0 && active < measured {
@@ -78,6 +79,7 @@ func (lp *Loadpoint) maxActivePhases() int {
 	physical := lp.GetPhases()
 	measured := lp.getMeasuredPhases()
 	vehicle := lp.getVehiclePhases()
+	charger := lp.getChargerPhysicalPhases()
 
 	// during 1p or unknown config, 1p measured is not a restriction
 	if physical <= 1 || vehicle == 1 {
@@ -89,7 +91,7 @@ func (lp *Loadpoint) maxActivePhases() int {
 		physical = lp.configuredPhases
 	}
 
-	return min(expect(vehicle), expect(physical), expect(measured))
+	return min(expect(vehicle), expect(physical), expect(measured), expect(charger))
 }
 
 func (lp *Loadpoint) getVehiclePhases() int {
@@ -100,6 +102,14 @@ func (lp *Loadpoint) getVehiclePhases() int {
 	return 0
 }
 
+func (lp *Loadpoint) getChargerPhysicalPhases() int {
+	if cc, ok := lp.charger.(api.PhaseDescriber); ok {
+		return cc.Phases()
+	}
+
+	return 0
+}
+
 func (lp *Loadpoint) hasPhaseSwitching() bool {
 	_, ok := lp.charger.(api.PhaseSwitcher)
 	return ok
diff --git a/core/site.go b/core/site.go
index c09327b9..2d38e870 100644
--- a/core/site.go
+++ b/core/site.go
@@ -242,13 +242,13 @@ func (site *Site) restoreMeters() {
 		site.Meters.GridMeterRef = v
 	}
 	if v, err := settings.String(keys.PvMeters); err == nil && v != "" {
-		site.Meters.PVMetersRef = append(site.Meters.PVMetersRef, strings.Split(v, ",")...)
+		site.Meters.PVMetersRef = append(site.Meters.PVMetersRef, filterConfigurable(strings.Split(v, ","))...)
 	}
 	if v, err := settings.String(keys.BatteryMeters); err == nil && v != "" {
-		site.Meters.BatteryMetersRef = append(site.Meters.BatteryMetersRef, strings.Split(v, ",")...)
+		site.Meters.BatteryMetersRef = append(site.Meters.BatteryMetersRef, filterConfigurable(strings.Split(v, ","))...)
 	}
 	if v, err := settings.String(keys.AuxMeters); err == nil && v != "" {
-		site.Meters.AuxMetersRef = append(site.Meters.AuxMetersRef, strings.Split(v, ",")...)
+		site.Meters.AuxMetersRef = append(site.Meters.AuxMetersRef, filterConfigurable(strings.Split(v, ","))...)
 	}
 }
 
diff --git a/evcc.dist.yaml b/evcc.dist.yaml
index 2ba2a2b8..6dc052bf 100644
--- a/evcc.dist.yaml
+++ b/evcc.dist.yaml
@@ -138,7 +138,6 @@ loadpoints:
     disable: # pv mode disable behavior
       delay: 3m # threshold must be exceeded for this long
       threshold: 0 # maximum import power (W)
-    guardDuration: 5m # switch charger contactor not more often than this (default 5m)
 
 # tariffs are the fixed or variable tariffs
 tariffs:
diff --git a/go.mod b/go.mod
index 966e768f..91681c91 100644
--- a/go.mod
+++ b/go.mod
@@ -62,7 +62,7 @@ require (
 	github.com/libp2p/zeroconf/v2 v2.2.0
 	github.com/lorenzodonini/ocpp-go v0.17.1-0.20231031074144-d214d7c42cbc
 	github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
-	github.com/mabunixda/wattpilot v1.6.4
+	github.com/mabunixda/wattpilot v1.6.3
 	github.com/manifoldco/promptui v0.9.0
 	github.com/mitchellh/go-homedir v1.1.0
 	github.com/mitchellh/mapstructure v1.5.0
@@ -208,3 +208,5 @@ replace github.com/grid-x/modbus => github.com/evcc-io/modbus v0.0.0-20230705160
 replace github.com/imdario/mergo => github.com/imdario/mergo v0.3.16
 
 replace github.com/spf13/viper => github.com/spf13/viper v1.18.1
+
+replace github.com/mabunixda/wattpilot => github.com/mabunixda/wattpilot v1.6.2
diff --git a/go.sum b/go.sum
index 0dd242b6..dd19c1f4 100644
--- a/go.sum
+++ b/go.sum
@@ -419,8 +419,8 @@ github.com/lorenzodonini/ocpp-go v0.17.1-0.20231031074144-d214d7c42cbc/go.mod h1
 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
 github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
-github.com/mabunixda/wattpilot v1.6.4 h1:548QvdloTnRqfZ3wBZJ3QMrI9UxIFwnCtFX26nYLLlI=
-github.com/mabunixda/wattpilot v1.6.4/go.mod h1:whn/S4zLvWajy6t/DE5pXABkD1I34rAE+ML607BL620=
+github.com/mabunixda/wattpilot v1.6.2 h1:xZE/elFntUJgaWTK2V7CJVQRcE+WvkT8g6dCk/oIG78=
+github.com/mabunixda/wattpilot v1.6.2/go.mod h1:2tcdLRoqycO6bWYAjZMP9g1giMgw4t2MnbN3VxaCN1Q=
 github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
 github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
 github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
diff --git a/i18n/ca.toml b/i18n/ca.toml
index 290b558f..493ed9ae 100644
--- a/i18n/ca.toml
+++ b/i18n/ca.toml
@@ -241,7 +241,6 @@ cleanEnergyCharging = "Energia neta disponible. Carregant..."
 climating = "Precondicionament detectat."
 connected = "Connectat."
 disconnected = "Desconnectat."
-guard = "Protecció de commutació activa durant {remaining}"
 minCharge = "Càrrega mínima fins a {soc}%."
 pvDisable = "Excedent insuficient. Pausa en {remaining}..."
 pvEnable = "Excedent disponible. Començar en {remaining}..."
diff --git a/i18n/da.toml b/i18n/da.toml
index 323fe633..03279c64 100644
--- a/i18n/da.toml
+++ b/i18n/da.toml
@@ -261,6 +261,7 @@ solar = "solenergi"
 [main.loadpointSettings]
 currents = "Ladestrøm"
 default = "standard"
+disclaimerHint = "Bemærk:"
 onlyForSocBasedCharging = "Disse indstillinger er kun tilgængelig for køretøjer med kendt opladningsniveau"
 title = "Indstillinger {0}"
 vehicle = "Køretøj"
@@ -281,6 +282,7 @@ label = "Min. opladning %"
 
 [main.loadpointSettings.phasesConfigured]
 label = "Faser"
+no1p3pSupport = "Hvordan er din oplader forbundet"
 phases_0 = "automatisk skift"
 phases_1 = "1 fase"
 phases_1_hint = "({min} til {max})"
@@ -368,7 +370,6 @@ cleanEnergyCharging = "Oplader med CO2 neutral energi: {co2} (limit {limit})"
 climating = "Forkonditionering registreret."
 connected = "Forbundet."
 disconnected = "Afbrudt."
-guard = "Switching-beskyttelse er aktiv i {remaining}…"
 minCharge = "Minimum opladning til {soc} %."
 pvDisable = "Ikke nok overskud. Pause i {remaining}..."
 pvEnable = "Overskud tilgængeligt. Starter om {remaining}..."
diff --git a/i18n/de.toml b/i18n/de.toml
index ccaef28d..b5e5f69f 100644
--- a/i18n/de.toml
+++ b/i18n/de.toml
@@ -23,15 +23,59 @@ never = "nur mit genug PV-Überschuss"
 
 [config]
 
+[config.battery]
+titleAdd = "Batterie Zähler hinzufügen"
+titleEdit = "Batterie Zähler bearbeiten"
+
+[config.deviceValue]
+capacity = "Kapazität"
+chargeStatus = "Status"
+chargedEnergy = "Geladen"
+current = "Aktuell"
+enabled = "Aktiviert"
+energy = "Energie"
+odometer = "Kilometerstand"
+phaseCurrents = "Strom L1..L3"
+phasePowers = "Leistung L1..L3"
+phaseVoltages = "Spannung L1..L3"
+power = "Leistung"
+range = "Reichweite"
+soc = "SoC"
+socLimit = "SoC Begrenzung"
+
 [config.form]
 example = "Beispiel"
 optional = "optional"
 
+[config.grid]
+titleAdd = "Stromzähler (Netz) hinzufügen"
+titleEdit = "Stromzähler (Netz) bearbeiten"
+
 [config.main]
+addLoadpoint = "Ladepunkt hinzufügen"
+addPvBattery = "PV oder Speicher hinzufügen"
 addVehicle = "Fahrzeug hinzufügen"
 edit = "bearbeiten"
 title = "Konfiguration"
+unconfigured = "nicht konfiguriert"
 vehicles = "Meine Fahrzeuge"
+yaml = "Konfiguration in evcc.yaml gefunden. Nicht per UI editierbar"
+
+[config.meter]
+cancel = "Abbrechen"
+delete = "Löschen"
+save = "Speichern"
+template = "Hersteller"
+titleChoice = "Was möchtest du hinzufügen?"
+validateSave = "Überprüfen & speichern"
+
+[config.pv]
+titleAdd = "Zähler (PV) hinzufügen"
+titleEdit = "Zähler (PV) bearbeiten"
+
+[config.site]
+cancel = "Abbrechen"
+save = "Speichern"
 
 [config.validation]
 failed = "fehlgeschlagen"
@@ -213,6 +257,7 @@ solar = "Sonne"
 [main.loadpointSettings]
 currents = "Ladestrom"
 default = "default"
+disclaimerHint = "Hinweis:"
 onlyForSocBasedCharging = "Diese Optionen sind nur für Fahrzeuge mit bekanntem Ladestand verfügbar."
 title = "Einstellungen {0}"
 vehicle = "Fahrzeug"
@@ -298,7 +343,7 @@ fallbackName = "Fahrzeug"
 moreActions = "Weitere Aktionen"
 none = "Kein Fahrzeug"
 targetSoc = "Ladelimit"
-temp = "Temp."
+temp = "Temperatur"
 tempLimit = "Zieltemp."
 unknown = "Gastfahrzeug"
 vehicleSoc = "Ladestand"
@@ -317,7 +362,6 @@ cleanEnergyCharging = "Lade saubere Energie: {co2} (Grenze {limit})"
 climating = "Vorklimatisierung erkannt."
 connected = "Verbunden."
 disconnected = "Nicht verbunden."
-guard = "Schaltschutz aktiv für {remaining}."
 minCharge = "Mindestladung bis {soc}%."
 pvDisable = "Zu wenig Überschuss. Pausiere in {remaining} …"
 pvEnable = "Überschuss verfügbar. Starte in {remaining} …"
diff --git a/i18n/en.toml b/i18n/en.toml
index 8b5bc0e2..f48addfe 100644
--- a/i18n/en.toml
+++ b/i18n/en.toml
@@ -254,6 +254,7 @@ solar = "Solar"
 [main.loadpointSettings]
 currents = "Charging current"
 default = "default"
+disclaimerHint = "Note:"
 onlyForSocBasedCharging = "These options are only available for vehicles with known charging level."
 title = "Settings {0}"
 vehicle = "Vehicle"
@@ -358,7 +359,6 @@ cleanEnergyCharging = "Charging clean energy: {co2} (limit {limit})"
 climating = "Pre-conditioning detected."
 connected = "Connected."
 disconnected = "Disconnected."
-guard = "Switching protection active for {remaining}."
 minCharge = "Minimum charging to {soc}%."
 pvDisable = "Not enough surplus. Pausing in {remaining}…"
 pvEnable = "Surplus available. Starting in {remaining}…"
diff --git a/i18n/es.toml b/i18n/es.toml
index 5993dfa6..bc9590ed 100644
--- a/i18n/es.toml
+++ b/i18n/es.toml
@@ -266,6 +266,7 @@ solar = "Solar"
 [main.loadpointSettings]
 currents = "Corriente de carga"
 default = "defecto"
+disclaimerHint = "Nota:"
 onlyForSocBasedCharging = "Estas opciones sólo están disponibles para vehículos con nivel de carga conocido"
 title = "Configuraciones {0}"
 vehicle = "Vehículo"
@@ -286,6 +287,7 @@ label = "Carga de min. %"
 
 [main.loadpointSettings.phasesConfigured]
 label = "Fases"
+no1p3pSupport = "¿Cómo está conectado tu cargador?"
 phases_0 = "Cambio automático"
 phases_1 = "monofásica"
 phases_1_hint = "({min} a {max})"
@@ -373,9 +375,6 @@ cleanEnergyCharging = "Cargando con energía limpia: {co2} (límite {limit})"
 climating = "Preacondicionamiento detectado."
 connected = "Conectado."
 disconnected = "Desconectado."
-guard = "Cambiando la protección activa para {remaining}"
-guardDisable = "Pausando en {remaining}..."
-guardEnable = "Comenzando en {remaining}..."
 minCharge = "Carga mínima hasta {soc}%."
 pvDisable = "Excedente insuficiente. Pausa en {remaining}..."
 pvEnable = "Excedente disponibile. Empezar en {remaining}..."
diff --git a/i18n/fi.toml b/i18n/fi.toml
index 1747d414..98e6b1bf 100644
--- a/i18n/fi.toml
+++ b/i18n/fi.toml
@@ -199,6 +199,7 @@ solar = "Aurinkoenergia"
 [main.loadpointSettings]
 currents = "Latausvirta"
 default = "oletus"
+disclaimerHint = "Huomautus:"
 title = "Asetukset {0}"
 vehicle = "Ajoneuvo"
 
@@ -292,7 +293,6 @@ cleanEnergyCharging = "Lataa puhdasta energiaa: {co2} (limit {limit})"
 climating = "Esilämmitys/-viilennys havaittu."
 connected = "Yhdistetty."
 disconnected = "Irroitettu."
-guard = "Suojaus aktiivinen: {remaining}…"
 minCharge = "Ladataan minimissään {soc}%."
 pvDisable = "Ei tarpeeksi ylijäämää. Tauko {remaining}…"
 pvEnable = "Ylijäämää saatavilla. Aloitus {remaining}…"
diff --git a/i18n/fr.toml b/i18n/fr.toml
index 7c7d6873..9d5b59fc 100644
--- a/i18n/fr.toml
+++ b/i18n/fr.toml
@@ -216,6 +216,7 @@ solar = "Solaire"
 [main.loadpointSettings]
 currents = "Courant de charge"
 default = "défaut"
+disclaimerHint = "Remarque:"
 onlyForSocBasedCharging = "Ces options sont disponibles uniquement pour les véhicules avec niveau de charge connu."
 title = "Réglages {0}"
 vehicle = "Véhicule"
@@ -320,7 +321,6 @@ cleanEnergyCharging = "En charge avec énergie propre : {co2} (limit < {limit}
 climating = "Pré-conditionnement détecté."
 connected = "Connecté."
 disconnected = "Déconnecté."
-guard = "Protection de commutation active pour {remaining}..."
 minCharge = "Charge minimale jusqu'à {soc}%."
 pvDisable = "Pas assez de surplus. Pause dans {remaining}…"
 pvEnable = "Surplus disponible. Démarre dans {remaining}…"
diff --git a/i18n/hr.toml b/i18n/hr.toml
index e1f04c72..de8cacc7 100644
--- a/i18n/hr.toml
+++ b/i18n/hr.toml
@@ -218,6 +218,7 @@ solar = "Solarno"
 [main.loadpointSettings]
 currents = "Struja napajanja"
 default = "zadano"
+disclaimerHint = "Bilješka:"
 onlyForSocBasedCharging = "Ove su opcije dostupne samo za vozila s poznatom razinom napunjenosti."
 title = "Postavke {0}"
 vehicle = "Vozilo"
@@ -322,7 +323,6 @@ cleanEnergyCharging = "Napajanje čistom energijom: {co2} (ograničenje {limit})
 climating = "Otkriveno predkondicioniranje."
 connected = "Povezano."
 disconnected = "Nepovezano."
-guard = "Promjena zaštite aktivna za {remaining}."
 minCharge = "Minimalno napajanje do {soc} %."
 pvDisable = "Nema dovoljno viška. Zaustavlja se za {remaining} …"
 pvEnable = "Dostupan je višak. Počinje za {remaining} …"
diff --git a/i18n/it.toml b/i18n/it.toml
index 1857abb4..f4e179b5 100644
--- a/i18n/it.toml
+++ b/i18n/it.toml
@@ -101,6 +101,7 @@ remoteDisabledSoft = "{source}: Ricarica FV adattiva disabilitata"
 [main.loadpointSettings]
 currents = "Corrente di carica"
 default = "default"
+disclaimerHint = "Nota:"
 title = "Impostazioni {0}"
 vehicle = "Veicolo"
 
diff --git a/i18n/lb.toml b/i18n/lb.toml
index 0a1a7667..f379aa0e 100644
--- a/i18n/lb.toml
+++ b/i18n/lb.toml
@@ -94,6 +94,7 @@ power = "Leeschtung"
 [main.loadpointSettings]
 currents = "Ladestroum"
 default = "Standard"
+disclaimerHint = "Notiz:"
 title = "Astellungen {0}"
 vehicle = "Gefier"
 
diff --git a/i18n/lt.toml b/i18n/lt.toml
index 9808cdd9..e56750e8 100644
--- a/i18n/lt.toml
+++ b/i18n/lt.toml
@@ -266,6 +266,7 @@ solar = "Saulės"
 [main.loadpointSettings]
 currents = "Įkrovimo srovė"
 default = "standartiškai"
+disclaimerHint = "Pastaba:"
 onlyForSocBasedCharging = "Šie pasirinkimai galimi tik automobiliams su žinomu įkrovos lygiu."
 title = "Nustatymai {0}"
 vehicle = "Automobilis"
@@ -286,6 +287,7 @@ label = "Minimali įkrova %"
 
 [main.loadpointSettings.phasesConfigured]
 label = "Fazės"
+no1p3pSupport = "Kaip prijungtas jūsų įkroviklis?"
 phases_0 = "automatinis perjungimas"
 phases_1 = "1 fazė"
 phases_1_hint = "({min} iki {max})"
@@ -373,9 +375,6 @@ cleanEnergyCharging = "Įkraunama švaria energija: {co2} (limitas {limit})"
 climating = "Aptiktas išankstinis kondicionavimas."
 connected = "Prijungtas."
 disconnected = "Neprijungtas."
-guard = "Per dažno junginėjimo apsauga {remaining}…"
-guardDisable = "Pauzė už {remaining}…"
-guardEnable = "Pradedam už {remaining}…"
 minCharge = "Minimalus įkrovimas iki {soc}%."
 pvDisable = "Trūksta saulės. Pauzė už {remaining}..."
 pvEnable = "Saulės užtenka, įkrovimas už {remaining}..."
diff --git a/i18n/nl.toml b/i18n/nl.toml
index baecd794..fd4715d1 100644
--- a/i18n/nl.toml
+++ b/i18n/nl.toml
@@ -1,6 +1,8 @@
 [batterySettings]
 batteryLevel = "Batterij-niveau"
 capacity = "{energy} van {total}"
+control = "Batterij aansturing"
+discharge = "Beperk het ontladen in de snelle modus of bij gepland laden"
 disclaimerHint = "Opgelet:"
 disclaimerText = "Deze instellingen beïnvloeden enkel de zonnemodus. Oplaadgedrag wordt overeenkomstig aangepast."
 legendBottomName = "huis heeft prioriteit"
@@ -14,34 +16,78 @@ legendTopSubline = "zonder onderbrekingen"
 modalTitle = "Batterij-instellingen"
 
 [batterySettings.bufferStart]
-full = "als bijna vol"
+full = "Wanneer bijna vol"
 low = "indien beetje vol"
 medium = "indien half vol"
 never = "enkel bij voldoende overschot"
 
 [config]
 
+[config.battery]
+titleAdd = "Voeg meter voor batterijsysteem toe"
+titleEdit = "Bewerk meter voor batterijsysteem"
+
+[config.deviceValue]
+capacity = "Capaciteit"
+chargeStatus = "Status"
+chargedEnergy = "Geladen"
+current = "Stroomsterke"
+enabled = "Ingeschakeld"
+energy = "Energie"
+odometer = "Kilometerstand"
+phaseCurrents = "Stroomsterkte L1..L3"
+phasePowers = "Vermogen L1..L3"
+phaseVoltages = "Spanning L1..L3"
+power = "Vermogen"
+range = "Rijbereik"
+soc = "Batterijpercentage"
+socLimit = "Limiet"
+
 [config.form]
 example = "Voorbeeld"
 optional = "Optioneel"
 
+[config.grid]
+titleAdd = "Voeg net meter toe"
+titleEdit = "Bewerk net meter"
+
 [config.main]
-addVehicle = "Voertuig toevoegen"
+addLoadpoint = "Voeg laadpunt toe"
+addPvBattery = "Voeg zonnepanelen of batterij toe"
+addVehicle = "Voeg voertuig toe"
 edit = "Bewerken"
 title = "Configuratie"
+unconfigured = "Niet geconfigureerd"
 vehicles = "Mijn voertuigen"
+yaml = "Geconfigureerd in evcc.yml. Niet aanpasbaar in de interface"
+
+[config.meter]
+cancel = "Annuleren"
+delete = "Verwijder"
+save = "Opslaan"
+template = "Fabrikant"
+titleChoice = "Wat wil je toevoegen?"
+validateSave = "Valideren en opslaan"
+
+[config.pv]
+titleAdd = "Voeg meter zonnepanelen toe"
+titleEdit = "Bewerk meter zonnepanelen"
+
+[config.site]
+cancel = "Annuleren"
+save = "Opslaan"
 
 [config.validation]
 failed = "mislukt"
 label = "Status"
-running = "valideren..."
+running = "Bezig met valideren..."
 success = "succesvol"
 unknown = "onbekend"
 validate = "valideer"
 
 [config.vehicle]
 cancel = "Annuleer"
-delete = "Verwijder voertuig"
+delete = "Verwijder"
 generic = "Andere integraties"
 offline = "Algemeen voertuig"
 online = "Voertuig met online API"
@@ -67,21 +113,33 @@ powerSub2 = "laden..."
 tabTitle = "Live community"
 
 [footer.savings]
+co2Saved = "{value} opgeslagen"
+co2Title = "CO₂ uitstoot"
+configurePriceCo2 = "Meer informatie rond het configureren van prijs en CO₂ gegevens"
 footerLong = "{percent}% zonne-energie"
 footerShort = "{percent}% zon"
 modalTitle = "Oplaadenergie-overzicht"
+moneySaved = "{value} opgeslagen"
 percentGrid = "{grid} kWh net"
 percentSelf = "{self} kWh zon"
 percentTitle = "Zonne-energie"
+periodLabel = "Periode:"
 priceFeedIn = "{feedInPrice} injectie"
 priceGrid = "{gridPrice} net"
 priceTitle = "Energieprijs"
+referenceGrid = "net"
+referenceLabel = "Referentiedata:"
 savingsComparedToGrid = "in vergelijking met netafname"
 savingsTitle = "Besparing"
 savingsTotalEnergy = "{total} kWh geladen"
 since = "sinds {since}"
 tabTitle = "Mijn data"
 
+[footer.savings.period]
+30d = "Laatste 30 dagen"
+365d = "Laatste 365 dagen"
+total = "totaal"
+
 [footer.sponsor]
 becomeSponsor = "Word sponsor"
 confetti = "Klaar voor confetti?"
@@ -126,7 +184,7 @@ about = "Over"
 blog = "Blog"
 docs = "Documentatie"
 github = "GitHub"
-login = "Voertuig logins"
+login = "Logingegevens van voertuigen"
 needHelp = "Hulp nodig?"
 sessions = "Laadsessies"
 settings = "Instellingen"
@@ -140,7 +198,7 @@ light = "Design: licht"
 discussionsButton = "GitHub discussies"
 documentationButton = "Documentatie"
 issueButton = "Een bug melden"
-issueDescription = "Vreemd of fout gedrag gevonden?"
+issueDescription = "Vreemd of foutief gedrag gevonden?"
 modalTitle = "Hulp nodig?"
 primaryActions = "Werkt iets niet naar behoren? Er zijn goede plaatsen om hulp te krijgen."
 restartButton = "Herstart"
@@ -158,18 +216,25 @@ modalTitle = "Ben je zeker dat je wil herstarten?"
 vehicles = "Parking"
 
 [main.chargingPlan]
+active = "Actief"
 arrivalTab = "Aankomst"
+day = "Dag"
 departureTab = "Vertrek"
+goal = "Oplaaddoel"
 modalTitle = "Laad Plan"
 none = "geen"
+remove = "Verwijderen"
+time = "Tijd"
 title = "Plan"
 titleMinSoc = "Min lading"
 titleTargetCharge = "Vertrek"
+update = "Toepassen"
 
 [main.energyflow]
 battery = "Batterij"
 batteryCharge = "Batterij opladen"
 batteryDischarge = "Batterij ontladen"
+batteryHold = "Batterij (aanhouden)"
 batteryTooltip = "{energy} van {total} ({soc})"
 gridImport = "Netafname"
 homePower = "Consumptie"
@@ -179,9 +244,15 @@ pvExport = "Netinjectie"
 pvProduction = "Productie"
 selfConsumption = "Zelfverbruik"
 
+[main.heatingStatus]
+charging = "Bezig met verwarmen..."
+cheapEnergyCharging = "Verwarmen met goedkope energie: {price} (limiet {limit})"
+cleanEnergyCharging = "Verwarmen met schone energie: {co2} (limiet {limit})"
+waitForVehicle = "Klaar. Wachten op voertuigverwarming"
+
 [main.loadpoint]
 avgPrice = "⌀ Prijs"
-charged = "Geladen"
+charged = "Lading"
 co2 = "⌀ CO₂"
 duration = "Duur"
 fallbackName = "Laadpunt"
@@ -195,22 +266,29 @@ solar = "Zon"
 [main.loadpointSettings]
 currents = "Laadstroom"
 default = "standaard"
+disclaimerHint = "Let op:"
+onlyForSocBasedCharging = "Deze instellingen zijn alleen beschikbaar bij wagens waar het batterijpercentage gekend is"
 title = "Instellingen {0}"
 vehicle = "Voertuig"
 
+[main.loadpointSettings.limitSoc]
+description = "Laadlimiet wanneer dit voertuig aangesloten wordt"
+label = "Standaard limiet"
+
 [main.loadpointSettings.maxCurrent]
-label = "Max. Stroom"
+label = "Max. Stroomsterkte"
 
 [main.loadpointSettings.minCurrent]
-label = "Min. Stroom"
+label = "Min. Stroomsterkte"
 
 [main.loadpointSettings.minSoc]
-description = "Het voertuig wordt „snel” opgeladen tot {0}% in deze modus, en gaat dan verder met alleen het zonneoverschot."
-label = "Min. lading %"
+description = "Het voertuig wordt „snel” opgeladen tot {0}% in deze modus. Het laden gaat daarna verder met zonneoverschot. Nuttig om een bepaald rijbereik te garanderen, ook gedurende periodes met weinig of geen zonnestroom."
+label = "Min. batterij %"
 
 [main.loadpointSettings.phasesConfigured]
 label = "Fasen"
-phases_0 = "automatisch omschakelen"
+no1p3pSupport = "Hoe is je lader aangesloten?"
+phases_0 = "automatisch wisselen van aantal fases"
 phases_1 = "1 fase"
 phases_1_hint = "({min} tot {max})"
 phases_3 = "3 fase"
@@ -230,24 +308,31 @@ logout = "afmelden"
 activate = "Activeren"
 co2Limit = "CO₂ limiet van {co2}"
 costLimitIgnore = "De geconfigureerde {limit} zal genegeerd worden tijdens deze periode."
+currentPlan = "Geactiveerd laadplan"
 descriptionEnergy = "Tegen wanneer moet {targetEnergy} in het voertuig geladen zijn?"
 descriptionSoc = "Wanneer moet het voertuig geladen zijn naar {targetSoc}%?"
 inactiveLabel = "Ingestelde tijd"
 modalTitle = "Stel tijd in"
+notReachableInTime = "Laaddoel niet haalbaar bij gevraagde eindtijd. Geschatte eindtijd: {endTime}."
 onlyInPvMode = "Laadplan werkt alleen in PV modus"
 planDuration = "Oplaadtijd"
 planPeriodLabel = "Periode"
 planPeriodValue = "{start} tot {end}"
 planUnknown = "nog niet bekend"
+preview = "Schatting laadplan"
 priceLimit = "prijslimiet van {price}"
 remove = "Verwijder"
 setTargetTime = "geen"
+targetIsAboveLimit = "De ingestelde laadlimiet {limit} wordt genegeerd gedurende deze periode."
+targetIsAboveVehicleLimit = "Verhoog de limiet ({limit}) om het laaddoel te halen"
 targetIsInThePast = "Kies een tijd in de toekomst, Marty."
 targetIsTooFarInTheFuture = "We passen het plan aan zodra we meer weten over de toekomst."
 title = "Doeltijd"
 today = "Vandaag"
 tomorrow = "Morgen"
 update = "Updaten"
+vehicleCapacityDocs = "Meer informatie over het configureren hiervan"
+vehicleCapacityRequired = "De batterijcapaciteit van het voertuig is een vereiste parameter om de oplaadtijd te kunnen bepalen"
 
 [main.targetChargePlan]
 chargeDuration = "Oplaadtijd"
@@ -282,12 +367,11 @@ vehicleTarget = "Voertuiglimiet: {soc}%"
 
 [main.vehicleStatus]
 charging = "Opladen..."
-cheapEnergyCharging = "Goedkope energie beschikbaar. Aan het laden…"
-cleanEnergyCharging = "Groene energie beschikbaar. Aan het laden…"
+cheapEnergyCharging = "Laden actief met goedkope energie"
+cleanEnergyCharging = "Laden actief met schone energie. {co2} (limit {limit})"
 climating = "Voor-conditionering gedetecteerd."
 connected = "Verbonden."
 disconnected = "Niet verbonden."
-guard = "Schakelbeveiliging actief voor {remaining}."
 minCharge = "Minimaal opladen naar {soc}%."
 pvDisable = "Onvoldoende PV overschot beschikbaar. Pauzeren over {remaining}..."
 pvEnable = "Voldoende PV overschot beschikbaar. Starten over {remaining}..."
@@ -358,7 +442,7 @@ allVehicles = "alle voertuigen"
 filter = "filter"
 
 [settings]
-title = "Instellingen"
+title = "Algemene instellingen"
 
 [settings.gridDetails]
 co2 = "CO₂"
diff --git a/i18n/no.toml b/i18n/no.toml
index 43296d46..0fb657ae 100644
--- a/i18n/no.toml
+++ b/i18n/no.toml
@@ -192,6 +192,7 @@ solar = "Solenergi"
 [main.loadpointSettings]
 currents = "Ladestrøm"
 default = "forvalg"
+disclaimerHint = "Merknad:"
 title = "Innstillinger for {0}"
 vehicle = "Kjøretøy"
 
@@ -283,7 +284,6 @@ cleanEnergyCharging = "Ren energi tilgjengelig. Lader …"
 climating = "Klimaanlegg på forhånd oppdaget."
 connected = "Tilkoblet."
 disconnected = "Frakoblet."
-guard = "Bryterbeskyttelse aktiv i {remaining} …"
 minCharge = "Minimumslading til {soc}%."
 pvDisable = "Ikke nok overskudd. Pause i {remaining}..."
 pvEnable = "Overskudd tilgjengelig. Starter om {remaining}..."
diff --git a/i18n/pl.toml b/i18n/pl.toml
index 196c666a..e8bec426 100644
--- a/i18n/pl.toml
+++ b/i18n/pl.toml
@@ -217,6 +217,7 @@ solar = "Słoneczna"
 [main.loadpointSettings]
 currents = "Prąd ładowania"
 default = "domyślny"
+disclaimerHint = "Uwaga:"
 onlyForSocBasedCharging = "Opcje te są dostępne tylko dla pojazdów ze znanym poziomem naładowania."
 title = "Ustawienia {0}"
 vehicle = "Pojazd"
@@ -321,7 +322,6 @@ cleanEnergyCharging = "Ładowanie czystą energią: {co2} (ograniczenie {limit})
 climating = "Wykryto wstępne przygotowanie"
 connected = "Połączony."
 disconnected = "Rozłączony."
-guard = "Ochrona przełączania aktywna {remaining}…"
 minCharge = "Minimalne ładowanie do {soc}%."
 pvDisable = "Za mało nadwyżki. Pauza po {remaining}..."
 pvEnable = "Dostępna nadwyżka. Począwszy po {remaining}..."
diff --git a/i18n/pt.toml b/i18n/pt.toml
index 5c081fec..0b9f5372 100644
--- a/i18n/pt.toml
+++ b/i18n/pt.toml
@@ -216,6 +216,7 @@ solar = "Sol"
 [main.loadpointSettings]
 currents = "Corrente de carga"
 default = "predefinado"
+disclaimerHint = "Aviso:"
 onlyForSocBasedCharging = "Estas opções só estão disponíveis para veículos com nível de carregamento conhecido."
 title = "Configurações {0}"
 vehicle = "Veículo"
@@ -322,9 +323,6 @@ cleanEnergyCharging = "Carregando energia limpa: {co2} (limit {limit})"
 climating = "Pré-condicionamento detectado."
 connected = "Ligado."
 disconnected = "Não conectado."
-guard = "Proteção de ligação ativa para {remaining}."
-guardDisable = "Pausar em {remaining}…"
-guardEnable = "Iniciando em {remaining}..."
 minCharge = "Carga mínima até {soc}%"
 pvDisable = "Excesso insuficiente. Pausa em {remaining}..."
 pvEnable = "Excesso disponível. Carga em {remaining}..."
diff --git a/i18n/ro.toml b/i18n/ro.toml
index 0a478fd5..1881d1da 100644
--- a/i18n/ro.toml
+++ b/i18n/ro.toml
@@ -160,6 +160,7 @@ power = "Putere"
 [main.loadpointSettings]
 currents = "Curent încărcare"
 default = "default"
+disclaimerHint = "Nota:"
 title = "Setări {0}"
 vehicle = "Vehicul"
 
diff --git a/i18n/ru.toml b/i18n/ru.toml
index ce68153d..9c8aaacb 100644
--- a/i18n/ru.toml
+++ b/i18n/ru.toml
@@ -123,6 +123,7 @@ power = "Мощность"
 [main.loadpointSettings]
 currents = "Ток зарядки"
 default = "по умолчанию"
+disclaimerHint = "Заметка:"
 title = "Настройки {0}"
 vehicle = "Транспортное средство"
 
diff --git a/i18n/sl.toml b/i18n/sl.toml
index fc7b5038..79907a2d 100644
--- a/i18n/sl.toml
+++ b/i18n/sl.toml
@@ -217,6 +217,7 @@ solar = "Sončna energija"
 [main.loadpointSettings]
 currents = "Polnilni tok"
 default = "privzeto"
+disclaimerHint = "Opomba:"
 onlyForSocBasedCharging = "Te možnosti so na voljo samo za vozila z znano stopnjo napolnjenosti."
 title = "Nastavitve {0}"
 vehicle = "Vozilo"
diff --git a/i18n/sv.toml b/i18n/sv.toml
index 8b0181f9..418a8a9a 100644
--- a/i18n/sv.toml
+++ b/i18n/sv.toml
@@ -23,15 +23,59 @@ never = "bara med tillräckligt sol-överskott"
 
 [config]
 
+[config.battery]
+titleAdd = "Lägg till batterimätare"
+titleEdit = "Ändra batterimätare"
+
+[config.deviceValue]
+capacity = "Kapacitet"
+chargeStatus = "Status"
+chargedEnergy = "Laddad"
+current = "Nuvarande"
+enabled = "Aktiverad"
+energy = "Energi"
+odometer = "Mätarställning"
+phaseCurrents = "Ström L1..L3"
+phasePowers = "Effekt L1..L3"
+phaseVoltages = "Spänning L1..L3"
+power = "Effekt"
+range = "Räckvidd"
+soc = "SoC"
+socLimit = "SoC gräns"
+
 [config.form]
 example = "Exempel"
 optional = "valfri"
 
+[config.grid]
+titleAdd = "Lägg till elnätsmätare"
+titleEdit = "Ändra elnätsmätare"
+
 [config.main]
+addLoadpoint = "Lägg till laddbox"
+addPvBattery = "Lägg till solceller eller batteri"
 addVehicle = "Lägg till fordon"
 edit = "ändra"
 title = "Inställningar"
+unconfigured = "ej konfigurerad"
 vehicles = "Mina fordon"
+yaml = "Konfigurera i evcc.yaml. Kan inte ändras i UI."
+
+[config.meter]
+cancel = "Avbryt"
+delete = "Radera"
+save = "Spara"
+template = "Tillverkare"
+titleChoice = "Vad vill du lägga till?"
+validateSave = "Validera & spara"
+
+[config.pv]
+titleAdd = "Lägg till solcellsmätare"
+titleEdit = "Ändra solcellsmätare"
+
+[config.site]
+cancel = "Avbryt"
+save = "Spara"
 
 [config.validation]
 failed = "misslyckades"
@@ -43,7 +87,7 @@ validate = "validerar"
 
 [config.vehicle]
 cancel = "Avbryt"
-delete = "Radera fordon"
+delete = "Radera"
 generic = "Andra integrationer"
 offline = "Generiskt fordon"
 online = "Fordon med online API"
@@ -142,7 +186,7 @@ docs = "Dokumentation"
 github = "GitHub"
 login = "Fordons inloggningar"
 needHelp = "Behöver du hjälp?"
-sessions = "Laddnings sessioner"
+sessions = "Laddningar"
 settings = "Inställningar"
 
 [help]
@@ -195,6 +239,12 @@ pvExport = "Export till elnät"
 pvProduction = "Produktion"
 selfConsumption = "Egenförbrukning"
 
+[main.heatingStatus]
+charging = "Värmer..."
+cheapEnergyCharging = "Värmer med billig energi: {price} (limit {limit})"
+cleanEnergyCharging = "Värmer med CO2-neutral energi: {co2} (limit {limit})"
+waitForVehicle = "Redo, väntar på värmare..."
+
 [main.loadpoint]
 avgPrice = "⌀ pris"
 charged = "Laddat"
@@ -209,8 +259,9 @@ remoteDisabledSoft = "{source}: Adaptiv solladdning avstängd"
 solar = "Solenergi"
 
 [main.loadpointSettings]
-currents = "Laddningsström"
+currents = "Laddström"
 default = "förvald"
+disclaimerHint = "Anmärkning:"
 onlyForSocBasedCharging = "Dessa inställningar är endast tillgängliga för fordon med känd laddnivå"
 title = "Inställningar {0}"
 vehicle = "Fordon"
@@ -231,6 +282,7 @@ label = "Min. laddning %"
 
 [main.loadpointSettings.phasesConfigured]
 label = "Faser"
+no1p3pSupport = "Hur är din laddbox ansluten?"
 phases_0 = "automatisk växling"
 phases_1 = "1-fas"
 phases_1_hint = "({min} till {max})"
@@ -281,7 +333,7 @@ vehicleCapacityDocs = "Lär dig hur du konfigurerar."
 vehicleCapacityRequired = "Fordonets batterikapacitet behövs för att beräkna laddtid."
 
 [main.targetChargePlan]
-chargeDuration = "Laddningstid"
+chargeDuration = "Laddtid"
 co2Label = "CO₂-utsläpp ⌀"
 priceLabel = "Energipris"
 timeRange = "{day} {range} t"
@@ -318,7 +370,6 @@ cleanEnergyCharging = "Laddar grön energi: {co2} (gräns {limit})"
 climating = "Förvärmning identifierad."
 connected = "Inkopplad."
 disconnected = "Frånkopplad."
-guard = "Brytarskydd aktivt i {remaining}…"
 minCharge = "Minimiladdning till {soc}%."
 pvDisable = "Inte tillräckligt med överskott. Pausar i {remaining}..."
 pvEnable = "Överskott tillgängligt. Börjar om {remaining}..."
@@ -351,7 +402,7 @@ meterstop = "Mätare stopp"
 odometer = "Mätarställning"
 price = "Pris"
 started = "Start-tid"
-title = "Laddningssession"
+title = "Laddning"
 
 [sessions]
 avgPower = "⌀-effekt"
diff --git a/i18n/uk.toml b/i18n/uk.toml
index 2f575d06..49b2e405 100644
--- a/i18n/uk.toml
+++ b/i18n/uk.toml
@@ -188,6 +188,7 @@ solar = "Сонячна"
 [main.loadpointSettings]
 currents = "Струм зарядки"
 default = "за замовчуванням"
+disclaimerHint = "Примітка:"
 title = "Налаштування {0}"
 vehicle = "Транспортний засіб"
 
@@ -280,9 +281,6 @@ cleanEnergyCharging = "Чиста енергія доступна. Зарядж
 climating = "Виявлено попередню підготовку."
 connected = "Підключено."
 disconnected = "Відключено."
-guard = "Захист від перемикання активовано для {remaining}."
-guardDisable = "Пауза через {remaining}…"
-guardEnable = "Початок через {remaining}…"
 minCharge = "Мінімальна зарядка до {soc}%."
 pvDisable = "Недостатньо надлишку. Пауза через {remaining}…"
 pvEnable = "Надлишок доступний. Початок через {remaining}…"
diff --git a/i18n/zh-Hans.toml b/i18n/zh-Hans.toml
index 77ab9a14..f6167e62 100644
--- a/i18n/zh-Hans.toml
+++ b/i18n/zh-Hans.toml
@@ -198,6 +198,7 @@ solar = "太阳能"
 [main.loadpointSettings]
 currents = "充电货币"
 default = "默认"
+disclaimerHint = "笔记:"
 title = "设置 {0}"
 vehicle = "车辆"
 
diff --git a/meter/goodwe-wifi.go b/meter/goodwe-wifi.go
index 26652e21..c8ea6abf 100644
--- a/meter/goodwe-wifi.go
+++ b/meter/goodwe-wifi.go
@@ -1,15 +1,17 @@
 package meter
 
 import (
+	"time"
+
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/meter/goodwe"
 	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/request"
 )
 
 type goodWeWiFi struct {
-	*goodwe.Server
-	usage string
-	uri   string
+	usage    string
+	inverter *util.Monitor[goodwe.Inverter]
 }
 
 func init() {
@@ -19,31 +21,36 @@ func init() {
 //go:generate go run ../cmd/tools/decorate.go -f decorateGoodWeWifi -b *goodWeWiFi -r api.Meter -t "api.Battery,Soc,func() (float64, error)"
 
 func NewGoodWeWifiFromConfig(other map[string]interface{}) (api.Meter, error) {
-	var cc struct {
+	cc := struct {
 		capacity   `mapstructure:",squash"`
 		URI, Usage string
+		Timeout    time.Duration
+	}{
+		Timeout: request.Timeout,
 	}
 
 	if err := util.DecodeOther(other, &cc); err != nil {
 		return nil, err
 	}
 
-	return NewGoodWeWiFi(cc.URI, cc.Usage)
+	return NewGoodWeWiFi(cc.URI, cc.Usage, cc.Timeout)
 }
 
-func NewGoodWeWiFi(uri string, usage string) (api.Meter, error) {
-	instance, err := goodwe.Instance()
+func NewGoodWeWiFi(uri, usage string, timeout time.Duration) (api.Meter, error) {
+	instance, err := goodwe.Instance(util.NewLogger("goodwe-wifi"))
 	if err != nil {
 		return nil, err
 	}
 
-	res := &goodWeWiFi{
-		Server: instance,
-		usage:  usage,
-		uri:    uri,
+	inverter := instance.GetInverter(uri)
+	if inverter == nil {
+		inverter = instance.AddInverter(uri, timeout)
 	}
 
-	instance.AddInverter(uri)
+	res := &goodWeWiFi{
+		usage:    usage,
+		inverter: inverter,
+	}
 
 	// decorate api.BatterySoc
 	var batterySoc func() (float64, error)
@@ -55,7 +62,10 @@ func NewGoodWeWiFi(uri string, usage string) (api.Meter, error) {
 }
 
 func (m *goodWeWiFi) CurrentPower() (float64, error) {
-	data := m.GetInverter(m.uri)
+	data, err := m.inverter.Get()
+	if err != nil {
+		return 0, err
+	}
 
 	switch m.usage {
 	case "grid":
@@ -69,5 +79,6 @@ func (m *goodWeWiFi) CurrentPower() (float64, error) {
 }
 
 func (m *goodWeWiFi) batterySoc() (float64, error) {
-	return m.GetInverter(m.uri).Soc, nil
+	data, err := m.inverter.Get()
+	return data.Soc, err
 }
diff --git a/meter/goodwe/server.go b/meter/goodwe/server.go
index 9a847c3a..ac40acfa 100644
--- a/meter/goodwe/server.go
+++ b/meter/goodwe/server.go
@@ -5,6 +5,8 @@ import (
 	"net"
 	"sync"
 	"time"
+
+	"github.com/evcc-io/evcc/util"
 )
 
 var (
@@ -12,7 +14,7 @@ var (
 	mu       sync.RWMutex
 )
 
-func Instance() (*Server, error) {
+func Instance(log *util.Logger) (*Server, error) {
 	mu.Lock()
 	defer mu.Unlock()
 
@@ -21,7 +23,8 @@ func Instance() (*Server, error) {
 	}
 
 	instance = &Server{
-		inverters: make(map[string]Inverter),
+		log:       log,
+		inverters: make(map[string]*util.Monitor[Inverter]),
 	}
 
 	addr, err := net.ResolveUDPAddr("udp", "0.0.0.0:8899")
@@ -40,70 +43,81 @@ func Instance() (*Server, error) {
 	return instance, err
 }
 
-func (m *Server) AddInverter(ip string) {
+func (m *Server) AddInverter(ip string, timeout time.Duration) *util.Monitor[Inverter] {
 	mu.Lock()
 	defer mu.Unlock()
-	m.inverters[ip] = Inverter{IP: ip}
+	monitor := util.NewMonitor[Inverter](timeout)
+	m.inverters[ip] = monitor
+	return monitor
 }
 
-func (m *Server) GetInverter(uri string) Inverter {
+func (m *Server) GetInverter(ip string) *util.Monitor[Inverter] {
 	mu.RLock()
 	defer mu.RUnlock()
-	return m.inverters[uri]
+	return m.inverters[ip]
 }
 
 func (m *Server) readData() {
-	for _, inverter := range m.inverters {
-		addr, err := net.ResolveUDPAddr("udp", inverter.IP+":8899")
-		if err != nil {
-			return
-		}
-
-		if _, err := m.conn.WriteToUDP([]byte{0xF7, 0x03, 0x89, 0x1C, 0x00, 0x7D, 0x7A, 0xE7}, addr); err != nil {
-			return
+	for {
+		mu.RLock()
+		ips := make([]string, 0, len(m.inverters))
+		for ip := range m.inverters {
+			ips = append(ips, ip)
 		}
-		time.Sleep(5 * time.Second)
-		if _, err := m.conn.WriteToUDP([]byte{0xF7, 0x03, 0x90, 0x88, 0x00, 0x0D, 0x3D, 0xB3}, addr); err != nil {
-			return
+		mu.RUnlock()
+
+		for _, ip := range ips {
+			addr, err := net.ResolveUDPAddr("udp", net.JoinHostPort(ip, "8899"))
+			if err != nil {
+				m.log.ERROR.Println(err)
+				continue
+			}
+			if _, err := m.conn.WriteToUDP([]byte{0xF7, 0x03, 0x89, 0x1C, 0x00, 0x7D, 0x7A, 0xE7}, addr); err != nil {
+				m.log.ERROR.Println(err)
+				continue
+			}
+			time.Sleep(5 * time.Second)
+			if _, err := m.conn.WriteToUDP([]byte{0xF7, 0x03, 0x90, 0x88, 0x00, 0x0D, 0x3D, 0xB3}, addr); err != nil {
+				m.log.ERROR.Println(err)
+				continue
+			}
 		}
 	}
-	m.readData()
 }
 
 func (m *Server) listen() {
 	for {
 		buf := make([]byte, 1024)
-		_, addr, err := m.conn.ReadFromUDP(buf)
+		n, addr, err := m.conn.ReadFromUDP(buf)
 		if err != nil {
+			m.log.ERROR.Println(err)
 			continue
 		}
+		m.log.TRACE.Printf("recv from %s: % X", addr, buf[:n])
 
 		ip := addr.IP.String()
-
-		mu.Lock()
-		if buf[4] == 250 {
-			vPv1 := float64(int16(binary.BigEndian.Uint16(buf[11:]))) * 0.1
-			vPv2 := float64(int16(binary.BigEndian.Uint16(buf[19:]))) * 0.1
-			iPv1 := float64(int16(binary.BigEndian.Uint16(buf[13:]))) * 0.1
-			iPv2 := float64(int16(binary.BigEndian.Uint16(buf[21:]))) * 0.1
-			iBatt := float64(int16(binary.BigEndian.Uint16(buf[167:]))) * 0.1
-			vBatt := float64(int16(binary.BigEndian.Uint16(buf[165:]))) * 0.1
-
-			pvPower := vPv1*iPv1 + vPv2*iPv2
-
-			inverter := m.inverters[ip]
-			inverter.PvPower = pvPower
-			inverter.BatteryPower = vBatt * iBatt
-			inverter.NetPower = -float64(int32(binary.BigEndian.Uint32(buf[83:])))
-
-			m.inverters[ip] = inverter
+		monitor := m.GetInverter(ip)
+		if monitor == nil {
+			m.log.ERROR.Println("unknown inverter:", ip)
+			continue
 		}
 
-		if buf[4] == 26 {
-			inverter := m.inverters[ip]
-			inverter.Soc = float64(buf[20])
-			m.inverters[ip] = inverter
-		}
-		mu.Unlock()
+		monitor.SetFunc(func(inverter Inverter) Inverter {
+			if buf[4] == 250 {
+				ui := func(u, i int16) float64 {
+					return float64(int16(binary.BigEndian.Uint16(buf[u:]))) *
+						float64(int16(binary.BigEndian.Uint16(buf[i:]))) / 100
+				}
+				inverter.PvPower = ui(11, 13) + ui(19, 21)
+				inverter.BatteryPower = ui(165, 167)
+				inverter.NetPower = -float64(int32(binary.BigEndian.Uint32(buf[83:])))
+			}
+
+			if buf[4] == 26 {
+				inverter.Soc = float64(buf[20])
+			}
+
+			return inverter
+		})
 	}
 }
diff --git a/meter/goodwe/types.go b/meter/goodwe/types.go
index 06bb9986..02c372eb 100644
--- a/meter/goodwe/types.go
+++ b/meter/goodwe/types.go
@@ -2,15 +2,17 @@ package goodwe
 
 import (
 	"net"
+
+	"github.com/evcc-io/evcc/util"
 )
 
 type Server struct {
+	log       *util.Logger
 	conn      *net.UDPConn
-	inverters map[string]Inverter
+	inverters map[string]*util.Monitor[Inverter]
 }
 
 type Inverter struct {
-	IP           string
 	PvPower      float64
 	NetPower     float64
 	BatteryPower float64
diff --git a/provider/go.go b/provider/go.go
index ecf13871..d9e4610a 100644
--- a/provider/go.go
+++ b/provider/go.go
@@ -159,7 +159,7 @@ func (p *Go) handleSetter(param string, val any) error {
 func (p *Go) evaluate() (res any, err error) {
 	defer func() {
 		if r := recover(); r != nil {
-			err = fmt.Errorf("go script panic: %v", r)
+			err = fmt.Errorf("panic: %v", r)
 		}
 	}()
 
diff --git a/provider/modbus_sunspec.go b/provider/modbus_sunspec.go
index aa9a5d29..39d7aec1 100644
--- a/provider/modbus_sunspec.go
+++ b/provider/modbus_sunspec.go
@@ -31,7 +31,7 @@ func init() {
 func NewModbusSunspecFromConfig(other map[string]interface{}) (Provider, error) {
 	cc := struct {
 		modbus.Settings `mapstructure:",squash"`
-		Value           string
+		Value           []string
 		Scale           float64
 		Delay           time.Duration
 		ConnectDelay    time.Duration
@@ -67,7 +67,7 @@ func NewModbusSunspecFromConfig(other map[string]interface{}) (Provider, error)
 	log := util.NewLogger("sunspec")
 	conn.Logger(log.TRACE)
 
-	if cc.Value == "" {
+	if len(cc.Value) == 0 {
 		return nil, errors.New("value is required")
 	}
 
@@ -77,9 +77,13 @@ func NewModbusSunspecFromConfig(other map[string]interface{}) (Provider, error)
 		return nil, err
 	}
 
-	ops, err := modbus.ParsePoint(cc.Value)
-	if err != nil {
-		return nil, fmt.Errorf("invalid sunspec value: %s", cc.Value)
+	var ops []modbus.SunSpecOperation
+	for _, val := range cc.Value {
+		op, err := modbus.ParsePoint(val)
+		if err != nil {
+			return nil, fmt.Errorf("invalid sunspec value: %s", cc.Value)
+		}
+		ops = append(ops, op)
 	}
 
 	mb := &ModbusSunspec{
diff --git a/templates/definition/meter/fronius-ohmpilot.yaml b/templates/definition/meter/fronius-ohmpilot.yaml
new file mode 100644
index 00000000..19ceb0ab
--- /dev/null
+++ b/templates/definition/meter/fronius-ohmpilot.yaml
@@ -0,0 +1,19 @@
+template: fronius-ohmpilot
+products:
+  - brand: Fronius
+    description:
+      generic: Ohmpilot
+params:
+  - name: usage
+    choice: ["aux"]
+  - name: host
+render: |
+  type: custom
+  power:
+    source: http
+    uri: http://{{ .host }}/solar_api/v1/GetPowerFlowRealtimeData.fcgi
+    jq: if .Body.Data.Smartloads.Ohmpilots."720896".P_AC_Total == null then 0 else .Body.Data.Smartloads.Ohmpilots."720896".P_AC_Total * -1 end
+  soc:
+    source: http
+    uri: http://{{ .host }}/solar_api/v1/GetPowerFlowRealtimeData.fcgi
+    jq: if .Body.Data.Smartloads.Ohmpilots."720896".Temperature == null then 0 else .Body.Data.Smartloads.Ohmpilots."720896".Temperature end
diff --git a/templates/definition/meter/smartfox-em2.yaml b/templates/definition/meter/smartfox-em2.yaml
index 3901c38c..7062502e 100644
--- a/templates/definition/meter/smartfox-em2.yaml
+++ b/templates/definition/meter/smartfox-em2.yaml
@@ -6,14 +6,12 @@ products:
 requirements:
   description:
     de: |
-      Kann verwendet werden, um Daten für `grid`, `pv` und `aux` zu erhalten. 
-      Wenn `usage` nicht definiert ist, wird die Leistung für die Warmwasserbereitung zurückgegeben (als `aux` zu verwenden).
+      `aux` kann für die Leistung der Warmwasserbereitung verwendet werden.
     en: |
-      Can be used to get `grid`, `pv` and `aux` data. 
-      If `usage` is not defined, then return the power for the water heating (to be used as `aux`).
+      `aux` can be used for water heating power.
 params:
   - name: usage
-    choice: ["grid", "pv"]
+    choice: ["grid", "pv", "aux"]
   - name: host
 render: |
   {{- define "uri" -}}
@@ -21,13 +19,6 @@ render: |
   {{- end }}
   type: custom
   # jq: parse json generated from response values.xml (https://jqplay.org is your friend to test queries)
-  {{- if not .usage }}
-  power: # heating power in W
-    source: http
-    uri: {{ include "uri" . }}
-    jq: .values.value[] | select(.attrid=="htPowerMeasValue")."#content" | rtrimstr(" kW")
-    scale: 1000
-  {{- end }}
   {{- if eq .usage "grid" }}
   power: # grid power in W
     source: http
@@ -80,3 +71,10 @@ render: |
     uri: {{ include "uri" . }}
     jq: '[.values.value[] | select(.attrid|test("wr\\d+EnergyValue"))."#content" | rtrimstr(" kWh") | tonumber] | add'
   {{- end }}
+  {{- if eq .usage "aux" }}
+  power: # heating power in W
+    source: http
+    uri: {{ include "uri" . }}
+    jq: .values.value[] | select(.attrid=="htPowerMeasValue")."#content" | rtrimstr(" kW")
+    scale: 1000
+  {{- end }}
diff --git a/templates/definition/meter/smartfox.yaml b/templates/definition/meter/smartfox.yaml
index 6a4a4a1c..b79d8fa2 100644
--- a/templates/definition/meter/smartfox.yaml
+++ b/templates/definition/meter/smartfox.yaml
@@ -6,26 +6,18 @@ products:
 requirements:
   description:
     de: |
-      Kann verwendet werden, um Daten für `grid`, `pv` und `aux` zu erhalten. 
-      Wenn `usage` nicht definiert ist, wird die Leistung für die Warmwasserbereitung zurückgegeben (als `aux` zu verwenden).
+      `aux` kann für die Leistung der Warmwasserbereitung verwendet werden.
     en: |
-      Can be used to get `grid`, `pv` and `aux` data. 
-      If `usage` is not defined, then return the power for the water heating (to be used as `aux`).
+      `aux` can be used for water heating power.
 params:
   - name: usage
-    choice: ["grid", "pv"]
+    choice: ["grid", "pv", "aux"]
   - name: host
 render: |
   {{- define "uri" -}}
   http://{{ .host }}/all
   {{- end }}
   type: custom
-  {{- if not .usage }}
-  power:
-    source: http
-    uri: {{ include "uri" . }}
-    jq: .power_sf
-  {{- end }}
   {{- if eq .usage "grid" }}
   power:
     source: http
@@ -78,3 +70,9 @@ render: |
     jq: .PvEnergy[0]
     scale: 0.001
   {{- end }}
+  {{- if eq .usage "aux" }}
+  power:
+    source: http
+    uri: {{ include "uri" . }}
+    jq: .power_sf
+  {{- end }}
diff --git a/templates/definition/meter/solaredge-hybrid.yaml b/templates/definition/meter/solaredge-hybrid.yaml
index e20eaa75..183ee216 100644
--- a/templates/definition/meter/solaredge-hybrid.yaml
+++ b/templates/definition/meter/solaredge-hybrid.yaml
@@ -37,17 +37,19 @@ render: |
   power:
     source: calc
     add:
-    - source: sunspec
-      {{- include "modbus" . | indent 4 }}
-      timeout: {{ .timeout }}
-      value: 101|103:DCW
-    - source: modbus
-      {{- include "modbus" . | indent 4 }}
-      timeout: {{ .timeout }}
-      register:
-        address: 62836 # Battery 1 Instantaneous Power
-        type: holding
-        decode: float32s
+      - source: sunspec
+        {{- include "modbus" . | indent 6 }}
+        timeout: {{ .timeout }}
+        value: 
+          - 101:DCW
+          - 103:DCW
+      - source: modbus
+        {{- include "modbus" . | indent 6 }}
+        timeout: {{ .timeout }}
+        register:
+          address: 62836 # Battery 1 Instantaneous Power
+          type: holding
+          decode: float32s
   {{- end }}
   {{- if eq .usage "battery" }}
   power:
diff --git a/templates/definition/meter/solaredge-inverter.yaml b/templates/definition/meter/solaredge-inverter.yaml
index d3fe7e0f..64853a05 100644
--- a/templates/definition/meter/solaredge-inverter.yaml
+++ b/templates/definition/meter/solaredge-inverter.yaml
@@ -33,5 +33,7 @@ render: |
     source: sunspec
     {{- include "modbus" . | indent 2 }}
     timeout: {{ .timeout }}
-    value: 101|103:W # sunspec inverter AC power reading
+    value:
+      - 101:W
+      - 103:W
   {{- end }}
diff --git a/templates/definition/meter/sunspec-hybrid.yaml b/templates/definition/meter/sunspec-hybrid.yaml
index 72c0fa53..4c44d1e0 100644
--- a/templates/definition/meter/sunspec-hybrid.yaml
+++ b/templates/definition/meter/sunspec-hybrid.yaml
@@ -15,59 +15,104 @@ params:
     advanced: true
 render: |
   {{- if eq .usage "grid" }}
-  type: mbmd
-  {{- include "modbus" . }}
+  type: custom
   # sunspec model 203 (int+sf)/ 213 (float) meter
-  power: 203|213:W
-  energy: 203|213:TotWhImp
+  power:
+    source: sunspec
+    {{- include "modbus" . | indent 2 }}
+    value: 
+      - 203:W
+      - 213:W
+  energy:
+    source: sunspec
+    {{- include "modbus" . | indent 2 }}
+    value: 
+      - 203:TotWhImp
+      - 213:TotWhImp
   currents:
-    - 203|213:AphA
-    - 203|213:AphB
-    - 203|213:AphC
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:AphA
+        - 213:AphA
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:AphB
+        - 213:AphB
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:AphC
+        - 213:AphC
   voltages:
-    - 203|213:PhVphA
-    - 203|213:PhVphB
-    - 203|213:PhVphC
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:PhVphA
+        - 213:PhVphA
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:PhVphB
+        - 213:PhVphB
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:PhVphC
+        - 213:PhVphC
   powers:
-    - 203|213:WphA
-    - 203|213:WphB
-    - 203|213:WphC
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:WphA
+        - 213:WphA
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:WphB
+        - 213:WphB
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:WphC
+        - 213:WphC
   {{- end }}
   {{- if eq .usage "pv" }}
   type: custom
   power:
     source: calc
     add:
-    - source: sunspec
-      {{- include "modbus" . | indent 4 }}
-      value: 160:1:DCW # mppt 1
-    - source: sunspec
-      {{- include "modbus" . | indent 4 }}
-      value: 160:2:DCW # mppt 2
+      - source: sunspec
+        {{- include "modbus" . | indent 6 }}
+        value: 160:1:DCW # mppt 1
+      - source: sunspec
+        {{- include "modbus" . | indent 6 }}
+        value: 160:2:DCW # mppt 2
   energy:
     source: calc
     add:
-    - source: sunspec
-      {{- include "modbus" . | indent 4 }}
-      value: 160:1:DCWH # mppt 1
-      scale: 0.001
-    - source: sunspec
-      {{- include "modbus" . | indent 4 }}
-      value: 160:2:DCWH # mppt 2
-      scale: 0.001
+      - source: sunspec
+        {{- include "modbus" . | indent 6 }}
+        value: 160:1:DCWH # mppt 1
+        scale: 0.001
+      - source: sunspec
+        {{- include "modbus" . | indent 6 }}
+        value: 160:2:DCWH # mppt 2
+        scale: 0.001
   {{- end }}
   {{- if eq .usage "battery" }}
   type: custom
   power:
     source: calc
     add:
-    - source: sunspec
-      {{- include "modbus" . | indent 4 }}
-      value: 160:3:DCW # mppt 3 (charge)
-      scale: -1
-    - source: sunspec
-      {{- include "modbus" . | indent 4 }}
-      value: 160:4:DCW # mppt 4 (discharge)
+      - source: sunspec
+        {{- include "modbus" . | indent 6 }}
+        value: 160:3:DCW # mppt 3 (charge)
+        scale: -1
+      - source: sunspec
+        {{- include "modbus" . | indent 6 }}
+        value: 160:4:DCW # mppt 4 (discharge)
   energy:
     source: sunspec
     {{- include "modbus" . | indent 2 }}
@@ -76,8 +121,8 @@ render: |
   soc:
     source: sunspec
     {{- include "modbus" . | indent 2 }}
-    value: 124:ChaState
-  {{- if .capacity }}
+    value:
+      - 124:ChaState
+      - 802:SoC
   capacity: {{ .capacity }} # kWh
   {{- end }}
-  {{- end }}
diff --git a/templates/definition/meter/sunspec-inverter.yaml b/templates/definition/meter/sunspec-inverter.yaml
index 031227db..8a7ac6d1 100644
--- a/templates/definition/meter/sunspec-inverter.yaml
+++ b/templates/definition/meter/sunspec-inverter.yaml
@@ -9,63 +9,96 @@ params:
     choice: ["grid", "pv", "battery"]
   - name: modbus
     choice: ["tcpip", "rs485"]
-  - name: integer
-    description:
-      de: Integer Registerzugriff (nur für Netzzähler)
-      en: Integer register access (grid meter only)
-    help:
-      de: Einstellung Float/Integer im Wechselrichter überprüfen
-      en: Verify Float/Integer setting in inverter
-    advanced: true
-    type: bool
   - name: capacity
     advanced: true
 render: |
-  type: mbmd
-  {{- include "modbus" . }}
   {{- if eq .usage "grid" }}
-  model: sunspec
-  {{- if eq .integer "true" }}
-  # sunspec model 203 (int+sf) meter
-  power: 203:W
-  energy: 203:TotWhImp
-  currents:
-    - 203:AphA
-    - 203:AphB
-    - 203:AphC
-  voltages:
-    - 203:PhVphA
-    - 203:PhVphB
-    - 203:PhVphC
-  powers:
-    - 203:WphA
-    - 203:WphB
-    - 203:WphC
-  {{ else }}
-  # sunspec model 213 (float) meter
-  power: 213:W
-  energy: 213:TotWhImp
+  type: custom
+  # sunspec model 203 (int+sf)/ 213 (float) meter
+  power:
+    source: sunspec
+    {{- include "modbus" . | indent 2 }}
+    value: 
+      - 203:W
+      - 213:W
+  energy:
+    source: sunspec
+    {{- include "modbus" . | indent 2 }}
+    value: 
+      - 203:TotWhImp
+      - 213:TotWhImp
   currents:
-    - 213:AphA
-    - 213:AphB
-    - 213:AphC
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:AphA
+        - 213:AphA
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:AphB
+        - 213:AphB
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:AphC
+        - 213:AphC
   voltages:
-    - 213:PhVphA
-    - 213:PhVphB
-    - 213:PhVphC
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:PhVphA
+        - 213:PhVphA
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:PhVphB
+        - 213:PhVphB
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:PhVphC
+        - 213:PhVphC
   powers:
-    - 213:WphA
-    - 213:WphB
-    - 213:WphC
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:WphA
+        - 213:WphA
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:WphB
+        - 213:WphB
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      value: 
+        - 203:WphC
+        - 213:WphC
+  {{- end }}
+  {{- if eq .usage "pv" }}
+  type: custom
+  power:
+    source: sunspec
+    {{- include "modbus" . | indent 2 }}
+    value:
+      - 101:W
+      - 103:W
+  energy:
+    source: sunspec
+    {{- include "modbus" . | indent 2 }}
+    value:
+      - 101:WH
+      - 103:WH
+    scale: 0.001
   {{- end }}
-  {{- else }}
-  model: sunspec
-  power: Power
-  energy: Export
   {{- if eq .usage "battery" }}
-  soc: 124:ChaState
-  {{- if .capacity }}
+  type: custom
+  soc:
+    source: sunspec
+    {{- include "modbus" . | indent 2 }}
+    value:
+      - 124:ChaState
+      - 802:SoC
   capacity: {{ .capacity }} # kWh
   {{- end }}
-  {{- end }}  
-  {{- end }}
diff --git a/util/modbus/mbmd.go b/util/modbus/mbmd.go
index 7ceeece8..ec9693eb 100644
--- a/util/modbus/mbmd.go
+++ b/util/modbus/mbmd.go
@@ -21,11 +21,9 @@ func ParseOperation(dev meters.Device, measurement string) (Operation, error) {
 	// if measurement cannot be parsed it could be SunSpec model/block/point
 	op.MBMD.IEC61850, err = meters.MeasurementString(measurement)
 	if err != nil {
-		suns, err := ParsePoint(measurement)
-		if err == nil {
-			op.SunSpec = suns[0]
+		if op.SunSpec, err = ParsePoint(measurement); err != nil {
+			return op, err
 		}
-		return op, err
 	}
 
 	// for RS485 check if producer supports the measurement
diff --git a/util/modbus/register.go b/util/modbus/register.go
index c672bfaf..3f7c8096 100644
--- a/util/modbus/register.go
+++ b/util/modbus/register.go
@@ -22,9 +22,6 @@ type Register struct {
 }
 
 func (r Register) Error() error {
-	if r.Address == 0 {
-		return errors.New("address is required")
-	}
 	if r.Type == "" {
 		return errors.New("type is required")
 	}
diff --git a/util/modbus/sunspec.go b/util/modbus/sunspec.go
index 363ef73c..8b862910 100644
--- a/util/modbus/sunspec.go
+++ b/util/modbus/sunspec.go
@@ -13,39 +13,30 @@ type SunSpecOperation struct {
 }
 
 // ParsePoint parses sunspec point from string
-func ParsePoint(selector string) ([]SunSpecOperation, error) {
+func ParsePoint(selector string) (SunSpecOperation, error) {
+	var (
+		res SunSpecOperation
+		err error
+	)
+
 	el := strings.Split(selector, ":")
 	if len(el) < 2 || len(el) > 3 {
-		return nil, fmt.Errorf("invalid sunspec format: %s", selector)
+		return res, fmt.Errorf("invalid sunspec format: %s", selector)
 	}
 
-	models := strings.Split(el[0], "|")
-	if len(models) == 0 {
-		return nil, fmt.Errorf("missing sunspec model: %s", selector)
+	if res.Model, err = strconv.Atoi(el[0]); err != nil {
+		return res, fmt.Errorf("invalid sunspec model: %s", selector)
 	}
 
-	var res []SunSpecOperation
-	for _, m := range models {
-		model, err := strconv.Atoi(m)
+	if len(el) == 3 {
+		// block is the middle element
+		res.Block, err = strconv.Atoi(el[1])
 		if err != nil {
-			return nil, fmt.Errorf("invalid sunspec model: %s", selector)
-		}
-
-		var block int
-		if len(el) == 3 {
-			// block is the middle element
-			block, err = strconv.Atoi(el[1])
-			if err != nil {
-				return nil, fmt.Errorf("invalid sunspec block: %s", selector)
-			}
+			return res, fmt.Errorf("invalid sunspec block: %s", selector)
 		}
-
-		res = append(res, SunSpecOperation{
-			Model: model,
-			Block: block,
-			Point: el[len(el)-1],
-		})
 	}
 
+	res.Point = el[len(el)-1]
+
 	return res, nil
 }
diff --git a/vehicle/polestar/api.go b/vehicle/polestar/api.go
index 1ffc08a2..0508af66 100644
--- a/vehicle/polestar/api.go
+++ b/vehicle/polestar/api.go
@@ -2,6 +2,9 @@ package polestar
 
 import (
 	"context"
+	"net/http"
+	"net/url"
+	"strings"
 
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
@@ -9,12 +12,16 @@ import (
 	"golang.org/x/oauth2"
 )
 
+// https://github.com/leeyuentuen/polestar_api
 // https://github.com/TA2k/ioBroker.polestar
 
-const ApiURI = "https://pc-api.polestar.com/eu-north-1/my-star"
+const (
+	ApiURI   = "https://pc-api.polestar.com/eu-north-1"
+	ApiURIv1 = ApiURI + "/my-star"
+	ApiURIv2 = ApiURI + "/mystar-v2"
+)
 
 type API struct {
-	*request.Helper
 	client *graphql.Client
 }
 
@@ -28,12 +35,9 @@ func NewAPI(log *util.Logger, identity oauth2.TokenSource) *API {
 	}
 
 	v := &API{
-		Helper: request.NewHelper(log),
-		client: graphql.NewClient(ApiURI, httpClient),
+		client: graphql.NewClient(ApiURIv2, httpClient),
 	}
 
-	v.Client.Transport = httpClient.Transport
-
 	return v
 }
 
@@ -42,7 +46,10 @@ func (v *API) Vehicles(ctx context.Context) ([]ConsumerCar, error) {
 		GetConsumerCarsV2 []ConsumerCar `graphql:"getConsumerCarsV2"`
 	}
 
-	err := v.client.Query(ctx, &res, nil, graphql.OperationName("getCars"))
+	err := v.client.WithRequestModifier(func(req *http.Request) {
+		req.URL, _ = url.Parse(strings.ReplaceAll(req.URL.String(), "mystar-v2", "my-star"))
+	}).Query(ctx, &res, nil, graphql.OperationName("getCars"))
+
 	return res.GetConsumerCarsV2, err
 }
 
@@ -51,9 +58,10 @@ func (v *API) Status(ctx context.Context, vin string) (BatteryData, error) {
 		BatteryData `graphql:"getBatteryData(vin: $vin)"`
 	}
 
-	err := v.client.Query(ctx, &res, map[string]interface{}{
+	err := v.client.Query(ctx, &res, map[string]any{
 		"vin": vin,
 	}, graphql.OperationName("GetBatteryData"))
+
 	return res.BatteryData, err
 }
 
@@ -62,8 +70,9 @@ func (v *API) Odometer(ctx context.Context, vin string) (OdometerData, error) {
 		OdometerData `graphql:"getOdometerData(vin: $vin)"`
 	}
 
-	err := v.client.Query(ctx, &res, map[string]interface{}{
+	err := v.client.Query(ctx, &res, map[string]any{
 		"vin": vin,
 	}, graphql.OperationName("GetOdometerData"))
+
 	return res.OdometerData, err
 }
diff --git a/vehicle/polestar/identity.go b/vehicle/polestar/identity.go
index f35f3e8d..09b890aa 100644
--- a/vehicle/polestar/identity.go
+++ b/vehicle/polestar/identity.go
@@ -1,15 +1,18 @@
 package polestar
 
 import (
+	"context"
 	"fmt"
 	"net/http"
 	"net/http/cookiejar"
 	"net/url"
 	"strings"
+	"time"
 
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/oauth"
 	"github.com/evcc-io/evcc/util/request"
+	"github.com/hasura/go-graphql-client"
 	"github.com/samber/lo"
 	"golang.org/x/net/publicsuffix"
 	"golang.org/x/oauth2"
@@ -17,20 +20,23 @@ import (
 
 // https://github.com/TA2k/ioBroker.polestar
 
-const (
-	OAuthURI  = "https://polestarid.eu.polestar.com"
-	basicAuth = "cG9seHBsb3JlOlhhaUtvb0hlaXJlaXNvb3NhaDBFdjZxdW9oczhjb2hGZUtvaHdpZTFhZTdraWV3b2hkb295ZWk5QWVZZWlXb2g"
-)
+const OAuthURI = "https://polestarid.eu.polestar.com"
 
 // https://polestarid.eu.polestar.com/.well-known/openid-configuration
 var OAuth2Config = &oauth2.Config{
-	ClientID:    "polxplore",
-	RedirectURL: "polestar-explore://explore.polestar.com",
+	ClientID:    "polmystar",
+	RedirectURL: "https://www.polestar.com/sign-in-callback",
 	Endpoint: oauth2.Endpoint{
 		AuthURL:  OAuthURI + "/as/authorization.oauth2",
 		TokenURL: OAuthURI + "/as/token.oauth2",
 	},
-	Scopes: []string{"openid", "profile", "email", "customer:attributes"},
+	Scopes: []string{
+		"openid", "profile", "email", "customer:attributes",
+		// "conve:recharge_status", "conve:fuel_status", "conve:odometer_status",
+		// "energy:charging_connection_status", "energy:electric_range", "energy:estimated_charging_time", "energy:recharge_status",
+		// "energy:battery_charge_level", "energy:charging_system_status", "energy:charging_timer", "energy:electric_range", "energy:recharge_status",
+		// "energy:battery_charge_level",
+	},
 }
 
 type Identity struct {
@@ -40,101 +46,86 @@ type Identity struct {
 
 // NewIdentity creates Polestar identity
 func NewIdentity(log *util.Logger) *Identity {
-	return &Identity{
+	v := &Identity{
 		Helper: request.NewHelper(log),
 	}
-}
 
-func (v *Identity) Login(user, password string) error {
-	if v.Client.Jar == nil {
-		var err error
-		v.Client.Jar, err = cookiejar.New(&cookiejar.Options{
-			PublicSuffixList: publicsuffix.List,
-		})
-		if err != nil {
-			return err
-		}
-	}
+	v.Client.Jar, _ = cookiejar.New(&cookiejar.Options{
+		PublicSuffixList: publicsuffix.List,
+	})
 
-	cv := oauth2.GenerateVerifier()
+	return v
+}
 
+func (v *Identity) Login(user, password string) error {
 	state := lo.RandomString(16, lo.AlphanumericCharset)
-	uri := OAuth2Config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.S256ChallengeOption(cv))
+	uri := OAuth2Config.AuthCodeURL(state, oauth2.AccessTypeOffline)
 
 	var param request.InterceptResult
-	v.Client.CheckRedirect, param = request.InterceptRedirect("resumePath", false)
+	v.Client.CheckRedirect, param = request.InterceptRedirect("resumePath", true)
+	defer func() { v.Client.CheckRedirect = nil }()
 
-	_, err := v.Get(uri)
-
-	var resume string
-	if err == nil {
-		resume, err = param()
+	if _, err := v.Get(uri); err != nil {
+		return err
 	}
 
-	v.Client.CheckRedirect = nil
-
-	var code string
-	if err == nil {
-		params := url.Values{
-			"pf.username": []string{user},
-			"pf.pass":     []string{password},
-		}
+	resume, err := param()
+	if err != nil {
+		return err
+	}
 
-		var param request.InterceptResult
-		v.Client.CheckRedirect, param = request.InterceptRedirect("code", true)
+	params := url.Values{
+		"pf.username": []string{user},
+		"pf.pass":     []string{password},
+	}
 
-		uri = fmt.Sprintf("%s/as/%s/resume/as/authorization.ping?client_id=%s", OAuthURI, resume, OAuth2Config.ClientID)
+	uri = fmt.Sprintf("%s/as/%s/resume/as/authorization.ping?client_id=%s", OAuthURI, resume, OAuth2Config.ClientID)
+	v.Client.CheckRedirect, param = request.InterceptRedirect("code", true)
+	defer func() { v.Client.CheckRedirect = nil }()
 
-		if _, err = v.Post(uri, request.FormContent, strings.NewReader(params.Encode())); err == nil {
-			code, err = param()
-		}
+	var code string
+	if _, err = v.Post(uri, request.FormContent, strings.NewReader(params.Encode())); err == nil {
+		code, err = param()
+	}
 
-		v.Client.CheckRedirect = nil
+	if err != nil {
+		return err
 	}
 
-	var token oauth.Token
-	if err == nil {
-		params := url.Values{
-			"code":          []string{code},
-			"code_verifier": []string{cv},
-			"redirect_uri":  []string{OAuth2Config.RedirectURL},
-			"grant_type":    []string{"authorization_code"},
-		}
-
-		var req *http.Request
-		req, err = request.New(http.MethodPost, OAuth2Config.Endpoint.TokenURL, strings.NewReader(params.Encode()), map[string]string{
-			"Content-Type":  request.FormContent,
-			"Authorization": "Basic " + basicAuth,
-		})
-		if err == nil {
-			err = v.DoJSON(req, &token)
-		}
+	var res struct {
+		Token `graphql:"getAuthToken(code: $code)"`
 	}
 
-	if err == nil {
-		v.TokenSource = oauth.RefreshTokenSource((*oauth2.Token)(&token), v)
+	if err := graphql.NewClient(ApiURI+"/auth", v.Client).
+		Query(context.Background(), &res, map[string]any{
+			"code": code,
+		}, graphql.OperationName("getAuthToken")); err != nil {
+		return err
 	}
 
+	v.TokenSource = oauth.RefreshTokenSource(&oauth2.Token{
+		AccessToken:  res.AccessToken,
+		RefreshToken: res.RefreshToken,
+		Expiry:       time.Now().Add(time.Duration(res.ExpiresIn) * time.Second),
+	}, v)
+
 	return err
 }
 
 func (v *Identity) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) {
-	data := url.Values{
-		"redirect_uri":  []string{OAuth2Config.RedirectURL},
-		"refresh_token": []string{token.RefreshToken},
-		"grant_type":    []string{"refresh_token"},
-	}
-	req, err := request.New(http.MethodPost, OAuth2Config.Endpoint.TokenURL, strings.NewReader(data.Encode()), map[string]string{
-		"Content-Type":  request.FormContent,
-		"Authorization": "Basic " + basicAuth,
-	})
-
-	var tok oauth.Token
-	if err == nil {
-		if err := v.DoJSON(req, &tok); err != nil {
-			return nil, err
-		}
+	var res struct {
+		Token `graphql:"refreshAuthToken(token: $token)"`
 	}
 
-	return (*oauth2.Token)(&tok), err
+	err := graphql.NewClient(ApiURI+"/auth", v.Client).WithRequestModifier(func(req *http.Request) {
+		req.Header.Set("Authorization", "Bearer "+token.AccessToken)
+	}).Query(context.Background(), &res, map[string]any{
+		"token": token.RefreshToken,
+	}, graphql.OperationName("refreshAuthToken"))
+
+	return &oauth2.Token{
+		AccessToken:  res.AccessToken,
+		RefreshToken: res.RefreshToken,
+		Expiry:       time.Now().Add(time.Duration(res.ExpiresIn) * time.Second),
+	}, err
 }
diff --git a/vehicle/polestar/provider.go b/vehicle/polestar/provider.go
index 398fae77..5b71def7 100644
--- a/vehicle/polestar/provider.go
+++ b/vehicle/polestar/provider.go
@@ -9,8 +9,6 @@ import (
 	"github.com/evcc-io/evcc/util"
 )
 
-// https://github.com/TA2k/ioBroker.polestar
-
 type Provider struct {
 	statusG func() (BatteryData, error)
 	odoG    func() (OdometerData, error)
@@ -36,7 +34,7 @@ func NewProvider(log *util.Logger, api *API, vin string, timeout, cache time.Dur
 // Soc implements the api.Vehicle interface
 func (v *Provider) Soc() (float64, error) {
 	res, err := v.statusG()
-	return float64(res.BatteryChargeLevelPercentage), err
+	return res.BatteryChargeLevelPercentage, err
 }
 
 var _ api.ChargeState = (*Provider)(nil)
@@ -69,7 +67,7 @@ var _ api.VehicleOdometer = (*Provider)(nil)
 // Odometer implements the Provider.VehicleOdometer interface
 func (v *Provider) Odometer() (float64, error) {
 	res, err := v.odoG()
-	return float64(res.OdometerMeters) / 1e3, err
+	return res.OdometerMeters / 1e3, err
 }
 
 var _ api.VehicleFinishTimer = (*Provider)(nil)
diff --git a/vehicle/polestar/types.go b/vehicle/polestar/types.go
index a01f4df1..2d445733 100644
--- a/vehicle/polestar/types.go
+++ b/vehicle/polestar/types.go
@@ -1,18 +1,33 @@
 package polestar
 
+import "time"
+
+type Token struct {
+	AccessToken  string `graphql:"access_token"`
+	RefreshToken string `graphql:"refresh_token"`
+	ExpiresIn    int    `graphql:"expires_in"`
+}
+
 type ConsumerCar struct {
 	VIN                       string
 	InternalVehicleIdentifier string
 }
 
 type BatteryData struct {
-	BatteryChargeLevelPercentage       int
+	BatteryChargeLevelPercentage       float64
 	ChargerConnectionStatus            string
 	ChargingStatus                     string
 	EstimatedChargingTimeToFullMinutes int
 	EstimatedDistanceToEmptyKm         int
+	EventUpdatedTimestamp              EventUpdatedTimestamp
 }
 
 type OdometerData struct {
-	OdometerMeters int
+	OdometerMeters        float64
+	EventUpdatedTimestamp EventUpdatedTimestamp
+}
+
+type EventUpdatedTimestamp struct {
+	ISO time.Time
+	// Unix int64 `json:",string"`
 }
diff --git a/vehicle/tesla-vehicle-command/identity.go b/vehicle/tesla-vehicle-command/identity.go
index c2d0073c..8878e262 100644
--- a/vehicle/tesla-vehicle-command/identity.go
+++ b/vehicle/tesla-vehicle-command/identity.go
@@ -76,15 +76,22 @@ func NewIdentity(log *util.Logger, token *oauth2.Token) (*Identity, error) {
 
 	// database token
 	if !token.Valid() {
-		if err := settings.Json(v.settingsKey(), &token); err != nil {
-			return nil, fmt.Errorf("missing token setting for %s: %w", claims.Subject, err)
+		var tok oauth2.Token
+		if err := settings.Json(v.settingsKey(), &tok); err == nil {
+			token = &tok
 		}
+	}
 
-		if !token.Valid() {
-			return nil, errors.New("token expired")
+	if !token.Valid() && token.RefreshToken != "" {
+		if tok, err := v.RefreshToken(token); err == nil {
+			token = tok
 		}
 	}
 
+	if !token.Valid() {
+		return nil, errors.New("token expired")
+	}
+
 	// acct, err := account.New(token.AccessToken, userAgent)
 	// if err != nil {
 	// 	return nil, err
diff --git a/vehicle/tronity.go b/vehicle/tronity.go
index ae2571b8..c1f194cf 100644
--- a/vehicle/tronity.go
+++ b/vehicle/tronity.go
@@ -165,7 +165,7 @@ func (v *Tronity) RefreshToken(_ *oauth2.Token) (*oauth2.Token, error) {
 
 // vehicles implements the vehicles api
 func (v *Tronity) vehicles() ([]tronity.Vehicle, error) {
-	uri := fmt.Sprintf("%s/v1/vehicles", tronity.URI)
+	uri := fmt.Sprintf("%s/tronity/vehicles", tronity.URI)
 
 	var res tronity.Vehicles
 	err := v.GetJSON(uri, &res)
@@ -175,7 +175,7 @@ func (v *Tronity) vehicles() ([]tronity.Vehicle, error) {
 
 // bulk implements the bulk api
 func (v *Tronity) bulk() (tronity.Bulk, error) {
-	uri := fmt.Sprintf("%s/v1/vehicles/%s/bulk", tronity.URI, v.vid)
+	uri := fmt.Sprintf("%s/tronity/vehicles/%s/last_record", tronity.URI, v.vid)
 
 	var res tronity.Bulk
 	err := v.GetJSON(uri, &res)
@@ -235,12 +235,12 @@ func (v *Tronity) post(uri string) error {
 
 // startCharge implements the api.VehicleChargeController interface
 func (v *Tronity) startCharge() error {
-	uri := fmt.Sprintf("%s/v1/vehicles/%s/charge_start", tronity.URI, v.vid)
+	uri := fmt.Sprintf("%s/tronity/vehicles/%s/start_charging", tronity.URI, v.vid)
 	return v.post(uri)
 }
 
 // stopCharge implements the api.VehicleChargeController interface
 func (v *Tronity) stopCharge() error {
-	uri := fmt.Sprintf("%s/v1/vehicles/%s/charge_stop", tronity.URI, v.vid)
+	uri := fmt.Sprintf("%s/tronity/vehicles/%s/stop_charging", tronity.URI, v.vid)
 	return v.post(uri)
 }
diff --git a/vehicle/tronity/auth.go b/vehicle/tronity/auth.go
index 00257ddd..0a027c3b 100644
--- a/vehicle/tronity/auth.go
+++ b/vehicle/tronity/auth.go
@@ -4,15 +4,15 @@ import (
 	"golang.org/x/oauth2"
 )
 
-const URI = "https://api-eu.tronity.io"
+const URI = "https://api.tronity.tech"
 
 func OAuth2Config(id, secret string) (*oauth2.Config, error) {
 	return &oauth2.Config{
 		ClientID:     id,
 		ClientSecret: secret,
 		Endpoint: oauth2.Endpoint{
-			AuthURL:  "https://api-eu.tronity.io/oauth/authorize",
-			TokenURL: "https://api-eu.tronity.io/oauth/authentication",
+			AuthURL:  "https://auth.tronity.io/oauth/v2/authorize",
+			TokenURL: "https://api.tronity.tech/authentication",
 		},
 		Scopes: []string{"read_vin", "read_vehicle_info", "read_odometer", "read_charge", "read_charge", "read_battery", "read_location", "write_charge_start_stop", "write_wake_up"},
 	}, nil
diff --git a/vehicle/tronity/types.go b/vehicle/tronity/types.go
index 3bb20924..8748a073 100644
--- a/vehicle/tronity/types.go
+++ b/vehicle/tronity/types.go
@@ -1,16 +1,11 @@
 package tronity
 
-// https://app.platform.tronity.io/docs#operation
+// https://app.tronity.tech/docs#section/Authentication-Flow
 
 const (
-	ReadBattery          = "read_battery"            // Read EV battery's capacity and state of charge
-	ReadCharge           = "read_charge"             // Know whether vehicle is charging
-	ReadLocation         = "read_location"           // Access location
-	ReadOdometer         = "read_odometer"           // Retrieve total distance traveled
-	ReadVehicleInfo      = "read_vehicle_info"       // Know make, model, and year
-	ReadVIN              = "read_vin"                // Read VIN
-	WriteChargeStartStop = "write_charge_start_stop" // Start or stop your vehicle's charging
-	WriteWakeUp          = "write_wake_up"           // Wake up car. Only valid for Tesla
+	ReadCharge           = "tronity_charging" // Know whether vehicle is charging
+	ReadOdometer         = "tronity_odometer" // Retrieve total distance traveled
+	WriteChargeStartStop = "tronity_control_charging"
 )
 
 type Vehicles struct {
@@ -26,7 +21,6 @@ type Vehicle struct {
 }
 
 type Bulk struct {
-	VIN      string
 	Odometer float64
 	Range    float64
 	Level    float64


Test patch:
diff --git a/assets/js/components/VehicleStatus.test.js b/assets/js/components/VehicleStatus.test.js
index dd00117d..60311bdc 100644
--- a/assets/js/components/VehicleStatus.test.js
+++ b/assets/js/components/VehicleStatus.test.js
@@ -144,43 +144,6 @@ describe("timer", () => {
       { remaining: "1:30\u202Fm" }
     );
   });
-  test("show guard timer if it exists", () => {
-    expectStatus(
-      {
-        connected: true,
-        guardAction: "enable",
-        guardRemainingInterpolated: 90,
-      },
-      "guard",
-      { remaining: "1:30\u202Fm" }
-    );
-  });
-  test("don't show guard timer if another timer exists", () => {
-    expectStatus(
-      {
-        connected: true,
-        charging: true,
-        pvAction: "disable",
-        pvRemainingInterpolated: 30,
-        guardAction: "enable",
-        guardRemainingInterpolated: 90,
-      },
-      "pvDisable",
-      { remaining: "30\u202Fs" }
-    );
-  });
-  test("show guard timer if charging and no other timer exists", () => {
-    expectStatus(
-      {
-        connected: true,
-        charging: true,
-        guardAction: "enable",
-        guardRemainingInterpolated: 90,
-      },
-      "guard",
-      { remaining: "1:30\u202Fm" }
-    );
-  });
 });
 
 describe("vehicle target soc", () => {
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index 548920ab..ecd6e472 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -527,10 +527,7 @@ func cacheExpecter(t *testing.T, lp *Loadpoint) (*util.Cache, func(key string, v
 		time.Sleep(100 * time.Millisecond) // wait for cache to catch up
 
 		p := cache.Get(key)
-		t.Logf("%s: %v", key, p.Val) // REMOVE
-		if p.Val != val {
-			t.Errorf("%s wanted: %v, got %v", key, val, p.Val)
-		}
+		assert.Equal(t, val, p.Val, key)
 	}
 	return cache, expect
 }
@@ -614,98 +611,6 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
 	ctrl.Finish()
 }
 
-// func TestTargetSoc(t *testing.T) {
-// 	ctrl := gomock.NewController(t)
-// 	vhc := api.NewMockVehicle(ctrl)
-
-// 	// TODO make vehicle settings mockable
-// 	// make vehicle settings discoverable
-// 	config.Vehicles().Add(config.NewStaticDevice[api.Vehicle](config.Named{}, vhc))
-
-// 	tc := []struct {
-// 		vehicle    api.Vehicle
-// 		limitSoc   int
-// 		vehicleSoc float64
-// 		res        bool
-// 	}{
-// 		{nil, 0, 0, false},     // never reached without vehicle
-// 		{nil, 0, 10, false},    // never reached without vehicle
-// 		{nil, 80, 0, false},    // never reached without vehicle
-// 		{nil, 80, 80, false},   // never reached without vehicle
-// 		{nil, 80, 100, false},  // never reached without vehicle
-// 		{vhc, 0, 0, false},     // target disabled
-// 		{vhc, 0, 10, false},    // target disabled
-// 		{vhc, 80, 0, false},    // target not reached
-// 		{vhc, 80, 80, true},    // target reached
-// 		{vhc, 80, 100, true},   // target reached
-// 		{vhc, 100, 100, false}, // target reached, let ev control deactivation
-// 	}
-
-// 	for _, tc := range tc {
-// 		t.Logf("%+v", tc)
-
-// 		lp := &Loadpoint{
-// 			vehicle:    tc.vehicle,
-// 			limitSoc:   tc.limitSoc,
-// 			vehicleSoc: tc.vehicleSoc,
-// 		}
-
-// 		if res := lp.limitSocReached(); tc.res != res {
-// 			t.Errorf("expected %v, got %v", tc.res, res)
-// 		}
-// 	}
-// }
-
-// func TestMinSoc(t *testing.T) {
-// 	ctrl := gomock.NewController(t)
-// 	vhc := api.NewMockVehicle(ctrl)
-
-// 	// TODO make vehicle settings mockable
-// 	// make vehicle settings discoverable
-// 	config.Vehicles().Add(config.NewStaticDevice[api.Vehicle](config.Named{}, vhc))
-
-// 	tc := []struct {
-// 		vehicle *api.MockVehicle
-// 		min     int
-// 		soc     float64
-// 		energy  float64
-// 		res     bool
-// 	}{
-// 		{nil, 0, 0, 0, false},    // never reached without vehicle
-// 		{nil, 0, 10, 0, false},   // never reached without vehicle
-// 		{nil, 80, 0, 0, false},   // never reached without vehicle
-// 		{nil, 80, 80, 0, false},  // never reached without vehicle
-// 		{nil, 80, 100, 0, false}, // never reached without vehicle
-// 		{vhc, 0, 0, 0, false},    // min disabled
-// 		{vhc, 0, 10, 0, false},   // min disabled
-// 		{vhc, 80, 0, 0, true},    // min not reached
-// 		{vhc, 80, 10, 0, true},   // min not reached
-// 		{vhc, 80, 0, 8.0, true},  // min energy not reached
-// 		{vhc, 80, 0, 9.0, false}, // min energy reached
-// 		{vhc, 80, 80, 0, false},  // min reached
-// 		{vhc, 80, 100, 0, false}, // min reached
-// 	}
-
-// 	for _, tc := range tc {
-// 		lp := &Loadpoint{
-// 			log: util.NewLogger("foo"),
-// 			Soc: SocConfig{
-// 				min: tc.min,
-// 			},
-// 			vehicleSoc:    tc.soc,
-// 			sessionEnergy: NewEnergyMetrics(),
-// 		}
-// 		lp.sessionEnergy.Update(tc.energy / 1e3)
-
-// 		if v := tc.vehicle; v != nil {
-// 			lp.vehicle = tc.vehicle // avoid assigning nil to interface
-// 			v.EXPECT().Capacity().Return(10.0).MaxTimes(1)
-// 		}
-
-// 		assert.Equal(t, tc.res, lp.minSocNotReached(), tc)
-// 	}
-// }
-
 func TestSocPoll(t *testing.T) {
 	clock := clock.NewMock()
 	tRefresh := pollInterval
@@ -787,133 +692,74 @@ func TestSocPoll(t *testing.T) {
 			lp.socUpdated = clock.Now()
 		}
 
-		if tc.res != res {
-			t.Errorf("expected %v, got %v", tc.res, res)
-		}
+		assert.Equal(t, tc.res, res)
 	}
 }
 
-// test guard timer is properly published during disable cycle
-func TestGuardPublish(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	defer ctrl.Finish()
-	clock := clock.NewMock()
-	charger := api.NewMockCharger(ctrl)
-	rater := api.NewMockChargeRater(ctrl)
-	startPointInTime := clock.Now()
-
-	lp := &Loadpoint{
-		log:           util.NewLogger("foo"),
-		bus:           evbus.New(),
-		clock:         clock,
-		charger:       charger,
-		chargeMeter:   &Null{}, // silence nil panics
-		chargeRater:   rater,
-		chargeTimer:   &Null{}, // silence nil panics
-		guardUpdated:  startPointInTime,
-		pvTimer:       time.Time{},
-		wakeUpTimer:   NewTimer(),
-		sessionEnergy: NewEnergyMetrics(),
-		minCurrent:    minA,
-		maxCurrent:    maxA,
-		chargeCurrent: maxA,
-		status:        api.StatusC,
-		mode:          api.ModePV,
-		enabled:       true,
-		Disable:       ThresholdConfig{Delay: 2 * time.Minute, Threshold: 0}, // t, W
-		GuardDuration: 3 * time.Minute,
+// test PV hysteresis after phase switch down, depending on remaining energy
+func TestPVHysteresisAfterPhaseSwitch(t *testing.T) {
+	const dt = time.Minute
+	type se struct {
+		site    float64
+		delay   time.Duration // test case delay since start
+		current float64
 	}
-
-	util.LogLevel("DEBUG", nil)
-
-	attachListeners(t, lp)
-	// attach cache for verifying values
-	_, expectCache := cacheExpecter(t, lp)
-
-	rater.EXPECT().ChargedEnergy().AnyTimes()
-	charger.EXPECT().Enabled().DoAndReturn(func() (bool, error) { return lp.enabled, nil }).AnyTimes() // just follow the loadpoint
-	charger.EXPECT().Status().Return(api.StatusC, nil).Times(4)
-	charger.EXPECT().Enable(false).Return(nil)
-
 	tc := []struct {
-		step                string
-		guardUpdates        time.Time
-		guardTimerRemaining time.Duration
+		series []se
 	}{
-		{"kick off pv disable timer", startPointInTime, 3 * time.Minute},
-		{"continue pv disable timer", startPointInTime, 2 * time.Minute},
-		{"disable charger prevented by guard", startPointInTime, 1 * time.Minute},
-		{"guard elapse, publish new guard time", startPointInTime.Add(3 * time.Minute), time.Duration(0)},
+		// immediately disable when threshold met after phase switch
+		{[]se{
+			{1200, 0, minA},
+			{1200, 1, minA},
+			{1200, dt - 1, minA},
+			{1200, dt + 1, 0},
+		}},
+		// stay enabled when threshold not met after phase switch
+		{[]se{
+			{500, 0, minA},
+			{500, 1, minA},
+			{500, dt - 1, minA},
+			{500, dt + 1, minA},
+		}},
 	}
 
 	for _, tc := range tc {
-		t.Logf("%+v", tc)
-		lp.Update(15000, false, false, false, 0, nil, nil)
-		assert.Equal(t, tc.guardUpdates, lp.guardUpdated)
-		expectCache(guardTimer+"Remaining", tc.guardTimerRemaining)
-		clock.Add(time.Minute)
-	}
-}
+		t.Log(tc)
 
-// test guard timer is properly published if charger disable sequence is aborted
-func TestGuardPublishOnDisableAbort(t *testing.T) {
-	ctrl := gomock.NewController(t)
-	defer ctrl.Finish()
-	clock := clock.NewMock()
-	charger := api.NewMockCharger(ctrl)
-	rater := api.NewMockChargeRater(ctrl)
-	startPointInTime := clock.Now()
+		clck := clock.NewMock()
+		ctrl := gomock.NewController(t)
 
-	lp := &Loadpoint{
-		log:           util.NewLogger("foo"),
-		bus:           evbus.New(),
-		clock:         clock,
-		charger:       charger,
-		chargeMeter:   &Null{}, // silence nil panics
-		chargeRater:   rater,
-		chargeTimer:   &Null{}, // silence nil panics
-		guardUpdated:  startPointInTime,
-		pvTimer:       time.Time{},
-		wakeUpTimer:   NewTimer(),
-		sessionEnergy: NewEnergyMetrics(),
-		minCurrent:    minA,
-		maxCurrent:    maxA,
-		chargeCurrent: maxA,
-		status:        api.StatusC,
-		mode:          api.ModePV,
-		enabled:       true,
-		Disable:       ThresholdConfig{Delay: 2 * time.Minute, Threshold: 0}, // t, W
-		GuardDuration: 4 * time.Minute,
-	}
+		switcher := api.NewMockPhaseSwitcher(ctrl)
+		switcher.EXPECT().Phases1p3p(1)
 
-	util.LogLevel("DEBUG", nil)
+		charger := struct {
+			*api.MockCharger
+			*api.MockPhaseSwitcher
+		}{
+			api.NewMockCharger(ctrl), switcher,
+		}
 
-	attachListeners(t, lp)
-	// attach cache for verifying values
-	_, expectCache := cacheExpecter(t, lp)
+		Voltage = 100
+		lp := &Loadpoint{
+			log:        util.NewLogger("foo"),
+			clock:      clck,
+			charger:    charger,
+			minCurrent: minA,
+			maxCurrent: maxA,
+			Disable: ThresholdConfig{
+				Delay: dt,
+			},
+			status:  api.StatusC,
+			enabled: true,
+		}
 
-	rater.EXPECT().ChargedEnergy().AnyTimes()
-	charger.EXPECT().Enabled().DoAndReturn(func() (bool, error) { return lp.enabled, nil }).AnyTimes() // just follow the loadpoint
-	charger.EXPECT().Status().Return(api.StatusC, nil).Times(5)
+		start := clck.Now()
 
-	tc := []struct {
-		step                string
-		sitePower           float64
-		guardUpdated        time.Time
-		guardTimerRemaining time.Duration
-	}{
-		{"kick off pv disable timer", 15000, startPointInTime, 4 * time.Minute},
-		{"continue pv disable timer", 15000, startPointInTime, 3 * time.Minute},
-		{"disable charger prevented by guard", 15000, startPointInTime, 2 * time.Minute},
-		{"disable charger reset, pv power is back", -1, startPointInTime, 1 * time.Minute},
-		{"guard elapsed, publish new guard time", -1, elapsed, time.Duration(0)},
-	}
+		for step, se := range tc.series {
+			clck.Set(start.Add(se.delay))
+			assert.Equal(t, se.current, lp.pvMaxCurrent(api.ModePV, se.site, false, false), step)
+		}
 
-	for _, tc := range tc {
-		t.Logf("%+v", tc)
-		lp.Update(tc.sitePower, false, false, false, 0, nil, nil)
-		assert.Equal(t, tc.guardUpdated, lp.guardUpdated)
-		expectCache(guardTimer+"Remaining", tc.guardTimerRemaining)
-		clock.Add(time.Minute)
+		ctrl.Finish()
 	}
 }
diff --git a/tests/sessions.spec.js b/tests/sessions.spec.js
index 28e7b9dc..49eeff7d 100644
--- a/tests/sessions.spec.js
+++ b/tests/sessions.spec.js
@@ -234,18 +234,10 @@ test.describe("columns desktop", async () => {
     await page.goto("/#/sessions?year=2023&month=5");
     await expect(page.getByTestId("vehicle")).toBeVisible();
   });
-  test("hide vehicle column only one exists", async ({ page }) => {
-    await page.goto("/#/sessions?year=2023&month=3");
-    await expect(page.getByTestId("vehicle")).toHaveCount(0);
-  });
   test("show loadpoint column if multiple different exist", async ({ page }) => {
     await page.goto("/#/sessions?year=2023&month=5");
     await expect(page.getByTestId("loadpoint")).toBeVisible();
   });
-  test("hide loadpoint column only one exists", async ({ page }) => {
-    await page.goto("/#/sessions?year=2023&month=3");
-    await expect(page.getByTestId("loadpoint")).toHaveCount(0);
-  });
   test("show co2 column it has values", async ({ page }) => {
     await page.goto("/#/sessions?year=2023&month=3");
     await expect(page.getByTestId("sessions-head-co2")).toBeVisible();
diff --git a/util/modbus/modbus_test.go b/util/modbus/modbus_test.go
index bf87c71e..70a18082 100644
--- a/util/modbus/modbus_test.go
+++ b/util/modbus/modbus_test.go
@@ -9,11 +9,10 @@ import (
 func TestParsePoint(t *testing.T) {
 	tc := []struct {
 		in  string
-		ops []SunSpecOperation
+		ops SunSpecOperation
 	}{
-		{"103:W", []SunSpecOperation{{103, 0, "W"}}},
-		{"802:1:V", []SunSpecOperation{{802, 1, "V"}}},
-		{"101|103:DCW", []SunSpecOperation{{101, 0, "DCW"}, {103, 0, "DCW"}}},
+		{"103:W", SunSpecOperation{103, 0, "W"}},
+		{"802:1:V", SunSpecOperation{802, 1, "V"}},
 	}
 
 	for _, tc := range tc {