Repository: evcc-io/evcc
Pull request #11283 (2023-12-27 11:27:38)

Show "No vehicle" instead of "Guest vehicle" when loadpoint is disconnected
Fix https://github.com/evcc-io/evcc/issues/11049

@naltatis Test wäre sicher noch gut- den Teil durchschaue ich leider nicht :/

Mir fällt noch auf, dass der Import hier ohne .vue erfolgt:

> import VehicleIcon from "./VehicleIcon";



Patch:
diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml
index 825810b3..a98869a9 100644
--- a/.github/workflows/default.yml
+++ b/.github/workflows/default.yml
@@ -18,7 +18,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      # - uses: actions/setup-go@v4
+      # - uses: actions/setup-go@v5
       - uses: erezrokah/setup-go@feat/add_cache_prefix
         with:
           go-version: ${{ env.GO_VERSION }}
@@ -70,7 +70,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      # - uses: actions/setup-go@v4
+      # - uses: actions/setup-go@v5
       - uses: erezrokah/setup-go@feat/add_cache_prefix
         with:
           go-version: ${{ env.GO_VERSION }}
@@ -87,7 +87,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      - uses: actions/setup-go@v4
+      - uses: actions/setup-go@v5
         with:
           go-version: ${{ env.GO_VERSION }}
           cache: false # avoid cache thrashing
@@ -166,7 +166,7 @@ jobs:
       - name: Run tests
         run: npx playwright test
 
-      - uses: actions/upload-artifact@v3
+      - uses: actions/upload-artifact@v4
         if: always()
         with:
           name: playwright-report
diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml
index 8d88aad2..b59d70c7 100644
--- a/.github/workflows/documentation.yml
+++ b/.github/workflows/documentation.yml
@@ -17,7 +17,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      - uses: actions/setup-go@v4
+      - uses: actions/setup-go@v5
         with:
           go-version: ${{ env.GO_VERSION }}
         id: go
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index 432535d2..ec5e228e 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -82,7 +82,7 @@ jobs:
         with:
           fetch-depth: 0
 
-      - uses: actions/setup-go@v4
+      - uses: actions/setup-go@v5
         with:
           go-version: ${{ env.GO_VERSION }}
         id: go
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 7e6c2f47..b0e803c3 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -61,7 +61,7 @@ jobs:
           fetch-depth: 0
 
       - name: Setup Go
-        uses: actions/setup-go@v4
+        uses: actions/setup-go@v5
         with:
           go-version: ${{ env.GO_VERSION }}
         id: go
diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml
index 3496a24e..988724ae 100644
--- a/.github/workflows/stale.yaml
+++ b/.github/workflows/stale.yaml
@@ -9,7 +9,7 @@ jobs:
   stale:
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/stale@v8
+      - uses: actions/stale@v9
         id: stale
         with:
           days-before-stale: 20
diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml
index c23d1757..87ab8ad4 100644
--- a/.github/workflows/website.yml
+++ b/.github/workflows/website.yml
@@ -6,7 +6,7 @@ env:
 on:
   push:
     branches: [master]
-    paths: ["templates/evcc.io/**"]
+    paths: ["templates/**"]
   workflow_dispatch:
 
 jobs:
@@ -17,7 +17,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      - uses: actions/setup-go@v4
+      - uses: actions/setup-go@v5
         with:
           go-version: ${{ env.GO_VERSION }}
         id: go
@@ -25,6 +25,9 @@ jobs:
       - name: Build docs
         run: make install docs
 
+      - name: Remove .gitignore to allow brands.json to be committed
+        run: rm templates/evcc.io/.gitignore
+
       - name: Deploy to evcc.io repo
         uses: peaceiris/actions-gh-pages@v3
         with:
diff --git a/api/api.go b/api/api.go
index 8568b12c..7cdcba1e 100644
--- a/api/api.go
+++ b/api/api.go
@@ -9,7 +9,7 @@ import (
 	"time"
 )
 
-//go:generate mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,PhaseSwitcher,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery,Tariff,BatteryController
+//go:generate mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery,Tariff,BatteryController
 
 // ChargeMode is the charge operation mode. Valid values are off, now, minpv and pv
 type ChargeMode string
diff --git a/api/mock.go b/api/mock.go
index fbab5af0..0fb7eb35 100644
--- a/api/mock.go
+++ b/api/mock.go
@@ -1,5 +1,5 @@
 // Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,PhaseSwitcher,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery,Tariff,BatteryController)
+// Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery,Tariff,BatteryController)
 
 // Package api is a generated GoMock package.
 package api
@@ -129,6 +129,45 @@ func (mr *MockChargeStateMockRecorder) Status() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Status", reflect.TypeOf((*MockChargeState)(nil).Status))
 }
 
+// MockCurrentLimiter is a mock of CurrentLimiter interface.
+type MockCurrentLimiter struct {
+	ctrl     *gomock.Controller
+	recorder *MockCurrentLimiterMockRecorder
+}
+
+// MockCurrentLimiterMockRecorder is the mock recorder for MockCurrentLimiter.
+type MockCurrentLimiterMockRecorder struct {
+	mock *MockCurrentLimiter
+}
+
+// NewMockCurrentLimiter creates a new mock instance.
+func NewMockCurrentLimiter(ctrl *gomock.Controller) *MockCurrentLimiter {
+	mock := &MockCurrentLimiter{ctrl: ctrl}
+	mock.recorder = &MockCurrentLimiterMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockCurrentLimiter) EXPECT() *MockCurrentLimiterMockRecorder {
+	return m.recorder
+}
+
+// GetMinMaxCurrent mocks base method.
+func (m *MockCurrentLimiter) GetMinMaxCurrent() (float64, float64, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetMinMaxCurrent")
+	ret0, _ := ret[0].(float64)
+	ret1, _ := ret[1].(float64)
+	ret2, _ := ret[2].(error)
+	return ret0, ret1, ret2
+}
+
+// GetMinMaxCurrent indicates an expected call of GetMinMaxCurrent.
+func (mr *MockCurrentLimiterMockRecorder) GetMinMaxCurrent() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMinMaxCurrent", reflect.TypeOf((*MockCurrentLimiter)(nil).GetMinMaxCurrent))
+}
+
 // MockPhaseSwitcher is a mock of PhaseSwitcher interface.
 type MockPhaseSwitcher struct {
 	ctrl     *gomock.Controller
diff --git a/assets/js/components/BatterySettingsModal.vue b/assets/js/components/BatterySettingsModal.vue
index ed8816d8..e08fb930 100644
--- a/assets/js/components/BatterySettingsModal.vue
+++ b/assets/js/components/BatterySettingsModal.vue
@@ -216,40 +216,35 @@
 										</small>
 									</span>
 								</p>
-								<p>
-									<small>
-										{{ $t("batterySettings.note") }}
-									</small>
+								<p class="small text-muted">
+									<strong class="text-evcc">
+										{{ $t("batterySettings.disclaimerHint") }}
+									</strong>
+									{{ $t("batterySettings.disclaimerText") }}
 								</p>
 							</div>
 						</div>
-						<div v-if="$hiddenFeatures()">
-							<FormRow
-								v-if="controllable"
-								id="batteryDischargeControl"
-								:label="`${$t('batterySettings.control')}`"
-							>
-								<div class="form-check form-switch col-form-label">
-									<input
-										id="batteryDischargeControl"
-										:checked="batteryDischargeControl"
-										class="form-check-input"
-										type="checkbox"
-										role="switch"
-										@change="changeDischargeControl"
-									/>
-									<div class="form-check-label">
-										<label for="batteryDischargeControl">
-											{{ $t("batterySettings.discharge") }}
-											<span title="experimental">🧪</span>
-										</label>
-									</div>
+						<FormRow
+							v-if="controllable"
+							id="batteryDischargeControl"
+							:label="`${$t('batterySettings.control')}`"
+						>
+							<div class="form-check form-switch col-form-label">
+								<input
+									id="batteryDischargeControl"
+									:checked="batteryDischargeControl"
+									class="form-check-input"
+									type="checkbox"
+									role="switch"
+									@change="changeDischargeControl"
+								/>
+								<div class="form-check-label">
+									<label for="batteryDischargeControl">
+										{{ $t("batterySettings.discharge") }}
+									</label>
 								</div>
-							</FormRow>
-							<p v-else>
-								<small>{{ $t("batterySettings.notControllable") }}</small>
-							</p>
-						</div>
+							</div>
+						</FormRow>
 					</div>
 				</div>
 			</div>
diff --git a/assets/js/components/ChargingPlan.vue b/assets/js/components/ChargingPlan.vue
index 99d51187..1ac12682 100644
--- a/assets/js/components/ChargingPlan.vue
+++ b/assets/js/components/ChargingPlan.vue
@@ -43,7 +43,9 @@
 						<div class="modal-header">
 							<h5 class="modal-title">
 								{{ $t("main.chargingPlan.modalTitle")
-								}}<span v-if="socBasedPlanning">: {{ vehicle.title }}</span>
+								}}<span v-if="socBasedPlanning && vehicle"
+									>: {{ vehicle.title }}</span
+								>
 							</h5>
 							<button
 								type="button"
diff --git a/assets/js/components/ChargingPlanArrival.vue b/assets/js/components/ChargingPlanArrival.vue
index a7a5b727..a42f09e5 100644
--- a/assets/js/components/ChargingPlanArrival.vue
+++ b/assets/js/components/ChargingPlanArrival.vue
@@ -1,7 +1,4 @@
 <template>
-	<div v-if="!socBasedCharging" class="alert alert-secondary my-4" role="alert">
-		{{ $t("main.loadpointSettings.onlyForSocBasedCharging") }}
-	</div>
 	<div class="mt-4 container">
 		<div class="row">
 			<div class="col-6 col-lg-3 col-form-label">
@@ -50,6 +47,12 @@
 			</small>
 		</div>
 	</div>
+	<div v-if="!socBasedCharging" class="mx-2 small text-muted">
+		<strong class="text-evcc">
+			{{ $t("main.loadpointSettings.disclaimerHint") }}
+		</strong>
+		{{ $t("main.loadpointSettings.onlyForSocBasedCharging") }}
+	</div>
 </template>
 
 <script>
diff --git a/assets/js/components/ChargingSessionModal.vue b/assets/js/components/ChargingSessionModal.vue
index a51a177f..405a0306 100644
--- a/assets/js/components/ChargingSessionModal.vue
+++ b/assets/js/components/ChargingSessionModal.vue
@@ -39,6 +39,7 @@
 											class="options"
 											:vehicles="vehicles"
 											:is-unknown="false"
+											connected
 											@change-vehicle="changeVehicle"
 											@remove-vehicle="removeVehicle"
 										>
diff --git a/assets/js/components/VehicleOptions.vue b/assets/js/components/VehicleOptions.vue
index 7b95649d..e86ebf11 100644
--- a/assets/js/components/VehicleOptions.vue
+++ b/assets/js/components/VehicleOptions.vue
@@ -21,7 +21,8 @@
 			</li>
 			<li v-if="!isUnknown">
 				<button type="button" class="dropdown-item" @click="removeVehicle()">
-					{{ $t("main.vehicle.unknown") }}
+					<span v-if="connected">{{ $t("main.vehicle.unknown") }}</span>
+					<span v-else>{{ $t("main.vehicle.none") }}</span>
 				</button>
 			</li>
 		</ul>
@@ -35,6 +36,7 @@ import Dropdown from "bootstrap/js/dist/dropdown";
 export default {
 	name: "VehicleOptions",
 	props: {
+		connected: Boolean,
 		id: [String, Number],
 		vehicles: Array,
 		isUnknown: Boolean,
diff --git a/assets/js/components/VehicleTitle.vue b/assets/js/components/VehicleTitle.vue
index fe3c397f..4b788f49 100644
--- a/assets/js/components/VehicleTitle.vue
+++ b/assets/js/components/VehicleTitle.vue
@@ -19,6 +19,7 @@
 			></shopicon-regular-cablecharge>
 			<VehicleOptions
 				v-if="showOptions"
+				v-bind="vehicleOptionsProps"
 				:id="id"
 				class="options"
 				:vehicles="otherVehicles"
@@ -40,14 +41,15 @@
 <script>
 import "@h2d2/shopicons/es/regular/refresh";
 import "@h2d2/shopicons/es/regular/cablecharge";
-import VehicleIcon from "./VehicleIcon";
 import Tooltip from "bootstrap/js/dist/tooltip";
-
+import VehicleIcon from "./VehicleIcon";
 import VehicleOptions from "./VehicleOptions.vue";
+import collector from "../mixins/collector";
 
 export default {
 	name: "VehicleTitle",
 	components: { VehicleOptions, VehicleIcon },
+	mixins: [collector],
 	props: {
 		connected: Boolean,
 		id: [String, Number],
@@ -87,6 +89,9 @@ export default {
 		showOptions() {
 			return !this.isUnknown || this.vehicles.length;
 		},
+		vehicleOptionsProps: function () {
+			return this.collectProps(VehicleOptions);
+		},
 	},
 	watch: {
 		icon: function () {
diff --git a/charger/config.go b/charger/config.go
index 9616d86e..0c0617ea 100644
--- a/charger/config.go
+++ b/charger/config.go
@@ -19,7 +19,7 @@ func (r chargerRegistry) Add(name string, factory func(map[string]interface{}) (
 func (r chargerRegistry) Get(name string) (func(map[string]interface{}) (api.Charger, error), error) {
 	factory, exists := r[name]
 	if !exists {
-		return nil, fmt.Errorf("charger type not registered: %s", name)
+		return nil, fmt.Errorf("invalid charger type: %s", name)
 	}
 	return factory, nil
 }
@@ -27,15 +27,16 @@ func (r chargerRegistry) Get(name string) (func(map[string]interface{}) (api.Cha
 var registry chargerRegistry = make(map[string]func(map[string]interface{}) (api.Charger, error))
 
 // NewFromConfig creates charger from configuration
-func NewFromConfig(typ string, other map[string]interface{}) (v api.Charger, err error) {
+func NewFromConfig(typ string, other map[string]interface{}) (api.Charger, error) {
 	factory, err := registry.Get(strings.ToLower(typ))
-	if err == nil {
-		if v, err = factory(other); err != nil {
-			err = fmt.Errorf("cannot create charger '%s': %w", typ, err)
-		}
-	} else {
-		err = fmt.Errorf("invalid charger type: %s", typ)
+	if err != nil {
+		return nil, err
 	}
 
-	return
+	v, err := factory(other)
+	if err != nil {
+		err = fmt.Errorf("cannot create charger type '%s': %w", typ, err)
+	}
+
+	return v, err
 }
diff --git a/charger/delta.go b/charger/delta.go
new file mode 100644
index 00000000..cb971510
--- /dev/null
+++ b/charger/delta.go
@@ -0,0 +1,291 @@
+package charger
+
+import (
+	"fmt"
+	"math"
+	"sync"
+	"time"
+
+	"github.com/evcc-io/evcc/api"
+	"github.com/evcc-io/evcc/core/loadpoint"
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/modbus"
+	"github.com/evcc-io/evcc/util/sponsor"
+	"github.com/volkszaehler/mbmd/encoding"
+)
+
+// Delta charger implementation
+type Delta struct {
+	log     *util.Logger
+	conn    *modbus.Connection
+	lp      loadpoint.API
+	mu      sync.Mutex
+	curr    float64
+	base    uint16
+	enabled bool
+}
+
+const (
+	// EV Charger
+	// Read Input Registers (0x04)
+	deltaRegState   = 100 // Charger State - UINT16 0: not ready, 1: operational, 10: faulted, 255: not responding
+	deltaRegVersion = 101 // Charger Version - UINT16
+	deltaRegCount   = 102 // Charger EVSE Count - UINT16
+	deltaRegError   = 103 // Charger Error - UINT16
+	deltaRegSerial  = 110 // Charger Serial - STRING20
+	deltaRegModel   = 130 // Charger Model - STRING20
+
+	// Write Multiple Registers (0x10)
+	deltaRegCommunicationTimeoutEnabled = 201 // Communication Timeout Enabled 0/1
+	deltaRegCommunicationTimeout        = 202 // Communication Timeout [s]
+	deltaRegFallbackPower               = 203 // Fallback Power [W]
+
+	// EVSE - The following Register tables are defined as repeating blocks for each single EVSE
+	// Read Input Registers (0x04)
+	deltaRegEvseState                 = 0   // EVSE State - UINT16 0: Unavailable, 1: Available, 2: Occupied, 3: Preparing, 4: Charging, 5: Finishing, 6: Suspended EV, 7: Suspended EVSE, 8: Not ready, 9: Faulted
+	deltaRegEvseChargerState          = 1   // EVSE Charger State - UINT16 0: Charging process not started (no vehicle connected), 1: Connected, waiting for release (by RFID or local), 2: Charging process starts, 3: Charging, 4: Suspended (paused), 5: Charging process successfully completed (vehicle still plugged in), 6: Charging process completed by user (vehicle still plugged in), 7: Charging ended with error (vehicle still connected)
+	deltaRegEvseActualOutputVoltage   = 3   // EVSE Actual Output Voltage - FLOAT32 [V]
+	deltaRegEvseActualChargingPower   = 5   // EVSE Actual Charging Power - UINT32 [W]
+	deltaRegEvseActualChargingCurrent = 7   // EVSE Actual Charging Current - FLOAT32 [A]
+	deltaRegEvseActualOutputPower     = 9   // EVSE Actual Output Power - FLOAT32 [W]
+	deltaRegEvseSoc                   = 11  // EVSE SOC [%/10]
+	deltaRegEvseChargingTime          = 17  // EVSE Charging Time [s]
+	deltaRegEvseChargedEnergy         = 19  // EVSE Charged Energy [Wh]
+	deltaRegEvseRfidUID               = 100 // EVSE Used Authentication ID - STRING
+
+	// Write Multiple Registers (0x10)
+	deltaRegEvseChargingPowerLimit = 600 // EVSE Charging Power Limit - UINT32 [W]
+	deltaRegEvseSuspendCharging    = 602 // EVSE Suspend Charging - UINT16 - 0: no pause, 1 charging pause (lock on)
+)
+
+func init() {
+	registry.Add("delta", NewDeltaFromConfig)
+}
+
+// NewDeltaFromConfig creates a Delta charger from generic config
+func NewDeltaFromConfig(other map[string]interface{}) (api.Charger, error) {
+	cc := struct {
+		Connector       uint16
+		modbus.Settings `mapstructure:",squash"`
+	}{
+		Connector: 1,
+		Settings: modbus.Settings{
+			ID: 1,
+		},
+	}
+
+	if err := util.DecodeOther(other, &cc); err != nil {
+		return nil, err
+	}
+
+	return NewDelta(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.ProtocolFromRTU(cc.RTU), cc.ID, cc.Connector)
+}
+
+// NewDelta creates Delta charger
+func NewDelta(uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8, connector uint16) (api.Charger, error) {
+	conn, err := modbus.NewConnection(uri, device, comset, baudrate, proto, slaveID)
+	if err != nil {
+		return nil, err
+	}
+
+	if !sponsor.IsAuthorized() {
+		return nil, api.ErrSponsorRequired
+	}
+
+	log := util.NewLogger("delta")
+	conn.Logger(log.TRACE)
+
+	wb := &Delta{
+		log:  log,
+		conn: conn,
+		curr: 6000, // assume min current
+	}
+
+	wb.base = connector * 1000
+
+	// get failsafe timeout from charger
+	b, err := wb.conn.ReadHoldingRegisters(deltaRegCommunicationTimeout, 1)
+	if err != nil {
+		return nil, fmt.Errorf("failsafe timeout: %w", err)
+	}
+	if u := encoding.Uint16(b); u > 0 {
+		go wb.heartbeat(time.Duration(u) * time.Second / 2)
+	}
+
+	return wb, err
+}
+
+func (wb *Delta) heartbeat(timeout time.Duration) {
+	for range time.Tick(timeout) {
+		wb.mu.Lock()
+		var curr float64
+		if wb.enabled {
+			curr = wb.curr
+		}
+		wb.mu.Unlock()
+		if err := wb.setCurrent(curr); err != nil {
+			wb.log.ERROR.Println("heartbeat:", err)
+		}
+	}
+}
+
+// Status implements the api.Charger interface
+func (wb *Delta) Status() (api.ChargeStatus, error) {
+	b, err := wb.conn.ReadInputRegisters(wb.base+deltaRegEvseChargerState, 1)
+	if err != nil {
+		return api.StatusNone, err
+	}
+
+	// 0: Charging process not started (no vehicle connected)
+	// 1: Connected, waiting for release (by RFID or local)
+	// 2: Charging process starts
+	// 3: Charging
+	// 4: Suspended (loading paused)
+	// 5: Charging process successfully completed (vehicle still plugged in)
+	// 6: Charging process completed by user (vehicle still plugged in)
+	// 7: Charging ended with error (vehicle still connected)
+	switch s := encoding.Uint16(b); s {
+	case 0:
+		return api.StatusA, nil
+	case 1, 2, 4, 5, 6, 7:
+		return api.StatusB, nil
+	case 3:
+		return api.StatusC, nil
+	default:
+		return api.StatusNone, fmt.Errorf("invalid status: %0x", s)
+	}
+}
+
+// Enabled implements the api.Charger interface
+func (wb *Delta) Enabled() (bool, error) {
+	b, err := wb.conn.ReadHoldingRegisters(wb.base+deltaRegEvseChargingPowerLimit, 2)
+	if err != nil {
+		return false, err
+	}
+
+	return encoding.Uint32(b) > 0, nil
+}
+
+// Enable implements the api.Charger interface
+func (wb *Delta) Enable(enable bool) error {
+	var curr float64
+	if enable {
+		wb.mu.Lock()
+		curr = wb.curr
+		wb.mu.Unlock()
+	}
+
+	err := wb.setCurrent(curr)
+	if err == nil {
+		wb.mu.Lock()
+		wb.enabled = enable
+		wb.mu.Unlock()
+	}
+
+	return err
+}
+
+// setCurrent writes the current limit in A
+func (wb *Delta) setCurrent(current float64) error {
+	activePhases := 3
+	if wb.lp != nil {
+		activePhases = wb.lp.ActivePhases()
+	}
+
+	b := make([]byte, 4)
+	encoding.PutUint32(b, uint32(math.Trunc(230.0*current*float64(activePhases))))
+
+	_, err := wb.conn.WriteMultipleRegisters(wb.base+deltaRegEvseChargingPowerLimit, 2, b)
+
+	return err
+}
+
+// MaxCurrent implements the api.Charger interface
+func (wb *Delta) MaxCurrent(current int64) error {
+	return wb.MaxCurrentMillis(float64(current))
+}
+
+var _ api.ChargerEx = (*Delta)(nil)
+
+// MaxCurrentMillis implements the api.ChargerEx interface
+func (wb *Delta) MaxCurrentMillis(current float64) error {
+	if current < 6 {
+		return fmt.Errorf("invalid current %.1f", current)
+	}
+
+	err := wb.setCurrent(current)
+	if err == nil {
+		wb.mu.Lock()
+		wb.curr = current
+		wb.mu.Unlock()
+	}
+
+	return err
+}
+
+var _ api.Meter = (*Delta)(nil)
+
+// CurrentPower implements the api.Meter interface
+func (wb *Delta) CurrentPower() (float64, error) {
+	b, err := wb.conn.ReadInputRegisters(wb.base+deltaRegEvseActualChargingPower, 2)
+	if err != nil {
+		return 0, err
+	}
+
+	return float64(encoding.Uint32(b)), err
+}
+
+var _ api.ChargeRater = (*Delta)(nil)
+
+// ChargedEnergy implements the api.ChargeRater interface
+func (wb *Delta) ChargedEnergy() (float64, error) {
+	b, err := wb.conn.ReadInputRegisters(wb.base+deltaRegEvseChargedEnergy, 2)
+	if err != nil {
+		return 0, err
+	}
+
+	return float64(encoding.Uint32(b)) / 1e3, err
+}
+
+var _ api.Identifier = (*Delta)(nil)
+
+// Identify implements the api.Identifier interface
+func (wb *Delta) Identify() (string, error) {
+	b, err := wb.conn.ReadInputRegisters(wb.base+deltaRegEvseRfidUID, 6)
+	if err != nil {
+		return "", err
+	}
+
+	return bytesAsString(b), nil
+}
+
+var _ api.Diagnosis = (*Delta)(nil)
+
+// Diagnose implements the api.Diagnosis interface
+func (wb *Delta) Diagnose() {
+	if b, err := wb.conn.ReadInputRegisters(deltaRegState, 1); err == nil {
+		fmt.Printf("\tState:\t%d\n", encoding.Uint16(b))
+	}
+	if b, err := wb.conn.ReadInputRegisters(deltaRegVersion, 1); err == nil {
+		fmt.Printf("\tVersion:\t%d\n", encoding.Uint16(b))
+	}
+	if b, err := wb.conn.ReadInputRegisters(deltaRegCount, 1); err == nil {
+		fmt.Printf("\tEVSE Count:\t%d\n", encoding.Uint16(b))
+	}
+	if b, err := wb.conn.ReadInputRegisters(deltaRegError, 1); err == nil {
+		fmt.Printf("\tError:\t%d\n", encoding.Uint16(b))
+	}
+	if b, err := wb.conn.ReadInputRegisters(deltaRegSerial, 20); err == nil {
+		fmt.Printf("\tSerial:\t%s\n", bytesAsString(b))
+	}
+	if b, err := wb.conn.ReadInputRegisters(deltaRegModel, 20); err == nil {
+		fmt.Printf("\tModel:\t%s\n", bytesAsString(b))
+	}
+}
+
+var _ loadpoint.Controller = (*Delta)(nil)
+
+// LoadpointControl implements loadpoint.Controller
+func (wb *Delta) LoadpointControl(lp loadpoint.API) {
+	wb.lp = lp
+}
diff --git a/charger/huawei-ac.go b/charger/huawei-ac.go
index d41b1d65..def40557 100644
--- a/charger/huawei-ac.go
+++ b/charger/huawei-ac.go
@@ -84,13 +84,9 @@ func NewHuaweiACCharger(uri string, slaveID uint8) (api.Charger, error) {
 }
 
 func (wb *HuaweiACCharger) setCurrent(current float64) error {
-	var phases int
-	// get (expectedly) active phases from loadpoint
+	phases := 3
 	if wb.lp != nil {
-		phases = wb.lp.GetPhases()
-	}
-	if phases == 0 {
-		phases = 3
+		phases = wb.lp.ActivePhases()
 	}
 
 	b := make([]byte, 4)
diff --git a/charger/openwb-2.0.go b/charger/openwb-2.0.go
index 8e96d6df..224d5aa0 100644
--- a/charger/openwb-2.0.go
+++ b/charger/openwb-2.0.go
@@ -182,7 +182,7 @@ func (wb *OpenWB20) getPhaseValues(reg uint16) (float64, float64, float64, error
 
 	var res [3]float64
 	for i := range res {
-		res[i] = float64(binary.BigEndian.Uint16(b[2*i:])) / 100
+		res[i] = float64(int16(binary.BigEndian.Uint16(b[2*i:]))) / 100
 	}
 
 	return res[0], res[1], res[2], nil
diff --git a/charger/phoenix-charx.go b/charger/phoenix-charx.go
index 2ae37c03..7b3357cb 100644
--- a/charger/phoenix-charx.go
+++ b/charger/phoenix-charx.go
@@ -212,22 +212,17 @@ func (wb *PhoenixCharx) totalEnergy() (float64, error) {
 
 // currents implements the api.PhaseCurrents interface
 func (wb *PhoenixCharx) currents() (float64, float64, float64, error) {
-	b, err := wb.conn.ReadHoldingRegisters(wb.register(charxRegCurrents), 3*2)
-	if err != nil {
-		return 0, 0, 0, err
-	}
-
-	var res [3]float64
-	for i := range res {
-		res[i] = float64(encoding.Int32(b[4*i:])) / 1e3
-	}
-
-	return res[0], res[1], res[2], nil
+	return wb.getPhaseValues(charxRegCurrents)
 }
 
 // voltages implements the api.PhaseVoltages interface
 func (wb *PhoenixCharx) voltages() (float64, float64, float64, error) {
-	b, err := wb.conn.ReadHoldingRegisters(wb.register(charxRegVoltages), 3*2)
+	return wb.getPhaseValues(charxRegVoltages)
+}
+
+// getPhaseValues returns 3 sequential phase values
+func (wb *PhoenixCharx) getPhaseValues(reg uint16) (float64, float64, float64, error) {
+	b, err := wb.conn.ReadHoldingRegisters(wb.register(reg), 6)
 	if err != nil {
 		return 0, 0, 0, err
 	}
diff --git a/charger/tasmota.go b/charger/tasmota.go
index 5c0b13ed..bba34e9a 100644
--- a/charger/tasmota.go
+++ b/charger/tasmota.go
@@ -23,6 +23,8 @@ func init() {
 	registry.Add("tasmota", NewTasmotaFromConfig)
 }
 
+//go:generate go run ../cmd/tools/decorate.go -f decorateTasmota -b *Tasmota -r api.Charger -t "api.PhaseVoltages,Voltages,func() (float64, float64, float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)"
+
 // NewTasmotaFromConfig creates a Tasmota charger from generic config
 func NewTasmotaFromConfig(other map[string]interface{}) (api.Charger, error) {
 	cc := struct {
@@ -46,19 +48,29 @@ func NewTasmotaFromConfig(other map[string]interface{}) (api.Charger, error) {
 }
 
 // NewTasmota creates Tasmota charger
-func NewTasmota(embed embed, uri, user, password string, channels []int, standbypower float64, cache time.Duration) (*Tasmota, error) {
+func NewTasmota(embed embed, uri, user, password string, channels []int, standbypower float64, cache time.Duration) (api.Charger, error) {
 	conn, err := tasmota.NewConnection(uri, user, password, channels, cache)
 	if err != nil {
 		return nil, err
 	}
 
+	if err := conn.RelayExists(); err != nil {
+		return nil, err
+	}
+
 	c := &Tasmota{
 		conn: conn,
 	}
 
 	c.switchSocket = NewSwitchSocket(&embed, c.Enabled, c.conn.CurrentPower, standbypower)
 
-	return c, c.conn.RelayExists()
+	var currents, voltages func() (float64, float64, float64, error)
+	if len(channels) == 3 {
+		currents = c.currents
+		voltages = c.voltages
+	}
+
+	return decorateTasmota(c, currents, voltages), nil
 }
 
 // Enabled implements the api.Charger interface
@@ -78,16 +90,12 @@ func (c *Tasmota) TotalEnergy() (float64, error) {
 	return c.conn.TotalEnergy()
 }
 
-var _ api.PhaseCurrents = (*Tasmota)(nil)
-
 // Currents implements the api.PhaseCurrents interface
-func (c *Tasmota) Currents() (float64, float64, float64, error) {
+func (c *Tasmota) currents() (float64, float64, float64, error) {
 	return c.conn.Currents()
 }
 
-var _ api.PhaseVoltages = (*Tasmota)(nil)
-
 // Voltages implements the api.PhaseVoltages interface
-func (c *Tasmota) Voltages() (float64, float64, float64, error) {
+func (c *Tasmota) voltages() (float64, float64, float64, error) {
 	return c.conn.Voltages()
 }
diff --git a/charger/tasmota_decorators.go b/charger/tasmota_decorators.go
new file mode 100644
index 00000000..2b7cd21e
--- /dev/null
+++ b/charger/tasmota_decorators.go
@@ -0,0 +1,69 @@
+package charger
+
+// Code generated by github.com/evcc-io/evcc/cmd/tools/decorate.go. DO NOT EDIT.
+
+import (
+	"github.com/evcc-io/evcc/api"
+)
+
+func decorateTasmota(base *Tasmota, phaseVoltages func() (float64, float64, float64, error), phaseCurrents func() (float64, float64, float64, error)) api.Charger {
+	switch {
+	case phaseCurrents == nil && phaseVoltages == nil:
+		return base
+
+	case phaseCurrents == nil && phaseVoltages != nil:
+		return &struct {
+			*Tasmota
+			api.PhaseVoltages
+		}{
+			Tasmota: base,
+			PhaseVoltages: &decorateTasmotaPhaseVoltagesImpl{
+				phaseVoltages: phaseVoltages,
+			},
+		}
+
+	case phaseCurrents != nil && phaseVoltages == nil:
+		return &struct {
+			*Tasmota
+			api.PhaseCurrents
+		}{
+			Tasmota: base,
+			PhaseCurrents: &decorateTasmotaPhaseCurrentsImpl{
+				phaseCurrents: phaseCurrents,
+			},
+		}
+
+	case phaseCurrents != nil && phaseVoltages != nil:
+		return &struct {
+			*Tasmota
+			api.PhaseCurrents
+			api.PhaseVoltages
+		}{
+			Tasmota: base,
+			PhaseCurrents: &decorateTasmotaPhaseCurrentsImpl{
+				phaseCurrents: phaseCurrents,
+			},
+			PhaseVoltages: &decorateTasmotaPhaseVoltagesImpl{
+				phaseVoltages: phaseVoltages,
+			},
+		}
+	}
+
+	return nil
+}
+
+type decorateTasmotaPhaseCurrentsImpl struct {
+	phaseCurrents func() (float64, float64, float64, error)
+}
+
+func (impl *decorateTasmotaPhaseCurrentsImpl) Currents() (float64, float64, float64, error) {
+	return impl.phaseCurrents()
+}
+
+type decorateTasmotaPhaseVoltagesImpl struct {
+	phaseVoltages func() (float64, float64, float64, error)
+}
+
+func (impl *decorateTasmotaPhaseVoltagesImpl) Voltages() (float64, float64, float64, error) {
+	return impl.phaseVoltages()
+}
diff --git a/charger/victron.go b/charger/victron.go
index 5e9a5846..b47898b8 100644
--- a/charger/victron.go
+++ b/charger/victron.go
@@ -187,7 +187,7 @@ var _ api.ChargeRater = (*Victron)(nil)
 
 // ChargedEnergy implements the api.MeterEnergy interface
 func (wb *Victron) ChargedEnergy() (float64, error) {
-	b, err := wb.conn.ReadHoldingRegisters(wb.regs.Energy, cast.ToUint16(wb.regs.isGX))
+	b, err := wb.conn.ReadHoldingRegisters(wb.regs.Energy, 1+cast.ToUint16(wb.regs.isGX))
 	if err != nil {
 		return 0, err
 	}
diff --git a/cmd/root.go b/cmd/root.go
index d7b61de9..264d86d8 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -147,7 +147,7 @@ func runRoot(cmd *cobra.Command, args []string) {
 	go socketHub.Run(pipe.NewDropper(ignoreEmpty).Pipe(tee.Attach()), cache)
 
 	// setup values channel
-	valueChan := make(chan util.Param, 1)
+	valueChan := make(chan util.Param, 64)
 	go tee.Run(valueChan)
 
 	// capture log messages for UI
diff --git a/cmd/setup.go b/cmd/setup.go
index a76112a8..432999a6 100644
--- a/cmd/setup.go
+++ b/cmd/setup.go
@@ -282,7 +282,7 @@ func vehicleInstance(cc config.Named) (api.Vehicle, error) {
 	if err != nil {
 		var ce *util.ConfigError
 		if errors.As(err, &ce) {
-			return nil, fmt.Errorf("cannot create vehicle '%s': %w", cc.Name, err)
+			return nil, err
 		}
 
 		// wrap non-config vehicle errors to prevent fatals
@@ -592,52 +592,42 @@ func configureMessengers(conf messagingConfig, valueChan chan util.Param, cache
 	return messageChan, nil
 }
 
-func configureTariffs(conf tariffConfig) (tariff.Tariffs, error) {
-	var grid, feedin, co2, planner api.Tariff
-	var currencyCode currency.Unit = currency.EUR
-	var err error
+func configureTariff(name string, conf config.Typed, t *api.Tariff, wg *sync.WaitGroup) {
+	defer wg.Done()
 
-	if conf.Currency != "" {
-		currencyCode = currency.MustParseISO(conf.Currency)
+	if conf.Type == "" {
+		return
 	}
 
-	if conf.Grid.Type != "" {
-		grid, err = tariff.NewFromConfig(conf.Grid.Type, conf.Grid.Other)
-		if err != nil {
-			grid = nil
-			log.ERROR.Printf("failed configuring grid tariff: %v", err)
-		}
+	res, err := tariff.NewFromConfig(conf.Type, conf.Other)
+	if err != nil {
+		log.ERROR.Printf("failed configuring %s tariff: %v", name, err)
+		return
 	}
 
-	if conf.FeedIn.Type != "" {
-		feedin, err = tariff.NewFromConfig(conf.FeedIn.Type, conf.FeedIn.Other)
-		if err != nil {
-			feedin = nil
-			log.ERROR.Printf("failed configuring feed-in tariff: %v", err)
-		}
-	}
+	*t = res
+}
 
-	if conf.Co2.Type != "" {
-		co2, err = tariff.NewFromConfig(conf.Co2.Type, conf.Co2.Other)
-		if err != nil {
-			co2 = nil
-			log.ERROR.Printf("failed configuring co2 tariff: %v", err)
-		}
+func configureTariffs(conf tariffConfig) (*tariff.Tariffs, error) {
+	tariffs := tariff.Tariffs{
+		Currency: currency.EUR,
 	}
 
-	if conf.Planner.Type != "" {
-		planner, err = tariff.NewFromConfig(conf.Planner.Type, conf.Planner.Other)
-		if err != nil {
-			planner = nil
-			log.ERROR.Printf("failed configuring planner tariff: %v", err)
-		} else if planner.Type() == api.TariffTypeCo2 {
-			log.WARN.Printf("tariff configuration changed, use co2 instead of planner for co2 tariff")
-		}
+	if conf.Currency != "" {
+		tariffs.Currency = currency.MustParseISO(conf.Currency)
 	}
 
-	tariffs := tariff.NewTariffs(currencyCode, grid, feedin, co2, planner)
+	var wg sync.WaitGroup
+	wg.Add(4)
+
+	go configureTariff("grid", conf.Grid, &tariffs.Grid, &wg)
+	go configureTariff("feedin", conf.FeedIn, &tariffs.FeedIn, &wg)
+	go configureTariff("co2", conf.Co2, &tariffs.Co2, &wg)
+	go configureTariff("planner", conf.Planner, &tariffs.Planner, &wg)
+
+	wg.Wait()
 
-	return *tariffs, nil
+	return &tariffs, nil
 }
 
 func configureDevices(conf globalConfig) error {
@@ -668,7 +658,7 @@ func configureSiteAndLoadpoints(conf globalConfig) (*core.Site, error) {
 	return configureSite(conf.Site, loadpoints, tariffs)
 }
 
-func configureSite(conf map[string]interface{}, loadpoints []*core.Loadpoint, tariffs tariff.Tariffs) (*core.Site, error) {
+func configureSite(conf map[string]interface{}, loadpoints []*core.Loadpoint, tariffs *tariff.Tariffs) (*core.Site, error) {
 	site, err := core.NewSiteFromConfig(log, conf, loadpoints, tariffs)
 	if err != nil {
 		return nil, fmt.Errorf("failed configuring site: %w", err)
diff --git a/core/loadpoint.go b/core/loadpoint.go
index fbe24e6d..cff26970 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -534,7 +534,7 @@ func (lp *Loadpoint) evChargeCurrentHandler(current float64) {
 // If physical charge meter is present this handler is not used.
 // The actual value is published by the evChargeCurrentHandler
 func (lp *Loadpoint) evChargeCurrentWrappedMeterHandler(current float64) {
-	power := current * float64(lp.activePhases()) * Voltage
+	power := current * float64(lp.ActivePhases()) * Voltage
 
 	// if disabled we cannot be charging
 	if !lp.enabled || !lp.charging() {
@@ -582,7 +582,7 @@ func (lp *Loadpoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
 
 	lp.setConfiguredPhases(lp.ConfiguredPhases)
 	lp.publish(keys.PhasesEnabled, lp.phases)
-	lp.publish(keys.PhasesActive, lp.activePhases())
+	lp.publish(keys.PhasesActive, lp.ActivePhases())
 	lp.publishTimer(phaseTimer, 0, timerInactive)
 	lp.publishTimer(pvTimer, 0, timerInactive)
 	lp.publishTimer(guardTimer, 0, timerInactive)
@@ -820,7 +820,7 @@ func (lp *Loadpoint) limitEnergyReached() bool {
 // limitSocReached returns true if the effective limit has been reached
 func (lp *Loadpoint) limitSocReached() bool {
 	limit := lp.effectiveLimitSoc()
-	return limit > 0 && lp.vehicleSoc >= float64(limit)
+	return limit > 0 && limit < 100 && lp.vehicleSoc >= float64(limit)
 }
 
 // minSocNotReached checks if minimum is configured and not reached.
@@ -1052,7 +1052,7 @@ func (lp *Loadpoint) pvScalePhases(sitePower, minCurrent, maxCurrent float64) bo
 	}
 
 	var waiting bool
-	activePhases := lp.activePhases()
+	activePhases := lp.ActivePhases()
 	availablePower := lp.chargePower - sitePower
 	scalable := (sitePower > 0 || !lp.enabled) && activePhases > 1 && lp.ConfiguredPhases < 3
 
@@ -1156,7 +1156,7 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower float64, batter
 
 	// calculate target charge current from delta power and actual current
 	effectiveCurrent := lp.effectiveCurrent()
-	activePhases := lp.activePhases()
+	activePhases := lp.ActivePhases()
 	deltaCurrent := powerToCurrent(-sitePower, activePhases)
 	targetCurrent := max(effectiveCurrent+deltaCurrent, 0)
 
diff --git a/core/loadpoint/api.go b/core/loadpoint/api.go
index d3d480fc..9f245a06 100644
--- a/core/loadpoint/api.go
+++ b/core/loadpoint/api.go
@@ -49,6 +49,8 @@ type API interface {
 	GetPhases() int
 	// SetPhases sets the enabled phases
 	SetPhases(int) error
+	// ActivePhases returns the active phases for the current vehicle
+	ActivePhases() int
 
 	// GetLimitSoc returns the session limit soc
 	GetLimitSoc() int
diff --git a/core/loadpoint/mock.go b/core/loadpoint/mock.go
index 091a6593..8138ff1a 100644
--- a/core/loadpoint/mock.go
+++ b/core/loadpoint/mock.go
@@ -35,6 +35,20 @@ func (m *MockAPI) EXPECT() *MockAPIMockRecorder {
 	return m.recorder
 }
 
+// ActivePhases mocks base method.
+func (m *MockAPI) ActivePhases() int {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "ActivePhases")
+	ret0, _ := ret[0].(int)
+	return ret0
+}
+
+// ActivePhases indicates an expected call of ActivePhases.
+func (mr *MockAPIMockRecorder) ActivePhases() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ActivePhases", reflect.TypeOf((*MockAPI)(nil).ActivePhases))
+}
+
 // EffectiveMaxPower mocks base method.
 func (m *MockAPI) EffectiveMaxPower() float64 {
 	m.ctrl.T.Helper()
diff --git a/core/loadpoint_effective.go b/core/loadpoint_effective.go
index acc0f52d..57a441f6 100644
--- a/core/loadpoint_effective.go
+++ b/core/loadpoint_effective.go
@@ -60,38 +60,46 @@ func (lp *Loadpoint) SocBasedPlanning() bool {
 
 // effectiveMinCurrent returns the effective min current
 func (lp *Loadpoint) effectiveMinCurrent() float64 {
+	minCurrent := lp.GetMinCurrent()
+
 	if v := lp.GetVehicle(); v != nil {
 		if res, ok := v.OnIdentified().GetMinCurrent(); ok {
-			return res
+			minCurrent = max(minCurrent, res)
 		}
 	}
 
 	if c, ok := lp.charger.(api.CurrentLimiter); ok {
 		if res, _, err := c.GetMinMaxCurrent(); err == nil {
-			lp.publish(keys.EffectiveMinCurrent, res)
-			return res
+			if res > 0 && res < minCurrent {
+				minCurrent = res
+			} else {
+				minCurrent = max(minCurrent, res)
+			}
+			lp.publish(keys.EffectiveMinCurrent, minCurrent)
 		}
 	}
 
-	return lp.GetMinCurrent()
+	return minCurrent
 }
 
 // effectiveMaxCurrent returns the effective max current
 func (lp *Loadpoint) effectiveMaxCurrent() float64 {
+	maxCurrent := lp.GetMaxCurrent()
+
 	if v := lp.GetVehicle(); v != nil {
 		if res, ok := v.OnIdentified().GetMaxCurrent(); ok {
-			return res
+			maxCurrent = min(maxCurrent, res)
 		}
 	}
 
 	if c, ok := lp.charger.(api.CurrentLimiter); ok {
 		if _, res, err := c.GetMinMaxCurrent(); err == nil {
-			lp.publish(keys.EffectiveMaxCurrent, res)
-			return res
+			maxCurrent = min(maxCurrent, res)
+			lp.publish(keys.EffectiveMaxCurrent, maxCurrent)
 		}
 	}
 
-	return lp.GetMaxCurrent()
+	return maxCurrent
 }
 
 // effectiveLimitSoc returns the effective session limit soc
@@ -110,6 +118,7 @@ func (lp *Loadpoint) effectiveLimitSoc() int {
 		}
 	}
 
+	// MUST return 100 here as UI looks at effectiveLimitSoc and not limitSoc (VehicleSoc.vue)
 	return 100
 }
 
diff --git a/core/loadpoint_phases.go b/core/loadpoint_phases.go
index 61cc6d65..e82b8801 100644
--- a/core/loadpoint_phases.go
+++ b/core/loadpoint_phases.go
@@ -44,7 +44,7 @@ func (lp *Loadpoint) resetMeasuredPhases() {
 	lp.measuredPhases = 0
 	lp.Unlock()
 
-	lp.publish(keys.PhasesActive, lp.activePhases())
+	lp.publish(keys.PhasesActive, lp.ActivePhases())
 }
 
 // getMeasuredPhases provides synchronized access to measuredPhases
@@ -64,9 +64,9 @@ func expect(phases int) int {
 	return unknownPhases
 }
 
-// activePhases returns the number of expectedly active phases for the meter.
+// ActivePhases returns the number of expectedly active phases for the meter.
 // If unknown for 1p3p chargers during startup it will assume 3p.
-func (lp *Loadpoint) activePhases() int {
+func (lp *Loadpoint) ActivePhases() int {
 	physical := lp.GetPhases()
 	vehicle := lp.getVehiclePhases()
 	measured := lp.getMeasuredPhases()
diff --git a/core/loadpoint_vehicle.go b/core/loadpoint_vehicle.go
index 94af1d3c..673c18f9 100644
--- a/core/loadpoint_vehicle.go
+++ b/core/loadpoint_vehicle.go
@@ -156,7 +156,7 @@ func (lp *Loadpoint) setActiveVehicle(v api.Vehicle) {
 	}
 
 	// re-publish vehicle settings
-	lp.publish(keys.PhasesActive, lp.activePhases())
+	lp.publish(keys.PhasesActive, lp.ActivePhases())
 	lp.unpublishVehicle()
 
 	// publish effective values
diff --git a/core/site.go b/core/site.go
index 1213948e..b41146c4 100644
--- a/core/site.go
+++ b/core/site.go
@@ -82,8 +82,8 @@ type Site struct {
 	bufferStartSoc          float64 // start charging on battery above this Soc
 	batteryDischargeControl bool    // prevent battery discharge for fast and planned charging
 
-	tariffs     tariff.Tariffs           // Tariff
 	loadpoints  []*Loadpoint             // Loadpoints
+	tariffs     *tariff.Tariffs          // Tariffs
 	coordinator *coordinator.Coordinator // Vehicles
 	prioritizer *prioritizer.Prioritizer // Power budgets
 	stats       *Stats                   // Stats
@@ -111,7 +111,7 @@ func NewSiteFromConfig(
 	log *util.Logger,
 	other map[string]interface{},
 	loadpoints []*Loadpoint,
-	tariffs tariff.Tariffs,
+	tariffs *tariff.Tariffs,
 ) (*Site, error) {
 	site := NewSite()
 	if err := util.DecodeOther(other, site); err != nil {
@@ -205,7 +205,7 @@ func NewSiteFromConfig(
 
 	// revert battery mode on shutdown
 	shutdown.Register(func() {
-		if mode := site.GetBatteryMode(); mode != api.BatteryUnknown && mode != api.BatteryNormal {
+		if mode := site.GetBatteryMode(); batteryModeModified(mode) {
 			if err := site.updateBatteryMode(api.BatteryNormal); err != nil {
 				site.log.ERROR.Println("battery mode:", err)
 			}
@@ -249,7 +249,9 @@ func (site *Site) restoreSettings() error {
 		}
 	}
 	if v, err := settings.Bool(keys.BatteryDischargeControl); err == nil {
-		site.batteryDischargeControl = v
+		if err := site.SetBatteryDischargeControl(v); err != nil {
+			return err
+		}
 	}
 	return nil
 }
@@ -817,6 +819,10 @@ func (site *Site) update(lp Updater) {
 
 // prepare publishes initial values
 func (site *Site) prepare() {
+	if err := site.restoreSettings(); err != nil {
+		site.log.ERROR.Println(err)
+	}
+
 	site.publish(keys.SiteTitle, site.Title)
 
 	site.publish(keys.GridConfigured, site.gridMeter != nil)
@@ -825,20 +831,17 @@ func (site *Site) prepare() {
 	site.publish(keys.BufferSoc, site.bufferSoc)
 	site.publish(keys.BufferStartSoc, site.bufferStartSoc)
 	site.publish(keys.PrioritySoc, site.prioritySoc)
+	site.publish(keys.BatteryMode, site.batteryMode)
+	site.publish(keys.BatteryDischargeControl, site.batteryDischargeControl)
 	site.publish(keys.ResidualPower, site.ResidualPower)
-	site.publish(keys.SmartCostLimit, site.smartCostLimit)
-	site.publish(keys.SmartCostType, nil)
+
+	site.publish(keys.Currency, site.tariffs.Currency)
 	site.publish(keys.SmartCostActive, false)
+	site.publish(keys.SmartCostLimit, site.smartCostLimit)
 	if tariff := site.GetTariff(PlannerTariff); tariff != nil {
-		site.publish(keys.SmartCostType, tariff.Type().String())
-	}
-	site.publish(keys.Currency, site.tariffs.Currency.String())
-
-	site.publish(keys.BatteryDischargeControl, site.batteryDischargeControl)
-	site.publish(keys.BatteryMode, site.batteryMode.String())
-
-	if err := site.restoreSettings(); err != nil {
-		site.log.ERROR.Println(err)
+		site.publish(keys.SmartCostType, tariff.Type())
+	} else {
+		site.publish(keys.SmartCostType, nil)
 	}
 
 	site.publishVehicles()
diff --git a/core/site_api.go b/core/site_api.go
index 014d8b22..915a8edb 100644
--- a/core/site_api.go
+++ b/core/site_api.go
@@ -195,7 +195,7 @@ func (site *Site) GetTariff(tariff string) api.Tariff {
 			return site.tariffs.Planner
 
 		case site.tariffs.Grid != nil && site.tariffs.Grid.Type() == api.TariffTypePriceForecast:
-			// prio 1: dynamic grid tariff
+			// prio 1: grid tariff with forecast
 			return site.tariffs.Grid
 
 		case site.tariffs.Co2 != nil:
@@ -225,7 +225,7 @@ func (site *Site) SetBatteryDischargeControl(val bool) error {
 
 	if site.GetBatteryDischargeControl() != val {
 		// reset to normal when disabling
-		if mode := site.GetBatteryMode(); mode != api.BatteryNormal {
+		if mode := site.GetBatteryMode(); !val && batteryModeModified(mode) {
 			if err := site.updateBatteryMode(api.BatteryNormal); err != nil {
 				return err
 			}
diff --git a/core/site_battery.go b/core/site_battery.go
index 0492fdff..fc26bb41 100644
--- a/core/site_battery.go
+++ b/core/site_battery.go
@@ -6,6 +6,10 @@ import (
 	"github.com/evcc-io/evcc/core/loadpoint"
 )
 
+func batteryModeModified(mode api.BatteryMode) bool {
+	return mode != api.BatteryUnknown && mode != api.BatteryNormal
+}
+
 // GetBatteryMode returns the battery mode
 func (site *Site) GetBatteryMode() api.BatteryMode {
 	site.RLock()
diff --git a/i18n/bg.toml b/i18n/bg.toml
index 4049e442..51f9094d 100644
--- a/i18n/bg.toml
+++ b/i18n/bg.toml
@@ -1,6 +1,8 @@
 [batterySettings]
 batteryLevel = "Батерия %"
 capacity = "{energy} от {total}"
+disclaimerHint = "Бележка:"
+disclaimerText = "Тези настройки важат само за режим на работа използвайки енергия от фотоволтаици. Зареждането ще бъде настроено спрямо тях"
 legendBottomName = "приоритизиране на дом"
 legendBottomSubline = "не се използва за зареждане"
 legendMiddleName = "приоритизиране на кола"
@@ -10,7 +12,6 @@ legendTopAutostart = "автоматично стартиране"
 legendTopName = "зареждане използвайки батерия"
 legendTopSubline = "без прекъсвания"
 modalTitle = "Настройки на батерия"
-note = "Бележка: Тези настройки важат само за режим на работа използвайки енергия от фотоволтаици. Зареждането ще бъде настроено спрямо тях"
 
 [batterySettings.bufferStart]
 full = "при почти заредена батерия"
diff --git a/i18n/ca.toml b/i18n/ca.toml
index 29c93c7e..a4a22980 100644
--- a/i18n/ca.toml
+++ b/i18n/ca.toml
@@ -1,6 +1,8 @@
 [batterySettings]
 batteryLevel = "Nivell de bateria"
 capacity = "{energy} de {total}"
+disclaimerHint = "Nota:"
+disclaimerText = "aquests paràmetres només afecten el mode solar. El comportament de càrrega s'ajusta en conseqüència."
 legendBottomName = "prioritat de la casa"
 legendBottomSubline = "no utilitzat per carregar"
 legendMiddleName = "primer, el vehicle"
@@ -10,7 +12,6 @@ legendTopAutostart = "s'inicia automaticament"
 legendTopName = "càrrega soportada per bateria"
 legendTopSubline = "sensa interrupcions"
 modalTitle = "Configuració bateria"
-note = "Nota: aquests paràmetres només afecten el mode solar. El comportament de càrrega s'ajusta en conseqüència."
 
 [batterySettings.bufferStart]
 full = "quan gairebé ple"
diff --git a/i18n/cs.toml b/i18n/cs.toml
index fd86d4fa..3408a482 100644
--- a/i18n/cs.toml
+++ b/i18n/cs.toml
@@ -1,6 +1,8 @@
 [batterySettings]
 batteryLevel = "Nabití baterie"
 capacity = "{energy} z {total}"
+disclaimerHint = "Poznámka:"
+disclaimerText = "Tato nastavení ovlivňují pouze solární režim. Chování dobíjení je příslušně upraveno."
 legendBottomName = "dom má přednost"
 legendBottomSubline = "nepoužito pro dobíjení"
 legendMiddleName = "nejdříve vozidlo"
@@ -10,7 +12,6 @@ legendTopAutostart = "startuje automaticky"
 legendTopName = "dobíjení podporované baterií"
 legendTopSubline = "bez přerušování"
 modalTitle = "Nastavení baterie"
-note = "Poznámka: Tato nastavení ovlivňují pouze solární režim. Chování dobíjení je příslušně upraveno."
 
 [batterySettings.bufferStart]
 full = "když je skoro plná"
diff --git a/i18n/da.toml b/i18n/da.toml
index 39ac4ef4..919ef5a7 100644
--- a/i18n/da.toml
+++ b/i18n/da.toml
@@ -3,6 +3,8 @@ batteryLevel = "Batteriniveau"
 capacity = "{energy} af {total}"
 control = "Batteri indstillinger"
 discharge = "Forhindrer afladning ved planlagt opladning og i hurtig mode"
+disclaimerHint = "OBS:"
+disclaimerText = "Disse indstillinger gælder kun for solar modus. Indstillingerne ændres tilsvarende."
 legendBottomName = "Prioritér hjemmet"
 legendBottomSubline = "Anvendes ikke til opladning"
 legendMiddleName = "Køretøj først"
@@ -12,8 +14,6 @@ legendTopAutostart = "starter automatisk"
 legendTopName = "batteri understøttet opladning"
 legendTopSubline = "uden afbrydelser"
 modalTitle = "batteri indstillinger"
-notControllable = "Indstilling af batteriet er ikke mulig for dette batteri"
-note = "OBS: Disse indstillinger gælder kun for solar modus. Indstillingerne ændres tilsvarende."
 
 [batterySettings.bufferStart]
 full = "næsten fyldt"
@@ -174,6 +174,7 @@ departureTab = "Afgang"
 goal = "Opladnings mål"
 modalTitle = "Lade plan"
 none = "ingen"
+remove = "Fjern"
 time = "Tid"
 title = "Plan"
 titleMinSoc = "Min. ladning"
@@ -202,6 +203,9 @@ duration = "Varighed"
 fallbackName = "Ladepunkt"
 power = "Effekt"
 price = "Σ Pris"
+remaining = "Resterende tid"
+remoteDisabledHard = "{source}: slukket"
+remoteDisabledSoft = "{source}: Adaptiv sol-opladning er slukket"
 solar = "solenergi"
 
 [main.loadpointSettings]
@@ -264,6 +268,7 @@ planPeriodValue = "{start} til {end}"
 planUnknown = "Ikke kendt endnu"
 preview = "forhåndsvising"
 priceLimit = "pris grænse på {price}"
+remove = "Fjern"
 setPlan = "Angiv en ladeplan"
 setTargetTime = "ingen"
 targetIsAboveLimit = "Den indstillede opladningsgrænse på {limit} ignoreres i denne periode\""
diff --git a/i18n/de.toml b/i18n/de.toml
index 44beb45c..68c65a73 100644
--- a/i18n/de.toml
+++ b/i18n/de.toml
@@ -3,17 +3,17 @@ batteryLevel = "Ladestand"
 capacity = "{energy} von {total}"
 control = "Batteriesteuerung"
 discharge = "Verhindere Entladung im Schnell-Modus und bei geplantem Laden."
+disclaimerHint = "Hinweis:"
+disclaimerText = "Nur relevant im PV-Modus. Das Ladeverhalten wird entsprechend angepasst."
 legendBottomName = "Haus hat Priorität"
 legendBottomSubline = "nicht zum Laden verwenden"
 legendMiddleName = "Fahrzeug zuerst"
 legendMiddleSubline = "dann das Haus"
 legendTitle = "Wie soll Sonnenstrom genutzt werden?"
 legendTopAutostart = "startet automatisch"
-legendTopName = "batterieunterstütztes Laden"
+legendTopName = "Batterieunterstütztes Laden"
 legendTopSubline = "ohne Unterbrechungen"
 modalTitle = "Batterieeinstellungen"
-notControllable = "Batteriesteuerung ist für diesen Speicher nicht verfügbar."
-note = "Hinweis: Nur relevant im PV-Modus. Das Ladeverhalten wird entsprechend angepasst."
 
 [batterySettings.bufferStart]
 full = "wenn fast voll"
@@ -182,7 +182,7 @@ update = "Anwenden"
 battery = "Batterie"
 batteryCharge = "Batterie laden"
 batteryDischarge = "Batterie entladen"
-batteryHold = "Battery (gesperrt)"
+batteryHold = "Batterie (gesperrt)"
 batteryTooltip = "{energy} von {total} ({soc})"
 gridImport = "Netzbezug"
 homePower = "Verbrauch"
diff --git a/i18n/en.toml b/i18n/en.toml
index d8590900..c0610edc 100644
--- a/i18n/en.toml
+++ b/i18n/en.toml
@@ -3,6 +3,8 @@ batteryLevel = "Battery level"
 capacity = "{energy} of {total}"
 control = "Battery control"
 discharge = "Prevent discharge in fast mode and planned charging."
+disclaimerHint = "Note:"
+disclaimerText = "These settings only affect solar mode. Charging behaviour is adjusted accordingly."
 legendBottomName = "home priority"
 legendBottomSubline = "not used for charging"
 legendMiddleName = "vehicle first"
@@ -12,8 +14,6 @@ legendTopAutostart = "starts automatically"
 legendTopName = "battery supported charging"
 legendTopSubline = "without interruptions"
 modalTitle = "Battery settings"
-notControllable = "Battery control is not available for this battery."
-note = "Note: These settings only affect the solar mode. Charging behaviour is adjusted accordingly."
 
 [batterySettings.bufferStart]
 full = "when nearly full"
diff --git a/i18n/es.toml b/i18n/es.toml
index 77eaedf2..f2fe3970 100644
--- a/i18n/es.toml
+++ b/i18n/es.toml
@@ -3,6 +3,8 @@ batteryLevel = "Nivel de bateria"
 capacity = "{energy} de {total}"
 control = "Control de la batería"
 discharge = "Evitar la descarga en modo rápido y la carga planificada"
+disclaimerHint = "Nota:"
+disclaimerText = "Estos parámetros solo afectan al modo solar. El comportamiento de carga se ajusta correspondientemente."
 legendBottomName = "prioridad de la casa"
 legendBottomSubline = "no se usa para la carga"
 legendMiddleName = "primero el vehículo"
@@ -12,8 +14,6 @@ legendTopAutostart = "inicio automático"
 legendTopName = "carga soportada por bateria"
 legendTopSubline = "sin interrupciones"
 modalTitle = "Configuración de batería"
-notControllable = "El control de la batería no está disponible para esta batería"
-note = "Nota: Estos parámetros solo afectan al modo solar. El comportamiento de carga se ajusta correspondientemente."
 
 [batterySettings.bufferStart]
 full = "cuando está casi llena"
diff --git a/i18n/fi.toml b/i18n/fi.toml
index 5106ef18..1c44bf27 100644
--- a/i18n/fi.toml
+++ b/i18n/fi.toml
@@ -1,6 +1,8 @@
 [batterySettings]
 batteryLevel = "Akun varaustaso"
 capacity = "{energy} / {total}"
+disclaimerHint = "Huomaa:"
+disclaimerText = "Nämä asetukset vaikuttavat vain aurinkotilaan. Latauskäyttäytymistä säädetään vastaavasti."
 legendBottomName = "talo on etusijalla"
 legendBottomSubline = "ei käytetty lataamiseen"
 legendMiddleName = "ajoneuvo ensin"
@@ -10,7 +12,6 @@ legendTopAutostart = "alkaa automaattisesti"
 legendTopName = "akkutuettu lataus"
 legendTopSubline = "keskeytyksettä"
 modalTitle = "Akun asetukset"
-note = "Huomaa: Nämä asetukset vaikuttavat vain aurinkotilaan. Latauskäyttäytymistä säädetään vastaavasti."
 
 [batterySettings.bufferStart]
 full = "kun melkein täynnä"
diff --git a/i18n/fr.toml b/i18n/fr.toml
index 63b0997b..02226d7a 100644
--- a/i18n/fr.toml
+++ b/i18n/fr.toml
@@ -1,6 +1,8 @@
 [batterySettings]
 batteryLevel = "Niveau de la batterie"
 capacity = "{energy} sur {total}"
+disclaimerHint = "NB :"
+disclaimerText = "ces réglages n'affectent que le mode solaire. Le comportement de charge est adapté en conséquence."
 legendBottomName = "priorité maison"
 legendBottomSubline = "pas utilisé pour la recharge"
 legendMiddleName = "véhicule en premier"
@@ -10,7 +12,6 @@ legendTopAutostart = "démarre automatiquement"
 legendTopName = "recharge soutenue par la batterie"
 legendTopSubline = "sans interruption"
 modalTitle = "Réglages de la batterie"
-note = "NB : ces réglages n'affectent que le mode solaire. Le comportement de charge est adapté en conséquence."
 
 [batterySettings.bufferStart]
 full = "quand presque pleine"
diff --git a/i18n/hr.toml b/i18n/hr.toml
index 3c6c46c3..0fbca898 100644
--- a/i18n/hr.toml
+++ b/i18n/hr.toml
@@ -1,6 +1,8 @@
 [batterySettings]
 batteryLevel = "Razina baterije"
 capacity = "{energy} od {total}"
+disclaimerHint = "Napomena:"
+disclaimerText = "Ove postavke utječu samo na solarni modus. Napajanje se prilagođava u skladu s tim."
 legendBottomName = "kuća ima prioritet"
 legendBottomSubline = "ne koristi se za napajanje"
 legendMiddleName = "najprije vozilo"
@@ -10,7 +12,6 @@ legendTopAutostart = "počinje automatski"
 legendTopName = "podržano baterijsko napajanje"
 legendTopSubline = "bez prekida"
 modalTitle = "Postavke baterije"
-note = "Napomena: Ove postavke utječu samo na solarni modus. Napajanje se prilagođava u skladu s tim."
 
 [batterySettings.bufferStart]
 full = "kad je skoro pun"
diff --git a/i18n/lt.toml b/i18n/lt.toml
index 89242b9e..570cdd15 100644
--- a/i18n/lt.toml
+++ b/i18n/lt.toml
@@ -3,6 +3,8 @@ batteryLevel = "Kaupiklio įkrova"
 capacity = "{energy} iš {total}"
 control = "Kaupiklio valdymas"
 discharge = "Neleisti kaupiklio iškrovimo režime Greitas ir planiniame įkrovime."
+disclaimerHint = "Pastaba:"
+disclaimerText = "Šie nustatymai paveikia tik įkrovimą Saulė. Įkrovimo algoritmas atitinkamai pakeičiamas."
 legendBottomName = "namo pirmumas"
 legendBottomSubline = "nenaudojama įkrovimui"
 legendMiddleName = "pirmiau automobilis"
@@ -12,8 +14,6 @@ legendTopAutostart = "startuoja automatiškai"
 legendTopName = "kaupiklis padeda įkrauti"
 legendTopSubline = "be pertraukų"
 modalTitle = "Kaupiklio nustatymai"
-notControllable = "Šiam kaupikliui valdymo nėra."
-note = "Pastaba: Šie nustatymai paveikia tik įkrovimą Saulė. Įkrovimo algoritmas atitinkamai pakeičiamas."
 
 [batterySettings.bufferStart]
 full = "kai beveik pilnas"
diff --git a/i18n/nl.toml b/i18n/nl.toml
index 44668a57..d7ea44a1 100644
--- a/i18n/nl.toml
+++ b/i18n/nl.toml
@@ -1,6 +1,8 @@
 [batterySettings]
 batteryLevel = "Batterij-niveau"
 capacity = "{energy} van {total}"
+disclaimerHint = "Opgelet:"
+disclaimerText = "Deze instellingen beïnvloeden enkel de zonnemodus. Oplaadgedrag wordt overeenkomstig aangepast."
 legendBottomName = "huis heeft prioriteit"
 legendBottomSubline = "niet gebruikt voor opladen"
 legendMiddleName = "eerst voertuig"
@@ -10,7 +12,6 @@ legendTopAutostart = "start automatisch"
 legendTopName = "batterij-ondersteund opladen"
 legendTopSubline = "zonder onderbrekingen"
 modalTitle = "Batterij-instellingen"
-note = "Opgelet: Deze instellingen beïnvloeden enkel de zonnemodus. Oplaadgedrag wordt overeenkomstig aangepast."
 
 [batterySettings.bufferStart]
 full = "als bijna vol"
diff --git a/i18n/no.toml b/i18n/no.toml
index 57915269..f01c3840 100644
--- a/i18n/no.toml
+++ b/i18n/no.toml
@@ -1,6 +1,8 @@
 [batterySettings]
 batteryLevel = "Ladenivå"
 capacity = "{energy} av {total}"
+disclaimerHint = "Merk:"
+disclaimerText = "Kun relevant i PV-modus. Ladeadferden justeres deretter."
 legendBottomName = "Hjemmelading har prioritet"
 legendBottomSubline = "ikke bruk til lading"
 legendMiddleName = "kjøretøy først"
@@ -10,7 +12,6 @@ legendTopAutostart = "starter automatisk"
 legendTopName = "batteriassistert lading"
 legendTopSubline = "uten avbrudd"
 modalTitle = "Batteriinnstillinger"
-note = "Merk: Kun relevant i PV-modus. Ladeadferden justeres deretter."
 
 [batterySettings.bufferStart]
 full = "når det er nesten fullt"
diff --git a/i18n/pl.toml b/i18n/pl.toml
index 1605ee56..469be373 100644
--- a/i18n/pl.toml
+++ b/i18n/pl.toml
@@ -1,6 +1,10 @@
 [batterySettings]
 batteryLevel = "Poziom baterii"
 capacity = "{energy} z {total}"
+control = "Kontrola baterii"
+discharge = "Zapobiegaj rozładowaniu w trybie szybkim i planowemu ładowaniu."
+disclaimerHint = "Uwaga:"
+disclaimerText = "te ustawienia mają wpływ tylko na tryb solarny. Sposób ładowania jest odpowiednio dostosowywany."
 legendBottomName = "priorytet domu"
 legendBottomSubline = "nie używany do ładowania"
 legendMiddleName = "pojazd najpierw"
@@ -10,7 +14,7 @@ legendTopAutostart = "uruchamia się automatycznie"
 legendTopName = "ładowanie wspomagane bateryjnie"
 legendTopSubline = "bez przerw"
 modalTitle = "Ustawienia baterii"
-note = "Uwaga: te ustawienia mają wpływ tylko na tryb solarny. Sposób ładowania jest odpowiednio dostosowywany."
+notControllable = "Sterowanie baterią nie jest dostępne dla tej baterii."
 
 [batterySettings.bufferStart]
 full = "kiedy prawie pełny"
@@ -169,18 +173,25 @@ modalTitle = "Czy na pewno chcesz uruchomić ponownie?"
 vehicles = "Garaż"
 
 [main.chargingPlan]
+active = "Aktywny"
 arrivalTab = "Przyjazd"
+day = "Dzień"
 departureTab = "Wyjazd"
+goal = "Cel ładowania"
 modalTitle = "Plan ładowania"
 none = "nic"
+remove = "Wyjąć"
+time = "Czas"
 title = "Plan"
 titleMinSoc = "Minimalne ładowanie"
 titleTargetCharge = "Wyjazd"
+update = "Wykorzystać"
 
 [main.energyflow]
 battery = "Bateria"
 batteryCharge = "Ładowanie baterii"
 batteryDischarge = "Rozładowanie baterii"
+batteryHold = "Bateria (locked)"
 batteryTooltip = "{energy} z {total} ({soc})"
 gridImport = "Z sieci"
 homePower = "Zużycie"
@@ -208,9 +219,14 @@ currents = "Prąd ładowania"
 default = "domyślny"
 disclaimerHint = "Uwaga:"
 disclaimerText = "Zmiany zostaną zresetowane po restarcie serwera."
+onlyForSocBasedCharging = "Opcje te są dostępne tylko dla pojazdów ze znanym poziomem naładowania."
 title = "Ustawienia {0}"
 vehicle = "Pojazd"
 
+[main.loadpointSettings.limitSoc]
+description = "Limit ładowania używany, gdy pojazd jest podłączony."
+label = "Domyślny limit"
+
 [main.loadpointSettings.maxCurrent]
 label = "Maks. prąd"
 
@@ -243,18 +259,23 @@ logout = "wyloguj"
 activate = "Aktywować"
 co2Limit = "Limit CO₂ z {co2}"
 costLimitIgnore = "Skonfigurowany {limit} będzie ignorowany w tym okresie."
+currentPlan = "Plan aktywny"
 descriptionEnergy = "Do kiedy należy załadować {targetEnergy} do pojazdu?"
 descriptionSoc = "Kiedy pojazd powinien zostać naladowany do {targetSoc}%?"
 inactiveLabel = "Czas docelowy"
 modalTitle = "Ustaw docelowy czas"
+notReachableInTime = "Cel nieosiągalny na czas. Szacowane wykończenie: {endTime}."
 onlyInPvMode = "Plan ładowania działa tylko w trybie solarnym."
 planDuration = "Czas ładowania"
 planPeriodLabel = "Okres"
 planPeriodValue = "{start} do {end}"
 planUnknown = "jeszcze nie wiadomo"
+preview = "Podgląd planu"
 priceLimit = "limit ceny {cena}"
 remove = "Usuń"
 setTargetTime = "brak"
+targetIsAboveLimit = "Skonfigurowany limit ładowania wynoszący {limit} będzie w tym okresie ignorowany."
+targetIsAboveVehicleLimit = "Zwiększ limit pojazdu ({limit}) , aby osiągnąć cel ładowania."
 targetIsInThePast = "Wybierz czas w przyszłości."
 targetIsTooFarInTheFuture = "Dostosujemy plan, gdy tylko dowiemy się więcej o przyszłości."
 title = "Czas Docelowy"
diff --git a/i18n/pt.toml b/i18n/pt.toml
index 766f4858..70b5b7f6 100644
--- a/i18n/pt.toml
+++ b/i18n/pt.toml
@@ -3,6 +3,8 @@ batteryLevel = "Nível da bateria"
 capacity = "{energy} de {total}"
 control = "Controle de bateria"
 discharge = "Prevenir a descarga no modo rápido e o carregamento planejado."
+disclaimerHint = "Nota:"
+disclaimerText = "Estas configurações só afetam o modo solar. O comportamento de carregamento é ajustado de acordo."
 legendBottomName = "prioridade doméstica"
 legendBottomSubline = "não usado para carregar"
 legendMiddleName = "veículo primeiro"
@@ -12,8 +14,6 @@ legendTopAutostart = "inicio automatico"
 legendTopName = "carregamento suportado por bateria"
 legendTopSubline = "sem interrupções"
 modalTitle = "Configuraçao da bateria"
-notControllable = "O controle de bateria não está disponível para esta bateria."
-note = "Nota: Estas configurações só afetam o modo solar. O comportamento de carregamento é ajustado de acordo."
 
 [batterySettings.bufferStart]
 full = "quando quase cheio"
@@ -179,6 +179,7 @@ departureTab = "Partida"
 goal = "Real de compartilhamento"
 modalTitle = "Plano de carregamento"
 none = "sem"
+remove = "Remover"
 time = "Tempo"
 title = "Plano"
 titleMinSoc = "Carga mínima"
diff --git a/i18n/ro.toml b/i18n/ro.toml
index 8f8d2a34..71c14cb4 100644
--- a/i18n/ro.toml
+++ b/i18n/ro.toml
@@ -1,6 +1,8 @@
 [batterySettings]
 batteryLevel = "Nivelul bateriei"
 capacity = "{energy} din {total}"
+disclaimerHint = "Nota:"
+disclaimerText = "Aceste setari afecteaza doar modul solar. Procesul de incarcare este ajustat corespunzator"
 legendBottomName = "prioritate casa"
 legendBottomSubline = "neutilizat pentru incarcare"
 legendMiddleName = "in primul rand vehiculul"
@@ -10,7 +12,6 @@ legendTopAutostart = "porneste automat"
 legendTopName = "Incarcare ajutata de baterie"
 legendTopSubline = "fara intreruperi"
 modalTitle = "Setari baterie"
-note = "Nota: Aceste setari afecteaza doar modul solar. Procesul de incarcare este ajustat corespunzator"
 
 [batterySettings.bufferStart]
 full = "cand este aproape plin"
diff --git a/i18n/ru.toml b/i18n/ru.toml
index 7bb55ae2..df3f1c9f 100644
--- a/i18n/ru.toml
+++ b/i18n/ru.toml
@@ -3,6 +3,8 @@ batteryLevel = "Уровень заряда батареи"
 capacity = "{energy} из {total}"
 control = "Управление батареей"
 discharge = "Предотвратить разряд в быстром режиме и запланированной зарядке."
+disclaimerHint = "Примечание:"
+disclaimerText = "Эти настройки влияют только на режим солнечной батареи. Поведение зарядки изменяется соответствующе."
 legendBottomName = "домашний приоритет"
 legendBottomSubline = "не используется для зарядки"
 legendMiddleName = "сначала автомобиль"
@@ -12,8 +14,6 @@ legendTopAutostart = "запускается автоматически"
 legendTopName = "зарядка с поддержкой от аккумулятора"
 legendTopSubline = "без перебоев"
 modalTitle = "Настройки батареи"
-notControllable = "Управление батарей недоступно для этой батареи"
-note = "Примечание: Эти настройки влияют только на режим солнечной батареи. Поведение зарядки изменяется соответствующе."
 
 [batterySettings.bufferStart]
 full = "когда почти полная"
diff --git a/i18n/sl.toml b/i18n/sl.toml
index 94e193d4..a7c45abe 100644
--- a/i18n/sl.toml
+++ b/i18n/sl.toml
@@ -3,6 +3,8 @@ batteryLevel = "Raven baterije"
 capacity = "{energy} od {total}"
 control = "Nadzor baterije"
 discharge = "Preprečite praznjenje v hitrem načinu in načrtovano polnjenje."
+disclaimerHint = "Opomba:"
+disclaimerText = "Te nastavitve vplivajo samo na solarni način. Obnašanje pri polnjenju je ustrezno prilagojeno."
 legendBottomName = "domača prednost"
 legendBottomSubline = "se ne uporablja za polnjenje"
 legendMiddleName = "najprej vozilo"
@@ -12,8 +14,6 @@ legendTopAutostart = "samodejno se zažene"
 legendTopName = "polnjenje, ki ga podpira baterija"
 legendTopSubline = "brez prekinitev"
 modalTitle = "Nastavitve baterije"
-notControllable = "Nadzor baterije ni na voljo za to baterijo."
-note = "Opomba: Te nastavitve vplivajo samo na solarni način. Obnašanje pri polnjenju je ustrezno prilagojeno."
 
 [batterySettings.bufferStart]
 full = "ko je skoraj poln"
@@ -167,18 +167,25 @@ modalTitle = "Ali ste prepričani, da želite znova zagnati?"
 vehicles = "Parkiranje"
 
 [main.chargingPlan]
+active = "Aktivno"
 arrivalTab = "Prihod"
+day = "Dan"
 departureTab = "Odhod"
+goal = "Cilj polnjenja"
 modalTitle = "Načrt polnjenja"
 none = "brez"
+remove = "Odstrani"
+time = "Čas"
 title = "Načrt"
 titleMinSoc = "Min. polnjenje"
 titleTargetCharge = "Odhod"
+update = "Uporabi"
 
 [main.energyflow]
 battery = "Baterija"
 batteryCharge = "Polnjenje baterije"
 batteryDischarge = "Praznjenje baterije"
+batteryHold = "Baterija (zaklenjena)"
 batteryTooltip = "{energy} od {total} ({soc})"
 gridImport = "Uporaba omrežja"
 homePower = "Poraba"
@@ -196,6 +203,9 @@ duration = "Trajanje"
 fallbackName = "Polnilno mesto"
 power = "Moč"
 price = "Σ Cena"
+remaining = "Preostalo"
+remoteDisabledHard = "{source}: izključeno"
+remoteDisabledSoft = "{source}: izklopljeno prilagodljivo solarno polnjenje"
 solar = "Sončna energija"
 
 [main.loadpointSettings]
@@ -203,9 +213,14 @@ currents = "Polnilni tok"
 default = "privzeto"
 disclaimerHint = "Opomba:"
 disclaimerText = "Po ponovnem zagonu strežnika bodo spremembe izgubljene."
+onlyForSocBasedCharging = "Te možnosti so na voljo samo za vozila z znano stopnjo napolnjenosti."
 title = "Nastavitve {0}"
 vehicle = "Vozilo"
 
+[main.loadpointSettings.limitSoc]
+description = "Omejitev polnjenja, ki se uporablja, ko je to vozilo povezano."
+label = "Privzeta omejitev"
+
 [main.loadpointSettings.maxCurrent]
 label = "Maksimalni tok"
 
@@ -238,23 +253,31 @@ logout = "odjava"
 activate = "Aktiviraj"
 co2Limit = "Omejitev CO₂ {co2}"
 costLimitIgnore = "V tem obdobju se nastavljena {limit} ne bo upoštevala."
+currentPlan = "Aktivni načrt"
 descriptionEnergy = "Do kdaj želite da se vozilo napolni za {targetEnergy}?"
 descriptionSoc = "Kdaj želite, da je vozilo napolnjeno na {targetSoc}%?"
 inactiveLabel = "Ciljni čas"
 modalTitle = "Nastavite ciljni čas"
+notReachableInTime = "Cilja ni mogoče doseči pravočasno. Predvideni cilj: {endTime}."
 onlyInPvMode = "Načrt polnjenja deluje samo v solarnem načinu."
 planDuration = "Čas polnjenja"
 planPeriodLabel = "Obdobje"
 planPeriodValue = "{start} do {end}"
 planUnknown = "še ni znano"
+preview = "Predogled načrta"
 priceLimit = "cenovna omejitev {price}"
+remove = "Odstrani"
 setTargetTime = "brez"
+targetIsAboveLimit = "Konfigurirana omejitev polnjenja {limit} se ne bo upoštevala v tem obdobju."
+targetIsAboveVehicleLimit = "Povečaj omejitev vozila ({limit}), da dosežeš cilj polnjenja."
 targetIsInThePast = "Izberi čas v prihodnosti, Marty."
 targetIsTooFarInTheFuture = "Načrt bomo prilagodili takoj, ko bomo izvedeli več o novi funkcionalnosti."
 title = "Ciljni čas"
 today = "danes"
 tomorrow = "jutri"
 update = "Posodobitev"
+vehicleCapacityDocs = "Naučite se konfigurirati."
+vehicleCapacityRequired = "Za oceno časa polnjenja je potrebna kapaciteta baterije vozila."
 
 [main.targetChargePlan]
 chargeDuration = "Čas polnjenja"
@@ -289,8 +312,8 @@ vehicleTarget = "Omejitev vozila: {soc}%"
 
 [main.vehicleStatus]
 charging = "Polnjenje…"
-cheapEnergyCharging = "Na voljo je poceni energija. Polnjenje..."
-cleanEnergyCharging = "Na voljo je čista energija. Polnjenje..."
+cheapEnergyCharging = "Polnjenje s poceni energijo: {price} (omejitev {limit})"
+cleanEnergyCharging = "Polnjenje s čisto energijo: {co2} (omejitev {limit})"
 climating = "Zaznano predkondicioniranje."
 connected = "Povezan."
 disconnected = "Odklopljen."
diff --git a/i18n/sv.toml b/i18n/sv.toml
index 87d1e3a9..5ac639e2 100644
--- a/i18n/sv.toml
+++ b/i18n/sv.toml
@@ -3,6 +3,8 @@ batteryLevel = "Batterinivå"
 capacity = "{energy} av {total}"
 control = "Batteriinställningar"
 discharge = "Förhindra urladdning i snabbt läge och vid schemalagd laddning."
+disclaimerHint = "OBS:"
+disclaimerText = "Inställningarna gäller enbart sol-läge. Laddning justeras därefter"
 legendBottomName = "Prioritera hem"
 legendBottomSubline = "används inte till laddning"
 legendMiddleName = "fordon först"
@@ -12,8 +14,6 @@ legendTopAutostart = "startar automatiskt"
 legendTopName = "laddning med batteristöd"
 legendTopSubline = "utan avbrott"
 modalTitle = "Batteriinställningar"
-notControllable = "Inställning ej möjligt för detta batteri"
-note = "OBS: Inställningarna gäller enbart sol-läge. Laddning justeras därefter"
 
 [batterySettings.bufferStart]
 full = "när det är nästan fullt"
@@ -174,6 +174,7 @@ departureTab = "Avfärd"
 goal = "Laddmål"
 modalTitle = "Laddplan"
 none = "ingen"
+remove = "Ta bort"
 time = "Tid"
 title = "Plan"
 titleMinSoc = "Min. laddning"
@@ -196,12 +197,15 @@ selfConsumption = "Egenförbrukning"
 
 [main.loadpoint]
 avgPrice = "⌀ pris"
-charged = "Laddad"
+charged = "Laddat"
 co2 = "⌀ CO₂"
 duration = "Varaktighet"
 fallbackName = "Laddplats"
 power = "Effekt"
 price = "Σ pris"
+remaining = "Återstående tid"
+remoteDisabledHard = "{source}: avstängd"
+remoteDisabledSoft = "{source}: Adaptiv solladdning avstängd"
 solar = "Solenergi"
 
 [main.loadpointSettings]
@@ -209,6 +213,7 @@ currents = "Laddningsström"
 default = "förvald"
 disclaimerHint = "Anmärkning:"
 disclaimerText = "Ändringarna försvinner när servern startas om."
+onlyForSocBasedCharging = "Dessa inställningar är endast tillgängliga för fordon med känd laddnivå"
 title = "Inställningar {0}"
 vehicle = "Fordon"
 
@@ -248,7 +253,7 @@ logout = "logga ut"
 activate = "Aktivera"
 co2Limit = "CO₂ gräns av {co2}"
 costLimitIgnore = "Den inställda nivån {limit} kommer att ignoreras den här perioden."
-currentPlan = "Laddningsschema"
+currentPlan = "Aktiv plan"
 descriptionEnergy = "När ska fordonet vara laddat till {targetEnergy}?"
 descriptionSoc = "När ska fordonet vara laddat till {targetSoc}%?"
 inactiveLabel = "Mål-tid"
@@ -263,6 +268,7 @@ planPeriodValue = "{start} till {end}"
 planUnknown = "ännu inte känt"
 preview = "Förhandsvisning"
 priceLimit = "prisgräns {price}"
+remove = "Ta bort"
 setPlan = "Laddplan"
 setTargetTime = "ingen"
 targetIsAboveLimit = "Konfigurerat laddmål {limit} kommer ignoreras denna tidsperiod."
@@ -358,7 +364,7 @@ csvMonth = "Ladda hem {month} CSV"
 csvTotal = "Ladda hem total CSV"
 date = "Start"
 downloadCsv = "Ladda hem som CSV"
-energy = "Laddad"
+energy = "Laddat"
 loadpoint = "Laddplats"
 noData = "Inga laddningar denna månad"
 price = "Σ pris"
diff --git a/i18n/uk.toml b/i18n/uk.toml
index 8cef343f..2a09796a 100644
--- a/i18n/uk.toml
+++ b/i18n/uk.toml
@@ -1,6 +1,8 @@
 [batterySettings]
 batteryLevel = "Рівень заряду батареї"
 capacity = "{energy} із {total}"
+disclaimerHint = "Примітка:"
+disclaimerText = "Ці налаштування впливають лише на сонячний режим. Поведінка зарядки регулюється відповідно."
 legendBottomName = "домашній пріоритет"
 legendBottomSubline = "не використовується для зарядки"
 legendMiddleName = "спочатку транспортний засіб"
@@ -10,7 +12,6 @@ legendTopAutostart = "запускається автоматично"
 legendTopName = "заряджання з підтримкою батареї"
 legendTopSubline = "без перебоїв"
 modalTitle = "Налаштування батареї"
-note = "Примітка: Ці налаштування впливають лише на сонячний режим. Поведінка зарядки регулюється відповідно."
 
 [batterySettings.bufferStart]
 full = "коли майже повний"
diff --git a/meter/config.go b/meter/config.go
index 691c3b92..68555c6b 100644
--- a/meter/config.go
+++ b/meter/config.go
@@ -19,7 +19,7 @@ func (r meterRegistry) Add(name string, factory func(map[string]interface{}) (ap
 func (r meterRegistry) Get(name string) (func(map[string]interface{}) (api.Meter, error), error) {
 	factory, exists := r[name]
 	if !exists {
-		return nil, fmt.Errorf("meter type not registered: %s", name)
+		return nil, fmt.Errorf("invalid meter type: %s", name)
 	}
 	return factory, nil
 }
@@ -27,15 +27,16 @@ func (r meterRegistry) Get(name string) (func(map[string]interface{}) (api.Meter
 var registry meterRegistry = make(map[string]func(map[string]interface{}) (api.Meter, error))
 
 // NewFromConfig creates meter from configuration
-func NewFromConfig(typ string, other map[string]interface{}) (v api.Meter, err error) {
+func NewFromConfig(typ string, other map[string]interface{}) (api.Meter, error) {
 	factory, err := registry.Get(strings.ToLower(typ))
-	if err == nil {
-		if v, err = factory(other); err != nil {
-			err = fmt.Errorf("cannot create meter '%s': %w", typ, err)
-		}
-	} else {
-		err = fmt.Errorf("invalid meter type: %s", typ)
+	if err != nil {
+		return nil, err
 	}
 
-	return
+	v, err := factory(other)
+	if err != nil {
+		err = fmt.Errorf("cannot create meter type '%s': %w", typ, err)
+	}
+
+	return v, err
 }
diff --git a/meter/meter.go b/meter/meter.go
index 3a274347..f7893ae8 100644
--- a/meter/meter.go
+++ b/meter/meter.go
@@ -96,7 +96,7 @@ func NewConfigurableFromConfig(other map[string]interface{}) (api.Meter, error)
 		batModeS = cc.battery.LimitController(socG, limitSocS)
 
 	case cc.BatteryMode != nil:
-		modeS, err := provider.NewIntSetterFromConfig("mode", *cc.BatteryMode)
+		modeS, err := provider.NewIntSetterFromConfig("batteryMode", *cc.BatteryMode)
 		if err != nil {
 			return nil, fmt.Errorf("battery mode: %w", err)
 		}
diff --git a/meter/tasmota.go b/meter/tasmota.go
index f046b808..933cdc13 100644
--- a/meter/tasmota.go
+++ b/meter/tasmota.go
@@ -56,7 +56,7 @@ func NewTasmota(uri, user, password, usage string, channels []int, cache time.Du
 	}
 
 	var currents, voltages func() (float64, float64, float64, error)
-	if usage != "grid" {
+	if usage != "grid" && len(channels) == 3 {
 		currents = c.currents
 		voltages = c.voltages
 	}
diff --git a/meter/tasmota/connection.go b/meter/tasmota/connection.go
index 438e5626..818831bf 100644
--- a/meter/tasmota/connection.go
+++ b/meter/tasmota/connection.go
@@ -220,38 +220,36 @@ func (c *Connection) TotalEnergy() (float64, error) {
 
 // Currents implements the api.PhaseCurrents interface
 func (c *Connection) Currents() (float64, float64, float64, error) {
-	s, err := c.statusSnsG.Get()
-	if err != nil {
-		return 0, 0, 0, err
-	}
-
-	var res [3]float64
-	for i, c := range c.channels {
-		res[i], err = s.StatusSNS.Energy.Current.Channel(c)
-		if err != nil {
-			return 0, 0, 0, err
-		}
-	}
-
-	return res[0], res[1], res[2], err
+	return c.getPhaseValues(func(s StatusSNSResponse) Channels {
+		return s.StatusSNS.Energy.Current
+	})
 }
 
 // Voltages implements the api.PhaseVoltages interface
 func (c *Connection) Voltages() (float64, float64, float64, error) {
+	return c.getPhaseValues(func(s StatusSNSResponse) Channels {
+		return s.StatusSNS.Energy.Voltage
+	})
+}
+
+// getPhaseValues returns 3 sequential phase values
+func (c *Connection) getPhaseValues(fun func(StatusSNSResponse) Channels) (float64, float64, float64, error) {
 	s, err := c.statusSnsG.Get()
 	if err != nil {
 		return 0, 0, 0, err
 	}
 
+	all := fun(s)
+
 	var res [3]float64
-	for i, c := range c.channels {
-		res[i], err = s.StatusSNS.Energy.Voltage.Channel(c)
+	for i := range res {
+		res[i], err = all.Channel(c.channels[i])
 		if err != nil {
 			return 0, 0, 0, err
 		}
 	}
 
-	return res[0], res[1], res[2], err
+	return res[0], res[1], res[2], nil
 }
 
 // SmlPower provides the sml sensor power
diff --git a/meter/tasmota/types.go b/meter/tasmota/types.go
index 4435ee82..e8cba524 100644
--- a/meter/tasmota/types.go
+++ b/meter/tasmota/types.go
@@ -92,7 +92,7 @@ type StatusSNSResponse struct {
 	}
 }
 
-// Channels is a Tasmota specifc helper type to handle meter value lists and single meter values
+// Channels is a Tasmota specific helper type to handle meter value lists and single meter values
 type Channels []float64
 
 func (ch *Channels) Channel(channel int) (float64, error) {
diff --git a/provider/go.go b/provider/go.go
index fc4ca78c..53498659 100644
--- a/provider/go.go
+++ b/provider/go.go
@@ -155,7 +155,13 @@ func (p *Go) handleSetter(param string, val any) error {
 	return transformOutputs(p.out, vv)
 }
 
-func (p *Go) evaluate() (any, error) {
+func (p *Go) evaluate() (res any, err error) {
+	defer func() {
+		if r := recover(); r != nil {
+			err = fmt.Errorf("go script panic: %v", r)
+		}
+	}()
+
 	v, err := p.vm.Eval(p.script)
 	if err != nil {
 		return nil, err
diff --git a/tariff/awattar.go b/tariff/awattar.go
index 843f4285..2175629d 100644
--- a/tariff/awattar.go
+++ b/tariff/awattar.go
@@ -70,8 +70,6 @@ func (t *Awattar) run(done chan error) {
 			continue
 		}
 
-		once.Do(func() { close(done) })
-
 		data := make(api.Rates, 0, len(res.Data))
 		for _, r := range res.Data {
 			ar := api.Rate{
@@ -84,6 +82,7 @@ func (t *Awattar) run(done chan error) {
 		data.Sort()
 
 		t.data.Set(data)
+		once.Do(func() { close(done) })
 	}
 }
 
diff --git a/tariff/config.go b/tariff/config.go
index fe04e43f..34dda811 100644
--- a/tariff/config.go
+++ b/tariff/config.go
@@ -19,7 +19,7 @@ func (r tariffRegistry) Add(name string, factory func(map[string]interface{}) (a
 func (r tariffRegistry) Get(name string) (func(map[string]interface{}) (api.Tariff, error), error) {
 	factory, exists := r[name]
 	if !exists {
-		return nil, fmt.Errorf("tariff type not registered: %s", name)
+		return nil, fmt.Errorf("invalid tariff type: %s", name)
 	}
 	return factory, nil
 }
@@ -27,15 +27,16 @@ func (r tariffRegistry) Get(name string) (func(map[string]interface{}) (api.Tari
 var registry tariffRegistry = make(map[string]func(map[string]interface{}) (api.Tariff, error))
 
 // NewFromConfig creates tariff from configuration
-func NewFromConfig(typ string, other map[string]interface{}) (v api.Tariff, err error) {
+func NewFromConfig(typ string, other map[string]interface{}) (api.Tariff, error) {
 	factory, err := registry.Get(strings.ToLower(typ))
-	if err == nil {
-		if v, err = factory(other); err != nil {
-			err = fmt.Errorf("cannot create tariff '%s': %w", typ, err)
-		}
-	} else {
-		err = fmt.Errorf("invalid tariff type: %s", typ)
+	if err != nil {
+		return nil, err
 	}
 
-	return
+	v, err := factory(other)
+	if err != nil {
+		err = fmt.Errorf("cannot create tariff type '%s': %w", typ, err)
+	}
+
+	return v, err
 }
diff --git a/tariff/edf-tempo.go b/tariff/edf-tempo.go
index 48d45acf..6ea38b08 100644
--- a/tariff/edf-tempo.go
+++ b/tariff/edf-tempo.go
@@ -131,8 +131,6 @@ func (t *EdfTempo) run(done chan error) {
 			continue
 		}
 
-		once.Do(func() { close(done) })
-
 		data := make(api.Rates, 0, 24*len(res.Data.Values))
 		for _, r := range res.Data.Values {
 			for ts := r.StartDate.Local(); ts.Before(r.EndDate); ts = ts.Add(time.Hour) {
@@ -147,6 +145,7 @@ func (t *EdfTempo) run(done chan error) {
 		data.Sort()
 
 		t.data.Set(data)
+		once.Do(func() { close(done) })
 	}
 }
 
diff --git a/tariff/electricitymaps.go b/tariff/electricitymaps.go
index 6be4155e..574923e9 100644
--- a/tariff/electricitymaps.go
+++ b/tariff/electricitymaps.go
@@ -98,8 +98,6 @@ func (t *ElectricityMaps) run(done chan error) {
 			continue
 		}
 
-		once.Do(func() { close(done) })
-
 		data := make(api.Rates, 0, len(res.Forecast))
 		for _, r := range res.Forecast {
 			ar := api.Rate{
@@ -112,6 +110,7 @@ func (t *ElectricityMaps) run(done chan error) {
 		data.Sort()
 
 		t.data.Set(data)
+		once.Do(func() { close(done) })
 	}
 }
 
diff --git a/tariff/elering.go b/tariff/elering.go
index 3e9580b4..a3d42d9d 100644
--- a/tariff/elering.go
+++ b/tariff/elering.go
@@ -79,8 +79,6 @@ func (t *Elering) run(done chan error) {
 			continue
 		}
 
-		once.Do(func() { close(done) })
-
 		data := make(api.Rates, 0, len(res.Data[t.region]))
 		for _, r := range res.Data[t.region] {
 			ts := time.Unix(r.Timestamp, 0)
@@ -95,6 +93,7 @@ func (t *Elering) run(done chan error) {
 		data.Sort()
 
 		t.data.Set(data)
+		once.Do(func() { close(done) })
 	}
 }
 
diff --git a/tariff/energinet.go b/tariff/energinet.go
index 66267b14..8b4f94cc 100644
--- a/tariff/energinet.go
+++ b/tariff/energinet.go
@@ -79,8 +79,6 @@ func (t *Energinet) run(done chan error) {
 			continue
 		}
 
-		once.Do(func() { close(done) })
-
 		data := make(api.Rates, 0, len(res.Records))
 		for _, r := range res.Records {
 			date, _ := time.Parse("2006-01-02T15:04:05", r.HourUTC)
@@ -94,6 +92,7 @@ func (t *Energinet) run(done chan error) {
 		data.Sort()
 
 		t.data.Set(data)
+		once.Do(func() { close(done) })
 	}
 }
 
diff --git a/tariff/entsoe.go b/tariff/entsoe.go
index 1cdad77f..7394e156 100644
--- a/tariff/entsoe.go
+++ b/tariff/entsoe.go
@@ -154,8 +154,6 @@ func (t *Entsoe) run(done chan error) {
 			continue
 		}
 
-		once.Do(func() { close(done) })
-
 		data := make(api.Rates, 0, len(res))
 		for _, r := range res {
 			ar := api.Rate{
@@ -168,6 +166,7 @@ func (t *Entsoe) run(done chan error) {
 		data.Sort()
 
 		t.data.Set(data)
+		once.Do(func() { close(done) })
 	}
 }
 
@@ -182,5 +181,5 @@ func (t *Entsoe) Rates() (api.Rates, error) {
 
 // Type implements the api.Tariff interface
 func (t *Entsoe) Type() api.TariffType {
-	return api.TariffTypePriceDynamic
+	return api.TariffTypePriceForecast
 }
diff --git a/tariff/gruenstromindex.go b/tariff/gruenstromindex.go
index 012c11b7..fa027ce2 100644
--- a/tariff/gruenstromindex.go
+++ b/tariff/gruenstromindex.go
@@ -114,8 +114,6 @@ func (t *GrünStromIndex) run(done chan error) {
 			continue
 		}
 
-		once.Do(func() { close(done) })
-
 		data := make(api.Rates, 0, len(res.Forecast))
 		for _, r := range res.Forecast {
 			data = append(data, api.Rate{
@@ -127,6 +125,7 @@ func (t *GrünStromIndex) run(done chan error) {
 		data.Sort()
 
 		t.data.Set(data)
+		once.Do(func() { close(done) })
 	}
 }
 
diff --git a/tariff/ngeso.go b/tariff/ngeso.go
index e6295c80..a04a7d98 100644
--- a/tariff/ngeso.go
+++ b/tariff/ngeso.go
@@ -94,8 +94,6 @@ func (t *Ngeso) run(done chan error) {
 			continue
 		}
 
-		once.Do(func() { close(done) })
-
 		data := make(api.Rates, 0, len(carbonResponse.Results()))
 		for _, r := range carbonResponse.Results() {
 			ar := api.Rate{
@@ -109,6 +107,7 @@ func (t *Ngeso) run(done chan error) {
 		data.Sort()
 
 		t.data.Set(data)
+		once.Do(func() { close(done) })
 	}
 }
 
diff --git a/tariff/octopus.go b/tariff/octopus.go
index bc9af13d..35d1c8bd 100644
--- a/tariff/octopus.go
+++ b/tariff/octopus.go
@@ -74,8 +74,6 @@ func (t *Octopus) run(done chan error) {
 			continue
 		}
 
-		once.Do(func() { close(done) })
-
 		data := make(api.Rates, 0, len(res.Results))
 		for _, r := range res.Results {
 			ar := api.Rate{
@@ -89,6 +87,7 @@ func (t *Octopus) run(done chan error) {
 		data.Sort()
 
 		t.data.Set(data)
+		once.Do(func() { close(done) })
 	}
 }
 
diff --git a/tariff/tariffs.go b/tariff/tariffs.go
index 4a3c4857..ffeb024c 100644
--- a/tariff/tariffs.go
+++ b/tariff/tariffs.go
@@ -12,16 +12,6 @@ type Tariffs struct {
 	Grid, FeedIn, Co2, Planner api.Tariff
 }
 
-func NewTariffs(currency currency.Unit, grid, feedin, co2 api.Tariff, planner api.Tariff) *Tariffs {
-	return &Tariffs{
-		Currency: currency,
-		Grid:     grid,
-		FeedIn:   feedin,
-		Co2:      co2,
-		Planner:  planner,
-	}
-}
-
 func currentPrice(t api.Tariff) (float64, error) {
 	if t != nil {
 		if rr, err := t.Rates(); err == nil {
diff --git a/tariff/tibber.go b/tariff/tibber.go
index 09f746c1..c6fe7145 100644
--- a/tariff/tibber.go
+++ b/tariff/tibber.go
@@ -100,13 +100,12 @@ func (t *Tibber) run(done chan error) {
 			continue
 		}
 
-		once.Do(func() { close(done) })
-
 		pi := res.Viewer.Home.CurrentSubscription.PriceInfo
 		data := append(t.rates(pi.Today), t.rates(pi.Tomorrow)...)
 		data.Sort()
 
 		t.data.Set(data)
+		once.Do(func() { close(done) })
 	}
 }
 
diff --git a/templates/definition/charger/delta.yaml b/templates/definition/charger/delta.yaml
new file mode 100644
index 00000000..c296be49
--- /dev/null
+++ b/templates/definition/charger/delta.yaml
@@ -0,0 +1,26 @@
+template: delta
+products:
+  - brand: Delta
+    description:
+      generic: AC Max Basic
+  - brand: Delta
+    description:
+      generic: AC MAX Smart
+  - brand: Delta
+    description:
+      generic: SLIM Charger
+  - brand: Delta
+    description:
+      generic: Ultra Fast Charger
+capabilities: ["mA", "rfid"]
+requirements:
+  evcc: ["sponsorship"]
+params:
+  - name: modbus
+    choice: ["rs485", "tcpip"]
+    baudrate: 115200
+  - name: connector
+render: |
+  type: delta
+  {{- include "modbus" . }}
+  connector: {{ .connector }}
diff --git a/templates/definition/meter/siemens-7kt1665.yaml b/templates/definition/meter/siemens-7kt1665.yaml
index 665765f5..8751e08a 100644
--- a/templates/definition/meter/siemens-7kt1665.yaml
+++ b/templates/definition/meter/siemens-7kt1665.yaml
@@ -16,7 +16,7 @@ render: |
     register:
       address: 57
       type: input
-      decode: uint32
+      decode: int32
     scale: 0.01
     timeout: 5s
   energy:
diff --git a/templates/definition/meter/sofarsolar-g3.yaml b/templates/definition/meter/sofarsolar-g3.yaml
index 0b8c6bd0..ad15fa59 100644
--- a/templates/definition/meter/sofarsolar-g3.yaml
+++ b/templates/definition/meter/sofarsolar-g3.yaml
@@ -158,7 +158,7 @@ render: |
   limitsoc:
     source: go
     script:
-      return int(100-limitSoc)
+      int(100-limitSoc)
     out:
     - name: dod
       type: int
diff --git a/templates/definition/meter/solaredge-hybrid.yaml b/templates/definition/meter/solaredge-hybrid.yaml
index dbc2c894..a8da2664 100644
--- a/templates/definition/meter/solaredge-hybrid.yaml
+++ b/templates/definition/meter/solaredge-hybrid.yaml
@@ -22,10 +22,6 @@ params:
   - name: timeout
   - name: capacity
     advanced: true
-  - name: watchdog
-    type: duration
-    default: 60s
-    advanced: true
 render: |
   type: custom
   {{- if eq .usage "grid" }}
diff --git a/templates/definition/meter/solax.yaml b/templates/definition/meter/solax.yaml
index cc833a6d..eff1c4f9 100644
--- a/templates/definition/meter/solax.yaml
+++ b/templates/definition/meter/solax.yaml
@@ -7,6 +7,7 @@ products:
   - brand: Qcells
     description:
       generic: Q.HOME ESS HYB-G3
+capabilities: ["battery-control"]
 params:
   - name: usage
     choice: ["grid", "pv", "battery"]
@@ -75,6 +76,75 @@ render: |
       address: 28 # 0x001C Battery Capacity
       type: input
       decode: uint16
+  batterymode:
+    source: switch
+    switch:
+    - case: 1 # normal
+      set:
+        source: sequence
+        set:
+        - source: const
+          value: 0 # self use mode
+          set:
+            source: modbus
+            {{- include "modbus" . | indent 10 }}
+            register:
+              address: 0x001F # SolarChargeUseMode
+              type: writesingle
+              decode: uint16
+        - source: const
+          value: 0 # Stop force charge & discharge
+          set:
+            source: modbus
+            {{- include "modbus" . | indent 10 }}
+            register:
+              address: 0x0020 # Manual mode
+              type: writesingle
+              decode: uint16
+    - case: 2 # hold
+      set:
+        source: sequence
+        set:
+        - source: const
+          value: 2 # backup mode
+          set:
+            source: modbus
+            {{- include "modbus" . | indent 10 }}
+            register:
+              address: 0x001F # SolarChargeUseMode
+              type: writesingle
+              decode: uint16
+        - source: const
+          value: 0 # Stop force charge & discharge
+          set:
+            source: modbus
+            {{- include "modbus" . | indent 10 }}
+            register:
+              address: 0x0020 # Manual mode
+              type: writesingle
+              decode: uint16
+    - case: 3 # charge
+      set:
+        source: sequence
+        set:
+        - source: const
+          value: 3 # manual mode
+          set:
+            source: modbus
+            {{- include "modbus" . | indent 10 }}
+            register:
+              address: 0x001F # SolarChargeUseMode
+              type: writesingle
+              decode: uint16
+        - source: const
+          value: 1 # Force charge
+          set:
+            source: modbus
+            {{- include "modbus" . | indent 10 }}
+            register:
+              address: 0x0020 # Manual mode
+              type: writesingle
+              decode: uint16
   {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
diff --git a/templates/evcc.io/brands.json b/templates/evcc.io/brands.json
deleted file mode 100644
index 06ab4e69..00000000
--- a/templates/evcc.io/brands.json
+++ /dev/null
@@ -1,237 +0,0 @@
-{
- "Chargers": [
-  "ABB",
-  "ABL",
-  "Alfen",
-  "Alphatec",
-  "Amperfied",
-  "Audi",
-  "Bender",
-  "BMW",
-  "cFos",
-  "Compleo",
-  "Cupra",
-  "Dadapower",
-  "DaheimLaden",
-  "E.ON Drive",
-  "E3/DC",
-  "Easee",
-  "Ebee",
-  "echarge",
-  "Elli",
-  "EM2GO",
-  "Ensto",
-  "ESL",
-  "Etrel",
-  "EVBox",
-  "Fronius",
-  "Garo",
-  "go-e",
-  "HardyBarth",
-  "Heidelberg",
-  "Hesotec",
-  "Homecharge",
-  "Huawei",
-  "Innogy",
-  "INRO",
-  "Juice",
-  "KEBA",
-  "KSE",
-  "LadeFoxx",
-  "Mennekes",
-  "NRGKick",
-  "OBO Bettermann",
-  "OpenEVSE",
-  "openWB",
-  "Optec",
-  "Orbis",
-  "PC Electric",
-  "Phoenix Contact",
-  "Porsche",
-  "Pracht",
-  "Pulsares",
-  "Schneider",
-  "Schrack",
-  "SENEC",
-  "Siemens",
-  "Skoda",
-  "SMA",
-  "SolarEdge",
-  "Sonnen",
-  "Stark in Strom",
-  "TechniSat",
-  "Tesla",
-  "TinkerForge",
-  "Ubitricity",
-  "Vestel",
-  "Victron",
-  "Volkswagen",
-  "Wallbe",
-  "wallbox",
-  "Walther Werke",
-  "Webasto",
-  "Zaptec"
- ],
- "SmartPlugs": [
-  "AVM",
-  "Homematic IP",
-  "HomeWizard",
-  "myStrom",
-  "Shelly",
-  "Tasmota",
-  "TP-Link"
- ],
- "Meters": [
-  "ABB",
-  "Acrel",
-  "Alpha ESS",
-  "AVM",
-  "Bernecker Engineering",
-  "Carlo Gavazzi",
-  "cFos",
-  "Deye",
-  "DSMR",
-  "DZG",
-  "E3/DC",
-  "Eastron",
-  "Enphase",
-  "ESPHome",
-  "FENECON",
-  "FoxESS",
-  "Fronius",
-  "go-e",
-  "GoodWe",
-  "Growatt",
-  "Homematic IP",
-  "HomeWizard",
-  "Huawei",
-  "inepro",
-  "Janitza",
-  "KEBA",
-  "Kostal",
-  "LG",
-  "Loxone",
-  "M-TEC",
-  "my-PV",
-  "myStrom",
-  "OpenEMS",
-  "Orno",
-  "P1Monitor",
-  "Powerfox",
-  "Qcells",
-  "RCT",
-  "Saia-Burgess Controls",
-  "SAJ",
-  "SAX",
-  "Schneider Electric",
-  "SENEC",
-  "Shelly",
-  "Siemens",
-  "SMA",
-  "Smartfox",
-  "SofarSolar",
-  "Solaranzeige",
-  "SolarEdge",
-  "SolarMax",
-  "Solarwatt",
-  "Solax",
-  "Sonnen",
-  "Sungrow",
-  "Sunsynk",
-  "Tasmota",
-  "Tesla",
-  "Tibber",
-  "TQ",
-  "VARTA",
-  "Victron",
-  "Volkszähler",
-  "Youless",
-  "ZCS Azzurro",
-  "Zuidwijk"
- ],
- "PVBattery": [
-  "Acrel",
-  "Ads-tec",
-  "Alpha ESS",
-  "Anker",
-  "AVM",
-  "Bosswerk",
-  "Deye",
-  "E3/DC",
-  "Enphase",
-  "FENECON",
-  "FoxESS",
-  "Fronius",
-  "Ginlong",
-  "go-e",
-  "GoodWe",
-  "Growatt",
-  "Homematic IP",
-  "HomeWizard",
-  "Hoymiles",
-  "Huawei",
-  "IGEN Tech",
-  "Kostal",
-  "LG",
-  "Loxone",
-  "M-TEC",
-  "myStrom",
-  "OpenEMS",
-  "Powerfox",
-  "Qcells",
-  "RCT",
-  "SAJ",
-  "SAX",
-  "SENEC",
-  "Senergy",
-  "Shelly",
-  "SMA",
-  "Smartfox",
-  "SofarSolar",
-  "Solaranzeige",
-  "SolarEdge",
-  "SolarMax",
-  "Solarwatt",
-  "Solax",
-  "Sonnen",
-  "Steca",
-  "Sungrow",
-  "Sunsynk",
-  "Tasmota",
-  "Tesla",
-  "TP-Link",
-  "VARTA",
-  "Victron",
-  "Youless",
-  "ZCS Azzurro"
- ],
- "Vehicles": [
-  "Aiways",
-  "Audi",
-  "BMW",
-  "Citroën",
-  "Dacia",
-  "DS",
-  "Fiat",
-  "Ford",
-  "Hyundai",
-  "Jaguar",
-  "Jeep",
-  "Kia",
-  "Land Rover",
-  "Mini",
-  "Nissan",
-  "NIU",
-  "Opel",
-  "Peugeot",
-  "Polestar",
-  "Porsche",
-  "Renault",
-  "Seat",
-  "Skoda",
-  "Smart",
-  "Tesla",
-  "Volkswagen",
-  "Volvo"
- ]
-}
\ No newline at end of file
diff --git a/util/decoder.go b/util/decoder.go
index 19cba3e4..3d6528d2 100644
--- a/util/decoder.go
+++ b/util/decoder.go
@@ -43,6 +43,10 @@ type ConfigError struct {
 	err error
 }
 
+func NewConfigError(err error) error {
+	return &ConfigError{err}
+}
+
 func (e *ConfigError) Error() string {
 	return e.err.Error()
 }
diff --git a/util/templates/render_instance.go b/util/templates/render_instance.go
index 645ffda9..fbd23028 100644
--- a/util/templates/render_instance.go
+++ b/util/templates/render_instance.go
@@ -32,15 +32,17 @@ func RenderInstance(class Class, other map[string]interface{}) (*Instance, error
 
 	b, _, err := tmpl.RenderResult(TemplateRenderModeInstance, other)
 	if err != nil {
-		return nil, err
+		return nil, util.NewConfigError(err)
 	}
 
 	var instance Instance
-	if err = yaml.Unmarshal(b, &instance); err == nil && instance.Type == "" {
-		err = errors.New("empty instance type- check for missing usage")
-	} else if err != nil {
-		err = fmt.Errorf("%w:\n%s", err, string(b))
+	if err := yaml.Unmarshal(b, &instance); err != nil {
+		return nil, fmt.Errorf("%w:\n%s", err, string(b))
+	}
+
+	if instance.Type == "" {
+		return nil, errors.New("empty instance type- check for missing usage")
 	}
 
-	return &instance, err
+	return &instance, nil
 }
diff --git a/vehicle/config.go b/vehicle/config.go
index 51f217e3..ca692e59 100644
--- a/vehicle/config.go
+++ b/vehicle/config.go
@@ -26,7 +26,7 @@ func (r vehicleRegistry) Add(name string, factory func(map[string]interface{}) (
 func (r vehicleRegistry) Get(name string) (func(map[string]interface{}) (api.Vehicle, error), error) {
 	factory, exists := r[name]
 	if !exists {
-		return nil, fmt.Errorf("vehicle type not registered: %s", name)
+		return nil, fmt.Errorf("invalid vehicle type: %s", name)
 	}
 	return factory, nil
 }
@@ -43,7 +43,7 @@ func Types() []string {
 }
 
 // NewFromConfig creates vehicle from configuration
-func NewFromConfig(typ string, other map[string]interface{}) (v api.Vehicle, err error) {
+func NewFromConfig(typ string, other map[string]interface{}) (api.Vehicle, error) {
 	var cc struct {
 		Cloud bool
 		Other map[string]interface{} `mapstructure:",remain"`
@@ -59,13 +59,14 @@ func NewFromConfig(typ string, other map[string]interface{}) (v api.Vehicle, err
 	}
 
 	factory, err := registry.Get(strings.ToLower(typ))
-	if err == nil {
-		if v, err = factory(cc.Other); err != nil {
-			err = fmt.Errorf("cannot create vehicle '%s': %w", typ, err)
-		}
-	} else {
-		err = fmt.Errorf("invalid vehicle type: %s", typ)
+	if err != nil {
+		return nil, err
+	}
+
+	v, err := factory(cc.Other)
+	if err != nil {
+		err = fmt.Errorf("cannot create vehicle type '%s': %w", typ, err)
 	}
 
-	return
+	return v, err
 }


Test patch:
diff --git a/core/loadpoint_effective_test.go b/core/loadpoint_effective_test.go
new file mode 100644
index 00000000..060a836f
--- /dev/null
+++ b/core/loadpoint_effective_test.go
@@ -0,0 +1,66 @@
+package core
+
+import (
+	"testing"
+
+	"github.com/evcc-io/evcc/api"
+	"github.com/evcc-io/evcc/util"
+	"github.com/golang/mock/gomock"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestEffectiveLimitSoc(t *testing.T) {
+	lp := NewLoadpoint(util.NewLogger("foo"), nil)
+	assert.Equal(t, 100, lp.effectiveLimitSoc())
+}
+
+func TestEffectiveMinMaxCurrent(t *testing.T) {
+	tc := []struct {
+		chargerMin, chargerMax     float64
+		vehicleMin, vehicleMax     float64
+		effectiveMin, effectiveMax float64
+	}{
+		{0, 0, 0, 0, 6, 16},
+		{1, 10, 0, 0, 1, 10},     // charger lower
+		{10, 20, 0, 0, 10, 16},   // charger higher - max ignored
+		{0, 0, 1, 10, 6, 10},     // vehicle lower - min ignored
+		{0, 0, 10, 20, 10, 16},   // vehicle higher - max ignored
+		{1, 10, 2, 12, 1, 10},    // charger + vehicle lower
+		{10, 20, 12, 22, 10, 16}, // charger + vehicle higher
+	}
+
+	for _, tc := range tc {
+		t.Logf("%+v", tc)
+		ctrl := gomock.NewController(t)
+
+		lp := NewLoadpoint(util.NewLogger("foo"), nil)
+		lp.charger = api.NewMockCharger(ctrl)
+
+		if tc.chargerMin+tc.chargerMax > 0 {
+			currentLimiter := api.NewMockCurrentLimiter(ctrl)
+			currentLimiter.EXPECT().GetMinMaxCurrent().Return(tc.chargerMin, tc.chargerMax, nil).AnyTimes()
+
+			lp.charger = struct {
+				api.Charger
+				api.CurrentLimiter
+			}{
+				Charger:        lp.charger,
+				CurrentLimiter: currentLimiter,
+			}
+		}
+
+		if tc.vehicleMin+tc.vehicleMax > 0 {
+			vehicle := api.NewMockVehicle(ctrl)
+			ac := api.ActionConfig{
+				MinCurrent: tc.vehicleMin,
+				MaxCurrent: tc.vehicleMax,
+			}
+			vehicle.EXPECT().OnIdentified().Return(ac).AnyTimes()
+
+			lp.vehicle = vehicle
+		}
+
+		assert.Equal(t, tc.effectiveMin, lp.effectiveMinCurrent())
+		assert.Equal(t, tc.effectiveMax, lp.effectiveMaxCurrent())
+	}
+}
diff --git a/core/loadpoint_phases_test.go b/core/loadpoint_phases_test.go
index 52fa6cdd..2c92c068 100644
--- a/core/loadpoint_phases_test.go
+++ b/core/loadpoint_phases_test.go
@@ -125,7 +125,7 @@ func TestMaxActivePhases(t *testing.T) {
 func testScale(t *testing.T, lp *Loadpoint, sitePower float64, direction string, tc testCase) {
 	t.Helper()
 
-	act := lp.activePhases()
+	act := lp.ActivePhases()
 	max := lp.maxActivePhases()
 
 	testDirection := direction[0:1] // (d)own or (u)p
@@ -221,7 +221,7 @@ func TestPvScalePhases(t *testing.T) {
 			t.Error("wrong phases", lp.phases, tc.physical)
 		}
 
-		if phs := lp.activePhases(); phs != tc.actExpected {
+		if phs := lp.ActivePhases(); phs != tc.actExpected {
 			t.Errorf("expected active %d, got %d", tc.actExpected, phs)
 		}
 		if phs := lp.maxActivePhases(); phs != tc.maxExpected {
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index 89330116..a08ab6c4 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -88,9 +88,6 @@ func TestNew(t *testing.T) {
 	if lp.phases != 0 {
 		t.Errorf("Phases %v", lp.phases)
 	}
-	if lp.MinCurrent != minA {
-		t.Errorf("MinCurrent %v", lp.MinCurrent)
-	}
 	if lp.MaxCurrent != maxA {
 		t.Errorf("MaxCurrent %v", lp.MaxCurrent)
 	}
diff --git a/tests/plan.evcc.yaml b/tests/plan.evcc.yaml
index e0f2fb48..344bbedc 100755
--- a/tests/plan.evcc.yaml
+++ b/tests/plan.evcc.yaml
@@ -16,10 +16,8 @@ meters:
 loadpoints:
   - title: Loadpoint
     charger: charger
-    vehicle: vehicleSocCapacity
   - title: Loadpoint with SoC
     charger: chargerSoc
-    vehicle: vehicle
 
 chargers:
   - name: charger
diff --git a/tests/plan.spec.js b/tests/plan.spec.js
index b6be2d2d..e29b246a 100644
--- a/tests/plan.spec.js
+++ b/tests/plan.spec.js
@@ -36,6 +36,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 expect(lp1.getByTestId("plan-marker")).not.toBeVisible();
     await expect(lp1.getByText("Loadpoint", { exact: true })).toBeVisible();
 
@@ -53,9 +57,6 @@ test.describe("basic functionality", async () => {
       "tomorrow 9:30 AM80%"
     );
 
-    await restart(CONFIG);
-    await page.reload();
-
     await expect(lp1.getByTestId("vehicle-status")).toContainText("Target charging starts at");
     await expect(lp1.getByTestId("plan-marker")).toBeVisible();
     await expect(lp1.getByTestId("charging-plan").getByRole("button")).toHaveText(
@@ -74,8 +75,7 @@ test.describe("vehicle variations", async () => {
       const lp1 = await page.getByTestId("loadpoint").first();
 
       // change vehicle
-      await lp1.getByTestId("change-vehicle").click();
-      await lp1.getByRole("button", { name: "Guest vehicle" }).click();
+      await expect(lp1.getByRole("button", { name: "Guest vehicle" })).toBeVisible();
 
       // kWh based limit
       await lp1.getByTestId("limit-energy").getByRole("combobox").selectOption("50 kWh");
@@ -92,7 +92,7 @@ test.describe("vehicle variations", async () => {
       const lp1 = await page.getByTestId("loadpoint").first();
 
       // change vehicle
-      await lp1.getByTestId("change-vehicle").click();
+      await lp1.getByRole("button", { name: "Guest vehicle" }).click();
       await lp1.getByRole("button", { name: "Vehicle no SoC no Capacity" }).click();
 
       // kWh based limit
@@ -110,7 +110,7 @@ test.describe("vehicle variations", async () => {
       const lp1 = await page.getByTestId("loadpoint").first();
 
       // change vehicle
-      await lp1.getByTestId("change-vehicle").click();
+      await lp1.getByRole("button", { name: "Guest vehicle" }).click();
       await lp1.getByRole("button", { name: "Vehicle no SoC with Capacity" }).click();
 
       // kWh based limit
@@ -128,7 +128,7 @@ test.describe("vehicle variations", async () => {
       const lp1 = await page.getByTestId("loadpoint").first();
 
       // change vehicle
-      await lp1.getByTestId("change-vehicle").click();
+      await lp1.getByRole("button", { name: "Guest vehicle" }).click();
       await lp1.getByRole("button", { name: "Vehicle with SoC no Capacity" }).click();
 
       // soc based limit
@@ -146,7 +146,7 @@ test.describe("vehicle variations", async () => {
       const lp1 = await page.getByTestId("loadpoint").first();
 
       // change vehicle
-      await lp1.getByTestId("change-vehicle").click();
+      await lp1.getByRole("button", { name: "Guest vehicle" }).click();
       await lp1.getByRole("button", { name: "Vehicle with SoC with Capacity" }).click();
 
       // soc based limit
@@ -164,8 +164,7 @@ test.describe("vehicle variations", async () => {
       const lp2 = await page.getByTestId("loadpoint").last();
 
       // change vehicle
-      await lp2.getByTestId("change-vehicle").click();
-      await lp2.getByRole("button", { name: "Guest Vehicle" }).click();
+      await expect(lp2.getByRole("button", { name: "Guest vehicle" })).toBeVisible();
 
       // soc based limit
       await lp2.getByTestId("limit-soc").getByRole("combobox").selectOption("80%");
@@ -182,7 +181,7 @@ test.describe("vehicle variations", async () => {
       const lp2 = await page.getByTestId("loadpoint").last();
 
       // change vehicle
-      await lp2.getByTestId("change-vehicle").click();
+      await lp2.getByRole("button", { name: "Guest vehicle" }).click();
       await lp2.getByRole("button", { name: "Vehicle no SoC with Capacity" }).click();
 
       // soc based limit
@@ -215,7 +214,7 @@ test.describe("preview", async () => {
       const lp1 = await page.getByTestId("loadpoint").first();
 
       // change vehicle
-      await lp1.getByTestId("change-vehicle").click();
+      await lp1.getByRole("button", { name: "Guest vehicle" }).click();
       await lp1.getByRole("button", { name: c.vehicle }).click();
 
       await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
@@ -256,7 +255,7 @@ test.describe("warnings", async () => {
     const lp1 = await page.getByTestId("loadpoint").first();
 
     // change vehicle
-    await lp1.getByTestId("change-vehicle").click();
+    await lp1.getByRole("button", { name: "Guest vehicle" }).click();
     await lp1.getByRole("button", { name: "Vehicle with SoC with massive Capacity" }).click();
     await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();
 
@@ -272,7 +271,7 @@ test.describe("warnings", async () => {
     const lp1 = await page.getByTestId("loadpoint").first();
 
     // change vehicle
-    await lp1.getByTestId("change-vehicle").click();
+    await lp1.getByRole("button", { name: "Guest vehicle" }).click();
     await lp1.getByRole("button", { name: "Vehicle with SoC with Capacity" }).click();
     await lp1.getByTestId("charging-plan").getByRole("button", { name: "none" }).click();