Repository: evcc-io/evcc
Pull request #12052 (2024-02-04 11:30:40)

Add PhaseDescriber api
Fix https://github.com/evcc-io/evcc/issues/12035

Todo

- [x] Ui

Out of scope:

- [x] rename `phases1p3p` to `chargerPhases1p3p`- it really is a charger property @naltatis 


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/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..b601113e 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,
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/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/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..54ca2927 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -595,13 +595,19 @@ 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} {
 		lp.publishChargerFeature(f)
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/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/da.toml b/i18n/da.toml
index 323fe633..d79c6191 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})"
diff --git a/i18n/de.toml b/i18n/de.toml
index ccaef28d..c9f439bf 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 editieren"
+
+[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"
diff --git a/i18n/en.toml b/i18n/en.toml
index 8b5bc0e2..8abf3e5a 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"
diff --git a/i18n/es.toml b/i18n/es.toml
index 5993dfa6..2e24754f 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})"
diff --git a/i18n/fi.toml b/i18n/fi.toml
index 1747d414..6ce62616 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"
 
diff --git a/i18n/fr.toml b/i18n/fr.toml
index 7c7d6873..9b3c6103 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"
diff --git a/i18n/hr.toml b/i18n/hr.toml
index e1f04c72..54aef691 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"
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..d6cd6c7e 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})"
diff --git a/i18n/nl.toml b/i18n/nl.toml
index baecd794..50739292 100644
--- a/i18n/nl.toml
+++ b/i18n/nl.toml
@@ -195,6 +195,7 @@ solar = "Zon"
 [main.loadpointSettings]
 currents = "Laadstroom"
 default = "standaard"
+disclaimerHint = "Let op:"
 title = "Instellingen {0}"
 vehicle = "Voertuig"
 
diff --git a/i18n/no.toml b/i18n/no.toml
index 43296d46..60c92d4e 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"
 
diff --git a/i18n/pl.toml b/i18n/pl.toml
index 196c666a..8123c8c2 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"
diff --git a/i18n/pt.toml b/i18n/pt.toml
index 5c081fec..15f3d569 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"
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..c5883188 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"
@@ -281,7 +332,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"
@@ -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..493261c8 100644
--- a/i18n/uk.toml
+++ b/i18n/uk.toml
@@ -188,6 +188,7 @@ solar = "Сонячна"
 [main.loadpointSettings]
 currents = "Струм зарядки"
 default = "за замовчуванням"
+disclaimerHint = "Примітка:"
 title = "Налаштування {0}"
 vehicle = "Транспортний засіб"
 
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/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/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/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


Test patch:
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();