Repository: evcc-io/evcc
Pull request #17930 (2024-12-28 12:03:45)

chore: update schema
/cc @naltatis es scheint die Schemata würden nicht enforced (sonst hätte es egtl. vorher einen Fehler geben müssen). War das schon immer so?

naltatis: @andig puh, mit den Schemata hatte ich noch keinen Kontakt. 🤷


Patch:
diff --git a/assets/js/components/Config/Modbus.vue b/assets/js/components/Config/Modbus.vue
index 533536da..56a88841 100644
--- a/assets/js/components/Config/Modbus.vue
+++ b/assets/js/components/Config/Modbus.vue
@@ -116,9 +116,9 @@
 			<PropertyField
 				id="modbusBaudrate"
 				property="baudrate"
-				type="Int"
+				type="Choice"
 				class="me-2 w-50"
-				:valid-values="baudrateOptions"
+				:choice="baudrateOptions"
 				required
 				:model-value="baudrate || defaultBaudrate"
 				@change="$emit('update:baudrate', $event.target.value)"
@@ -128,9 +128,9 @@
 			<PropertyField
 				id="modbusComset"
 				property="comset"
-				type="String"
+				type="Choice"
 				class="me-2 w-50"
-				:valid-values="comsetOptions"
+				:choice="comsetOptions"
 				required
 				:model-value="comset || defaultComset || '8N1'"
 				@change="$emit('update:comset', $event.target.value)"
diff --git a/assets/js/components/Config/PropertyEntry.vue b/assets/js/components/Config/PropertyEntry.vue
index a1e5df70..a1fe5bef 100644
--- a/assets/js/components/Config/PropertyEntry.vue
+++ b/assets/js/components/Config/PropertyEntry.vue
@@ -14,7 +14,7 @@
 			:type="Type"
 			class="me-2"
 			:required="Required"
-			:validValues="ValidValues"
+			:choice="Choice"
 		/>
 	</FormRow>
 </template>
@@ -36,7 +36,7 @@ export default {
 		Example: String,
 		Type: String,
 		Mask: Boolean,
-		ValidValues: Array,
+		Choice: Array,
 		modelValue: [String, Number, Boolean, Object],
 	},
 	emits: ["update:modelValue"],
diff --git a/assets/js/components/Config/PropertyField.vue b/assets/js/components/Config/PropertyField.vue
index 77dd2ca6..ac3f1533 100644
--- a/assets/js/components/Config/PropertyField.vue
+++ b/assets/js/components/Config/PropertyField.vue
@@ -111,7 +111,7 @@ export default {
 		size: String,
 		scale: Number,
 		required: Boolean,
-		validValues: { type: Array, default: () => [] },
+		choice: { type: Array, default: () => [] },
 		modelValue: [String, Number, Boolean, Object],
 	},
 	emits: ["update:modelValue"],
@@ -171,15 +171,15 @@ export default {
 			return this.type === "List";
 		},
 		select() {
-			return this.validValues.length > 0;
+			return this.choice.length > 0;
 		},
 		selectOptions() {
 			// If the valid values are already in the correct format, return them
-			if (typeof this.validValues[0] === "object") {
-				return this.validValues;
+			if (typeof this.choice[0] === "object") {
+				return this.choice;
 			}
 
-			let values = [...this.validValues];
+			let values = [...this.choice];
 
 			if (this.icons && !this.required) {
 				values = ["", ...values];
diff --git a/assets/js/components/Config/VehicleModal.vue b/assets/js/components/Config/VehicleModal.vue
index 76282154..ec4d6947 100644
--- a/assets/js/components/Config/VehicleModal.vue
+++ b/assets/js/components/Config/VehicleModal.vue
@@ -96,9 +96,9 @@
 										<PropertyField
 											id="vehicleParamMode"
 											v-model="values.mode"
-											type="String"
+											type="Choice"
 											class="w-100"
-											:valid-values="[
+											:choice="[
 												{ key: 'off', name: $t('main.mode.off') },
 												{ key: 'pv', name: $t('main.mode.pv') },
 												{ key: 'minpv', name: $t('main.mode.minpv') },
@@ -177,10 +177,10 @@
 										<PropertyField
 											id="vehicleParamPriority"
 											v-model="values.priority"
-											type="Int"
+											type="Choice"
 											size="w-100"
 											class="me-2"
-											:valid-values="priorityOptions"
+											:choice="priorityOptions"
 											required
 										/>
 									</FormRow>
@@ -313,7 +313,7 @@ export default {
 			};
 		},
 		templateParams() {
-			return (this.template?.Params || [])
+			const params = (this.template?.Params || [])
 				.filter((p) => !CUSTOM_FIELDS.includes(p.Name))
 				.map((p) => {
 					if (p.Name === "title" || p.Name === "icon") {
@@ -322,6 +322,12 @@ export default {
 					}
 					return p;
 				});
+
+			// always start with title and icon field
+			const order = { title: -2, icon: -1 };
+			params.sort((a, b) => (order[a.Name] || 0) - (order[b.Name] || 0));
+
+			return params;
 		},
 		normalParams() {
 			return this.templateParams.filter((p) => !p.Advanced);
diff --git a/assets/js/components/Config/defaultYaml/tariffs.yaml b/assets/js/components/Config/defaultYaml/tariffs.yaml
index f7e9e913..49b44c59 100644
--- a/assets/js/components/Config/defaultYaml/tariffs.yaml
+++ b/assets/js/components/Config/defaultYaml/tariffs.yaml
@@ -9,5 +9,6 @@
 #  price: 0.08 # EUR/kWh
 
 #co2: # carbon intensity forecast data
-#  type: grünstromindex
+#  type: template
+#  template: grünstromindex
 #  zip: <zip>
diff --git a/assets/js/components/Energyflow/Energyflow.vue b/assets/js/components/Energyflow/Energyflow.vue
index 30f86aa6..76013265 100644
--- a/assets/js/components/Energyflow/Energyflow.vue
+++ b/assets/js/components/Energyflow/Energyflow.vue
@@ -103,7 +103,7 @@
 							data-testid="energyflow-entry-batterydischarge"
 							@details-clicked="openBatterySettingsModal"
 						>
-							<template v-if="batteryGridChargeActive" #subline>
+							<template v-if="batteryGridChargeLimitSet" #subline>
 								<div class="d-none d-md-block">&nbsp;</div>
 							</template>
 						</EnergyflowEntry>
@@ -177,13 +177,25 @@
 							detailsClickable
 							@details-clicked="openBatterySettingsModal"
 						>
-							<template v-if="batteryGridChargeActive" #subline>
+							<template v-if="batteryGridChargeLimitSet" #subline>
 								<button
 									type="button"
-									class="btn-reset d-flex justify-content-between"
+									class="btn-reset d-flex justify-content-between text-start pe-4"
 									@click.stop="openBatterySettingsModal"
 								>
-									{{ batteryGridChargeText }} (≤ {{ batteryGridChargeLimitFmt }})
+									<span v-if="batteryGridChargeActive">
+										{{ $t("main.energyflow.batteryGridChargeActive") }}
+										<span class="text-nowrap"
+											>(≤ <u>{{ batteryGridChargeLimitFmt }}</u
+											>)</span
+										>
+									</span>
+									<span v-else>
+										{{ $t("main.energyflow.batteryGridChargeLimit") }}
+										<span class="text-nowrap"
+											>≤ <u>{{ batteryGridChargeLimitFmt }}</u></span
+										>
+									</span>
 								</button>
 							</template>
 						</EnergyflowEntry>
@@ -341,19 +353,17 @@ export default {
 		pvPossible() {
 			return this.pvConfigured || this.gridConfigured;
 		},
-		batteryGridChargeText() {
-			return this.$t(
-				`main.energyflow.${this.co2Available ? "clean" : "cheap"}BatteryGridCharge`
-			);
-		},
 		batteryGridChargeNow() {
 			if (this.co2Available) {
 				return this.fmtCo2Short(this.tariffCo2);
 			}
 			return this.fmtPricePerKWh(this.tariffGrid, this.currency, true);
 		},
+		batteryGridChargeLimitSet() {
+			return this.batteryGridChargeLimit !== null;
+		},
 		batteryGridChargeLimitFmt() {
-			if (this.batteryGridChargeLimit === null) {
+			if (!this.batteryGridChargeLimitSet) {
 				return;
 			}
 			if (this.co2Available) {
diff --git a/assets/js/components/SelectGroup.vue b/assets/js/components/SelectGroup.vue
index e6ccadb0..a746dbb7 100644
--- a/assets/js/components/SelectGroup.vue
+++ b/assets/js/components/SelectGroup.vue
@@ -21,7 +21,7 @@ export default {
 	props: {
 		id: String,
 		options: Array,
-		modelValue: [Number, String],
+		modelValue: [Number, String, Boolean],
 		equalWidth: Boolean,
 		large: Boolean,
 		transparent: Boolean,
diff --git a/assets/js/components/Sessions/AvgCostGroupedChart.vue b/assets/js/components/Sessions/AvgCostGroupedChart.vue
index 75feaf48..3af675bf 100644
--- a/assets/js/components/Sessions/AvgCostGroupedChart.vue
+++ b/assets/js/components/Sessions/AvgCostGroupedChart.vue
@@ -92,8 +92,7 @@ export default {
 					...commonOptions.plugins,
 					tooltip: {
 						...commonOptions.plugins.tooltip,
-						intersect: false,
-						mode: "index",
+						axis: "r",
 						position: "topBottomCenter",
 						callbacks: {
 							title: () => null,
diff --git a/assets/js/components/Sessions/CostGroupedChart.vue b/assets/js/components/Sessions/CostGroupedChart.vue
index 3ace87e2..fc010d1a 100644
--- a/assets/js/components/Sessions/CostGroupedChart.vue
+++ b/assets/js/components/Sessions/CostGroupedChart.vue
@@ -86,9 +86,8 @@ export default {
 					...commonOptions.plugins,
 					tooltip: {
 						...commonOptions.plugins.tooltip,
-						mode: "index",
+						axis: "r",
 						position: "center",
-						intersect: false,
 						callbacks: {
 							label: (tooltipItem) => this.formatValue(tooltipItem.raw || 0),
 							labelColor: tooltipLabelColor(false),
diff --git a/assets/js/components/Sessions/CostHistoryChart.vue b/assets/js/components/Sessions/CostHistoryChart.vue
index 59dbe09f..d1df88d0 100644
--- a/assets/js/components/Sessions/CostHistoryChart.vue
+++ b/assets/js/components/Sessions/CostHistoryChart.vue
@@ -223,8 +223,7 @@ export default {
 					...commonOptions.plugins,
 					tooltip: {
 						...commonOptions.plugins.tooltip,
-						mode: "index",
-						intersect: false,
+						axis: "x",
 						positioner: (context) => {
 							const { chart, tooltipPosition } = context;
 							const { tooltip } = chart;
diff --git a/assets/js/components/Sessions/EnergyGroupedChart.vue b/assets/js/components/Sessions/EnergyGroupedChart.vue
index 0f49d25d..560e788e 100644
--- a/assets/js/components/Sessions/EnergyGroupedChart.vue
+++ b/assets/js/components/Sessions/EnergyGroupedChart.vue
@@ -102,9 +102,8 @@ export default {
 					...commonOptions.plugins,
 					tooltip: {
 						...commonOptions.plugins.tooltip,
-						mode: "index",
+						axis: "r",
 						position: "center",
-						intersect: false,
 						callbacks: {
 							label: (tooltipItem) => this.formatValue(tooltipItem.raw || 0),
 							labelColor: tooltipLabelColor(false),
diff --git a/assets/js/components/Sessions/EnergyHistoryChart.vue b/assets/js/components/Sessions/EnergyHistoryChart.vue
index 20814479..73c5a256 100644
--- a/assets/js/components/Sessions/EnergyHistoryChart.vue
+++ b/assets/js/components/Sessions/EnergyHistoryChart.vue
@@ -159,8 +159,7 @@ export default {
 					...commonOptions.plugins,
 					tooltip: {
 						...commonOptions.plugins.tooltip,
-						mode: "index",
-						intersect: false,
+						axis: "x",
 						positioner: (context) => {
 							const { chart, tooltipPosition } = context;
 							const { tooltip } = chart;
diff --git a/assets/js/components/Sessions/SolarGroupedChart.vue b/assets/js/components/Sessions/SolarGroupedChart.vue
index cc0894d6..4c374d04 100644
--- a/assets/js/components/Sessions/SolarGroupedChart.vue
+++ b/assets/js/components/Sessions/SolarGroupedChart.vue
@@ -92,8 +92,7 @@ export default {
 					...commonOptions.plugins,
 					tooltip: {
 						...commonOptions.plugins.tooltip,
-						intersect: false,
-						mode: "index",
+						axis: "r",
 						position: "topBottomCenter",
 						callbacks: {
 							title: () => null,
diff --git a/assets/js/components/Sessions/SolarYearChart.vue b/assets/js/components/Sessions/SolarYearChart.vue
index aac5745a..1d60d4bc 100644
--- a/assets/js/components/Sessions/SolarYearChart.vue
+++ b/assets/js/components/Sessions/SolarYearChart.vue
@@ -147,8 +147,7 @@ export default {
 					...commonOptions.plugins,
 					tooltip: {
 						...commonOptions.plugins.tooltip,
-						intersect: false,
-						mode: "index",
+						axis: "xy",
 						position: "topBottomCenter",
 						callbacks: {
 							label: (tooltipItem) => {
diff --git a/assets/js/components/Sessions/chartConfig.js b/assets/js/components/Sessions/chartConfig.js
index 0db9db15..a69cbc8b 100644
--- a/assets/js/components/Sessions/chartConfig.js
+++ b/assets/js/components/Sessions/chartConfig.js
@@ -45,6 +45,8 @@ export const commonOptions = {
       boxPadding: 5,
       usePointStyle: false,
       borderWidth: 0.00001,
+      mode: "index",
+      intersect: false,
     },
   },
 };
diff --git a/charger/config.go b/charger/config.go
index 6449c3d7..05b95ae9 100644
--- a/charger/config.go
+++ b/charger/config.go
@@ -2,14 +2,12 @@ package charger
 
 import (
 	"context"
-	"fmt"
-	"strings"
 
 	"github.com/evcc-io/evcc/api"
-	reg "github.com/evcc-io/evcc/util/registry"
+	"github.com/evcc-io/evcc/charger/config"
 )
 
-var registry = reg.New[api.Charger]("charger")
+var registry = config.Registry
 
 // Types returns the list of types
 func Types() []string {
@@ -18,15 +16,5 @@ func Types() []string {
 
 // NewFromConfig creates charger from configuration
 func NewFromConfig(ctx context.Context, typ string, other map[string]interface{}) (api.Charger, error) {
-	factory, err := registry.Get(strings.ToLower(typ))
-	if err != nil {
-		return nil, err
-	}
-
-	v, err := factory(ctx, other)
-	if err != nil {
-		err = fmt.Errorf("cannot create charger type '%s': %w", typ, err)
-	}
-
-	return v, err
+	return config.NewFromConfig(ctx, typ, other)
 }
diff --git a/charger/config/config.go b/charger/config/config.go
new file mode 100644
index 00000000..33a88b06
--- /dev/null
+++ b/charger/config/config.go
@@ -0,0 +1,27 @@
+package config
+
+import (
+	"context"
+	"fmt"
+	"strings"
+
+	"github.com/evcc-io/evcc/api"
+	reg "github.com/evcc-io/evcc/util/registry"
+)
+
+var Registry = reg.New[api.Charger]("charger")
+
+// NewFromConfig creates charger from configuration
+func NewFromConfig(ctx context.Context, typ string, other map[string]interface{}) (api.Charger, error) {
+	factory, err := Registry.Get(strings.ToLower(typ))
+	if err != nil {
+		return nil, err
+	}
+
+	v, err := factory(ctx, other)
+	if err != nil {
+		err = fmt.Errorf("cannot create charger type '%s': %w", typ, err)
+	}
+
+	return v, err
+}
diff --git a/charger/fritzdect.go b/charger/fritzdect.go
index 81a0fc9f..81cd659c 100644
--- a/charger/fritzdect.go
+++ b/charger/fritzdect.go
@@ -44,6 +44,9 @@ func NewFritzDECTFromConfig(other map[string]interface{}) (api.Charger, error) {
 // NewFritzDECT creates a new connection with standbypower for charger
 func NewFritzDECT(embed embed, uri, ain, user, password string, standbypower float64) (*FritzDECT, error) {
 	conn, err := fritzdect.NewConnection(uri, ain, user, password)
+	if err != nil {
+		return nil, err
+	}
 
 	c := &FritzDECT{
 		conn: conn,
@@ -51,7 +54,7 @@ func NewFritzDECT(embed embed, uri, ain, user, password string, standbypower flo
 
 	c.switchSocket = NewSwitchSocket(&embed, c.Enabled, c.conn.CurrentPower, standbypower)
 
-	return c, err
+	return c, nil
 }
 
 // Status implements the api.Charger interface
diff --git a/charger/homematic.go b/charger/homematic.go
index 30c547f3..ec2a7382 100644
--- a/charger/homematic.go
+++ b/charger/homematic.go
@@ -44,6 +44,9 @@ func NewCCUFromConfig(other map[string]interface{}) (api.Charger, error) {
 // NewCCU creates a new connection with standbypower for charger
 func NewCCU(embed embed, uri, deviceid, meterid, switchid, user, password string, standbypower float64, cache time.Duration) (*CCU, error) {
 	conn, err := homematic.NewConnection(uri, deviceid, meterid, switchid, user, password, cache)
+	if err != nil {
+		return nil, err
+	}
 
 	c := &CCU{
 		conn: conn,
@@ -51,7 +54,7 @@ func NewCCU(embed embed, uri, deviceid, meterid, switchid, user, password string
 
 	c.switchSocket = NewSwitchSocket(&embed, c.Enabled, c.conn.CurrentPower, standbypower)
 
-	return c, err
+	return c, nil
 }
 
 // Enabled implements the api.Charger interface
diff --git a/charger/mypv-elwa2.go b/charger/mypv.go
similarity index 64%
rename from charger/mypv-elwa2.go
rename to charger/mypv.go
index 151e2f6a..c6a5d275 100644
--- a/charger/mypv-elwa2.go
+++ b/charger/mypv.go
@@ -29,11 +29,12 @@ import (
 	"github.com/evcc-io/evcc/util/sponsor"
 )
 
-// MyPvElwa2 charger implementation
-type MyPvElwa2 struct {
-	log   *util.Logger
-	conn  *modbus.Connection
-	power uint32
+// MyPv charger implementation
+type MyPv struct {
+	log      *util.Logger
+	conn     *modbus.Connection
+	power    uint32
+	regPower uint16
 }
 
 const (
@@ -42,16 +43,23 @@ const (
 	elwaRegTempLimit = 1002
 	elwaRegStatus    = 1003
 	elwaRegPower     = 1074
+	thorRegPower     = 1060
 )
 
 func init() {
-	registry.AddCtx("ac-elwa-2", NewMyPvElwa2FromConfig)
-}
+	// https://github.com/evcc-io/evcc/discussions/12761
+	registry.AddCtx("ac-elwa-2", func(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
+		return newMyPvFromConfig(ctx, "ac-elwa-2", other, elwaRegPower)
+	})
 
-// https://github.com/evcc-io/evcc/discussions/12761
+	// https: // github.com/evcc-io/evcc/issues/18020
+	registry.AddCtx("ac-thor", func(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
+		return newMyPvFromConfig(ctx, "ac-thor", other, thorRegPower)
+	})
+}
 
-// NewMyPvElwa2FromConfig creates a MyPvElwa2 charger from generic config
-func NewMyPvElwa2FromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
+// newMyPvFromConfig creates a MyPv charger from generic config
+func newMyPvFromConfig(ctx context.Context, name string, other map[string]interface{}, regPower uint16) (api.Charger, error) {
 	cc := modbus.TcpSettings{
 		ID: 1,
 	}
@@ -60,11 +68,11 @@ func NewMyPvElwa2FromConfig(ctx context.Context, other map[string]interface{}) (
 		return nil, err
 	}
 
-	return NewMyPvElwa2(ctx, cc.URI, cc.ID)
+	return NewMyPv(ctx, name, cc.URI, cc.ID, regPower)
 }
 
-// NewMyPvElwa2 creates myPV AC Elwa 2 charger
-func NewMyPvElwa2(ctx context.Context, uri string, slaveID uint8) (api.Charger, error) {
+// NewMyPv creates myPV AC Elwa 2 or Thor charger
+func NewMyPv(ctx context.Context, name, uri string, slaveID uint8, regPower uint16) (api.Charger, error) {
 	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID)
 	if err != nil {
 		return nil, err
@@ -74,12 +82,13 @@ func NewMyPvElwa2(ctx context.Context, uri string, slaveID uint8) (api.Charger,
 		return nil, api.ErrSponsorRequired
 	}
 
-	log := util.NewLogger("ac-elwa-2")
+	log := util.NewLogger(name)
 	conn.Logger(log.TRACE)
 
-	wb := &MyPvElwa2{
-		log:  log,
-		conn: conn,
+	wb := &MyPv{
+		log:      log,
+		conn:     conn,
+		regPower: regPower,
 	}
 
 	go wb.heartbeat(ctx, 30*time.Second)
@@ -87,21 +96,21 @@ func NewMyPvElwa2(ctx context.Context, uri string, slaveID uint8) (api.Charger,
 	return wb, nil
 }
 
-var _ api.IconDescriber = (*MyPvElwa2)(nil)
+var _ api.IconDescriber = (*MyPv)(nil)
 
 // Icon implements the api.IconDescriber interface
-func (v *MyPvElwa2) Icon() string {
+func (v *MyPv) Icon() string {
 	return "waterheater"
 }
 
-var _ api.FeatureDescriber = (*MyPvElwa2)(nil)
+var _ api.FeatureDescriber = (*MyPv)(nil)
 
 // Features implements the api.FeatureDescriber interface
-func (wb *MyPvElwa2) Features() []api.Feature {
+func (wb *MyPv) Features() []api.Feature {
 	return []api.Feature{api.IntegratedDevice, api.Heating}
 }
 
-func (wb *MyPvElwa2) heartbeat(ctx context.Context, timeout time.Duration) {
+func (wb *MyPv) heartbeat(ctx context.Context, timeout time.Duration) {
 	for tick := time.Tick(timeout); ; {
 		select {
 		case <-tick:
@@ -122,7 +131,7 @@ func (wb *MyPvElwa2) heartbeat(ctx context.Context, timeout time.Duration) {
 }
 
 // Status implements the api.Charger interface
-func (wb *MyPvElwa2) Status() (api.ChargeStatus, error) {
+func (wb *MyPv) Status() (api.ChargeStatus, error) {
 	res := api.StatusA
 	b, err := wb.conn.ReadHoldingRegisters(elwaRegStatus, 1)
 	if err != nil {
@@ -138,7 +147,7 @@ func (wb *MyPvElwa2) Status() (api.ChargeStatus, error) {
 }
 
 // Enabled implements the api.Charger interface
-func (wb *MyPvElwa2) Enabled() (bool, error) {
+func (wb *MyPv) Enabled() (bool, error) {
 	b, err := wb.conn.ReadHoldingRegisters(elwaRegSetPower, 1)
 	if err != nil {
 		return false, err
@@ -147,7 +156,7 @@ func (wb *MyPvElwa2) Enabled() (bool, error) {
 	return binary.BigEndian.Uint16(b) > 0, nil
 }
 
-func (wb *MyPvElwa2) setPower(power uint16) error {
+func (wb *MyPv) setPower(power uint16) error {
 	b := make([]byte, 2)
 	binary.BigEndian.PutUint16(b, power)
 
@@ -156,7 +165,7 @@ func (wb *MyPvElwa2) setPower(power uint16) error {
 }
 
 // Enable implements the api.Charger interface
-func (wb *MyPvElwa2) Enable(enable bool) error {
+func (wb *MyPv) Enable(enable bool) error {
 	var power uint16
 	if enable {
 		power = uint16(atomic.LoadUint32(&wb.power))
@@ -166,14 +175,14 @@ func (wb *MyPvElwa2) Enable(enable bool) error {
 }
 
 // MaxCurrent implements the api.Charger interface
-func (wb *MyPvElwa2) MaxCurrent(current int64) error {
+func (wb *MyPv) MaxCurrent(current int64) error {
 	return wb.MaxCurrentMillis(float64(current))
 }
 
-var _ api.ChargerEx = (*MyPvElwa2)(nil)
+var _ api.ChargerEx = (*MyPv)(nil)
 
 // MaxCurrentMillis implements the api.ChargerEx interface
-func (wb *MyPvElwa2) MaxCurrentMillis(current float64) error {
+func (wb *MyPv) MaxCurrentMillis(current float64) error {
 	power := uint16(230 * current)
 
 	err := wb.setPower(power)
@@ -184,11 +193,11 @@ func (wb *MyPvElwa2) MaxCurrentMillis(current float64) error {
 	return err
 }
 
-var _ api.Meter = (*MyPvElwa2)(nil)
+var _ api.Meter = (*MyPv)(nil)
 
 // CurrentPower implements the api.Meter interface
-func (wb *MyPvElwa2) CurrentPower() (float64, error) {
-	b, err := wb.conn.ReadHoldingRegisters(elwaRegPower, 1)
+func (wb *MyPv) CurrentPower() (float64, error) {
+	b, err := wb.conn.ReadHoldingRegisters(wb.regPower, 1)
 	if err != nil {
 		return 0, err
 	}
@@ -196,10 +205,10 @@ func (wb *MyPvElwa2) CurrentPower() (float64, error) {
 	return float64(binary.BigEndian.Uint16(b)), nil
 }
 
-var _ api.Battery = (*MyPvElwa2)(nil)
+var _ api.Battery = (*MyPv)(nil)
 
 // CurrentPower implements the api.Meter interface
-func (wb *MyPvElwa2) Soc() (float64, error) {
+func (wb *MyPv) Soc() (float64, error) {
 	b, err := wb.conn.ReadHoldingRegisters(elwaRegTemp, 1)
 	if err != nil {
 		return 0, err
@@ -208,10 +217,10 @@ func (wb *MyPvElwa2) Soc() (float64, error) {
 	return float64(binary.BigEndian.Uint16(b)) / 10, nil
 }
 
-var _ api.SocLimiter = (*MyPvElwa2)(nil)
+var _ api.SocLimiter = (*MyPv)(nil)
 
 // GetLimitSoc implements the api.SocLimiter interface
-func (wb *MyPvElwa2) GetLimitSoc() (int64, error) {
+func (wb *MyPv) GetLimitSoc() (int64, error) {
 	b, err := wb.conn.ReadHoldingRegisters(elwaRegTempLimit, 1)
 	if err != nil {
 		return 0, err
diff --git a/charger/vaillant.go b/charger/vaillant.go
index 0911b51d..cb529d35 100644
--- a/charger/vaillant.go
+++ b/charger/vaillant.go
@@ -71,16 +71,17 @@ func NewVaillantFromConfig(ctx context.Context, other map[string]interface{}) (a
 	}
 
 	log := util.NewLogger("vaillant").Redact(cc.User, cc.Password)
-	client := request.NewClient(log)
-	clientCtx := context.WithValue(ctx, oauth2.HTTPClient, client)
+	logCtx := context.WithValue(ctx, oauth2.HTTPClient, request.NewClient(log))
 
 	oc := sensonet.Oauth2ConfigForRealm(cc.Realm)
-	token, err := oc.PasswordCredentialsToken(clientCtx, cc.User, cc.Password)
+	token, err := oc.PasswordCredentialsToken(logCtx, cc.User, cc.Password)
 	if err != nil {
 		return nil, err
 	}
 
-	conn, err := sensonet.NewConnection(oc.TokenSource(clientCtx, token), sensonet.WithHttpClient(client), sensonet.WithLogger(log.TRACE))
+	conn, err := sensonet.NewConnection(oc.TokenSource(logCtx, token),
+		sensonet.WithHttpClient(request.NewClient(log)),
+		sensonet.WithLogger(log.TRACE))
 	if err != nil {
 		return nil, err
 	}
diff --git "a/charger/weidm\303\274ller.go" "b/charger/weidm\303\274ller.go"
index efebcd11..9afbae91 100644
--- "a/charger/weidm\303\274ller.go"
+++ "b/charger/weidm\303\274ller.go"
@@ -2,7 +2,7 @@ package charger
 
 // LICENSE
 
-// Copyright (c) 2023 premultiply
+// Copyright (c) 2023-2025 premultiply
 
 // This module is NOT covered by the MIT license. All rights reserved.
 
@@ -18,8 +18,10 @@ package charger
 // SOFTWARE.
 
 import (
+	"context"
 	"encoding/binary"
 	"fmt"
+	"time"
 
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util"
@@ -36,23 +38,27 @@ type Weidmüller struct {
 }
 
 const (
-	wmRegCarStatus    = 301  // GD_ID_EVCC_CAR_STATE CHAR
-	wmRegEvccStatus   = 302  // GD_ID_EVCC_EVSE_STATE UINT16
-	wmRegPhases       = 318  // GD_ID_EVCC_PHASES_LLM UINT16
-	wmRegVoltages     = 400  // GD_ID_CM_VOLTAGE_PHASE UINT32
-	wmRegCurrents     = 406  // GD_ID_CM_CURRENT_PHASE UINT32
-	wmRegActivePower  = 418  // GD_ID_CM_ACTIVE_POWER UINT32
-	wmRegTotalEnergy  = 457  // GD_ID_CM_CONSUMED_ENERGY_TOTAL_WH UINT64
-	wmRegCurrentLimit = 702  // GD_ID_AUT_USER_CURRENT_LIMIT UINT16
-	wmRegCardId       = 1000 // GD_ID_RFID_TAG_UID CHAR[21]
+	wmRegCarStatus    = 301   // GD_ID_EVCC_CAR_STATE CHAR
+	wmRegEvccStatus   = 302   // GD_ID_EVCC_EVSE_STATE UINT16
+	wmRegPhases       = 318   // GD_ID_EVCC_PHASES_LLM UINT16
+	wmRegVoltages     = 400   // GD_ID_CM_VOLTAGE_PHASE UINT32
+	wmRegCurrents     = 406   // GD_ID_CM_CURRENT_PHASE UINT32
+	wmRegActivePower  = 418   // GD_ID_CM_ACTIVE_POWER UINT32
+	wmRegTotalEnergy  = 457   // GD_ID_CM_CONSUMED_ENERGY_TOTAL_WH UINT64
+	wmRegCardId       = 1000  // GD_ID_RFID_TAG_UID CHAR[21]
+	wmRegTimeout      = 11050 // GD_ID_LCM_TIMEOUT UINT32
+	wmRegCurrentLimit = 11052 // GD_ID_LCM_ACTUAL_CURRENT_LIMIT UINT16 (A)
+
+	wmHeartbeatInterval = 5 * time.Second
+	wmTimeout           = 65535 // ms
 )
 
 func init() {
-	registry.Add("weidmüller", NewWeidmüllerFromConfig)
+	registry.AddCtx("weidmüller", NewWeidmüllerFromConfig)
 }
 
 // NewWeidmüllerFromConfig creates a Weidmüller charger from generic config
-func NewWeidmüllerFromConfig(other map[string]interface{}) (api.Charger, error) {
+func NewWeidmüllerFromConfig(ctx context.Context, other map[string]interface{}) (api.Charger, error) {
 	cc := modbus.TcpSettings{
 		ID: 255,
 	}
@@ -61,11 +67,13 @@ func NewWeidmüllerFromConfig(other map[string]interface{}) (api.Charger, error)
 		return nil, err
 	}
 
-	return NewWeidmüller(cc.URI, cc.ID)
+	return NewWeidmüller(ctx, cc.URI, cc.ID)
 }
 
+//go:generate decorate -f decorateWeidmüller -b *Weidmüller -r api.Charger -t "api.MeterEnergy,TotalEnergy,func() (float64, error)"
+
 // NewWeidmüller creates Weidmüller charger
-func NewWeidmüller(uri string, id uint8) (api.Charger, error) {
+func NewWeidmüller(ctx context.Context, uri string, id uint8) (api.Charger, error) {
 	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, id)
 	if err != nil {
 		return nil, err
@@ -93,7 +101,32 @@ func NewWeidmüller(uri string, id uint8) (api.Charger, error) {
 		wb.curr = curr
 	}
 
-	return wb, err
+	// failsafe
+	go wb.heartbeat(ctx, wmHeartbeatInterval)
+
+	// check presence of energy meter
+	if b, err := wb.conn.ReadHoldingRegisters(wmRegTotalEnergy, 2); err == nil && binary.BigEndian.Uint32(b) > 0 {
+		return decorateWeidmüller(wb, wb.totalEnergy), nil
+	}
+
+	return wb, nil
+}
+
+func (wb *Weidmüller) heartbeat(ctx context.Context, timeout time.Duration) {
+	b := make([]byte, 4)
+	binary.BigEndian.PutUint32(b, wmTimeout)
+
+	for tick := time.Tick(timeout); ; {
+		select {
+		case <-tick:
+		case <-ctx.Done():
+			return
+		}
+
+		if _, err := wb.conn.WriteMultipleRegisters(wmRegTimeout, 2, b); err != nil {
+			wb.log.ERROR.Println("heartbeat:", err)
+		}
+	}
 }
 
 func (wb *Weidmüller) setCurrent(current uint16) error {
@@ -184,10 +217,8 @@ func (wb *Weidmüller) CurrentPower() (float64, error) {
 	return float64(encoding.Uint32LswFirst(b)) / 1e3, err
 }
 
-var _ api.MeterEnergy = (*Weidmüller)(nil)
-
 // TotalEnergy implements the api.MeterEnergy interface
-func (wb *Weidmüller) TotalEnergy() (float64, error) {
+func (wb *Weidmüller) totalEnergy() (float64, error) {
 	b, err := wb.conn.ReadHoldingRegisters(wmRegTotalEnergy, 2)
 	if err != nil {
 		return 0, err
diff --git "a/charger/weidm\303\274ller_decorators.go" "b/charger/weidm\303\274ller_decorators.go"
new file mode 100644
index 00000000..1df75453
--- /dev/null
+++ "b/charger/weidm\303\274ller_decorators.go"
@@ -0,0 +1,35 @@
+package charger
+
+// Code generated by github.com/evcc-io/evcc/cmd/tools/decorate.go. DO NOT EDIT.
+
+import (
+	"github.com/evcc-io/evcc/api"
+)
+
+func decorateWeidmüller(base *Weidmüller, meterEnergy func() (float64, error)) api.Charger {
+	switch {
+	case meterEnergy == nil:
+		return base
+
+	case meterEnergy != nil:
+		return &struct {
+			*Weidmüller
+			api.MeterEnergy
+		}{
+			Weidmüller: base,
+			MeterEnergy: &decorateWeidmüllerMeterEnergyImpl{
+				meterEnergy: meterEnergy,
+			},
+		}
+	}
+
+	return nil
+}
+
+type decorateWeidmüllerMeterEnergyImpl struct {
+	meterEnergy func() (float64, error)
+}
+
+func (impl *decorateWeidmüllerMeterEnergyImpl) TotalEnergy() (float64, error) {
+	return impl.meterEnergy()
+}
diff --git a/cmd/configure/helper.go b/cmd/configure/helper.go
index d2e994c1..8e2e72ac 100644
--- a/cmd/configure/helper.go
+++ b/cmd/configure/helper.go
@@ -444,7 +444,7 @@ func (c *CmdConfigure) processInputConfig(param templates.Param) string {
 		exampleValue: param.Example,
 		help:         param.Help.ShortString(c.lang),
 		valueType:    param.Type,
-		validValues:  param.ValidValues,
+		choice:       param.Choice,
 		mask:         param.IsMasked(),
 		required:     param.IsRequired(),
 	})
diff --git a/cmd/configure/survey.go b/cmd/configure/survey.go
index be27dbbe..09268fce 100644
--- a/cmd/configure/survey.go
+++ b/cmd/configure/survey.go
@@ -104,7 +104,7 @@ type question struct {
 	label, help                    string
 	defaultValue, exampleValue     string
 	invalidValues                  []string
-	validValues                    []string
+	choice                         []string
 	valueType                      templates.ParamType
 	minNumberValue, maxNumberValue int64
 	mask, required                 bool
@@ -130,11 +130,11 @@ func (c *CmdConfigure) askParam(p templates.Param) string {
 	}
 
 	return c.askValue(question{
-		label:       p.Description.String(c.lang),
-		valueType:   p.Type,
-		validValues: p.Choice, // TODO proper choice handling
-		mask:        mask,
-		required:    required,
+		label:     p.Description.String(c.lang),
+		valueType: p.Type,
+		choice:    p.Choice,
+		mask:      mask,
+		required:  required,
 	})
 }
 
@@ -155,8 +155,8 @@ func (c *CmdConfigure) askValue(q question) string {
 
 	if q.valueType == templates.TypeChoice {
 		label := strings.TrimSpace(strings.Join([]string{q.label, c.localizedString("Value_Choice")}, " "))
-		idx, _ := c.askChoice(label, q.validValues)
-		return q.validValues[idx]
+		idx, _ := c.askChoice(label, q.choice)
+		return q.choice[idx]
 	}
 
 	if q.valueType == templates.TypeChargeModes {
@@ -181,7 +181,7 @@ func (c *CmdConfigure) askValue(q question) string {
 			return errors.New(c.localizedString("ValueError_Used"))
 		}
 
-		if q.validValues != nil && !slices.Contains(q.validValues, value) {
+		if q.choice != nil && !slices.Contains(q.choice, value) {
 			return errors.New(c.localizedString("ValueError_Invalid"))
 		}
 
diff --git a/cmd/demo.yaml b/cmd/demo.yaml
index acca352b..c54a5b7e 100644
--- a/cmd/demo.yaml
+++ b/cmd/demo.yaml
@@ -263,5 +263,6 @@ tariffs:
     type: fixed
     price: 0.08 # EUR/kWh
   co2:
-    type: grünstromindex
+    type: template
+    template: grünstromindex
     zip: 12349
diff --git a/core/keys/site.go b/core/keys/site.go
index 4ffbb147..11784db6 100644
--- a/core/keys/site.go
+++ b/core/keys/site.go
@@ -3,7 +3,9 @@ package keys
 const (
 	Aux                   = "aux"
 	AuxPower              = "auxPower"
+	Circuits              = "circuits"
 	Currency              = "currency"
+	Ext                   = "ext"
 	GreenShareHome        = "greenShareHome"
 	GreenShareLoadpoints  = "greenShareLoadpoints"
 	GridConfigured        = "gridConfigured"
@@ -28,8 +30,6 @@ const (
 	TariffPriceHome       = "tariffPriceHome"
 	TariffPriceLoadpoints = "tariffPriceLoadpoints"
 	Vehicles              = "vehicles"
-	Circuits              = "circuits"
-	Ext                   = "ext"
 
 	// meters
 	GridMeter     = "gridMeter"
diff --git a/core/site.go b/core/site.go
index 1e1b2df0..a9cccbde 100644
--- a/core/site.go
+++ b/core/site.go
@@ -42,20 +42,14 @@ type updater interface {
 	Update(sitePower, batteryBoostPower float64, rates api.Rates, batteryBuffered, batteryStart bool, greenShare float64, effectivePrice, effectiveCo2 *float64)
 }
 
-// meterMeasurement is used as slice element for publishing structured data
-type meterMeasurement struct {
-	Power         float64 `json:"power"`
-	Energy        float64 `json:"energy,omitempty"`
-	ExcessDCPower float64 `json:"excessdcpower,omitempty"`
-}
-
-// batteryMeasurement is used as slice element for publishing structured data
-type batteryMeasurement struct {
-	Power        float64 `json:"power"`
-	Energy       float64 `json:"energy,omitempty"`
-	Soc          float64 `json:"soc,omitempty"`
-	Capacity     float64 `json:"capacity,omitempty"`
-	Controllable bool    `json:"controllable"`
+// measurement is used as slice element for publishing structured data
+type measurement struct {
+	Power         float64  `json:"power"`
+	Energy        float64  `json:"energy,omitempty"`
+	ExcessDCPower float64  `json:"excessdcpower,omitempty"`
+	Capacity      *float64 `json:"capacity,omitempty"`
+	Soc           *float64 `json:"soc,omitempty"`
+	Controllable  *bool    `json:"controllable,omitempty"`
 }
 
 var _ site.API = (*Site)(nil)
@@ -434,25 +428,17 @@ func (site *Site) publishDelta(key string, val interface{}) {
 	site.publish(key, val)
 }
 
-// updatePvMeters updates pv meters. All measurements are optional.
-func (site *Site) updatePvMeters() {
-	if len(site.pvMeters) == 0 {
-		return
-	}
-
+func (site *Site) collectMeters(key string, meters []api.Meter) []measurement {
 	var wg sync.WaitGroup
-
-	mm := make([]meterMeasurement, len(site.pvMeters))
+	mm := make([]measurement, len(meters))
 
 	fun := func(i int, meter api.Meter) {
 		// power
 		power, err := backoff.RetryWithData(meter.CurrentPower, bo())
 		if err == nil {
-			if power < -500 {
-				site.log.WARN.Printf("pv %d power: %.0fW is negative - check configuration if sign is correct", i+1, power)
-			}
+			site.log.DEBUG.Printf("%s %d power: %.0fW", key, i+1, power)
 		} else {
-			site.log.ERROR.Printf("pv %d power: %v", i+1, err)
+			site.log.ERROR.Printf("%s %d power: %v", key, i+1, err)
 		}
 
 		// energy (production)
@@ -460,171 +446,95 @@ func (site *Site) updatePvMeters() {
 		if m, ok := meter.(api.MeterEnergy); err == nil && ok {
 			energy, err = m.TotalEnergy()
 			if err != nil {
-				site.log.ERROR.Printf("pv %d energy: %v", i+1, err)
+				site.log.ERROR.Printf("%s %d energy: %v", key, i+1, err)
 			}
 		}
 
-		var excessDC float64
-		var excessStr string
-		if m, ok := meter.(api.MaxACPower); ok {
-			if dc := m.MaxACPower() - power; dc < 0 && power > 0 {
-				excessDC = -dc
-				excessStr = fmt.Sprintf(" (includes %.0fW excess DC)", -dc)
-			}
-		}
-
-		if len(site.pvMeters) > 1 {
-			site.log.DEBUG.Printf("pv %d power: %.0fW"+excessStr, i+1, power)
-		}
-
-		mm[i] = meterMeasurement{
-			Power:         power,
-			Energy:        energy,
-			ExcessDCPower: excessDC,
+		mm[i] = measurement{
+			Power:  power,
+			Energy: energy,
 		}
 
 		wg.Done()
 	}
 
-	wg.Add(len(site.pvMeters))
-	for i, meter := range site.pvMeters {
+	wg.Add(len(meters))
+	for i, meter := range meters {
 		go fun(i, meter)
 	}
 	wg.Wait()
 
-	site.pvPower = lo.Reduce(mm, func(acc float64, m meterMeasurement, _ int) float64 {
-		return acc + max(0, m.Power)
-	}, 0)
-	site.excessDCPower = lo.Reduce(mm, func(acc float64, m meterMeasurement, _ int) float64 {
-		return acc - math.Abs(m.ExcessDCPower)
-	}, 0)
-	totalEnergy := lo.Reduce(mm, func(acc float64, m meterMeasurement, _ int) float64 {
-		return acc + m.Energy
-	}, 0)
-
-	var excessStr string
-	if site.excessDCPower < 0 {
-		excessStr = fmt.Sprintf(" (includes %.0fW excess DC)", -site.excessDCPower)
-	}
-
-	site.log.DEBUG.Printf("pv power: %.0fW"+excessStr, site.pvPower)
-	site.publish(keys.PvPower, site.pvPower)
-	site.publish(keys.PvEnergy, totalEnergy)
-	site.publish(keys.Pv, mm)
+	return mm
 }
 
-// updateAuxMeters updates aux meters
-func (site *Site) updateAuxMeters() {
-	if len(site.auxMeters) == 0 {
+// updatePvMeters updates pv meters. All measurements are optional.
+func (site *Site) updatePvMeters() {
+	if len(site.pvMeters) == 0 {
 		return
 	}
 
-	var wg sync.WaitGroup
+	mm := site.collectMeters("pv", site.pvMeters)
 
-	mm := make([]meterMeasurement, len(site.auxMeters))
+	for i, meter := range site.pvMeters {
+		power := mm[i].Power
 
-	fun := func(i int, meter api.Meter) {
-		if power, err := meter.CurrentPower(); err == nil {
-			mm[i].Power = power
-			site.log.DEBUG.Printf("aux power %d: %.0fW", i+1, power)
-		} else {
-			site.log.ERROR.Printf("aux meter %d: %v", i+1, err)
+		if power < -500 {
+			site.log.WARN.Printf("pv %d power: %.0fW is negative - check configuration if sign is correct", i+1, power)
 		}
 
-		wg.Done()
-	}
-
-	wg.Add(len(site.auxMeters))
-	for i, meter := range site.auxMeters {
-		go fun(i, meter)
+		if m, ok := meter.(api.MaxACPower); ok {
+			if dc := m.MaxACPower() - power; dc < 0 && power > 0 {
+				mm[i].ExcessDCPower = -dc
+				site.log.DEBUG.Printf("pv %d excess DC: %.0fW", i+1, -dc)
+			}
+		}
 	}
-	wg.Wait()
 
-	site.auxPower = lo.Reduce(mm, func(acc float64, m meterMeasurement, _ int) float64 {
-		return acc + m.Power
+	site.pvPower = lo.Reduce(mm, func(acc float64, m measurement, _ int) float64 {
+		return acc + max(0, m.Power)
+	}, 0)
+	site.excessDCPower = lo.Reduce(mm, func(acc float64, m measurement, _ int) float64 {
+		return acc - math.Abs(m.ExcessDCPower)
+	}, 0)
+	totalEnergy := lo.Reduce(mm, func(acc float64, m measurement, _ int) float64 {
+		return acc + m.Energy
 	}, 0)
 
-	site.log.DEBUG.Printf("aux power: %.0fW", site.auxPower)
-	site.publish(keys.AuxPower, site.auxPower)
-	site.publish(keys.Aux, mm)
-}
-
-// updateExtMeters updates ext meters
-func (site *Site) updateExtMeters() {
-	if len(site.extMeters) == 0 {
-		return
-	}
-
-	mm := make([]meterMeasurement, len(site.extMeters))
-
-	for i, meter := range site.extMeters {
-		// ext power
-		power, err := backoff.RetryWithData(meter.CurrentPower, bo())
-		if err != nil {
-			site.log.ERROR.Printf("ext meter %d power: %v", i+1, err)
-		}
-
-		// ext energy
-		var energy float64
-		if m, ok := meter.(api.MeterEnergy); err == nil && ok {
-			energy, err = m.TotalEnergy()
-			if err != nil {
-				site.log.ERROR.Printf("ext meter %d energy: %v", i+1, err)
-			}
+	if len(site.pvMeters) > 1 {
+		var excessStr string
+		if site.excessDCPower < 0 {
+			excessStr = fmt.Sprintf(" (includes %.0fW excess DC)", -site.excessDCPower)
 		}
 
-		mm[i] = meterMeasurement{
-			Power:  power,
-			Energy: energy,
-		}
+		site.log.DEBUG.Printf("pv power: %.0fW"+excessStr, site.pvPower)
 	}
 
-	// Publishing will be done in separate PR
+	site.publish(keys.PvPower, site.pvPower)
+	site.publish(keys.PvEnergy, totalEnergy)
+	site.publish(keys.Pv, mm)
 }
 
 // updateBatteryMeters updates battery meters
-func (site *Site) updateBatteryMeters() error {
+func (site *Site) updateBatteryMeters() {
 	if len(site.batteryMeters) == 0 {
-		return nil
+		return
 	}
 
-	var eg errgroup.Group
-
-	mm := make([]batteryMeasurement, len(site.batteryMeters))
-
-	fun := func(i int, meter api.Meter) error {
-		power, err := backoff.RetryWithData(meter.CurrentPower, bo())
-		if err != nil {
-			// power is required- return on error
-			return fmt.Errorf("battery %d power: %v", i+1, err)
-		}
-
-		if len(site.batteryMeters) > 1 {
-			site.log.DEBUG.Printf("battery %d power: %.0fW", i+1, power)
-		}
-
-		// battery energy (discharge)
-		var energy float64
-		if m, ok := meter.(api.MeterEnergy); ok {
-			energy, err = m.TotalEnergy()
-			if err != nil {
-				site.log.ERROR.Printf("battery %d energy: %v", i+1, err)
-			}
-		}
+	mm := site.collectMeters("battery", site.batteryMeters)
 
+	for i, meter := range site.batteryMeters {
 		// battery soc and capacity
 		var batSoc, capacity float64
-		if meter, ok := meter.(api.Battery); ok {
-			batSoc, err = soc.Guard(meter.Soc())
+		var err error
 
+		if m, ok := meter.(api.Battery); ok {
+			batSoc, err = soc.Guard(m.Soc())
 			if err == nil {
-				if m, ok := meter.(api.BatteryCapacity); ok {
+				if m, ok := m.(api.BatteryCapacity); ok {
 					capacity = m.Capacity()
 				}
 
-				if len(site.batteryMeters) > 1 {
-					site.log.DEBUG.Printf("battery %d soc: %.0f%%", i+1, batSoc)
-				}
+				site.log.DEBUG.Printf("battery %d soc: %.0f%%", i+1, batSoc)
 			} else {
 				site.log.ERROR.Printf("battery %d soc: %v", i+1, err)
 			}
@@ -632,35 +542,21 @@ func (site *Site) updateBatteryMeters() error {
 
 		_, controllable := meter.(api.BatteryController)
 
-		mm[i] = batteryMeasurement{
-			Power:        power,
-			Energy:       energy,
-			Soc:          batSoc,
-			Capacity:     capacity,
-			Controllable: controllable,
-		}
-
-		return nil
+		mm[i].Soc = lo.ToPtr(batSoc)
+		mm[i].Capacity = lo.ToPtr(capacity)
+		mm[i].Controllable = lo.ToPtr(controllable)
 	}
 
-	for i, meter := range site.batteryMeters {
-		eg.Go(func() error { return fun(i, meter) })
-	}
-
-	if err := eg.Wait(); err != nil {
-		return err
-	}
-
-	site.batterySoc = lo.Reduce(mm, func(acc float64, m batteryMeasurement, _ int) float64 {
+	site.batterySoc = lo.Reduce(mm, func(acc float64, m measurement, _ int) float64 {
 		// weigh soc by capacity
-		weighedSoc := m.Soc
-		if m.Capacity > 0 {
-			weighedSoc *= m.Capacity
+		weighedSoc := *m.Soc
+		if *m.Capacity > 0 {
+			weighedSoc *= *m.Capacity
 		}
 		return acc + weighedSoc
 	}, 0)
-	totalCapacity := lo.Reduce(mm, func(acc float64, m batteryMeasurement, _ int) float64 {
-		return acc + m.Capacity
+	totalCapacity := lo.Reduce(mm, func(acc float64, m measurement, _ int) float64 {
+		return acc + *m.Capacity
 	}, 0)
 
 	// convert weighed socs to total soc
@@ -669,23 +565,53 @@ func (site *Site) updateBatteryMeters() error {
 	}
 	site.batterySoc /= totalCapacity
 
-	site.log.DEBUG.Printf("battery soc: %.0f%%", math.Round(site.batterySoc))
-	site.publish(keys.BatteryCapacity, totalCapacity)
-	site.publish(keys.BatterySoc, site.batterySoc)
-
-	site.batteryPower = lo.Reduce(mm, func(acc float64, m batteryMeasurement, _ int) float64 {
+	site.batteryPower = lo.Reduce(mm, func(acc float64, m measurement, _ int) float64 {
 		return acc + m.Power
 	}, 0)
-	totalEnergy := lo.Reduce(mm, func(acc float64, m batteryMeasurement, _ int) float64 {
+	totalEnergy := lo.Reduce(mm, func(acc float64, m measurement, _ int) float64 {
 		return acc + m.Energy
 	}, 0)
 
-	site.log.DEBUG.Printf("battery power: %.0fW", site.batteryPower)
+	if len(site.batteryMeters) > 1 {
+		site.log.DEBUG.Printf("battery power: %.0fW", site.batteryPower)
+		site.log.DEBUG.Printf("battery soc: %.0f%%", math.Round(site.batterySoc))
+	}
+
+	site.publish(keys.BatteryCapacity, totalCapacity)
+	site.publish(keys.BatterySoc, site.batterySoc)
+
 	site.publish(keys.BatteryPower, site.batteryPower)
 	site.publish(keys.BatteryEnergy, totalEnergy)
 	site.publish(keys.Battery, mm)
+}
 
-	return nil
+// updateAuxMeters updates aux meters
+func (site *Site) updateAuxMeters() {
+	if len(site.auxMeters) == 0 {
+		return
+	}
+
+	mm := site.collectMeters("aux", site.auxMeters)
+	site.auxPower = lo.Reduce(mm, func(acc float64, m measurement, _ int) float64 {
+		return acc + m.Power
+	}, 0)
+
+	if len(site.auxMeters) > 1 {
+		site.log.DEBUG.Printf("aux power: %.0fW", site.auxPower)
+	}
+
+	site.publish(keys.AuxPower, site.auxPower)
+	site.publish(keys.Aux, mm)
+}
+
+// updateExtMeters updates ext meters
+func (site *Site) updateExtMeters() {
+	if len(site.extMeters) == 0 {
+		return
+	}
+
+	mm := site.collectMeters("ext", site.extMeters)
+	site.publish(keys.Ext, mm)
 }
 
 // updateGridMeter updates grid meter
@@ -742,10 +668,10 @@ func (site *Site) updateMeters() error {
 	var eg errgroup.Group
 
 	eg.Go(func() error { site.updatePvMeters(); return nil })
+	eg.Go(func() error { site.updateBatteryMeters(); return nil })
 	eg.Go(func() error { site.updateAuxMeters(); return nil })
 	eg.Go(func() error { site.updateExtMeters(); return nil })
 
-	eg.Go(site.updateBatteryMeters)
 	eg.Go(site.updateGridMeter)
 
 	return eg.Wait()
@@ -769,7 +695,7 @@ func (site *Site) sitePower(totalChargePower, flexiblePower float64) (float64, b
 	// ensure safe default for residual power
 	residualPower := site.GetResidualPower()
 	if len(site.batteryMeters) > 0 && site.batterySoc < site.prioritySoc && residualPower <= 0 {
-		residualPower = 100 // W
+		residualPower = 100 // Wsite.publish(keys.PvPower,
 	}
 
 	// allow using grid and charge as estimate for pv power
diff --git a/evcc.dist.yaml b/evcc.dist.yaml
index 08bb7512..ef51b0f2 100644
--- a/evcc.dist.yaml
+++ b/evcc.dist.yaml
@@ -151,86 +151,19 @@ tariffs:
         price: 0.2 # EUR/kWh
       - days: Sat,Sun
         price: 0.15 # EUR/kWh
-
-    # or variable tariffs
-    # type: tibber
-    # token: "476c477d8a039529478ebd690d35ddd80e3308ffc49b59c65b142321aee963a4" # access token
-    # homeid: "cc83e83e-8cbf-4595-9bf7-c3cf192f7d9c" # optional if multiple homes associated to account
-
-    # type: awattar
-    # region: de # optional, choose at for Austria
-    # charges: # optional, additional charges per kWh
-    # tax: # optional, additional tax (0.1 for 10%)
-
-    # type: octopusenergy
-    # tariff: AGILE-FLEX-22-11-25 # Tariff code
-    # region: A # optional
-
-    # type: elering # Nordpool
-    # region: ee # or lt, lv, fi
-    # charges: # optional, additional charges per kWh
-    # tax: # optional, additional tax (0.1 for 10%)
-
-    # type: energinet # Energinet using the price in DKK
-    # region: dk1 # or dk2
-    # charges: # optional, additional charges per kWh
-    # tax: # optional, additional tax (0.1 for 10%)
-
-    # type: entsoe # Entso-E european market data
-    # domain: BZN|DE-LU # https://transparency.entsoe.eu/content/static_content/Static%20content/web%20api/Guide.html#_areas
-    # securitytoken: # api token
-    # charges: # optional, additional charges per kWh
-    # tax: # optional, additional tax (0.1 for 10%)
-
-    # type: pun # PUN - Prezzo unico nazionale - Hourly Italian wholesale prices
-    # charges: 0 # optional, additional charges per kWh
-    # tax: 0 # optional, additional tax (0.1 for 10%)
-
-    # type: amber
-    # token: # api token from https://app.amber.com.au/developers/
-    # siteid: # site ID returned by the API
-    # channel: general
-
-    # 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
-
-    # type: template
-    # template: energy-charts-api # epex spot market prices
-    # bzn: DE-LU
-    # charges: 0.15
-    # tax: 0.1
-    # formula: math.Min((price + charges) * (1 + tax), 0.5)
+    # see: https://docs.evcc.io/en/docs/devices/tariffs
 
   feedin:
     # rate for feeding excess (pv) energy to the grid
     type: fixed
     price: 0.08 # EUR/kWh
-
-    # type: octopusenergy
-    # tariff: AGILE-FLEX-22-11-25 # Tariff code
-    # region: A # optional
-
-    # type: amber
-    # token: # api token from https://app.amber.com.au/developers/
-    # siteid: # site ID returned by the API
-    # channel: feedIn
+    # see: https://docs.evcc.io/en/docs/devices/tariffs
   co2:
     # co2 tariff provides co2 intensity forecast and is for co2-optimized target charging if no variable grid tariff is specified
-    # type: grünstromindex # GrünStromIndex (Germany only)
+    # type: template
+    # template: grünstromindex # GrünStromIndex (Germany only)
     # zip: <zip>
-
-    # type: electricitymaps # https://app.electricitymaps.com/map
-    # uri: <uri>
-    # token: <token> # needs to be a token with forecast (not in the free tier)
-    # zone: DE
-
-    # type: ngeso # National Grid Electricity System Operator data (Great Britain only) https://carbonintensity.org.uk/
-    # provides national data if both region and postcode are omitted - Choose ONE only!
-    # region: 1 # optional, coarser than using a postcode - The region details are at https://carbon-intensity.github.io/api-definitions/#region-list
-    # postcode: SW1 # optional - Outward postcode i.e. RG41 or SW1 or TF8. Do not include full postcode, outward postcode only
+    # see: https://docs.evcc.io/en/docs/devices/tariffs#co-forecast
 
 # mqtt message broker
 mqtt:
diff --git a/i18n/bg.toml b/i18n/bg.toml
index 8f517861..cd673510 100644
--- a/i18n/bg.toml
+++ b/i18n/bg.toml
@@ -1,20 +1,25 @@
 [batterySettings]
 batteryLevel = "Батерия %"
 capacity = "{energy} от {total}"
+control = "Управление на батерията"
+discharge = "Предотврати разреждането в бърз режим и планираното зареждане"
 disclaimerHint = "Бележка:"
 disclaimerText = "Тези настройки важат само за режим на работа използвайки енергия от фотоволтаици. Зареждането ще бъде настроено спрямо тях"
-legendBottomName = "приоритизиране на дом"
-legendBottomSubline = "не се използва за зареждане"
-legendMiddleName = "приоритизиране на кола"
-legendMiddleSubline = "дом на заден план"
+gridChargeTab = "Зареждане от мрежата"
+legendBottomName = "Приоритизиране на зареждането на домашната батерия"
+legendBottomSubline = "докато достигне {soc}."
+legendMiddleName = "Приоритизиране на зареждането на превозното средство"
+legendMiddleSubline = "когато домашната батерия е над {soc}."
 legendTitle = "Как да се използва енергията от фотоволтаиците?"
 legendTopAutostart = "автоматично стартиране"
 legendTopName = "зареждане използвайки батерия"
-legendTopSubline = "без прекъсвания"
-modalTitle = "Настройки на батерия"
+legendTopSubline = "когато домашната батерия е над {soc}."
+modalTitle = "Домашна батерия"
+usageTab = "Използване на батерията"
 
 [batterySettings.bufferStart]
-full = "при почти заредена батерия"
+above = "когато над {soc}"
+full = "при {soc}."
 low = "при донякъде заредена батерия"
 medium = "при наполовина заредена батерия"
 never = "само при наличие на излишък"
@@ -22,62 +27,213 @@ never = "само при наличие на излишък"
 [config]
 
 [config.battery]
+titleAdd = "Добави батерия"
+titleEdit = "Редактиране на батерията"
 
 [config.circuits]
+description = "Гарантира, че сумата от всички точки на натоварване, свързани към една верига, не надвишава конфигурираните граници на мощност и ток. Веригите могат да бъдат вложени, за да се изгради йерархия."
+title = "Управление на натоварването"
 
 [config.control]
+description = "Обикновено стойностите по подразбиране са наред. Променяйте ги само ако знаете какво правите."
+descriptionInterval = "Цикъл на актуализация на контролния цикъл в секунди. Определя колко често evcc чете данни от измервателния уред, регулира мощността на зареждане и актуализира потребителския интерфейс. Кратките интервали (< 30s) могат да причинят осцилации и нежелано поведение."
+descriptionResidualPower = "Премества работната точка на контролния цикъл. Ако имате домашна батерия, се препоръчва да зададете стойност от 100 W. По този начин батерията ще има лек приоритет пред използването на мрежата."
+labelInterval = "Интервал на актуализация"
+labelResidualPower = "Остатъчна мощност"
+title = "Поведение на управление"
 
 [config.deviceValue]
+broker = "Брокер"
+bucket = "Кофа"
+capacity = "Капацитет"
+chargeStatus = "Статус"
+chargeStatusA = "не е свързан"
+chargeStatusB = "свързан"
+chargeStatusC = "зареждане"
+chargeStatusE = "няма мощност"
+chargeStatusF = "грешка"
+chargedEnergy = "Заредено"
+co2 = "Мрежа CO₂"
+configured = "Конфигуриран"
+controllable = "Контролируем"
+currency = "Валута"
+current = "Електричеството"
+currentRange = "Електричеството"
+enabled = "Готов за зареждане"
+energy = "Енергия"
+feedinPrice = "Цена за изкупуване"
+gridPrice = "Цена на мрежата"
+hemsType = "Система"
+no = "не"
+odometer = "одометър"
+org = "Организация"
+phaseCurrents = "Текущ L1, L2, L3"
+phasePowers = "Мощност L1, L2, L3"
+phaseVoltages = "Напрежение L1, L2, L3"
+power = "Мощност"
+powerRange = "Мощност"
+range = "Обхват"
+soc = "Състояние на заряда"
+socLimit = "Лимит"
+temp = "Температура"
+topic = "Тема"
+url = "URL"
+yes = "да"
 
 [config.eebus]
+description = "Основна конфигурация за комуникация с други EEBus устройства."
+title = "EEBus"
 
 [config.form]
 example = "Пример"
 optional = "незадължителен"
 
 [config.general]
+cancel = "Отказ"
+docsLink = "Виж документацията."
+experimental = "Експериментален"
+hideAdvancedSettings = "Скрий разширените настройки"
+off = "изключено"
+on = "включено"
+password = "Парола"
+readFromFile = "Прочети от файл"
+remove = "Премахни"
+save = "Запази"
+showAdvancedSettings = "Покажи разширените настройки"
+telemetry = "Телеметрия"
+title = "Заглавие"
 
 [config.grid]
+title = "Мрежов измервателен уред"
+titleAdd = "Добави мрежов измервателен уред"
+titleEdit = "Редактирай мрежов измервателен уред"
 
 [config.hems]
+description = "Свържете evcc с друга система за управление на домашната енергия."
+title = "„Система за управление на енергията в дома“"
 
 [config.influx]
+description = "Записва данни за зареждане и други метрики в InfluxDB. Използвайте Grafana или други инструменти за визуализиране на данните."
+descriptionToken = "Проверете документацията на InfluxDB, за да научите как да създадете такъв. https://docs.influxdata.com/influxdb/v2/admin/"
+labelBucket = "Кофа"
+labelCheckInsecure = "Разреши самоподписани сертификати"
+labelDatabase = "База данни"
+labelInsecure = "„Проверка на сертификат“"
+labelOrg = "Организация"
+labelPassword = "Парола"
+labelToken = "API токен"
+labelUrl = "URL"
+labelUser = "Потребителско име"
+title = "InfluxDB"
+v1Support = "Нуждаете се от поддръжка за InfluxDB 1.x?"
+v2Support = "Назад към InfluxDB 2.x"
 
 [config.main]
+addLoadpoint = "Добави зарядна точка"
+addPvBattery = "Добави соларен панел или батерия"
 addVehicle = "Добави автомобил"
+configured = "Конфигуриран"
 edit = "редактирай"
 title = "Конфигурация"
+unconfigured = "не е конфигуриран"
 vehicles = "Моите Автомобили"
+yaml = "Конфигурирано в evcc.yaml. Не може да се редактира в потребителския интерфейс."
 
 [config.messaging]
+description = "Получавайте съобщения за вашите сесии на зареждане."
+title = "Notifications"
 
 [config.meter]
+cancel = "Отказ"
+delete = "Изтрий"
+save = "Запази"
+template = "Производител"
+titleChoice = "Какво искате да добавите?"
+validateSave = "Валидирай и запази"
 
 [config.modbusproxy]
+description = "Позволете на няколко клиента да имат достъп до едно Modbus устройство."
+title = "„Модбус прокси“"
 
 [config.mqtt]
+authentication = "Автентикация"
+description = "Свържете се с MQTT брокер, за да обменяте данни с други системи във вашата мрежа."
+descriptionClientId = "Автор на съобщенията. Ако е празно, се използва `evcc-[rand]`."
+descriptionTopic = "Оставете празно, за да деактивирате публикуването."
+labelBroker = "Брокер"
+labelCaCert = "Сървърен сертификат (CA)"
+labelCheckInsecure = "Позволете самоподписани сертификати"
+labelClientCert = "Клиентски сертификат"
+labelClientId = "Клиентски идентификатор"
+labelClientKey = "Клиентски ключ"
+labelInsecure = "Валидиране на сертификат"
+labelPassword = "Парола"
+labelTopic = "Тема"
+labelUser = "Потребителско име"
+publishing = "Публикуване"
+title = "MQTT"
 
 [config.network]
+descriptionHost = "„Използвайте разширението .local, за да активирате mDNS. Това е от значение за разпознаване на мобилни приложения и някои зарядни станции OCPP.“"
+descriptionPort = "Порт за уеб интерфейса и API. Ще трябва да актуализирате URL адреса на браузъра си, ако промените това."
+descriptionSchema = "Влияе само на начина, по който се генерират URL адресите. Изборът на HTTPS няма да активира криптиране."
+labelHost = "Хост име"
+labelPort = "Порт"
+labelSchema = "Схема"
+title = "Мрежа"
 
 [config.options]
 
 [config.options.boolean]
+no = "не"
+yes = "да"
 
 [config.options.endianness]
+big = "голям ендиан"
+little = "„малко ендианско“"
 
 [config.options.schema]
+http = "HTTP (некриптиран)"
+https = "HTTPS (криптиран)"
 
 [config.pv]
+titleAdd = "Добавяне на соларен метър"
+titleEdit = "Редактиране на соларен метър"
 
 [config.section]
+general = "Общи"
+grid = "Мрежова връзка"
+integrations = "Интеграции"
+loadpoints = "Зарядни точки"
+meter = "Солар и батерия"
+system = "Система"
+vehicles = "Превозни средства"
 
 [config.sponsor]
+addToken = "Въведете спонсорски токен"
+changeToken = "Промяна на спонсорския токен"
+description = "Моделът за спонсорство ни помага да поддържаме проекта и устойчиво да изграждаме нови и вълнуващи функции. Като спонсор получавате достъп до всички реализации на зарядни устройства."
+descriptionToken = "Получавате токена от {url}. Ние също предлагаме пробен токен за тестване."
+error = "Спонсорският токен не е валиден."
+labelToken = "Спонсорски токен"
+title = "Спонсорство"
 
 [config.system]
+logs = "Логове"
+restart = "Рестартиране"
+restartRequiredDescription = "Моля, рестартирайте, за да видите ефекта."
+restartRequiredMessage = "Конфигурацията е променена."
+restartingDescription = "Моля, изчакайте..."
+restartingMessage = "Рестартиране на evcc."
 
 [config.tariffs]
+description = "Определете вашите енергийни тарифи, за да изчислите разходите за вашите сесии на зареждане."
+title = "Тарифи"
 
 [config.title]
+description = "Показва се на главния екран и в раздела на браузъра."
+label = "Заглавие"
+title = "Редактиране на заглавие"
 
 [config.validation]
 failed = "неуспешен"
@@ -115,15 +271,22 @@ powerSub2 = "зарежда..."
 tabTitle = "Общност"
 
 [footer.savings]
+co2Saved = "{value} запазени"
+co2Title = "CO₂ емисии"
+configurePriceCo2 = "Научете как да конфигурирате данните за цените и CO₂."
 footerLong = "{percent} енергия от слънцето"
 footerShort = "{percent} слънчева енергия"
 modalTitle = "Информация за зареждането"
+moneySaved = "{value} запазени"
 percentGrid = "{grid} кВч от мрежата"
 percentSelf = "{self} кВч от фотоволтаици"
 percentTitle = "Енергия от фотоволтаици"
+periodLabel = "Период:"
 priceFeedIn = "{feedInPrice} feed-in"
 priceGrid = "{gridPrice} мрежа"
 priceTitle = "Цена"
+referenceGrid = "Мрежа"
+referenceLabel = "Референтни данни:"
 savingsComparedToGrid = "сравнено с мрежата"
 savingsTitle = "Спестявате"
 savingsTotalEnergy = "{total} kWh заредени"
@@ -131,9 +294,14 @@ since = "от {since}"
 tabTitle = "Моите данни"
 
 [footer.savings.period]
+30d = "последните 30 дни"
+365d = "последните 365 дни"
+thisYear = "тази година"
+total = "всички времена"
 
 [footer.sponsor]
 becomeSponsor = "Станете Спонсор"
+becomeSponsorExtended = "Подкрепете ни директно, за да получите стикери."
 confetti = "Готови ли сте за конфети?"
 confettiPromise = "Получавате стикери и дигитални конфети"
 sticker = "... или evcc стикери?"
@@ -141,6 +309,10 @@ supportUs = "Нашата мисия е да направим слънчеват
 thanks = "Благодарим Ви, {sponsor}! С ваша помощ продължаваме да развиваме evcc."
 titleNoSponsor = "Подкрепете ни"
 titleSponsor = "Вие сте спонсор"
+titleTrial = "Режим на проба"
+titleVictron = "Спонсорирано от Victron Energy"
+trial = "Вие сте в пробен режим и можете да използвате всички функции. Моля, обмислете възможността да подкрепите проекта."
+victron = "Вие използвате evcc на хардуера на Victron Energy и имате достъп до всички функции."
 
 [footer.telemetry]
 optIn = "Искам да споделям моите данни."
@@ -161,96 +333,438 @@ modalUpdateStarted = "Стартирай новата версия на evcc..."
 modalUpdateStatusStart = "Инсталацията започна:"
 
 [header]
+about = "Относно"
+blog = "Блог"
+docs = "Документация"
+github = "GitHub"
+login = "Автомобил-влизания в системата"
+logout = "Изход"
+nativeSettings = "Промяна на сървъра"
+needHelp = "Нуждаете се от помощ?"
+sessions = "Сесии за зареждане"
 
 [help]
+discussionsButton = "GitHub дискусии"
+documentationButton = "Документация"
+issueButton = "Докладвайте за грешка"
+issueDescription = "Открихте ли странно или неправилно поведение?"
+logsButton = "Преглед на дневниците"
+logsDescription = "Проверете дневниците за грешки."
+modalTitle = "Нуждаете се от помощ?"
+primaryActions = "Нещо не работи както трябва? Това са добри места, където можете да получите помощ."
+restartButton = "Рестартиране"
+restartDescription = "Опитахте ли да го изключите и включите отново?"
+secondaryActions = "Все още не можете да решите проблема си? Ето някои по-сериозни опции."
 
 [help.restart]
+cancel = "Отказ"
+confirm = "Да, рестартирайте!"
+description = "При нормални обстоятелства рестартирането не би трябвало да е необходимо. Моля, обмислете подаването на сигнал за грешка, ако трябва редовно да рестартирате evcc."
+disclaimer = "Забележка: evcc ще се прекрати и ще разчита на операционната система за рестартиране на услугата."
+modalTitle = "Сигурни ли сте, че искате да рестартирате?"
 
 [log]
+areaLabel = "Филтриране по област"
+areas = "Всички области"
+download = "Изтеглете пълния дневник"
+levelLabel = "Филтриране по ниво на дневника"
+nAreas = "{count} области"
+noResults = "Няма съвпадащи записи в дневника."
+search = "Търсене"
+selectAll = "Изберете всички"
+showAll = "Показване на всички записи"
+title = "Дневници"
+update = "Автоматично обновяване"
 
 [loginModal]
+cancel = "Отказ"
+error = "Входът не бе успешен: "
+iframeHint = "Отворете evcc в нов раздел."
+iframeIssue = "Вашата парола е правилна, но изглежда, че браузърът ви е загубил бисквитката за удостоверяване. Това може да се случи, ако стартирате evcc в iframe чрез HTTP."
+invalid = "Паролата е невалидна."
+login = "Вход"
+password = "Парола"
+reset = "Нулиране на паролата?"
+title = "Удостоверяване"
 
 [main]
+vehicles = "Паркиране"
 
 [main.chargingPlan]
+active = "Активен"
+addRepeatingPlan = "Добавяне на повтарящ се план"
+arrivalTab = "Пристигане"
+day = "Ден"
+departureTab = "Заминаване"
+goal = "Цел на зареждане"
+modalTitle = "План за зареждане"
+none = "няма"
+planNumber = "План {number}"
+remove = "Премахване"
+repeating = "повтарящ се"
+repeatingPlans = "Повтарящи се планове"
+selectAll = "Изберете всички"
+time = "Време"
+title = "План"
+titleMinSoc = "Минимално зареждане"
+titleTargetCharge = "Заминаване"
+unsavedChanges = "Има незаписани промени. Приложи сега?"
+update = "Приложи"
+weekdays = "Дни"
 
 [main.energyflow]
+battery = "Батерия"
+batteryCharge = "Зареждане на батерията"
+batteryDischarge = "Разреждане на батерията"
+batteryHold = "Батерия (заключена)"
+batteryTooltip = "{energy} от {total} ({soc})"
+gridImport = "Използвана енергия от мрежата"
+homePower = "Потребление"
+loadpoints = "Зарядно устройство | Зарядно устройство | {count} зарядни устройства"
+noEnergy = "Няма данни от измервателния уред"
+pvExport = "Енергия, която се подава в електрическата мрежа"
+pvProduction = "Производство"
+selfConsumption = "Самопотребление"
 
 [main.heatingStatus]
+charging = "Загряване…"
+waitForVehicle = "Готово. Чака се нагревателят …"
 
 [main.loadpoint]
+avgPrice = "⌀ Цена"
+charged = "Заредено"
+co2 = "⌀ CO₂"
+duration = "Продължителност на зареждането"
+fallbackName = "Зарядна точка"
+power = "Мощност"
+price = "Цена"
+remaining = "Оставащо време"
+remoteDisabledHard = "{source}: изключено"
+remoteDisabledSoft = "{source}: изключено адаптивното соларно зареждане"
+solar = "Соларен"
 
 [main.loadpointSettings]
+batteryUsage = "Домашна батерия"
+currents = "Заряден ток"
+default = "по подразбиране"
+disclaimerHint = "Забележка:"
+onlyForSocBasedCharging = "Тези опции са налични само за превозни средства с известен заряд."
+smartCostCheap = "Евтино зареждане от мрежата"
+smartCostClean = "Чисто зареждане от мрежата"
+title = "Настройки {0}"
+vehicle = "Превозно средство"
 
 [main.loadpointSettings.batteryBoost]
+description = "Бързо зареждане от домашната батерия"
+label = "Батериен бууст"
+mode = "Налично само в соларен и мин+соларен режим"
+once = "Буустът е активен за тази сесия на зареждане"
 
 [main.loadpointSettings.limitSoc]
+description = "Лимит на зареждане, който се използва, когато това превозно средство е свързано"
+label = "Лимит по подразбиране"
 
 [main.loadpointSettings.maxCurrent]
+label = "Макс. ток"
 
 [main.loadpointSettings.minCurrent]
+label = "Мин. ток"
 
 [main.loadpointSettings.minSoc]
+description = "Превозното средство се зарежда „бързо“ до {0} в соларен режим. След това продължава със соларен излишък. Полезно за осигуряване на минимален пробег дори в по-тъмни дни."
+label = "Мин. заряд %"
 
 [main.loadpointSettings.phasesConfigured]
+label = "Фази"
+no1p3pSupport = "Как е свързана вашата зарядна станция?"
+phases_0 = "Автоматично превключване"
+phases_1 = "1 фаза"
+phases_1_hint = "({min} до {max})"
+phases_3 = "3 фази"
+phases_3_hint = "({min} до {max})"
 
 [main.mode]
+minpv = "Мин+Солар"
+now = "Бързо"
+off = "Изкл."
+pv = "Солар"
+smart = "Смарт"
 
 [main.provider]
+login = "Вход"
+logout = "Изход"
 
 [main.targetCharge]
+activate = "Активиране"
+co2Limit = "Лимит на CO₂ от {co2}"
+costLimitIgnore = "Конфигурираният {limit} ще бъде игнориран през този период."
+currentPlan = "Активен план"
+descriptionEnergy = "До кога трябва да бъде заредена {targetEnergy} в превозното средство?"
+descriptionSoc = "Кога трябва да бъде заредено превозното средство до {targetSoc}?"
+inactiveLabel = "Целево време"
+nextPlan = "Следващ план"
+notReachableInTime = "Целта ще бъде постигната {overrun} по-късно."
+onlyInPvMode = "Планът за зареждане работи само в соларен режим."
+planDuration = "Време за зареждане"
+planPeriodLabel = "Период"
+planPeriodValue = "{start} до {end}"
+planUnknown = "Все още не е известно"
+preview = "Преглед на плана"
+priceLimit = "Лимит на цената от {price}"
+remove = "Премахване"
+setTargetTime = "няма"
+targetIsAboveLimit = "Конфигурираният лимит за зареждане от {limit} ще бъде игнориран през този период."
+targetIsAboveVehicleLimit = "Лимитът на превозното средство е под целта за зареждане."
+targetIsInThePast = "Изберете време в бъдещето, Марти."
+targetIsTooFarInTheFuture = "Ще коригираме плана веднага щом научим повече за бъдещето."
+title = "Целево време"
+today = "днес"
+tomorrow = "утре"
+update = "Обновяване"
+vehicleCapacityDocs = "Научете как да го конфигурирате."
+vehicleCapacityRequired = "Капацитетът на батерията на превозното средство е необходим за изчисляване на времето за зареждане."
 
 [main.targetChargePlan]
+chargeDuration = "Време за зареждане"
+co2Label = "Емисия на CO₂ ⌀"
+priceLabel = "Цена на енергията"
+timeRange = "{day} {range} ч"
+unknownPrice = "Все още не е известно"
 
 [main.targetEnergy]
+label = "Лимит"
+noLimit = "няма"
 
 [main.vehicle]
+addVehicle = "Добавяне на превозно средство"
+changeVehicle = "Промяна на превозното средство"
+detectionActive = "Откриване на превозно средство…"
+fallbackName = "Превозно средство"
+moreActions = "Още действия"
+none = "Няма превозно средство"
+notReachable = "Превозното средство не беше достъпно. Опитайте да рестартирате evcc."
+targetSoc = "Лимит"
+temp = "Температура"
+tempLimit = "Целева температура"
+unknown = "Гостуващо превозно средство"
+vehicleSoc = "Зареждане"
 
 [main.vehicleSoc]
+charging = "зареждане"
+connected = "свързан"
+disconnected = "разединен"
+ready = "готов"
+vehicleLimit = "Лимит на превозното средство: {soc}"
 
 [main.vehicleStatus]
+awaitingAuthorization = "Изчакване на разрешение."
+batteryBoost = "Активно ускоряване на батерията."
+charging = "Зареждане…"
+cheapEnergyCharging = "Налична е евтина енергия."
+cheapEnergyNextStart = "Евтината енергия в {duration}"
+cheapEnergySet = "Лимитът на цената е зададен."
+cleanEnergyCharging = "Налична е чиста енергия."
+cleanEnergyNextStart = "Чиста енергия в {продължителност}."
+cleanEnergySet = "Лимитът на CO₂ е зададен."
+climating = "Открито е предварително кондициониране."
+connected = "Превозното средство е свързано"
+disconnectRequired = "Процесът е прекратен. Моля, свържете се отново."
+disconnected = "Разединен."
+finished = "Завършен."
+minCharge = "Минимално зареждане до {soc}."
+pvDisable = "Недостатъчен излишък. Скоро ще бъде пауза."
+pvEnable = "Наличен излишък. Скоро ще започне."
+scale1p = "Скоро ще се намали до еднофазно зареждане."
+scale3p = "Скоро ще се увеличи до трифазно зареждане."
+targetChargeActive = "Планът за зареждане е активен. Очаквано завършване след {duration}."
+targetChargePlanned = "Планът за таксуване започва в {duration}"
+targetChargeWaitForVehicle = "Планът за зареждане е готов. Очаква се превозното средство…"
+unknown = ""
+vehicleLimit = "Лимит на превозното средство."
+vehicleLimitReached = "Достигнат е лимитът на превозното средство."
+waitForVehicle = "Готов. Очакване на превозното средство…"
+welcome = "Кратко първоначално зареждане за потвърждаване на връзката."
 
 [notifications]
+dismissAll = "Отхвърли всички"
+logs = "Вижте пълните дневници"
+modalTitle = "Известия"
 
 [offline]
+configurationError = "Грешка при стартиране. Проверете конфигурацията си и рестартирайте."
+message = "Няма връзка със сървъра."
+restart = "Рестартиране"
+restartNeeded = "Необходимо за прилагане на промените."
+restarting = "Сървърът ще бъде отново на линия след малко."
 
 [passwordModal]
+description = "Задайте парола за защита на настройките на конфигурацията. Използването на основния екран е възможно и без влизане в системата."
+empty = "Паролата не трябва да бъде празна."
+error = "Грешка: "
+labelCurrent = "Текуща парола"
+labelNew = "Нова парола"
+labelRepeat = "Повторете паролата"
+newPassword = "Създайте парола"
+noMatch = "Паролите не съвпадат."
+titleNew = "Задайте парола на администратора"
+titleUpdate = "Обновяване на паролата на администратора"
+updatePassword = "Обновяване на паролата"
 
 [session]
+cancel = "Отказ"
+co2 = "CO₂"
+date = "Период"
+delete = "Изтрий"
+finished = "Завършено"
+meter = "Километраж"
+meterstart = "Начало на брояча"
+meterstop = "Край на брояча"
+odometer = "Пробег"
+price = "Цена"
+started = "Начално време"
+title = "Сесия на зареждане"
 
 [sessions]
+avgPower = "⌀ Мощност"
+avgPrice = "⌀ Цена"
+chargeDuration = "Продължителност"
+co2 = "⌀ CO₂"
+csvMonth = "Изтегляне на {month} CSV"
+csvTotal = "Изтегляне на общ CSV"
+date = "Начало"
+downloadCsv = "Изтегляне като CSV"
+energy = "Заредено"
+loadpoint = "Ладеен пункт"
+noData = "Няма сесии на зареждане този месец."
+overview = "Общ преглед"
+price = "Цена"
+reallyDelete = "Наистина ли искате да изтриете тази сесия?"
+showIndividualEntries = "Показване на отделни сесии"
+solar = "Слънчева енергия"
+title = "Сесии на зареждане"
+total = "Общо"
+vehicle = "Превозно средство"
 
 [sessions.chartTitle]
+avgCo2ByGroup = "⌀ CO₂ {byGroup}"
+avgPriceByGroup = "⌀ Цена {byGroup}"
+byGroupLoadpoint = "По зарядна точка"
+byGroupVehicle = "По превозно средство"
+energy = "Заредена енергия"
+energyGrouped = "Слънчева енергия срещу мрежова енергия"
+energyGroupedByGroup = "Енергия {byGroup}"
+energySubSolar = "{value} слънчева енергия"
+energySubTotal = "{value} общо"
+groupedCo2ByGroup = "Количество CO₂ {byGroup}"
+groupedPriceByGroup = "Обща цена {byGroup}"
+historyCo2 = "CO₂-емисии"
+historyCo2Sub = "{value} общо"
+historyPrice = "Разходи за зареждане"
+historyPriceSub = "{value} общо"
+solar = "Делът на слънчевата енергия в електроенергията през годината"
+solarByGroup = "Слънчев дял {byGroup}"
 
 [sessions.csv]
+chargedenergy = "Енергия (kWh)"
+chargeduration = "Продължителност"
+co2perkwh = "CO₂/kWh"
+created = "Начално време"
+finished = "Завършено"
+identifier = "Идентификатор"
+loadpoint = "Зарядна точка"
+meterstart = "Начално показание на електромера (kWh)"
+meterstop = "Крайно показание на електромера (kWh)"
+odometer = "Пробег (км)"
+price = "Цена"
+priceperkwh = "Цена/kWh"
+solarpercentage = "Слънчева енергия (%)"
+vehicle = "Превозно средство"
 
 [sessions.filter]
+allLoadpoints = "Всички зарядни точки"
+allVehicles = "Всички превозни средства"
+filter = "Филтър"
 
 [sessions.group]
+co2 = "Емисии"
+grid = "Мрежа"
+price = "Цена"
+self = "Слънчева енергия"
 
 [sessions.groupBy]
+loadpoint = "Зарядна точка"
+none = "Общо"
+vehicle = "Превозно средство"
 
 [sessions.period]
+month = "Месец"
+total = "Общо"
+year = "Година"
 
 [sessions.type]
+co2 = "CO₂"
+price = "Цена"
+solar = "Слънчева енергия"
 
 [settings]
+title = "Потребителски интерфейс"
 
 [settings.fullscreen]
+enter = "Въведете в режим на цял екран"
+exit = "Изход от режим на цял екран"
+label = "Цял екран"
 
 [settings.hiddenFeatures]
+label = "Експериментален"
+value = "Показване на експериментални функции на потребителския интерфейс."
 
 [settings.language]
+auto = "Автоматичен"
+label = "Език"
 
 [settings.sponsorToken]
+expires = "Вашият спонсорски токен изтича след {inXDays}. {getNewToken} и го актуализирайте тук."
+getNew = "Вземете нов"
+hint = "Забележка: В бъдеще ще автоматизираме това."
 
 [settings.telemetry]
+label = "Телеметрия"
 
 [settings.theme]
+auto = "Система"
+dark = "Тъмен"
+label = "Дизайн"
+light = "Светъл"
 
 [settings.unit]
+km = "km"
+label = "Единици"
+mi = "Мили"
 
 [smartCost]
 activeHours = "{charging} от {total}"
+activeHoursLabel = "Активни часове"
+applyToAll = "Приложи навсякъде?"
+batteryDescription = "Зарежда домашната батерия с енергия от мрежата."
+cheapTitle = "Евтино зареждане от мрежата"
+cleanTitle = "Чисто зареждане от мрежата"
+co2Label = "Емисия на CO₂"
+co2Limit = "Граница на CO₂"
+loadpointDescription = "Позволява временно бързо зареждане в соларен режим."
+modalTitle = "Интелигентно зареждане от мрежата"
+none = "Няма"
+priceLabel = "Цена на енергията"
+priceLimit = "Граница на цената"
+saved = "Запазено."
 
 [startupError]
+configFile = "Използван конфигурационен файл:"
+configuration = "Конфигурация"
+description = "Моля, проверете конфигурационния си файл. Ако съобщението за грешка не помогне, проверете {0}."
+discussions = "Дискусии в GitHub"
+fixAndRestart = "Моля, отстранете проблема и рестартирайте сървъра."
+hint = "Забележка: Възможно е също така да имате дефектно устройство (инвертор, измервателен уред и т.н.). Проверете мрежовите си връзки."
+lineError = "Грешка в {0}."
+lineErrorLink = "Ред {0}"
+restartButton = "Рестартиране"
+title = "Грешка при стартиране"
diff --git a/i18n/da.toml b/i18n/da.toml
index 94ca9673..e057d38d 100644
--- a/i18n/da.toml
+++ b/i18n/da.toml
@@ -423,8 +423,6 @@ batteryCharge = "Batteriet oplades"
 batteryDischarge = "Batteriet aflades"
 batteryHold = "Batteri (låst)"
 batteryTooltip = "{energi} af {total} ({soc})"
-cheapBatteryGridCharge = "billig strøm på elnettet"
-cleanBatteryGridCharge = "Grøn strøm på elnettet"
 gridImport = "Forbrug fra elnet"
 homePower = "Forbrug"
 loadpoints = "Oplader| Oplader | {count} opladere"
diff --git a/i18n/de.toml b/i18n/de.toml
index 4e45a865..ab44b7bf 100644
--- a/i18n/de.toml
+++ b/i18n/de.toml
@@ -408,10 +408,10 @@ weekdays = "Tage"
 battery = "Batterie"
 batteryCharge = "Batterie laden"
 batteryDischarge = "Batterie entladen"
+batteryGridChargeActive = "Netzladen aktiv"
+batteryGridChargeLimit = "Netzladen wenn"
 batteryHold = "Batterie (gesperrt)"
 batteryTooltip = "{energy} von {total} ({soc})"
-cheapBatteryGridCharge = "günstige Netzenergie"
-cleanBatteryGridCharge = "saubere Netzenergie"
 gridImport = "Netzbezug"
 homePower = "Verbrauch"
 loadpoints = "Ladepunkt | Ladepunkt | {count} Ladepunkte"
diff --git a/i18n/el.toml b/i18n/el.toml
index f3439690..ab4a6400 100644
--- a/i18n/el.toml
+++ b/i18n/el.toml
@@ -410,8 +410,6 @@ batteryCharge = "Φόρτιση μπαταρίας"
 batteryDischarge = "Αποφόρτιση μπαταρίας"
 batteryHold = "Μπαταρία (κλειδωμένο)"
 batteryTooltip = "{energy} από {total} ({soc})"
-cheapBatteryGridCharge = "φθηνή ενέργεια δικτύου"
-cleanBatteryGridCharge = "πράσινη ενέργεια δικτύου"
 gridImport = "Χρήση δικτύου"
 homePower = "Κατανάλωση"
 loadpoints = "Φορτιστής| Φορτιστής | {count} φορτιστές"
diff --git a/i18n/en.toml b/i18n/en.toml
index 2c81f7e8..6e289b9b 100644
--- a/i18n/en.toml
+++ b/i18n/en.toml
@@ -407,10 +407,10 @@ weekdays = "Days"
 battery = "Battery"
 batteryCharge = "Battery charging"
 batteryDischarge = "Battery discharging"
+batteryGridChargeActive = "grid charging active"
+batteryGridChargeLimit = "grid charging when"
 batteryHold = "Battery (locked)"
 batteryTooltip = "{energy} of {total} ({soc})"
-cheapBatteryGridCharge = "cheap grid energy"
-cleanBatteryGridCharge = "clean grid energy"
 gridImport = "Grid use"
 homePower = "Consumption"
 loadpoints = "Charger| Charger | {count} chargers"
diff --git a/i18n/es.toml b/i18n/es.toml
index 09c7185c..d058bd5b 100644
--- a/i18n/es.toml
+++ b/i18n/es.toml
@@ -428,8 +428,6 @@ batteryCharge = "Cargar la batería"
 batteryDischarge = "Descargar la batería"
 batteryHold = "Batería (bloqueada)"
 batteryTooltip = "{energy} de {total} ({soc})"
-cheapBatteryGridCharge = "Energía de red barata"
-cleanBatteryGridCharge = "Energía de red limpia"
 gridImport = "Consumo de red"
 homePower = "Consumo"
 loadpoints = "Punto de carga | Punto de carga | {count} Puntos de carga"
diff --git a/i18n/fi.toml b/i18n/fi.toml
index 2ac7b5ac..e6056a3f 100644
--- a/i18n/fi.toml
+++ b/i18n/fi.toml
@@ -419,8 +419,6 @@ batteryCharge = "Akunlataus"
 batteryDischarge = "Akku purkautuu"
 batteryHold = "Akku (lukittu)"
 batteryTooltip = "{energy} / {total} ({soc})"
-cheapBatteryGridCharge = "halpaa energiaa verkosta"
-cleanBatteryGridCharge = "puhdasta energiaa verkosta"
 gridImport = "Kulutus sähköverkosta"
 homePower = "Kodin sähkönkulutus"
 loadpoints = "Latauslaite| Latauslaite | {count} latauslaitetta"
diff --git a/i18n/fr.toml b/i18n/fr.toml
index 04ddea38..62412a76 100644
--- a/i18n/fr.toml
+++ b/i18n/fr.toml
@@ -428,8 +428,6 @@ batteryCharge = "Batterie en charge"
 batteryDischarge = "Décharge de la batterie"
 batteryHold = "Batterie (verrouillée)"
 batteryTooltip = "{energy} sur {total} ({soc})"
-cheapBatteryGridCharge = "énergie réseau bon marché"
-cleanBatteryGridCharge = "énergie réseau verte"
 gridImport = "Utilisation du réseau"
 homePower = "Consommation"
 loadpoints = "Recharge| Recharge | {count} chargeurs"
diff --git a/i18n/hr.toml b/i18n/hr.toml
index d7406b83..9c9c7afa 100644
--- a/i18n/hr.toml
+++ b/i18n/hr.toml
@@ -420,8 +420,6 @@ batteryCharge = "Punjenje baterije"
 batteryDischarge = "Pražnjenje baterije"
 batteryHold = "Baterija (zaključano)"
 batteryTooltip = "{energy} od {total} ({soc})"
-cheapBatteryGridCharge = "jeftina energije iz mreže"
-cleanBatteryGridCharge = "čista energija iz mreže"
 gridImport = "Upotreba mreže"
 homePower = "Potrošnja"
 loadpoints = "Punjač| Punjač | {count} punjača"
@@ -586,6 +584,7 @@ scale3p = "Uskoro se prebacuje na trofazno punjenje."
 targetChargeActive = "Aktivan je plan punjenja. Procijenjeni završetak za {duration}."
 targetChargePlanned = "Planirano punjenje počinje za {duration}."
 targetChargeWaitForVehicle = "Planirano punjenje je spremno. Čeka se na vozilo…"
+unknown = ""
 vehicleLimit = "Granica vozila."
 vehicleLimitReached = "Dosegnuto ograničenje na vozilu."
 waitForVehicle = "Spremno. Čeka se na vozilo …"
diff --git a/i18n/hu.toml b/i18n/hu.toml
index c11785a0..3cb54677 100644
--- a/i18n/hu.toml
+++ b/i18n/hu.toml
@@ -410,8 +410,6 @@ batteryCharge = "Energiatároló töltés"
 batteryDischarge = "Energiatároló kisütés"
 batteryHold = "Energiatároló (lezárva)"
 batteryTooltip = "{energy} / {total} ({soc})"
-cheapBatteryGridCharge = "olcsó hálózati energia"
-cleanBatteryGridCharge = "tiszta hálózati energia"
 gridImport = "Hálózatból import"
 homePower = "Fogyasztás"
 loadpoints = "Töltő| Töltő | {count} töltő"
diff --git a/i18n/it.toml b/i18n/it.toml
index 4b91132b..92d924f8 100644
--- a/i18n/it.toml
+++ b/i18n/it.toml
@@ -422,8 +422,6 @@ batteryCharge = "Carica della batteria"
 batteryDischarge = "Scarico della batteria"
 batteryHold = "Batteria (locked)"
 batteryTooltip = "{energy} di {total} ({soc})"
-cheapBatteryGridCharge = "energia di rete a basso costo"
-cleanBatteryGridCharge = "energia di rete pulita"
 gridImport = "Uso della rete"
 homePower = "Consumo"
 loadpoints = "Caricabatterie| Caricabatterie | {count} caricabatterie"
diff --git a/i18n/lb.toml b/i18n/lb.toml
index ab740e99..3c4a87f7 100644
--- a/i18n/lb.toml
+++ b/i18n/lb.toml
@@ -421,8 +421,6 @@ batteryCharge = "Batterie oplueden"
 batteryDischarge = "Batterie entlueden"
 batteryHold = "Batterie (gespäert)"
 batteryTooltip = "{energy} vun {total} ({soc})"
-cheapBatteryGridCharge = "Gënschtege Stroum vum Netz"
-cleanBatteryGridCharge = "grénge Stroum vum Netz"
 gridImport = "Verbrauch vum Stroumnetz"
 homePower = "Verbrauch"
 loadpoints = "Wallbox | Wallbox | {count} Wallboxen"
diff --git a/i18n/lt.toml b/i18n/lt.toml
index 9b54f212..4dc18dc0 100644
--- a/i18n/lt.toml
+++ b/i18n/lt.toml
@@ -428,8 +428,6 @@ batteryCharge = "Kaupiklis įkraunamas"
 batteryDischarge = "Kaupiklis iškraunamas"
 batteryHold = "Kaupiklis (užblokuotas)"
 batteryTooltip = "{energy} iš {total} ({soc})"
-cheapBatteryGridCharge = "pigi tinklo energija"
-cleanBatteryGridCharge = "švari tinklo energija"
 gridImport = "Iš tinklo"
 homePower = "Namo suvartojimas"
 loadpoints = "Įkroviklis | Įkroviklis | {count} įkrovikliai"
diff --git a/i18n/nl.toml b/i18n/nl.toml
index 1d393471..f33b818e 100644
--- a/i18n/nl.toml
+++ b/i18n/nl.toml
@@ -425,8 +425,6 @@ batteryCharge = "Batterij opladen"
 batteryDischarge = "Batterij ontladen"
 batteryHold = "Batterij (aanhouden)"
 batteryTooltip = "{energy} van {total} ({soc})"
-cheapBatteryGridCharge = "goedkope net-energie"
-cleanBatteryGridCharge = "schone net-energie"
 gridImport = "Netafname"
 homePower = "Consumptie"
 loadpoints = "Lader| Lader | {count} laders"
diff --git a/i18n/pt.toml b/i18n/pt.toml
index d8401e66..a512b1a7 100644
--- a/i18n/pt.toml
+++ b/i18n/pt.toml
@@ -424,8 +424,6 @@ batteryCharge = "Carrego de bateria"
 batteryDischarge = "Descarrego de bateria"
 batteryHold = "Bateria (suspensa)"
 batteryTooltip = "{energy} de {total} ({soc})"
-cheapBatteryGridCharge = "energia de rede barata"
-cleanBatteryGridCharge = "energia de rede verde"
 gridImport = "Consumo de rede"
 homePower = "Consumo"
 loadpoints = "Carregador | Carregador | {count} Carregadores"
diff --git a/i18n/sl.toml b/i18n/sl.toml
index d44c5349..5e8bcb37 100644
--- a/i18n/sl.toml
+++ b/i18n/sl.toml
@@ -417,8 +417,6 @@ batteryCharge = "Polnjenje baterije"
 batteryDischarge = "Praznjenje baterije"
 batteryHold = "Baterija (zaklenjena)"
 batteryTooltip = "{energy} od {total} ({soc})"
-cheapBatteryGridCharge = "poceni omrežna energija"
-cleanBatteryGridCharge = "energija iz čistega omrežja"
 gridImport = "Uvoz iz omrežja"
 homePower = "Poraba objekta"
 loadpoints = "Polnilnica | Polnilnici | {count} polnilnih mest"
diff --git a/i18n/sv.toml b/i18n/sv.toml
index e5c35ea1..d16e0142 100644
--- a/i18n/sv.toml
+++ b/i18n/sv.toml
@@ -423,8 +423,6 @@ batteryCharge = "Batteri laddas"
 batteryDischarge = "Batteri laddas ur"
 batteryHold = "Batteri (låst)"
 batteryTooltip = "{energy} av {total} ({soc})"
-cheapBatteryGridCharge = "billig nät-energi"
-cleanBatteryGridCharge = "grön nät-energi"
 gridImport = "Import från elnät"
 homePower = "Konsumtion"
 loadpoints = "Laddare | Laddare | {count} laddare"
diff --git a/i18n/ta.toml b/i18n/ta.toml
index f02a059c..b2f97f2a 100644
--- a/i18n/ta.toml
+++ b/i18n/ta.toml
@@ -409,8 +409,6 @@ batteryCharge = "Battery charging"
 batteryDischarge = "Battery discharging"
 batteryHold = "Battery (locked)"
 batteryTooltip = "{energy} of {total} ({soc})"
-cheapBatteryGridCharge = "cheap வலைவாய் energy"
-cleanBatteryGridCharge = "clean வலைவாய் energy"
 gridImport = "Grid use"
 homePower = "Consumption"
 loadpoints = "Charger| மின்னூட்டி | {count} chargers"
diff --git a/i18n/tr.toml b/i18n/tr.toml
index bd6e2e80..24e48355 100644
--- a/i18n/tr.toml
+++ b/i18n/tr.toml
@@ -402,7 +402,7 @@ titleMinSoc = "Asgari doldurma"
 titleTargetCharge = "Ayrılış"
 unsavedChanges = "Kaydedilmemiş değişiklikler var. Şimdi uygulansın mı?"
 update = "Uygula"
-weekdays = "“Hafta günleri”"
+weekdays = "Günler"
 
 [main.energyflow]
 battery = "Batarya"
@@ -410,8 +410,6 @@ batteryCharge = "Batarya doldurma"
 batteryDischarge = "Batarya boşaltma"
 batteryHold = "Batarya (kilitli)"
 batteryTooltip = "{total} ({soc})'ın {energy}'ı"
-cheapBatteryGridCharge = "“ucuz şebeke enerjisi”"
-cleanBatteryGridCharge = "“temiz şebeke enerjisi”"
 gridImport = "Şebeke kullanımı"
 homePower = "Tüketim"
 loadpoints = "Doldurma Cihazı| Doldurma Cihazı | {count} doldurma cihazları"
@@ -576,6 +574,7 @@ scale3p = "Birazdan 3 fazla doldurmaya yükseltilecek."
 targetChargeActive = "Doldurma planı yürürlükte. Tahmini bitiş süresi {duration} içerisinde."
 targetChargePlanned = "Doldurma planı {duration} içerisinde başlayacak."
 targetChargeWaitForVehicle = "Doldurma planı hazır. Araç bekleniyor…"
+unknown = ""
 vehicleLimit = "Araç sınırı."
 vehicleLimitReached = "Araç sınırına ulaşıldı."
 vehicleTargetReached = "Araç sınırı {soc} ulaşıldı."
diff --git a/meter/e3dc.go b/meter/e3dc.go
index 89c53019..1e4fc45e 100644
--- a/meter/e3dc.go
+++ b/meter/e3dc.go
@@ -176,7 +176,7 @@ func (m *E3dc) setBatteryMode(mode api.BatteryMode) error {
 	case api.BatteryCharge:
 		res, err = m.conn.SendMultiple([]rscp.Message{
 			e3dcDischargeBatteryLimit(false, 0),
-			e3dcBatteryCharge(10000), // 10kWh
+			e3dcBatteryCharge(50000), // max. 50kWh
 		})
 
 	default:
diff --git a/provider/charger.go b/provider/charger.go
new file mode 100644
index 00000000..6696f331
--- /dev/null
+++ b/provider/charger.go
@@ -0,0 +1,87 @@
+package provider
+
+import (
+	"context"
+
+	"github.com/evcc-io/evcc/api"
+	charger "github.com/evcc-io/evcc/charger/config"
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/config"
+	"github.com/spf13/cast"
+)
+
+type switchChargerProvider struct {
+	charger api.Charger
+}
+
+func init() {
+	registry.AddCtx("charger", NewChargerEnableFromConfig)
+}
+
+// NewChargerEnableFromConfig creates type conversion provider
+func NewChargerEnableFromConfig(ctx context.Context, other map[string]interface{}) (Provider, error) {
+	var cc struct {
+		Config config.Typed
+	}
+
+	if err := util.DecodeOther(other, &cc); err != nil {
+		return nil, err
+	}
+
+	charger, err := charger.NewFromConfig(ctx, cc.Config.Type, cc.Config.Other)
+	if err != nil {
+		return nil, err
+	}
+
+	o := &switchChargerProvider{
+		charger: charger,
+	}
+
+	return o, nil
+}
+
+var _ FloatProvider = (*switchChargerProvider)(nil)
+
+func (o *switchChargerProvider) FloatGetter() (func() (float64, error), error) {
+	return func() (float64, error) {
+		v, err := o.charger.Enabled()
+		return cast.ToFloat64(v), err
+	}, nil
+}
+
+var _ IntProvider = (*switchChargerProvider)(nil)
+
+func (o *switchChargerProvider) IntGetter() (func() (int64, error), error) {
+	return func() (int64, error) {
+		v, err := o.charger.Enabled()
+		return cast.ToInt64(v), err
+	}, nil
+}
+
+var _ SetIntProvider = (*switchChargerProvider)(nil)
+
+func (o *switchChargerProvider) IntSetter(param string) (func(int64) error, error) {
+	return func(val int64) error {
+		b, err := cast.ToBoolE(val)
+		if err != nil {
+			return err
+		}
+		return o.charger.Enable(b)
+	}, nil
+}
+
+var _ BoolProvider = (*switchChargerProvider)(nil)
+
+func (o *switchChargerProvider) BoolGetter() (func() (bool, error), error) {
+	return func() (bool, error) {
+		return o.charger.Enabled()
+	}, nil
+}
+
+var _ SetBoolProvider = (*switchChargerProvider)(nil)
+
+func (o *switchChargerProvider) BoolSetter(param string) (func(bool) error, error) {
+	return func(val bool) error {
+		return o.charger.Enable(val)
+	}, nil
+}
diff --git a/provider/const.go b/provider/const.go
index 638e048c..692f15dc 100644
--- a/provider/const.go
+++ b/provider/const.go
@@ -33,19 +33,9 @@ func NewConstFromConfig(ctx context.Context, other map[string]interface{}) (Prov
 		return nil, err
 	}
 
-	pipe, err := pipeline.New(nil, cc.Settings)
-	if err != nil {
-		return nil, err
-	}
-
-	b, err := pipe.Process([]byte(cc.Value))
-	if err != nil {
-		return nil, err
-	}
-
 	p := &constProvider{
 		ctx: ctx,
-		str: string(b),
+		str: cc.Value,
 		set: cc.Set,
 	}
 
diff --git a/provider/pipeline/pipeline.go b/provider/pipeline/pipeline.go
index fd7fc68d..1e2e1bc1 100644
--- a/provider/pipeline/pipeline.go
+++ b/provider/pipeline/pipeline.go
@@ -21,6 +21,7 @@ type Pipeline struct {
 	re         *regexp.Regexp
 	jq         *gojq.Query
 	allowEmpty bool
+	quote      bool
 	dflt       string
 	unpack     string
 	decode     string
@@ -28,6 +29,7 @@ type Pipeline struct {
 
 type Settings struct {
 	AllowEmpty bool
+	Quote      bool
 	Regex      string
 	Default    string
 	Jq         string
@@ -39,6 +41,7 @@ func New(log *util.Logger, cc Settings) (*Pipeline, error) {
 	p := &Pipeline{
 		log:        log,
 		allowEmpty: cc.AllowEmpty,
+		quote:      cc.Quote,
 	}
 
 	var err error
@@ -191,6 +194,9 @@ func (p *Pipeline) Process(in []byte) ([]byte, error) {
 	}
 
 	if p.jq != nil {
+		if p.quote {
+			b = []byte(fmt.Sprintf("%q", string(b)))
+		}
 		v, err := jq.Query(p.jq, b)
 		if err != nil {
 			return b, backoff.Permanent(err)
diff --git a/provider/timeseries.go b/provider/timeseries.go
new file mode 100644
index 00000000..bcae29b7
--- /dev/null
+++ b/provider/timeseries.go
@@ -0,0 +1,40 @@
+package provider
+
+import (
+	"context"
+	"encoding/json"
+	"time"
+
+	"github.com/evcc-io/evcc/api"
+	"github.com/jinzhu/now"
+)
+
+type timeseriesProvider struct{}
+
+func init() {
+	registry.AddCtx("timeseries", TimeSeriesFromConfig)
+}
+
+// TimeSeriesFromConfig creates timeseries provider
+func TimeSeriesFromConfig(_ context.Context, _ map[string]interface{}) (Provider, error) {
+	return new(timeseriesProvider), nil
+}
+
+var _ StringProvider = (*timeseriesProvider)(nil)
+
+func (p *timeseriesProvider) StringGetter() (func() (string, error), error) {
+	return func() (string, error) {
+		res := make(api.Rates, 48)
+		ts := now.BeginningOfHour()
+		for i := 0; i < 48; i++ {
+			res[i] = api.Rate{
+				Start: ts,
+				End:   ts.Add(time.Hour),
+			}
+			ts = ts.Add(time.Hour)
+		}
+
+		b, err := json.Marshal(res)
+		return string(b), err
+	}, nil
+}
diff --git a/templates/definition/charger/ac-thor.yaml b/templates/definition/charger/ac-thor.yaml
new file mode 100644
index 00000000..534b287c
--- /dev/null
+++ b/templates/definition/charger/ac-thor.yaml
@@ -0,0 +1,14 @@
+template: ac-thor
+products:
+  - brand: my-PV
+    description:
+      generic: AC THOR, AC THOR 9s
+group: heating
+requirements:
+  evcc: ["sponsorship"]
+params:
+  - name: modbus
+    choice: ["tcpip"]
+render: |
+  type: ac-thor
+  {{- include "modbus" . }}
diff --git a/templates/definition/charger/demo-charger.yaml b/templates/definition/charger/demo-charger.yaml
index 6d959d9c..9ced9183 100644
--- a/templates/definition/charger/demo-charger.yaml
+++ b/templates/definition/charger/demo-charger.yaml
@@ -13,7 +13,8 @@ params:
     description:
       de: Ladezustand
       en: Charge status
-    validvalues: [A, B, C, D, E, F]
+    type: choice
+    choice: [A, B, C, D, E, F]
     default: A
   - name: power
     description:
diff --git a/templates/definition/charger/stiebel-lwa.yaml b/templates/definition/charger/stiebel-lwa.yaml
index b10ce469..e2cf3ada 100644
--- a/templates/definition/charger/stiebel-lwa.yaml
+++ b/templates/definition/charger/stiebel-lwa.yaml
@@ -13,7 +13,8 @@ params:
   - name: modbus
     choice: ["tcpip"]
   - name: tempsource
-    validvalues: ["", "warmwater"]
+    type: choice
+    choice: ["", "warmwater"]
     description:
       de: "Temperaturquelle"
       en: "Temperature source"
diff --git a/templates/definition/charger/stiebel-wpm.yaml b/templates/definition/charger/stiebel-wpm.yaml
index 78c1ca35..0d7a2692 100644
--- a/templates/definition/charger/stiebel-wpm.yaml
+++ b/templates/definition/charger/stiebel-wpm.yaml
@@ -10,7 +10,8 @@ params:
   - name: modbus
     choice: ["tcpip"]
   - name: tempsource
-    validvalues: ["", "warmwater", "buffer"]
+    type: choice
+    choice: ["", "warmwater", "buffer"]
     description:
       de: "Temperaturquelle"
       en: "Temperature source"
diff --git a/templates/definition/charger/weishaupt-wpm.yaml b/templates/definition/charger/weishaupt-wpm.yaml
new file mode 100644
index 00000000..1a45bb1a
--- /dev/null
+++ b/templates/definition/charger/weishaupt-wpm.yaml
@@ -0,0 +1,113 @@
+template: weishaupt-wpm
+products:
+  - brand: Weishaupt
+    description:
+      generic: WPM (SG Ready)
+group: heating
+# requirements:
+#   evcc: ["sponsorship"]
+params:
+  - name: modbus
+    choice: ["tcpip"]
+  - name: tempsource
+    type: choice
+    choice: ["", "warmwater", "buffer"]
+    description:
+      de: "Temperaturquelle"
+      en: "Temperature source"
+render: |
+  type: sgready
+  getmode:
+    source: map
+    values:
+      8:  1 # ABTAUEN
+      10: 3 # EVU_SPERRE
+      14: 2 # ERHOEHTER_BETRIEB
+      18: 3 # FROSTSCHUTZ
+      19: 1 # HEIZBETRIEB
+    get:
+      source: modbus
+      {{- include "modbus" . | indent 4 }}
+      register:
+        address: 30006
+        type: input
+        encoding: uint16
+  setmode:
+    source: switch
+    switch:
+    - case: 1 # normal
+      set:
+        source: sequence
+        set:
+        - source: const
+          value: 0
+          set:
+            source: modbus
+            {{- include "modbus" . | indent 10 }}
+            register:
+              address: 45101 # 4001
+              type: writesingle
+              encoding: uint16
+        - source: const
+          value: 0
+          set:
+            source: modbus
+            {{- include "modbus" . | indent 10 }}
+            register:
+              address: 45102 # 4002
+              type: writesingle
+              encoding: uint16
+    - case: 2 # boost
+      set:
+        source: sequence
+        set:
+        - source: const
+          value: 1
+          set:
+            source: modbus
+            {{- include "modbus" . | indent 10 }}
+            register:
+              address: 45101
+              type: writesingle
+              encoding: uint16
+        - source: const
+          value: 0
+          set:
+            source: modbus
+            {{- include "modbus" . | indent 10 }}
+            register:
+              address: 45102
+              type: writesingle
+              encoding: uint16
+    - case: 3 # dimm
+      set:
+        source: sequence
+        set:
+        - source: const
+          value: 0
+          set:
+            source: modbus
+            {{- include "modbus" . | indent 10 }}
+            register:
+              address: 45101
+              type: writesingle
+              encoding: uint16
+        - source: const
+          value: 1
+          set:
+            source: modbus
+            {{- include "modbus" . | indent 10 }}
+            register:
+              address: 45102
+              type: writesingle
+              encoding: uint16
+  {{- if .tempsource }}
+  temp:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: {{ if eq .tempsource "warmwater" -}} 32102 {{ else }} 33104 {{- end }} # 32102 = WW; 33104 Vorlauf
+      type: input
+      encoding: int16
+    scale: 0.1
+  {{- end }}
diff --git a/templates/definition/common-schema.json b/templates/definition/common-schema.json
index 3d03aae5..0e8c41a1 100644
--- a/templates/definition/common-schema.json
+++ b/templates/definition/common-schema.json
@@ -199,7 +199,6 @@
           "enum": [
             "string",
             "bool",
-            "number",
             "float",
             "int",
             "list",
@@ -207,12 +206,6 @@
             "duration"
           ]
         },
-        "validvalues": {
-          "type": "array",
-          "items": {
-            "type": "string"
-          }
-        },
         "choice": {
           "type": "array",
           "items": {
diff --git a/templates/definition/defaults-schema.json b/templates/definition/defaults-schema.json
index a444cfd9..aede6d99 100644
--- a/templates/definition/defaults-schema.json
+++ b/templates/definition/defaults-schema.json
@@ -50,6 +50,7 @@
       },
       "required": [
         "generic",
+        "heating",
         "sockets",
         "switchsockets",
         "scooter"
diff --git a/templates/definition/devices-schema.json b/templates/definition/devices-schema.json
index 2d6f47c7..efedc8b1 100644
--- a/templates/definition/devices-schema.json
+++ b/templates/definition/devices-schema.json
@@ -65,7 +65,9 @@
           "type": "string"
         }
       },
-      "required": ["name"],
+      "required": [
+        "name"
+      ],
       "title": "Template"
     },
     "Product": {
@@ -80,14 +82,28 @@
         }
       },
       "anyOf": [
-        {"required": ["brand"]},
-        {"required": ["description"]}
+        {
+          "required": [
+            "brand"
+          ]
+        },
+        {
+          "required": [
+            "description"
+          ]
+        }
       ],
       "title": "Product"
     },
     "Groups": {
       "type": "string",
-      "enum": ["generic", "switchsockets", "scooter", "sockets"]
+      "enum": [
+        "generic",
+        "heating",
+        "switchsockets",
+        "scooter",
+        "sockets"
+      ]
     },
     "Linked": {
       "type": "object",
@@ -99,7 +115,11 @@
         },
         "usage": {
           "type": "string",
-          "enum": ["grid", "pv", "battery"]
+          "enum": [
+            "grid",
+            "pv",
+            "battery"
+          ]
         },
         "multiple": {
           "type": "boolean"
@@ -109,11 +129,21 @@
           "minLength": 1
         }
       },
-      "required": ["template", "usage"]
+      "required": [
+        "template",
+        "usage"
+      ]
     },
     "Capability": {
       "type": "string",
-      "enum": ["1p3p", "iso151182", "rfid", "smahems", "mA"]
+      "enum": [
+        "1p3p",
+        "battery-control",
+        "iso151182",
+        "rfid",
+        "smahems",
+        "mA"
+      ]
     }
   }
 }
\ No newline at end of file
diff --git a/templates/definition/meter/enphase.yaml b/templates/definition/meter/enphase.yaml
index 581e4c67..7d22b15a 100644
--- a/templates/definition/meter/enphase.yaml
+++ b/templates/definition/meter/enphase.yaml
@@ -30,6 +30,34 @@ render: |
     insecure: true
     {{- end }}
     jq: .consumption[] | select(.measurementType == "net-consumption").wNow
+  currents:
+    - source: http
+      uri: http://{{ .host }}/production.json?details=1
+      {{- if .token }}
+      auth:
+        type: bearer
+        password: {{ .token }}
+      insecure: true
+      {{- end }}
+      jq: if .consumption[] | select(.measurementType == "net-consumption").activeCount >= 1 then .consumption[] | select(.measurementType == "net-consumption").lines[0].rmsCurrent else 0 end
+    - source: http
+      uri: http://{{ .host }}/production.json?details=1
+      {{- if .token }}
+      auth:
+        type: bearer
+        password: {{ .token }}
+      insecure: true
+      {{- end }}
+      jq: if .consumption[] | select(.measurementType == "net-consumption").activeCount >= 1 then .consumption[] | select(.measurementType == "net-consumption").lines[1].rmsCurrent else 0 end
+    - source: http
+      uri: http://{{ .host }}/production.json?details=1
+      {{- if .token }}
+      auth:
+        type: bearer
+        password: {{ .token }}
+      insecure: true
+      {{- end }}
+      jq: if .consumption[] | select(.measurementType == "net-consumption").activeCount >= 1 then .consumption[] | select(.measurementType == "net-consumption").lines[2].rmsCurrent else 0 end
   {{- end }}
   {{- if eq .usage "pv" }}
   power:
@@ -53,6 +81,34 @@ render: |
     {{- end }}
     jq: if (.production | length) > 1 and (.production[] | select(.measurementType == "production").activeCount >= 1) then .production[] | select(.measurementType == "production").whLifetime else .production[] | select(.type == "inverters").whLifetime end
     scale: 0.001
+  currents:
+    - source: http
+      uri: http://{{ .host }}/production.json?details=1
+      {{- if .token }}
+      auth:
+        type: bearer
+        password: {{ .token }}
+      insecure: true
+      {{- end }}
+      jq: if .production[] | select(.measurementType == "production").activeCount >= 1 then .production[] | select(.measurementType == "production").lines[0].rmsCurrent else 0 end
+    - source: http
+      uri: http://{{ .host }}/production.json?details=1
+      {{- if .token }}
+      auth:
+        type: bearer
+        password: {{ .token }}
+      insecure: true
+      {{- end }}
+      jq: if .production[] | select(.measurementType == "production").activeCount >= 1 then .production[] | select(.measurementType == "production").lines[1].rmsCurrent else 0 end
+    - source: http
+      uri: http://{{ .host }}/production.json?details=1
+      {{- if .token }}
+      auth:
+        type: bearer
+        password: {{ .token }}
+      insecure: true
+      {{- end }}
+      jq: if .production[] | select(.measurementType == "production").activeCount >= 1 then .production[] | select(.measurementType == "production").lines[2].rmsCurrent else 0 end
   {{- end }}
   {{- if eq .usage "battery" }}
   power:
diff --git a/templates/definition/meter/goodwe-hybrid.yaml b/templates/definition/meter/goodwe-hybrid.yaml
index 5cb263fd..de984aa8 100644
--- a/templates/definition/meter/goodwe-hybrid.yaml
+++ b/templates/definition/meter/goodwe-hybrid.yaml
@@ -13,9 +13,8 @@ params:
     id: 247
   - name: battery
     default: 1
-    validvalues:
-      - 1
-      - 2
+    type: choice
+    choice: [1, 2]
   - name: capacity
     advanced: true
   - name: maxacpower
diff --git a/templates/definition/meter/kostal-plenticore-gen2.yaml b/templates/definition/meter/kostal-plenticore-gen2.yaml
index c8f5efe7..4319a417 100644
--- a/templates/definition/meter/kostal-plenticore-gen2.yaml
+++ b/templates/definition/meter/kostal-plenticore-gen2.yaml
@@ -34,7 +34,8 @@ params:
     description:
       de: Byte-Reihenfolge (Little/Big)
       en: Endianness (Little/Big)
-    validvalues: ["big", "little"]
+    type: choice
+    choice: ["little", "big"]
     default: little
     advanced: true
   - name: capacity
diff --git a/templates/definition/meter/kostal-plenticore.yaml b/templates/definition/meter/kostal-plenticore.yaml
index 815adf99..caac55c6 100644
--- a/templates/definition/meter/kostal-plenticore.yaml
+++ b/templates/definition/meter/kostal-plenticore.yaml
@@ -30,7 +30,8 @@ params:
     description:
       de: Byte-Reihenfolge (Little/Big)
       en: Endianness (Little/Big)
-    validvalues: ["big", "little"]
+    type: choice
+    choice: ["little", "big"]
     default: little
     advanced: true
   - name: capacity
diff --git a/templates/definition/meter/saj-r5.yaml b/templates/definition/meter/saj-r5.yaml
new file mode 100644
index 00000000..c434a851
--- /dev/null
+++ b/templates/definition/meter/saj-r5.yaml
@@ -0,0 +1,31 @@
+template: saj-r5
+products:
+  - brand: SAJ
+    description:
+      generic: R5 Series Solar Inverter
+params:
+  - name: usage
+    choice: ["pv"]
+  - name: modbus
+    choice: ["rs485"]
+    baudrate: 9600
+    comset: 8N1
+render: |
+  type: custom
+  {{- if eq .usage "pv" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 275 # 0x0113 Active power of inverter total output
+      type: holding
+      decode: uint16
+  energy:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 305 # 0x0131 Total Energy
+      type: holding
+      decode: uint32
+    scale: 0.01
+  {{- end }}
diff --git a/templates/definition/meter/senec-home.yaml b/templates/definition/meter/senec-home.yaml
index c22b7f27..314e4669 100644
--- a/templates/definition/meter/senec-home.yaml
+++ b/templates/definition/meter/senec-home.yaml
@@ -11,9 +11,7 @@ params:
   - name: host
   - name: schema
     type: choice
-    validvalues:
-      - https
-      - http
+    choice: ["https", "http"]
     default: https
   - name: capacity
     advanced: true
diff --git a/templates/definition/meter/zendure.yaml b/templates/definition/meter/zendure.yaml
index 5f53f2dc..0136965a 100644
--- a/templates/definition/meter/zendure.yaml
+++ b/templates/definition/meter/zendure.yaml
@@ -19,8 +19,8 @@ params:
       en: "You can find this in the Zendure App in the settings of the device"
   - name: region
     type: choice
+    choice: ["EU", "Global"]
     default: EU
-    validvalues: ["EU", "Global"]
   - name: capacity
     default: 2
     advanced: true
diff --git a/templates/definition/tariff/awattar.yaml b/templates/definition/tariff/awattar.yaml
index 9e9794a8..27c795a5 100644
--- a/templates/definition/tariff/awattar.yaml
+++ b/templates/definition/tariff/awattar.yaml
@@ -9,7 +9,8 @@ group: price
 params:
   - name: region
     example: AT
-    validvalues: ["DE", "AT"]
+    type: choice
+    choice: ["DE", "AT"]
   - preset: tariff-base
 render: |
   type: awattar
diff --git a/templates/definition/tariff/elering.yaml b/templates/definition/tariff/elering.yaml
index faf7f53a..9656c76d 100644
--- a/templates/definition/tariff/elering.yaml
+++ b/templates/definition/tariff/elering.yaml
@@ -12,7 +12,8 @@ group: price
 params:
   - name: region
     example: ee
-    validvalues: ["ee", "lt", "lv", "fi"]
+    type: choice
+    choice: ["ee", "lt", "lv", "fi"]
   - preset: tariff-base
 render: |
   type: elering
diff --git a/templates/definition/tariff/energinet.yaml b/templates/definition/tariff/energinet.yaml
index 3f45119e..280877aa 100644
--- a/templates/definition/tariff/energinet.yaml
+++ b/templates/definition/tariff/energinet.yaml
@@ -10,7 +10,8 @@ group: price
 params:
   - name: region
     example: dk1
-    validvalues: ["dk1", "dk2"]
+    type: choice
+    choice: ["dk1", "dk2"]
   - preset: tariff-base
 render: |
   type: energinet
diff --git a/templates/definition/tariff/energy-charts-api.yaml b/templates/definition/tariff/energy-charts-api.yaml
index a6038223..5b364ca0 100644
--- a/templates/definition/tariff/energy-charts-api.yaml
+++ b/templates/definition/tariff/energy-charts-api.yaml
@@ -9,9 +9,9 @@ requirements:
 group: price
 params:
   - name: bzn
-    type: string
+    type: choice
     required: true
-    validvalues:
+    choice:
       [
         "AT",
         "BE",
diff --git a/templates/definition/tariff/enever.yaml b/templates/definition/tariff/enever.yaml
index 99e131b0..366bc458 100644
--- a/templates/definition/tariff/enever.yaml
+++ b/templates/definition/tariff/enever.yaml
@@ -11,7 +11,8 @@ params:
   - name: token
     required: true
   - name: provider
-    validvalues:
+    type: choice
+    choice:
       [
         "",
         "AA",
diff --git a/templates/definition/tariff/nordpool.yaml b/templates/definition/tariff/nordpool.yaml
index dc2e3132..7490ab33 100644
--- a/templates/definition/tariff/nordpool.yaml
+++ b/templates/definition/tariff/nordpool.yaml
@@ -10,7 +10,8 @@ group: price
 params:
   - name: region
     example: GER
-    validvalues:
+    type: choice
+    choice:
       [
         "EE",
         "LT",
@@ -38,7 +39,8 @@ params:
       ]
   - name: currency
     default: EUR
-    validvalues: ["DKK", "EUR", "NOK", "PLN", "RON", "SEK"]
+    type: choice
+    choice: ["DKK", "EUR", "NOK", "PLN", "RON", "SEK"]
   - preset: tariff-base
 render: |
   type: custom
diff --git a/templates/definition/tariff/octopus-productcode.yaml b/templates/definition/tariff/octopus-productcode.yaml
index 5b033d72..bc810861 100644
--- a/templates/definition/tariff/octopus-productcode.yaml
+++ b/templates/definition/tariff/octopus-productcode.yaml
@@ -13,8 +13,8 @@ params:
       de: "Der Tarifcode für Ihren Energievertrag. Stellen Sie sicher, dass dieser auf Ihren Importtarifcode eingestellt ist."
       en: "The tariff code for your energy contract. Make sure this is set to your import tariff code."
   - name: region
-    type: string
-    validvalues: ["A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P"]
+    type: choice
+    choice: ["A", "B", "C", "D", "E", "F", "G", "H", "J", "K", "L", "M", "N", "P"]
     required: true
     help:
       de: "Die DNO-Region, in der Sie sich befinden. Weitere Informationen: https://www.energy-stats.uk/dno-region-codes-explained/"
diff --git a/templates/definition/tariff/spottyenergy.yaml b/templates/definition/tariff/spottyenergy.yaml
index 61b4909b..b0c85960 100644
--- a/templates/definition/tariff/spottyenergy.yaml
+++ b/templates/definition/tariff/spottyenergy.yaml
@@ -15,7 +15,8 @@ params:
       en: "You can get your contract id from the customer portal https://i.spottyenergie.at/"
   - name: pricetype
     default: CONSUMPTION
-    validvalues: ["MARKET", "CONSUMPTION", "GENERATION"]
+    type: choice
+    choice: ["MARKET", "CONSUMPTION", "GENERATION"]
     required: true
     help:
       de: "Preistyp, entweder Börsenpreis, Verbrauchspreis oder Einspeisevergütung (falls vereinbart), siehe https://www.spottyenergie.at/blog/energie-smart-produzieren"
diff --git a/templates/definition/vehicle/bmw.yaml b/templates/definition/vehicle/bmw.yaml
index ec572189..be57f390 100644
--- a/templates/definition/vehicle/bmw.yaml
+++ b/templates/definition/vehicle/bmw.yaml
@@ -4,9 +4,9 @@ products:
 requirements:
   description:
     de: |
-      Benötigt `hcaptcha` Token. Dieses muss einmalig unter https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html generiert werden. Das Token ist nur für kurze Zeit gültig. Bitte möglichst schnell nach Generierung in die Konfiguration kopieren und evcc starten.
+      Benötigt `hcaptcha` Token. Dieses muss einmalig unter [bimmer-connected.readthedocs.io](https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html) generiert werden. Das Token ist nur für kurze Zeit gültig. Bitte möglichst schnell nach Generierung in die Konfiguration kopieren und evcc starten.
     en: |
-      Requires `hcaptcha` token. This must be generated once at https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html. The token is only valid for a short time. Please copy it into the configuration and start evcc as soon as possible after generation.
+      Requires `hcaptcha` token. This must be generated once at [bimmer-connected.readthedocs.io](https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html). The token is only valid for a short time. Please copy it into the configuration and start evcc as soon as possible after generation.
 params:
   - preset: vehicle-base
   - name: vin
@@ -15,7 +15,8 @@ params:
     description:
       de: Region
       en: Region
-    validvalues: ["NA", "EU"]
+    type: choice
+    choice: ["EU", "NA"]
     default: EU
     advanced: true
   - name: hcaptcha
diff --git a/templates/definition/vehicle/ford.yaml b/templates/definition/vehicle/ford.yaml
index 088f9692..ddab0d27 100644
--- a/templates/definition/vehicle/ford.yaml
+++ b/templates/definition/vehicle/ford.yaml
@@ -7,8 +7,9 @@ params:
   - name: vin
     example: WF0FXX...
   - name: domain
+    type: choice
+    choice: ["com", "de"]
     default: com
-    validvalues: ["de", "com"]
 render: |
   type: ford
   {{ include "vehicle-base" . }}
diff --git a/templates/definition/vehicle/mercedes.yaml b/templates/definition/vehicle/mercedes.yaml
index 83914fbf..9e3f07f8 100644
--- a/templates/definition/vehicle/mercedes.yaml
+++ b/templates/definition/vehicle/mercedes.yaml
@@ -13,7 +13,8 @@ params:
     required: true
   - name: region
     required: true
-    validvalues: [EMEA, APAC, NORAM]
+    type: choice
+    choice: ["EMEA", "APAC", "NORAM"]
     default: EMEA
   - name: accessToken
     required: true
diff --git a/templates/definition/vehicle/mg.yaml b/templates/definition/vehicle/mg.yaml
index 2a55b41c..498f1cf0 100644
--- a/templates/definition/vehicle/mg.yaml
+++ b/templates/definition/vehicle/mg.yaml
@@ -9,7 +9,8 @@ params:
     description:
       de: Region
       en: Region
-    validvalues: ["EU", "AU"]
+    type: choice
+    choice: ["EU", "AU"]
     default: EU
     advanced: true
 render: |
diff --git a/templates/definition/vehicle/mini.yaml b/templates/definition/vehicle/mini.yaml
index e72ad4f0..8611f871 100644
--- a/templates/definition/vehicle/mini.yaml
+++ b/templates/definition/vehicle/mini.yaml
@@ -4,9 +4,9 @@ products:
 requirements:
   description:
     de: |
-      Benötigt `hcaptcha` Token. Dieses muss einmalig unter https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html generiert werden. Das Token ist nur für kurze Zeit gültig. Bitte möglichst schnell nach Generierung in die Konfiguration kopieren und evcc starten.
+      Benötigt `hcaptcha` Token. Dieses muss einmalig unter [bimmer-connected.readthedocs.io](https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html) generiert werden. Das Token ist nur für kurze Zeit gültig. Bitte möglichst schnell nach Generierung in die Konfiguration kopieren und evcc starten.
     en: |
-      Requires `hcaptcha` token. This must be generated once at https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html. The token is only valid for a short time. Please copy it into the configuration and start evcc as soon as possible after generation.
+      Requires `hcaptcha` token. This must be generated once at [bimmer-connected.readthedocs.io](https://bimmer-connected.readthedocs.io/en/latest/captcha/rest_of_world.html). The token is only valid for a short time. Please copy it into the configuration and start evcc as soon as possible after generation.
 params:
   - preset: vehicle-base
   - name: vin
@@ -15,7 +15,8 @@ params:
     description:
       de: Region
       en: Region
-    validvalues: ["NA", "EU"]
+    type: choice
+    choice: ["EU", "NA"]
     default: EU
     advanced: true
   - name: hcaptcha
diff --git a/templates/definition/vehicle/smart.yaml b/templates/definition/vehicle/smart.yaml
index fb8d12e2..dd6b6613 100644
--- a/templates/definition/vehicle/smart.yaml
+++ b/templates/definition/vehicle/smart.yaml
@@ -3,8 +3,36 @@ products:
   - brand: Smart
     description:
       generic: EQ
+requirements:
+  description:
+    de: |
+      Benötigt `access` und `refresh` Tokens. Diese können über den Befehl `evcc token [name]` generiert werden.
+    en: |
+      Requires `access` and `refresh` tokens. These can be generated with command `evcc token [name]`.
 params:
-  - preset: vehicle-base
+  - preset: vehicle-common
+  - name: user
+    required: true
+  - name: region
+    required: true
+    choice: [EMEA, APAC, NORAM]
+    default: EMEA
+  - name: accessToken
+    required: true
+    mask: true
+  - name: refreshToken
+    required: true
+    mask: true
+  - name: vin
+    example: V...
+  - name: cache
+    default: 15m
 render: |
-  type: smart
-  {{ include "vehicle-base" . }}
+  type: smart-eq
+  vin: {{ .vin }}
+  user: {{ .user }}
+  region: {{ .region }}
+  tokens:
+    access: {{ .accessToken }}
+    refresh: {{ .refreshToken }}
+  {{ include "vehicle-common" . }}
diff --git a/util/templates/defaults.yaml b/util/templates/defaults.yaml
index cef74212..1db8a105 100644
--- a/util/templates/defaults.yaml
+++ b/util/templates/defaults.yaml
@@ -222,7 +222,8 @@ params:
       de: "'de' für Deutsch und 'en' für Englisch"
       en: "'en' for English and 'de' for German"
     default: en
-    validvalues: ["de", "en"]
+    type: choice
+    choice: ["en", "de"]
   - name: icon
     description:
       de: Icon
@@ -231,7 +232,7 @@ params:
       de: "Icon in der Benutzeroberfläche"
       en: "Icon as shown in user interface"
     type: choice
-    validvalues:
+    choice:
       - car
       - bike
       - bus
diff --git a/util/templates/types.go b/util/templates/types.go
index d0fa2273..9b834192 100644
--- a/util/templates/types.go
+++ b/util/templates/types.go
@@ -187,7 +187,6 @@ type Param struct {
 	Values        []string     `json:",omitempty"` // user provided list of values e.g. for Type "list"
 	Usages        []string     `json:",omitempty"` // restrict param to these usage types, e.g. "battery" for home battery capacity
 	Type          ParamType    // string representation of the value type, "string" is default
-	ValidValues   []string     `json:",omitempty"` // list of valid values the user can provide
 	Choice        []string     `json:",omitempty"` // defines a set of choices, e.g. "grid", "pv", "battery", "charge" for "usage"
 	AllInOne      *bool        `json:"-"`          // defines if the defined usages can all be present in a single device
 
diff --git a/vehicle/mercedes.go b/vehicle/mercedes.go
index cfb461ae..9eeab3f7 100644
--- a/vehicle/mercedes.go
+++ b/vehicle/mercedes.go
@@ -1,6 +1,7 @@
 package vehicle
 
 import (
+	"errors"
 	"time"
 
 	"github.com/evcc-io/evcc/api"
@@ -15,11 +16,16 @@ type Mercedes struct {
 }
 
 func init() {
-	registry.Add("mercedes", NewMercedesFromConfig)
+	registry.Add("mercedes", func(other map[string]interface{}) (api.Vehicle, error) {
+		return newMercedesFromConfig("mercedes", other)
+	})
+	registry.Add("smart-eq", func(other map[string]interface{}) (api.Vehicle, error) {
+		return newMercedesFromConfig("smart-eq", other)
+	})
 }
 
-// NewMercedesFromConfig creates a new vehicle
-func NewMercedesFromConfig(other map[string]interface{}) (api.Vehicle, error) {
+// newMercedesFromConfig creates a new vehicle
+func newMercedesFromConfig(brand string, other map[string]interface{}) (api.Vehicle, error) {
 	cc := struct {
 		embed    `mapstructure:",squash"`
 		Tokens   Tokens
@@ -45,21 +51,28 @@ func NewMercedesFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 		cc.User = cc.Account_
 	}
 
-	log := util.NewLogger("mercedes").Redact(cc.Tokens.Access, cc.Tokens.Refresh)
+	log := util.NewLogger(brand).Redact(cc.Tokens.Access, cc.Tokens.Refresh)
 	identity, err := mercedes.NewIdentity(log, token, cc.User, cc.Region)
 	if err != nil {
 		return nil, err
 	}
 
-	v := &Mercedes{
-		embed: &cc.embed,
-	}
-
 	api := mercedes.NewAPI(log, identity)
 
-	cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles)
-	if err == nil {
-		v.Provider = mercedes.NewProvider(api, cc.VIN, cc.Cache)
+	if brand == "smart-eq" {
+		if cc.VIN == "" {
+			return nil, errors.New("missing VIN")
+		}
+	} else {
+		cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	v := &Mercedes{
+		embed:    &cc.embed,
+		Provider: mercedes.NewProvider(api, cc.VIN, cc.Cache),
 	}
 
 	return v, err
diff --git a/vehicle/smart.go b/vehicle/smart.go
deleted file mode 100644
index 15eecb99..00000000
--- a/vehicle/smart.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package vehicle
-
-import (
-	"fmt"
-	"time"
-
-	"github.com/evcc-io/evcc/api"
-	"github.com/evcc-io/evcc/util"
-	"github.com/evcc-io/evcc/vehicle/mb"
-	"github.com/evcc-io/evcc/vehicle/smart"
-)
-
-// Smart is an api.Vehicle implementation for Smart cars
-type Smart struct {
-	*embed
-	*smart.Provider
-}
-
-func init() {
-	registry.Add("smart", NewSmartFromConfig)
-}
-
-// NewSmartFromConfig creates a new vehicle
-func NewSmartFromConfig(other map[string]interface{}) (api.Vehicle, error) {
-	cc := struct {
-		embed          `mapstructure:",squash"`
-		User, Password string
-		VIN            string
-		Expiry         time.Duration
-		Cache          time.Duration
-	}{
-		Expiry: expiry,
-		Cache:  interval,
-	}
-
-	if err := util.DecodeOther(other, &cc); err != nil {
-		return nil, err
-	}
-
-	if cc.User == "" || cc.Password == "" {
-		return nil, api.ErrMissingCredentials
-	}
-
-	log := util.NewLogger("smart").Redact(cc.User, cc.Password, cc.VIN)
-
-	v := &Smart{
-		embed: &cc.embed,
-	}
-
-	identity := mb.NewIdentity(log, smart.OAuth2Config)
-	err := identity.Login(cc.User, cc.Password)
-	if err != nil {
-		return v, fmt.Errorf("login failed: %w", err)
-	}
-
-	api := smart.NewAPI(log, identity)
-
-	cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles)
-
-	if err == nil {
-		v.Provider = smart.NewProvider(log, api, cc.VIN, cc.Expiry, cc.Cache)
-	}
-
-	return v, err
-}


Test patch:
diff --git a/tests/battery-settings.spec.js b/tests/battery-settings.spec.js
index 4dba33b1..6fd3ead5 100644
--- a/tests/battery-settings.spec.js
+++ b/tests/battery-settings.spec.js
@@ -48,7 +48,11 @@ test.describe("battery settings", async () => {
     await page.getByRole("button", { name: "Close" }).click();
     await expect(page.getByTestId("battery-settings-modal")).not.toBeVisible();
     await page.getByTestId("energyflow").click();
-    await page.getByRole("button", { name: "cheap grid energy (≤ 50.0 ct)" }).click();
+    await page.getByRole("button", { name: "grid charging active (≤ 50.0 ct)" }).click();
     await expect(page.getByTestId("battery-settings-modal")).toBeVisible();
+    await page.getByLabel("Price limit").selectOption({ label: "≤ -10.0 ct/kWh" });
+    await page.getByRole("button", { name: "Close" }).click();
+    await expect(page.getByTestId("battery-settings-modal")).not.toBeVisible();
+    await expect(page.getByRole("button", { name: "grid charging when ≤ -10.0 ct" })).toBeVisible();
   });
 });