Repository: evcc-io/evcc
Pull request #18028 (2025-01-03 12:01:18)

Fiat: support start/stop charge on TWC3
As we are still waiting for OCCP support for TWC3 with no idea when (and if) it may come, I propose to add support for starting & stopping in the fiat implementation based on charge schedule when used with a TWC3 charger (or other charger relying on vehicle controller to start/stop charge).

This implementation will take control of the first schedule available from the FIAT app and use it to ask the vehicle to start or stop the charge based on the time. If evcc decides to start the charge, then all other *charge* schedule will be disabled to avoid conflict (no climate schedule will be changed).

I’ve been using this implementation since July in a custom build without any issues.


Patch:
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/charger/abl.go b/charger/abl.go
index 9a5953b2..c2f0f57c 100644
--- a/charger/abl.go
+++ b/charger/abl.go
@@ -3,7 +3,7 @@ package charger
 // LICENSE
 
 // Copyright (c) 2019-2022 andig
-// Copyright (c) 2022 premultiply
+// Copyright (c) 2022-2024 premultiply
 
 // This module is NOT covered by the MIT license. All rights reserved.
 
@@ -157,15 +157,20 @@ func (wb *ABLeMH) Status() (api.ChargeStatus, error) {
 		return api.StatusNone, err
 	}
 
-	r := rune(b[1]>>4-0x0A) + 'A'
+	s := string(rune((b[1]>>4)-0x0A) + 'A')
 
-	switch r {
-	case 'A', 'B', 'C':
-		return api.ChargeStatus(r), nil
+	switch s {
+	case "A", "B", "C":
+		return api.ChargeStatusString(s)
 	default:
+		// ensure Outlet is re-enabled after wake-up
+		if b[1] == 0xE0 { // Outlet is disabled
+			return api.StatusB, wb.set(ablRegModifyState, 0xA1A1)
+		}
+
 		status, ok := ablStatus[b[1]]
 		if !ok {
-			status = string(r)
+			status = s
 		}
 
 		return api.StatusNone, fmt.Errorf("invalid status: %s", status)
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/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/zendure/connection.go b/meter/zendure/connection.go
index da819469..f1c7de9f 100644
--- a/meter/zendure/connection.go
+++ b/meter/zendure/connection.go
@@ -73,12 +73,10 @@ func (c *Connection) handler(data string) {
 	}
 
 	c.data.SetFunc(func(v Data) Data {
-		if err := mergo.Merge(&v, res.Data); err != nil {
+		if err := mergo.Merge(&v, res.Data, mergo.WithOverride); err != nil {
 			c.log.ERROR.Println(err)
 		}
 
-		c.log.TRACE.Printf("!! data: %+v", v)
-
 		return v
 	})
 }
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..d82b3b9c 100644
--- a/provider/const.go
+++ b/provider/const.go
@@ -11,7 +11,6 @@ import (
 )
 
 type constProvider struct {
-	*getter
 	ctx context.Context
 	str string
 	set Config
@@ -33,24 +32,12 @@ 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,
 	}
 
-	p.getter = defaultGetters(p, 1)
-
 	return p, nil
 }
 
@@ -62,6 +49,45 @@ func (p *constProvider) StringGetter() (func() (string, error), error) {
 	}, nil
 }
 
+var _ IntProvider = (*constProvider)(nil)
+
+func (p *constProvider) IntGetter() (func() (int64, error), error) {
+	val, err := strconv.ParseInt(p.str, 10, 64)
+	if err != nil && p.str == "" {
+		err = nil
+	}
+
+	return func() (int64, error) {
+		return val, err
+	}, err
+}
+
+var _ FloatProvider = (*constProvider)(nil)
+
+func (p *constProvider) FloatGetter() (func() (float64, error), error) {
+	val, err := strconv.ParseFloat(p.str, 64)
+	if err != nil && p.str == "" {
+		err = nil
+	}
+
+	return func() (float64, error) {
+		return val, err
+	}, err
+}
+
+var _ BoolProvider = (*constProvider)(nil)
+
+func (p *constProvider) BoolGetter() (func() (bool, error), error) {
+	val, err := strconv.ParseBool(p.str)
+	if err != nil && p.str == "" {
+		err = nil
+	}
+
+	return func() (bool, error) {
+		return val, err
+	}, err
+}
+
 var _ SetIntProvider = (*constProvider)(nil)
 
 func (p *constProvider) IntSetter(param string) (func(int64) error, error) {
diff --git a/provider/golang/registry.go b/provider/golang/registry.go
index 21d024f6..72f0bcf2 100644
--- a/provider/golang/registry.go
+++ b/provider/golang/registry.go
@@ -8,13 +8,6 @@ import (
 	"github.com/traefik/yaegi/interp"
 )
 
-const Imports = `import (
-	"fmt"
-	"math"
-	"strings"
-	"time"
-)`
-
 var (
 	mu       sync.Mutex
 	registry = make(map[string]*interp.Interpreter)
@@ -31,14 +24,10 @@ func RegisteredVM(name, init string) (*interp.Interpreter, error) {
 	// create new VM
 	if !ok {
 		vm = interp.New(interp.Options{})
-
 		if err := vm.Use(stdlib.Symbols); err != nil {
 			return nil, err
 		}
-
-		if _, err := vm.Eval(Imports); err != nil {
-			return nil, err
-		}
+		vm.ImportUsed()
 
 		if init != "" {
 			if _, err := vm.Eval(init); err != nil {
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/server/eebus/types.go b/server/eebus/types.go
index 6ad12480..1da233e5 100644
--- a/server/eebus/types.go
+++ b/server/eebus/types.go
@@ -1,11 +1,15 @@
 package eebus
 
+import "github.com/evcc-io/evcc/util"
+
 const (
-	BrandName  string = "EVCC"
-	Model      string = "HEMS"
-	DeviceCode string = "EVCC_HEMS_01" // used as common name in cert generation
+	BrandName string = "EVCC"
+	Model     string = "HEMS"
 )
 
+// used as common name in cert generation
+var DeviceCode = util.Getenv("EEBUS_DEVICE_CODE", "EVCC_HEMS_01")
+
 type Config struct {
 	URI         string
 	ShipID      string
diff --git a/tariff/amber.go b/tariff/amber.go
index b60748bc..7fd6e1ef 100644
--- a/tariff/amber.go
+++ b/tariff/amber.go
@@ -83,7 +83,7 @@ func NewAmberFromConfig(other map[string]interface{}) (api.Tariff, error) {
 func (t *Amber) run(done chan error) {
 	var once sync.Once
 
-	for tick := time.Tick(time.Hour); ; <-tick {
+	for tick := time.Tick(time.Minute); ; <-tick {
 		var res []amber.PriceInfo
 		uri := fmt.Sprintf("%s&endDate=%s", t.uri, time.Now().AddDate(0, 0, 2).Format("2006-01-02"))
 
diff --git a/tariff/embed.go b/tariff/embed.go
index c9c523a3..effc0213 100644
--- a/tariff/embed.go
+++ b/tariff/embed.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"time"
 
-	"github.com/evcc-io/evcc/provider/golang"
 	"github.com/evcc-io/evcc/provider/golang/stdlib"
 	"github.com/traefik/yaegi/interp"
 )
@@ -29,35 +28,24 @@ func (t *embed) init() (err error) {
 		return nil
 	}
 
-	vm := interp.New(interp.Options{})
-	if err := vm.Use(stdlib.Symbols); err != nil {
-		return err
-	}
-
-	if _, err := vm.Eval(fmt.Sprintf(`%s
-	var (
-		price float64
-		charges float64 = %f
-		tax float64 = %f
-		ts time.Time
-	)`, golang.Imports, t.Charges, t.Tax)); err != nil {
-		return err
-	}
-
-	prg, err := vm.Compile(t.Formula)
-	if err != nil {
-		return err
-	}
-
 	t.calc = func(price float64, ts time.Time) (float64, error) {
+		vm := interp.New(interp.Options{})
+		if err := vm.Use(stdlib.Symbols); err != nil {
+			return 0, err
+		}
+		vm.ImportUsed()
+
 		if _, err := vm.Eval(fmt.Sprintf(`
-			price = %f
+		var (
+			price float64 = %f
+			charges float64 = %f
+			tax float64 = %f
 			ts = time.Unix(%d, 0).Local()
-		`, price, ts.Unix())); err != nil {
+		)`, price, t.Charges, t.Tax, ts.Unix())); err != nil {
 			return 0, err
 		}
 
-		res, err := vm.Execute(prg)
+		res, err := vm.Eval(t.Formula)
 		if err != nil {
 			return 0, err
 		}
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/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/vehicle/bmw.yaml b/templates/definition/vehicle/bmw.yaml
index b4ac04d3..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
diff --git a/templates/definition/vehicle/fiat.yaml b/templates/definition/vehicle/fiat.yaml
index 66e7b0c9..f29f02ec 100644
--- a/templates/definition/vehicle/fiat.yaml
+++ b/templates/definition/vehicle/fiat.yaml
@@ -9,13 +9,13 @@ params:
   - name: pin
     mask: true
     help:
-      en: Required for evcc to refresh the SoC while charging.
-      de: Benötigt zur Aktualisierung des Ladestands während der Ladung.
+      en: Required for evcc to refresh the SoC while charging. When connected to TWC3, used to start/stop charging.
+      de: Benötigt zur Aktualisierung des Ladestands während der Ladung. Bei Nutzung mit TWC3 kann der Ladevorgang mittels PIN gestartet und gestoppt werden.
   - preset: vehicle-features
 render: |
   type: fiat
   {{ include "vehicle-base" . }}
   {{- if .pin }}
-  pin: {{ .pin }} # mandatory to deep refresh Soc
+  pin: {{ .pin }} # mandatory to deep refresh Soc & start/stop charge
   {{- end }}
   {{ include "vehicle-features" . }}
diff --git a/templates/definition/vehicle/mini.yaml b/templates/definition/vehicle/mini.yaml
index 56f28c65..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
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/vehicle/bmw/provider.go b/vehicle/bmw/provider.go
index 6978347b..3c65beee 100644
--- a/vehicle/bmw/provider.go
+++ b/vehicle/bmw/provider.go
@@ -108,6 +108,14 @@ func (v *Provider) GetLimitSoc() (int64, error) {
 	return res.State.ElectricChargingState.ChargingTarget, nil
 }
 
+var _ api.VehicleClimater = (*Provider)(nil)
+
+// Climater implements the api.VehicleClimater interface
+func (v *Provider) Climater() (bool, error) {
+	res, err := v.statusG()
+	return res.State.ClimateControlState.Activity == "HEATING" || res.State.ClimateControlState.Activity == "COOLING", err
+}
+
 var _ api.Resurrector = (*Provider)(nil)
 
 func (v *Provider) WakeUp() error {
diff --git a/vehicle/bmw/types.go b/vehicle/bmw/types.go
index 6d2f7aa7..5f8f07ec 100644
--- a/vehicle/bmw/types.go
+++ b/vehicle/bmw/types.go
@@ -19,5 +19,8 @@ type VehicleStatus struct {
 			ChargingStatus       string
 			ChargingTarget       int64
 		}
+		ClimateControlState struct {
+			Activity string
+		}
 	}
 }
diff --git a/vehicle/fiat.go b/vehicle/fiat.go
index c219069c..dc51cf9e 100644
--- a/vehicle/fiat.go
+++ b/vehicle/fiat.go
@@ -15,6 +15,7 @@ import (
 type Fiat struct {
 	*embed
 	*fiat.Provider
+	*fiat.Controller
 }
 
 func init() {
@@ -59,6 +60,7 @@ func NewFiatFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 
 	if err == nil {
 		v.Provider = fiat.NewProvider(api, cc.VIN, cc.PIN, cc.Expiry, cc.Cache)
+		v.Controller = fiat.NewController(v.Provider, api, cc.VIN, cc.PIN)
 	}
 
 	return v, err
diff --git a/vehicle/fiat/api.go b/vehicle/fiat/api.go
index ef0e0dd4..76e78df7 100644
--- a/vehicle/fiat/api.go
+++ b/vehicle/fiat/api.go
@@ -158,3 +158,35 @@ func (v *API) Action(vin, pin, action, cmd string) (ActionResponse, error) {
 
 	return res, err
 }
+
+func (v *API) UpdateSchedule(vin, pin string, schedules []Schedule) (ActionResponse, error) {
+	var res ActionResponse
+
+	token, err := v.pinAuth(pin)
+	if err != nil {
+		return res, err
+	}
+
+	uri := fmt.Sprintf("%s/v2/accounts/%s/vehicles/%s/ev/schedule", ApiURI, v.identity.UID(), vin)
+
+	data := struct {
+		Command   string     `json:"command"`
+		PinAuth   string     `json:"pinAuth"`
+		Schedules []Schedule `json:"schedules"`
+	}{
+		Command:   "CPPLUS",
+		PinAuth:   token,
+		Schedules: schedules,
+	}
+
+	req, err := v.request(http.MethodPost, uri, request.MarshalJSON(data))
+	if err == nil {
+		err = v.DoJSON(req, &res)
+	}
+
+	if err == nil && res.Message != "" {
+		err = fmt.Errorf("action schedules: %s", res.Message)
+	}
+
+	return res, err
+}
diff --git a/vehicle/fiat/controller.go b/vehicle/fiat/controller.go
new file mode 100644
index 00000000..71b47e80
--- /dev/null
+++ b/vehicle/fiat/controller.go
@@ -0,0 +1,102 @@
+package fiat
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/evcc-io/evcc/api"
+)
+
+type Controller struct {
+	pvd *Provider
+	api *API
+	vin string
+	pin string
+}
+
+// NewController creates a vehicle current and charge controller
+func NewController(provider *Provider, api *API, vin string, pin string) *Controller {
+	impl := &Controller{
+		pvd: provider,
+		api: api,
+		vin: vin,
+		pin: pin,
+	}
+	return impl
+}
+
+var _ api.CurrentController = (*Controller)(nil)
+
+// MaxCurrent implements the api.CurrentController interface
+func (c *Controller) MaxCurrent(current int64) error {
+	// Even if we cannot control the current, this interface must be implemented otherwise the ChargeEnable is never called
+	return nil
+}
+
+var _ api.ChargeController = (*Controller)(nil)
+
+// ChargeEnable implements the api.ChargeController interface
+func (c *Controller) ChargeEnable(enable bool) error {
+	if c.pin == "" {
+		return api.ErrMissingCredentials
+	}
+
+	// get current schedule status from provider (cached)
+	stat, err := c.pvd.statusG()
+	if err != nil {
+		return err
+	}
+	if stat.EvInfo == nil || stat.EvInfo.Schedules == nil || len(stat.EvInfo.Schedules) == 0 {
+		return api.ErrNotAvailable
+	}
+
+	// configure first schedule and make sure it's active
+	c.configureChargeSchedule(&stat.EvInfo.Schedules[0])
+
+	if enable {
+		// start charging by updating active charge schedule to start now and end in 12h
+		stat.EvInfo.Schedules[0].StartTime = time.Now().Format("15:04")                   // only hour and minutes
+		stat.EvInfo.Schedules[0].EndTime = time.Now().Add(time.Hour * 12).Format("15:04") // only hour and minutes
+	} else {
+		// stop charging by updating active charge schedule end time to now
+		stat.EvInfo.Schedules[0].EndTime = time.Now().Format("15:04") // only hour and minutes
+	}
+
+	// make sure the other charge schedules are disabled in case user changed them
+	c.disableConflictingChargeSchedule(&stat.EvInfo.Schedules[1])
+	c.disableConflictingChargeSchedule(&stat.EvInfo.Schedules[2])
+
+	// post new schedule
+	res, err := c.api.UpdateSchedule(c.vin, c.pin, stat.EvInfo.Schedules)
+	if err == nil && res.ResponseStatus != "pending" {
+		err = fmt.Errorf("invalid response status: %s", res.ResponseStatus)
+	}
+
+	return err
+}
+
+func (c *Controller) configureChargeSchedule(schedule *Schedule) {
+	// all values are set to be sure no manual change can lead to inconsistencies
+	schedule.CabinPriority = false
+	schedule.ChargeToFull = false
+	schedule.EnableScheduleType = true
+	schedule.RepeatSchedule = true
+	schedule.ScheduleType = "CHARGE"
+
+	// only enable for current day to avoid undesired charge start in the future
+	weekday := time.Now().Weekday()
+	schedule.ScheduledDays.Monday = (weekday == time.Monday)
+	schedule.ScheduledDays.Tuesday = (weekday == time.Tuesday)
+	schedule.ScheduledDays.Wednesday = (weekday == time.Wednesday)
+	schedule.ScheduledDays.Thursday = (weekday == time.Thursday)
+	schedule.ScheduledDays.Friday = (weekday == time.Friday)
+	schedule.ScheduledDays.Saturday = (weekday == time.Saturday)
+	schedule.ScheduledDays.Sunday = (weekday == time.Sunday)
+}
+
+func (c *Controller) disableConflictingChargeSchedule(schedule *Schedule) {
+	// make sure the other charge schedules are disabled in case user changed them
+	if schedule.ScheduleType == "CHARGE" && schedule.EnableScheduleType {
+		schedule.EnableScheduleType = false
+	}
+}
diff --git a/vehicle/fiat/types.go b/vehicle/fiat/types.go
index c175fba0..babb2467 100644
--- a/vehicle/fiat/types.go
+++ b/vehicle/fiat/types.go
@@ -28,6 +28,25 @@ type Vehicle struct {
 	VIN string
 }
 
+type Schedule struct {
+	CabinPriority      bool   `json:"cabinPriority"`
+	ChargeToFull       bool   `json:"chargeToFull"`
+	EnableScheduleType bool   `json:"enableScheduleType"`
+	EndTime            string `json:"endTime"`
+	RepeatSchedule     bool   `json:"repeatSchedule"`
+	ScheduleType       string `json:"scheduleType"`
+	ScheduledDays      struct {
+		Friday    bool `json:"friday"`
+		Monday    bool `json:"monday"`
+		Saturday  bool `json:"saturday"`
+		Sunday    bool `json:"sunday"`
+		Thursday  bool `json:"thursday"`
+		Tuesday   bool `json:"tuesday"`
+		Wednesday bool `json:"wednesday"`
+	} `json:"scheduledDays"`
+	StartTime string `json:"startTime"`
+}
+
 type StatusResponse struct {
 	VehicleInfo struct {
 		Odometer struct {
@@ -52,6 +71,7 @@ type StatusResponse struct {
 			TimeToFullyChargeL2 int     // 540
 			TotalRange          int     // 17
 		}
+		Schedules []Schedule
 		Timestamp TimeMillis
 	} `json:",omitempty"`
 	Timestamp TimeMillis
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/mercedes/api.go b/vehicle/mercedes/api.go
index 47acfb02..4a87d655 100644
--- a/vehicle/mercedes/api.go
+++ b/vehicle/mercedes/api.go
@@ -114,5 +114,15 @@ func (v *API) Status(vin string) (StatusResponse, error) {
 		}
 	}
 
+	// There are two attributes for the proconditioning status, precondNow and precondActive
+	if val, ok := message.Attributes["precondNow"]; ok {
+		res.Preconditioning.Active = val.GetBoolValue()
+	}
+	if !res.Preconditioning.Active {
+		if val, ok := message.Attributes["precondActive"]; ok {
+			res.Preconditioning.Active = val.GetBoolValue()
+		}
+	}
+
 	return res, err
 }
diff --git a/vehicle/mercedes/provider.go b/vehicle/mercedes/provider.go
index babbb57d..2898d2a0 100644
--- a/vehicle/mercedes/provider.go
+++ b/vehicle/mercedes/provider.go
@@ -64,6 +64,14 @@ func (v *Provider) Status() (api.ChargeStatus, error) {
 	return status, err
 }
 
+var _ api.VehicleClimater = (*Provider)(nil)
+
+// Climater implements the api.VehicleClimater interface
+func (v *Provider) Climater() (bool, error) {
+	res, err := v.dataG()
+	return res.Preconditioning.Active, err
+}
+
 var _ api.VehiclePosition = (*Provider)(nil)
 
 // Position implements the api.VehiclePosition interface
diff --git a/vehicle/mercedes/types.go b/vehicle/mercedes/types.go
index 98278087..1875d88e 100644
--- a/vehicle/mercedes/types.go
+++ b/vehicle/mercedes/types.go
@@ -73,5 +73,9 @@ type StatusResponse struct {
 		Longitude float64
 		Latitude  float64
 	}
+	Preconditioning struct {
+		Active    bool
+		Timestamp time.Time
+	}
 	Timestamp time.Time
 }
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/meter/zendure/connection_test.go b/meter/zendure/connection_test.go
new file mode 100644
index 00000000..12eb9bfb
--- /dev/null
+++ b/meter/zendure/connection_test.go
@@ -0,0 +1,31 @@
+package zendure
+
+import (
+	"testing"
+	"time"
+
+	"github.com/evcc-io/evcc/util"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestHandler(t *testing.T) {
+	conn := &Connection{
+		log:  util.NewLogger("test"),
+		data: util.NewMonitor[Data](time.Minute),
+	}
+
+	{
+		conn.handler(`{"solarInputPower":113,"sn":"serial"}`)
+		res, err := conn.data.Get()
+		assert.NoError(t, err)
+
+		assert.Equal(t, Data{SolarInputPower: 113, Sn: "serial"}, res)
+	}
+	{
+		conn.handler(`{"solarInputPower":125,"sn":"serial"}`)
+		res, err := conn.data.Get()
+		assert.NoError(t, err)
+
+		assert.Equal(t, Data{SolarInputPower: 125, Sn: "serial"}, res)
+	}
+}
diff --git a/provider/const_test.go b/provider/const_test.go
new file mode 100644
index 00000000..aff1e4cf
--- /dev/null
+++ b/provider/const_test.go
@@ -0,0 +1,40 @@
+package provider
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestConst(t *testing.T) {
+	p, err := NewConstFromConfig(context.TODO(), map[string]interface{}{"value": nil})
+	assert.NoError(t, err)
+
+	{
+		g, err := p.(StringProvider).StringGetter()
+		assert.NoError(t, err)
+
+		v, err := g()
+		assert.NoError(t, err)
+		assert.Equal(t, "", v)
+	}
+
+	{
+		g, err := p.(IntProvider).IntGetter()
+		assert.NoError(t, err)
+
+		v, err := g()
+		assert.NoError(t, err)
+		assert.Equal(t, int64(0), v)
+	}
+
+	{
+		g, err := p.(FloatProvider).FloatGetter()
+		assert.NoError(t, err)
+
+		v, err := g()
+		assert.NoError(t, err)
+		assert.Equal(t, float64(0), v)
+	}
+}
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();
   });
 });