Repository: evcc-io/evcc
Pull request #11119 (2023-12-16 09:38:20)

Easee: poll chargerOpMode on demand
Third attempt at fixing #10075

During status updates we will put Pilot Mode, OpMode and charge power into context and check if this makes sense. If these values are inconsistent, we will trigger an OpMode poll. As the poll might be triggered on every interval, I have added a timer to ensure we only perform the poll every 3 mins. 

Leaving as draft for now, I have to build and test this one first.


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/easee.go b/charger/easee.go
index a3d0e037..2b2b5b75 100644
--- a/charger/easee.go
+++ b/charger/easee.go
@@ -45,8 +45,8 @@ type Easee struct {
 	*request.Helper
 	charger                 string
 	site, circuit           int
-	updated                 time.Time
 	lastEnergyPollTriggered time.Time
+	lastOpModePollTriggered time.Time
 	log                     *util.Logger
 	mux                     sync.Mutex
 	lastEnergyPollMux       sync.Mutex
@@ -58,6 +58,7 @@ type Easee struct {
 	authorize               bool
 	enabled                 bool
 	opMode                  int
+	pilotMode               string
 	reasonForNoCurrent      int
 	phaseMode               int
 	sessionStartEnergy      *float64
@@ -69,6 +70,7 @@ type Easee struct {
 	obsC       chan easee.Observation
 	obsTime    map[easee.ObservationID]time.Time
 	stopTicker chan struct{}
+	once       sync.Once
 }
 
 func init() {
@@ -243,11 +245,6 @@ func (c *Easee) subscribe(client signalr.Client) {
 				if err := <-client.Send("SubscribeWithCurrentState", c.charger, true); err != nil {
 					c.log.ERROR.Printf("SubscribeWithCurrentState: %v", err)
 				}
-				// poll opMode from charger as API can give outdated initial data after (re)connect
-				uri := fmt.Sprintf("%s/chargers/%s/commands/poll_chargeropmode", easee.API, c.charger)
-				if _, err := c.Post(uri, request.JSONContent, nil); err != nil {
-					c.log.WARN.Printf("failed to poll CHARGER_OP_MODE, results may vary: %v", err)
-				}
 			}
 		}
 	}()
@@ -255,10 +252,7 @@ func (c *Easee) subscribe(client signalr.Client) {
 
 // ProductUpdate implements the signalr receiver
 func (c *Easee) ProductUpdate(i json.RawMessage) {
-	var (
-		once sync.Once
-		res  easee.Observation
-	)
+	var res easee.Observation
 
 	if err := json.Unmarshal(i, &res); err != nil {
 		c.log.ERROR.Printf("invalid message: %s %v", i, err)
@@ -278,13 +272,6 @@ func (c *Easee) ProductUpdate(i json.RawMessage) {
 	c.mux.Lock()
 	defer c.mux.Unlock()
 
-	if c.updated.IsZero() {
-		defer once.Do(func() {
-			close(c.done)
-		})
-	}
-	c.updated = time.Now()
-
 	if prevTime, ok := c.obsTime[res.ID]; ok && prevTime.After(res.Timestamp) {
 		// received observation is outdated, ignoring
 		return
@@ -373,8 +360,13 @@ func (c *Easee) ProductUpdate(i json.RawMessage) {
 
 		c.opMode = opMode
 
+		// startup completed
+		c.once.Do(func() { close(c.done) })
+
 	case easee.REASON_FOR_NO_CURRENT:
 		c.reasonForNoCurrent = value.(int)
+	case easee.PILOT_MODE:
+		c.pilotMode = value.(string)
 	}
 
 	select {
@@ -414,6 +406,7 @@ func (c *Easee) chargers() ([]easee.Charger, error) {
 // Status implements the api.Charger interface
 func (c *Easee) Status() (api.ChargeStatus, error) {
 	c.updateSmartCharging()
+	c.confirmStatusConsistency()
 
 	c.mux.Lock()
 	defer c.mux.Unlock()
@@ -840,7 +833,29 @@ func (c *Easee) updateSmartCharging() {
 	}
 }
 
+var _ loadpoint.Controller = (*Easee)(nil)
+
 // LoadpointControl implements loadpoint.Controller
 func (c *Easee) LoadpointControl(lp loadpoint.API) {
 	c.lp = lp
 }
+
+// checks that opMode matches powerflow and polls if inconsistent
+func (c *Easee) confirmStatusConsistency() {
+	c.mux.Lock()
+	opCharging := c.opMode == easee.ModeCharging
+	pilotCharging := c.pilotMode == "C"
+	powerFlowing := c.currentPower > 0
+	c.mux.Unlock()
+
+	if opCharging != powerFlowing || opCharging != pilotCharging {
+		// poll opMode from charger as API can give outdated data after SignalR (re)connect
+		if time.Since(c.lastOpModePollTriggered) > time.Minute*3 { // api rate limit, max once in 3 minutes
+			uri := fmt.Sprintf("%s/chargers/%s/commands/poll_chargeropmode", easee.API, c.charger)
+			if _, err := c.Post(uri, request.JSONContent, nil); err != nil {
+				c.log.WARN.Printf("failed to poll CHARGER_OP_MODE, results may vary: %v", err)
+			}
+			c.lastOpModePollTriggered = time.Now()
+		}
+	}
+}
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/ocpp.go b/charger/ocpp.go
index 9d9854fe..94bad37f 100644
--- a/charger/ocpp.go
+++ b/charger/ocpp.go
@@ -493,6 +493,8 @@ func (c *OCPP) phases1p3p(phases int) error {
 // 	return "", errors.New("not implemented")
 // }
 
+var _ loadpoint.Controller = (*OCPP)(nil)
+
 // LoadpointControl implements loadpoint.Controller
 func (c *OCPP) LoadpointControl(lp loadpoint.API) {
 	c.lp = lp
diff --git a/charger/openwb-2.0.go b/charger/openwb-2.0.go
index 35c8a30d..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
@@ -228,5 +228,5 @@ func (wb *OpenWB20) identify() (string, error) {
 	if err != nil {
 		return "", err
 	}
-	return utf16BytesToString(b, binary.BigEndian), nil
+	return bytesAsString(b), nil
 }
diff --git a/charger/openwb.go b/charger/openwb.go
index 62d1db99..baed6950 100644
--- a/charger/openwb.go
+++ b/charger/openwb.go
@@ -74,7 +74,7 @@ func NewOpenWB(log *util.Logger, mqttconf mqtt.Config, id int, topic string, p1p
 	}
 
 	// check if loadpoint configured
-	configured, err := to.BoolGetter(mq(openwb.ConfiguredTopic))
+	configured, err := provider.NewMqtt(log, client, fmt.Sprintf("%s/lp/%d/%s", topic, id, openwb.ConfiguredTopic), timeout).BoolGetter()
 	if err != nil {
 		return nil, err
 	}
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/shelly.go b/charger/shelly.go
index 67f0cba1..e992bb42 100644
--- a/charger/shelly.go
+++ b/charger/shelly.go
@@ -59,8 +59,7 @@ func (c *Shelly) Enabled() (bool, error) {
 
 // Enable implements the api.Charger interface
 func (c *Shelly) Enable(enable bool) error {
-	err := c.conn.Enable(enable)
-	if err != nil {
+	if err := c.conn.Enable(enable); err != nil {
 		return err
 	}
 
diff --git a/charger/switchsocket.go b/charger/switchsocket.go
index 7e4f5fe6..99ef1333 100644
--- a/charger/switchsocket.go
+++ b/charger/switchsocket.go
@@ -1,6 +1,9 @@
 package charger
 
-import "github.com/evcc-io/evcc/api"
+import (
+	"github.com/evcc-io/evcc/api"
+	"github.com/evcc-io/evcc/core/loadpoint"
+)
 
 // switchSocket implements the api.Charger Status and CurrentPower methods
 // using basic generic switch socket functions
@@ -9,6 +12,7 @@ type switchSocket struct {
 	enabled      func() (bool, error)
 	currentPower func() (float64, error)
 	standbypower float64
+	lp           loadpoint.API
 }
 
 func NewSwitchSocket(
@@ -27,6 +31,10 @@ func NewSwitchSocket(
 
 // Status calculates a generic switches status
 func (c *switchSocket) Status() (api.ChargeStatus, error) {
+	if c.lp != nil && c.lp.GetMode() == api.ModeOff {
+		return api.StatusA, nil
+	}
+
 	res := api.StatusB
 
 	// static mode
@@ -83,3 +91,10 @@ func (c *switchSocket) CurrentPower() (float64, error) {
 
 	return power, err
 }
+
+var _ loadpoint.Controller = (*switchSocket)(nil)
+
+// LoadpointControl implements loadpoint.Controller
+func (c *switchSocket) LoadpointControl(lp loadpoint.API) {
+	c.lp = lp
+}
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/charger/warp2.go b/charger/warp2.go
index cd405eb0..dd15be57 100644
--- a/charger/warp2.go
+++ b/charger/warp2.go
@@ -60,19 +60,19 @@ func NewWarp2FromConfig(other map[string]interface{}) (api.Charger, error) {
 	}
 
 	var currentPower, totalEnergy func() (float64, error)
-	if wb.hasFeature(cc.Topic, warp.FeatureMeter) {
+	if wb.hasFeature(cc.Topic, warp.FeatureMeter, cc.Timeout) {
 		currentPower = wb.currentPower
 		totalEnergy = wb.totalEnergy
 	}
 
 	var currents, voltages func() (float64, float64, float64, error)
-	if wb.hasFeature(cc.Topic, warp.FeatureMeterPhases) {
+	if wb.hasFeature(cc.Topic, warp.FeatureMeterPhases, cc.Timeout) {
 		currents = wb.currents
 		voltages = wb.voltages
 	}
 
 	var identity func() (string, error)
-	if wb.hasFeature(cc.Topic, warp.FeatureNfc) {
+	if wb.hasFeature(cc.Topic, warp.FeatureNfc, cc.Timeout) {
 		identity = wb.identify
 	}
 
@@ -160,14 +160,14 @@ func NewWarp2(mqttconf mqtt.Config, topic, emTopic string, timeout time.Duration
 	return wb, nil
 }
 
-func (wb *Warp2) hasFeature(root, feature string) bool {
+func (wb *Warp2) hasFeature(root, feature string, timeout time.Duration) bool {
 	if wb.features != nil {
 		return slices.Contains(wb.features, feature)
 	}
 
 	topic := fmt.Sprintf("%s/info/features", root)
 
-	if dataG, err := provider.NewMqtt(wb.log, wb.client, topic, 0).StringGetter(); err == nil {
+	if dataG, err := provider.NewMqtt(wb.log, wb.client, topic, timeout).StringGetter(); err == nil {
 		if data, err := dataG(); err == nil {
 			if err := json.Unmarshal([]byte(data), &wb.features); err == nil {
 				return slices.Contains(wb.features, feature)
diff --git a/cmd/configure/configure.go b/cmd/configure/configure.go
index 43fe8704..b1b832d6 100644
--- a/cmd/configure/configure.go
+++ b/cmd/configure/configure.go
@@ -17,15 +17,14 @@ type device struct {
 }
 
 type loadpoint struct {
-	Title             string // TODO Perspektivisch können wir was aus core wiederverwenden, für später
-	Charger           string
-	ChargeMeter       string
-	Vehicle           string
-	Mode              string
-	MinCurrent        int
-	MaxCurrent        int
-	Phases            int
-	ResetOnDisconnect string
+	Title       string // TODO Perspektivisch können wir was aus core wiederverwenden, für später
+	Charger     string
+	ChargeMeter string
+	Vehicle     string
+	Mode        string
+	MinCurrent  int
+	MaxCurrent  int
+	Phases      int
 }
 
 type globalConfig struct {
diff --git a/cmd/configure/configure.tpl b/cmd/configure/configure.tpl
index 1bc1e6d7..07c85359 100644
--- a/cmd/configure/configure.tpl
+++ b/cmd/configure/configure.tpl
@@ -57,7 +57,6 @@ loadpoints:
   phases: {{ .Phases }}
   mincurrent: {{ .MinCurrent }}
   maxcurrent: {{ .MaxCurrent }}
-  resetOnDisconnect: {{ .ResetOnDisconnect }}
 {{- end }}
 {{- end }}
 
diff --git a/cmd/configure/main.go b/cmd/configure/main.go
index 283584b9..041142df 100644
--- a/cmd/configure/main.go
+++ b/cmd/configure/main.go
@@ -426,12 +426,6 @@ func (c *CmdConfigure) configureLoadpoints() {
 
 		fmt.Println()
 		loadpoint.Mode = c.askValue(question{valueType: templates.TypeChargeModes, excludeNone: true})
-
-		fmt.Println()
-		loadpoint.ResetOnDisconnect = c.askValue(question{
-			label:     c.localizedString("Loadpoint_ResetOnDisconnect"),
-			valueType: templates.TypeBool,
-		})
 		c.configuration.AddLoadpoint(loadpoint)
 
 		fmt.Println()
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 01775efe..432999a6 100644
--- a/cmd/setup.go
+++ b/cmd/setup.go
@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"net"
 	"net/http"
+	"regexp"
 	"slices"
 	"strconv"
 	"strings"
@@ -68,6 +69,8 @@ var conf = globalConfig{
 	},
 }
 
+var nameRE = regexp.MustCompile(`^[a-zA-Z0-9_.:-]+$`)
+
 type globalConfig struct {
 	Network      networkConfig
 	Log          string
@@ -271,11 +274,15 @@ func configureChargers(static []config.Named, names ...string) error {
 }
 
 func vehicleInstance(cc config.Named) (api.Vehicle, error) {
+	if !nameRE.MatchString(cc.Name) {
+		return nil, fmt.Errorf("vehicle name must not contain special characters or spaces: %s", cc.Name)
+	}
+
 	instance, err := vehicle.NewFromConfig(cc.Type, cc.Other)
 	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
@@ -585,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 {
@@ -661,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 d4c2e415..cff26970 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -384,8 +384,20 @@ func (lp *Loadpoint) pushEvent(event string) {
 
 // publish sends values to UI and databases
 func (lp *Loadpoint) publish(key string, val interface{}) {
-	if lp.uiChan != nil {
-		lp.uiChan <- util.Param{Key: key, Val: val}
+	// test helper
+	if lp.uiChan == nil {
+		return
+	}
+
+	p := util.Param{Key: key, Val: val}
+
+	// https://github.com/evcc-io/evcc/issues/11191 prevent deadlock
+	select {
+	case lp.uiChan <- p:
+	default:
+		go func() {
+			lp.uiChan <- p
+		}()
 	}
 }
 
@@ -522,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() {
@@ -570,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)
@@ -808,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.
@@ -1040,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
 
@@ -1144,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 3d97340a..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
 }
@@ -360,9 +362,15 @@ func (site *Site) publish(key string, val interface{}) {
 		val = s.String()
 	}
 
-	site.uiChan <- util.Param{
-		Key: key,
-		Val: val,
+	p := util.Param{Key: key, Val: val}
+
+	// https://github.com/evcc-io/evcc/issues/11191 prevent deadlock
+	select {
+	case site.uiChan <- p:
+	default:
+		go func() {
+			site.uiChan <- p
+		}()
 	}
 }
 
@@ -811,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)
@@ -819,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/evcc.dist.yaml b/evcc.dist.yaml
index 338cb8f8..65a78e45 100644
--- a/evcc.dist.yaml
+++ b/evcc.dist.yaml
@@ -95,8 +95,6 @@ vehicles:
     vin: WREN...
     onIdentify: # set defaults when vehicle is identified
       mode: pv # enable PV-charging when vehicle is identified
-      minSoc: 20 # immediately charge to 20% regardless of mode unless "off" (disabled)
-      targetSoc: 90 # limit charge to 90%
 
 # site describes the EVU connection, PV and home battery
 site:
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 39862337..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]
@@ -209,6 +213,7 @@ currents = "Ladestrøm"
 default = "standard"
 disclaimerHint = "Bemærk:"
 disclaimerText = "Ændringer vil gå tabt, efter at serveren er genstartet."
+onlyForSocBasedCharging = "Disse indstillinger er kun tilgængelig for køretøjer med kendt opladningsniveau"
 title = "Indstillinger {0}"
 vehicle = "Køretøj"
 
@@ -248,12 +253,13 @@ logout = "Log ud"
 activate = "Tænd"
 co2Limit = "CO₂ grænse på {co2}"
 costLimitIgnore = "Den konfigurerede {limit} vil blive ignoreret i denne periode."
-currentPlan = "Opladningsplan"
+currentPlan = "Aktiv plan"
 descriptionEnergy = "Hvornår skal {targetEnergy} være sendt til køretøjet?"
 descriptionSoc = "Hvornår skal køretøjet være opladt til {targetSoc} %?"
 inactiveLabel = "Planlagt tid"
 modalTitle = "Sæt måltid"
 noActivePlan = "Ingen aktiv plan"
+notReachableInTime = "Mål kan ikke opnås i tide. Forventet færdig: {endTime}."
 onlyInPvMode = "Opladningsplan virker kun i solar mode"
 planDescription = "Angiv et afgangstidpunkt og evcc vil oplade køretøjet så billigt og miljøvenligt som muligt"
 planDuration = "Opladningstid"
@@ -262,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 6f2114e3..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"
@@ -179,6 +179,7 @@ departureTab = "Salidas"
 goal = "Objetivo de la carga"
 modalTitle = "Plan de carga"
 none = "ninguno"
+remove = "Quitar"
 time = "Tiempo"
 title = "Plan"
 titleMinSoc = "Carga mínima"
@@ -217,6 +218,7 @@ currents = "Corriente de carga"
 default = "defecto"
 disclaimerHint = "Nota:"
 disclaimerText = "Actualmente, los cambios aún no son persistentes y se restablecen al iniciar el servicio."
+onlyForSocBasedCharging = "Estas opciones sólo están disponibles para vehículos con nivel de carga conocido"
 title = "Configuraciones {0}"
 vehicle = "Vehículo"
 
@@ -256,12 +258,13 @@ logout = "cerrar sesión"
 activate = "Activar"
 co2Limit = "Límite de CO₂ del {co2}"
 costLimitIgnore = "El {limit} configurado será ignorado durante este periodo."
-currentPlan = "Horarios programados"
+currentPlan = "Plan activo"
 descriptionEnergy = "¿Hasta cuándo deben haberse cargado {targetEnergy} al vehículo?"
 descriptionSoc = "¿Hasta cuándo debe haberse cargarse el vehículo al {targetSoc}%?"
 inactiveLabel = "Tiempo objetivo"
 modalTitle = "Establecer el tiempo objetivo"
 noActivePlan = "Ningún plan activo"
+notReachableInTime = "Objetivo no alcanzable en el tiempo. Fin estimado: {endTime}"
 onlyInPvMode = "El plan de carga sólo funciona en modo solar"
 planDescription = "Introduzca una hora de salida y evcc cargará el vehículo de la forma más económica o ecológica posible"
 planDuration = "Tiempo de carga"
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 051b15c0..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"
@@ -179,6 +179,7 @@ departureTab = "Išvykimas"
 goal = "Įkrovimo tikslas"
 modalTitle = "Įkrovimo planas"
 none = "nėra"
+remove = "Pašalinti"
 time = "Laikas"
 title = "Planas"
 titleMinSoc = "Minimali įkrova"
@@ -217,6 +218,7 @@ currents = "Įkrovimo srovė"
 default = "standartiškai"
 disclaimerHint = "Pastaba:"
 disclaimerText = "Šie pakeitimai neišlieka po evcc serverio restarto."
+onlyForSocBasedCharging = "Šie pasirinkimai galimi tik automobiliams su žinomu įkrovos lygiu."
 title = "Nustatymai {0}"
 vehicle = "Automobilis"
 
@@ -256,12 +258,13 @@ logout = "atsijungti"
 activate = "Aktyvuoti"
 co2Limit = "CO₂ riba iš {co2}"
 costLimitIgnore = "Šiuo laikotarpiu sukonfigūruotas {limit} bus ignoruojamas."
-currentPlan = "Įkrovimo grafikas"
+currentPlan = "Planas aktyvus"
 descriptionEnergy = "Iki kada {targetEnergy} turėtų būti įkrauta į automobilį?"
 descriptionSoc = "Kada automobilis turėtų būti įkrautas iki {targetSoc}%?"
 inactiveLabel = "Suplanuotas laikas"
 modalTitle = "Nustatyti įkrovimo pabaigos laiką"
 noActivePlan = "Nėra Aktyvaus Plano"
+notReachableInTime = "Tikslo pasiekti nespėsime. Numatoma pabaiga: {endTime}."
 onlyInPvMode = "Įkrovimo planas veikia tik nustatyme Saulė."
 planDescription = "Įveskite išvykimo laiką ir evcc įkraus automobilį kiek įmanoma ekonomiškiau arba ekologiškiau."
 planDuration = "Įkrovimo laikas"
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 b9d7cef7..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"
@@ -172,16 +172,19 @@ modalTitle = "Tens a certeza que queres reiniciar"
 vehicles = "Estacionamento"
 
 [main.chargingPlan]
+active = "Ativo"
 arrivalTab = "Chegada"
 day = "Dia"
 departureTab = "Partida"
 goal = "Real de compartilhamento"
 modalTitle = "Plano de carregamento"
 none = "sem"
+remove = "Remover"
 time = "Tempo"
 title = "Plano"
 titleMinSoc = "Carga mínima"
 titleTargetCharge = "Partida"
+update = "Aplicar"
 
 [main.energyflow]
 battery = "Bateria"
@@ -215,6 +218,7 @@ currents = "Corrente de carga"
 default = "predefinado"
 disclaimerHint = "Aviso:"
 disclaimerText = "As alterações serão perdidas depois que o servidor for reiniciado."
+onlyForSocBasedCharging = "Estas opções só estão disponíveis para veículos com nível de carregamento conhecido."
 title = "Configurações {0}"
 vehicle = "Veículo"
 
@@ -254,10 +258,12 @@ logout = "encerrar sessão"
 activate = "Ativar"
 co2Limit = "Limite de CO₂ de {co2}"
 costLimitIgnore = "O {limit} configurado será ignorado durante este período."
+currentPlan = "Plano ativo"
 descriptionEnergy = "Até quando {targetEnergy} deve ser carregado no veículo?"
 descriptionSoc = "Quando o veículo deve ser carregado até {targetSoc}%?"
 inactiveLabel = "Tempo alvo"
 modalTitle = "Definir hora-alvo"
+notReachableInTime = "O objetivo não é alcançável no tempo. Final estimado: {endTime}."
 onlyInPvMode = "O plano de carregamento só funciona no modo solar."
 planDescription = "Entrar um tempo de partida, e evcc irá carregar o veículo o mais rentável ou ambientalmente amigável possível."
 planDuration = "Tempo de carga"
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 4e05252b..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,12 +253,13 @@ 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"
 modalTitle = "Ange måltid"
 noActivePlan = "Ingen aktiv prenumeration"
+notReachableInTime = "Laddmål kan inte nås i tid. Beräknad sluttid: {endTime}"
 onlyInPvMode = "Laddplan fungerar enbart i sol-läge."
 planDescription = "Ange din avgångstid och evcc kommer att ladda så kostnadseffektivt och miljövänligt som möjligt."
 planDuration = "Laddtid"
@@ -262,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."
@@ -357,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/openwb.go b/meter/openwb.go
index 3f623477..11242cb2 100644
--- a/meter/openwb.go
+++ b/meter/openwb.go
@@ -26,8 +26,8 @@ func NewOpenWBFromConfig(other map[string]interface{}) (api.Meter, error) {
 		Usage       string
 		capacity    `mapstructure:",squash"`
 	}{
-		Topic:   "openWB",
-		Timeout: 15 * time.Second,
+		Topic:   openwb.RootTopic,
+		Timeout: openwb.Timeout,
 	}
 
 	if err := util.DecodeOther(other, &cc); err != nil {
@@ -76,7 +76,8 @@ func NewOpenWBFromConfig(other map[string]interface{}) (api.Meter, error) {
 		currents = collectPhaseProviders(curr)
 
 	case "pv":
-		configuredG, err := to.BoolGetter(mq("%s/pv/1/%s", cc.Topic, openwb.PvConfigured)) // first pv
+		// first pv
+		configuredG, err := provider.NewMqtt(log, client, fmt.Sprintf("%s/pv/1/%s", cc.Topic, openwb.PvConfigured), cc.Timeout).BoolGetter()
 		if err != nil {
 			return nil, err
 		}
@@ -99,7 +100,7 @@ func NewOpenWBFromConfig(other map[string]interface{}) (api.Meter, error) {
 		}
 
 	case "battery":
-		configuredG, err := to.BoolGetter(mq("%s/housebattery/%s", cc.Topic, openwb.BatteryConfigured))
+		configuredG, err := provider.NewMqtt(log, client, fmt.Sprintf("%s/housebattery/%s", cc.Topic, openwb.BatteryConfigured), cc.Timeout).BoolGetter()
 		if err != nil {
 			return nil, 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/config.go b/provider/config.go
index da01fca1..38a540e7 100644
--- a/provider/config.go
+++ b/provider/config.go
@@ -1,6 +1,7 @@
 package provider
 
 import (
+	"errors"
 	"fmt"
 )
 
@@ -43,6 +44,9 @@ func (r providerRegistry) Add(name string, factory func(map[string]interface{})
 }
 
 func (r providerRegistry) Get(name string) (func(map[string]interface{}) (Provider, error), error) {
+	if name == "" {
+		return nil, errors.New("missing configuration")
+	}
 	factory, exists := r[name]
 	if !exists {
 		return nil, fmt.Errorf("invalid plugin source: %s", name)
diff --git a/provider/go.go b/provider/go.go
index fc4ca78c..be534d71 100644
--- a/provider/go.go
+++ b/provider/go.go
@@ -155,12 +155,22 @@ 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
 	}
 
+	if v.IsNil() {
+		return nil, nil
+	}
+
 	return normalizeValue(v.Interface())
 }
 
diff --git a/provider/javascript.go b/provider/javascript.go
index 9071af70..4480a0dc 100644
--- a/provider/javascript.go
+++ b/provider/javascript.go
@@ -145,6 +145,10 @@ func (p *Javascript) evaluate() (any, error) {
 		return nil, err
 	}
 
+	if vv == nil {
+		return nil, nil
+	}
+
 	return normalizeValue(vv)
 }
 
diff --git a/provider/mqtt/client.go b/provider/mqtt/client.go
index 98b4ef6f..e5932464 100644
--- a/provider/mqtt/client.go
+++ b/provider/mqtt/client.go
@@ -129,7 +129,7 @@ func (m *Client) Publish(topic string, retained bool, payload interface{}) error
 	return nil
 }
 
-// Listen validates uniqueness and registers and attaches listener
+// Listen attaches listener to slice of listeners for given topic
 func (m *Client) Listen(topic string, callback func(string)) error {
 	m.mux.Lock()
 	m.listener[topic] = append(m.listener[topic], callback)
diff --git a/provider/transformation.go b/provider/transformation.go
index eedd6d24..da03d6a2 100644
--- a/provider/transformation.go
+++ b/provider/transformation.go
@@ -145,6 +145,10 @@ func transformInputs(in []inputTransformation, set func(string, any) error) erro
 
 func transformOutputs(out []outputTransformation, v any) error {
 	for _, cc := range out {
+		if v == nil {
+			return fmt.Errorf("no value to transform")
+		}
+
 		if err := cc.function(v); err != nil {
 			return fmt.Errorf("%s: %w", cc.name, err)
 		}
diff --git a/server/mqtt.go b/server/mqtt.go
index 3ab37436..f52c4144 100644
--- a/server/mqtt.go
+++ b/server/mqtt.go
@@ -287,7 +287,8 @@ func (m *MQTT) listenLoadpointSetters(topic string, site site.API, lp loadpoint.
 	}
 	if err == nil {
 		err = m.Handler.ListenSetter(topic+"/vehicle", func(payload string) error {
-			if payload == "" {
+			// https://github.com/evcc-io/evcc/issues/11184 empty payload is swallowed by listener
+			if payload == "-" {
 				lp.SetVehicle(nil)
 				return nil
 			}
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/huawei-sun2000-dongle-powersensor.yaml b/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml
index 69df646a..47c24ae3 100644
--- a/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml
+++ b/templates/definition/meter/huawei-sun2000-dongle-powersensor.yaml
@@ -23,7 +23,6 @@ render: |
   power:
     source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     connectdelay: 1s
     register:
@@ -34,7 +33,6 @@ render: |
   energy:
     source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     register:
       address: 37121 # Active energy import from the grid
@@ -44,7 +42,6 @@ render: |
   currents:
   - source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     register:
       address: 37107 # Huawei phase A grid current
@@ -53,7 +50,6 @@ render: |
     scale: -0.01
   - source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     register:
       address: 37109 # Huawei phase B grid current
@@ -62,7 +58,6 @@ render: |
     scale: -0.01
   - source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     register:
       address: 37111 # Huawei phase C grid current
@@ -74,7 +69,6 @@ render: |
   power:
     source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     connectdelay: 1s
     register:
@@ -84,7 +78,6 @@ render: |
   energy:
     source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     register:
       address: 32106 # Accumulated energy yield
@@ -96,7 +89,6 @@ render: |
   power:
     source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     connectdelay: 1s
     register:
@@ -112,7 +104,6 @@ render: |
   soc:
     source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     register:
       {{- if eq .storageunit "1" }}
@@ -127,7 +118,6 @@ render: |
   energy:
     source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     register:
       {{- if eq .storageunit "1" }}
diff --git a/templates/definition/meter/huawei-sun2000-dongle.yaml b/templates/definition/meter/huawei-sun2000-dongle.yaml
index f4fe7b12..f6987b43 100644
--- a/templates/definition/meter/huawei-sun2000-dongle.yaml
+++ b/templates/definition/meter/huawei-sun2000-dongle.yaml
@@ -15,7 +15,6 @@ render: |
   power:
     source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     connectdelay: 1s
     register:
@@ -25,7 +24,6 @@ render: |
   energy:
     source: modbus
     {{- include "modbus" . | indent 2 }}
-    model: huawei
     timeout: {{ .timeout }}
     register:
       address: 32106 # Accumulated energy yield
diff --git a/templates/definition/meter/keba-kecontact.yaml b/templates/definition/meter/keba-kecontact.yaml
new file mode 100644
index 00000000..deca220e
--- /dev/null
+++ b/templates/definition/meter/keba-kecontact.yaml
@@ -0,0 +1,26 @@
+template: keba-kecontact
+# clone of kostal-ksem with different slave id
+products:
+  - brand: KEBA
+    description:
+      generic: KeContact E10
+params:
+  - name: usage
+    choice: ["grid"]
+  - name: modbus
+    choice: ["tcpip"]
+    id: 1
+render: |
+  type: modbus
+  {{- include "modbus" . }}
+  model: sunspec
+  power: Power
+  energy: Import
+  currents:
+    - CurrentL1
+    - CurrentL2
+    - CurrentL3
+  powers:
+    - PowerL1
+    - PowerL2
+    - PowerL3
diff --git a/templates/definition/meter/kostal-plenticore.yaml b/templates/definition/meter/kostal-plenticore.yaml
index ae4e8641..9d8a73ed 100644
--- a/templates/definition/meter/kostal-plenticore.yaml
+++ b/templates/definition/meter/kostal-plenticore.yaml
@@ -3,6 +3,7 @@ products:
   - brand: Kostal
     description:
       generic: Plenticore Hybrid
+capabilities: ["battery-control"]
 linked:
   - template: kostal-ksem-inverter
     usage: grid
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/smartfox-em2.yaml b/templates/definition/meter/smartfox-em2.yaml
new file mode 100644
index 00000000..3901c38c
--- /dev/null
+++ b/templates/definition/meter/smartfox-em2.yaml
@@ -0,0 +1,82 @@
+template: smartfox-em2
+products:
+  - brand: Smartfox
+    description:
+      generic: Pro, Pro 2, Pro Light, Pro Light 2, Light, Reg, Reg extended (EM2 firmware)
+requirements:
+  description:
+    de: |
+      Kann verwendet werden, um Daten für `grid`, `pv` und `aux` zu erhalten. 
+      Wenn `usage` nicht definiert ist, wird die Leistung für die Warmwasserbereitung zurückgegeben (als `aux` zu verwenden).
+    en: |
+      Can be used to get `grid`, `pv` and `aux` data. 
+      If `usage` is not defined, then return the power for the water heating (to be used as `aux`).
+params:
+  - name: usage
+    choice: ["grid", "pv"]
+  - name: host
+render: |
+  {{- define "uri" -}}
+  http://{{ .host }}/values.xml
+  {{- end }}
+  type: custom
+  # jq: parse json generated from response values.xml (https://jqplay.org is your friend to test queries)
+  {{- if not .usage }}
+  power: # heating power in W
+    source: http
+    uri: {{ include "uri" . }}
+    jq: .values.value[] | select(.attrid=="htPowerMeasValue")."#content" | rtrimstr(" kW")
+    scale: 1000
+  {{- end }}
+  {{- if eq .usage "grid" }}
+  power: # grid power in W
+    source: http
+    uri: {{ include "uri" . }}
+    jq: .values.value[] | select(.attrid=="detailsPowerValue")."#content" | rtrimstr(" W")
+  energy: # grid energy in kWh
+    source: http
+    uri: {{ include "uri" . }}
+    jq: .values.value[] | select(.attrid=="energyValue")."#content" | rtrimstr(" kWh")
+  voltages: # grid voltages in V
+  - source: http
+    uri: {{ include "uri" . }}
+    jq: .values.value[] | select(.attrid=="voltageL1Value")."#content" | rtrimstr(" V")
+  - source: http
+    uri: {{ include "uri" . }}
+    jq: .values.value[] | select(.attrid=="voltageL2Value")."#content" | rtrimstr(" V")
+  - source: http
+    uri: {{ include "uri" . }}
+    jq: .values.value[] | select(.attrid=="voltageL3Value")."#content" | rtrimstr(" V")
+  currents: # grid currents in A
+  - source: http
+    uri: {{ include "uri" . }}
+    jq: .values.value[] | select(.attrid=="ampereL1Value")."#content" | rtrimstr(" A")
+  - source: http
+    uri: {{ include "uri" . }}
+    jq: .values.value[] | select(.attrid=="ampereL2Value")."#content" | rtrimstr(" A")
+  - source: http
+    uri: {{ include "uri" . }}
+    jq: .values.value[] | select(.attrid=="ampereL3Value")."#content" | rtrimstr(" A")
+  powers: # grid powers in W
+  - source: http
+    uri: {{ include "uri" . }}
+    jq: .values.value[] | select(.attrid=="powerL1Value")."#content" | rtrimstr(" W")
+  - source: http
+    uri: {{ include "uri" . }}
+    jq: .values.value[] | select(.attrid=="powerL2Value")."#content" | rtrimstr(" W")
+  - source: http
+    uri: {{ include "uri" . }}
+    jq: .values.value[] | select(.attrid=="powerL3Value")."#content" | rtrimstr(" W")
+  {{- end }}
+  {{- if eq .usage "pv" }}
+  # PV power and energy values are delivered for each inverter, we select them via regex and sum the values
+  power: # PV power in W
+    source: http
+    uri: {{ include "uri" . }}
+    jq: '[.values.value[] | select(.attrid|test("wr\\d+PowerValue"))."#content" | rtrimstr(" kW") | tonumber] | add'
+    scale: 1000 # wr1PowerValue, ..., wr5PowerValue are in kW
+  energy: # PV energy in kWh
+    source: http
+    uri: {{ include "uri" . }}
+    jq: '[.values.value[] | select(.attrid|test("wr\\d+EnergyValue"))."#content" | rtrimstr(" kWh") | tonumber] | add'
+  {{- end }}
diff --git a/templates/definition/meter/sofarsolar-g3.yaml b/templates/definition/meter/sofarsolar-g3.yaml
index cb232641..ad15fa59 100644
--- a/templates/definition/meter/sofarsolar-g3.yaml
+++ b/templates/definition/meter/sofarsolar-g3.yaml
@@ -12,17 +12,21 @@ products:
   - brand: SofarSolar
     description:
       generic: SOFAR 5…24KTL-G3
+capabilities: ["battery-control"]
+requirements:
+  description:
+    de: Es wird dringend empfohlen die Verbindung über einen LSE-3 Logger Stick herzustellen. Andere Verbindungswege (z.B. LSW-3 oder via RS485) sind u.U. instabil und benötigen Verzögerungen (delay) > 0s.
+    en: It is strongly recommended to connect via an LSE-3 logger stick. Other connection paths (e.g. LSW-3 or via RS485) may be unstable and require delays > 0s.
 params:
   - name: usage
     choice: ["grid", "pv", "battery"]
   - name: modbus
-    choice: ["rs485", "tcpip"]
-    baudrate: 9600
-    comset: 8N1
-    port: 502
+    choice: ["tcpip", "rs485"]
+    port: 8899
     id: 1
   - name: delay
-    default: 1s
+    default: 0s
+    advanced: true
   - name: capacity
     advanced: true
   # battery control
@@ -154,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/definition/meter/sungrow-hybrid.yaml b/templates/definition/meter/sungrow-hybrid.yaml
index b87769df..697f7811 100644
--- a/templates/definition/meter/sungrow-hybrid.yaml
+++ b/templates/definition/meter/sungrow-hybrid.yaml
@@ -4,6 +4,7 @@ products:
   - brand: Sungrow
     description:
       generic: SH Series Hybrid Inverter
+capabilities: ["battery-control"]
 requirements:
   description:
     de: Die Verbindung ist seriell (RS485) oder über die native (hintere) LAN-Buchse am Wechselrichter möglich. Verbindungen über das WiNet-S-Dongle (WiFi oder LAN) funktionieren NICHT korrekt.
diff --git a/templates/definition/meter/tesla-powerwall.yaml b/templates/definition/meter/tesla-powerwall.yaml
index 8ef478fb..6c33b672 100644
--- a/templates/definition/meter/tesla-powerwall.yaml
+++ b/templates/definition/meter/tesla-powerwall.yaml
@@ -3,6 +3,7 @@ products:
   - brand: Tesla
     description:
       generic: Powerwall
+capabilities: ["battery-control"]
 requirements:
   description:
     de: |
diff --git a/templates/definition/meter/victron-energy.yaml b/templates/definition/meter/victron-energy.yaml
index 4d66baad..9f003520 100644
--- a/templates/definition/meter/victron-energy.yaml
+++ b/templates/definition/meter/victron-energy.yaml
@@ -3,6 +3,7 @@ products:
   - brand: Victron
     description:
       generic: Energy
+capabilities: ["battery-control"]
 params:
   - name: usage
     choice: ["grid", "pv", "battery"]
diff --git a/templates/definition/vehicle/vw-id.yaml b/templates/definition/vehicle/vw-id.yaml
deleted file mode 100644
index d617c217..00000000
--- a/templates/definition/vehicle/vw-id.yaml
+++ /dev/null
@@ -1,21 +0,0 @@
-template: id
-products:
-  - brand: Volkswagen
-    description:
-      generic: We Connect ID
-requirements:
-  description:
-    de: ID.3, ID.4 (funktioniert auch für e-Golf, e-Up)
-    en: ID.3, ID.4 (also works for e-Golf, e-Up)
-params:
-  - preset: vehicle-base
-  - preset: vehicle-identify
-  - name: vin
-    example: WVWZZZ...
-  - name: timeout
-    default: 10s
-render: |
-  type: id
-  {{ include "vehicle-base" . }}
-  {{ include "vehicle-identify" . }}
-  timeout: {{ .timeout }}
diff --git a/templates/definition/vehicle/vw.yaml b/templates/definition/vehicle/vw.yaml
index f0259b74..382cb8e1 100644
--- a/templates/definition/vehicle/vw.yaml
+++ b/templates/definition/vehicle/vw.yaml
@@ -1,12 +1,13 @@
 template: vw
+covers: ["id"]
 products:
   - brand: Volkswagen
     description:
-      generic: We Connect (legacy)
+      generic: We Connect ID
 requirements:
   description:
-    de: ältere Modelle wie e-Up, e-Golf, etc.
-    en: older Models like e-Up, e-Golf, etc.
+    de: e-Golf, e-Up, ID Familie
+    en: e-Golf, e-Up, ID family
 params:
   - preset: vehicle-base
   - preset: vehicle-identify
diff --git a/templates/evcc.io/.gitignore b/templates/evcc.io/.gitignore
index b5990282..f59ec20a 100644
--- a/templates/evcc.io/.gitignore
+++ b/templates/evcc.io/.gitignore
@@ -1,1 +1,1 @@
-!*.json
\ No newline at end of file
+*
\ No newline at end of file
diff --git a/templates/evcc.io/brands.json b/templates/evcc.io/brands.json
deleted file mode 100644
index 3b22463d..00000000
--- a/templates/evcc.io/brands.json
+++ /dev/null
@@ -1,236 +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",
-  "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/monitor.go b/util/monitor.go
index 7f056a26..98693a7c 100644
--- a/util/monitor.go
+++ b/util/monitor.go
@@ -7,6 +7,7 @@ import (
 	"github.com/evcc-io/evcc/api"
 )
 
+// Monitor monitors values for regular updates
 type Monitor[T any] struct {
 	val     T
 	mu      sync.RWMutex
@@ -16,6 +17,7 @@ type Monitor[T any] struct {
 	timeout time.Duration
 }
 
+// NewMonitor created a new monitor with given timeout
 func NewMonitor[T any](timeout time.Duration) *Monitor[T] {
 	return &Monitor[T]{
 		done:    make(chan struct{}),
@@ -62,9 +64,32 @@ func (m *Monitor[T]) GetFunc(get func(T)) error {
 		default:
 			return api.ErrOutdated
 		}
-	} else if time.Since(m.updated) > m.timeout {
+	}
+
+	if time.Since(m.updated) > m.timeout {
+		err := api.ErrOutdated
+
+		// wait once on very first call
+		if m.updated.IsZero() {
+			m.mu.RUnlock()
+
+			// mark as waited once
+			m.mu.Lock()
+			m.updated.Add(time.Nanosecond)
+			m.mu.Unlock()
+
+			select {
+			case <-m.done:
+				// got value and updated timestamp
+				err = nil
+			case <-time.After(m.timeout):
+			}
+
+			m.mu.RLock()
+		}
+
 		get(m.val)
-		return api.ErrOutdated
+		return err
 	}
 
 	get(m.val)
diff --git a/util/tee.go b/util/tee.go
index 7bbae57d..6fb4e873 100644
--- a/util/tee.go
+++ b/util/tee.go
@@ -20,7 +20,7 @@ type Tee struct {
 func (t *Tee) Attach() <-chan Param {
 	// TODO find better approach to prevent deadlocks
 	// this will buffer the receiver channel to prevent deadlocks when consumers use mutex-protected loadpoint api
-	out := make(chan Param, 16)
+	out := make(chan Param, 128)
 	t.add(out)
 	return out
 }
diff --git a/util/templates/defaults.yaml b/util/templates/defaults.yaml
index d6a581fd..0f76b31c 100644
--- a/util/templates/defaults.yaml
+++ b/util/templates/defaults.yaml
@@ -303,10 +303,6 @@ presets:
     params:
       - name: mode
         advanced: true
-      - name: minSoc
-        advanced: true
-      - name: targetSoc
-        advanced: true
       - name: minCurrent
         advanced: true
       - name: maxCurrent
diff --git a/util/templates/generate/main.go b/util/templates/generate/main.go
index 15934bc4..8e7a9c4b 100644
--- a/util/templates/generate/main.go
+++ b/util/templates/generate/main.go
@@ -20,7 +20,9 @@ const (
 
 func main() {
 	for _, lang := range []string{"de", "en"} {
-		generateDocs(lang)
+		if err := generateDocs(lang); err != nil {
+			panic(err)
+		}
 	}
 
 	if err := generateBrandJSON(); err != nil {
@@ -28,13 +30,13 @@ func main() {
 	}
 }
 
-func generateDocs(lang string) {
+func generateDocs(lang string) error {
 	for _, class := range templates.ClassValues() {
 		path := fmt.Sprintf("%s/%s/%s", docsPath, lang, strings.ToLower(class.String()))
 		_, err := os.Stat(path)
 		if os.IsNotExist(err) {
 			if err := os.MkdirAll(path, 0o755); err != nil {
-				panic(err)
+				return err
 			}
 		}
 		if err := clearDir(path); err != nil {
@@ -42,9 +44,11 @@ func generateDocs(lang string) {
 		}
 
 		if err := generateClass(class, lang); err != nil {
-			panic(err)
+			return err
 		}
 	}
+
+	return nil
 }
 
 func generateClass(class templates.Class, lang string) error {
diff --git a/util/templates/includes/vehicle-identify.tpl b/util/templates/includes/vehicle-identify.tpl
index 9ef7ed7d..41a7bfab 100644
--- a/util/templates/includes/vehicle-identify.tpl
+++ b/util/templates/includes/vehicle-identify.tpl
@@ -1,5 +1,5 @@
 {{ define "vehicle-identify" }}
-{{- if or .mode .minSoc .targetSoc .minCurrent .maxCurrent .priority }}
+{{- if or .mode .minCurrent .maxCurrent .priority }}
 onIdentify:
 {{- if .mode }}
   mode: {{ .mode }}
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/util/templates/types.go b/util/templates/types.go
index 3542a1a2..192f6f7b 100644
--- a/util/templates/types.go
+++ b/util/templates/types.go
@@ -54,14 +54,15 @@ const (
 var ValidDependencies = []string{DependencyCheckEmpty, DependencyCheckNotEmpty, DependencyCheckEqual}
 
 const (
-	CapabilityISO151182 = "iso151182" // ISO 15118-2 support
-	CapabilityMilliAmps = "mA"        // Granular current control support
-	CapabilityRFID      = "rfid"      // RFID support
-	Capability1p3p      = "1p3p"      // 1P/3P phase switching support
-	CapabilitySMAHems   = "smahems"   // SMA HEMS Support
+	CapabilityISO151182      = "iso151182"       // ISO 15118-2 support
+	CapabilityMilliAmps      = "mA"              // Granular current control support
+	CapabilityRFID           = "rfid"            // RFID support
+	Capability1p3p           = "1p3p"            // 1P/3P phase switching support
+	CapabilitySMAHems        = "smahems"         // SMA HEMS support
+	CapabilityBatteryControl = "battery-control" // Battery control support
 )
 
-var ValidCapabilities = []string{CapabilityISO151182, CapabilityMilliAmps, CapabilityRFID, Capability1p3p, CapabilitySMAHems}
+var ValidCapabilities = []string{CapabilityISO151182, CapabilityMilliAmps, CapabilityRFID, Capability1p3p, CapabilitySMAHems, CapabilityBatteryControl}
 
 const (
 	RequirementEEBUS       = "eebus"       // EEBUS Setup is required
diff --git a/vehicle/bmw/api.go b/vehicle/bmw/api.go
index ea7b0ca0..7df5de7c 100644
--- a/vehicle/bmw/api.go
+++ b/vehicle/bmw/api.go
@@ -55,11 +55,13 @@ func (v *API) Status(vin string) (VehicleStatus, error) {
 	var res VehicleStatus
 	uri := fmt.Sprintf("%s/eadrax-vcs/v4/vehicles/state?apptimezone=120&appDateTime=%d", regions[v.region].CocoApiURI, time.Now().UnixMilli())
 
-	req, err := request.New(http.MethodGet, uri, nil, map[string]string{
+	req, _ := request.New(http.MethodGet, uri, nil, map[string]string{
 		"bmw-vin": vin,
 	})
-	if err == nil {
-		err = v.DoJSON(req, &res)
+
+	err := v.DoJSON(req, &res)
+	if err != nil && res.StatusCode != 0 {
+		err = fmt.Errorf("%d: %s (%w)", res.StatusCode, res.Message, err)
 	}
 
 	return res, err
diff --git a/vehicle/bmw/types.go b/vehicle/bmw/types.go
index ddd54fd0..6d2f7aa7 100644
--- a/vehicle/bmw/types.go
+++ b/vehicle/bmw/types.go
@@ -7,7 +7,9 @@ type Vehicle struct {
 }
 
 type VehicleStatus struct {
-	State struct {
+	StatusCode int
+	Message    string
+	State      struct {
 		CurrentMileage        int64
 		Range                 int64
 		ElectricChargingState struct {
diff --git a/vehicle/cloud.go b/vehicle/cloud.go
index 0adaf754..f5ed9dda 100644
--- a/vehicle/cloud.go
+++ b/vehicle/cloud.go
@@ -101,7 +101,6 @@ func (v *Cloud) chargeState() (float64, error) {
 	defer cancel()
 
 	res, err := v.client.SoC(ctx, req)
-
 	if err != nil && strings.Contains(err.Error(), api.ErrMustRetry.Error()) {
 		return 0, api.ErrMustRetry
 	}
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
 }
diff --git a/vehicle/smart/hello/api.go b/vehicle/smart/hello/api.go
index a76e1681..1a1d2a76 100644
--- a/vehicle/smart/hello/api.go
+++ b/vehicle/smart/hello/api.go
@@ -156,8 +156,7 @@ func (v *API) UpdateSession(vin string) error {
 }
 
 func (v *API) Status(vin string) (VehicleStatus, error) {
-	if _, err := v.identity.UpdateSession(vin); err != nil {
-		// if err := v.UpdateSession(vin); err != nil {
+	if err := v.UpdateSession(vin); err != nil {
 		return VehicleStatus{}, fmt.Errorf("update session failed: %w", err)
 	}
 
diff --git a/vehicle/smart/hello/helper.go b/vehicle/smart/hello/helper.go
index c9ee01b1..cf82541f 100644
--- a/vehicle/smart/hello/helper.go
+++ b/vehicle/smart/hello/helper.go
@@ -5,8 +5,8 @@ import (
 	"crypto/md5"
 	"crypto/sha1"
 	"encoding/base64"
-	"encoding/json"
 	"fmt"
+	"io"
 	"net/url"
 	"strconv"
 	"time"
@@ -14,13 +14,13 @@ import (
 	"github.com/samber/lo"
 )
 
-func createSignature(method, path string, params url.Values, post any) (string, string, string, error) {
+func createSignature(method, path string, params url.Values, body io.Reader) (string, string, string, error) {
 	nonce := lo.RandomString(16, lo.AlphanumericCharset)
 	ts := strconv.FormatInt(time.Now().UnixMilli(), 10)
 
 	md5Hash := "1B2M2Y8AsgTpgAmY7PhCfg=="
-	if post != nil {
-		bytes, err := json.Marshal(post)
+	if body != nil {
+		bytes, err := io.ReadAll(body)
 		if err != nil {
 			return "", "", "", err
 		}
diff --git a/vehicle/smart/hello/identity.go b/vehicle/smart/hello/identity.go
index 854c0ede..80616a44 100644
--- a/vehicle/smart/hello/identity.go
+++ b/vehicle/smart/hello/identity.go
@@ -173,7 +173,7 @@ func (v *Identity) appToken(token *oauth2.Token) (*oauth2.Token, string, error)
 	}
 
 	path := "/auth/account/session/secure"
-	nonce, ts, sign, err := createSignature(http.MethodPost, path, params, data)
+	nonce, ts, sign, err := createSignature(http.MethodPost, path, params, request.MarshalJSON(data))
 	if err != nil {
 		return nil, "", err
 	}
@@ -211,54 +211,3 @@ func (v *Identity) appToken(token *oauth2.Token) (*oauth2.Token, string, error)
 
 	return &tok, res.Data.UserId, nil
 }
-
-func (v *Identity) UpdateSession(vin string) (string, error) {
-	token, err := v.Token()
-	if err != nil {
-		return "", err
-	}
-
-	params := url.Values{
-		"identity_type": []string{"smart"},
-	}
-
-	data := map[string]string{
-		"vin":          vin,
-		"sessionToken": token.AccessToken,
-		"language":     "",
-	}
-
-	path := "/device-platform/user/session/update"
-	nonce, ts, sign, err := createSignature(http.MethodPost, path, params, data)
-	if err != nil {
-		return "", err
-	}
-
-	uri := fmt.Sprintf("%s/%s?%s", ApiURI, strings.TrimPrefix(path, "/"), params.Encode())
-	req, _ := request.New(http.MethodPost, uri, request.MarshalJSON(data), map[string]string{
-		"Accept":                  "application/json;responseformat=3",
-		"Content-Type":            "application/json; charset=utf-8",
-		"X-Api-Signature-Version": "1.0",
-		"X-Api-Signature-Nonce":   nonce,
-		"Authorization":           token.AccessToken,
-		"X-App-Id":                appID,
-		"X-Device-Identifier":     v.deviceID,
-		"X-Operator-Code":         operatorCode,
-		"X-Signature":             sign,
-		"X-Timestamp":             ts,
-	})
-
-	var res struct {
-		Code    ResponseCode
-		Message string
-		Data    AppToken
-	}
-
-	if err := v.DoJSON(req, &res); err != nil {
-		return "", err
-	} else if res.Code != ResponseOK {
-		return "", fmt.Errorf("%d: %s", res.Code, res.Message)
-	}
-
-	return "", nil
-}
diff --git a/vehicle/vw-id.go b/vehicle/vw-id.go
deleted file mode 100644
index ac833a4d..00000000
--- a/vehicle/vw-id.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package vehicle
-
-import (
-	"time"
-
-	"github.com/evcc-io/evcc/api"
-	"github.com/evcc-io/evcc/util"
-	"github.com/evcc-io/evcc/util/request"
-	"github.com/evcc-io/evcc/vehicle/vag/loginapps"
-	"github.com/evcc-io/evcc/vehicle/vag/vwidentity"
-	"github.com/evcc-io/evcc/vehicle/vw/id"
-)
-
-// https://github.com/TA2k/ioBroker.vw-connect
-
-// ID is an api.Vehicle implementation for ID cars
-type ID struct {
-	*embed
-	*id.Provider // provides the api implementations
-}
-
-func init() {
-	registry.Add("id", NewIDFromConfig)
-}
-
-// NewIDFromConfig creates a new vehicle
-func NewIDFromConfig(other map[string]interface{}) (api.Vehicle, error) {
-	cc := struct {
-		embed               `mapstructure:",squash"`
-		User, Password, VIN string
-		Cache               time.Duration
-		Timeout             time.Duration
-	}{
-		Cache:   interval,
-		Timeout: request.Timeout,
-	}
-
-	if err := util.DecodeOther(other, &cc); err != nil {
-		return nil, err
-	}
-
-	if cc.User == "" || cc.Password == "" {
-		return nil, api.ErrMissingCredentials
-	}
-
-	v := &ID{
-		embed: &cc.embed,
-	}
-
-	log := util.NewLogger("id").Redact(cc.User, cc.Password, cc.VIN)
-
-	q, err := vwidentity.LoginWithAuthURL(log, id.LoginURL, id.AuthParams, cc.User, cc.Password)
-	if err != nil {
-		return nil, err
-	}
-
-	apps := loginapps.New(log)
-	token, err := apps.Exchange(q)
-	if err != nil {
-		return nil, err
-	}
-
-	api := id.NewAPI(log, apps.TokenSource(token))
-	api.Client.Timeout = cc.Timeout
-
-	vehicle, err := ensureVehicleEx(
-		cc.VIN, api.Vehicles,
-		func(v id.Vehicle) string {
-			return v.VIN
-		},
-	)
-
-	if v.Title_ == "" {
-		v.Title_ = vehicle.Nickname
-	}
-
-	if err == nil {
-		v.Provider = id.NewProvider(api, vehicle.VIN, cc.Cache)
-	}
-
-	return v, err
-}
diff --git a/vehicle/vw.go b/vehicle/vw.go
index f2db0016..54126dfb 100644
--- a/vehicle/vw.go
+++ b/vehicle/vw.go
@@ -6,25 +6,26 @@ import (
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
-	"github.com/evcc-io/evcc/vehicle/vag/service"
-	"github.com/evcc-io/evcc/vehicle/vw"
+	"github.com/evcc-io/evcc/vehicle/vag/loginapps"
+	"github.com/evcc-io/evcc/vehicle/vag/vwidentity"
+	"github.com/evcc-io/evcc/vehicle/vw/id"
 )
 
-// https://github.com/trocotronic/weconnect
 // https://github.com/TA2k/ioBroker.vw-connect
 
-// VW is an api.Vehicle implementation for VW cars
-type VW struct {
+// ID is an api.Vehicle implementation for ID cars
+type ID struct {
 	*embed
-	*vw.Provider // provides the api implementations
+	*id.Provider // provides the api implementations
 }
 
 func init() {
-	registry.Add("vw", NewVWFromConfig)
+	registry.Add("vw", NewIDFromConfig)
+	registry.Add("id", NewIDFromConfig)
 }
 
-// NewVWFromConfig creates a new vehicle
-func NewVWFromConfig(other map[string]interface{}) (api.Vehicle, error) {
+// NewIDFromConfig creates a new vehicle
+func NewIDFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 	cc := struct {
 		embed               `mapstructure:",squash"`
 		User, Password, VIN string
@@ -43,27 +44,39 @@ func NewVWFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 		return nil, api.ErrMissingCredentials
 	}
 
-	v := &VW{
+	v := &ID{
 		embed: &cc.embed,
 	}
 
-	log := util.NewLogger("vw").Redact(cc.User, cc.Password, cc.VIN)
+	log := util.NewLogger("id").Redact(cc.User, cc.Password, cc.VIN)
 
-	trs, err := service.TokenRefreshServiceTokenSource(log, vw.TRSParams, vw.AuthParams, cc.User, cc.Password)
+	q, err := vwidentity.LoginWithAuthURL(log, id.LoginURL, id.AuthParams, cc.User, cc.Password)
 	if err != nil {
 		return nil, err
 	}
 
-	ts := service.MbbTokenSource(log, trs, vw.AuthClientID)
-	api := vw.NewAPI(log, ts, vw.Brand, vw.Country)
+	apps := loginapps.New(log)
+	token, err := apps.Exchange(q)
+	if err != nil {
+		return nil, err
+	}
+
+	api := id.NewAPI(log, apps.TokenSource(token))
 	api.Client.Timeout = cc.Timeout
 
-	cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles)
+	vehicle, err := ensureVehicleEx(
+		cc.VIN, api.Vehicles,
+		func(v id.Vehicle) string {
+			return v.VIN
+		},
+	)
+
+	if v.Title_ == "" {
+		v.Title_ = vehicle.Nickname
+	}
 
 	if err == nil {
-		if err = api.HomeRegion(cc.VIN); err == nil {
-			v.Provider = vw.NewProvider(api, cc.VIN, cc.Cache)
-		}
+		v.Provider = id.NewProvider(api, vehicle.VIN, cc.Cache)
 	}
 
 	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 9411da11..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)
 	}
@@ -526,6 +523,7 @@ func cacheExpecter(t *testing.T, lp *Loadpoint) (*util.Cache, func(key string, v
 	go cache.Run(paramC)
 
 	expect := func(key string, val interface{}) {
+		time.Sleep(100 * time.Millisecond) // wait for cache to catch up
 		p := cache.Get(key)
 		t.Logf("%s: %.f", key, p.Val) // REMOVE
 		if p.Val != val {
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();