Repository: evcc-io/evcc
Pull request #10282 (2023-10-12 13:49:54)

Feature - extend debug logging
Moin!

Ich habe gerade meine ersten Gehversuche mit dem Feature **Min Ladung** unternommen. Im UI wird das grafisch schön durch den roten Balken dargestellt. Im DEBUG Log des evcc Containers ist davon leider nichts zu sehen. Ich habe deshalb in der Funktion `minSocNotReached` mal eine DEBUG Meldung eingebaut, die anzeigt, dass der Ladevorgang im Modus *Schnell* erfolgt, obwohl als Modus *PV* ausgewählt ist. Das sieht dann so aus:
```
[lp-1  ] DEBUG 2023/10/15 12:00:42 Minimum charging: vehicleSoc 43% < Minimum Soc 50%
```

Als weitere Ergänzung habe ich die Meldung in `core/site.go`, die anzeigt, dass das Laden der Batterie Priorität vor dem Laden des Autos hat, so erweitert, dass nicht nur der aktuelle Ladezustand der Batterie, sondern auch der obere Grenzwert für diese Laden mit Priorität angezeigt wird:
```
evcc  | [site  ] DEBUG 2023/10/12 16:36:10 giving priority to battery charging at soc 37% until 60% is reached
```

Ich hoffe, dass es eure Zustimmung findet.

Danke und beste Grüße

Jens

jheinitz: Ich weiß nicht warum der Test fehlschlägt.... Liegt es daran, dass `core/loadpoint_test.go` mit dem DEBUG nicht klar kommt?

Patch:
diff --git a/README.md b/README.md
index 0e63f59f..b0856581 100644
--- a/README.md
+++ b/README.md
@@ -61,7 +61,7 @@ For frontend development start the Vue toolchain in dev-mode. Open http://127.0.
 
 ```sh
 npm install
-npm run start
+npm run dev
 ```
 
 ### Integration tests
diff --git a/api/tariff.go b/api/tariff.go
index c433506b..d8b1b37a 100644
--- a/api/tariff.go
+++ b/api/tariff.go
@@ -8,5 +8,6 @@ const (
 	_ TariffType = iota
 	TariffTypePriceStatic
 	TariffTypePriceDynamic
+	TariffTypePriceForecast
 	TariffTypeCo2
 )
diff --git a/api/tarifftype_enumer.go b/api/tarifftype_enumer.go
index ac0a6e74..9a7b11cf 100644
--- a/api/tarifftype_enumer.go
+++ b/api/tarifftype_enumer.go
@@ -7,11 +7,11 @@ import (
 	"strings"
 )
 
-const _TariffTypeName = "pricestaticpricedynamicco2"
+const _TariffTypeName = "pricestaticpricedynamicpriceforecastco2"
 
-var _TariffTypeIndex = [...]uint8{0, 11, 23, 26}
+var _TariffTypeIndex = [...]uint8{0, 11, 23, 36, 39}
 
-const _TariffTypeLowerName = "pricestaticpricedynamicco2"
+const _TariffTypeLowerName = "pricestaticpricedynamicpriceforecastco2"
 
 func (i TariffType) String() string {
 	i -= 1
@@ -27,24 +27,28 @@ func _TariffTypeNoOp() {
 	var x [1]struct{}
 	_ = x[TariffTypePriceStatic-(1)]
 	_ = x[TariffTypePriceDynamic-(2)]
-	_ = x[TariffTypeCo2-(3)]
+	_ = x[TariffTypePriceForecast-(3)]
+	_ = x[TariffTypeCo2-(4)]
 }
 
-var _TariffTypeValues = []TariffType{TariffTypePriceStatic, TariffTypePriceDynamic, TariffTypeCo2}
+var _TariffTypeValues = []TariffType{TariffTypePriceStatic, TariffTypePriceDynamic, TariffTypePriceForecast, TariffTypeCo2}
 
 var _TariffTypeNameToValueMap = map[string]TariffType{
 	_TariffTypeName[0:11]:       TariffTypePriceStatic,
 	_TariffTypeLowerName[0:11]:  TariffTypePriceStatic,
 	_TariffTypeName[11:23]:      TariffTypePriceDynamic,
 	_TariffTypeLowerName[11:23]: TariffTypePriceDynamic,
-	_TariffTypeName[23:26]:      TariffTypeCo2,
-	_TariffTypeLowerName[23:26]: TariffTypeCo2,
+	_TariffTypeName[23:36]:      TariffTypePriceForecast,
+	_TariffTypeLowerName[23:36]: TariffTypePriceForecast,
+	_TariffTypeName[36:39]:      TariffTypeCo2,
+	_TariffTypeLowerName[36:39]: TariffTypeCo2,
 }
 
 var _TariffTypeNames = []string{
 	_TariffTypeName[0:11],
 	_TariffTypeName[11:23],
-	_TariffTypeName[23:26],
+	_TariffTypeName[23:36],
+	_TariffTypeName[36:39],
 }
 
 // TariffTypeString retrieves an enum value from the enum constants string name.
diff --git a/assets/js/components/ChargingPlanArrival.vue b/assets/js/components/ChargingPlanArrival.vue
index 59fab295..3d2e6969 100644
--- a/assets/js/components/ChargingPlanArrival.vue
+++ b/assets/js/components/ChargingPlanArrival.vue
@@ -15,12 +15,8 @@
 						:disabled="!socBasedCharging"
 						@change="changeMinSoc"
 					>
-						<option
-							v-for="soc in [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50]"
-							:key="soc"
-							:value="soc"
-						>
-							{{ soc ? `${soc}%` : "--" }}
+						<option v-for="soc in minSocOptions" :key="soc.value" :value="soc.value">
+							{{ soc.name }}
 						</option>
 					</select>
 				</div>
@@ -45,6 +41,16 @@ export default {
 	data: function () {
 		return { selectedMinSoc: this.minSoc };
 	},
+	computed: {
+		minSocOptions() {
+			// a list of entries from 0 to 95 with a step of 5
+			return Array.from(Array(20).keys())
+				.map((i) => i * 5)
+				.map((soc) => {
+					return { value: soc, name: soc === 0 ? "--" : `${soc}%` };
+				});
+		},
+	},
 	watch: {
 		minSoc: function (value) {
 			this.selectedMinSoc = value;
diff --git a/assets/js/components/CustomSelect.vue b/assets/js/components/CustomSelect.vue
index 3c1ee851..5a8931e5 100644
--- a/assets/js/components/CustomSelect.vue
+++ b/assets/js/components/CustomSelect.vue
@@ -40,7 +40,7 @@ export default {
 	left: 0;
 	top: 0;
 	bottom: 0;
-	right: 0;
+	width: 100%;
 	position: absolute;
 	opacity: 0;
 	-webkit-appearance: menulist-button;
diff --git a/assets/js/components/Energyflow/Energyflow.vue b/assets/js/components/Energyflow/Energyflow.vue
index 30ac225c..b7b0ffdb 100644
--- a/assets/js/components/Energyflow/Energyflow.vue
+++ b/assets/js/components/Energyflow/Energyflow.vue
@@ -171,7 +171,7 @@ import GridSettingsModal from "../GridSettingsModal.vue";
 import formatter from "../../mixins/formatter";
 import AnimatedNumber from "../AnimatedNumber.vue";
 import settings from "../../settings";
-import { CO2_TYPE, PRICE_DYNAMIC_TYPE } from "../../units";
+import { CO2_TYPE, PRICE_DYNAMIC_TYPE, PRICE_FORECAST_TYPE } from "../../units";
 import collector from "../../mixins/collector";
 import BatterySettingsModal from "../BatterySettingsModal.vue";
 
@@ -216,7 +216,7 @@ export default {
 	},
 	computed: {
 		smartCostAvailable: function () {
-			return [CO2_TYPE, PRICE_DYNAMIC_TYPE].includes(this.smartCostType);
+			return [CO2_TYPE, PRICE_DYNAMIC_TYPE, PRICE_FORECAST_TYPE].includes(this.smartCostType);
 		},
 		gridImport: function () {
 			return Math.max(0, this.gridPower);
diff --git a/assets/js/components/GridSettingsModal.vue b/assets/js/components/GridSettingsModal.vue
index 0b6bb339..101bd60d 100644
--- a/assets/js/components/GridSettingsModal.vue
+++ b/assets/js/components/GridSettingsModal.vue
@@ -114,6 +114,7 @@ export default {
 	props: {
 		smartCostLimit: Number,
 		smartCostType: String,
+		tariffGrid: Number,
 		currency: String,
 	},
 	data: function () {
@@ -145,7 +146,7 @@ export default {
 			}
 			values.sort((a, b) => a - b);
 			return values.map((value) => {
-				const name = `< ${
+				const name = `≤ ${
 					this.isCo2
 						? this.fmtCo2Medium(value)
 						: this.fmtPricePerKWh(value, this.currency)
@@ -194,7 +195,7 @@ export default {
 				// TODO: handle multiple matching time slots
 				const price = this.findSlotInRange(start, end, rates)?.price;
 				const charging =
-					price < this.selectedSmartCostLimit && this.selectedSmartCostLimit !== 0;
+					price <= this.selectedSmartCostLimit && this.selectedSmartCostLimit !== 0;
 				const selectable = price !== undefined;
 				result.push({ day, price, startHour, endHour, charging, selectable });
 			}
@@ -241,6 +242,11 @@ export default {
 				this.updateTariff();
 			}
 		},
+		tariffGrid() {
+			if (this.isModalVisible) {
+				this.updateTariff();
+			}
+		},
 		smartCostLimit(limit) {
 			this.selectedSmartCostLimit = limit;
 		},
@@ -315,7 +321,7 @@ export default {
 				this.selectedSmartCostLimit = 0;
 				return;
 			}
-			const nextOption = this.costOptions.find(({ value }) => value > limit);
+			const nextOption = this.costOptions.find(({ value }) => value >= limit);
 			if (nextOption) {
 				this.selectedSmartCostLimit = nextOption.value;
 			}
diff --git a/assets/js/components/Loadpoint.vue b/assets/js/components/Loadpoint.vue
index 04b869af..777d072a 100644
--- a/assets/js/components/Loadpoint.vue
+++ b/assets/js/components/Loadpoint.vue
@@ -195,6 +195,7 @@ export default {
 		guardAction: String,
 		smartCostLimit: Number,
 		smartCostType: String,
+		smartCostActive: Boolean,
 		tariffGrid: Number,
 		tariffCo2: Number,
 		currency: String,
diff --git a/assets/js/components/Loadpoints.vue b/assets/js/components/Loadpoints.vue
index 0a03afb2..283060c6 100644
--- a/assets/js/components/Loadpoints.vue
+++ b/assets/js/components/Loadpoints.vue
@@ -15,6 +15,7 @@
 					:vehicles="vehicles"
 					:smartCostLimit="smartCostLimit"
 					:smartCostType="smartCostType"
+					:smartCostActive="smartCostActive"
 					:tariffGrid="tariffGrid"
 					:tariffCo2="tariffCo2"
 					:currency="currency"
@@ -51,6 +52,7 @@ export default {
 		vehicles: Array,
 		smartCostLimit: Number,
 		smartCostType: String,
+		smartCostActive: Boolean,
 		tariffGrid: Number,
 		tariffCo2: Number,
 		currency: String,
diff --git a/assets/js/components/Site.vue b/assets/js/components/Site.vue
index ed332cd0..642e46d1 100644
--- a/assets/js/components/Site.vue
+++ b/assets/js/components/Site.vue
@@ -23,6 +23,7 @@
 				:vehicles="vehicles"
 				:smartCostLimit="smartCostLimit"
 				:smartCostType="smartCostType"
+				:smartCostActive="smartCostActive"
 				:tariffGrid="tariffGrid"
 				:tariffCo2="tariffCo2"
 				:currency="currency"
@@ -103,6 +104,7 @@ export default {
 		sponsorTokenExpires: Number,
 		smartCostLimit: Number,
 		smartCostType: String,
+		smartCostActive: Boolean,
 	},
 	computed: {
 		energyflow: function () {
diff --git a/assets/js/components/Vehicle.vue b/assets/js/components/Vehicle.vue
index 617f1e2b..e9c5192b 100644
--- a/assets/js/components/Vehicle.vue
+++ b/assets/js/components/Vehicle.vue
@@ -119,6 +119,7 @@ export default {
 		climaterActive: Boolean,
 		smartCostLimit: Number,
 		smartCostType: String,
+		smartCostActive: Boolean,
 		tariffGrid: Number,
 		tariffCo2: Number,
 		currency: String,
diff --git a/assets/js/components/VehicleStatus.vue b/assets/js/components/VehicleStatus.vue
index 95529d07..c4b4b959 100644
--- a/assets/js/components/VehicleStatus.vue
+++ b/assets/js/components/VehicleStatus.vue
@@ -18,6 +18,7 @@ export default {
 		charging: Boolean,
 		targetTime: String,
 		planProjectedStart: String,
+		planActive: Boolean,
 		phaseAction: String,
 		phaseRemainingInterpolated: Number,
 		pvAction: String,
@@ -28,8 +29,10 @@ export default {
 		climaterActive: Boolean,
 		smartCostLimit: Number,
 		smartCostType: String,
+		smartCostActive: Boolean,
 		tariffGrid: Number,
 		tariffCo2: Number,
+		currency: String,
 	},
 	computed: {
 		phaseTimerActive() {
@@ -46,9 +49,6 @@ export default {
 		guardTimerActive() {
 			return this.guardRemainingInterpolated > 0 && this.guardAction === "enable";
 		},
-		isCo2() {
-			return this.smartCostType === CO2_TYPE;
-		},
 		message: function () {
 			const t = (key, data) => {
 				return this.$t(`main.vehicleStatus.${key}`, data);
@@ -62,12 +62,12 @@ export default {
 				return t("minCharge", { soc: this.minSoc });
 			}
 
-			// target charge
+			// plan
 			if (this.targetTime && !this.targetChargeDisabled) {
-				if (this.charging) {
+				if (this.planActive && this.charging) {
 					return t("targetChargeActive");
 				}
-				if (this.enabled) {
+				if (this.planActive && this.enabled) {
 					return t("targetChargeWaitForVehicle");
 				}
 				if (this.planProjectedStart) {
@@ -77,14 +77,17 @@ export default {
 				}
 			}
 
-			// clean energy
-			if (this.charging && this.isCo2 && this.tariffCo2 < this.smartCostLimit) {
-				return t("cleanEnergyCharging");
-			}
-
-			// cheap energy
-			if (this.charging && !this.isCo2 && this.tariffGrid < this.smartCostLimit) {
-				return t("cheapEnergyCharging");
+			// clean or cheap energy
+			if (this.charging && this.smartCostActive) {
+				return this.smartCostType === CO2_TYPE
+					? t("cleanEnergyCharging", {
+							co2: this.fmtCo2Short(this.tariffCo2),
+							limit: this.fmtCo2Short(this.smartCostLimit),
+					  })
+					: t("cheapEnergyCharging", {
+							price: this.fmtPricePerKWh(this.tariffGrid, this.currency, true),
+							limit: this.fmtPricePerKWh(this.smartCostLimit, this.currency, true),
+					  });
 			}
 
 			if (this.pvTimerActive && !this.enabled && this.pvAction === "enable") {
diff --git a/assets/js/units.js b/assets/js/units.js
index 58ab3b8b..ca11d560 100644
--- a/assets/js/units.js
+++ b/assets/js/units.js
@@ -6,6 +6,7 @@ export const UNITS = [KM, MILES];
 
 export const CO2_TYPE = "co2";
 export const PRICE_DYNAMIC_TYPE = "pricedynamic";
+export const PRICE_FORECAST_TYPE = "priceforecast";
 
 const MILES_FACTOR = 0.6213711922;
 
diff --git a/charger/abb.go b/charger/abb.go
index b2ff703f..2961b7c2 100644
--- a/charger/abb.go
+++ b/charger/abb.go
@@ -209,38 +209,33 @@ func (wb *ABB) ChargedEnergy() (float64, error) {
 	return float64(binary.BigEndian.Uint32(b)) / 1e3, err
 }
 
-var _ api.PhaseCurrents = (*ABB)(nil)
-
-// Currents implements the api.PhaseCurrents interface
-func (wb *ABB) Currents() (float64, float64, float64, error) {
-	b, err := wb.conn.ReadHoldingRegisters(abbRegCurrents, 6)
+// getPhaseValues returns 3 sequential register values
+func (wb *ABB) getPhaseValues(reg uint16, divider float64) (float64, float64, float64, error) {
+	b, err := wb.conn.ReadHoldingRegisters(reg, 6)
 	if err != nil {
 		return 0, 0, 0, err
 	}
 
-	var curr [3]float64
-	for l := 0; l < 3; l++ {
-		curr[l] = float64(binary.BigEndian.Uint32(b[4*l:])) / 1e3
+	var res [3]float64
+	for i := 0; i < 3; i++ {
+		res[i] = float64(binary.BigEndian.Uint32(b[4*i:])) / divider
 	}
 
-	return curr[0], curr[1], curr[2], nil
+	return res[0], res[1], res[2], nil
+}
+
+var _ api.PhaseCurrents = (*ABB)(nil)
+
+// Currents implements the api.PhaseCurrents interface
+func (wb *ABB) Currents() (float64, float64, float64, error) {
+	return wb.getPhaseValues(abbRegCurrents, 1e3)
 }
 
 var _ api.PhaseVoltages = (*ABB)(nil)
 
 // Voltages implements the api.PhaseVoltages interface
 func (wb *ABB) Voltages() (float64, float64, float64, error) {
-	b, err := wb.conn.ReadHoldingRegisters(abbRegVoltages, 6)
-	if err != nil {
-		return 0, 0, 0, err
-	}
-
-	var volt [3]float64
-	for l := 0; l < 3; l++ {
-		volt[l] = float64(binary.BigEndian.Uint32(b[4*l:])) / 10
-	}
-
-	return volt[0], volt[1], volt[2], nil
+	return wb.getPhaseValues(abbRegVoltages, 10)
 }
 
 // var _ api.PhaseSwitcher = (*ABB)(nil)
diff --git a/charger/abl.go b/charger/abl.go
index 84012f2c..ea7b70ca 100644
--- a/charger/abl.go
+++ b/charger/abl.go
@@ -236,17 +236,17 @@ func (wb *ABLeMH) currents() (float64, float64, float64, error) {
 		return 0, 0, 0, err
 	}
 
-	var currents []float64
-	for i := 2; i < 5; i++ {
-		u := binary.BigEndian.Uint16(b[2*i:])
+	var res [3]float64
+	for i := 0; i < 3; i++ {
+		u := binary.BigEndian.Uint16(b[2*(2+i):])
 		if u == ablAmpsDisabled || u == 1 {
 			u = 0
 		}
 
-		currents = append(currents, float64(u)/10)
+		res[i] = float64(u) / 10
 	}
 
-	return currents[2], currents[1], currents[0], nil
+	return res[2], res[1], res[0], nil
 }
 
 var _ api.Diagnosis = (*ABLeMH)(nil)
diff --git a/charger/bender.go b/charger/bender.go
index 45ea2d40..3b91f3be 100644
--- a/charger/bender.go
+++ b/charger/bender.go
@@ -280,34 +280,29 @@ func (wb *BenderCC) totalEnergy() (float64, error) {
 	return float64(binary.BigEndian.Uint32(b)) / 1e3, nil
 }
 
-// currents implements the api.PhaseCurrents interface
-func (wb *BenderCC) currents() (float64, float64, float64, error) {
-	b, err := wb.conn.ReadHoldingRegisters(bendRegCurrents, 6)
+// getPhaseValues returns 3 sequential register values
+func (wb *BenderCC) getPhaseValues(reg uint16, divider float64) (float64, float64, float64, error) {
+	b, err := wb.conn.ReadHoldingRegisters(reg, 6)
 	if err != nil {
 		return 0, 0, 0, err
 	}
 
-	var curr [3]float64
-	for l := 0; l < 3; l++ {
-		curr[l] = float64(binary.BigEndian.Uint32(b[4*l:4*(l+1)])) / 1e3
+	var res [3]float64
+	for i := 0; i < 3; i++ {
+		res[i] = float64(binary.BigEndian.Uint32(b[4*i:])) / divider
 	}
 
-	return curr[0], curr[1], curr[2], nil
+	return res[0], res[1], res[2], nil
+}
+
+// currents implements the api.PhaseCurrents interface
+func (wb *BenderCC) currents() (float64, float64, float64, error) {
+	return wb.getPhaseValues(bendRegCurrents, 1e3)
 }
 
 // voltages implements the api.PhaseVoltages interface
 func (wb *BenderCC) voltages() (float64, float64, float64, error) {
-	b, err := wb.conn.ReadHoldingRegisters(bendRegVoltages, 6)
-	if err != nil {
-		return 0, 0, 0, err
-	}
-
-	var volt [3]float64
-	for l := 0; l < 3; l++ {
-		volt[l] = float64(binary.BigEndian.Uint32(b[4*l : 4*(l+1)]))
-	}
-
-	return volt[0], volt[1], volt[2], nil
+	return wb.getPhaseValues(bendRegVoltages, 1)
 }
 
 // identify implements the api.Identifier interface
diff --git a/charger/dadapower.go b/charger/dadapower.go
index f5391ae4..e4afb938 100644
--- a/charger/dadapower.go
+++ b/charger/dadapower.go
@@ -207,8 +207,30 @@ var _ api.PhaseSwitcher = (*Dadapower)(nil)
 
 // Phases1p3p implements the api.PhaseSwitcher interface
 func (wb *Dadapower) Phases1p3p(phases int) error {
-	_, err := wb.conn.WriteSingleRegister(dadapowerRegActivePhases+wb.regOffset, uint16(phases))
-	return err
+	enabled, err := wb.Enabled()
+	if err != nil {
+		return err
+	}
+
+	if enabled {
+		if err := wb.Enable(false); err != nil {
+			return err
+		}
+		time.Sleep(5 * time.Second)
+	}
+
+	if _, err := wb.conn.WriteSingleRegister(dadapowerRegActivePhases+wb.regOffset, uint16(phases)); err != nil {
+		return err
+	}
+
+	if enabled {
+		time.Sleep(2 * time.Second)
+		if err := wb.Enable(true); err != nil {
+			return err
+		}
+	}
+
+	return nil
 }
 
 var _ api.Identifier = (*Dadapower)(nil)
diff --git a/charger/daheimladen-mb.go b/charger/daheimladen-mb.go
index 89d09d76..df7e844e 100644
--- a/charger/daheimladen-mb.go
+++ b/charger/daheimladen-mb.go
@@ -203,9 +203,10 @@ func (wb *DaheimLadenMB) MaxCurrent(current int64) error {
 		return fmt.Errorf("invalid current %d", current)
 	}
 
-	err := wb.setCurrent(wb.curr)
+	curr := uint16(current * 10)
+	err := wb.setCurrent(curr)
 	if err == nil {
-		wb.curr = uint16(current * 10)
+		wb.curr = curr
 	}
 
 	return err
diff --git a/charger/easee.go b/charger/easee.go
index 9c5538cf..7b6b3966 100644
--- a/charger/easee.go
+++ b/charger/easee.go
@@ -189,6 +189,12 @@ func NewEasee(user, password, charger string, timeout time.Duration, authorize b
 		err = os.ErrDeadlineExceeded
 	}
 
+	if err == nil {
+		// poll opMode from charger as API can give outdated initial data
+		uri := fmt.Sprintf("%s/chargers/%s/commands/poll_chargeropmode", easee.API, c.charger)
+		_, err = c.Post(uri, request.JSONContent, nil)
+	}
+
 	return c, err
 }
 
diff --git a/charger/eebus.go b/charger/eebus.go
index 43e086c5..255a7c0b 100644
--- a/charger/eebus.go
+++ b/charger/eebus.go
@@ -453,11 +453,9 @@ func (c *EEBus) currents() (float64, float64, float64, error) {
 		return 0, 0, 0, err
 	}
 
-	count := len(currents)
-	if count < 3 {
-		for fill := 0; fill < 3-count; fill++ {
-			currents = append(currents, 0)
-		}
+	// fill phases
+	for i := len(currents); i < 3; i++ {
+		currents = append(currents, 0)
 	}
 
 	return currents[0], currents[1], currents[2], nil
diff --git a/charger/heidelberg-ec.go b/charger/heidelberg-ec.go
index ef6f8b05..13abf98a 100644
--- a/charger/heidelberg-ec.go
+++ b/charger/heidelberg-ec.go
@@ -262,38 +262,33 @@ func (wb *HeidelbergEC) TotalEnergy() (float64, error) {
 	return float64(binary.BigEndian.Uint32(b)) / 1e3, nil
 }
 
-var _ api.PhaseCurrents = (*HeidelbergEC)(nil)
-
-// Currents implements the api.PhaseCurrents interface
-func (wb *HeidelbergEC) Currents() (float64, float64, float64, error) {
-	b, err := wb.conn.ReadInputRegisters(hecRegCurrents, 3)
+// getPhaseValues returns 3 sequential register values
+func (wb *HeidelbergEC) getPhaseValues(reg uint16, divider float64) (float64, float64, float64, error) {
+	b, err := wb.conn.ReadInputRegisters(reg, 3)
 	if err != nil {
 		return 0, 0, 0, err
 	}
 
-	var curr [3]float64
-	for l := 0; l < 3; l++ {
-		curr[l] = float64(binary.BigEndian.Uint16(b[2*l:])) / 10
+	var res [3]float64
+	for i := 0; i < 3; i++ {
+		res[i] = float64(binary.BigEndian.Uint16(b[2*i:])) / divider
 	}
 
-	return curr[0], curr[1], curr[2], nil
+	return res[0], res[1], res[2], nil
+}
+
+var _ api.PhaseCurrents = (*HeidelbergEC)(nil)
+
+// Currents implements the api.PhaseCurrents interface
+func (wb *HeidelbergEC) Currents() (float64, float64, float64, error) {
+	return wb.getPhaseValues(hecRegCurrents, 10)
 }
 
 var _ api.PhaseVoltages = (*HeidelbergEC)(nil)
 
 // Voltages implements the api.PhaseVoltages interface
 func (wb *HeidelbergEC) Voltages() (float64, float64, float64, error) {
-	b, err := wb.conn.ReadInputRegisters(hecRegVoltages, 3)
-	if err != nil {
-		return 0, 0, 0, err
-	}
-
-	var volt [3]float64
-	for l := 0; l < 3; l++ {
-		volt[l] = float64(binary.BigEndian.Uint16(b[2*l:]))
-	}
-
-	return volt[0], volt[1], volt[2], nil
+	return wb.getPhaseValues(hecRegVoltages, 1)
 }
 
 var _ api.Diagnosis = (*HeidelbergEC)(nil)
diff --git a/charger/nrgble_linux.go b/charger/nrgble_linux.go
index 2c11ed55..ad1d329b 100644
--- a/charger/nrgble_linux.go
+++ b/charger/nrgble_linux.go
@@ -126,8 +126,7 @@ func (nrg *NRGKickBLE) connect() (*device.Device1, error) {
 		return nil, fmt.Errorf("find device: %s", err)
 	}
 
-	err = ble.Connect(dev, nrg.agent, nrg.device)
-	if err != nil {
+	if err := ble.Connect(dev, nrg.agent, nrg.device); err != nil {
 		return nil, err
 	}
 
@@ -249,7 +248,7 @@ func (nrg *NRGKickBLE) Enabled() (bool, error) {
 
 // Enable implements the api.Charger interface
 func (nrg *NRGKickBLE) Enable(enable bool) error {
-	res := ble.Info{}
+	var res ble.Info
 	if err := nrg.read(ble.InfoService, &res); err != nil {
 		return err
 	}
@@ -276,7 +275,7 @@ func (nrg *NRGKickBLE) Enable(enable bool) error {
 
 // MaxCurrent implements the api.Charger interface
 func (nrg *NRGKickBLE) MaxCurrent(current int64) error {
-	res := ble.Info{}
+	var res ble.Info
 	if err := nrg.read(ble.InfoService, &res); err != nil {
 		return err
 	}
@@ -293,7 +292,7 @@ var _ api.Meter = (*NRGKickBLE)(nil)
 
 // CurrentPower implements the api.Meter interface
 func (nrg *NRGKickBLE) CurrentPower() (float64, error) {
-	res := ble.Power{}
+	var res ble.Power
 	if err := nrg.read(ble.PowerService, &res); err != nil {
 		return 0, err
 	}
@@ -307,7 +306,7 @@ var _ api.MeterEnergy = (*NRGKickBLE)(nil)
 
 // TotalEnergy implements the api.MeterEnergy interface
 func (nrg *NRGKickBLE) TotalEnergy() (float64, error) {
-	res := ble.Energy{}
+	var res ble.Energy
 	if err := nrg.read(ble.EnergyService, &res); err != nil {
 		return 0, err
 	}
@@ -321,7 +320,7 @@ var _ api.PhaseCurrents = (*NRGKickBLE)(nil)
 
 // Currents implements the api.PhaseCurrents interface
 func (nrg *NRGKickBLE) Currents() (float64, float64, float64, error) {
-	res := ble.VoltageCurrent{}
+	var res ble.VoltageCurrent
 	if err := nrg.read(ble.VoltageCurrentService, &res); err != nil {
 		return 0, 0, 0, err
 	}
diff --git a/charger/nrgconnect.go b/charger/nrgconnect.go
index cac4f707..5e84b796 100644
--- a/charger/nrgconnect.go
+++ b/charger/nrgconnect.go
@@ -87,27 +87,27 @@ func (nrg *NRGKickConnect) apiURL(api string) string {
 
 func (nrg *NRGKickConnect) putJSON(url string, data interface{}) error {
 	req, err := request.New(http.MethodPut, url, request.MarshalJSON(data), request.JSONEncoding)
+	if err != nil {
+		return err
+	}
 
-	if err == nil {
-		var res struct {
-			Message string
-		}
-
-		if err = nrg.DoJSON(req, &res); err != nil {
-			if err == io.EOF {
-				err = nil
-			} else if res.Message != "" {
-				return errors.New(res.Message)
-			}
-		}
+	var res struct {
+		Message string
+	}
 
-		if err == nil {
-			nrg.settingsG.Reset()
-			nrg.measurementsG.Reset()
+	if err := nrg.DoJSON(req, &res); err != nil {
+		switch {
+		case res.Message != "":
+			return errors.New(res.Message)
+		case err != io.EOF:
+			return err
 		}
 	}
 
-	return err
+	nrg.settingsG.Reset()
+	nrg.measurementsG.Reset()
+
+	return nil
 }
 
 // Status implements the api.Charger interface
diff --git a/charger/ocpp.go b/charger/ocpp.go
index 74f34cd2..8573f38a 100644
--- a/charger/ocpp.go
+++ b/charger/ocpp.go
@@ -4,6 +4,7 @@ import (
 	"errors"
 	"fmt"
 	"math"
+	"slices"
 	"sort"
 	"strconv"
 	"strings"
@@ -16,7 +17,6 @@ import (
 	"github.com/lorenzodonini/ocpp-go/ocpp1.6/core"
 	"github.com/lorenzodonini/ocpp-go/ocpp1.6/smartcharging"
 	"github.com/lorenzodonini/ocpp-go/ocpp1.6/types"
-	"github.com/samber/lo"
 )
 
 // OCPP charger implementation
@@ -274,9 +274,14 @@ func NewOCPP(id string, connector int, idtag string,
 	return c, conn.Initialized()
 }
 
+// Connector returns the connector instance
+func (c *OCPP) Connector() *ocpp.Connector {
+	return c.conn
+}
+
 // hasMeasurement checks if meterValuesSample contains given measurement
 func (c *OCPP) hasMeasurement(val types.Measurand) bool {
-	return lo.Contains(strings.Split(c.meterValuesSample, ","), string(val))
+	return slices.Contains(strings.Split(c.meterValuesSample, ","), string(val))
 }
 
 // configure updates CP configuration
diff --git a/charger/ocpp/connector_core.go b/charger/ocpp/connector_core.go
index 70796fd2..8dec3b60 100644
--- a/charger/ocpp/connector_core.go
+++ b/charger/ocpp/connector_core.go
@@ -74,10 +74,10 @@ func (conn *Connector) StartTransaction(request *core.StartTransactionRequest) (
 	defer conn.mu.Unlock()
 
 	// expired request
-	if request.Timestamp != nil && conn.clock.Since(request.Timestamp.Time) < transactionExpiry {
+	if request.Timestamp != nil && conn.clock.Since(request.Timestamp.Time) > transactionExpiry {
 		res := &core.StartTransactionConfirmation{
 			IdTagInfo: &types.IdTagInfo{
-				Status: types.AuthorizationStatusExpired,
+				Status: types.AuthorizationStatusExpired, // reject
 			},
 		}
 
@@ -105,7 +105,7 @@ func (conn *Connector) StopTransaction(request *core.StopTransactionRequest) (*c
 	if request.Timestamp != nil && conn.clock.Since(request.Timestamp.Time) > transactionExpiry {
 		res := &core.StopTransactionConfirmation{
 			IdTagInfo: &types.IdTagInfo{
-				Status: types.AuthorizationStatusExpired, // accept
+				Status: types.AuthorizationStatusExpired, // reject
 			},
 		}
 
diff --git a/charger/ocpp/cp.go b/charger/ocpp/cp.go
index b51ea983..af5faadb 100644
--- a/charger/ocpp/cp.go
+++ b/charger/ocpp/cp.go
@@ -11,7 +11,7 @@ import (
 // Since ocpp-go interfaces at charge point level, we need to manage multiple connector separately
 
 type CP struct {
-	mu   sync.Mutex
+	mu   sync.RWMutex
 	once sync.Once
 	log  *util.Logger
 
@@ -46,15 +46,15 @@ func (cp *CP) registerConnector(id int, conn *Connector) error {
 }
 
 func (cp *CP) connectorByID(id int) *Connector {
-	cp.mu.Lock()
-	defer cp.mu.Unlock()
+	cp.mu.RLock()
+	defer cp.mu.RUnlock()
 
 	return cp.connectors[id]
 }
 
 func (cp *CP) connectorByTransactionID(id int) *Connector {
-	cp.mu.Lock()
-	defer cp.mu.Unlock()
+	cp.mu.RLock()
+	defer cp.mu.RUnlock()
 
 	for _, conn := range cp.connectors {
 		if txn, err := conn.TransactionID(); err == nil && txn == id {
@@ -66,8 +66,8 @@ func (cp *CP) connectorByTransactionID(id int) *Connector {
 }
 
 func (cp *CP) ID() string {
-	cp.mu.Lock()
-	defer cp.mu.Unlock()
+	cp.mu.RLock()
+	defer cp.mu.RUnlock()
 
 	return cp.id
 }
@@ -97,8 +97,8 @@ func (cp *CP) connect(connect bool) {
 }
 
 func (cp *CP) Connected() bool {
-	cp.mu.Lock()
-	defer cp.mu.Unlock()
+	cp.mu.RLock()
+	defer cp.mu.RUnlock()
 
 	return cp.connected
 }
diff --git a/charger/ocpp/instance.go b/charger/ocpp/instance.go
index 8b8cae70..270b9c6a 100644
--- a/charger/ocpp/instance.go
+++ b/charger/ocpp/instance.go
@@ -5,6 +5,7 @@ import (
 	"time"
 
 	"github.com/evcc-io/evcc/util"
+	"github.com/lorenzodonini/ocpp-go/ocpp"
 	ocpp16 "github.com/lorenzodonini/ocpp-go/ocpp1.6"
 	"github.com/lorenzodonini/ocpp-go/ocpp1.6/core"
 	"github.com/lorenzodonini/ocpp-go/ocpp1.6/firmware"
@@ -23,6 +24,8 @@ var (
 
 func Instance() *CS {
 	once.Do(func() {
+		log := util.NewLogger("ocpp")
+
 		timeoutConfig := ws.NewServerTimeoutConfig()
 		timeoutConfig.PingWait = 90 * time.Second
 
@@ -33,11 +36,15 @@ func Instance() *CS {
 		dispatcher.SetTimeout(time.Minute)
 
 		endpoint := ocppj.NewServer(server, dispatcher, nil, core.Profile, localauth.Profile, firmware.Profile, reservation.Profile, remotetrigger.Profile, smartcharging.Profile)
+		endpoint.SetInvalidMessageHook(func(client ws.Channel, err *ocpp.Error, rawMessage string, parsedFields []interface{}) *ocpp.Error {
+			log.ERROR.Printf("%v (%s)", err, rawMessage)
+			return nil
+		})
 
 		cs := ocpp16.NewCentralSystem(endpoint, server)
 
 		instance = &CS{
-			log:           util.NewLogger("ocpp"),
+			log:           log,
 			cps:           make(map[string]*CP),
 			CentralSystem: cs,
 		}
diff --git a/charger/openevse.go b/charger/openevse.go
index c56a93dd..bf4729fe 100644
--- a/charger/openevse.go
+++ b/charger/openevse.go
@@ -94,7 +94,9 @@ func (c *OpenEVSE) setOverride() error {
 	uri := fmt.Sprintf("%s/override", c.uri)
 
 	if err := c.GetJSON(uri, &data); err != nil {
-		return err
+		if err.Error() != "unexpected status: 404 (Not Found)" {
+			return err
+		}
 	}
 
 	state := openevse.Disabled
diff --git a/charger/openwb.go b/charger/openwb.go
index 8f67e0ca..ff257ca3 100644
--- a/charger/openwb.go
+++ b/charger/openwb.go
@@ -214,17 +214,17 @@ var _ api.PhaseCurrents = (*OpenWB)(nil)
 
 // Currents implements the api.PhaseCurrents interface
 func (m *OpenWB) Currents() (float64, float64, float64, error) {
-	var currents []float64
+	var res []float64
 	for _, currentG := range m.currentsG {
 		c, err := currentG()
 		if err != nil {
 			return 0, 0, 0, err
 		}
 
-		currents = append(currents, c)
+		res = append(res, c)
 	}
 
-	return currents[0], currents[1], currents[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 var _ api.Authorizer = (*OpenWB)(nil)
diff --git a/charger/pulsares.go b/charger/pulsares.go
new file mode 100644
index 00000000..68bc737f
--- /dev/null
+++ b/charger/pulsares.go
@@ -0,0 +1,196 @@
+package charger
+
+// LICENSE
+
+// Copyright (c) 2023 premultiply
+
+// This module is NOT covered by the MIT license. All rights reserved.
+
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+import (
+	"encoding/binary"
+	"fmt"
+	"time"
+
+	"github.com/evcc-io/evcc/api"
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/modbus"
+)
+
+// Pulsares charger implementation
+type Pulsares struct {
+	log  *util.Logger
+	conn *modbus.Connection
+	curr uint16
+}
+
+const (
+	pulsaresRegStatus  = 0x1f
+	pulsaresRegCurrent = 0x5d
+	pulsaresRegBackup  = 0x61
+)
+
+func init() {
+	registry.Add("pulsares", NewPulsaresFromConfig)
+}
+
+// NewPulsaresFromConfig creates a Pulsares charger from generic config
+func NewPulsaresFromConfig(other map[string]interface{}) (api.Charger, error) {
+	cc := struct {
+		modbus.Settings `mapstructure:",squash"`
+	}{
+		Settings: modbus.Settings{
+			ID: 1,
+		},
+	}
+
+	if err := util.DecodeOther(other, &cc); err != nil {
+		return nil, err
+	}
+
+	return NewPulsares(cc.URI, cc.Device, cc.Comset, cc.Baudrate, modbus.ProtocolFromRTU(cc.RTU), cc.ID)
+}
+
+// NewPulsares creates Pulsares charger
+func NewPulsares(uri, device, comset string, baudrate int, proto modbus.Protocol, slaveID uint8) (api.Charger, error) {
+	conn, err := modbus.NewConnection(uri, device, comset, baudrate, proto, slaveID)
+	if err != nil {
+		return nil, err
+	}
+
+	log := util.NewLogger("pulsares")
+	conn.Logger(log.TRACE)
+
+	wb := &Pulsares{
+		conn: conn,
+		curr: 6000,
+	}
+
+	// get initial state from charger
+	curr, err := wb.getCurrent()
+	if err != nil {
+		return nil, fmt.Errorf("current limit: %w", err)
+	}
+	if curr >= 6000 {
+		wb.curr = curr
+	}
+
+	// get failsafe timeout from charger
+	b, err := wb.conn.ReadHoldingRegisters(pulsaresRegBackup, 1)
+	if err != nil {
+		return nil, fmt.Errorf("failsafe timeout: %w", err)
+	}
+
+	var t time.Duration
+	switch u := binary.BigEndian.Uint16(b); u {
+	case 2:
+		t = 2 * time.Second
+	case 3:
+		t = 5 * time.Second
+	case 4:
+		t = 10 * time.Second
+	case 5:
+		t = 30 * time.Second
+	case 6:
+		t = 60 * time.Second
+	}
+
+	if t > 0 {
+		go wb.heartbeat(t / 2)
+	}
+
+	return wb, err
+}
+
+func (wb *Pulsares) heartbeat(timeout time.Duration) {
+	for range time.Tick(timeout) {
+		if _, err := wb.conn.ReadHoldingRegisters(pulsaresRegBackup, 1); err != nil {
+			wb.log.ERROR.Println("heartbeat:", err)
+		}
+	}
+}
+
+func (wb *Pulsares) setCurrent(current uint16) error {
+	b := make([]byte, 2)
+	binary.BigEndian.PutUint16(b, uint16(current))
+
+	_, err := wb.conn.WriteMultipleRegisters(pulsaresRegCurrent, 1, b)
+
+	return err
+}
+
+func (wb *Pulsares) getCurrent() (uint16, error) {
+	b, err := wb.conn.ReadHoldingRegisters(pulsaresRegCurrent, 1)
+	if err != nil {
+		return 0, err
+	}
+
+	return binary.BigEndian.Uint16(b), nil
+}
+
+// Status implements the api.Charger interface
+func (wb *Pulsares) Status() (api.ChargeStatus, error) {
+	b, err := wb.conn.ReadHoldingRegisters(pulsaresRegStatus, 1)
+	if err != nil {
+		return api.StatusNone, err
+	}
+
+	switch u := binary.BigEndian.Uint16(b); u {
+	case 0:
+		return api.StatusA, nil
+	case 1, 2:
+		return api.StatusB, nil
+	case 3, 4:
+		return api.StatusC, nil
+	default:
+		return api.StatusNone, fmt.Errorf("invalid status: %d", u)
+	}
+}
+
+// Enabled implements the api.Charger interface
+func (wb *Pulsares) Enabled() (bool, error) {
+	curr, err := wb.getCurrent()
+
+	return curr >= 6000, err
+}
+
+// Enable implements the api.Charger interface
+func (wb *Pulsares) Enable(enable bool) error {
+	var curr uint16
+	if enable {
+		curr = wb.curr
+	}
+
+	return wb.setCurrent(curr)
+}
+
+// MaxCurrent implements the api.Charger interface
+func (wb *Pulsares) MaxCurrent(current int64) error {
+	return wb.MaxCurrentMillis(float64(current))
+}
+
+var _ api.ChargerEx = (*Pulsares)(nil)
+
+// MaxCurrent implements the api.ChargerEx interface
+func (wb *Pulsares) MaxCurrentMillis(current float64) error {
+	if current < 6 {
+		return fmt.Errorf("invalid current %.1f", current)
+	}
+
+	err := wb.setCurrent(wb.curr)
+	if err == nil {
+		wb.curr = uint16(current * 1e3)
+	}
+
+	return err
+}
diff --git a/charger/smaevcharger.go b/charger/smaevcharger.go
index 37502204..f557b81c 100644
--- a/charger/smaevcharger.go
+++ b/charger/smaevcharger.go
@@ -252,18 +252,18 @@ var _ api.PhaseCurrents = (*Smaevcharger)(nil)
 
 // Currents implements the api.PhaseCurrents interface
 func (wb *Smaevcharger) Currents() (float64, float64, float64, error) {
-	var curr []float64
+	var res [3]float64
 
-	for _, phase := range []string{"A", "B", "C"} {
+	for i, phase := range []string{"A", "B", "C"} {
 		val, err := wb.getMeasurement("Measurement.GridMs.A.phs" + phase)
 		if err != nil {
 			return 0, 0, 0, err
 		}
 
-		curr = append(curr, -val)
+		res[i] = -val
 	}
 
-	return curr[0], curr[1], curr[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 // reset cache
diff --git a/charger/vestel.go b/charger/vestel.go
index b4e1a223..9d4c8df7 100644
--- a/charger/vestel.go
+++ b/charger/vestel.go
@@ -42,11 +42,13 @@ const (
 	vestelRegSessionEnergy   = 1502
 	vestelRegFailsafeTimeout = 2002
 	vestelRegAlive           = 6000
-	//vestelRegChargepointState = 1000
+	// vestelRegChargepointState = 1000
 )
 
-var vestelRegCurrents = []uint16{1008, 1010, 1012} // non-continuous uint16 registers!
-var vestelRegVoltages = []uint16{1014, 1016, 1018} // non-continuous uint16 registers!
+var (
+	vestelRegCurrents = []uint16{1008, 1010, 1012} // non-continuous uint16 registers!
+	vestelRegVoltages = []uint16{1014, 1016, 1018} // non-continuous uint16 registers!
+)
 
 // Vestel is an api.Charger implementation for Vestel/Hymes wallboxes with Ethernet (SW modells).
 // It uses Modbus TCP to communicate with the wallbox at modbus client id 255.
@@ -208,38 +210,33 @@ func (wb *Vestel) ChargedEnergy() (float64, error) {
 	return float64(binary.BigEndian.Uint32(b)) / 1e3, err
 }
 
-var _ api.PhaseCurrents = (*Vestel)(nil)
-
-// Currents implements the api.PhaseCurrents interface
-func (wb *Vestel) Currents() (float64, float64, float64, error) {
-	var currents []float64
-	for _, regCurrent := range vestelRegCurrents {
-		b, err := wb.conn.ReadInputRegisters(regCurrent, 1)
+// getPhaseValues returns 3 sequential register values
+func (wb *Vestel) getPhaseValues(regs []uint16, divider float64) (float64, float64, float64, error) {
+	var res [3]float64
+	for i, reg := range regs {
+		b, err := wb.conn.ReadInputRegisters(reg, 1)
 		if err != nil {
 			return 0, 0, 0, err
 		}
 
-		currents = append(currents, float64(binary.BigEndian.Uint16(b))/1e3)
+		res[i] = float64(binary.BigEndian.Uint16(b)) / divider
 	}
 
-	return currents[0], currents[1], currents[2], nil
+	return res[0], res[1], res[2], nil
+}
+
+var _ api.PhaseCurrents = (*Vestel)(nil)
+
+// Currents implements the api.PhaseCurrents interface
+func (wb *Vestel) Currents() (float64, float64, float64, error) {
+	return wb.getPhaseValues(vestelRegCurrents, 1e3)
 }
 
 var _ api.PhaseVoltages = (*Vestel)(nil)
 
 // Voltages implements the api.PhaseVoltages interface
 func (wb *Vestel) Voltages() (float64, float64, float64, error) {
-	var voltages []float64
-	for _, regVoltage := range vestelRegVoltages {
-		b, err := wb.conn.ReadInputRegisters(regVoltage, 1)
-		if err != nil {
-			return 0, 0, 0, err
-		}
-
-		voltages = append(voltages, float64(binary.BigEndian.Uint16(b)))
-	}
-
-	return voltages[0], voltages[1], voltages[2], nil
+	return wb.getPhaseValues(vestelRegVoltages, 1)
 }
 
 var _ api.Diagnosis = (*Vestel)(nil)
diff --git a/charger/wallbe.go b/charger/wallbe.go
index 8b5a1f4d..9f43c159 100644
--- a/charger/wallbe.go
+++ b/charger/wallbe.go
@@ -195,17 +195,17 @@ func (wb *Wallbe) totalEnergy() (float64, error) {
 
 // currents implements the api.PhaseCurrents interface
 func (wb *Wallbe) currents() (float64, float64, float64, error) {
-	var currents []float64
-	for _, regCurrent := range wbRegCurrents {
-		b, err := wb.conn.ReadInputRegisters(regCurrent, 2)
+	var res [3]float64
+	for i, reg := range wbRegCurrents {
+		b, err := wb.conn.ReadInputRegisters(reg, 2)
 		if err != nil {
 			return 0, 0, 0, err
 		}
 
-		currents = append(currents, rs485.RTUInt32ToFloat64Swapped(b))
+		res[i] = rs485.RTUInt32ToFloat64Swapped(b)
 	}
 
-	return currents[0], currents[1], currents[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 var _ api.Diagnosis = (*Wallbe)(nil)
diff --git a/charger/webasto-next.go b/charger/webasto-next.go
index 6b2c7030..57871405 100644
--- a/charger/webasto-next.go
+++ b/charger/webasto-next.go
@@ -217,17 +217,17 @@ var _ api.PhaseCurrents = (*WebastoNext)(nil)
 
 // Currents implements the api.PhaseCurrents interface
 func (wb *WebastoNext) Currents() (float64, float64, float64, error) {
-	var curr [3]float64
+	var res [3]float64
 	for l := uint16(0); l < 3; l++ {
 		b, err := wb.conn.ReadInputRegisters(tqRegCurrents+2*l, 1)
 		if err != nil {
 			return 0, 0, 0, err
 		}
 
-		curr[l] = float64(binary.BigEndian.Uint16(b)) / 1e3
+		res[l] = float64(binary.BigEndian.Uint16(b)) / 1e3
 	}
 
-	return curr[0], curr[1], curr[2], nil
+	return res[0], res[1], res[2], nil
 }
 
 var _ api.Identifier = (*WebastoNext)(nil)
diff --git a/core/loadpoint.go b/core/loadpoint.go
index 95cbcc3b..930f7add 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -153,7 +153,7 @@ type Loadpoint struct {
 	planner     *planner.Planner
 	targetTime  time.Time // time goal
 	planSlotEnd time.Time // current plan slot end time
-	planActive  bool      // plan is active
+	planActive  bool      // charge plan exists and has a currently active slot
 
 	// cached state
 	status         api.ChargeStatus       // Charger status
@@ -668,7 +668,8 @@ func (lp *Loadpoint) syncCharger() error {
 				return err
 			}
 
-			if lp.chargeCurrent != current {
+			// smallest adjustment most PWM-Controllers can do is: 100%÷256×0,6A = 0.234A
+			if math.Abs(lp.chargeCurrent-current) > 0.23 {
 				if lp.guardGracePeriodElapsed() {
 					lp.log.WARN.Printf("charger logic error: current mismatch (got %.3gA, expected %.3gA)", current, lp.chargeCurrent)
 				}
@@ -685,8 +686,8 @@ func (lp *Loadpoint) syncCharger() error {
 		// ignore disabled state if vehicle was disconnected ^(lp.enabled && ^lp.connected)
 		if lp.guardGracePeriodElapsed() && lp.phaseSwitchCompleted() && (enabled || lp.connected()) {
 			lp.log.WARN.Printf("charger out of sync: expected %vd, got %vd", status[lp.enabled], status[enabled])
+			lp.elapseGuard()
 		}
-		lp.elapseGuard()
 		return nil
 	}
 
@@ -824,6 +825,9 @@ func (lp *Loadpoint) minSocNotReached() bool {
 	}
 
 	if lp.vehicleSoc != 0 {
+		if lp.vehicleSoc < float64(lp.Soc.min) {
+			lp.log.DEBUG.Printf("forced charging at vehicle soc %.0f%% (< %.0f%% min soc)", lp.vehicleSoc, float64(lp.Soc.min))
+		}
 		return lp.vehicleSoc < float64(lp.Soc.min)
 	}
 
@@ -1377,7 +1381,7 @@ func (lp *Loadpoint) publishSocAndRange() {
 		return
 	}
 
-	if err == nil || lp.vehicleSocPollAllowed() {
+	if err == nil || lp.chargerHasFeature(api.IntegratedDevice) || lp.vehicleSocPollAllowed() {
 		lp.socUpdated = lp.clock.Now()
 
 		f, err := lp.socEstimator.Soc(lp.getChargedEnergy())
diff --git a/core/loadpoint_plan.go b/core/loadpoint_plan.go
index ed5faf37..94002aa0 100644
--- a/core/loadpoint_plan.go
+++ b/core/loadpoint_plan.go
@@ -66,7 +66,7 @@ func (lp *Loadpoint) GetPlan(targetTime time.Time, maxPower float64) (time.Durat
 	return requiredDuration, plan, err
 }
 
-// plannerActive checks if the charging plan has an active slot
+// plannerActive checks if the charging plan has a currently active slot
 func (lp *Loadpoint) plannerActive() (active bool) {
 	defer func() {
 		lp.setPlanActive(active)
diff --git a/core/site.go b/core/site.go
index 7878fc95..a4c9c226 100644
--- a/core/site.go
+++ b/core/site.go
@@ -613,7 +613,7 @@ func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, b
 
 		// if battery is charging below prioritySoc give it priority
 		if site.batterySoc < site.PrioritySoc && batteryPower < 0 {
-			site.log.DEBUG.Printf("giving priority to battery charging at soc: %.0f%%", site.batterySoc)
+			site.log.DEBUG.Printf("battery has priority at soc %.0f%% (< %.0f%%)", site.batterySoc, site.PrioritySoc)
 			batteryPower = 0
 		} else {
 			// if battery is above bufferSoc allow using it for charging
@@ -758,6 +758,7 @@ func (site *Site) update(lp Updater) {
 		if err == nil {
 			limit := site.GetSmartCostLimit()
 			autoCharge = limit != 0 && rate.Price <= limit
+			site.publish("smartCostActive", autoCharge)
 		} else {
 			site.log.ERROR.Println("tariff:", err)
 		}
@@ -802,6 +803,7 @@ func (site *Site) prepare() {
 	site.publish("residualPower", site.ResidualPower)
 	site.publish("smartCostLimit", site.SmartCostLimit)
 	site.publish("smartCostType", nil)
+	site.publish("smartCostActive", false)
 	if tariff := site.GetTariff(PlannerTariff); tariff != nil {
 		site.publish("smartCostType", tariff.Type().String())
 	}
diff --git a/core/site_api.go b/core/site_api.go
index f7d4ba7e..6be6cdeb 100644
--- a/core/site_api.go
+++ b/core/site_api.go
@@ -147,7 +147,7 @@ func (site *Site) GetTariff(tariff string) api.Tariff {
 			// prio 0: manually set planner tariff
 			return site.tariffs.Planner
 
-		case site.tariffs.Grid != nil && site.tariffs.Grid.Type() == api.TariffTypePriceDynamic:
+		case site.tariffs.Grid != nil && site.tariffs.Grid.Type() == api.TariffTypePriceForecast:
 			// prio 1: dynamic grid tariff
 			return site.tariffs.Grid
 
diff --git a/evcc.dist.yaml b/evcc.dist.yaml
index 990598d2..5ed71c87 100644
--- a/evcc.dist.yaml
+++ b/evcc.dist.yaml
@@ -95,7 +95,7 @@ vehicles:
     vin: WREN...
     onIdentify: # set defaults when vehicle is identified
       mode: pv # enable PV-charging when vehicle is identified
-      minSoc: 20 # immediately charge to 0% regardless of mode unless "off" (disabled)
+      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
@@ -194,6 +194,13 @@ tariffs:
     # securitytoken: # api token
     # charges: # optional, additional charges per kWh
     # tax: # optional, additional tax (0.1 for 10%)
+
+    # type: custom # price from a plugin source; see https://docs.evcc.io/docs/reference/plugins
+    # price:
+    #   source: http
+    #   uri: https://example.org/price.json
+    #   jq: .price.current
+
   feedin:
     # rate for feeding excess (pv) energy to the grid
     type: fixed
diff --git a/go.mod b/go.mod
index cd9d1a46..4d9e7191 100644
--- a/go.mod
+++ b/go.mod
@@ -36,7 +36,6 @@ require (
 	github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
 	github.com/godbus/dbus/v5 v5.1.0
 	github.com/gokrazy/updater v0.0.0-20230215172637-813ccc7f21e2
-	github.com/golang-module/carbon/v2 v2.2.8
 	github.com/golang/mock v1.6.0
 	github.com/google/go-github/v32 v32.1.0
 	github.com/google/uuid v1.3.1
@@ -51,6 +50,7 @@ require (
 	github.com/itchyny/gojq v0.12.13
 	github.com/jeremywohl/flatten v1.0.1
 	github.com/jinzhu/copier v0.4.0
+	github.com/jinzhu/now v1.1.5
 	github.com/joeshaw/carwings v0.0.0-20230425210048-195f7e10e966
 	github.com/joho/godotenv v1.5.1
 	github.com/jpfielding/go-http-digest v0.0.0-20211006141426-fbc93758452e
@@ -59,7 +59,7 @@ require (
 	github.com/korylprince/ipnetgen v1.0.1
 	github.com/kr/pretty v0.3.1
 	github.com/libp2p/zeroconf/v2 v2.2.0
-	github.com/lorenzodonini/ocpp-go v0.17.0
+	github.com/lorenzodonini/ocpp-go v0.17.1-0.20231018201659-a575f3e7454d
 	github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
 	github.com/mabunixda/wattpilot v1.6.2
 	github.com/manifoldco/promptui v0.9.0
@@ -92,7 +92,7 @@ require (
 	gitlab.com/bboehmke/sunny v0.15.1-0.20211022160056-2fba1c86ade6
 	golang.org/x/crypto/x509roots/fallback v0.0.0-20231005151211-e3cc52e598e3
 	golang.org/x/exp v0.0.0-20231006140011-7918f672742d
-	golang.org/x/net v0.16.0
+	golang.org/x/net v0.17.0
 	golang.org/x/oauth2 v0.13.0
 	golang.org/x/sync v0.4.0
 	golang.org/x/text v0.13.0
@@ -145,7 +145,6 @@ require (
 	github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf // indirect
 	github.com/itchyny/timefmt-go v0.1.5 // indirect
 	github.com/jinzhu/inflection v1.0.0 // indirect
-	github.com/jinzhu/now v1.1.5 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/json-iterator/go v1.1.12 // indirect
 	github.com/klauspost/compress v1.17.0 // indirect
diff --git a/go.sum b/go.sum
index 1b1dc724..8a44a885 100644
--- a/go.sum
+++ b/go.sum
@@ -297,8 +297,6 @@ github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
 github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
 github.com/gokrazy/updater v0.0.0-20230215172637-813ccc7f21e2 h1:kBY5R1tSf+EYZ+QaSrofLaVJtBqYsVNVBWkdMq3Smcg=
 github.com/gokrazy/updater v0.0.0-20230215172637-813ccc7f21e2/go.mod h1:PYOvzGOL4nlBmuxu7IyKQTFLaxr61+WPRNRzVtuYOHw=
-github.com/golang-module/carbon/v2 v2.2.8 h1:a1VxHHKAR7fc1ho7sYXhS1s5S4x7+oqAf2EY5p8C46A=
-github.com/golang-module/carbon/v2 v2.2.8/go.mod h1:XDALX7KgqmHk95xyLeaqX9/LJGbfLATyruTziq68SZ8=
 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
 github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -529,8 +527,8 @@ github.com/libp2p/zeroconf/v2 v2.2.0 h1:Cup06Jv6u81HLhIj1KasuNM/RHHrJ8T7wOTS4+Tv
 github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs=
 github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
 github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
-github.com/lorenzodonini/ocpp-go v0.17.0 h1:dDukZNxtQhlDfai20P/jFbJCxinnqx44ml/mrcawfGQ=
-github.com/lorenzodonini/ocpp-go v0.17.0/go.mod h1:jVzgk5k3JVGltWMU/QGlU7chojlCd/bJYc4WYl8R2M4=
+github.com/lorenzodonini/ocpp-go v0.17.1-0.20231018201659-a575f3e7454d h1:ISizXg3UbeviwUvJ0aJTHQlsn0QmGqYwEfxBzPsSWcY=
+github.com/lorenzodonini/ocpp-go v0.17.1-0.20231018201659-a575f3e7454d/go.mod h1:jVzgk5k3JVGltWMU/QGlU7chojlCd/bJYc4WYl8R2M4=
 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
 github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
@@ -959,8 +957,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
-golang.org/x/net v0.16.0 h1:7eBu7KsSvFDtSXUIDbh3aqlK4DPsZ1rByC8PFfBThos=
-golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
diff --git a/i18n/da.toml b/i18n/da.toml
index d9c3c997..43a113b0 100644
--- a/i18n/da.toml
+++ b/i18n/da.toml
@@ -1,5 +1,5 @@
 [batterySettings]
-batteryLevel = "Batteri niveau"
+batteryLevel = "Batteriniveau"
 capacity = "{energy} af {total}"
 legendBottomName = "Prioritér hjemmet"
 legendBottomSubline = "Anvendes ikke til opladning"
@@ -7,7 +7,7 @@ legendMiddleName = "Bil først"
 legendMiddleSubline = "Hjem efterfølgende"
 legendTitle = "Hvordan skal solenergi benyttes?"
 legendTopAutostart = "starter automatisk"
-legendTopName = "batterie understøtted opladning"
+legendTopName = "batterie understøttet opladning"
 legendTopSubline = "uden afbrydelser"
 modalTitle = "batterie indstillinger"
 note = "OBS: Disse indstillinger gælder kun for solar modus. Indstillingerne ændres tilsvarende."
@@ -27,7 +27,7 @@ optional = "valgfrit"
 [config.main]
 addVehicle = "Tilføj bil"
 edit = "rediger"
-title = "Konfiguration"
+title = "Opsætning"
 vehicles = "Mine biler"
 
 [config.validation]
@@ -45,9 +45,11 @@ generic = "Andre integrationer"
 offline = "Generisk bil"
 online = "Biler med online API"
 save = "Gem"
-scooter = "Scooter"
+scooter = "Knallert/Scooter"
 template = "Fabrikant"
 titleAdd = "Tilføj køretøj"
+titleEdit = "Rediger køretøj"
+validateSave = "Valider og gem"
 
 [footer]
 
@@ -64,8 +66,8 @@ powerSub2 = "opladning ..."
 tabTitle = "Live fællesskab"
 
 [footer.savings]
-footerLong = "{percent}% Fotovoltaisk energi"
-footerShort = "{percent}% Fotovoltaisk energi"
+footerLong = "{percent}% Solenergi"
+footerShort = "{percent}% Solenergi"
 modalTitle = "Oversigt over ladeenergi"
 percentGrid = "{grid} kWh Net"
 percentSelf = "{self} kWh solenergi"
@@ -197,7 +199,7 @@ label = "Max. Nuværende"
 label = "Min. Nuværende"
 
 [main.loadpointSettings.minSoc]
-description = "Til nødstilfælde. Køretøjet bliver „hurtigt” opladet til {0} % fra al tilgængelig solenergi, og fortsætter derefter med kun solenergioverskuddet."
+description = "Køretøjet bliver „hurtigt” opladet til {0} % i solar mode, og fortsætter derefter med solenergioverskuddet. Nyttig til at sikre en minimum rækkevidde selv på mørke dage"
 label = "Min. opladning %"
 
 [main.loadpointSettings.phasesConfigured]
@@ -209,7 +211,7 @@ phases_3 = "3 fase"
 phases_3_hint = "({min} til {max})"
 
 [main.mode]
-minpv = "Min+Solar"
+minpv = "Min+Sol"
 now = "Hurtig"
 off = "Fra"
 pv = "Sol"
@@ -225,17 +227,19 @@ co2Limit = "CO₂ grænse på {co2}"
 costLimitIgnore = "Den konfigurerede {limit} vil blive ignoreret i denne periode."
 descriptionEnergy = "Hvornår skal {targetEnergy} være sendt til køretøjet?"
 descriptionSoc = "Hvornår skal køretøjet oplades til {targetSoc} %?"
-inactiveLabel = "Måltid"
+inactiveLabel = "Planlagt tid"
 modalTitle = "Sæt måltid"
+onlyInPvMode = "Opladningsplan virker kun i solar mode"
 planDuration = "Opladningstid"
 planPeriodLabel = "Periode"
 planPeriodValue = "{start} til {end}"
 planUnknown = "Ikke kendt endnu"
 priceLimit = "pris grænse på {price}"
 setTargetTime = "ingen"
+targetEnergyRequired = "Du skal også indstille en energigrænse"
 targetIsInThePast = "Vælg et tidspunkt i fremtiden, Marty."
 targetIsTooFarInTheFuture = "Vi justerer planen så snart vi ved mere om fremtiden."
-title = "Måltid"
+title = "Planlagt tid"
 today = "i dag"
 tomorrow = "i morgen"
 update = "Opdatering"
@@ -259,6 +263,8 @@ fallbackName = "Køretøj"
 moreActions = "Flere handlinger"
 none = "Intet køretøj"
 targetSoc = "Begrænse"
+temp = "Temp"
+tempLimit = "Temp. grænse"
 unknown = "Gæstekøretøj"
 vehicleSoc = "Oplade"
 
@@ -271,8 +277,8 @@ vehicleTarget = "Køretøjsgrænse: {soc}%"
 
 [main.vehicleStatus]
 charging = "Opladning..."
-cheapEnergyCharging = "Billig energi tilgængelig. Oplader..."
-cleanEnergyCharging = "Ren energi tilgængelig. Oplader..."
+cheapEnergyCharging = "Oplader med billig energi: {price} (limit < {limit})"
+cleanEnergyCharging = "Oplader med CO2 neutral energi: {co2} (limit < {limit})"
 climating = "Forkonditionering registreret."
 connected = "Forbundet."
 disconnected = "Afbrudt."
@@ -312,7 +318,9 @@ started = "Startet"
 title = "Lade session"
 
 [sessions]
+avgPower = "⌀ Effekt"
 avgPrice = "⌀ pris"
+chargeDuration = "Varighed"
 co2 = "⌀ CO₂"
 csvMonth = "Download {month} CSV"
 csvTotal = "Download total CSV"
@@ -376,7 +384,7 @@ mi = "mil"
 
 [startupError]
 configFile = "Konfigurationsfil brugt:"
-configuration = "Konfiguration"
+configuration = "Opsætning"
 description = "Tjek venligst din konfigurationsfil. Hvis fejlmeddelelsen ikke hjælper, så tjek {0}."
 discussions = "GitHub Diskussioner"
 fixAndRestart = "Løs problemet og genstart serveren."
diff --git a/i18n/de.toml b/i18n/de.toml
index 8f9293b7..ac005704 100644
--- a/i18n/de.toml
+++ b/i18n/de.toml
@@ -282,8 +282,8 @@ vehicleTarget = "Fahrzeuglimit: {soc}%"
 
 [main.vehicleStatus]
 charging = "Ladevorgang aktiv …"
-cheapEnergyCharging = "Günstige Energie verfügbar. Ladevorgang aktiv …"
-cleanEnergyCharging = "Saubere Energie verfügbar. Ladevorgang aktiv …"
+cheapEnergyCharging = "Lade günstige Energie: {price} (Grenze {limit})"
+cleanEnergyCharging = "Lade saubere Energie: {co2} (Grenze {limit})"
 climating = "Vorklimatisierung erkannt."
 connected = "Verbunden."
 disconnected = "Nicht verbunden."
diff --git a/i18n/en.toml b/i18n/en.toml
index 6430c9e6..e78f176b 100644
--- a/i18n/en.toml
+++ b/i18n/en.toml
@@ -279,8 +279,8 @@ vehicleTarget = "Vehicle limit: {soc}%"
 
 [main.vehicleStatus]
 charging = "Charging…"
-cheapEnergyCharging = "Cheap energy available. Charging…"
-cleanEnergyCharging = "Clean energy available. Charging…"
+cheapEnergyCharging = "Charging cheap energy: {price} (limit {limit})"
+cleanEnergyCharging = "Charging clean energy: {co2} (limit {limit})"
 climating = "Pre-conditioning detected."
 connected = "Connected."
 disconnected = "Disconnected."
diff --git a/i18n/es.toml b/i18n/es.toml
index cd10aedf..5c12f3a6 100644
--- a/i18n/es.toml
+++ b/i18n/es.toml
@@ -286,8 +286,8 @@ vehicleTarget = "límite de vehículo: {soc}%"
 
 [main.vehicleStatus]
 charging = "Cargando ..."
-cheapEnergyCharging = "Energía barata disponible. Cargando..."
-cleanEnergyCharging = "Energía limpia disponible. Cargando..."
+cheapEnergyCharging = "Cargando con energía barata: {price} (limit {limit})"
+cleanEnergyCharging = "Cargando con energía limpia: {co2} (límite {limit})"
 climating = "Preacondicionamiento detectado."
 connected = "Conectado."
 disconnected = "Desconectado."
diff --git a/i18n/hr.toml b/i18n/hr.toml
index a0efde43..9a488667 100644
--- a/i18n/hr.toml
+++ b/i18n/hr.toml
@@ -18,6 +18,39 @@ low = "kad je donekle pun"
 medium = "kad je napola pun"
 never = "samo s dovoljno viška"
 
+[config]
+
+[config.form]
+example = "Primjer"
+optional = "opcionalno"
+
+[config.main]
+addVehicle = "Dodaj vozilo"
+edit = "uredi"
+title = "Konfiguracija"
+vehicles = "Moja vozila"
+
+[config.validation]
+failed = "nauspjelo"
+label = "Stanje"
+running = "verifikacija …"
+success = "uspješno"
+unknown = "nepoznato"
+validate = "verificiraj"
+
+[config.vehicle]
+cancel = "Odustani"
+delete = "Izbriši vozilo"
+generic = "Druge integracije"
+offline = "Opće vozilo"
+online = "Vozilo s internetskim API sučeljem"
+save = "Spremi"
+scooter = "Skuter"
+template = "Proizvođač"
+titleAdd = "Dodaj vozilo"
+titleEdit = "Uredi vozilo"
+validateSave = "Verificiraj i spremi"
+
 [footer]
 
 [footer.community]
@@ -231,6 +264,8 @@ fallbackName = "Vozilo"
 moreActions = "Više radnji"
 none = "Nijedno vozilo"
 targetSoc = "Ograničenje"
+temp = "Temperatura"
+tempLimit = "Granica temperature"
 unknown = "Vozilo gosta"
 vehicleSoc = "Napajanje"
 
@@ -243,8 +278,8 @@ vehicleTarget = "Ograničenje vozila: {soc} %"
 
 [main.vehicleStatus]
 charging = "Napajanje …"
-cheapEnergyCharging = "Dostupna je jeftina energija. Napaja se …"
-cleanEnergyCharging = "Dostupna je čista energija. Napaja se …"
+cheapEnergyCharging = "Napajanje jeftinom energijom: {price} (ograničenje < {limit})"
+cleanEnergyCharging = "Napajanje čistom energijom: {co2} (ograničenje < {limit})"
 climating = "Otkriveno predkondicioniranje."
 connected = "Povezano."
 disconnected = "Nepovezano."
@@ -284,7 +319,9 @@ started = "Počelo"
 title = "Sesije napajanja"
 
 [sessions]
+avgPower = "⌀ snaga"
 avgPrice = "⌀ cijena"
+chargeDuration = "Trajanje"
 co2 = "⌀ CO₂"
 csvMonth = "Preuzmi CSV za {month}"
 csvTotal = "Preuzmi CSV za ukupno"
diff --git a/i18n/lt.toml b/i18n/lt.toml
index b7bb81d4..db6ffad8 100644
--- a/i18n/lt.toml
+++ b/i18n/lt.toml
@@ -272,7 +272,7 @@ fallbackName = "Automobilis"
 moreActions = "Daugiau veiksmų"
 none = "Nėra automobilio"
 targetSoc = "Limitas"
-temp = "Temp."
+temp = "Temp"
 tempLimit = "Temp. limitas"
 unknown = "Nežinomas automobilis"
 vehicleSoc = "Baterija"
@@ -286,8 +286,8 @@ vehicleTarget = "Automobilyje nustatytas limitas: {soc}%"
 
 [main.vehicleStatus]
 charging = "Įkraunama..."
-cheapEnergyCharging = "Energijos kaina žema. Įkraunama…"
-cleanEnergyCharging = "Yra švarios energijos. Įkraunama..."
+cheapEnergyCharging = "Įkraunama pigia energija: {price} (limit {limit})"
+cleanEnergyCharging = "Įkraunama švaria energija: {co2} (limit {limit})"
 climating = "Aptiktas išankstinis kondicionavimas."
 connected = "Prijungtas."
 disconnected = "Neprijungtas."
diff --git a/i18n/nl.toml b/i18n/nl.toml
index db42e397..8e9b2879 100644
--- a/i18n/nl.toml
+++ b/i18n/nl.toml
@@ -14,10 +14,43 @@ note = "Opgelet: Deze instellingen beïnvloeden enkel de zonnemodus. Oplaadgedra
 
 [batterySettings.bufferStart]
 full = "als bijna vol"
-low = "als beetje vol"
-medium = "als half vol"
+low = "indien beetje vol"
+medium = "indien half vol"
 never = "enkel bij voldoende overschot"
 
+[config]
+
+[config.form]
+example = "Voorbeeld"
+optional = "Optioneel"
+
+[config.main]
+addVehicle = "Voertuig toevoegen"
+edit = "Bewerken"
+title = "Configuratie"
+vehicles = "Mijn voertuigen"
+
+[config.validation]
+failed = "mislukt"
+label = "Status"
+running = "valideren..."
+success = "succesvol"
+unknown = "onbekend"
+validate = "valideer"
+
+[config.vehicle]
+cancel = "Annuleer"
+delete = "Verwijder voertuig"
+generic = "Andere integraties"
+offline = "Algemeen voertuig"
+online = "Voertuig met online API"
+save = "Opslaan"
+scooter = "Scooter"
+template = "Fabrikant"
+titleAdd = "Voertuig toevoegen"
+titleEdit = "Voertuig bewerken"
+validateSave = "Valideren & opslaan"
+
 [footer]
 
 [footer.community]
@@ -174,7 +207,7 @@ label = "Max. Stroom"
 label = "Min. Stroom"
 
 [main.loadpointSettings.minSoc]
-description = "Voor noodgevallen. Het voertuig wordt „snel” opgeladen tot {0}% van alle beschikbare zonne-energie, en gaat dan verder met alleen het zonneoverschot."
+description = "Het voertuig wordt „snel” opgeladen tot {0}% in deze modus, en gaat dan verder met alleen het zonneoverschot."
 label = "Min. lading %"
 
 [main.loadpointSettings.phasesConfigured]
@@ -204,6 +237,7 @@ descriptionEnergy = "Tegen wanneer moet {targetEnergy} in het voertuig geladen z
 descriptionSoc = "Wanneer moet het voertuig geladen zijn naar {targetSoc}%?"
 inactiveLabel = "Ingestelde tijd"
 modalTitle = "Stel tijd in"
+onlyInPvMode = "Laadplan werkt alleen in PV modus"
 planDuration = "Oplaadtijd"
 planPeriodLabel = "Periode"
 planPeriodValue = "{start} tot {end}"
@@ -211,6 +245,7 @@ planUnknown = "nog niet bekend"
 priceLimit = "prijslimiet van {price}"
 remove = "Verwijder"
 setTargetTime = "geen"
+targetEnergyRequired = "Je dient ook een energie limiet in te stellen"
 targetIsInThePast = "Kies een tijd in de toekomst, Marty."
 targetIsTooFarInTheFuture = "We passen het plan aan zodra we meer weten over de toekomst."
 title = "Doeltijd"
@@ -237,6 +272,8 @@ fallbackName = "Voertuig"
 moreActions = "Meer acties"
 none = "Geen voertuig"
 targetSoc = "Limiet"
+temp = "Temperatuur"
+tempLimit = "Temperatuur limiet"
 unknown = "Gast voertuig"
 vehicleSoc = "Lading"
 
@@ -290,7 +327,9 @@ started = "Gestart"
 title = "Oplaadsessie"
 
 [sessions]
+avgPower = "⌀ Vermogen"
 avgPrice = "⌀ Prijs"
+chargeDuration = "Duur"
 co2 = "⌀ CO₂"
 csvMonth = "Download CSV van {month}"
 csvTotal = "Download totale CSV"
@@ -303,6 +342,7 @@ price = "Σ Prijs"
 reallyDelete = "Wilt u deze sessie echt verwijderen?"
 solar = "Zon"
 title = "Oplaadsessies"
+total = "Totaal"
 vehicle = "Voertuig"
 
 [sessions.csv]
diff --git a/i18n/pt.toml b/i18n/pt.toml
index 1dbd3e1b..228ef307 100644
--- a/i18n/pt.toml
+++ b/i18n/pt.toml
@@ -286,8 +286,8 @@ vehicleTarget = "Limite de veículo: {soc}%"
 
 [main.vehicleStatus]
 charging = "Carregando..."
-cheapEnergyCharging = "Energia barata disponível. Carregando..."
-cleanEnergyCharging = "Energia limpa disponível. Carregando..."
+cheapEnergyCharging = "Carregando energia barata: {price} (limit {limit})"
+cleanEnergyCharging = "Carregando energia limpa: {co2} (limit {limit})"
 climating = "Pré-condicionamento detectado."
 connected = "Ligado."
 disconnected = "Não conectado."
diff --git a/i18n/zh-Hans.toml b/i18n/zh-Hans.toml
new file mode 100644
index 00000000..f2185193
--- /dev/null
+++ b/i18n/zh-Hans.toml
@@ -0,0 +1,188 @@
+[batterySettings]
+batteryLevel = "电池等级"
+capacity = "{energy} / {total}"
+legendBottomName = "家庭优先"
+legendBottomSubline = "不用充电"
+legendMiddleName = "交通工具优先"
+legendMiddleSubline = "家用其次"
+legendTitle = "太阳能应该如何利用?"
+legendTopAutostart = "自动开启"
+legendTopName = "电池支持充电"
+legendTopSubline = "勿打扰"
+modalTitle = "电池配置"
+note = "注意:这些设置仅影响太阳能模式.充电行为会相应调整."
+
+[batterySettings.bufferStart]
+full = "当快满的时候"
+low = "当有点满的时候"
+medium = "半满时"
+never = "只要有足够的剩余"
+
+[config]
+
+[config.form]
+example = "示例"
+optional = "可选"
+
+[config.main]
+addVehicle = "添加车辆"
+edit = "编辑"
+title = "配置"
+vehicles = "我的车辆"
+
+[config.validation]
+failed = "失败"
+label = "状态"
+running = "验证中..."
+success = "成功"
+unknown = "未知"
+validate = "验证"
+
+[config.vehicle]
+cancel = "取消"
+delete = "移除车辆"
+generic = "其他集成"
+offline = "通用车辆"
+online = "具有在线API的车辆"
+save = "保存"
+scooter = "小型摩托车"
+template = "制造商"
+titleAdd = "添加车辆"
+titleEdit = "编辑车辆"
+validateSave = "校验和保存"
+
+[footer]
+
+[footer.community]
+greenEnergy = "太阳能"
+greenEnergySub1 = "用evcc充电"
+greenEnergySub2 = "自 2022 年 10 月起"
+greenShare = "太阳能分享"
+greenShareSub1 = "电力被提供"
+greenShareSub2 = "太阳能和电池存储"
+power = "充电电源"
+powerSub1 = "{activeClients} of {totalClients} 参与者"
+powerSub2 = "充电中..."
+tabTitle = "Live社区"
+
+[footer.savings]
+footerLong = "{percent}% 太阳能"
+footerShort = "{percent}% 太阳能"
+modalTitle = "充电概览"
+percentGrid = "{grid} kWh 电网"
+percentSelf = "{self} kWh 太阳能"
+percentTitle = "太阳能"
+priceFeedIn = "{feedInPrice} 冲入"
+priceGrid = "{gridPrice} 电网"
+priceTitle = "电力价格"
+savingsComparedToGrid = "和电网比较"
+savingsTitle = "节省"
+savingsTotalEnergy = "{total} kWh 已充"
+since = "从 {since}"
+tabTitle = "我的数据"
+
+[footer.sponsor]
+becomeSponsor = "成为赞助商"
+confetti = "准备好庆祝了吗?"
+confettiPromise = "你会得到贴纸和数字纸屑"
+sticker = "… 或者 evcc 贴纸?"
+supportUs = "我们的使命是让太阳能成为常态。通过支付对您有价值的东西来帮助 evcc"
+thanks = "谢谢你, {sponsor}! 您的贡献有助于进一步发展evcc"
+titleNoSponsor = "支持我们"
+titleSponsor = "你是支持者"
+
+[footer.telemetry]
+optIn = "我想贡献我的数据"
+optInMoreDetails = "更多细节 {0}."
+optInMoreDetailsLink = "这里"
+optInSponsorship = "需要赞助."
+
+[footer.version]
+availableLong = "新版本可用"
+modalCancel = "取消"
+modalDownload = "下载"
+modalInstalledVersion = "已经安装版本"
+modalNoReleaseNotes = "没有可用的发行说明. 有关新版本的更多信息:"
+modalTitle = "新版本可用"
+modalUpdate = "安装"
+modalUpdateNow = "现在安装"
+modalUpdateStarted = "启动新版本的evcc..."
+modalUpdateStatusStart = "安装已开始:"
+
+[gridSettings]
+activeHours = "{charging} / {total}"
+activeHoursLabel = "运行小时"
+co2Label = "CO₂ 排放"
+co2Limit = "CO₂ 限制"
+costLimitDescription = "在太阳能模式下实现临时快速充电."
+modalTitle = "智能电网充电"
+none = "无"
+priceLabel = "电价"
+priceLimit = "限价"
+
+[header]
+about = "关于"
+blog = "博客"
+docs = " 文档"
+github = "GitHub"
+login = "车辆登录"
+needHelp = "需要帮助?"
+sessions = "充电期"
+settings = "设置"
+
+[help]
+discussionsButton = "GitHub 讨论"
+documentationButton = "文档"
+issueButton = "报告 bug"
+issueDescription = "发现奇怪或错误的行为?"
+modalTitle = "需要帮助?"
+primaryActions = "有些东西没有按照它应该的方式工作 ?这些都是获得帮助的好地方."
+restartButton = "重启"
+restartDescription = "试过关机再开?"
+secondaryActions = "仍然无法解决您的问题?这里有一些更严厉的选择."
+
+[help.restart]
+cancel = "取消"
+confirm = "是, 重启!"
+description = "在正常情况下,没有必要重新启动.如果您需要定期重新启动 evcc,请考虑提交错误."
+disclaimer = "注意: evcc 将终止并依靠操作系统重新启动服务."
+modalTitle = "你确定要重启吗?"
+
+[main]
+vehicles = "停车中"
+
+[main.chargingPlan]
+activeLabel = "{time}"
+arrivalTab = "抵达"
+departureTab = "离开"
+modalTitle = "充电计划"
+none = "无"
+title = "计划"
+titleMinSoc = "平均充电"
+titleTargetCharge = "离开"
+
+[main.energyflow]
+battery = "电池"
+batteryCharge = "电池充电"
+batteryDischarge = "电池放电"
+batteryTooltip = "{energy} / {total} ({soc})"
+gridImport = "电网使用"
+homePower = "消耗"
+loadpoints = "充电器| 充电器 | {count} 充电器"
+noEnergy = "无仪表数据"
+pvExport = "电网导出"
+pvProduction = "生产"
+selfConsumption = "内耗"
+
+[main.loadpoint]
+avgPrice = "⌀ 价格"
+charged = "已充"
+co2 = "⌀ CO₂"
+duration = "时长"
+fallbackName = "充电积分"
+power = "电力"
+price = "Σ 价格"
+solar = "太阳能"
+
+[main.loadpointSettings]
+currents = "充电货币"
diff --git a/meter/sma.go b/meter/sma.go
index dd9e806b..3a8e35ad 100644
--- a/meter/sma.go
+++ b/meter/sma.go
@@ -125,12 +125,12 @@ func (sm *SMA) Currents() (float64, float64, float64, error) {
 		}
 	}
 
-	var currents [3]float64
+	var res [3]float64
 	for i, id := range []sunny.ValueID{sunny.CurrentL1, sunny.CurrentL2, sunny.CurrentL3} {
-		currents[i] = util.SignFromPower(sma.AsFloat(values[id]), powers[i])
+		res[i] = util.SignFromPower(sma.AsFloat(values[id]), powers[i])
 	}
 
-	return currents[0], currents[1], currents[2], err
+	return res[0], res[1], res[2], err
 }
 
 var _ api.PhaseVoltages = (*SMA)(nil)
@@ -139,12 +139,12 @@ var _ api.PhaseVoltages = (*SMA)(nil)
 func (sm *SMA) Voltages() (float64, float64, float64, error) {
 	values, err := sm.device.Values()
 
-	var voltages [3]float64
+	var res [3]float64
 	for i, id := range []sunny.ValueID{sunny.VoltageL1, sunny.VoltageL2, sunny.VoltageL3} {
-		voltages[i] = sma.AsFloat(values[id])
+		res[i] = sma.AsFloat(values[id])
 	}
 
-	return voltages[0], voltages[1], voltages[2], err
+	return res[0], res[1], res[2], err
 }
 
 var _ api.PhasePowers = (*SMA)(nil)
@@ -153,15 +153,15 @@ var _ api.PhasePowers = (*SMA)(nil)
 func (sm *SMA) Powers() (float64, float64, float64, error) {
 	values, err := sm.device.Values()
 
-	var powers [3]float64
+	var res [3]float64
 	for i, id := range []sunny.ValueID{sunny.ActivePowerPlusL1, sunny.ActivePowerPlusL2, sunny.ActivePowerPlusL3} {
-		powers[i] = sma.AsFloat(values[id])
+		res[i] = sma.AsFloat(values[id])
 	}
 	for i, id := range []sunny.ValueID{sunny.ActivePowerMinusL1, sunny.ActivePowerMinusL2, sunny.ActivePowerMinusL3} {
-		powers[i] -= sma.AsFloat(values[id])
+		res[i] -= sma.AsFloat(values[id])
 	}
 
-	return powers[0], powers[1], powers[2], err
+	return res[0], res[1], res[2], err
 }
 
 // soc implements the api.Battery interface
diff --git a/package-lock.json b/package-lock.json
index 25127f0d..4ac5d965 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1584,9 +1584,9 @@
       }
     },
     "node_modules/@babel/traverse": {
-      "version": "7.23.0",
-      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.0.tgz",
-      "integrity": "sha512-t/QaEvyIoIkwzpiZ7aoSKK8kObQYeF7T2v+dazAYCb8SXtp58zEVkWW7zAnju8FNKNdr4ScAOEDmMItbyOmEYw==",
+      "version": "7.23.2",
+      "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz",
+      "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==",
       "dependencies": {
         "@babel/code-frame": "^7.22.13",
         "@babel/generator": "^7.23.0",
diff --git a/tariff/awattar.go b/tariff/awattar.go
index 3949575e..5b94f43c 100644
--- a/tariff/awattar.go
+++ b/tariff/awattar.go
@@ -99,5 +99,5 @@ func (t *Awattar) Rates() (api.Rates, error) {
 
 // Type implements the api.Tariff interface
 func (t *Awattar) Type() api.TariffType {
-	return api.TariffTypePriceDynamic
+	return api.TariffTypePriceForecast
 }
diff --git a/tariff/elering.go b/tariff/elering.go
index 99105168..3dd1a766 100644
--- a/tariff/elering.go
+++ b/tariff/elering.go
@@ -112,5 +112,5 @@ func (t *Elering) Rates() (api.Rates, error) {
 
 // Type implements the api.Tariff interface
 func (t *Elering) Type() api.TariffType {
-	return api.TariffTypePriceDynamic
+	return api.TariffTypePriceForecast
 }
diff --git a/tariff/energinet.go b/tariff/energinet.go
index d3ddaaf3..c5ec71c1 100644
--- a/tariff/energinet.go
+++ b/tariff/energinet.go
@@ -109,5 +109,5 @@ func (t *Energinet) Rates() (api.Rates, error) {
 
 // Type implements the api.Tariff interface
 func (t *Energinet) Type() api.TariffType {
-	return api.TariffTypePriceDynamic
+	return api.TariffTypePriceForecast
 }
diff --git a/tariff/fixed.go b/tariff/fixed.go
index 4c97328e..0c1a41b4 100644
--- a/tariff/fixed.go
+++ b/tariff/fixed.go
@@ -3,12 +3,13 @@ package tariff
 import (
 	"fmt"
 	"sort"
+	"time"
 
 	"github.com/benbjohnson/clock"
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/tariff/fixed"
 	"github.com/evcc-io/evcc/util"
-	"github.com/golang-module/carbon/v2"
+	"github.com/jinzhu/now"
 )
 
 type Fixed struct {
@@ -83,20 +84,20 @@ func NewFixedFromConfig(other map[string]interface{}) (api.Tariff, error) {
 func (t *Fixed) Rates() (api.Rates, error) {
 	var res api.Rates
 
-	start := carbon.CreateFromStdTime(t.clock.Now().Local()).StartOfDay()
+	start := now.With(t.clock.Now().Local()).BeginningOfDay()
 	for i := 0; i < 7; i++ {
-		dow := fixed.Day((start.DayOfWeek() + i) % 7)
+		dow := fixed.Day((int(start.Weekday()) + i) % 7)
 
 		zones := t.zones.ForDay(dow)
 		if len(zones) == 0 {
 			return nil, fmt.Errorf("no zones for weekday %d", dow)
 		}
 
-		dayStart := start.AddDays(i)
+		dayStart := start.AddDate(0, 0, i)
 		markers := zones.TimeTableMarkers()
 
 		for i, m := range markers {
-			ts := dayStart.AddMinutes(m.Minutes())
+			ts := dayStart.Add(time.Minute * time.Duration(m.Minutes()))
 
 			var zone *fixed.Zone
 			for j := len(zones) - 1; j >= 0; j-- {
@@ -111,15 +112,15 @@ func (t *Fixed) Rates() (api.Rates, error) {
 			}
 
 			// end rate at end of day or next marker
-			end := dayStart.AddDay()
+			end := dayStart.AddDate(0, 0, 1)
 			if i+1 < len(markers) {
-				end = dayStart.AddMinutes(markers[i+1].Minutes())
+				end = dayStart.Add(time.Minute * time.Duration(markers[i+1].Minutes()))
 			}
 
 			rate := api.Rate{
 				Price: zone.Price,
-				Start: ts.ToStdTime(),
-				End:   end.ToStdTime(),
+				Start: ts,
+				End:   end,
 			}
 
 			res = append(res, rate)
@@ -132,7 +133,7 @@ func (t *Fixed) Rates() (api.Rates, error) {
 // Type implements the api.Tariff interface
 func (t *Fixed) Type() api.TariffType {
 	if t.dynamic {
-		return api.TariffTypePriceDynamic
+		return api.TariffTypePriceForecast
 	}
 	return api.TariffTypePriceStatic
 }
diff --git a/tariff/octopus.go b/tariff/octopus.go
index 14867ef1..33ff2278 100644
--- a/tariff/octopus.go
+++ b/tariff/octopus.go
@@ -110,5 +110,5 @@ func (t *Octopus) Rates() (api.Rates, error) {
 
 // Type implements the api.Tariff interface
 func (t *Octopus) Type() api.TariffType {
-	return api.TariffTypePriceDynamic
+	return api.TariffTypePriceForecast
 }
diff --git a/tariff/tariff.go b/tariff/tariff.go
new file mode 100644
index 00000000..ba759c4d
--- /dev/null
+++ b/tariff/tariff.go
@@ -0,0 +1,68 @@
+package tariff
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/evcc-io/evcc/api"
+	"github.com/evcc-io/evcc/provider"
+	"github.com/evcc-io/evcc/util"
+	"github.com/jinzhu/now"
+)
+
+type Tariff struct {
+	*embed
+	priceG func() (float64, error)
+}
+
+var _ api.Tariff = (*Tariff)(nil)
+
+func init() {
+	registry.Add(api.Custom, NewConfigurableFromConfig)
+}
+
+func NewConfigurableFromConfig(other map[string]interface{}) (api.Tariff, error) {
+	var cc struct {
+		embed `mapstructure:",squash"`
+		Price provider.Config
+	}
+
+	if err := util.DecodeOther(other, &cc); err != nil {
+		return nil, err
+	}
+
+	priceG, err := provider.NewFloatGetterFromConfig(cc.Price)
+	if err != nil {
+		return nil, fmt.Errorf("price: %w", err)
+	}
+
+	t := &Tariff{
+		embed:  &cc.embed,
+		priceG: priceG,
+	}
+
+	return t, nil
+}
+
+// Rates implements the api.Tariff interface
+func (t *Tariff) Rates() (api.Rates, error) {
+	price, err := t.priceG()
+	if err != nil {
+		return nil, err
+	}
+
+	res := api.Rates{
+		{
+			Start: now.BeginningOfHour(),
+			End:   now.BeginningOfHour().Add(48 * time.Hour),
+			Price: t.totalPrice(price),
+		},
+	}
+
+	return res, nil
+}
+
+// Type implements the api.Tariff interface
+func (t *Tariff) Type() api.TariffType {
+	return api.TariffTypePriceDynamic
+}
diff --git a/tariff/tibber.go b/tariff/tibber.go
index e54c4f3a..b1978b0e 100644
--- a/tariff/tibber.go
+++ b/tariff/tibber.go
@@ -140,5 +140,5 @@ func (t *Tibber) Rates() (api.Rates, error) {
 
 // Type implements the api.Tariff interface
 func (t *Tibber) Type() api.TariffType {
-	return api.TariffTypePriceDynamic
+	return api.TariffTypePriceForecast
 }
diff --git a/templates/definition/charger/go-e-v3.yaml b/templates/definition/charger/go-e-v3.yaml
index de79a1b9..10c8de97 100644
--- a/templates/definition/charger/go-e-v3.yaml
+++ b/templates/definition/charger/go-e-v3.yaml
@@ -1,9 +1,9 @@
 template: go-e-v3
 covers: ["go-e-gemini"]
 products:
-  - brand: go-eCharger
+  - brand: go-e
     description:
-      generic: Gemini, HOME+, HOMEfix (V3)
+      generic: Charger Gemini, HOME+, HOMEfix (V3)
 capabilities: ["1p3p", "rfid"]
 requirements:
   description:
@@ -11,10 +11,14 @@ requirements:
       Benötigt mindestens Firmware 052.1 oder neuer.
 
       Für 1P/3P-Phasenumschaltung muss die HTTP API v2 im Charger aktiviert sein.
+
+      In der Go-E App (Menüpunkt "Auto") sollte die Option "Ausstecken simulieren" aktiviert sein.
     en: |
       Requires firmware 052.1 or later.
 
       For 1P/3P-Phase switching the HTTP API v2 in the charger setup needs to be activated.
+
+      The “simulate unplugging” option should be activated in the Go-E app ("Car" menu item).
   uri: https://docs.evcc.io/docs/devices/chargers#go-echarger
   evcc: ["sponsorship"]
 params:
diff --git a/templates/definition/charger/go-e.yaml b/templates/definition/charger/go-e.yaml
index ef244d47..87925653 100644
--- a/templates/definition/charger/go-e.yaml
+++ b/templates/definition/charger/go-e.yaml
@@ -1,8 +1,8 @@
 template: go-e
 products:
-  - brand: go-eCharger
+  - brand: go-e
     description:
-      generic: HOME+, HOMEfix, Pro
+      generic: Charger HOME+, HOMEfix, Pro
 capabilities: ["rfid"]
 requirements:
   description:
diff --git a/templates/definition/charger/pulsares.yaml b/templates/definition/charger/pulsares.yaml
new file mode 100644
index 00000000..36d41535
--- /dev/null
+++ b/templates/definition/charger/pulsares.yaml
@@ -0,0 +1,13 @@
+template: pulsares
+products:
+  - brand: Pulsares
+    description:
+      generic: SimpleBox
+params:
+  - name: modbus
+    choice: ["rs485"]
+    baudrate: 9600
+    comset: 8N1
+render: |
+  type: pulsares
+  {{- include "modbus" . }}
diff --git a/templates/definition/meter/go-e-controller.yaml b/templates/definition/meter/go-e-controller.yaml
new file mode 100644
index 00000000..5eb1efb3
--- /dev/null
+++ b/templates/definition/meter/go-e-controller.yaml
@@ -0,0 +1,31 @@
+template: go-e-controller
+products:
+  - brand: go-e
+    description:
+      generic: Controller
+params:
+  - name: usage
+    choice: ["grid", "pv"]
+  - name: host
+render: |
+  type: custom
+  {{- if eq .usage "grid" }}
+  power:
+    source: http
+    uri: http://{{ .host }}/api/status?filter=ccp
+    jq: .ccp[1]
+  energy:
+    source: http
+    uri: http://{{ .host }}/api/status?filter=cec
+    jq: .cec[1][0]/1000
+  {{- end }}
+  {{- if eq .usage "pv" }}
+  power:
+    source: http
+    uri: http://{{ .host }}/api/status?filter=ccp
+    jq: .ccp[4]
+  energy:
+    source: http
+    uri: http://{{ .host }}/api/status?filter=cec
+    jq: .cec[4][0]/1000
+  {{- end }}
diff --git a/templates/definition/meter/smartfox.yaml b/templates/definition/meter/smartfox.yaml
new file mode 100644
index 00000000..287dcdfb
--- /dev/null
+++ b/templates/definition/meter/smartfox.yaml
@@ -0,0 +1,80 @@
+template: smartfox
+products:
+  - brand: Smartfox
+    description:
+      generic: Smartfox Reg/Reg extended/Pro/Pro 2/Pro Light/Pro Light 2/Light
+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 }}/all
+  {{- end }}
+  type: custom
+  {{- if not .usage }}
+  power:
+    source: http
+    uri: {{ include "uri" . }}
+    jq: .power_sf
+  {{- end }}
+  {{- if eq .usage "grid" }}
+  power:
+    source: http
+    uri: {{ include "uri" . }}
+    jq: .power_io
+  energy:
+    source: http
+    uri: {{ include "uri" . }}
+    jq: .energy_in
+    scale: 0.001  
+  voltages:
+  - source: http
+    uri: {{ include "uri" . }}
+    jq: .voltages[0]
+  - source: http
+    uri: {{ include "uri" . }}
+    jq: .voltages[1]
+  - source: http
+    uri: {{ include "uri" . }}
+    jq: .voltages[2]
+  currents:
+  - source: http
+    uri: {{ include "uri" . }}
+    jq: .currents[0]
+  - source: http
+    uri: {{ include "uri" . }}
+    jq: .currents[1]
+  - source: http
+    uri: {{ include "uri" . }}
+    jq: .currents[2]
+  powers:
+  - source: http
+    uri: {{ include "uri" . }}
+    jq: .powers[0]
+  - source: http
+    uri: {{ include "uri" . }}
+    jq: .powers[1]
+  - source: http
+    uri: {{ include "uri" . }}
+    jq: .powers[2]
+  {{- end }}
+  {{- if eq .usage "pv" }}
+  power:
+    source: http
+    uri: {{ include "uri" . }}
+    jq: .PvPower[0]
+  energy:
+    source: http
+    uri: {{ include "uri" . }}
+    jq: .PvEnergy[0]
+    scale: 0.001
+  {{- end }}
diff --git a/templates/evcc.io/brands.json b/templates/evcc.io/brands.json
index bbcfb9d1..c5c2f728 100644
--- a/templates/evcc.io/brands.json
+++ b/templates/evcc.io/brands.json
@@ -26,7 +26,7 @@
   "EVBox",
   "Fronius",
   "Garo",
-  "go-eCharger",
+  "go-e",
   "HardyBarth",
   "Heidelberg",
   "Hesotec",
@@ -48,6 +48,7 @@
   "Phoenix Contact",
   "Porsche",
   "Pracht",
+  "Pulsares",
   "Schneider",
   "Schrack",
   "SENEC",
@@ -97,6 +98,7 @@
   "FENECON",
   "FoxESS",
   "Fronius",
+  "go-e",
   "GoodWe",
   "Growatt",
   "Homematic IP",
@@ -122,6 +124,7 @@
   "Shelly",
   "Siemens",
   "SMA",
+  "Smartfox",
   "SofarSolar",
   "Solaranzeige",
   "SolarEdge",
@@ -153,6 +156,7 @@
   "FoxESS",
   "Fronius",
   "Ginlong",
+  "go-e",
   "GoodWe",
   "Growatt",
   "Homematic IP",
@@ -173,6 +177,7 @@
   "Senergy",
   "Shelly",
   "SMA",
+  "Smartfox",
   "SofarSolar",
   "Solaranzeige",
   "SolarEdge",
diff --git a/util/templates/generate/main.go b/util/templates/generate/main.go
index 99497038..15934bc4 100644
--- a/util/templates/generate/main.go
+++ b/util/templates/generate/main.go
@@ -6,7 +6,6 @@ import (
 	"os"
 	"path"
 	"slices"
-	"sort"
 	"strings"
 
 	"github.com/evcc-io/evcc/util/templates"
@@ -88,8 +87,8 @@ func clearDir(dir string) error {
 }
 
 func sorted(keys []string) []string {
-	sort.Slice(keys, func(i, j int) bool {
-		return strings.ToLower(keys[i]) < strings.ToLower(keys[j])
+	slices.SortFunc(keys, func(i, j string) int {
+		return strings.Compare(strings.ToLower(i), strings.ToLower(j))
 	})
 	return slices.Compact(keys)
 }
diff --git a/vehicle/ford.go b/vehicle/ford.go
index b5e4b75b..177448fa 100644
--- a/vehicle/ford.go
+++ b/vehicle/ford.go
@@ -7,6 +7,7 @@ import (
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/vehicle/ford"
+	"github.com/evcc-io/evcc/vehicle/ford/autonomic"
 )
 
 // https://github.com/d4v3y0rk/ffpass-module
@@ -28,11 +29,9 @@ func NewFordFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 	cc := struct {
 		embed               `mapstructure:",squash"`
 		User, Password, VIN string
-		Expiry              time.Duration
 		Cache               time.Duration
 	}{
-		Expiry: expiry,
-		Cache:  interval,
+		Cache: interval,
 	}
 
 	if err := util.DecodeOther(other, &cc); err != nil {
@@ -58,9 +57,19 @@ func NewFordFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 	api := ford.NewAPI(log, identity)
 
 	cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles)
+	if err != nil {
+		return nil, err
+	}
+
+	autoIdentity, err := autonomic.NewIdentity(log, identity)
+	if err != nil {
+		return nil, err
+	}
+
+	autoApi := autonomic.NewAPI(log, autoIdentity)
 
 	if err == nil {
-		v.Provider = ford.NewProvider(api, cc.VIN, cc.Expiry, cc.Cache)
+		v.Provider = ford.NewProvider(autoApi, cc.VIN, cc.Cache)
 	}
 
 	return v, err
diff --git a/vehicle/ford/api.go b/vehicle/ford/api.go
index 37d98027..37476545 100644
--- a/vehicle/ford/api.go
+++ b/vehicle/ford/api.go
@@ -27,19 +27,22 @@ func NewAPI(log *util.Logger, ts oauth2.TokenSource) *API {
 	v.Client.Transport = &transport.Decorator{
 		Decorator: func(req *http.Request) error {
 			token, err := ts.Token()
-			if err == nil {
-				for k, v := range map[string]string{
-					"Content-type":   request.JSONContent,
-					"User-Agent":     "FordPass/5 CFNetwork/1333.0.4 Darwin/21.5.0",
-					"locale":         "de-DE",
-					"Application-Id": ApplicationID,
-					"Auth-Token":     token.AccessToken,
-					"CountryCode":    "DEU",
-				} {
-					req.Header.Set(k, v)
-				}
+			if err != nil {
+				return err
 			}
-			return err
+
+			for k, v := range map[string]string{
+				"Content-type":   request.JSONContent,
+				"User-Agent":     "FordPass/5 CFNetwork/1333.0.4 Darwin/21.5.0",
+				"locale":         "de-DE",
+				"Application-Id": ApplicationID,
+				"Auth-Token":     token.AccessToken,
+				"CountryCode":    "DEU",
+			} {
+				req.Header.Set(k, v)
+			}
+
+			return nil
 		},
 		Base: v.Client.Transport,
 	}
diff --git a/vehicle/ford/autonomic/api.go b/vehicle/ford/autonomic/api.go
new file mode 100644
index 00000000..7cc9fff1
--- /dev/null
+++ b/vehicle/ford/autonomic/api.go
@@ -0,0 +1,87 @@
+package autonomic
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/request"
+	"github.com/evcc-io/evcc/util/transport"
+	"golang.org/x/oauth2"
+)
+
+const ApiURI = "https://api.autonomic.ai/v1beta/telemetry/sources/fordpass"
+
+var (
+	ApplicationID = "667D773E-1BDC-4139-8AD0-2B16474E8DC7"
+	Dyna          = "MT_3_30_2352378557_3-0_" + "uuidv4()" + "_0_789_87"
+)
+
+// API is the Ford api client
+type API struct {
+	*request.Helper
+}
+
+// NewAPI creates a new api client
+func NewAPI(log *util.Logger, ts oauth2.TokenSource) *API {
+	v := &API{
+		Helper: request.NewHelper(log),
+	}
+
+	v.Client.Transport = &transport.Decorator{
+		Decorator: func(req *http.Request) error {
+			token, err := ts.Token()
+			if err != nil {
+				return err
+			}
+
+			for k, v := range map[string]string{
+				"Content-type":   request.JSONContent,
+				"Application-Id": ApplicationID,
+				"x-dynatrace":    Dyna,
+				"Authorization":  "Bearer " + token.AccessToken,
+			} {
+				req.Header.Set(k, v)
+			}
+
+			return nil
+		},
+		Base: v.Client.Transport,
+	}
+
+	return v
+}
+
+// Status retrieves a metrics result using :query
+func (v *API) Status(vin string) (MetricsResponse, error) {
+	var res MetricsResponse
+
+	uri := fmt.Sprintf("%s/vehicles/%s:query", ApiURI, vin)
+	req, err := request.New(http.MethodPost, uri, strings.NewReader("{}"), request.JSONEncoding)
+	if err == nil {
+		err = v.DoJSON(req, &res)
+	}
+
+	return res, err
+}
+
+// Refresh executes the refresh command
+func (v *API) Refresh(vin string) (any, error) {
+	var res map[string]any
+
+	data := map[string]any{
+		"properties": struct{}{},
+		"tags":       struct{}{},
+		"type":       "statusRefresh",
+		"wakeUp":     true,
+	}
+
+	uri := fmt.Sprintf("%s/vehicles/%s/command", ApiURI, vin)
+	req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding)
+	if err == nil {
+		err = v.DoJSON(req, &res)
+	}
+
+	return res, err
+}
diff --git a/vehicle/ford/autonomic/identity.go b/vehicle/ford/autonomic/identity.go
new file mode 100644
index 00000000..2f062b37
--- /dev/null
+++ b/vehicle/ford/autonomic/identity.go
@@ -0,0 +1,78 @@
+package autonomic
+
+import (
+	"net/http"
+	"net/url"
+	"strings"
+
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/oauth"
+	"github.com/evcc-io/evcc/util/request"
+	"golang.org/x/oauth2"
+)
+
+// https://accounts.autonomic.ai/v1/auth/oidc/.well-known/openid-configuration/
+var OAuth2Config = &oauth2.Config{
+	Endpoint: oauth2.Endpoint{
+		AuthURL:  "https://accounts.autonomic.ai/v1/auth/oidc/authorize",
+		TokenURL: "https://accounts.autonomic.ai/v1/auth/oidc/token",
+	},
+	Scopes: []string{"openid"},
+}
+
+type Identity struct {
+	*request.Helper
+	ts oauth2.TokenSource
+	oauth2.TokenSource
+}
+
+// NewIdentity creates autonomic token source
+func NewIdentity(log *util.Logger, ts oauth2.TokenSource) (*Identity, error) {
+	v := &Identity{
+		Helper: request.NewHelper(log),
+		ts:     ts,
+	}
+
+	token, err := ts.Token()
+	if err != nil {
+		return nil, err
+	}
+
+	auto, err := v.exchange(token)
+	if err != nil {
+		return nil, err
+	}
+
+	v.TokenSource = oauth.RefreshTokenSource(auto, v)
+
+	return v, nil
+}
+
+// exchange authenticates with username/password to get new aws credentials
+func (v *Identity) exchange(token *oauth2.Token) (*oauth2.Token, error) {
+	data := url.Values{
+		"subject_token":      {token.AccessToken},
+		"subject_issuer":     {"fordpass"},
+		"client_id":          {"fordpass-prod"},
+		"grant_type":         {"urn:ietf:params:oauth:grant-type:token-exchange"},
+		"subject_token_type": {"urn:ietf:params:oauth:token-type:jwt"},
+	}
+
+	var auto *oauth.Token
+	req, err := request.New(http.MethodPost, OAuth2Config.Endpoint.TokenURL, strings.NewReader(data.Encode()), request.URLEncoding)
+	if err == nil {
+		err = v.DoJSON(req, &auto)
+	}
+
+	return (*oauth2.Token)(auto), err
+}
+
+// RefreshToken implements oauth.TokenRefresher
+func (v *Identity) RefreshToken(_ *oauth2.Token) (*oauth2.Token, error) {
+	token, err := v.ts.Token()
+	if err != nil {
+		return nil, err
+	}
+
+	return v.exchange(token)
+}
diff --git a/vehicle/ford/autonomic/types.go b/vehicle/ford/autonomic/types.go
new file mode 100644
index 00000000..db69030d
--- /dev/null
+++ b/vehicle/ford/autonomic/types.go
@@ -0,0 +1,32 @@
+package autonomic
+
+import "time"
+
+type IntValue struct {
+	UpdateTime time.Time
+	Value      int
+}
+type FloatValue struct {
+	UpdateTime time.Time
+	Value      float64
+}
+type StringValue struct {
+	UpdateTime time.Time
+	Value      string
+}
+
+type MetricsResponse struct {
+	Metrics struct {
+		Position struct {
+			Value struct {
+				Location struct {
+					Lat, Lon float64
+				}
+			}
+		}
+		Odometer                FloatValue
+		XevPlugChargerStatus    StringValue
+		XevBatteryRange         FloatValue
+		XevBatteryStateOfCharge FloatValue
+	}
+}
diff --git a/vehicle/ford/identity.go b/vehicle/ford/identity.go
index 443f62fa..a321e5d0 100644
--- a/vehicle/ford/identity.go
+++ b/vehicle/ford/identity.go
@@ -41,7 +41,7 @@ type Identity struct {
 	oauth2.TokenSource
 }
 
-// NewIdentity creates Fiat identity
+// NewIdentity creates Ford identity
 func NewIdentity(log *util.Logger, user, password string) *Identity {
 	return &Identity{
 		Helper:   request.NewHelper(log),
diff --git a/vehicle/ford/provider.go b/vehicle/ford/provider.go
index 1df8ccb1..7718bf91 100644
--- a/vehicle/ford/provider.go
+++ b/vehicle/ford/provider.go
@@ -1,85 +1,40 @@
 package ford
 
 import (
+	"fmt"
 	"time"
 
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/provider"
+	"github.com/evcc-io/evcc/vehicle/ford/autonomic"
 )
 
-const refreshTimeout = time.Minute
-
 type Provider struct {
-	statusG     func() (StatusResponse, error)
-	expiry      time.Duration
-	refreshTime time.Time
-	refreshId   string
-	wakeup      func() error
+	statusG  func() (autonomic.MetricsResponse, error)
+	refreshG func() error
 }
 
-func NewProvider(api *API, vin string, expiry, cache time.Duration) *Provider {
+func NewProvider(api *autonomic.API, vin string, cache time.Duration) *Provider {
 	impl := &Provider{
-		expiry: expiry,
+		statusG: provider.Cached(func() (autonomic.MetricsResponse, error) {
+			return api.Status(vin)
+		}, cache),
+		refreshG: func() error {
+			_, err := api.Refresh(vin)
+			return err
+		},
 	}
 
-	impl.statusG = provider.Cached(func() (StatusResponse, error) {
-		return impl.status(
-			func() (StatusResponse, error) { return api.Status(vin) },
-			func(id string) (StatusResponse, error) { return api.RefreshResult(vin, id) },
-			func() (string, error) { return api.RefreshRequest(vin) },
-		)
-	}, cache)
-
-	impl.wakeup = func() error { return api.WakeUp(vin) }
-
 	return impl
 }
 
-func (v *Provider) status(
-	statusG func() (StatusResponse, error),
-	refreshG func(id string) (StatusResponse, error),
-	refreshRequest func() (string, error),
-) (StatusResponse, error) {
-	if v.refreshId != "" {
-		res, err := refreshG(v.refreshId)
-
-		// update successful and completed
-		if err == nil {
-			v.refreshId = ""
-			return res, nil
-		}
-
-		// update still in progress, keep retrying
-		if time.Since(v.refreshTime) < refreshTimeout {
-			return res, api.ErrMustRetry
-		}
-
-		// give up
-		v.refreshId = ""
-		return res, api.ErrTimeout
-	}
-
-	res, err := statusG()
-
-	if err == nil {
-		if time.Since(res.VehicleStatus.LastRefresh.Time) > v.expiry {
-			if v.refreshId, err = refreshRequest(); err == nil {
-				v.refreshTime = time.Now()
-				err = api.ErrMustRetry
-			}
-		}
-	}
-
-	return res, err
-}
-
 var _ api.Battery = (*Provider)(nil)
 
 // Soc implements the api.Battery interface
 func (v *Provider) Soc() (float64, error) {
 	res, err := v.statusG()
 	if err == nil {
-		return res.VehicleStatus.BatteryFillLevel.Value, nil
+		return res.Metrics.XevBatteryStateOfCharge.Value, nil
 	}
 
 	return 0, err
@@ -91,7 +46,7 @@ var _ api.VehicleRange = (*Provider)(nil)
 func (v *Provider) Range() (int64, error) {
 	res, err := v.statusG()
 	if err == nil {
-		return int64(res.VehicleStatus.ElVehDTE.Value), nil
+		return int64(res.Metrics.XevBatteryRange.Value), nil
 	}
 
 	return 0, err
@@ -101,15 +56,19 @@ var _ api.ChargeState = (*Provider)(nil)
 
 // Status implements the api.ChargeState interface
 func (v *Provider) Status() (api.ChargeStatus, error) {
-	status := api.StatusA // disconnected
+	status := api.StatusNone
 
 	res, err := v.statusG()
 	if err == nil {
-		if res.VehicleStatus.PlugStatus.Value == 1 {
+		switch res.Metrics.XevPlugChargerStatus.Value {
+		case "DISCONNECTED":
+			status = api.StatusA // disconnected
+		case "CONNECTED":
 			status = api.StatusB // connected, not charging
-		}
-		if res.VehicleStatus.ChargingStatus.Value == "ChargingAC" {
+		case "CHARGING", "CHARGINGAC":
 			status = api.StatusC // charging
+		default:
+			err = fmt.Errorf("unknown charge status: %s", res.Metrics.XevPlugChargerStatus.Value)
 		}
 	}
 
@@ -121,7 +80,7 @@ var _ api.VehicleOdometer = (*Provider)(nil)
 // Odometer implements the api.VehicleOdometer interface
 func (v *Provider) Odometer() (float64, error) {
 	res, err := v.statusG()
-	return res.VehicleStatus.Odometer.Value, err
+	return res.Metrics.Odometer.Value, err
 }
 
 var _ api.VehiclePosition = (*Provider)(nil)
@@ -129,20 +88,13 @@ var _ api.VehiclePosition = (*Provider)(nil)
 // Position implements the api.VehiclePosition interface
 func (v *Provider) Position() (float64, float64, error) {
 	res, err := v.statusG()
-	return res.VehicleStatus.Gps.Latitude, res.VehicleStatus.Gps.Longitude, err
-}
-
-var _ api.VehicleFinishTimer = (*Provider)(nil)
-
-// FinishTime implements the api.VehicleFinishTimer interface
-func (v *Provider) FinishTime() (time.Time, error) {
-	res, err := v.statusG()
-	return res.VehicleStatus.ChargeEndTime.Value.Time, err
+	loc := res.Metrics.Position.Value.Location
+	return loc.Lat, loc.Lon, err
 }
 
 var _ api.Resurrector = (*Provider)(nil)
 
 // WakeUp implements the api.Resurrector interface
 func (v *Provider) WakeUp() error {
-	return v.wakeup()
+	return v.refreshG()
 }


Test patch:
diff --git a/assets/js/components/VehicleStatus.test.js b/assets/js/components/VehicleStatus.test.js
index 7612c3b9..c016bce7 100644
--- a/assets/js/components/VehicleStatus.test.js
+++ b/assets/js/components/VehicleStatus.test.js
@@ -41,14 +41,23 @@ describe("min charge", () => {
   });
 });
 
-describe("target charge", () => {
+describe("plan", () => {
   const targetTime = "2020-03-16T06:00:00Z";
   const planProjectedStart = "2020-03-16T02:00:00Z";
-  test("active if target time is set and status is charging", () => {
-    expectStatus({ targetTime, charging: true, connected: true }, "targetChargeActive");
+  test("charging if target time is set, status is charging but planned slot is not active", () => {
+    expectStatus({ targetTime, charging: true, connected: true }, "charging");
+  });
+  test("active if target time is set, status is charging and planned slot is active", () => {
+    expectStatus(
+      { targetTime, planActive: true, charging: true, connected: true },
+      "targetChargeActive"
+    );
   });
   test("waiting for vehicle if a target time is set, the charger is enabled but not charging", () => {
-    expectStatus({ targetTime, enabled: true, connected: true }, "targetChargeWaitForVehicle");
+    expectStatus(
+      { targetTime, planActive: true, enabled: true, connected: true },
+      "targetChargeWaitForVehicle"
+    );
   });
   test("show projected start if not enabled yet", () => {
     expectStatus({ targetTime, planProjectedStart, connected: true }, "targetChargePlanned", {
@@ -207,8 +216,9 @@ describe("smart grid charging", () => {
         tariffCo2: 400,
         smartCostLimit: 500,
         smartCostType: "co2",
+        smartCostActive: true,
       },
-      "cleanEnergyCharging"
+      `cleanEnergyCharging:{"co2":"400 g","limit":"500 g"}`
     );
   });
   test("show cheap energy message", () => {
@@ -219,9 +229,10 @@ describe("smart grid charging", () => {
         charging: true,
         tariffGrid: 0.28,
         smartCostLimit: 0.29,
-        currency: "EUR",
+        currency: "CHF",
+        smartCostActive: true,
       },
-      "cheapEnergyCharging"
+      `cheapEnergyCharging:{"price":"28,0 rp","limit":"29,0 rp"}`
     );
   });
 });
diff --git a/charger/ocpp_test.go b/charger/ocpp_test.go
index f46b171c..8cd7c446 100644
--- a/charger/ocpp_test.go
+++ b/charger/ocpp_test.go
@@ -95,13 +95,14 @@ func (suite *ocppTestSuite) handleTrigger(cp ocpp16.ChargePoint, connectorId int
 func (suite *ocppTestSuite) TestConnect() {
 	// 1st charge point- remote
 	cp1 := suite.startChargePoint("test-1", 1)
-	suite.NoError(cp1.Start(ocppTestUrl))
-	suite.True(cp1.IsConnected())
+	suite.Require().NoError(cp1.Start(ocppTestUrl))
+	suite.Require().True(cp1.IsConnected())
 
 	// 1st charge point- local
 	c1, err := NewOCPP("test-1", 1, "", "", 0, false, false, ocppTestConnectTimeout, ocppTestTimeout, "A")
 	suite.Require().NoError(err)
 
+	// status and meter values
 	{
 		suite.clock.Add(ocppTestTimeout)
 		c1.conn.TestClock(suite.clock)
@@ -121,6 +122,35 @@ func (suite *ocppTestSuite) TestConnect() {
 		suite.Equal(1.2, f)
 	}
 
+	// takeover
+	{
+		expectedTxn := 99
+
+		_, err := cp1.StopTransaction(0, types.NewDateTime(suite.clock.Now()), expectedTxn)
+		suite.Require().Error(err)
+
+		_, err = cp1.MeterValues(1, []types.MeterValue{
+			{
+				Timestamp: types.NewDateTime(suite.clock.Now()),
+				SampledValue: []types.SampledValue{
+					{Measurand: types.MeasurandPowerActiveImport, Value: "1000"},
+				},
+			},
+		}, func(request *core.MeterValuesRequest) {
+			request.TransactionId = &expectedTxn
+		})
+		suite.Require().NoError(err)
+
+		conn1 := c1.Connector()
+		txnId, err := conn1.TransactionID()
+		suite.Require().NoError(err)
+		suite.Equal(expectedTxn, txnId)
+
+		res, err := cp1.StopTransaction(0, types.NewDateTime(suite.clock.Now()), expectedTxn)
+		suite.Require().NoError(err)
+		suite.Equal(res.IdTagInfo.Status, types.AuthorizationStatusAccepted)
+	}
+
 	// 2nd charge point - remote
 	cp2 := suite.startChargePoint("test-2", 1)
 	suite.Require().NoError(cp2.Start(ocppTestUrl))
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index bb977436..5e8d23d5 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -768,6 +768,7 @@ func TestMinSoc(t *testing.T) {
 
 	for _, tc := range tc {
 		lp := &Loadpoint{
+			log: util.NewLogger("foo"),
 			Soc: SocConfig{
 				min: tc.min,
 			},
diff --git a/tariff/fixed_test.go b/tariff/fixed_test.go
index 62c964eb..15656af2 100644
--- a/tariff/fixed_test.go
+++ b/tariff/fixed_test.go
@@ -2,11 +2,12 @@ package tariff
 
 import (
 	"testing"
+	"time"
 
 	"github.com/benbjohnson/clock"
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/tariff/fixed"
-	"github.com/golang-module/carbon/v2"
+	"github.com/jinzhu/now"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -20,13 +21,13 @@ func TestFixed(t *testing.T) {
 
 	var expect api.Rates
 	for dow := 0; dow < 7; dow++ {
-		dayStart := carbon.CreateFromStdTime(tf.clock.Now()).StartOfDay().AddDays(dow)
+		dayStart := now.With(tf.clock.Now()).BeginningOfDay().AddDate(0, 0, dow)
 
 		for hour := 0; hour < 24; hour++ {
 			expect = append(expect, api.Rate{
 				Price: 0.3,
-				Start: dayStart.AddHours(hour).ToStdTime(),
-				End:   dayStart.AddHours(hour + 1).ToStdTime(),
+				Start: dayStart.Add(time.Hour * time.Duration(hour)),
+				End:   dayStart.Add(time.Hour * time.Duration(hour+1)),
 			})
 		}
 	}
@@ -53,37 +54,37 @@ func TestFixedSplitZones(t *testing.T) {
 
 	var expect api.Rates
 	for i := 0; i < 7; i++ {
-		dayStart := carbon.CreateFromStdTime(tf.clock.Now()).StartOfDay().AddDays(i)
+		dayStart := now.With(tf.clock.Now()).BeginningOfDay().AddDate(0, 0, i)
 
 		// 00:00-05:00 0.1
 		for hour := 0; hour < 5; hour++ {
 			expect = append(expect, api.Rate{
 				Price: 0.1,
-				Start: dayStart.AddHours(hour).ToStdTime(),
-				End:   dayStart.AddHours(hour + 1).ToStdTime(),
+				Start: dayStart.Add(time.Hour * time.Duration(hour)),
+				End:   dayStart.Add(time.Hour * time.Duration(hour+1)),
 			})
 		}
 
 		// 05:00-05:30 0.1
 		expect = append(expect, api.Rate{
 			Price: 0.1,
-			Start: dayStart.AddHours(5).ToStdTime(),
-			End:   dayStart.AddHours(5).AddMinutes(30).ToStdTime(),
+			Start: dayStart.Add(5 * time.Hour),
+			End:   dayStart.Add(5*time.Hour + 30*time.Minute),
 		})
 
 		// 05:30-06:00 0.5
 		expect = append(expect, api.Rate{
 			Price: 0.5,
-			Start: dayStart.AddHours(5).AddMinutes(30).ToStdTime(),
-			End:   dayStart.AddHours(6).ToStdTime(),
+			Start: dayStart.Add(5*time.Hour + 30*time.Minute),
+			End:   dayStart.Add(6 * time.Hour),
 		})
 
 		// 06:00-21:00 0.5
 		for hour := 6; hour < 21; hour++ {
 			expect = append(expect, api.Rate{
 				Price: 0.5,
-				Start: dayStart.AddHours(hour).ToStdTime(),
-				End:   dayStart.AddHours(hour + 1).ToStdTime(),
+				Start: dayStart.Add(time.Hour * time.Duration(hour)),
+				End:   dayStart.Add(time.Hour * time.Duration(hour+1)),
 			})
 		}
 
@@ -91,8 +92,8 @@ func TestFixedSplitZones(t *testing.T) {
 		for hour := 21; hour < 24; hour++ {
 			expect = append(expect, api.Rate{
 				Price: 0.1,
-				Start: dayStart.AddHours(hour).ToStdTime(),
-				End:   dayStart.AddHours(hour + 1).ToStdTime(),
+				Start: dayStart.Add(time.Hour * time.Duration(hour)),
+				End:   dayStart.Add(time.Hour * time.Duration(hour+1)),
 			})
 		}
 	}