Repository: evcc-io/evcc
Pull request #14244 (2024-06-08 10:31:22)

Site: remove root circuit configuration
Fix https://github.com/evcc-io/evcc/issues/13802, fix https://github.com/evcc-io/evcc/issues/14180

Also:

- double-checks that loadpoints are not additionally assigned to the root circuit
- allows root circuits wirhout meter

/cc @premultiply 

andig: @premultiply könntest Du bitte nochmal bzgl. https://github.com/evcc-io/evcc/issues/14180#issuecomment-2151524837 kommentieren? Das wird hiermit erzwungen. Ich bin aber unser, ob/wie wir das wirklich wollen?
premultiply: Ja, sehe das als notwendig und richtig an.
Der site-Circuit ist immer die Wurzel und alle Zweige davon dürfen nicht wieder in Richtung Wurzel zurück.
andig: Natürlich darf man keine Schleifen aus Circuits bauen. Aber warum dürfen keine Ladepunkte explizit am Root Circuit hängen? Das ist nicht so offensichtlich.
andig: Diskussion in Doku PR verschoben.

Patch:
diff --git a/assets/js/components/ChargingSessionModal.vue b/assets/js/components/ChargingSessionModal.vue
index d5f8c2d4..a712cb76 100644
--- a/assets/js/components/ChargingSessionModal.vue
+++ b/assets/js/components/ChargingSessionModal.vue
@@ -37,8 +37,9 @@
 										<VehicleOptions
 											:id="session.vehicle"
 											class="options"
-											:vehicles="vehicles"
+											:vehicles="vehicleOptions"
 											connected
+											:selected="session.vehicle"
 											@change-vehicle="changeVehicle"
 											@remove-vehicle="removeVehicle"
 										>
@@ -202,6 +203,12 @@ export default {
 		solarEnergy: function () {
 			return this.chargedEnergy * (this.session.solarPercentage / 100);
 		},
+		vehicleOptions: function () {
+			return this.vehicles.map((v) => ({
+				name: v.title,
+				title: v.title,
+			}));
+		},
 	},
 	methods: {
 		openSessionDetailsModal() {
@@ -217,15 +224,11 @@ export default {
 		formatKm: function (value) {
 			return `${this.fmtNumber(distanceValue(value), 0)} ${distanceUnit()}`;
 		},
-		async changeVehicle(name) {
-			await this.updateSession({
-				vehicle: this.vehicles.find((v) => v.name === name)?.title,
-			});
+		async changeVehicle(title) {
+			await this.updateSession({ vehicle: title });
 		},
 		async removeVehicle() {
-			await this.updateSession({
-				vehicle: null,
-			});
+			await this.updateSession({ vehicle: null });
 		},
 		async updateSession(data) {
 			try {
diff --git a/assets/js/components/Config/SponsorModal.vue b/assets/js/components/Config/SponsorModal.vue
index e8a3f445..e2f32d87 100644
--- a/assets/js/components/Config/SponsorModal.vue
+++ b/assets/js/components/Config/SponsorModal.vue
@@ -32,7 +32,7 @@
 					id="sponsorToken"
 					:label="sponsorTokenLabel"
 					:help="
-						$t('config.sponsor.descriptionToken', { url: 'https://sponsor.evcc.io' })
+						$t('config.sponsor.descriptionToken', { url: 'https://sponsor.evcc.io/' })
 					"
 					docs-link="/docs/sponsorship"
 					class="mt-4"
diff --git a/assets/js/components/VehicleOptions.vue b/assets/js/components/VehicleOptions.vue
index 84a876e3..a456a227 100644
--- a/assets/js/components/VehicleOptions.vue
+++ b/assets/js/components/VehicleOptions.vue
@@ -1,44 +1,36 @@
 <template>
-	<div>
-		<div
-			:id="dropdownId"
-			role="button"
-			tabindex="0"
-			data-bs-toggle="dropdown"
-			aria-expanded="false"
-			data-testid="change-vehicle"
-		>
-			<slot />
-		</div>
-		<ul class="dropdown-menu dropdown-menu-start" :aria-labelledby="dropdownId">
-			<li>
-				<h6 class="dropdown-header">{{ $t("main.vehicle.changeVehicle") }}</h6>
-			</li>
-			<li v-for="vehicle in vehicles" :key="vehicle.name">
-				<button type="button" class="dropdown-item" @click="changeVehicle(vehicle.name)">
-					{{ vehicle.title }}
-				</button>
-			</li>
-			<li>
-				<button type="button" class="dropdown-item" @click="removeVehicle()">
-					<span v-if="connected">{{ $t("main.vehicle.unknown") }}</span>
-					<span v-else>{{ $t("main.vehicle.none") }}</span>
-				</button>
-			</li>
-		</ul>
-	</div>
+	<label
+		class="position-relative d-block"
+		:for="dropdownId"
+		role="button"
+		data-testid="change-vehicle"
+	>
+		<select :id="dropdownId" :value="selected" class="custom-select" @change="change">
+			<option
+				v-for="{ name, title } in vehicles"
+				:key="name"
+				:value="name"
+				:selected="name === selected"
+			>
+				{{ title }}
+			</option>
+			<hr />
+			<option value="" :selected="!selected">
+				{{ $t(`main.vehicle.${connected ? "unknown" : "none"}`) }}
+			</option>
+		</select>
+		<slot></slot>
+	</label>
 </template>
 
 <script>
-import "@h2d2/shopicons/es/filled/options";
-import Dropdown from "bootstrap/js/dist/dropdown";
-
 export default {
 	name: "VehicleOptions",
 	props: {
 		connected: Boolean,
 		id: [String, Number],
 		vehicles: Array,
+		selected: String,
 	},
 	emits: ["change-vehicle", "remove-vehicle"],
 	computed: {
@@ -46,21 +38,27 @@ export default {
 			return `vehicleOptionsDropdown${this.id}`;
 		},
 	},
-	mounted() {
-		this.dropdown = new Dropdown(document.getElementById(this.dropdownId));
-	},
-	unmounted() {
-		this.dropdown?.dispose();
-	},
 	methods: {
-		changeVehicle(name) {
-			this.$emit("change-vehicle", name);
-		},
-		removeVehicle() {
-			this.$emit("remove-vehicle");
+		change(event) {
+			const name = event.target.value;
+			if (name) {
+				this.$emit("change-vehicle", name);
+			} else {
+				this.$emit("remove-vehicle");
+			}
 		},
 	},
 };
 </script>
-
-<style></style>
+<style scoped>
+.custom-select {
+	left: 0;
+	top: 0;
+	bottom: 0;
+	width: 100%;
+	cursor: pointer;
+	position: absolute;
+	opacity: 0;
+	-webkit-appearance: menulist-button;
+}
+</style>
diff --git a/assets/js/components/VehicleTitle.vue b/assets/js/components/VehicleTitle.vue
index e31a0990..62acf99c 100644
--- a/assets/js/components/VehicleTitle.vue
+++ b/assets/js/components/VehicleTitle.vue
@@ -24,7 +24,7 @@
 				v-bind="vehicleOptionsProps"
 				:id="id"
 				class="options"
-				:vehicles="otherVehicles"
+				:selected="vehicleName"
 				@change-vehicle="changeVehicle"
 				@remove-vehicle="removeVehicle"
 			>
@@ -104,9 +104,6 @@ export default {
 		vehicleKnown() {
 			return !!this.vehicleName;
 		},
-		otherVehicles() {
-			return this.vehicles.filter((v) => v.name !== this.vehicleName);
-		},
 		showOptions() {
 			return this.vehicleKnown || this.vehicles.length;
 		},
diff --git a/charger/easee.go b/charger/easee.go
index 0b0a9ad4..db9661cb 100644
--- a/charger/easee.go
+++ b/charger/easee.go
@@ -172,6 +172,9 @@ func NewEasee(user, password, charger string, timeout time.Duration, authorize b
 
 	client, err := signalr.NewClient(context.Background(),
 		signalr.WithConnector(c.connect(ts)),
+		signalr.WithBackoff(func() backoff.BackOff {
+			return backoff.NewExponentialBackOff(backoff.WithMaxElapsedTime(0)) // prevents SignalR stack to silently give up after 15 mins
+		}),
 		signalr.WithReceiver(c),
 		signalr.Logger(easee.SignalrLogger(c.log.TRACE), false),
 	)
diff --git a/charger/eebus/eebus.go b/charger/eebus/eebus.go
index c0170b6f..5e9d601c 100644
--- a/charger/eebus/eebus.go
+++ b/charger/eebus/eebus.go
@@ -34,10 +34,15 @@ type Config struct {
 	ShipID      string
 	Interfaces  []string
 	Certificate struct {
-		Public, Private []byte
+		Public, Private string
 	}
 }
 
+// Configured returns true if the EEbus server is configured
+func (c Config) Configured() bool {
+	return len(c.Certificate.Public) > 0 && len(c.Certificate.Private) > 0
+}
+
 type EEBusClientCBs struct {
 	onConnect    func(string) // , ship.Conn) error
 	onDisconnect func(string)
@@ -77,7 +82,7 @@ func NewServer(other Config) (*EEBus, error) {
 		serial = cc.ShipID
 	}
 
-	certificate, err := tls.X509KeyPair(cc.Certificate.Public, cc.Certificate.Private)
+	certificate, err := tls.X509KeyPair([]byte(cc.Certificate.Public), []byte(cc.Certificate.Private))
 	if err != nil {
 		return nil, err
 	}
diff --git a/charger/em2go-home.go b/charger/em2go-home.go
index d0a185c6..aeceec6d 100644
--- a/charger/em2go-home.go
+++ b/charger/em2go-home.go
@@ -165,6 +165,18 @@ func (wb *Em2GoHome) MaxCurrentMillis(current float64) error {
 	return err
 }
 
+var _ api.CurrentGetter = (*Em2GoHome)(nil)
+
+// GetMaxCurrent implements the api.CurrentGetter interface
+func (wb Em2GoHome) GetMaxCurrent() (float64, error) {
+	b, err := wb.conn.ReadHoldingRegisters(em2GoHomeRegCurrentLimit, 1)
+	if err != nil {
+		return 0, err
+	}
+
+	return float64(binary.BigEndian.Uint16(b)) / 10, err
+}
+
 var _ api.Meter = (*Em2GoHome)(nil)
 
 // CurrentPower implements the api.Meter interface
diff --git a/charger/etrel.go b/charger/etrel.go
index 0e5e807b..cc230bbe 100644
--- a/charger/etrel.go
+++ b/charger/etrel.go
@@ -190,6 +190,18 @@ func (wb *Etrel) MaxCurrentMillis(current float64) error {
 	return err
 }
 
+var _ api.CurrentGetter = (*Etrel)(nil)
+
+// GetMaxCurrent implements the api.CurrentGetter interface
+func (wb *Etrel) GetMaxCurrent() (float64, error) {
+	b, err := wb.conn.ReadHoldingRegisters(wb.base+etrelRegMaxCurrent, 2)
+	if err != nil {
+		return 0, err
+	}
+
+	return float64(math.Float32frombits(binary.BigEndian.Uint32(b))), nil
+}
+
 var _ api.Meter = (*Etrel)(nil)
 
 // CurrentPower implements the api.Meter interface
diff --git a/charger/huawei-ac.go b/charger/huawei-ac.go
deleted file mode 100644
index def40557..00000000
--- a/charger/huawei-ac.go
+++ /dev/null
@@ -1,200 +0,0 @@
-package charger
-
-// LICENSE
-
-// Copyright (c) 2023 premultiply
-
-// This module is NOT covered by the MIT license. All rights reserved.
-
-// The above copyright notice and this permission notice shall be included in all
-// copies or substantial portions of the Software.
-
-// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-// SOFTWARE.
-
-import (
-	"encoding/binary"
-	"fmt"
-	"math"
-
-	"github.com/evcc-io/evcc/api"
-	"github.com/evcc-io/evcc/core/loadpoint"
-	"github.com/evcc-io/evcc/util"
-	"github.com/evcc-io/evcc/util/modbus"
-	"github.com/evcc-io/evcc/util/sponsor"
-)
-
-type HuaweiACCharger struct {
-	conn *modbus.Connection
-	curr float64
-	lp   loadpoint.API
-}
-
-const (
-	huaweiRegVoltages = 0x1000 // uint32 * 3
-	huaweiRegCurrents = 0x1006 // uint32 * 3
-	huaweiRegPower    = 0x100c // uint32
-	huaweiRegStatus   = 0x100e // uint32
-	huaweiRegMaxPower = 0x2000 // uint32
-	huaweiRegControl  = 0x2006 // uint16
-)
-
-func init() {
-	registry.Add("huawei-ac", NewHuaweiACChargerFromConfig)
-}
-
-// NewHuaweiACChargerFromConfig creates a HuaweiACCharger charger from generic config
-func NewHuaweiACChargerFromConfig(other map[string]interface{}) (api.Charger, error) {
-	cc := modbus.TcpSettings{
-		ID: 255,
-	}
-
-	if err := util.DecodeOther(other, &cc); err != nil {
-		return nil, err
-	}
-
-	return NewHuaweiACCharger(cc.URI, cc.ID)
-}
-
-// NewHuaweiACCharger creates a HuaweiACCharger charger
-func NewHuaweiACCharger(uri string, slaveID uint8) (api.Charger, error) {
-	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID)
-	if err != nil {
-		return nil, err
-	}
-
-	if !sponsor.IsAuthorized() {
-		return nil, api.ErrSponsorRequired
-	}
-
-	log := util.NewLogger("huawei-ac")
-	conn.Logger(log.TRACE)
-
-	wb := &HuaweiACCharger{
-		conn: conn,
-		curr: 6,
-	}
-
-	return wb, err
-}
-
-func (wb *HuaweiACCharger) setCurrent(current float64) error {
-	phases := 3
-	if wb.lp != nil {
-		phases = wb.lp.ActivePhases()
-	}
-
-	b := make([]byte, 4)
-	binary.BigEndian.PutUint32(b, uint32(math.Trunc(230.0*current*float64(phases)*10.0)))
-
-	_, err := wb.conn.WriteMultipleRegisters(huaweiRegMaxPower, 2, b)
-
-	return err
-}
-
-// Status implements the api.Charger interface
-func (wb *HuaweiACCharger) Status() (api.ChargeStatus, error) {
-	b, err := wb.conn.ReadHoldingRegisters(huaweiRegControl, 1)
-	if err != nil {
-		return api.StatusNone, err
-	}
-
-	// ToDo: Real status
-	switch u := binary.BigEndian.Uint16(b); u {
-	case 0:
-		return api.StatusA, nil
-	case 1:
-		return api.StatusB, nil
-	case 2:
-		return api.StatusC, nil
-	default:
-		return api.StatusNone, fmt.Errorf("invalid status: %d", u)
-	}
-}
-
-// Enabled implements the api.Charger interface
-func (wb *HuaweiACCharger) Enabled() (bool, error) {
-	b, err := wb.conn.ReadHoldingRegisters(huaweiRegMaxPower, 2)
-	if err != nil {
-		return false, err
-	}
-
-	return binary.BigEndian.Uint32(b) > 0, nil
-}
-
-// Enable implements the api.Charger interface
-func (wb *HuaweiACCharger) Enable(enable bool) error {
-	var curr float64
-	if enable {
-		curr = wb.curr
-	}
-
-	return wb.setCurrent(curr)
-}
-
-// MaxCurrent implements the api.Charger interface
-func (wb *HuaweiACCharger) MaxCurrent(current int64) error {
-	return wb.MaxCurrentMillis(float64(current))
-}
-
-var _ api.ChargerEx = (*HuaweiACCharger)(nil)
-
-// MaxCurrent implements the api.ChargerEx interface
-func (wb *HuaweiACCharger) MaxCurrentMillis(current float64) error {
-	if current < 6 {
-		return fmt.Errorf("invalid current %.1f", current)
-	}
-
-	err := wb.setCurrent(current)
-	if err == nil {
-		wb.curr = current
-	}
-
-	return err
-}
-
-var _ api.Meter = (*HuaweiACCharger)(nil)
-
-// currentPower implements the api.Meter interface
-func (wb *HuaweiACCharger) CurrentPower() (float64, error) {
-	b, err := wb.conn.ReadHoldingRegisters(huaweiRegPower, 2)
-	if err != nil {
-		return 0, err
-	}
-
-	return float64(binary.BigEndian.Uint32(b)), nil
-}
-
-var _ api.PhaseCurrents = (*HuaweiACCharger)(nil)
-
-// currents implements the api.PhaseCurrents interface
-func (wb *HuaweiACCharger) Currents() (float64, float64, float64, error) {
-	return wb.getPhaseValues(huaweiRegCurrents)
-}
-
-var _ api.PhaseVoltages = (*HuaweiACCharger)(nil)
-
-// voltages implements the api.PhaseVoltages interface
-func (wb *HuaweiACCharger) Voltages() (float64, float64, float64, error) {
-	return wb.getPhaseValues(huaweiRegVoltages)
-}
-
-// getPhaseValues returns 3 sequential phase values
-func (wb *HuaweiACCharger) getPhaseValues(reg uint16) (float64, float64, float64, error) {
-	b, err := wb.conn.ReadHoldingRegisters(reg, 6)
-	if err != nil {
-		return 0, 0, 0, err
-	}
-
-	var res [3]float64
-	for i := range res {
-		res[i] = float64(binary.BigEndian.Uint32(b[4*i:])) / 10
-	}
-
-	return res[0], res[1], res[2], nil
-}
diff --git a/charger/kse.go b/charger/kse.go
index 0541365e..a939c9bf 100644
--- a/charger/kse.go
+++ b/charger/kse.go
@@ -2,7 +2,7 @@ package charger
 
 // LICENSE
 
-// Copyright (c) 2022 premultiply
+// Copyright (c) 2022-2024 premultiply
 
 // This module is NOT covered by the MIT license. All rights reserved.
 
@@ -29,24 +29,26 @@ import (
 
 // KSE charger implementation
 type KSE struct {
-	log  *util.Logger
-	conn *modbus.Connection
-	curr uint16
-	rfid bool
+	log     *util.Logger
+	conn    *modbus.Connection
+	curr    uint16
+	hasRfid bool
+	has1p3p bool
 }
 
 const (
 	kseRegSetMaxCurrent       = 0x03 // Externe Stromvorgabe via Bussystem / Ladefreigabe
 	kseRegChargeMode          = 0x0E // Lademodus
 	kseRegVehicleState        = 0x10 // State der Statemachine
+	kseRegVoltages            = 0x11 // Phasenspannung (3)
 	kseRegCurrents            = 0x14 // Phasenstrom (3)
 	kseRegCurrentLoadedEnergy = 0x17 // Zwischen anstecken und abstecken geladene Energie (10 Wh)
 	kseRegActualPower         = 0x18 // Aktuelle Ladeleistung (W)
 	kseRegFirmwareVersion     = 0x30 // Firmware Version
 	kseRegRFIDinstalled       = 0x31 // RFID-Leser vorhanden
+	kseRegRelayMode           = 0x35 // Umschalten 1 phasiges oder 3 phasiges Laden
+	kseRegEnergy              = 0x60
 	kseRegNFCTransactionID    = 0x67 // Tag ID (8 Bytes)
-	// kseRegRelayMode           = 0x35 // Umschalten 1 phasiges oder 3 phasiges Laden
-
 )
 
 func init() {
@@ -68,7 +70,7 @@ func NewKSEFromConfig(other map[string]interface{}) (api.Charger, error) {
 	return NewKSE(cc.URI, cc.Device, cc.Comset, cc.Baudrate, cc.ID)
 }
 
-//go:generate go run ../cmd/tools/decorate.go -f decorateKSE -b *KSE -r api.Charger -t "api.Identifier,Identify,func() (string, error)"
+//go:generate go run ../cmd/tools/decorate.go -f decorateKSE -b *KSE -r api.Charger -t "api.PhaseSwitcher,Phases1p3p,func(int) error" -t "api.PhaseGetter,GetPhases,func() (int, error)" -t "api.Identifier,Identify,func() (string, error)"
 
 // NewKSE creates KSE charger
 func NewKSE(uri, device, comset string, baudrate int, slaveID uint8) (api.Charger, error) {
@@ -90,14 +92,26 @@ func NewKSE(uri, device, comset string, baudrate int, slaveID uint8) (api.Charge
 		curr: 6, // assume min current
 	}
 
+	var (
+		phases1p3p func(int) error
+		getPhases  func() (int, error)
+		identify   func() (string, error)
+	)
+
+	// check presence of 1p3p switching
+	if b, err := wb.conn.ReadInputRegisters(kseRegFirmwareVersion, 1); err == nil && b[0] >= 0x52 { // >= HW Rev „R“
+		wb.has1p3p = true
+		phases1p3p = wb.phases1p3p
+		getPhases = wb.getPhases
+	}
+
 	// check presence of rfid
-	b, err := wb.conn.ReadDiscreteInputs(kseRegRFIDinstalled, 1)
-	if err == nil && b[0] != 0 {
-		wb.rfid = true
-		return decorateKSE(wb, wb.identify), err
+	if b, err := wb.conn.ReadDiscreteInputs(kseRegRFIDinstalled, 1); err == nil && b[0] != 0 {
+		wb.hasRfid = true
+		identify = wb.identify
 	}
 
-	return wb, err
+	return decorateKSE(wb, phases1p3p, getPhases, identify), err
 }
 
 // Status implements the api.Charger interface
@@ -185,6 +199,35 @@ func (wb *KSE) ChargedEnergy() (float64, error) {
 	return float64(binary.BigEndian.Uint16(b)) / 100, err
 }
 
+var _ api.MeterEnergy = (*KSE)(nil)
+
+// TotalEnergy implements the api.MeterEnergy interface
+func (wb *KSE) TotalEnergy() (float64, error) {
+	b, err := wb.conn.ReadInputRegisters(kseRegEnergy, 2)
+	if err != nil {
+		return 0, err
+	}
+
+	return float64(binary.BigEndian.Uint32(b)) / 1e3, nil
+}
+
+var _ api.PhaseVoltages = (*KSE)(nil)
+
+// Voltages implements the api.PhaseVoltages interface
+func (wb *KSE) Voltages() (float64, float64, float64, error) {
+	b, err := wb.conn.ReadInputRegisters(kseRegVoltages, 3)
+	if err != nil {
+		return 0, 0, 0, err
+	}
+
+	var res [3]float64
+	for i := range res {
+		res[i] = float64(binary.BigEndian.Uint16(b[2*i:]))
+	}
+
+	return res[0], res[1], res[2], nil
+}
+
 var _ api.PhaseCurrents = (*KSE)(nil)
 
 // Currents implements the api.PhaseCurrents interface
@@ -202,18 +245,30 @@ func (wb *KSE) Currents() (float64, float64, float64, error) {
 	return res[0], res[1], res[2], nil
 }
 
-// var _ api.PhaseSwitcher = (*KSE)(nil)
+// phases1p3p implements the api.PhaseSwitcher interface
+func (wb *KSE) phases1p3p(phases int) error {
+	var b uint16 = 0 // 3p
+	if phases == 1 {
+		b = 1 // 1p
+	}
 
-// // Phases1p3p implements the api.PhaseSwitcher interface
-// func (wb *KSE) Phases1p3p(phases int) error {
-// 	var b uint16 = 0
-// 	if phases == 1 {
-// 		b = 1 // 1p
-// 	}
+	_, err := wb.conn.WriteSingleRegister(kseRegRelayMode, b)
+	return err
+}
 
-// 	_, err := wb.conn.WriteSingleRegister(kseRegRelayMode, b)
-// 	return err
-// }
+// getPhases implements the api.PhaseGetter interface
+func (wb *KSE) getPhases() (int, error) {
+	b, err := wb.conn.ReadHoldingRegisters(kseRegRelayMode, 1)
+	if err != nil {
+		return 0, err
+	}
+
+	if binary.BigEndian.Uint16(b) == 0 {
+		return 3, nil
+	}
+
+	return 1, nil
+}
 
 // Identify implements the api.Identifier interface
 func (wb *KSE) identify() (string, error) {
@@ -230,7 +285,7 @@ var _ api.Diagnosis = (*KSE)(nil)
 // Diagnose implements the api.Diagnosis interface
 func (wb *KSE) Diagnose() {
 	if b, err := wb.conn.ReadInputRegisters(kseRegFirmwareVersion, 1); err == nil {
-		fmt.Printf("\tFirmware:\t%d.%d\n", b[0], b[1])
+		fmt.Printf("\tFirmware:\t%x\n", b)
 	}
 	if b, err := wb.conn.ReadInputRegisters(kseRegChargeMode, 1); err == nil {
 		fmt.Printf("\tCharge Mode:\t%d\n", binary.BigEndian.Uint16(b))
@@ -238,7 +293,12 @@ func (wb *KSE) Diagnose() {
 	if b, err := wb.conn.ReadInputRegisters(kseRegVehicleState, 1); err == nil {
 		fmt.Printf("\tState:\t%d\n", binary.BigEndian.Uint16(b))
 	}
-	if wb.rfid {
+	if wb.has1p3p {
+		if b, err := wb.conn.ReadHoldingRegisters(kseRegRelayMode, 1); err == nil {
+			fmt.Printf("\tPhases:\t%d\n", binary.BigEndian.Uint16(b))
+		}
+	}
+	if wb.hasRfid {
 		if b, err := wb.conn.ReadHoldingRegisters(kseRegNFCTransactionID, 4); err == nil {
 			fmt.Printf("\tNFC ID:\t%s\n", b)
 		}
diff --git a/charger/kse_decorators.go b/charger/kse_decorators.go
index c88e7b50..f6b46be7 100644
--- a/charger/kse_decorators.go
+++ b/charger/kse_decorators.go
@@ -6,20 +6,106 @@ import (
 	"github.com/evcc-io/evcc/api"
 )
 
-func decorateKSE(base *KSE, identifier func() (string, error)) api.Charger {
+func decorateKSE(base *KSE, phaseSwitcher func(int) error, phaseGetter func() (int, error), identifier func() (string, error)) api.Charger {
 	switch {
-	case identifier == nil:
+	case identifier == nil && phaseGetter == nil && phaseSwitcher == nil:
 		return base
 
-	case identifier != nil:
+	case identifier == nil && phaseGetter == nil && phaseSwitcher != nil:
+		return &struct {
+			*KSE
+			api.PhaseSwitcher
+		}{
+			KSE: base,
+			PhaseSwitcher: &decorateKSEPhaseSwitcherImpl{
+				phaseSwitcher: phaseSwitcher,
+			},
+		}
+
+	case identifier == nil && phaseGetter != nil && phaseSwitcher == nil:
+		return &struct {
+			*KSE
+			api.PhaseGetter
+		}{
+			KSE: base,
+			PhaseGetter: &decorateKSEPhaseGetterImpl{
+				phaseGetter: phaseGetter,
+			},
+		}
+
+	case identifier == nil && phaseGetter != nil && phaseSwitcher != nil:
+		return &struct {
+			*KSE
+			api.PhaseGetter
+			api.PhaseSwitcher
+		}{
+			KSE: base,
+			PhaseGetter: &decorateKSEPhaseGetterImpl{
+				phaseGetter: phaseGetter,
+			},
+			PhaseSwitcher: &decorateKSEPhaseSwitcherImpl{
+				phaseSwitcher: phaseSwitcher,
+			},
+		}
+
+	case identifier != nil && phaseGetter == nil && phaseSwitcher == nil:
+		return &struct {
+			*KSE
+			api.Identifier
+		}{
+			KSE: base,
+			Identifier: &decorateKSEIdentifierImpl{
+				identifier: identifier,
+			},
+		}
+
+	case identifier != nil && phaseGetter == nil && phaseSwitcher != nil:
 		return &struct {
 			*KSE
 			api.Identifier
+			api.PhaseSwitcher
 		}{
 			KSE: base,
 			Identifier: &decorateKSEIdentifierImpl{
 				identifier: identifier,
 			},
+			PhaseSwitcher: &decorateKSEPhaseSwitcherImpl{
+				phaseSwitcher: phaseSwitcher,
+			},
+		}
+
+	case identifier != nil && phaseGetter != nil && phaseSwitcher == nil:
+		return &struct {
+			*KSE
+			api.Identifier
+			api.PhaseGetter
+		}{
+			KSE: base,
+			Identifier: &decorateKSEIdentifierImpl{
+				identifier: identifier,
+			},
+			PhaseGetter: &decorateKSEPhaseGetterImpl{
+				phaseGetter: phaseGetter,
+			},
+		}
+
+	case identifier != nil && phaseGetter != nil && phaseSwitcher != nil:
+		return &struct {
+			*KSE
+			api.Identifier
+			api.PhaseGetter
+			api.PhaseSwitcher
+		}{
+			KSE: base,
+			Identifier: &decorateKSEIdentifierImpl{
+				identifier: identifier,
+			},
+			PhaseGetter: &decorateKSEPhaseGetterImpl{
+				phaseGetter: phaseGetter,
+			},
+			PhaseSwitcher: &decorateKSEPhaseSwitcherImpl{
+				phaseSwitcher: phaseSwitcher,
+			},
 		}
 	}
 
@@ -33,3 +119,19 @@ type decorateKSEIdentifierImpl struct {
 func (impl *decorateKSEIdentifierImpl) Identify() (string, error) {
 	return impl.identifier()
 }
+
+type decorateKSEPhaseGetterImpl struct {
+	phaseGetter func() (int, error)
+}
+
+func (impl *decorateKSEPhaseGetterImpl) GetPhases() (int, error) {
+	return impl.phaseGetter()
+}
+
+type decorateKSEPhaseSwitcherImpl struct {
+	phaseSwitcher func(int) error
+}
+
+func (impl *decorateKSEPhaseSwitcherImpl) Phases1p3p(p0 int) error {
+	return impl.phaseSwitcher(p0)
+}
diff --git a/cmd/error.go b/cmd/error.go
index d2e17574..7744080c 100644
--- a/cmd/error.go
+++ b/cmd/error.go
@@ -28,6 +28,27 @@ const (
 	ClassSponsorship
 )
 
+// FatalError is an error that can be marshaled
+type FatalError struct {
+	err error
+}
+
+func (e *FatalError) Error() string {
+	return e.err.Error()
+}
+
+func (e FatalError) MarshalJSON() ([]byte, error) {
+	if je, ok := e.err.(json.Marshaler); ok {
+		return je.MarshalJSON()
+	}
+
+	return json.Marshal(struct {
+		Error string `json:"error"`
+	}{
+		Error: e.err.Error(),
+	})
+}
+
 // DeviceError indicates the specific device that failed
 type DeviceError struct {
 	Name string
diff --git a/cmd/helper.go b/cmd/helper.go
index a42a46c3..183f0051 100644
--- a/cmd/helper.go
+++ b/cmd/helper.go
@@ -75,23 +75,25 @@ func shutdownDoneC() <-chan struct{} {
 	return doneC
 }
 
-func wrapError(err error) error {
-	if err != nil {
-		var opErr *net.OpError
-		var pathErr *os.PathError
+func wrapFatalError(err error) error {
+	if err == nil {
+		return nil
+	}
+
+	var opErr *net.OpError
+	var pathErr *os.PathError
 
-		switch {
-		case errors.As(err, &opErr):
-			if opErr.Op == "listen" && strings.Contains(opErr.Error(), "address already in use") {
-				err = fmt.Errorf("could not open port- check that evcc is not already running (%w)", err)
-			}
+	switch {
+	case errors.As(err, &opErr):
+		if opErr.Op == "listen" && strings.Contains(opErr.Error(), "address already in use") {
+			err = fmt.Errorf("could not open port- check that evcc is not already running (%w)", err)
+		}
 
-		case errors.As(err, &pathErr):
-			if pathErr.Op == "remove" && strings.Contains(pathErr.Error(), "operation not permitted") {
-				err = fmt.Errorf("could not remove file- check that evcc is not already running (%w)", err)
-			}
+	case errors.As(err, &pathErr):
+		if pathErr.Op == "remove" && strings.Contains(pathErr.Error(), "operation not permitted") {
+			err = fmt.Errorf("could not remove file- check that evcc is not already running (%w)", err)
 		}
 	}
 
-	return err
+	return &FatalError{err}
 }
diff --git a/cmd/root.go b/cmd/root.go
index a7c17e83..ecdd9aa1 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -291,7 +291,7 @@ func runRoot(cmd *cobra.Command, args []string) {
 
 	if err != nil {
 		// improve error message
-		err = wrapError(err)
+		err = wrapFatalError(err)
 		valueChan <- util.Param{Key: keys.Fatal, Val: err}
 
 		// TODO stop reboot loop if user updates config (or show countdown in UI)
@@ -307,5 +307,5 @@ func runRoot(cmd *cobra.Command, args []string) {
 	// uds health check listener
 	go server.HealthListener(site)
 
-	log.FATAL.Println(wrapError(httpd.ListenAndServe()))
+	log.FATAL.Println(wrapFatalError(httpd.ListenAndServe()))
 }
diff --git a/cmd/setup.go b/cmd/setup.go
index 71e659fa..ebc8fa0b 100644
--- a/cmd/setup.go
+++ b/cmd/setup.go
@@ -22,7 +22,6 @@ import (
 	"github.com/evcc-io/evcc/cmd/shutdown"
 	"github.com/evcc-io/evcc/core"
 	"github.com/evcc-io/evcc/core/keys"
-	"github.com/evcc-io/evcc/core/site"
 	"github.com/evcc-io/evcc/hems"
 	"github.com/evcc-io/evcc/meter"
 	"github.com/evcc-io/evcc/provider/golang"
@@ -63,6 +62,9 @@ var conf = globalconfig.All{
 	Mqtt: globalconfig.Mqtt{
 		Topic: "evcc",
 	},
+	EEBus: eebus.Config{
+		URI: ":4712",
+	},
 	Database: globalconfig.DB{
 		Type: "sqlite",
 		Dsn:  "~/.evcc/evcc.db",
@@ -724,7 +726,7 @@ func configureEEBus(conf eebus.Config) error {
 		}
 	}
 
-	if conf.URI == "" {
+	if !conf.Configured() {
 		return nil
 	}
 
@@ -911,7 +913,7 @@ func configureSiteAndLoadpoints(conf *globalconfig.All) (*core.Site, error) {
 	}
 
 	if len(config.Circuits().Devices()) > 0 {
-		if err := validateCircuits(site, loadpoints); err != nil {
+		if err := validateCircuits(loadpoints); err != nil {
 			return nil, &ClassError{ClassCircuit, err}
 		}
 	}
@@ -919,26 +921,39 @@ func configureSiteAndLoadpoints(conf *globalconfig.All) (*core.Site, error) {
 	return site, nil
 }
 
-func validateCircuits(site site.API, loadpoints []*core.Loadpoint) error {
+func validateCircuits(loadpoints []*core.Loadpoint) error {
+	var hasRoot bool
+
 CONTINUE:
 	for _, dev := range config.Circuits().Devices() {
 		instance := dev.Instance()
 
-		if instance.HasMeter() || site.GetCircuit() == instance {
-			continue
+		isRoot := instance.GetParent() == nil
+		if isRoot {
+			if hasRoot {
+				return errors.New("multiple root circuits")
+			}
+
+			hasRoot = true
 		}
 
 		for _, lp := range loadpoints {
 			if lp.GetCircuit() == instance {
+				if isRoot {
+					return fmt.Errorf("root circuit must not be assigned to loadpoint %s", lp.Title())
+				}
+
 				continue CONTINUE
 			}
 		}
 
-		return fmt.Errorf("circuit %s has no meter or loadpoint assigned", dev.Config().Name)
+		if !isRoot && !instance.HasMeter() {
+			return fmt.Errorf("circuit %s has no meter and no loadpoint assigned", dev.Config().Name)
+		}
 	}
 
-	if site.GetCircuit() == nil {
-		return errors.New("site has no circuit")
+	if !hasRoot {
+		return errors.New("missing root circuit")
 	}
 
 	return nil
@@ -947,18 +962,13 @@ CONTINUE:
 func configureSite(conf map[string]interface{}, loadpoints []*core.Loadpoint, tariffs *tariff.Tariffs) (*core.Site, error) {
 	site, err := core.NewSiteFromConfig(conf)
 	if err != nil {
-		// TODO proper handling
-		panic(err)
+		return nil, err
 	}
 
 	if err := site.Boot(log, loadpoints, tariffs); err != nil {
 		return nil, fmt.Errorf("failed configuring site: %w", err)
 	}
 
-	if len(config.Circuits().Devices()) > 0 && site.GetCircuit() == nil {
-		return nil, errors.New("site has no circuit")
-	}
-
 	return site, nil
 }
 
diff --git a/core/circuit.go b/core/circuit.go
index 8abda96e..5a2cd2d8 100644
--- a/core/circuit.go
+++ b/core/circuit.go
@@ -2,7 +2,9 @@ package core
 
 import (
 	"fmt"
+	"math"
 	"sync"
+	"time"
 
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util"
@@ -20,22 +22,29 @@ type Circuit struct {
 	parent   api.Circuit   // parent circuit
 	children []api.Circuit // child circuits
 	meter    api.Meter     // meter to determine current power
+	timeout  time.Duration
 
 	maxCurrent float64 // max allowed current
 	maxPower   float64 // max allowed power
 
 	current float64
 	power   float64
+
+	currentUpdated time.Time
+	powerUpdated   time.Time
 }
 
 // NewCircuitFromConfig creates a new Circuit
 func NewCircuitFromConfig(log *util.Logger, other map[string]interface{}) (api.Circuit, error) {
-	var cc struct {
-		Title      string  `mapstructure:"title"`      // title
-		ParentRef  string  `mapstructure:"parent"`     // parent circuit reference
-		MeterRef   string  `mapstructure:"meter"`      // meter reference
-		MaxCurrent float64 `mapstructure:"maxCurrent"` // the max allowed current
-		MaxPower   float64 `mapstructure:"maxPower"`   // the max allowed power
+	cc := struct {
+		Title      string        `mapstructure:"title"`      // title
+		ParentRef  string        `mapstructure:"parent"`     // parent circuit reference
+		MeterRef   string        `mapstructure:"meter"`      // meter reference
+		MaxCurrent float64       `mapstructure:"maxCurrent"` // the max allowed current
+		MaxPower   float64       `mapstructure:"maxPower"`   // the max allowed power
+		Timeout    time.Duration `mapstructure:"timeout"`    // timeout between meter updates
+	}{
+		Timeout: time.Minute,
 	}
 
 	if err := util.DecodeOther(other, &cc); err != nil {
@@ -51,7 +60,7 @@ func NewCircuitFromConfig(log *util.Logger, other map[string]interface{}) (api.C
 		meter = dev.Instance()
 	}
 
-	circuit, err := NewCircuit(log, cc.Title, cc.MaxCurrent, cc.MaxPower, meter)
+	circuit, err := NewCircuit(log, cc.Title, cc.MaxCurrent, cc.MaxPower, meter, cc.Timeout)
 	if err != nil {
 		return nil, err
 	}
@@ -68,13 +77,14 @@ func NewCircuitFromConfig(log *util.Logger, other map[string]interface{}) (api.C
 }
 
 // NewCircuit creates a circuit
-func NewCircuit(log *util.Logger, title string, maxCurrent, maxPower float64, meter api.Meter) (*Circuit, error) {
+func NewCircuit(log *util.Logger, title string, maxCurrent, maxPower float64, meter api.Meter, timeout time.Duration) (*Circuit, error) {
 	c := &Circuit{
 		log:        log,
 		title:      title,
 		maxCurrent: maxCurrent,
 		maxPower:   maxPower,
 		meter:      meter,
+		timeout:    timeout,
 	}
 
 	if maxPower == 0 {
@@ -173,17 +183,27 @@ func (c *Circuit) updateLoadpoints(loadpoints []api.CircuitLoad) {
 	}
 }
 
+func (c *Circuit) overloadOnError(t time.Time, val *float64) {
+	if c.timeout > 0 && time.Since(t) > c.timeout {
+		*val = math.MaxFloat64
+	}
+}
+
 func (c *Circuit) updateMeters() error {
 	if f, err := c.meter.CurrentPower(); err == nil {
 		c.power = f
+		c.powerUpdated = time.Now()
 	} else {
+		c.overloadOnError(c.powerUpdated, &c.power)
 		return fmt.Errorf("circuit power: %w", err)
 	}
 
 	if phaseMeter, ok := c.meter.(api.PhaseCurrents); ok {
 		if l1, l2, l3, err := phaseMeter.Currents(); err == nil {
 			c.current = max(l1, l2, l3)
+			c.currentUpdated = time.Now()
 		} else {
+			c.overloadOnError(c.currentUpdated, &c.current)
 			return fmt.Errorf("circuit currents: %w", err)
 		}
 	}
@@ -194,15 +214,15 @@ func (c *Circuit) updateMeters() error {
 func (c *Circuit) Update(loadpoints []api.CircuitLoad) (err error) {
 	defer func() {
 		if c.maxPower != 0 && c.power > c.maxPower {
-			c.log.WARN.Printf("over power detected: %gW > %gW", c.power, c.maxPower)
+			c.log.WARN.Printf("over power detected: %.3gW > %.3gW", c.power, c.maxPower)
 		} else {
-			c.log.DEBUG.Printf("power: %gW", c.power)
+			c.log.DEBUG.Printf("power: %.3gW", c.power)
 		}
 
 		if c.maxCurrent != 0 && c.current > c.maxCurrent {
-			c.log.WARN.Printf("over current detected: %gA > %gA", c.current, c.maxCurrent)
+			c.log.WARN.Printf("over current detected: %.3gA > %.3gA", c.current, c.maxCurrent)
 		} else {
-			c.log.DEBUG.Printf("current: %gA", c.current)
+			c.log.DEBUG.Printf("current: %.3gA", c.current)
 		}
 	}()
 
@@ -246,9 +266,9 @@ func (c *Circuit) ValidatePower(old, new float64) float64 {
 		potential := c.maxPower - c.power
 		if delta > potential {
 			new = max(0, old+potential)
-			c.log.DEBUG.Printf("validate power: %gW -> %gW <= %gW at %gW: capped at %gW", old, new, c.maxPower, c.power, new)
+			c.log.DEBUG.Printf("validate power: %.3gW -> %.3gW <= %.3gW at %.3gW: capped at %.3gW", old, new, c.maxPower, c.power, new)
 		} else {
-			c.log.TRACE.Printf("validate power: %gW -> %gW <= %gW at %gW: ok", old, new, c.maxPower, c.power)
+			c.log.TRACE.Printf("validate power: %.3gW -> %.3gW <= %.3gW at %.3gW: ok", old, new, c.maxPower, c.power)
 		}
 	}
 
@@ -267,9 +287,9 @@ func (c *Circuit) ValidateCurrent(old, new float64) float64 {
 		potential := c.maxCurrent - c.current
 		if delta > potential {
 			new = max(0, old+potential)
-			c.log.DEBUG.Printf("validate current: %gA -> %gA <= %gA at %gA: capped at %gA", old, new, c.maxCurrent, c.current, new)
+			c.log.DEBUG.Printf("validate current: %.3gA -> %.3gA <= %.3gA at %.3gA: capped at %.3gA", old, new, c.maxCurrent, c.current, new)
 		} else {
-			c.log.TRACE.Printf("validate current: %gA -> %gA <= %gA at %gA: ok", old, new, c.maxCurrent, c.current)
+			c.log.TRACE.Printf("validate current: %.3gA -> %.3gA <= %.3gA at %.3gA: ok", old, new, c.maxCurrent, c.current)
 		}
 	}
 
diff --git a/core/circuit/config.go b/core/circuit/config.go
new file mode 100644
index 00000000..30af21a7
--- /dev/null
+++ b/core/circuit/config.go
@@ -0,0 +1,15 @@
+package circuit
+
+import (
+	"github.com/evcc-io/evcc/api"
+	"github.com/evcc-io/evcc/util/config"
+)
+
+func Root() api.Circuit {
+	for _, dev := range config.Circuits().Devices() {
+		if c := dev.Instance(); c.GetParent() == nil {
+			return c
+		}
+	}
+	return nil
+}
diff --git a/core/loadpoint.go b/core/loadpoint.go
index 802cf70d..af6555bf 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -1149,9 +1149,6 @@ func (lp *Loadpoint) pvScalePhases(sitePower, minCurrent, maxCurrent float64) in
 		lp.publishTimer(phaseTimer, lp.Enable.Delay, phaseScale3p)
 
 		if elapsed := lp.clock.Since(lp.phaseTimer); elapsed >= lp.Enable.Delay {
-			if err := lp.scalePhases(3); err != nil {
-				lp.log.ERROR.Println(err)
-			}
 			if err := lp.scalePhases(3); err != nil {
 				lp.log.ERROR.Println(err)
 			}
diff --git a/core/loadpoint_phases.go b/core/loadpoint_phases.go
index e665e432..9c9849e4 100644
--- a/core/loadpoint_phases.go
+++ b/core/loadpoint_phases.go
@@ -69,6 +69,8 @@ func (lp *Loadpoint) ActivePhases() int {
 	// sanity check - we should not assume less active phases than actually measured
 	if measured > 0 && active < measured {
 		lp.log.WARN.Printf("phase mismatch between %dp measured for %dp vehicle and %dp charger", measured, vehicle, physical)
+	} else {
+		lp.log.DEBUG.Printf("!! active phases: %dp = min(%dp measured %dp vehicle %dp physical %dp charger)", active, measured, vehicle, physical, charger)
 	}
 
 	return active
diff --git a/core/site.go b/core/site.go
index e2db3d76..bad0f921 100644
--- a/core/site.go
+++ b/core/site.go
@@ -13,6 +13,7 @@ import (
 	"github.com/cenkalti/backoff/v4"
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/cmd/shutdown"
+	"github.com/evcc-io/evcc/core/circuit"
 	"github.com/evcc-io/evcc/core/coordinator"
 	"github.com/evcc-io/evcc/core/keys"
 	"github.com/evcc-io/evcc/core/loadpoint"
@@ -69,7 +70,8 @@ type Site struct {
 	Voltage       float64      `mapstructure:"voltage"`       // Operating voltage. 230V for Germany.
 	ResidualPower float64      `mapstructure:"residualPower"` // PV meter only: household usage. Grid meter: household safety margin
 	Meters        MetersConfig `mapstructure:"meters"`        // Meter references
-	CircuitRef    string       `mapstructure:"circuit"`       // Circuit reference
+	// TODO deprecated
+	CircuitRef_ string `mapstructure:"circuit"` // Circuit reference
 
 	MaxGridSupplyWhileBatteryCharging float64 `mapstructure:"maxGridSupplyWhileBatteryCharging"` // ignore battery charging if AC consumption is above this value
 
@@ -81,11 +83,10 @@ type Site struct {
 	auxMeters     []api.Meter // Auxiliary meters
 
 	// battery settings
-	prioritySoc                       float64 // prefer battery up to this Soc
-	bufferSoc                         float64 // continue charging on battery above this Soc
-	bufferStartSoc                    float64 // start charging on battery above this Soc
-	maxGridSupplyWhileBatteryCharging float64 // ignore battery charging if AC consumption is above this value
-	batteryDischargeControl           bool    // prevent battery discharge for fast and planned charging
+	prioritySoc             float64 // prefer battery up to this Soc
+	bufferSoc               float64 // continue charging on battery above this Soc
+	bufferStartSoc          float64 // start charging on battery above this Soc
+	batteryDischargeControl bool    // prevent battery discharge for fast and planned charging
 
 	loadpoints  []*Loadpoint             // Loadpoints
 	tariffs     *tariff.Tariffs          // Tariffs
@@ -98,7 +99,7 @@ type Site struct {
 	pvPower      float64         // PV power
 	batteryPower float64         // Battery charge power
 	batterySoc   float64         // Battery soc
-	batteryMode  api.BatteryMode // Battery mode
+	batteryMode  api.BatteryMode // Battery mode (runtime only, not persisted)
 
 	publishCache map[string]any // store last published values to avoid unnecessary republishing
 }
@@ -170,12 +171,8 @@ func (site *Site) Boot(log *util.Logger, loadpoints []*Loadpoint, tariffs *tarif
 	}
 
 	// circuit
-	if site.CircuitRef != "" {
-		dev, err := config.Circuits().ByName(site.CircuitRef)
-		if err != nil {
-			return err
-		}
-		site.circuit = dev.Instance()
+	if c := circuit.Root(); c != nil {
+		site.circuit = c
 	}
 
 	// grid meter
@@ -851,7 +848,7 @@ func (site *Site) prepare() {
 	site.publish(keys.PrioritySoc, site.prioritySoc)
 	site.publish(keys.BufferSoc, site.bufferSoc)
 	site.publish(keys.BufferStartSoc, site.bufferStartSoc)
-	site.publish(keys.MaxGridSupplyWhileBatteryCharging, site.maxGridSupplyWhileBatteryCharging)
+	site.publish(keys.MaxGridSupplyWhileBatteryCharging, site.MaxGridSupplyWhileBatteryCharging)
 	site.publish(keys.BatteryMode, site.batteryMode)
 	site.publish(keys.BatteryDischargeControl, site.batteryDischargeControl)
 	site.publish(keys.ResidualPower, site.GetResidualPower())
diff --git a/core/site_api.go b/core/site_api.go
index fb8eb3e1..62ae175a 100644
--- a/core/site_api.go
+++ b/core/site_api.go
@@ -246,7 +246,7 @@ func (site *Site) SetBufferStartSoc(soc float64) error {
 func (site *Site) GetMaxGridSupplyWhileBatteryCharging() float64 {
 	site.RLock()
 	defer site.RUnlock()
-	return site.maxGridSupplyWhileBatteryCharging
+	return site.MaxGridSupplyWhileBatteryCharging
 }
 
 func (site *Site) SetMaxGridSupplyWhileBatteryCharging(power float64) error {
@@ -259,10 +259,10 @@ func (site *Site) SetMaxGridSupplyWhileBatteryCharging(power float64) error {
 
 	site.log.DEBUG.Println("set max grid supply while battery charging:", power)
 
-	if site.maxGridSupplyWhileBatteryCharging != power {
-		site.maxGridSupplyWhileBatteryCharging = power
-		settings.SetFloat(keys.MaxGridSupplyWhileBatteryCharging, site.maxGridSupplyWhileBatteryCharging)
-		site.publish(keys.MaxGridSupplyWhileBatteryCharging, site.maxGridSupplyWhileBatteryCharging)
+	if site.MaxGridSupplyWhileBatteryCharging != power {
+		site.MaxGridSupplyWhileBatteryCharging = power
+		settings.SetFloat(keys.MaxGridSupplyWhileBatteryCharging, site.MaxGridSupplyWhileBatteryCharging)
+		site.publish(keys.MaxGridSupplyWhileBatteryCharging, site.MaxGridSupplyWhileBatteryCharging)
 	}
 
 	return nil
@@ -327,14 +327,14 @@ func (site *Site) GetTariff(tariff string) api.Tariff {
 	}
 }
 
-// GetBatteryControl returns the battery control mode
+// GetBatteryDischargeControl returns the battery control mode (no discharge only)
 func (site *Site) GetBatteryDischargeControl() bool {
 	site.RLock()
 	defer site.RUnlock()
 	return site.batteryDischargeControl
 }
 
-// SetBatteryControl sets the battery control mode
+// SetBatteryDischargeControl sets the battery control mode (no discharge only)
 func (site *Site) SetBatteryDischargeControl(val bool) error {
 	site.log.DEBUG.Println("set battery discharge control:", val)
 
@@ -343,7 +343,7 @@ func (site *Site) SetBatteryDischargeControl(val bool) error {
 
 	if site.batteryDischargeControl != val {
 		// reset to normal when disabling
-		if mode := site.GetBatteryMode(); !val && batteryModeModified(mode) {
+		if mode := site.batteryMode; !val && batteryModeModified(mode) {
 			if err := site.applyBatteryMode(api.BatteryNormal); err != nil {
 				return err
 			}
diff --git a/core/site_battery.go b/core/site_battery.go
index f78225a2..186275d7 100644
--- a/core/site_battery.go
+++ b/core/site_battery.go
@@ -19,6 +19,12 @@ func (site *Site) GetBatteryMode() api.BatteryMode {
 	return site.batteryMode
 }
 
+// setBatteryMode sets the battery mode
+func (site *Site) setBatteryMode(batMode api.BatteryMode) {
+	site.batteryMode = batMode
+	site.publish(keys.BatteryMode, batMode)
+}
+
 // SetBatteryMode sets the battery mode
 func (site *Site) SetBatteryMode(batMode api.BatteryMode) {
 	site.Lock()
@@ -27,11 +33,12 @@ func (site *Site) SetBatteryMode(batMode api.BatteryMode) {
 	site.log.DEBUG.Println("set battery mode:", batMode)
 
 	if site.batteryMode != batMode {
-		site.batteryMode = batMode
-		site.publish(keys.BatteryMode, batMode)
+		site.setBatteryMode(batMode)
 	}
 }
 
+// applyBatteryMode applies the mode to each battery and updates
+// internal state if successful (requires lock)
 func (site *Site) applyBatteryMode(mode api.BatteryMode) error {
 	// update batteries
 	for _, meter := range site.batteryMeters {
@@ -43,7 +50,7 @@ func (site *Site) applyBatteryMode(mode api.BatteryMode) error {
 	}
 
 	// update state and publish
-	site.SetBatteryMode(mode)
+	site.setBatteryMode(mode)
 
 	return nil
 }
@@ -89,8 +96,10 @@ func (site *Site) updateBatteryMode() {
 	}
 
 	if batMode := site.GetBatteryMode(); mode != batMode {
+		site.Lock()
 		if err := site.applyBatteryMode(mode); err != nil {
 			site.log.ERROR.Println("battery mode:", err)
 		}
+		site.Unlock()
 	}
 }
diff --git a/go.mod b/go.mod
index dd000cd3..7dffd147 100644
--- a/go.mod
+++ b/go.mod
@@ -63,7 +63,6 @@ require (
 	github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
 	github.com/mabunixda/wattpilot v1.7.2
 	github.com/mitchellh/go-homedir v1.1.0
-	github.com/mitchellh/mapstructure v1.5.0
 	github.com/mlnoga/rct v0.1.2-0.20240421173556-1c5b75037e2f
 	github.com/muka/go-bluetooth v0.0.0-20240115085408-dfdf79b8f61d
 	github.com/mxschmitt/golang-combinations v1.1.0
@@ -134,7 +133,7 @@ require (
 	github.com/go-playground/universal-translator v0.18.1 // indirect
 	github.com/golang/mock v1.6.0 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
-	github.com/gorilla/websocket v1.5.1 // indirect
+	github.com/gorilla/websocket v1.5.2 // indirect
 	github.com/grid-x/serial v0.0.0-20211107191517-583c7356b3aa // indirect
 	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/holoplot/go-avahi v1.0.1 // indirect
diff --git a/go.sum b/go.sum
index 76852498..6954ce1f 100644
--- a/go.sum
+++ b/go.sum
@@ -264,8 +264,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
 github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
 github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
 github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
-github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
-github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
+github.com/gorilla/websocket v1.5.2 h1:qoW6V1GT3aZxybsbC6oLnailWnB+qTMVwMreOso9XUw=
+github.com/gorilla/websocket v1.5.2/go.mod h1:0n9H61RBAcf5/38py2MCYbxzPIY9rOkpvvMT24Rqs30=
 github.com/gregdel/pushover v1.3.1 h1:4bMLITOZ15+Zpi6qqoGqOPuVHCwSUvMCgVnN5Xhilfo=
 github.com/gregdel/pushover v1.3.1/go.mod h1:EcaO66Nn1StkpEm1iKtBTV3d2A16SoMsVER1PthX7to=
 github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
@@ -432,8 +432,6 @@ github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS4
 github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
 github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
 github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
-github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
-github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
 github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
 github.com/mlnoga/rct v0.1.2-0.20240421173556-1c5b75037e2f h1:63hbvjkIpccli0EA60fqvzokvU1fH1ZPiNzlF/vUmvM=
diff --git a/i18n/da.toml b/i18n/da.toml
index d8d4640e..1f0b3402 100644
--- a/i18n/da.toml
+++ b/i18n/da.toml
@@ -28,14 +28,42 @@ never = "kun med nok solar overskud"
 titleAdd = "Tilføj batterimåler"
 titleEdit = "Rediger batterimåler"
 
+[config.circuits]
+description = "Sikrer, at summen af alle ladepunkter forbundet til et kredsløb ikke overstiger de konfigurerede effekt- og strømgrænser. Kredsløb kan indlejres for at opbygge et hierarki."
+title = "Belastningsstyring"
+
+[config.control]
+description = "Standardværdierne er normalt fine. Kun hvis du ved, hvad du gør, kan du ændre dem."
+descriptionInterval = "Opdateringscyklus i sekunder. Definerer, hvor ofte evcc læser målerdata, justerer ladeeffekt og opdaterer brugergrænsefladen. Korte intervaller (< 30s) kan forårsage store udsving og fejlagtigt resultat."
+descriptionMaxGridSupply = "Kun relevant for hybrid-invertere, der ikke er i stand til at udsende den fulde DC-effekt til hjemmet via AC. Dette scenario kan føre til uønsket netbrug, da evcc antager, at den fulde DC-effekt er tilgængelig. Brug en værdi på mindst 50 W for at forhindre dette."
+descriptionResidualPower = "Skifter driftspunktet for kontrolsløjfen. Hvis du har et hjemmebatteri, anbefales det at indstille en værdi på 100 W. På denne måde vil batteriet få en lille prioritet frem for netbrug."
+labelInterval = "Opdateringsinterval"
+labelMaxGridSupply = "Max. forsyning fra elnettet under opladning"
+labelResidualPower = "Tilbageværende effekt"
+title = "kontrol adfærd"
+
 [config.deviceValue]
+broker = "Broker"
+bucket = "Bucket"
 capacity = "Kapacitet"
 chargeStatus = "Status"
+chargeStatusA = "ikke tilsluttet"
+chargeStatusB = "tilsluttet"
+chargeStatusC = "oplader"
+chargeStatusE = "ingen strøm"
+chargeStatusF = "fejl"
 chargedEnergy = "Opladet"
+co2 = "Elnettet CO₂"
+configured = "Konfigureret"
+currency = "Møntenhed"
 current = "Ström"
 enabled = "Aktiveret"
 energy = "Energi"
+feedinPrice = "Indfødningstarif"
+gridPrice = "God pris"
+no = "nej"
 odometer = "Kilometertæller"
+org = "Organisation"
 phaseCurrents = "Strømstyrke L1..L3"
 phasePowers = "Effekt L1..L3"
 phaseVoltages = "Spænding L1..L3"
@@ -43,6 +71,14 @@ power = "Effekt"
 range = "Rækkevidde"
 soc = "SoC"
 socLimit = "Begrænsning"
+temp = "Temperatur"
+topic = "Emne"
+url = "URL"
+yes = "ja"
+
+[config.eebus]
+description = "Indstilling der tillader evcc at kommunikere med andre EEBus enheder."
+title = "EEBus"
 
 [config.form]
 example = "Eksempel"
@@ -50,22 +86,54 @@ optional = "valgfrit"
 
 [config.general]
 cancel = "Afbryd"
+docsLink = "Se dokumentation."
+experimental = "Eksperimentel"
+off = "slukket"
+on = "tændt"
+password = "Adgangskode"
+remove = "Fjern"
 save = "Gem"
+telemetry = "Telemetri"
+title = "Titel"
 
 [config.grid]
+title = "Elnet-måler"
 titleAdd = "Tilføj måler for elnettet"
 titleEdit = "Rediger måler for elnettet"
 
+[config.hems]
+description = "Tilslut evcc til et andet energistyringssystem i hjemmet."
+title = "HEMS"
+
+[config.influx]
+description = "Skriver opladningsdata og andre målinger til InfluxDB. Brug Grafana eller andre værktøjer til at visualisere data."
+descriptionToken = "Tjek InfluxDB-dokumentation for at lære, hvordan du opretter en. https://docs.influxdata.com/influxdb/v2/admin/"
+labelBucket = "Bucket"
+labelDatabase = "Database"
+labelOrg = "Organisation"
+labelPassword = "Adgangskode"
+labelToken = "API Token"
+labelUrl = "URL"
+labelUser = "Brugernavn"
+title = "InfluxDB"
+v1Support = "Har du brug for support til InfluxDB 1.x?"
+v2Support = "Tilbage til InfluxDB 2.x"
+
 [config.main]
 addLoadpoint = "Tilføj opladningspunkt"
 addPvBattery = "Tilføj solpanel eller batteri"
 addVehicle = "Tilføj køretøj"
+configured = "Konfigureret"
 edit = "rediger"
 title = "Opsætning"
 unconfigured = "ikke konfigureret"
 vehicles = "Mine køretøjer"
 yaml = "Konfigurer i evcc.yaml. Kan ikke ændres i UI"
 
+[config.messaging]
+description = "Modtag beskeder om dine opladningssessioner."
+title = "Beskeder"
+
 [config.meter]
 cancel = "Afbryd"
 delete = "Slet"
@@ -74,6 +142,34 @@ template = "Fabrikant"
 titleChoice = "Hvad ønsker du at tilføje?"
 validateSave = "Valider og gem"
 
+[config.modbusproxy]
+description = "Tillad flere klienter at få adgang til en enkelt Modbus-enhed."
+title = "Modbus Proxy"
+
+[config.mqtt]
+authentication = "Godkendelse"
+description = "Opret forbindelse til en MQTT-broker for at udveksle data med andre systemer på dit netværk."
+descriptionClientId = "Afsender af meddelelserne. Hvis tom bruges `evcc-[rand]`."
+descriptionTopic = "Efterlades tom for at deaktivere udgivelse."
+labelBroker = "Broker"
+labelCheckInsecure = "Tillad selvsignerede certifikater"
+labelClientId = "Klient ID"
+labelInsecure = "Certifikatvalidering"
+labelPassword = "Adgangskode"
+labelTopic = "Emne"
+labelUser = "Brugernavn"
+publishing = "Udgivelser"
+title = "MQTT"
+
+[config.network]
+descriptionHost = "Brug .local suffiks for at aktivere mDNS. Relevant for opdagelse af mobilappen og nogle OCPP-opladere."
+descriptionPort = "Port til webgrænsefladen og API. Du skal opdatere din browser-URL, hvis du ændrer dette."
+descriptionSchema = "Påvirker kun, hvordan URL'er genereres. Valg af HTTPS vil ikke aktivere kryptering."
+labelHost = "Værtsnavn"
+labelPort = "port"
+labelSchema = "Skema"
+title = "Netværk"
+
 [config.options]
 
 [config.options.endianness]
@@ -90,12 +186,26 @@ titleEdit = "Rediger solmåler"
 
 [config.section]
 general = "Generelle"
+grid = "Elnettet"
+integrations = "Integrationer"
+loadpoints = "Ladepunkter"
+meter = "Sol og batteri"
 system = "System"
+vehicles = "Køretøjer"
 
 [config.site]
 cancel = "Afbryd"
 save = "Gem"
 
+[config.sponsor]
+addToken = "Indtast sponsor token"
+changeToken = "Udskift sponsor token"
+description = "Sponsormodellen hjælper os med at vedligeholde projektet og bæredygtigt bygge nye og spændende funktioner. Som sponsor får du adgang til alle opladning mulighederne."
+descriptionToken = "Du får et token fra {url}. Vi tilbyder også et prøvetoken til test."
+error = "Dit Sponsortoken er ikke gyldigt."
+labelToken = "Sponsor token"
+title = "Sponsorat"
+
 [config.system]
 logs = "Log"
 restart = "Genstart"
@@ -104,6 +214,10 @@ restartRequiredMessage = "Konfiguration er ændret"
 restartingDescription = "Vent venligst..."
 restartingMessage = "Genstarter evcc."
 
+[config.tariffs]
+description = "Definer dine energitakster for at beregne omkostningerne ved dine opladningssessioner."
+title = "Tariffer"
+
 [config.title]
 description = "Vises på hovedskærm og på browserfane"
 label = "Titel"
@@ -446,6 +560,9 @@ modalTitle = "Underretninger"
 [offline]
 message = "Ikke forbundet til en server."
 reload = "Genindlæs?"
+restart = "Genstart"
+restartNeeded = "Påkrævet for at anvende ændringer."
+restarting = "Serveren kommer tilbage om et øjeblik."
 
 [passwordModal]
 description = "Angiv adgangskode for at beskytte konfigurations indstillinger. Det er stadig mulig at benytte hovedskærmen uden at logge på"
@@ -531,7 +648,7 @@ auto = "Automatisk"
 label = "Sprog"
 
 [settings.sponsorToken]
-expires = "Dit sponsor token udløber om {inXDays}. {getNewToken} og opdater din konfigurations fil."
+expires = "Din sponsortoken udløber {inXDays}. {getNewToken} og opdater det her."
 getNew = "Hent en ny"
 hint = "Note: Vi automatiserer dette i fremtiden."
 
diff --git a/i18n/de.toml b/i18n/de.toml
index 02000f4c..974c05ac 100644
--- a/i18n/de.toml
+++ b/i18n/de.toml
@@ -41,6 +41,7 @@ labelResidualPower = "Residualleistung"
 title = "Regelverhalten"
 
 [config.deviceValue]
+broker = "Broker"
 capacity = "Kapazität"
 chargeStatus = "Status"
 chargeStatusA = "nicht verbunden"
@@ -50,13 +51,16 @@ chargeStatusE = "keine Leistung"
 chargeStatusF = "Fehler"
 chargedEnergy = "Geladen"
 co2 = "Netz-CO₂"
+configured = "Konfiguriert"
 currency = "Währung"
 current = "Strom"
 enabled = "Aktiviert"
 energy = "Energie"
 feedinPrice = "Einspeisevergütung"
 gridPrice = "Netzpreis"
+no = "Nein"
 odometer = "Kilometerstand"
+org = "Organisation"
 phaseCurrents = "Strom L1..L3"
 phasePowers = "Leistung L1..L3"
 phaseVoltages = "Spannung L1..L3"
@@ -65,6 +69,9 @@ range = "Reichweite"
 soc = "SoC"
 socLimit = "SoC Begrenzung"
 temp = "Temperatur"
+topic = "Thema"
+url = "URL"
+yes = "Ja"
 
 [config.eebus]
 description = "Grundkonfiguration für die Kommunikation mit anderen EEBus-Geräten."
@@ -537,6 +544,9 @@ modalTitle = "Meldungen"
 [offline]
 message = "Keine Verbindung zum Server."
 reload = "Erneut laden?"
+restart = "Neustart"
+restartNeeded = "Erforderlich, um Änderungen zu übernehmen"
+restarting = "Server ist gleich wieder verfügbar."
 
 [passwordModal]
 description = "Setze ein Passwort, um die Konfiguration zu schützen. Die Hauptansicht bleibt ohne Login zugänglich."
diff --git a/i18n/fr.toml b/i18n/fr.toml
index 58dfcb75..19320818 100644
--- a/i18n/fr.toml
+++ b/i18n/fr.toml
@@ -28,14 +28,42 @@ never = "seulement quand surplus suffisant"
 titleAdd = "Ajouter compteur pour une batterie domestique"
 titleEdit = "Modifier compteur pour batterie"
 
+[config.circuits]
+description = "Vérifiez que la somme de tous les points de charges connectés à un circuit ne dépassent pas les limites de puissance et de courant. Les circuits peuvent être combinés pour construire une hiérarchie."
+title = "Gestion de la charge"
+
+[config.control]
+description = "Les valeurs par défaut sont généralement bonnes. Ne faites des changements que si vous savez ce que vous faites."
+descriptionInterval = "Intervalle de mise à jour (en seconde) entre chaque cycle de control. Défini la fréquence à laquelle evcc va lire les valeurs des mesures, ajuster la puissance de charge et mettre à jour l’affichage. Une intervalle courte (< 30s) peut créer des oscillations et comportements indésirables."
+descriptionMaxGridSupply = "Uniquement pertinent pour les onduleurs hybrides qui ne sont pas capables de fournir la puissance totale en courant continu à destination du circuit domestique en courant alternatif. Ce scénario peut mener à une utilisation non désirée d’électricité du réseau, puisque evcc part du principe que l’intégralité du courant continu est disponible. Définir une valeur d’au moins 50 W pour éviter cela."
+descriptionResidualPower = "Déplace le point d’opération de la boucle de contrôle. Si vous avez une batterie domestique, il est recommandé de définir une valeur de 100 W. Ainsi, la batterie aura une légère priorité par rapport à l’utilisation du réseau."
+labelInterval = "Intervalle de mise à jour"
+labelMaxGridSupply = "Utilisation max du réseau pendant la charge"
+labelResidualPower = "Puissance résiduelle"
+title = "Comportement du contrôle"
+
 [config.deviceValue]
+broker = "Courtier"
+bucket = "Bucket"
 capacity = "Capacité"
 chargeStatus = "Statut"
+chargeStatusA = "pas connecté"
+chargeStatusB = "connecté"
+chargeStatusC = "en charge"
+chargeStatusE = "pas de courant"
+chargeStatusF = "erreur"
 chargedEnergy = "Chargé"
+co2 = "CO₂ du réseau"
+configured = "Configuré"
+currency = "Devise"
 current = "Courant"
 enabled = "Activé"
 energy = "Énergie"
+feedinPrice = "Prix de revente"
+gridPrice = "Prix du réseau"
+no = "non"
 odometer = "Kilométrage"
+org = "Organisation"
 phaseCurrents = "Courant L1..L3"
 phasePowers = "Puissance L1..L3"
 phaseVoltages = "Voltage L1..L3"
@@ -43,6 +71,14 @@ power = "Puissance"
 range = "Autonomie"
 soc = "Etat de Charge"
 socLimit = "Limite"
+temp = "Température"
+topic = "Sujet"
+url = "URL"
+yes = "oui"
+
+[config.eebus]
+description = "Configuration permettant à evcc de communiquer avec d’autres appareils EEBus."
+title = "EEBus"
 
 [config.form]
 example = "Exemple"
@@ -50,22 +86,54 @@ optional = "optionnel"
 
 [config.general]
 cancel = "Annuler"
+docsLink = "Se référer à la documentation."
+experimental = "Expérimental"
+off = "désactivé"
+on = "activé"
+password = "Mot de passe"
+remove = "Supprimer"
 save = "Enregistrer"
+telemetry = "Télémétrie"
+title = "Titre"
 
 [config.grid]
+title = "Compteur du réseau"
 titleAdd = "Ajouter compteur pour réseau"
 titleEdit = "Modifier compteur pour réseau"
 
+[config.hems]
+description = "Connecter evcc à un autre Système de Gestion d’Energie Domestique."
+title = "SGED"
+
+[config.influx]
+description = "Écrit les données de charge et autres métriques dans InfluxDB. Utilise Grafana ou d’autres outils pour visualiser les données."
+descriptionToken = "Se référer à la document InfluxDB pour apprendre comment en créer une. https://docs.influxdata.com/influxdb/v2/admin/"
+labelBucket = "Seau"
+labelDatabase = "Base de données"
+labelOrg = "Organisation"
+labelPassword = "Mot de passe"
+labelToken = "Jeton d’API"
+labelUrl = "URL"
+labelUser = "Nom d’utilisateur"
+title = "InfluxDB"
+v1Support = "Besoin d’aide pour InfluxDB 1x?"
+v2Support = "Revenir à InfluxDB 2.x"
+
 [config.main]
 addLoadpoint = "Ajouter un point de charge"
 addPvBattery = "Ajouter du solaire ou une batterie"
 addVehicle = "Ajouter un véhicule"
+configured = "configuré"
 edit = "éditer"
 title = "Configuration"
 unconfigured = "non configuré"
 vehicles = "Mes véhicules"
 yaml = "Configuré dans evcc.yaml. Non modifiable ici."
 
+[config.messaging]
+description = "Recevoir des messages à propos de vos sessions de charge."
+title = "Notifications"
+
 [config.meter]
 cancel = "Annuler"
 delete = "Supprimer"
@@ -74,6 +142,34 @@ template = "Fabricant"
 titleChoice = "Que voulez-vous ajouter ?"
 validateSave = "Valider et enregistrer"
 
+[config.modbusproxy]
+description = "Autoriser plusieurs clients à accéder à un appareil Modbus unique."
+title = "Proxy Modbus"
+
+[config.mqtt]
+authentication = "Authentification"
+description = "Connectez-vous à un courtier MQTT pour échanger des données avec d'autres systèmes de votre réseau."
+descriptionClientId = "Auteur des messages. `evcc-XXXX` sera utilisé si laissé vide."
+descriptionTopic = "Laisser vide pour désactiver la publication."
+labelBroker = "Courtier"
+labelCheckInsecure = "Autoriser les certificats auto-signés"
+labelClientId = "Identifiant client"
+labelInsecure = "Validation du certificat"
+labelPassword = "Mot de passe"
+labelTopic = "Sujet"
+labelUser = "Nom d’utilisateur"
+publishing = "Publication"
+title = "MQTT"
+
+[config.network]
+descriptionHost = "Utilisez le suffixe .local pour activer mDNS. Pertinent pour la découverte de l'application mobile et de certains chargeurs OCPP."
+descriptionPort = "Port pour l'interface web et l'API. Vous devrez mettre à jour l'URL de votre navigateur si vous modifiez cela."
+descriptionSchema = "Cela n'affecte que la façon dont les URL sont générées. La sélection de HTTPS n'activera pas le cryptage."
+labelHost = "Nom d’hôte"
+labelPort = "Port"
+labelSchema = "Schéma"
+title = "Réseau"
+
 [config.options]
 
 [config.options.endianness]
@@ -90,12 +186,26 @@ titleEdit = "Modifier le compteur pour le solaire"
 
 [config.section]
 general = "Général"
+grid = "Réseau électrique"
+integrations = "Intégrations"
+loadpoints = "Points de charge"
+meter = "Solaire & Batterie"
 system = "Système"
+vehicles = "Véhicules"
 
 [config.site]
 cancel = "Annuler"
 save = "Enregistrer"
 
+[config.sponsor]
+addToken = "Entrer le jeton de sponsor"
+changeToken = "Modifier le jeton de sponsor"
+description = "Le modèle de parrainage nous aide à maintenir le projet et à construire de manière durable de nouvelles fonctionnalités passionnantes. En tant que sponsor, vous avez accès à toutes les implémentations de chargeurs."
+descriptionToken = "Vous pouvez obtenir un jeton via {url}. Nous offrons également un jeton d’essai pour les tests."
+error = "Le jeton de sponsor n’est pas valide."
+labelToken = "Jeton de sponsor"
+title = "Sponsoring"
+
 [config.system]
 logs = "Journaux"
 restart = "Redémarrer"
@@ -104,6 +214,10 @@ restartRequiredMessage = "La configuration a changé."
 restartingDescription = "Veuillez patienter…"
 restartingMessage = "Redémarrage d’evcc en cours."
 
+[config.tariffs]
+description = "Définissez vos tarifs d'énergie pour calculer les coûts de vos sessions de recharge."
+title = "Tarifs"
+
 [config.title]
 description = "Affiché sur l’écran principal et l’onglet du navigateur."
 label = "Titre"
@@ -259,7 +373,7 @@ error = "Connection échouée:"
 iframeHint = "Ouvrir evcc dans un nouvel onglet."
 iframeIssue = "Votre mot de passe est correct, mais votre navigateur n’a pas gardé le cookie d’authentification. Cela peut arriver si evcc est accédé dans un iframe via HTTP."
 invalid = "Mot de passe invalide."
-login = "Identifiant"
+login = "S’identifier"
 password = "Mot de passe"
 reset = "Réinitialiser le mot de passe ?"
 title = "Authentification"
@@ -448,6 +562,9 @@ modalTitle = "Notifications"
 [offline]
 message = "Pas de connexion au serveur."
 reload = "Recharger?"
+restart = "Redémarrer"
+restartNeeded = "Nécessaire afin de prendre en compte les modifications."
+restarting = "Le serveur se à nouveau disponible dans quelques instants."
 
 [passwordModal]
 description = "Configurez un mot de passe pour protéger la configuration. L’utilisation de l’écran principal et toujours possible sans authentification."
@@ -533,7 +650,7 @@ auto = "Automatique"
 label = "Langue"
 
 [settings.sponsorToken]
-expires = "Votre jeton de supporter expire dans {inXDays}. {getNewToken} et mettez à jour votre fichier de configuration."
+expires = "Votre jeton de sponsor expire dans {inXDays}. {getNewToken} et mettez-le à jour ici."
 getNew = "Obtenez-en un nouveau"
 hint = "Remarque : nous allons automatiser cela à l'avenir."
 
diff --git a/i18n/hu.toml b/i18n/hu.toml
index 93b0fe03..3cad477a 100644
--- a/i18n/hu.toml
+++ b/i18n/hu.toml
@@ -26,14 +26,31 @@ never = "csak ha van elég többlet"
 titleAdd = "Energiatároló Mérő Hozzáadása"
 titleEdit = "Energiatároló Mérő Szerkesztése"
 
+[config.circuits]
+title = "Terhelés Menedzsment"
+
+[config.control]
+description = "Általában az alapértelmezett értékek működőképesek. Csak akkor változtasd meg őket, ha tudod, hogy mit csinálsz."
+labelInterval = "Frissítési intervallum"
+labelMaxGridSupply = "Max. hálózati ellátás töltés közben"
+
 [config.deviceValue]
 capacity = "Kapacitás"
 chargeStatus = "Státusz"
+chargeStatusA = "nincs csatlakoztatva"
+chargeStatusB = "csatlakoztatva"
+chargeStatusC = "töltés"
+chargeStatusF = "hiba"
 chargedEnergy = "Töltve"
+co2 = "Hálózat CO₂"
+configured = "Konfigurálva"
+currency = "Valuta"
 current = "Jelenlegi"
 enabled = "Engedélyezve"
 energy = "Energia"
+no = "nem"
 odometer = "Óraállás"
+org = "Szervezet"
 phaseCurrents = "Áram L1..L3"
 phasePowers = "Teljesítmény L1..L3"
 phaseVoltages = "Feszültség L1..L3"
@@ -41,6 +58,13 @@ power = "Teljesítmény"
 range = "Hatótáv"
 soc = "SoC"
 socLimit = "Limit"
+temp = "Hőmérséklet"
+topic = "Téma"
+url = "URL"
+yes = "igen"
+
+[config.eebus]
+title = "EEBus"
 
 [config.form]
 example = "Példa"
@@ -48,22 +72,44 @@ optional = "opcionális"
 
 [config.general]
 cancel = "Mégse"
+experimental = "Kísérleti"
+off = "ki"
+on = "be"
+password = "Jelszó"
+remove = "Eltávolítás"
 save = "Mentés"
+telemetry = "Telemetria"
 
 [config.grid]
 titleAdd = "Hálózati Mérő Hozzáadása"
 titleEdit = "Hálózati Mérő Szerkesztése"
 
+[config.hems]
+description = "Az evcc csatlakoztatása más otthoni energia menedzsment rendszerhez."
+
+[config.influx]
+labelDatabase = "Adatbázis"
+labelOrg = "Szervezet"
+labelPassword = "Jelszó"
+labelToken = "API Token"
+labelUrl = "URL"
+labelUser = "Felhasználónév"
+title = "InfluxDB"
+
 [config.main]
 addLoadpoint = "Töltőpont hozzáadása"
 addPvBattery = "Napelem vagy energiatároló hozzáadása"
 addVehicle = "Jármű hozzáadása"
+configured = "konfigurálva"
 edit = "szerkesztés"
 title = "Konfiguráció"
 unconfigured = "nincs konfigurálva"
 vehicles = "Járműveim"
 yaml = "Az evcc.yaml fájlban konfigurálva. Nem szerkeszthető az UI-ról."
 
+[config.messaging]
+title = "Értesítések"
+
 [config.meter]
 cancel = "Mégse"
 delete = "Törlés"
@@ -72,6 +118,25 @@ template = "Gyártó"
 titleChoice = "Mit szeretnél hozzáadni?"
 validateSave = "Ellenőrzés & mentés"
 
+[config.modbusproxy]
+title = "Modbus Proxy"
+
+[config.mqtt]
+authentication = "Hitelesítés"
+labelCheckInsecure = "Önaláírt tanúsítványok engedélyezése"
+labelClientId = "Kliens ID"
+labelInsecure = "Tanúsítvány hitelesítés"
+labelPassword = "Jelszó"
+labelTopic = "Téma"
+labelUser = "Felhasználónév"
+title = "MQTT"
+
+[config.network]
+descriptionPort = "Port a webes felülethez és az API-hoz. Frissítened kell a böngésződ URL-jét, ha ezt megváltoztatod."
+labelPort = "Port"
+labelSchema = "Séma"
+title = "Hálózat"
+
 [config.options]
 
 [config.options.endianness]
@@ -88,7 +153,16 @@ titleEdit = "Napelemes Mérő Szerkesztése"
 
 [config.section]
 general = "Általános"
+integrations = "Integrációk"
 system = "Rendszer"
+vehicles = "Járművek"
+
+[config.sponsor]
+addToken = "Szponzor token beírása"
+changeToken = "Szponzor token megváltoztatása"
+description = "A szponzorációs modell segít a projekt fenntartásában és az új izgalmas funkciók bevezetésében. Szponzorként teljes hozzáférést kapsz az összes töltőberendezés implementációjához."
+error = "A szponzor token nem érvényes."
+labelToken = "Szponzor token"
 
 [config.system]
 logs = "Naplók"
@@ -429,6 +503,7 @@ modalTitle = "Értesítések"
 [offline]
 message = "Nem csatlakozik a szerverhez."
 reload = "Újratöltés?"
+restart = "Újraindítás"
 
 [passwordModal]
 description = "Állítson be egy jelszót a konfigurációs beállítások védelmére. A főképernyő használata továbbra is lehetséges bejelentkezés nélkül."
diff --git a/i18n/lt.toml b/i18n/lt.toml
index 2cd123d4..10c873e1 100644
--- a/i18n/lt.toml
+++ b/i18n/lt.toml
@@ -28,14 +28,42 @@ never = "tik su pakankamu pertekliumi"
 titleAdd = "Pridėti kaupiklio skaitiklį"
 titleEdit = "Redaguoti kaupiklio skaitiklį"
 
+[config.circuits]
+description = "Užtikrina, kad naudojant visus į grandinę sujungtus įkroviklius, neviršytumėte sukonfigūruotų galios ir srovės ribų. Grandines galima sudėlioti, kad būtų sukurta hierarchija."
+title = "Apkrovos valdymas"
+
+[config.control]
+description = "Paprastai numatytosios reikšmės yra pakankamos. Keiskite jas tik jei žinote, ką darote."
+descriptionInterval = "Valdymo ciklų intervalai, sekundėmis. Nurodo, kaip dažnai evcc nuskaito skaitiklių duomenis, koreguoja įkrovimo galią ir atnaujina vartotojo sąsają. Trumpi intervalai (< 30 s) gali sukelti svyravimus ir nepageidaujamus rezultatus."
+descriptionMaxGridSupply = "Aktualu tik hibridiniams inverteriams, kurie negali tiekti visos generuojamos nuolatinės srovės į namus per kintamąją srovę. Dėl to gali atsirasti nepageidaujamas tinklo energijos naudojimas, nes evcc daro prielaidą, kad visa nuolatinė srovė naudotina. Pasirinkite bent 50 W vertę, kad užkirsti tam kelią."
+descriptionResidualPower = "Pakeičia valdymo sistemos reguliavimo tašką. Jei turite namų kaupiklį, rekomenduojame nustatyti 100 W vertę. Tada kaupiklis turės nedidelį prioritetą, palyginus su tinklu."
+labelInterval = "Atnaujinimo intervalas"
+labelMaxGridSupply = "Maksimalus tiekimas iš tinklo įkraunant"
+labelResidualPower = "Likutinė galia"
+title = "Valdymo elgesys"
+
 [config.deviceValue]
+broker = "Broker sistema"
+bucket = "Bucket"
 capacity = "Talpa"
 chargeStatus = "Statusas"
+chargeStatusA = "neprijungta"
+chargeStatusB = "prijungtas"
+chargeStatusC = "įkraunama"
+chargeStatusE = "nėra elektros"
+chargeStatusF = "klaida"
 chargedEnergy = "Įkrauta"
+co2 = "Tinklo CO₂"
+configured = "Sukonfigūruota"
+currency = "Valiuta"
 current = "Srovė"
 enabled = "Aktyvuota"
 energy = "Energija"
+feedinPrice = "Tiekimo į tinklą kaina"
+gridPrice = "Energijos pirkimo kaina"
+no = "ne"
 odometer = "Odometras"
+org = "Organizacija"
 phaseCurrents = "Srovė L1..L3"
 phasePowers = "Galia L1..L3"
 phaseVoltages = "Įtampa L1..L3"
@@ -43,6 +71,14 @@ power = "Galia"
 range = "Nuvažiuojamas atstumas"
 soc = "Įkrova"
 socLimit = "Limitas"
+temp = "Temperatūra"
+topic = "Tema"
+url = "URL"
+yes = "taip"
+
+[config.eebus]
+description = "Konfigūracija, leidžianti evcc susisiekti su kitais EEBus įrenginiais."
+title = "EEBus"
 
 [config.form]
 example = "Pavyzdys"
@@ -50,22 +86,54 @@ optional = "pasirinktinai"
 
 [config.general]
 cancel = "Atšaukti"
+docsLink = "Žiūrėkite dokumentaciją"
+experimental = "Eksperimentinis"
+off = "išjungta"
+on = "Įjungta"
+password = "Slaptažodis"
+remove = "Pašalinti"
 save = "Išsaugoti"
+telemetry = "Telemetrija"
+title = "Pavadinimas"
 
 [config.grid]
+title = "Tinklo skaitiklis"
 titleAdd = "Pridėti tinklo skaitiklį"
 titleEdit = "Redaguoti tinklo skaitiklį"
 
+[config.hems]
+description = "Prijunkite evcc prie kitos namų energijos valdymo sistemos."
+title = "HEMS"
+
+[config.influx]
+description = "Įrašo įkrovimo ir kitus duomenis į InfluxDB. Duomenims vizualizuoti naudokite \"Grafana\" ar panašius įrankius."
+descriptionToken = "Patikrinkite InfluxDB dokumentaciją, kad sužinotumėte, kaip ją sukurti. https://docs.influxdata.com/influxdb/v2/admin/"
+labelBucket = "Bucket"
+labelDatabase = "Duomenų bazė"
+labelOrg = "Organizacija"
+labelPassword = "Slaptažodis"
+labelToken = "API Token'as"
+labelUrl = "URL"
+labelUser = "Vartotojo vardas"
+title = "InfluxDB"
+v1Support = "Reikia InfluxDB 1.x palaikymo?"
+v2Support = "Atgal į InfluxDB 2.x"
+
 [config.main]
 addLoadpoint = "Pridėti įkroviklį"
 addPvBattery = "Pridėti saulės ar kaupiklio skaitiklį"
 addVehicle = "Pridėti automobilį"
+configured = "sukonfigūruota"
 edit = "redaguoti"
 title = "Konfigūracija"
 unconfigured = "nesukonfigūruota"
 vehicles = "Mano automobiliai"
 yaml = "Čia redaguoti neina. Konfigūruokite evcc.yaml faile."
 
+[config.messaging]
+description = "Gaukite pranešimus apie įkrovimo sesijas."
+title = "Pranešimai"
+
 [config.meter]
 cancel = "Atšaukti"
 delete = "Ištrinti"
@@ -74,6 +142,34 @@ template = "Gamintojas"
 titleChoice = "Ką norite pridėti?"
 validateSave = "Patikrinti ir išsaugoti"
 
+[config.modbusproxy]
+description = "Leisti keliems klientams pasiekti vieną „Modbus“ įrenginį."
+title = "Modbus Proxy"
+
+[config.mqtt]
+authentication = "Autentifikavimas"
+description = "Prisijunkite prie MQTT brokerio, kad keistumėtės duomenimis su kitomis tinklo sistemomis."
+descriptionClientId = "Pranešimų autorius. Jei naudojamas tuščias `evcc-[rand]` ."
+descriptionTopic = "Palikite tuščią, kad išjungtumėte publikavimą."
+labelBroker = "Broker"
+labelCheckInsecure = "Leisti savarankiškai pasirašytus sertifikatus"
+labelClientId = "Kliento ID"
+labelInsecure = "Sertifikato patvirtinimas"
+labelPassword = "Slaptažodis"
+labelTopic = "Tema"
+labelUser = "Vartotojo vardas"
+publishing = "Publikavimas"
+title = "MQTT"
+
+[config.network]
+descriptionHost = "Naudokite .local priesagą, kad įgalintumėte mDNS. Tai svarbu norint aptikti progrmėlę mobiliesiems ir kai kuriuos OCPP įkroviklius."
+descriptionPort = "Žiniatinklio sąsajos ir API prievadas. Jei tai pakeisite, turėsite atnaujinti naršyklės URL."
+descriptionSchema = "Turi įtakos tik URL generavimui. Pasirinkus HTTPS šifravimas nebus įjungtas."
+labelHost = "Hostname"
+labelPort = "Port"
+labelSchema = "Schema"
+title = "Tinklas"
+
 [config.options]
 
 [config.options.endianness]
@@ -90,12 +186,26 @@ titleEdit = "Redaguoti saulės skaitiklį"
 
 [config.section]
 general = "Pagrindiniai"
+grid = "Elektros tinklas"
+integrations = "Integracijos"
+loadpoints = "Įkrovikliai"
+meter = "Saulės energijos ir kaupiklio"
 system = "Sisteminiai"
+vehicles = "Automobiliai"
 
 [config.site]
 cancel = "Atšaukti"
 save = "Išsaugoti"
 
+[config.sponsor]
+addToken = "Įveskite sponsor token"
+changeToken = "Pakeisti sponsor token"
+description = "Rėmimo modelis padeda mums išlaikyti projektą ir tvariai kurti naujas, įdomias funkcijas. Kaip rėmėjas jūs gaunate prieigą prie visų įkroviklių implementacijų."
+descriptionToken = "Tokeną gaunate iš {url}. Taip pat siūlome bandomąjį tokeną testavimui."
+error = "Sponsoriaus tokenas negaliojantis"
+labelToken = "Sponsoriaus tokenas"
+title = "Rėmimas"
+
 [config.system]
 logs = "Žurnalai"
 restart = "Paleisti iš naujo"
@@ -104,6 +214,10 @@ restartRequiredMessage = "Nustatymai pasikeitė"
 restartingDescription = "Palaukite..."
 restartingMessage = "Paleidžiama iš naujo"
 
+[config.tariffs]
+description = "Nustatykite savo energijos tarifus, kad apskaičiuotumėte įkrovimo sesijų išlaidas."
+title = "Tarifai"
+
 [config.title]
 description = "Rodoma pagrindiniame ekrane ir naršyklės kortelėje."
 label = "Pavadinimas"
@@ -451,6 +565,9 @@ modalTitle = "Pranešimai"
 [offline]
 message = "Nėra ryšio su serveriu."
 reload = "Perkrauti?"
+restart = "Restartuoti"
+restartNeeded = "Būtina, kad pakeitimai įsigaliotų."
+restarting = "Serveris netrukus vėl veiks."
 
 [passwordModal]
 description = "Nustatykite slaptažodį konfigūracijos apsaugai. Pagrindiniu ekranu galima naudotis ir neprisijungus."
@@ -542,7 +659,7 @@ auto = "Automatiškai"
 label = "Kalba"
 
 [settings.sponsorToken]
-expires = "Jūsų rėmėjo tokenas baigiasi {inXDays}. {getNewToken} ir pakeiskite savo configūracinį failą."
+expires = "Jūsų rėmėjo tokenas baigsis {inXDays}. {getNewToken} ir išsaugokite jį čia."
 getNew = "Gaukite naują"
 hint = "Pastaba: ateityje šis procesas bus automatizuotas."
 
diff --git a/i18n/pt.toml b/i18n/pt.toml
index fd6f6588..9138a8da 100644
--- a/i18n/pt.toml
+++ b/i18n/pt.toml
@@ -28,14 +28,42 @@ never = "apenas com excesso suficiente"
 titleAdd = "Adicionar contador de bateria"
 titleEdit = "Editar contador de bateria"
 
+[config.circuits]
+description = "Assegura que a soma de todos os pontos de carga conectados a um circuito não exceda os limites de potência e corrente configurados. Circuitos podem ser aninhados para construir uma hierarquia."
+title = "Gerenciamento de carga"
+
+[config.control]
+description = "No geral, os valores padrão estão bem. Apenas mude-os se você sabe o que está fazendo."
+descriptionInterval = "Ciclo de atualização de loop de controle em segundos. Define com que frequência o evcc lê dados do medidor, ajusta o poder de carregamento e atualiza a interface do usuário. Intervalos curtos (< 30s) podem causar aszilações e comportamento indesejado."
+descriptionMaxGridSupply = "Só relevante para inversores híbridos que não são capazes de produzir o poder de geração DC completo para a casa via AC. Este szenario pode levar ao uso indesejado da grade, uma vez que o evcc assume que a potência total DC está disponível. Use um valor de pelo menos 50 W para evitar isso."
+descriptionResidualPower = "Evita o ponto de operação do circuito de controle. Se você tem uma bateria em casa, é recomendável definir um valor de 100 W. Desta forma, a bateria terá uma leve prioridade sobre o uso da rede."
+labelInterval = "Intervalo actualizado"
+labelMaxGridSupply = "Máx. rede de fornecimento enquanto carrega"
+labelResidualPower = "Poder real"
+title = "Comportamento de controle"
+
 [config.deviceValue]
+broker = "Corretor"
+bucket = "Balde"
 capacity = "Capacidade"
 chargeStatus = "Estatuto"
+chargeStatusA = "não ligado"
+chargeStatusB = "ligado"
+chargeStatusC = "carregando"
+chargeStatusE = "sem poder"
+chargeStatusF = "erro"
 chargedEnergy = "Carregado"
+co2 = "Rede CO₂"
+configured = "Configurado"
+currency = "Curência"
 current = "Corrente"
 enabled = "Ativado"
 energy = "Energia"
+feedinPrice = "Preço de alimentação"
+gridPrice = "Preço da rede"
+no = "não"
 odometer = "Odómetro"
+org = "Organização"
 phaseCurrents = "Corrente L1..L3"
 phasePowers = "Energia L1..L3"
 phaseVoltages = "Voltagem L1..L3"
@@ -43,6 +71,14 @@ power = "Energia"
 range = "Distância"
 soc = "Nível de carga"
 socLimit = "Alvo de carga"
+temp = "Temperatura"
+topic = "Tema"
+url = "URL"
+yes = "sim"
+
+[config.eebus]
+description = "Configuração que permite o evcc se comunicar com outros dispositivos EEBus."
+title = "EEBus"
 
 [config.form]
 example = "Exemplo"
@@ -50,22 +86,54 @@ optional = "opcional"
 
 [config.general]
 cancel = "Cancelar"
+docsLink = "Ver documentação."
+experimental = "Experimental"
+off = "desligado"
+on = "ligado"
+password = "Palavra-passe"
+remove = "Remover"
 save = "Guardar"
+telemetry = "Telemetria"
+title = "Título"
 
 [config.grid]
+title = "Medidor de rede"
 titleAdd = "Adicionar contador de rede"
 titleEdit = "Editar contator de rede"
 
+[config.hems]
+description = "Conecte evcc a outro sistema de gestão de energia doméstica."
+title = "HOM"
+
+[config.influx]
+description = "Escreve dados de carrega e outras métricas para o InfluxDB. Use Grafana ou outras ferramentas para visualizar os dados."
+descriptionToken = "Verifique a documentação do InfluxDB para aprender a criar uma. https://docs.influxdata.com/influxdb/v2/admin/"
+labelBucket = "Balde"
+labelDatabase = "Base de dados"
+labelOrg = "Organização"
+labelPassword = "Palavra-passe"
+labelToken = "API Token"
+labelUrl = "URL"
+labelUser = "Nome de usuário"
+title = "InfluxDB"
+v1Support = "Apoio necessário para o InfluxDB 1.x?"
+v2Support = "Voltar ao InfluxDB 2.x"
+
 [config.main]
 addLoadpoint = "Adicionar ponto de carga"
 addPvBattery = "Adicionar solar ou bateria"
 addVehicle = "Adicionar veículo"
+configured = "configurado"
 edit = "editar"
 title = "Configuração"
 unconfigured = "não configurado"
 vehicles = "Meus Veículos"
 yaml = "Configurado em evcc.yaml. Não editável na UI."
 
+[config.messaging]
+description = "Receba as mensagens sobre suas sessões de carregamento."
+title = "Notificações"
+
 [config.meter]
 cancel = "Cancelar"
 delete = "Excluir"
@@ -74,6 +142,34 @@ template = "Fabricante"
 titleChoice = "O que você quer adicionar?"
 validateSave = "Validar e guardar"
 
+[config.modbusproxy]
+description = "Permitir que vários clientes acessem um único dispositivo Modbus."
+title = "Modbus Proxy"
+
+[config.mqtt]
+authentication = "Autenticação"
+description = "Conecte-se a um corretor MQTT para trocar dados com outros sistemas em sua rede."
+descriptionClientId = "Autor das mensagens. Se o `evcc-[rand]` vazio é usado."
+descriptionTopic = "Deixe vazio para desativar a publicação."
+labelBroker = "Corretor"
+labelCheckInsecure = "Permitir certificados auto-assinados"
+labelClientId = "ID do Cliente"
+labelInsecure = "Validação de certificado"
+labelPassword = "Palavra-passe"
+labelTopic = "Tema"
+labelUser = "Nome de usuário"
+publishing = "Publicação"
+title = "MQTT"
+
+[config.network]
+descriptionHost = "Use sufixo .local para permitir mDNS. Relevante para a descoberta do aplicativo móvel e alguns carregadores OCPP."
+descriptionPort = "Port para a interface web e API. Você precisará atualizar sua URL do navegador se você mudar isso."
+descriptionSchema = "Apenas afeta como as URLs são geradas. Selecionar HTTPS não permitirá criptografia."
+labelHost = "Nome de anfitrião"
+labelPort = "Porto"
+labelSchema = "Esquema"
+title = "Rede"
+
 [config.options]
 
 [config.options.endianness]
@@ -90,7 +186,21 @@ titleEdit = "Editar Contador Solar"
 
 [config.section]
 general = "Geral"
+grid = "Rede"
+integrations = "Integrações"
+loadpoints = "Pontos de partilha"
+meter = "Solar e Bateria"
 system = "Sistema"
+vehicles = "Veículos"
+
+[config.sponsor]
+addToken = "Enter token patrocinador"
+changeToken = "Alterar token patrocinador"
+description = "O modelo patrocinador nos ajuda a manter o projeto e a construir de forma sustentável novas e emocionantes características. Como patrocinador, você tem acesso a todas as implementações do carregador."
+descriptionToken = "Você obtém o token de {url}. Também oferecemos um token de avaliação para teste."
+error = "O token patrocinador não é válido."
+labelToken = "Token patrocinador"
+title = "Patrocínio"
 
 [config.system]
 logs = "Registos"
@@ -100,6 +210,10 @@ restartRequiredMessage = "A configuração mudou."
 restartingDescription = "Espere, por favor..."
 restartingMessage = "Reiniciar evcc."
 
+[config.tariffs]
+description = "Defina suas tarifas de energia para calcular os custos de suas sessões de carregamento."
+title = "Tarifas"
+
 [config.title]
 description = "Exibido no ecrã principal e na guia do navegador."
 label = "Título"
@@ -410,7 +524,7 @@ targetSoc = "Alvo de carga"
 temp = "Temperatura"
 tempLimit = "Temperatura alvo"
 unknown = "Veículo convidado"
-vehicleSoc = "Nível de carga"
+vehicleSoc = "Cobrar"
 
 [main.vehicleSoc]
 charging = "carregando"
@@ -446,6 +560,9 @@ modalTitle = "Notícias"
 [offline]
 message = "Não conectado a um servidor."
 reload = "Iniciar de novo?"
+restart = "Reiniciar"
+restartNeeded = "Obrigado a aplicar mudanças."
+restarting = "Servidor vai voltar num momento."
 
 [passwordModal]
 description = "Defina uma palavra-passe para proteger as definições de configuração. Continua a ser possível utilizar o ecrã principal sem iniciar sessão."
@@ -538,7 +655,7 @@ auto = "Automático"
 label = "Língua"
 
 [settings.sponsorToken]
-expires = "Seu token de patrocinador expira em {inXDays}. {getNewToken} e atualize seu arquivo de configuração."
+expires = "Seu token patrocinador expira em {inXDays}. {getNewToken} e atualize-o aqui."
 getNew = "Pegue um novo"
 hint = "Nota: Vamos automatizar isso no futuro."
 
diff --git a/i18n/sl.toml b/i18n/sl.toml
index 32cf87e2..94ec705d 100644
--- a/i18n/sl.toml
+++ b/i18n/sl.toml
@@ -28,14 +28,42 @@ never = "le z dovolj presežka"
 titleAdd = "Dodaj merilnik baterije"
 titleEdit = "Uredi merilnik baterije"
 
+[config.circuits]
+description = "Zagotavlja, da vsota vseh obremenitvenih točk, povezanih z vezjem, ne preseže konfiguriranih omejitev moči in toka. Vezja je mogoče ugnezditi za izgradnjo hierarhije."
+title = "Upravljanje obremenitve"
+
+[config.control]
+description = "Običajno so privzete vrednosti v redu. Spremenite jih le, če veste, kaj počnete."
+descriptionInterval = "Cikel posodabljanja krmilne zanke v sekundah. Določa, kako pogosto evcc bere podatke števca, prilagodi moč polnjenja in posodobi uporabniški vmesnik. Kratki intervali (< 30 s) lahko povzročijo nihanje in neželeno vedenje."
+descriptionMaxGridSupply = "Ustrezno samo za hibridne pretvornike, ki ne morejo oddajati celotne električne energije enosmernega toka v dom prek izmeničnega toka. Ta scenarij lahko povzroči neželeno uporabo omrežja, saj evcc predvideva, da je na voljo polna enosmerna moč. Uporabite vrednost najmanj 50 W da bi to preprečili."
+descriptionResidualPower = "Prestavi točko delovanja krmilne zanke. Če imate domačo baterijo, je priporočljivo, da nastavite vrednost 100 W. Na ta način bo baterija imela rahlo prednost pred uporabo omrežja."
+labelInterval = "Interval posodabljanja"
+labelMaxGridSupply = "Največja napetost omrežja med polnjenjem"
+labelResidualPower = "Preostala moč"
+title = "Nadzor vedenja"
+
 [config.deviceValue]
+broker = "Broker"
+bucket = "Bucket"
 capacity = "Kapaciteta"
 chargeStatus = "Status"
+chargeStatusA = "brez povezave"
+chargeStatusB = "povezan"
+chargeStatusC = "polnjenje"
+chargeStatusE = "brez moči"
+chargeStatusF = "napaka"
 chargedEnergy = "Napolnjeno"
+co2 = "CO₂ iz omrežja"
+configured = "Konfigurirano"
+currency = "Valuta"
 current = "Tok"
 enabled = "Omogočeno"
 energy = "Energija"
+feedinPrice = "Cena prejete energije iz omrežja"
+gridPrice = "Cena omrežja"
+no = "ne"
 odometer = "Število kilometrov"
+org = "Organizacija"
 phaseCurrents = "Tok L1..L3"
 phasePowers = "Moč L1..L3"
 phaseVoltages = "Napetost L1..L3"
@@ -43,6 +71,14 @@ power = "Moč"
 range = "Doseg"
 soc = "SoC"
 socLimit = "Omejitev"
+temp = "Temperatura"
+topic = "Topic"
+url = "URL"
+yes = "da"
+
+[config.eebus]
+description = "Konfiguracija, ki evcc omogoča komunikacijo z drugimi napravami EEBus."
+title = "EEBus"
 
 [config.form]
 example = "Primer"
@@ -50,22 +86,54 @@ optional = "neobvezno"
 
 [config.general]
 cancel = "Prekliči"
+docsLink = "Oglejte si dokumentacijo."
+experimental = "Eksperimentalno"
+off = "izklopljen"
+on = "vklopljen"
+password = "Geslo"
+remove = "Odstrani"
 save = "Shrani"
+telemetry = "Telemetrija"
+title = "Naslov"
 
 [config.grid]
+title = "Omrežni meter"
 titleAdd = "Dodaj števec električnega omrežja"
 titleEdit = "Uredi števec električnega omrežja"
 
+[config.hems]
+description = "Povežite evcc z drugim sistemom za upravljanje z energijo doma."
+title = "HEMS"
+
+[config.influx]
+description = "Zapisuje podatke o polnjenju in druge meritve v InfluxDB. Za vizualizacijo podatkov uporabite Grafano ali druga orodja."
+descriptionToken = "Preverite dokumentacijo InfluxDB, če želite izvedeti, kako ga ustvariti. https://docs.influxdata.com/influxdb/v2/admin/"
+labelBucket = "Bucket"
+labelDatabase = "Baza podatkov"
+labelOrg = "Organization"
+labelPassword = "Geslo"
+labelToken = "API Žeton"
+labelUrl = "URL"
+labelUser = "Uporabniško ime"
+title = "InfluxDB"
+v1Support = "Potrebujete podporo za InfluxDB 1.x?"
+v2Support = "Nazaj na InfluxDB 2.x"
+
 [config.main]
 addLoadpoint = "Dodaj polnilno mesto"
 addPvBattery = "Dodaj sončne celice ali baterijo"
 addVehicle = "Dodaj vozilo"
+configured = "konfigurirano"
 edit = "uredi"
 title = "Konfiguracija"
 unconfigured = "ni konfigurirano"
 vehicles = "Moja vozila"
 yaml = "Konfigurirano v evcc.yaml. Ni mogoče urejati v uporabniškem vmesniku."
 
+[config.messaging]
+description = "Prejemanje sporočil o vaših sejah polnjenja."
+title = "Obvestila"
+
 [config.meter]
 cancel = "Prekliči"
 delete = "Izbriši"
@@ -74,6 +142,9 @@ template = "Proizvajalec"
 titleChoice = "Kaj želite dodati?"
 validateSave = "Preveri in shrani"
 
+[config.modbusproxy]
+description = "Dovoli več odjemalcem dostop do ene naprave Modbus."
+
 [config.options]
 
 [config.options.endianness]
@@ -535,7 +606,7 @@ auto = "Samodejno"
 label = "Jezik"
 
 [settings.sponsorToken]
-expires = "Sponzorskemu žetonu poteče veljavnost čez {inXDays}. {getNewToken} in posodobite svojo konfiguracijsko datoteko."
+expires = "Sponzorskemu žetonu poteče veljavnost čez {inXDays}. {getNewToken} in ga posodobite tukaj."
 getNew = "Zgrabi svežega"
 hint = "Opomba: To bomo v prihodnosti avtomatizirali."
 
diff --git a/i18n/sv.toml b/i18n/sv.toml
index e0b255d8..66064f4c 100644
--- a/i18n/sv.toml
+++ b/i18n/sv.toml
@@ -28,6 +28,14 @@ never = "bara med tillräckligt sol-överskott"
 titleAdd = "Lägg till batterimätare"
 titleEdit = "Ändra batterimätare"
 
+[config.circuits]
+description = "Säkrar, att summan av laddpunkter kopplad till en krets inte överskrider den konfigurerade effektgränsen. Kretsar kan nästlas för att bygga en hierarki."
+title = "Belastningshantering"
+
+[config.control]
+description = "Standardvärdena är vanligtsvis ok. Ändra bara om du vet vad du gör."
+descriptionInterval = "Uppdateringsintervall i sekunder. Definierar hur ofta evcc läser in data, ändrar laddeffekt och uppdaterar UI. Korta intervall (<30s) kan orsaka självsvängning och andra felaktiga resultat."
+
 [config.deviceValue]
 capacity = "Kapacitet"
 chargeStatus = "Status"
diff --git a/i18n/tr.toml b/i18n/tr.toml
index ef5c299e..3c9545ed 100644
--- a/i18n/tr.toml
+++ b/i18n/tr.toml
@@ -249,7 +249,7 @@ day = "Gün"
 departureTab = "Ayrılış"
 goal = "Doldurma hedefi"
 modalTitle = "Doldurma Planı"
-none = "hiçbiri"
+none = "yok"
 remove = "Kaldır"
 time = "Zaman"
 title = "Plan"
@@ -295,7 +295,7 @@ solar = "Güneş Enerjisi"
 currents = "Doldurma Akımı"
 default = "varsayılan"
 disclaimerHint = "Not:"
-onlyForSocBasedCharging = "Bu seçenekler, sadece doluluk seviyesi bilinen araçlar için mevcut."
+onlyForSocBasedCharging = "Bu seçenekler, sadece doluluk seviyesi bilinen araçlar için açık."
 smartCostCheap = "Ucuz Şebeke Dolumu"
 smartCostClean = "Temiz Şebeke Dolumu"
 title = "Ayarlar {0}"
diff --git a/provider/http.go b/provider/http.go
index ae120318..6ee6f0ee 100644
--- a/provider/http.go
+++ b/provider/http.go
@@ -4,16 +4,15 @@ import (
 	"fmt"
 	"io"
 	"math"
+	"net/http"
 	"strconv"
 	"strings"
-	"text/template"
 	"time"
 
 	"github.com/evcc-io/evcc/provider/pipeline"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
 	"github.com/evcc-io/evcc/util/transport"
-	"github.com/go-sprout/sprout"
 	"github.com/gregjones/httpcache"
 	"github.com/jpfielding/go-http-digest/pkg/digest"
 )
@@ -55,6 +54,7 @@ func NewHTTPProviderFromConfig(other map[string]interface{}) (Provider, error) {
 		Cache             time.Duration
 	}{
 		Headers: make(map[string]string),
+		Method:  http.MethodGet,
 		Scale:   1,
 		Timeout: request.Timeout,
 	}
@@ -66,7 +66,7 @@ func NewHTTPProviderFromConfig(other map[string]interface{}) (Provider, error) {
 	log := util.NewLogger("http")
 	http := NewHTTP(
 		log,
-		cc.Method,
+		strings.ToUpper(cc.Method),
 		cc.URI,
 		cc.Insecure,
 		cc.Scale,
@@ -93,14 +93,9 @@ func NewHTTPProviderFromConfig(other map[string]interface{}) (Provider, error) {
 
 // NewHTTP create HTTP provider
 func NewHTTP(log *util.Logger, method, uri string, insecure bool, scale float64, cache time.Duration) *HTTP {
-	url := util.DefaultScheme(uri, "http")
-	if strings.HasPrefix(url, "http") && !strings.HasPrefix(uri, "http") {
-		log.WARN.Printf("missing scheme for %s, assuming http", uri)
-	}
-
 	p := &HTTP{
 		Helper: request.NewHelper(log),
-		url:    url,
+		url:    uri,
 		method: method,
 		scale:  scale,
 		cache:  cache,
@@ -157,25 +152,17 @@ func (p *HTTP) WithAuth(typ, user, password string) (*HTTP, error) {
 }
 
 // request executes the configured request or returns the cached value
-func (p *HTTP) request(url string, body ...string) ([]byte, error) {
+func (p *HTTP) request(url string, body string) ([]byte, error) {
 	if time.Since(p.updated) >= p.cache {
 		var b io.Reader
-		if len(body) == 1 {
-			b = strings.NewReader(body[0])
+		if p.method != http.MethodGet {
+			b = strings.NewReader(body)
 		}
 
-		tmpl, err := template.New("url").Funcs(sprout.TxtFuncMap()).Parse(url)
-		if err != nil {
-			return nil, err
-		}
-
-		builder := new(strings.Builder)
-		if err := tmpl.Execute(builder, nil); err != nil {
-			return nil, err
-		}
+		url := util.DefaultScheme(url, "http")
 
 		// empty method becomes GET
-		req, err := request.New(strings.ToUpper(p.method), builder.String(), b, p.headers)
+		req, err := request.New(p.method, url, b, p.headers)
 		if err != nil {
 			return []byte{}, err
 		}
@@ -192,7 +179,12 @@ var _ StringProvider = (*HTTP)(nil)
 // StringGetter sends string request
 func (p *HTTP) StringGetter() (func() (string, error), error) {
 	return func() (string, error) {
-		b, err := p.request(p.url, p.body)
+		url, err := setFormattedValue(p.url, "", "")
+		if err != nil {
+			return "", err
+		}
+
+		b, err := p.request(url, p.body)
 
 		if err == nil && p.pipeline != nil {
 			b, err = p.pipeline.Process(b)
diff --git a/provider/mqtt.go b/provider/mqtt.go
index 13b33e00..df464653 100644
--- a/provider/mqtt.go
+++ b/provider/mqtt.go
@@ -157,6 +157,20 @@ func (m *Mqtt) IntSetter(param string) (func(int64) error, error) {
 	}, nil
 }
 
+var _ SetFloatProvider = (*Mqtt)(nil)
+
+// FloatSetter publishes topic with parameter replaced by float value
+func (m *Mqtt) FloatSetter(param string) (func(float64) error, error) {
+	return func(v float64) error {
+		payload, err := setFormattedValue(m.payload, param, v)
+		if err != nil {
+			return err
+		}
+
+		return m.client.Publish(m.topic, m.retained, payload)
+	}, nil
+}
+
 var _ SetBoolProvider = (*Mqtt)(nil)
 
 // BoolSetter invokes script with parameter replaced by bool value
diff --git a/tariff/pun.go b/tariff/pun.go
index 4f1538b8..1a2b3d0d 100644
--- a/tariff/pun.go
+++ b/tariff/pun.go
@@ -21,10 +21,8 @@ import (
 
 type Pun struct {
 	*embed
-	log     *util.Logger
-	charges float32
-	tax     float32
-	data    *util.Monitor[api.Rates]
+	log  *util.Logger
+	data *util.Monitor[api.Rates]
 }
 
 type NewDataSet struct {
@@ -51,23 +49,16 @@ func init() {
 }
 
 func NewPunFromConfig(other map[string]interface{}) (api.Tariff, error) {
-	cc := struct {
-		Charges float32
-		Tax     float32
-	}{
-		Charges: 0.0,
-		Tax:     0.0,
-	}
+	var cc embed
 
 	if err := util.DecodeOther(other, &cc); err != nil {
 		return nil, err
 	}
 
 	t := &Pun{
-		log:     util.NewLogger("pun"),
-		charges: cc.Charges,
-		tax:     cc.Tax,
-		data:    util.NewMonitor[api.Rates](2 * time.Hour),
+		log:   util.NewLogger("pun"),
+		embed: &cc,
+		data:  util.NewMonitor[api.Rates](2 * time.Hour),
 	}
 
 	done := make(chan error)
@@ -217,7 +208,7 @@ func (t *Pun) getData(day time.Time) (api.Rates, error) {
 		ar := api.Rate{
 			Start: start,
 			End:   end,
-			Price: (price/1000.0 + float64(t.charges)) * (1 + float64(t.tax)),
+			Price: t.totalPrice(price / 1e3),
 		}
 		data = append(data, ar)
 	}
diff --git a/tariff/tariff.go b/tariff/tariff.go
index 8db25028..00bae7b5 100644
--- a/tariff/tariff.go
+++ b/tariff/tariff.go
@@ -93,6 +93,9 @@ func (t *Tariff) run(forecastG func() (string, error), done chan error) {
 			if err := json.Unmarshal([]byte(s), &data); err != nil {
 				return backoff.Permanent(err)
 			}
+			for i, r := range data {
+				data[i].Price = t.totalPrice(r.Price)
+			}
 			return nil
 		}, bo); err != nil {
 			once.Do(func() { done <- err })
diff --git a/templates/definition/charger/alphatec.yaml b/templates/definition/charger/alphatec.yaml
index 343217e0..c8e7fc3e 100644
--- a/templates/definition/charger/alphatec.yaml
+++ b/templates/definition/charger/alphatec.yaml
@@ -6,6 +6,9 @@ products:
   - brand: Alphatec
     description:
       generic: Ladesäule Twin
+  - brand: LRT
+    description:
+      generic: HOME Essential+
 requirements:
   description:
     de: Die Hauptplatine benötigt eine aktuelle Firmware. Eine aktuelle Softwareversion kann man daran erkennen, dass die Seriennummer auf dem braunen Relais mit 2022 beginnt oder auf den kleinen weißen Relais eine 15 steht. Andernfalls bitte direkt an den Hersteller wenden.
diff --git a/templates/definition/charger/huawai-ac.yaml b/templates/definition/charger/huawai-ac.yaml
deleted file mode 100644
index 1210d8de..00000000
--- a/templates/definition/charger/huawai-ac.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-template: huawei-ac
-products:
-  - brand: Huawei
-    description:
-      generic: FusionCharge AC
-requirements:
-  evcc: ["sponsorship"]
-params:
-  - name: modbus
-    choice: ["tcpip"]
-    id: 255
-render: |
-  type: huawei-ac
-  {{- include "modbus" . }}
diff --git a/templates/definition/charger/kse.yaml b/templates/definition/charger/kse.yaml
index 38d676c5..b743f26c 100644
--- a/templates/definition/charger/kse.yaml
+++ b/templates/definition/charger/kse.yaml
@@ -3,7 +3,7 @@ products:
   - brand: KSE
     description:
       generic: wBX16
-capabilities: ["rfid"]
+capabilities: ["rfid", "1p3p"]
 requirements:
   evcc: ["sponsorship"]
 params:
diff --git a/templates/definition/meter/eastron-sdm220_230.yaml b/templates/definition/meter/eastron-sdm220_230.yaml
index bfe7d4dd..a2539593 100644
--- a/templates/definition/meter/eastron-sdm220_230.yaml
+++ b/templates/definition/meter/eastron-sdm220_230.yaml
@@ -3,6 +3,12 @@ products:
   - brand: Eastron
     description:
       generic: SDM220/230
+  - brand: Weidmüller
+    description:
+      generic: EM110-RTU-2P
+  - brand: Weidmüller
+    description:
+      generic: EM111-RTU-2P
 params:
   - name: usage
     choice: ["grid", "charge"]
diff --git a/templates/definition/meter/eastron-sdm72v2_630.yaml b/templates/definition/meter/eastron-sdm72v2_630.yaml
index 287f476c..33dae884 100644
--- a/templates/definition/meter/eastron-sdm72v2_630.yaml
+++ b/templates/definition/meter/eastron-sdm72v2_630.yaml
@@ -6,6 +6,12 @@ products:
   - brand: Eastron
     description:
       generic: SDM72DM-V2
+  - brand: Weidmüller
+    description:
+      generic: EM120-RTU-2P
+  - brand: Weidmüller
+    description:
+      generic: EM122-RTU-2P
 params:
   - name: usage
     choice: ["grid", "charge"]
diff --git a/templates/definition/meter/sofarsolar.yaml b/templates/definition/meter/sofarsolar.yaml
index 5b20444d..095f76d4 100644
--- a/templates/definition/meter/sofarsolar.yaml
+++ b/templates/definition/meter/sofarsolar.yaml
@@ -80,10 +80,10 @@ render: |
     source: modbus
     {{- include "modbus" . | indent 2 }}
     register:
-      address: 0x020F # Battery Charge/Discharge current
+      address: 0x020D # Battery Charge/Discharge power
       type: holding
       decode: int16
-    scale: 0.01
+    scale: 10
   soc:
     source: modbus
     {{- include "modbus" . | indent 2 }}
diff --git a/templates/definition/meter/solaredge-hybrid.yaml b/templates/definition/meter/solaredge-hybrid.yaml
index 4bf7dde6..2e85e0c1 100644
--- a/templates/definition/meter/solaredge-hybrid.yaml
+++ b/templates/definition/meter/solaredge-hybrid.yaml
@@ -37,6 +37,63 @@ render: |
     subdevice: 1 # Metering device
     value: 203:W
     scale: -1
+  energy:
+    source: sunspec
+    {{- include "modbus" . | indent 2 }}
+    timeout: {{ .timeout }}
+    subdevice: 1 # Metering device
+    value: 203:TotWhImp
+  currents:
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      timeout: {{ .timeout }}
+      subdevice: 1 # Metering device
+      value: 203:AphA
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      timeout: {{ .timeout }}
+      subdevice: 1 # Metering device
+      value: 203:AphB
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      timeout: {{ .timeout }}
+      subdevice: 1 # Metering device
+      value: 203:AphC
+  voltages:
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      timeout: {{ .timeout }}
+      subdevice: 1 # Metering device
+      value: 203:PhVphA
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      timeout: {{ .timeout }}
+      subdevice: 1 # Metering device
+      value: 203:PhVphB
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      timeout: {{ .timeout }}
+      subdevice: 1 # Metering device
+      value: 203:PhVphC
+  powers:
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      timeout: {{ .timeout }}
+      subdevice: 1 # Metering device
+      value:  203:WphA
+      scale: -1
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      timeout: {{ .timeout }}
+      subdevice: 1 # Metering device
+      value: 203:WphB
+      scale: -1
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      timeout: {{ .timeout }}
+      subdevice: 1 # Metering device
+      value: 203:WphC
+      scale: -1
   {{- end }}
   {{- if eq .usage "pv" }}
   power:
diff --git a/templates/definition/meter/solaredge-inverter.yaml b/templates/definition/meter/solaredge-inverter.yaml
index 64853a05..0e27284f 100644
--- a/templates/definition/meter/solaredge-inverter.yaml
+++ b/templates/definition/meter/solaredge-inverter.yaml
@@ -24,9 +24,66 @@ render: |
     source: sunspec
     {{- include "modbus" . | indent 2 }}
     timeout: {{ .timeout }}
-    subdevice: 1
+    subdevice: 1 # Metering device
     value: 203:W # sunspec 3-phase meter power reading
     scale: -1
+  energy:
+    source: sunspec
+    {{- include "modbus" . | indent 2 }}
+    timeout: {{ .timeout }}
+    subdevice: 1 # Metering device
+    value: 203:TotWhImp
+  currents:
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      timeout: {{ .timeout }}
+      subdevice: 1 # Metering device
+      value: 203:AphA
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      timeout: {{ .timeout }}
+      subdevice: 1 # Metering device
+      value: 203:AphB
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      timeout: {{ .timeout }}
+      subdevice: 1 # Metering device
+      value: 203:AphC
+  voltages:
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      timeout: {{ .timeout }}
+      subdevice: 1 # Metering device
+      value: 203:PhVphA
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      timeout: {{ .timeout }}
+      subdevice: 1 # Metering device
+      value: 203:PhVphB
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      timeout: {{ .timeout }}
+      subdevice: 1 # Metering device
+      value: 203:PhVphC
+  powers:
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      timeout: {{ .timeout }}
+      subdevice: 1 # Metering device
+      value:  203:WphA
+      scale: -1
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      timeout: {{ .timeout }}
+      subdevice: 1 # Metering device
+      value: 203:WphB
+      scale: -1
+    - source: sunspec
+      {{- include "modbus" . | indent 4 }}
+      timeout: {{ .timeout }}
+      subdevice: 1 # Metering device
+      value: 203:WphC
+      scale: -1
   {{- end }}
   {{- if eq .usage "pv" }}
   power:
diff --git a/templates/definition/vehicle/jaguar-landrover.yaml b/templates/definition/vehicle/jaguar-landrover.yaml
index 872b0959..49a6a49b 100644
--- a/templates/definition/vehicle/jaguar-landrover.yaml
+++ b/templates/definition/vehicle/jaguar-landrover.yaml
@@ -1,4 +1,5 @@
 template: jaguar-landrover
+deprecated: true
 products:
   - brand: Jaguar
   - brand: Land Rover
diff --git a/util/sponsor/auth.go b/util/sponsor/auth.go
index 26bf1331..56f4e59d 100644
--- a/util/sponsor/auth.go
+++ b/util/sponsor/auth.go
@@ -13,7 +13,7 @@ import (
 )
 
 var (
-	mu             sync.Mutex
+	mu             sync.RWMutex
 	Subject, Token string
 	ExpiresAt      time.Time
 )
@@ -24,14 +24,14 @@ const (
 )
 
 func IsAuthorized() bool {
-	mu.Lock()
-	defer mu.Unlock()
+	mu.RLock()
+	defer mu.RUnlock()
 	return len(Subject) > 0
 }
 
 func IsAuthorizedForApi() bool {
-	mu.Lock()
-	defer mu.Unlock()
+	mu.RLock()
+	defer mu.RUnlock()
 	return IsAuthorized() && Subject != unavailable && Token != ""
 }
 
diff --git a/vehicle/niu/types.go b/vehicle/niu/types.go
index 7d4c2007..cf131470 100644
--- a/vehicle/niu/types.go
+++ b/vehicle/niu/types.go
@@ -2,6 +2,7 @@ package niu
 
 import (
 	"encoding/json"
+	"errors"
 	"time"
 
 	"golang.org/x/oauth2"
@@ -19,16 +20,24 @@ type Token oauth2.Token
 func (t *Token) UnmarshalJSON(data []byte) error {
 	var res struct {
 		Data struct {
-			Token struct {
+			Token *struct {
 				oauth2.Token
 				RefreshTokenExpiresIn int64 `json:"refresh_token_expires_in,omitempty"`
 				TokenExpiresIn        int64 `json:"token_expires_in,omitempty"`
 			}
+			Desc string
 		}
 	}
 
 	err := json.Unmarshal(data, &res)
 	if err == nil {
+		if res.Data.Token == nil {
+			if msg := res.Data.Desc; msg != "" {
+				return errors.New(msg)
+			}
+			return errors.New("missing token")
+		}
+
 		(*t) = (Token)(res.Data.Token.Token)
 
 		if res.Data.Token.Expiry.IsZero() && res.Data.Token.TokenExpiresIn != 0 {
diff --git a/vehicle/renault/provider.go b/vehicle/renault/provider.go
index 3daa547c..d5ae55c1 100644
--- a/vehicle/renault/provider.go
+++ b/vehicle/renault/provider.go
@@ -145,7 +145,7 @@ func (v *Provider) Climater() (bool, error) {
 		return false, err
 	}
 
-	return res.Data.Attributes.HvacStatus == 2, nil
+	return res.Data.Attributes.HvacStatus > 0, nil
 }
 
 var _ api.Resurrector = (*Provider)(nil)
diff --git a/vehicle/seat/cupra/api.go b/vehicle/seat/cupra/api.go
index 2b60c152..3ba65303 100644
--- a/vehicle/seat/cupra/api.go
+++ b/vehicle/seat/cupra/api.go
@@ -43,7 +43,7 @@ func (v *API) Vehicles(userID string) ([]Vehicle, error) {
 		Vehicles []Vehicle
 	}
 
-	uri := fmt.Sprintf("%s/v1/users/%s/garage/vehicles", BaseURL, userID)
+	uri := fmt.Sprintf("%s/v2/users/%s/garage/vehicles", BaseURL, userID)
 	err := v.GetJSON(uri, &res)
 
 	return res.Vehicles, err
diff --git a/vehicle/skoda-enyaq.go b/vehicle/skoda-enyaq.go
index 4218467a..cd4d72af 100644
--- a/vehicle/skoda-enyaq.go
+++ b/vehicle/skoda-enyaq.go
@@ -6,17 +6,16 @@ import (
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
-	"github.com/evcc-io/evcc/vehicle/skoda"
-	"github.com/evcc-io/evcc/vehicle/skoda/connect"
-	"github.com/evcc-io/evcc/vehicle/vag/service"
+	"github.com/evcc-io/evcc/vehicle/skoda/myskoda"
+	"github.com/evcc-io/evcc/vehicle/skoda/myskoda/service"
 )
 
-// https://github.com/lendy007/skodaconnect
+// https://gitlab.com/prior99/skoda
 
 // Enyaq is an api.Vehicle implementation for Skoda Enyaq cars
 type Enyaq struct {
 	*embed
-	*skoda.Provider // provides the api implementations
+	*myskoda.Provider // provides the api implementations
 }
 
 func init() {
@@ -51,36 +50,35 @@ func NewEnyaqFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 	log := util.NewLogger("enyaq").Redact(cc.User, cc.Password, cc.VIN)
 
 	// use Skoda api to resolve list of vehicles
-	ts, err := service.TokenRefreshServiceTokenSource(log, skoda.TRSParams, skoda.AuthParams, cc.User, cc.Password)
+	ts, err := service.TokenRefreshServiceTokenSource(log, myskoda.TRSParams, myskoda.AuthParams, cc.User, cc.Password)
 	if err != nil {
 		return nil, err
 	}
 
-	api := skoda.NewAPI(log, ts)
+	api := myskoda.NewAPI(log, ts)
 	api.Client.Timeout = cc.Timeout
 
 	vehicle, err := ensureVehicleEx(
 		cc.VIN, api.Vehicles,
-		func(v skoda.Vehicle) (string, error) {
+		func(v myskoda.Vehicle) (string, error) {
 			return v.VIN, nil
 		},
 	)
 
 	if err == nil {
-		v.fromVehicle(vehicle.Name, float64(vehicle.Specification.Battery.CapacityInKWh))
+		vehicle, err = api.VehicleDetails(vehicle.VIN)
 	}
 
-	// use Connect credentials to build provider
 	if err == nil {
-		ts, err := service.TokenRefreshServiceTokenSource(log, skoda.TRSParams, connect.AuthParams, cc.User, cc.Password)
-		if err != nil {
-			return nil, err
-		}
+		v.fromVehicle(vehicle.Name, float64(vehicle.Specification.Battery.CapacityInKWh))
+	}
 
-		api := skoda.NewAPI(log, ts)
+	// reuse tokenService to build provider
+	if err == nil {
+		api := myskoda.NewAPI(log, ts)
 		api.Client.Timeout = cc.Timeout
 
-		v.Provider = skoda.NewProvider(api, vehicle.VIN, cc.Cache)
+		v.Provider = myskoda.NewProvider(api, vehicle.VIN, cc.Cache)
 	}
 
 	return v, err
diff --git a/vehicle/skoda/myskoda/api.go b/vehicle/skoda/myskoda/api.go
new file mode 100644
index 00000000..6d198ebd
--- /dev/null
+++ b/vehicle/skoda/myskoda/api.go
@@ -0,0 +1,113 @@
+package myskoda
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/request"
+	"golang.org/x/oauth2"
+)
+
+const BaseURI = "https://mysmob.api.connect.skoda-auto.cz/api"
+
+// API is the Skoda api client
+type API struct {
+	*request.Helper
+}
+
+// NewAPI creates a new api client
+func NewAPI(log *util.Logger, ts oauth2.TokenSource) *API {
+	v := &API{
+		Helper: request.NewHelper(log),
+	}
+
+	v.Client.Transport = &oauth2.Transport{
+		Source: ts,
+		Base:   v.Client.Transport,
+	}
+
+	return v
+}
+
+// Vehicles implements the /v2/garage response
+func (v *API) Vehicles() ([]Vehicle, error) {
+	var res VehiclesResponse
+
+	uri := fmt.Sprintf("%s/v2/garage", BaseURI)
+	err := v.GetJSON(uri, &res)
+
+	return res.Vehicles, err
+}
+
+func (v *API) VehicleDetails(vin string) (Vehicle, error) {
+	var res Vehicle
+	uri := fmt.Sprintf("%s/v2/garage/vehicles/%s", BaseURI, vin)
+	err := v.GetJSON(uri, &res)
+	return res, err
+}
+
+// Status implements the /v2/vehicle-status/<vin> response
+func (v *API) Status(vin string) (StatusResponse, error) {
+	var res StatusResponse
+	uri := fmt.Sprintf("%s/v1/vehicle-health-report/warning-lights/%s", BaseURI, vin)
+	err := v.GetJSON(uri, &res)
+	return res, err
+}
+
+// Charger implements the /v1/charging/<vin> response
+func (v *API) Charger(vin string) (ChargerResponse, error) {
+	var res ChargerResponse
+	uri := fmt.Sprintf("%s/v1/charging/%s", BaseURI, vin)
+	err := v.GetJSON(uri, &res)
+	return res, err
+}
+
+// Settings implements the /v1/charging/<vin>/settings response
+func (v *API) Settings(vin string) (SettingsResponse, error) {
+	var res SettingsResponse
+
+	chrgRes, err := v.Charger(vin)
+	if err == nil {
+		res = chrgRes.Settings
+	}
+	return res, err
+}
+
+// Climater implements the /v2/air-conditioning/<vin> response
+func (v *API) Climater(vin string) (ClimaterResponse, error) {
+	var res ClimaterResponse
+	uri := fmt.Sprintf("%s/v2/air-conditioning/%s", BaseURI, vin)
+	err := v.GetJSON(uri, &res)
+	return res, err
+}
+
+const (
+	ActionCharge      = "charging"
+	ActionChargeStart = "start"
+	ActionChargeStop  = "stop"
+)
+
+// Action executes a vehicle action
+func (v *API) Action(vin, action, value string) error {
+	// @POST("api/v1/charging/{vin}/start")
+	// @POST("api/v1/charging/{vin}/stop")
+	uri := fmt.Sprintf("%s/v1/%s/%s/%s", BaseURI, action, vin, value)
+
+	req, err := request.New(http.MethodPost, uri, nil, request.JSONEncoding)
+	if err == nil {
+		err = v.DoJSON(req, nil)
+	}
+	return err
+}
+
+func (v *API) WakeUp(vin string) error {
+	// @POST("api/v1/vehicle-wakeup/{vin}")
+	uri := fmt.Sprintf("%s/v1/vehicle-wakeup/%s", BaseURI, vin)
+
+	req, err := request.New(http.MethodPost, uri, nil, request.JSONEncoding)
+	if err == nil {
+		err = v.DoJSON(req, nil)
+	}
+	return err
+}
diff --git a/vehicle/skoda/myskoda/params.go b/vehicle/skoda/myskoda/params.go
new file mode 100644
index 00000000..38c45162
--- /dev/null
+++ b/vehicle/skoda/myskoda/params.go
@@ -0,0 +1,24 @@
+package myskoda
+
+import "net/url"
+
+const (
+	Brand   = "VW"
+	Country = "CZ"
+
+	// Authorization ClientID
+	AuthClientID = "afb0473b-6d82-42b8-bfea-cead338c46ef"
+)
+
+// Skoda native api
+var AuthParams = url.Values{
+	"response_type": {"code id_token"},
+	"client_id":     {"7f045eee-7003-4379-9968-9355ed2adb06@apps_vw-dilab_com"},
+	"redirect_uri":  {"myskoda://redirect/login/"},
+	"scope":         {"address badge birthdate cars driversLicense dealers email mileage mbb nationalIdentifier openid phone profession profile vin"},
+}
+
+// TokenRefreshService parameters
+var TRSParams = url.Values{
+	"brand": {"skoda"},
+}
diff --git a/vehicle/skoda/myskoda/provider.go b/vehicle/skoda/myskoda/provider.go
new file mode 100644
index 00000000..f3ad10c7
--- /dev/null
+++ b/vehicle/skoda/myskoda/provider.go
@@ -0,0 +1,162 @@
+package myskoda
+
+import (
+	"time"
+
+	"github.com/evcc-io/evcc/api"
+	"github.com/evcc-io/evcc/provider"
+)
+
+// Provider implements the vehicle api
+type Provider struct {
+	statusG   func() (StatusResponse, error)
+	chargerG  func() (ChargerResponse, error)
+	settingsG func() (SettingsResponse, error)
+	climateG  func() (ClimaterResponse, error)
+	action    func(action, value string) error
+	wakeup    func() error
+}
+
+// NewProvider creates a vehicle api provider
+func NewProvider(api *API, vin string, cache time.Duration) *Provider {
+	impl := &Provider{
+		statusG: provider.Cached(func() (StatusResponse, error) {
+			return api.Status(vin)
+		}, cache),
+		chargerG: provider.Cached(func() (ChargerResponse, error) {
+			return api.Charger(vin)
+		}, cache),
+		climateG: provider.Cached(func() (ClimaterResponse, error) {
+			return api.Climater(vin)
+		}, cache),
+		settingsG: provider.Cached(func() (SettingsResponse, error) {
+			return api.Settings(vin)
+		}, cache),
+		action: func(action, value string) error {
+			return api.Action(vin, action, value)
+		},
+		wakeup: func() error {
+			return api.WakeUp(vin)
+		},
+	}
+	return impl
+}
+
+var _ api.Battery = (*Provider)(nil)
+
+// Soc implements the api.Vehicle interface
+func (v *Provider) Soc() (float64, error) {
+	res, err := v.chargerG()
+	if err == nil {
+		return float64(res.Status.Battery.StateOfChargeInPercent), nil
+	}
+
+	return 0, err
+}
+
+var _ api.ChargeState = (*Provider)(nil)
+
+// Status implements the api.ChargeState interface
+func (v *Provider) Status() (api.ChargeStatus, error) {
+	status := api.StatusA // disconnected
+
+	res, err := v.climateG()
+	if err == nil {
+		if res.ChargerConnectionState == "CONNECTED" {
+			status = api.StatusB
+		}
+	}
+
+	resChrg, err := v.chargerG()
+	if err == nil {
+		if resChrg.Status.State == "CHARGING" {
+			status = api.StatusC
+		}
+	}
+
+	return status, err
+}
+
+var _ api.VehicleFinishTimer = (*Provider)(nil)
+
+// FinishTime implements the api.VehicleFinishTimer interface
+func (v *Provider) FinishTime() (time.Time, error) {
+	res, err := v.chargerG()
+	if err == nil {
+		crg := res.Status
+
+		// estimate not available
+		if crg.State == "Error" || crg.ChargeType == "Invalid" {
+			return time.Time{}, api.ErrNotAvailable
+		}
+
+		remaining := time.Duration(crg.RemainingTimeToFullyChargedInMinutes) * time.Minute
+		return time.Now().Add(remaining), err
+	}
+
+	return time.Time{}, err
+}
+
+var _ api.VehicleRange = (*Provider)(nil)
+
+// Range implements the api.VehicleRange interface
+func (v *Provider) Range() (rng int64, err error) {
+	res, err := v.chargerG()
+	return res.Status.Battery.RemainingCruisingRangeInMeters / 1e3, err
+}
+
+var _ api.VehicleOdometer = (*Provider)(nil)
+
+// Odometer implements the api.VehicleOdometer interface
+func (v *Provider) Odometer() (odo float64, err error) {
+	res, err := v.statusG()
+	return res.MileageInKm, err
+}
+
+// var _ api.VehicleClimater = (*Provider)(nil)
+
+// // Climater implements the api.VehicleClimater interface
+// func (v *Provider) Climater() (active bool, outsideTemp float64, targetTemp float64, err error) {
+// 	res, err := v.climateG()
+// err == nil {
+// 		state := strings.ToLower(res.Climater.Status.ClimatisationStatusData.ClimatisationState.Content)
+// 		active := state != "off" && state != "invalid" && state != "error"
+
+// 		targetTemp = res.Climater.Settings.TargetTemperature.Content
+// 		outsideTemp = res.Climater.Status.TemperatureStatusData.OutdoorTemperature.Content
+// 		if math.IsNaN(outsideTemp) {
+// 			outsideTemp = targetTemp // cover "invalid"
+// 		}
+
+// 		return active, outsideTemp, targetTemp, nil
+// 	}
+
+// 	return active, outsideTemp, targetTemp, err
+// }
+
+var _ api.SocLimiter = (*Provider)(nil)
+
+// GetLimitSoc implements the api.SocLimiter interface
+func (v *Provider) GetLimitSoc() (int64, error) {
+	res, err := v.chargerG()
+	if err == nil {
+		return int64(res.Settings.TargetStateOfChargeInPercent), nil
+	}
+
+	return 0, err
+}
+
+var _ api.ChargeController = (*Provider)(nil)
+
+// ChargeEnable implements the api.ChargeController interface
+func (v *Provider) ChargeEnable(enable bool) error {
+	action := map[bool]string{true: ActionChargeStart, false: ActionChargeStop}
+	return v.action(ActionCharge, action[enable])
+}
+
+var _ api.Resurrector = (*Provider)(nil)
+
+// WakeUp implements the api.Resurrector interface
+func (v *Provider) WakeUp() error {
+	return v.wakeup()
+}
diff --git a/vehicle/skoda/myskoda/service/tokenrefreshservice.go b/vehicle/skoda/myskoda/service/tokenrefreshservice.go
new file mode 100644
index 00000000..7293aff6
--- /dev/null
+++ b/vehicle/skoda/myskoda/service/tokenrefreshservice.go
@@ -0,0 +1,25 @@
+package service
+
+import (
+	"net/url"
+
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/vehicle/skoda/myskoda/tokenrefreshservice"
+	"github.com/evcc-io/evcc/vehicle/vag"
+	"github.com/evcc-io/evcc/vehicle/vag/vwidentity"
+)
+
+func TokenRefreshServiceTokenSource(log *util.Logger, data, q url.Values, user, password string) (vag.TokenSource, error) {
+	q, err := vwidentity.Login(log, q, user, password)
+	if err != nil {
+		return nil, err
+	}
+
+	trs := tokenrefreshservice.New(log, data)
+	token, err := trs.Exchange(q)
+	if err != nil {
+		return nil, err
+	}
+
+	return trs.TokenSource(token), nil
+}
diff --git a/vehicle/skoda/myskoda/tokenrefreshservice/endpoint.go b/vehicle/skoda/myskoda/tokenrefreshservice/endpoint.go
new file mode 100644
index 00000000..67929b93
--- /dev/null
+++ b/vehicle/skoda/myskoda/tokenrefreshservice/endpoint.go
@@ -0,0 +1,91 @@
+package tokenrefreshservice
+
+import (
+	"encoding/json"
+	"net/url"
+	"time"
+
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/request"
+	"github.com/evcc-io/evcc/util/urlvalues"
+	"github.com/evcc-io/evcc/vehicle/vag"
+)
+
+const (
+	BaseURL         = "https://mysmob.api.connect.skoda-auto.cz"
+	CodeExchangeURL = BaseURL + "/api/v1/authentication/exchange-authorization-code?tokenType=CONNECT"
+	RefreshTokenURL = BaseURL + "/api/v1/authentication/refresh-token?tokenType=CONNECT"
+)
+
+var _ vag.TokenExchanger = (*Service)(nil)
+
+type Service struct {
+	*request.Helper
+	data url.Values
+}
+
+type skodaTokenResponse struct {
+	AccessToken  string `json:"accessToken"`
+	RefreshToken string `json:"refreshToken"`
+	IdToken      string `json:"idToken"`
+}
+
+func (s *skodaTokenResponse) toVagToken(v *vag.Token) {
+	v.AccessToken = s.AccessToken
+	v.RefreshToken = s.RefreshToken
+	v.IDToken = s.IdToken
+	v.Expiry = time.Now().Add(time.Minute * 60)
+}
+
+func New(log *util.Logger, q url.Values) *Service {
+	return &Service{
+		Helper: request.NewHelper(log),
+		data:   q,
+	}
+}
+
+func (v *Service) Exchange(q url.Values) (*vag.Token, error) {
+	if err := urlvalues.Require(q, "id_token", "code"); err != nil {
+		return nil, err
+	}
+
+	var skTok skodaTokenResponse
+
+	data := map[string]string{
+		"code":        q.Get("code"),
+		"redirectUri": "myskoda://redirect/login/",
+		"verifier":    q.Get("code_verifier"),
+	}
+
+	resp, err := v.Post(CodeExchangeURL, request.JSONContent, request.MarshalJSON(data))
+
+	if err == nil {
+		defer resp.Body.Close()
+		json.NewDecoder(resp.Body).Decode(&skTok)
+	}
+
+	var res vag.Token
+	skTok.toVagToken(&res)
+	return &res, err
+}
+
+func (v *Service) Refresh(token *vag.Token) (*vag.Token, error) {
+	var skTok skodaTokenResponse
+
+	data := map[string]string{"token": token.RefreshToken}
+	resp, err := v.Post(RefreshTokenURL, request.JSONContent, request.MarshalJSON(data))
+
+	if err == nil {
+		defer resp.Body.Close()
+		json.NewDecoder(resp.Body).Decode(&skTok)
+	}
+
+	var res vag.Token
+	skTok.toVagToken(&res)
+	return &res, err
+}
+
+// TokenSource creates token source. Token is refreshed automatically.
+func (v *Service) TokenSource(token *vag.Token) vag.TokenSource {
+	return vag.RefreshTokenSource(token, v.Refresh)
+}
diff --git a/vehicle/skoda/myskoda/types.go b/vehicle/skoda/myskoda/types.go
new file mode 100644
index 00000000..d5ccebdf
--- /dev/null
+++ b/vehicle/skoda/myskoda/types.go
@@ -0,0 +1,73 @@
+package myskoda
+
+// VehiclesResponse is the /v3/garage api
+type VehiclesResponse struct {
+	Vehicles []Vehicle
+}
+
+type Vehicle struct {
+	ID, VIN       string
+	Name          string // user-given name
+	LastUpdatedAt string
+	Specification Specification
+	// Connectivities
+	// Capabilities
+}
+
+type Specification struct {
+	Title         string
+	Model         string
+	ModelYear     string
+	Body          string
+	SystemCode    string
+	SystemModelId string
+	Engine        struct {
+		Typ       string `json:"type"`
+		PowerInKW int
+	}
+	Battery struct {
+		CapacityInKWh int
+	}
+	Gearbox struct {
+		Typ string `json:"type"`
+	}
+	TrimLevel            string
+	ManufacturingDate    string
+	MaxChargingPowerInKW int
+}
+
+// StatusResponse is the /v2/vehicle-status/<vin> api
+type StatusResponse struct {
+	MileageInKm float64
+}
+
+// ChargerResponse is the /v2/charging/<vin> api
+type ChargerResponse struct {
+	IsVehicleInSaveLocation bool
+	Status                  struct {
+		ChargingRateInKilometersPerHour      float64
+		ChargePowerInKw                      float64
+		RemainingTimeToFullyChargedInMinutes int64
+		State                                string
+		ChargeType                           string
+		Battery                              struct {
+			RemainingCruisingRangeInMeters int64
+			StateOfChargeInPercent         int
+		}
+	}
+	Settings SettingsResponse
+}
+
+// SettingsResponse is the /v1/charging/<vin>/settings api
+type SettingsResponse struct {
+	AutoUnlockPlugWhenCharged    string `json:"autoUnlockPlugWhenCharged"`
+	MaxChargeCurrentAc           string `json:"maxChargeCurrentAc"`
+	TargetStateOfChargeInPercent int    `json:"targetStateOfChargeInPercent"`
+}
+
+// ChargerResponse is the /v2/air-conditioning/<vin> api
+type ClimaterResponse struct {
+	State                  string `json:"state"`
+	ChargerConnectionState string `json:"chargerConnectionState"`
+	ChargerLockState       string `json:"chargerLockState"`
+}


Test patch:
diff --git a/charger/eebus/eebus_test.go b/charger/eebus/eebus_test.go
new file mode 100644
index 00000000..f4dc83ab
--- /dev/null
+++ b/charger/eebus/eebus_test.go
@@ -0,0 +1,25 @@
+package eebus
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/require"
+	"gopkg.in/yaml.v3"
+)
+
+func TestConfig(t *testing.T) {
+	conf := `
+certificate:
+  private: |
+    -----BEGIN EC PRIVATE KEY-----
+    MHcCfoo==
+    -----END EC PRIVATE KEY-----
+  public: |
+    -----BEGIN CERTIFICATE-----
+    MIIBbar=
+    -----END CERTIFICATE-----
+`
+
+	var res Config
+	require.NoError(t, yaml.Unmarshal([]byte(conf), &res))
+}
diff --git a/cmd/setup_circuits_test.go b/cmd/setup_circuits_test.go
index 58efe0d1..bba5cec4 100644
--- a/cmd/setup_circuits_test.go
+++ b/cmd/setup_circuits_test.go
@@ -6,10 +6,7 @@ import (
 
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/api/globalconfig"
-	"github.com/evcc-io/evcc/core"
-	"github.com/evcc-io/evcc/meter"
 	"github.com/evcc-io/evcc/server/db"
-	"github.com/evcc-io/evcc/tariff"
 	"github.com/evcc-io/evcc/util/config"
 	"github.com/stretchr/testify/suite"
 	"go.uber.org/mock/gomock"
@@ -39,7 +36,8 @@ func (suite *circuitsTestSuite) TestCircuitConf() {
 	var conf globalconfig.All
 	viper.SetConfigType("yaml")
 
-	suite.Require().NoError(viper.ReadConfig(strings.NewReader(`circuits:
+	suite.Require().NoError(viper.ReadConfig(strings.NewReader(`
+circuits:
 - name: master
   maxPower: 10000
 - name: slave
@@ -63,11 +61,43 @@ loadpoints:
 
 	lps, err := configureLoadpoints(conf)
 	suite.Require().NoError(err)
-	suite.Require().Len(lps, 1)
 	suite.Require().NotNil(lps[0].GetCircuit())
 }
 
-func (suite *circuitsTestSuite) TestLoadpointMissingCircuitError() {
+func (suite *circuitsTestSuite) TestCircuitMissingLoadpoint() {
+	var conf globalconfig.All
+	viper.SetConfigType("yaml")
+
+	suite.Require().NoError(viper.ReadConfig(strings.NewReader(`
+circuits:
+- name: master
+- name: slave
+  parent: master
+loadpoints:
+- charger: test
+`)))
+
+	suite.Require().NoError(viper.UnmarshalExact(&conf))
+
+	suite.Require().NoError(configureCircuits(conf.Circuits))
+	suite.Require().Len(config.Circuits().Devices(), 2)
+	suite.Require().False(config.Circuits().Devices()[0].Instance().HasMeter())
+
+	// empty charger
+	suite.Require().NoError(config.Chargers().Add(config.NewStaticDevice(config.Named{
+		Name: "test",
+	}, api.Charger(nil))))
+
+	lps, err := configureLoadpoints(conf)
+	suite.Require().NoError(err)
+
+	// circuit without device
+	err = validateCircuits(lps)
+	suite.Require().Error(err)
+	suite.Require().Equal("circuit slave has no meter and no loadpoint assigned", err.Error())
+}
+
+func (suite *circuitsTestSuite) TestMissingRootCircuit() {
 	var conf globalconfig.All
 	viper.SetConfigType("yaml")
 
@@ -94,47 +124,50 @@ loadpoints:
 	lps, err := configureLoadpoints(conf)
 	suite.Require().NoError(err)
 
-	site := core.NewSite()
-	circuit.EXPECT().HasMeter().Return(false)
-	suite.Require().Error(validateCircuits(site, lps))
+	// root circuit
+	circuit.EXPECT().GetParent().Return(nil)
+	circuit.EXPECT().HasMeter().Return(true)
+	suite.Require().NoError(validateCircuits(lps))
+
+	// no root circuit
+	circuit.EXPECT().GetParent().Return(circuit)
+	// circuit.EXPECT().HasMeter().Return(true)
+	err = validateCircuits(lps)
+	suite.Require().Error(err)
+	suite.Require().Equal("missing root circuit", err.Error())
 }
 
-func (suite *circuitsTestSuite) TestSiteMissingCircuitError() {
+func (suite *circuitsTestSuite) TestLoadpointUsingRootCircuit() {
 	var conf globalconfig.All
 	viper.SetConfigType("yaml")
 
 	suite.Require().NoError(viper.ReadConfig(strings.NewReader(`
 loadpoints:
 - charger: test
-site:
-  meters:
-    grid: grid
+  circuit: root
 `)))
 
 	suite.Require().NoError(viper.UnmarshalExact(&conf))
 
-	lps := []*core.Loadpoint{
-		new(core.Loadpoint),
-	}
+	ctrl := gomock.NewController(suite.T())
+	circuit := api.NewMockCircuit(ctrl)
 
 	// mock circuit
 	suite.Require().NoError(config.Circuits().Add(config.NewStaticDevice(config.Named{
-		Name: "test",
-	}, api.Circuit(nil))))
-
-	// mock meter
-	m, _ := meter.NewConfigurable(func() (float64, error) {
-		return 0, nil
-	})
-	suite.Require().NoError(config.Meters().Add(config.NewStaticDevice(config.Named{
-		Name: "grid",
-	}, api.Meter(m))))
+		Name: "root",
+	}, api.Circuit(circuit))))
 
 	// mock charger
 	suite.Require().NoError(config.Chargers().Add(config.NewStaticDevice(config.Named{
 		Name: "test",
 	}, api.Charger(nil))))
 
-	_, err := configureSite(conf.Site, lps, new(tariff.Tariffs))
+	lps, err := configureLoadpoints(conf)
+	suite.Require().NoError(err)
+
+	// root circuit
+	circuit.EXPECT().GetParent().Return(nil)
+	err = validateCircuits(lps)
 	suite.Require().Error(err)
+	suite.Require().Equal("root circuit must not be assigned to loadpoint ", err.Error())
 }
diff --git a/core/circuit_test.go b/core/circuit_test.go
index 73404908..dbcd82a4 100644
--- a/core/circuit_test.go
+++ b/core/circuit_test.go
@@ -61,7 +61,7 @@ func TestCircuitPower(t *testing.T) {
 
 	circ := func(t *testing.T, ctrl *gomock.Controller, maxP float64) (*Circuit, *api.MockMeter) {
 		m := api.NewMockMeter(ctrl)
-		c, err := NewCircuit(log, "foo", 0, maxP, m)
+		c, err := NewCircuit(log, "foo", 0, maxP, m, 0)
 		require.NoError(t, err)
 		return c, m
 	}
@@ -100,7 +100,7 @@ func TestCircuitCurrents(t *testing.T) {
 			api.NewMockMeter(ctrl),
 			api.NewMockPhaseCurrents(ctrl),
 		}
-		c, err := NewCircuit(log, "foo", maxC, 0, m)
+		c, err := NewCircuit(log, "foo", maxC, 0, m, 0)
 		require.NoError(t, err)
 		return c, m
 	}
diff --git a/provider/http_test.go b/provider/http_test.go
index 593d2cbe..7316c6d7 100644
--- a/provider/http_test.go
+++ b/provider/http_test.go
@@ -3,13 +3,11 @@ package provider
 import (
 	"net/http"
 	"net/http/httptest"
-	"net/url"
 	"testing"
 
 	"github.com/evcc-io/evcc/util"
 	"github.com/samber/lo"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
+	"github.com/stretchr/testify/suite"
 )
 
 type httpHandler struct {
@@ -23,40 +21,54 @@ func (h *httpHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 	_, _ = w.Write([]byte(h.val))
 }
 
-func TestHttpGet(t *testing.T) {
-	h := new(httpHandler)
-	srv := httptest.NewServer(h)
-	defer srv.Close()
+func TestHttp(t *testing.T) {
+	suite.Run(t, new(httpTestSuite))
+}
 
-	uri := srv.URL + "/foo/bar"
-	p := NewHTTP(util.NewLogger("foo"), http.MethodGet, uri, false, 1, 0)
+type httpTestSuite struct {
+	suite.Suite
+	h   *httpHandler
+	srv *httptest.Server
+}
+
+func (suite *httpTestSuite) SetupSuite() {
+	suite.h = new(httpHandler)
+	suite.srv = httptest.NewServer(suite.h)
+}
+
+func (suite *httpTestSuite) TearDown() {
+	suite.srv.Close()
+}
 
-	uriUrl, _ := url.Parse(uri)
+func (suite *httpTestSuite) TestGet() {
+	uri := suite.srv.URL + "/foo/bar{{\"/baz\"}}"
+	p := NewHTTP(util.NewLogger("foo"), http.MethodGet, uri, false, 1, 0)
 
 	g, err := p.StringGetter()
-	require.NoError(t, err)
+	suite.Require().NoError(err)
 
 	res, err := g()
-	require.NoError(t, err)
-	assert.Equal(t, uriUrl.Path, h.req.URL.Path)
-	assert.Equal(t, h.val, res)
+	suite.Require().NoError(err)
+	suite.Require().Equal("/foo/bar/baz", suite.h.req.URL.String())
+	suite.Require().Equal(suite.h.val, res)
 }
 
-func TestHttpSet(t *testing.T) {
-	h := new(httpHandler)
-	srv := httptest.NewServer(h)
-	defer srv.Close()
-
-	uri := srv.URL + "/foo/bar?baz={{.baz}}"
+func (suite *httpTestSuite) TestSetQuery() {
+	uri := suite.srv.URL + "/foo/bar?baz={{.baz}}"
 	p := NewHTTP(util.NewLogger("foo"), http.MethodGet, uri, false, 1, 0)
 
-	uriUrl, _ := url.Parse(uri)
-
 	s, err := p.StringSetter("baz")
-	require.NoError(t, err)
+	suite.Require().NoError(err)
+	suite.Require().NoError(s("4711"))
+	suite.Require().Equal("/foo/bar?baz=4711", suite.h.req.URL.String())
+}
 
-	err = s("4711")
-	require.NoError(t, err)
-	assert.Equal(t, uriUrl.Path, h.req.URL.Path)
-	assert.Equal(t, "baz=4711", h.req.URL.RawQuery)
+func (suite *httpTestSuite) TestSetPath() {
+	uri := suite.srv.URL + "/foo/bar/{{.baz}}"
+	p := NewHTTP(util.NewLogger("foo"), http.MethodGet, uri, false, 1, 0)
+
+	s, err := p.StringSetter("baz")
+	suite.Require().NoError(err)
+	suite.Require().NoError(s("4711"))
+	suite.Require().Equal("/foo/bar/4711", suite.h.req.URL.String())
 }
diff --git a/tests/limits.spec.js b/tests/limits.spec.js
index 520cfcb3..2aa05895 100644
--- a/tests/limits.spec.js
+++ b/tests/limits.spec.js
@@ -88,8 +88,9 @@ test.describe("limitEnergy", async () => {
   test("survives a reload", async ({ page }) => {
     await page.goto("/");
 
-    await page.getByRole("button", { name: "blauer e-Golf" }).click();
-    await page.getByRole("button", { name: "grüner Honda e" }).click();
+    await expect(page.getByTestId("vehicle-status")).toHaveText("Connected.");
+
+    await page.getByTestId("change-vehicle").locator("select").selectOption("grüner Honda e");
 
     await expect(page.getByTestId("limit-energy-value")).toHaveText("none");
     await page.getByTestId("limit-energy").getByRole("combobox").selectOption("10 kWh (+35%)");
@@ -101,12 +102,10 @@ test.describe("limitEnergy", async () => {
   test("should not be reset on vehicle change", async ({ page }) => {
     await page.goto("/");
 
-    await page.getByRole("button", { name: "blauer e-Golf" }).click();
-    await page.getByRole("button", { name: "grüner Honda e" }).click();
+    await page.getByTestId("change-vehicle").locator("select").selectOption("grüner Honda e");
     await page.getByTestId("limit-energy").getByRole("combobox").selectOption("10 kWh (+35%)");
 
-    await page.getByRole("button", { name: "grüner Honda e" }).click();
-    await page.getByRole("button", { name: "Guest vehicle" }).click();
+    await page.getByTestId("change-vehicle").locator("select").selectOption("Guest vehicle");
     await expect(page.getByTestId("limit-energy-value")).toHaveText("10 kWh");
   });
 });
diff --git a/tests/plan.spec.js b/tests/plan.spec.js
index 2836339b..92020e30 100644
--- a/tests/plan.spec.js
+++ b/tests/plan.spec.js
@@ -39,8 +39,10 @@ test.describe("basic functionality", async () => {
     const lp1 = await page.getByTestId("loadpoint").first();
 
     // change vehicle
-    await lp1.getByRole("button", { name: "Guest vehicle" }).click();
-    await lp1.getByRole("button", { name: "Vehicle with SoC with Capacity" }).click();
+    await lp1
+      .getByTestId("change-vehicle")
+      .locator("select")
+      .selectOption("Vehicle with SoC with Capacity");
 
     await expect(lp1.getByTestId("plan-marker")).not.toBeVisible();
     await expect(lp1.getByText("Loadpoint", { exact: true })).toBeVisible();
@@ -77,8 +79,7 @@ test.describe("vehicle variations", async () => {
 
       const lp1 = await page.getByTestId("loadpoint").first();
 
-      // change vehicle
-      await expect(lp1.getByRole("button", { name: "Guest vehicle" })).toBeVisible();
+      await expect(lp1.getByTestId("vehicle-name")).toHaveText("Guest vehicle");
 
       // kWh based limit
       await lp1.getByTestId("limit-energy").getByRole("combobox").selectOption("50 kWh");
@@ -95,8 +96,10 @@ test.describe("vehicle variations", async () => {
       const lp1 = await page.getByTestId("loadpoint").first();
 
       // change vehicle
-      await lp1.getByRole("button", { name: "Guest vehicle" }).click();
-      await lp1.getByRole("button", { name: "Vehicle no SoC no Capacity" }).click();
+      await lp1
+        .getByTestId("change-vehicle")
+        .locator("select")
+        .selectOption("Vehicle no SoC no Capacity");
 
       // kWh based limit
       await lp1.getByTestId("limit-energy").getByRole("combobox").selectOption("50 kWh");
@@ -113,8 +116,10 @@ test.describe("vehicle variations", async () => {
       const lp1 = await page.getByTestId("loadpoint").first();
 
       // change vehicle
-      await lp1.getByRole("button", { name: "Guest vehicle" }).click();
-      await lp1.getByRole("button", { name: "Vehicle no SoC with Capacity" }).click();
+      await lp1
+        .getByTestId("change-vehicle")
+        .locator("select")
+        .selectOption("Vehicle no SoC with Capacity");
 
       // kWh based limit
       await lp1.getByTestId("limit-energy").getByRole("combobox").selectOption("50 kWh (+50%)");
@@ -131,8 +136,10 @@ test.describe("vehicle variations", async () => {
       const lp1 = await page.getByTestId("loadpoint").first();
 
       // change vehicle
-      await lp1.getByRole("button", { name: "Guest vehicle" }).click();
-      await lp1.getByRole("button", { name: "Vehicle with SoC no Capacity" }).click();
+      await lp1
+        .getByTestId("change-vehicle")
+        .locator("select")
+        .selectOption("Vehicle with SoC no Capacity");
 
       // soc based limit
       await lp1.getByTestId("limit-soc").getByRole("combobox").selectOption("80%");
@@ -149,8 +156,10 @@ test.describe("vehicle variations", async () => {
       const lp1 = await page.getByTestId("loadpoint").first();
 
       // change vehicle
-      await lp1.getByRole("button", { name: "Guest vehicle" }).click();
-      await lp1.getByRole("button", { name: "Vehicle with SoC with Capacity" }).click();
+      await lp1
+        .getByTestId("change-vehicle")
+        .locator("select")
+        .selectOption("Vehicle with SoC with Capacity");
 
       // soc based limit
       await lp1.getByTestId("limit-soc").getByRole("combobox").selectOption("80%");
@@ -167,7 +176,7 @@ test.describe("vehicle variations", async () => {
       const lp2 = await page.getByTestId("loadpoint").last();
 
       // change vehicle
-      await expect(lp2.getByRole("button", { name: "Guest vehicle" })).toBeVisible();
+      await expect(lp2.getByTestId("vehicle-name")).toHaveText("Guest vehicle");
 
       // soc based limit
       await lp2.getByTestId("limit-soc").getByRole("combobox").selectOption("80%");
@@ -184,8 +193,10 @@ test.describe("vehicle variations", async () => {
       const lp2 = await page.getByTestId("loadpoint").last();
 
       // change vehicle
-      await lp2.getByRole("button", { name: "Guest vehicle" }).click();
-      await lp2.getByRole("button", { name: "Vehicle no SoC with Capacity" }).click();
+      await lp2
+        .getByTestId("change-vehicle")
+        .locator("select")
+        .selectOption("Vehicle no SoC with Capacity");
 
       // soc based limit
       await lp2.getByTestId("limit-soc").getByRole("combobox").selectOption("80%");
@@ -217,8 +228,7 @@ test.describe("preview", async () => {
       const lp1 = await page.getByTestId("loadpoint").first();
 
       // change vehicle
-      await lp1.getByRole("button", { name: "Guest vehicle" }).click();
-      await lp1.getByRole("button", { name: c.vehicle }).click();
+      await lp1.getByTestId("change-vehicle").locator("select").selectOption(c.vehicle);
 
       await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
 
@@ -258,8 +268,11 @@ test.describe("warnings", async () => {
     const lp1 = await page.getByTestId("loadpoint").first();
 
     // change vehicle
-    await lp1.getByRole("button", { name: "Guest vehicle" }).click();
-    await lp1.getByRole("button", { name: "Vehicle with SoC with massive Capacity" }).click();
+    await lp1
+      .getByTestId("change-vehicle")
+      .locator("select")
+      .selectOption("Vehicle with SoC with Massive Capacity");
+
     await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
 
     await page.getByTestId("plan-active").click();
@@ -274,8 +287,11 @@ test.describe("warnings", async () => {
     const lp1 = await page.getByTestId("loadpoint").first();
 
     // change vehicle
-    await lp1.getByRole("button", { name: "Guest vehicle" }).click();
-    await lp1.getByRole("button", { name: "Vehicle with SoC with Capacity" }).click();
+    await lp1
+      .getByTestId("change-vehicle")
+      .locator("select")
+      .selectOption("Vehicle with SoC with Capacity");
+
     await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
 
     await expect(page.getByTestId("plan-entry-warnings")).not.toBeVisible();
diff --git a/tests/vehicle-error.spec.js b/tests/vehicle-error.spec.js
index 9a58a80a..205cdcf8 100644
--- a/tests/vehicle-error.spec.js
+++ b/tests/vehicle-error.spec.js
@@ -22,8 +22,7 @@ test.describe("vehicle startup error", async () => {
 
   test("guest vehicle: normal title and no icon", async ({ page }) => {
     // switch to offline vehicle
-    await page.getByRole("button", { name: "Broken Tesla" }).click();
-    await page.getByRole("button", { name: "Guest vehicle" }).click();
+    await page.getByTestId("change-vehicle").locator("select").selectOption("Guest vehicle");
 
     await expect(page.getByTestId("vehicle-name")).toHaveText("Guest vehicle");
     await expect(page.getByTestId("vehicle-not-reachable-icon")).not.toBeVisible();
diff --git a/tests/vehicle-settings.spec.js b/tests/vehicle-settings.spec.js
index f5fed1d4..42940af6 100644
--- a/tests/vehicle-settings.spec.js
+++ b/tests/vehicle-settings.spec.js
@@ -91,8 +91,7 @@ test.describe("minSoc and limitSoc", async () => {
     await page.goto("/");
 
     // switch to offline vehicle
-    await page.getByRole("button", { name: "blauer e-Golf" }).click();
-    await page.getByRole("button", { name: "grüner Honda e" }).click();
+    await page.getByTestId("change-vehicle").locator("select").selectOption("grüner Honda e");
 
     await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
     await page.getByRole("link", { name: "Arrival" }).click();
@@ -104,8 +103,7 @@ test.describe("minSoc and limitSoc", async () => {
     await page.goto("/");
 
     // switch to offline vehicle
-    await page.getByRole("button", { name: "blauer e-Golf" }).click();
-    await page.getByRole("button", { name: "Guest vehicle" }).click();
+    await page.getByTestId("change-vehicle").locator("select").selectOption("Guest vehicle");
 
     await page.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
     await page.getByRole("link", { name: "Arrival" }).click();
diff --git a/util/decoder_test.go b/util/decoder_test.go
index 7f34b769..eb177625 100644
--- a/util/decoder_test.go
+++ b/util/decoder_test.go
@@ -3,7 +3,7 @@ package util
 import (
 	"testing"
 
-	"github.com/mitchellh/mapstructure"
+	"github.com/go-viper/mapstructure/v2"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 )