Repository: evcc-io/evcc
Pull request #18032 (2025-01-03 14:19:32)

Battery grid charge: show configured limit
fixes https://github.com/evcc-io/evcc/issues/17843

- 🫰 show grid charge limit in energy flow if it's configured
- 🧹 simplify active grid charge wording

**English**

<img width="1056" alt="en set" src="https://github.com/user-attachments/assets/0ecb277f-e866-41a9-bb43-757339f9b8f6" />

<img width="1089" alt="en active" src="https://github.com/user-attachments/assets/ed4b3a7d-731c-49bd-b8e7-97e05295e26e" />

**German**

<img width="1052" alt="de set" src="https://github.com/user-attachments/assets/83a141ac-a8bd-4799-8fce-a28554ef434d" />

<img width="1061" alt="de active" src="https://github.com/user-attachments/assets/ad944027-a15c-455e-bcbc-e09ef2bf4af1" />



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/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 add79734..cd673510 100644
--- a/i18n/bg.toml
+++ b/i18n/bg.toml
@@ -418,8 +418,6 @@ batteryCharge = "Зареждане на батерията"
 batteryDischarge = "Разреждане на батерията"
 batteryHold = "Батерия (заключена)"
 batteryTooltip = "{energy} от {total} ({soc})"
-cheapBatteryGridCharge = "евтина мрежова енергия"
-cleanBatteryGridCharge = "чиста мрежова енергия"
 gridImport = "Използвана енергия от мрежата"
 homePower = "Потребление"
 loadpoints = "Зарядно устройство | Зарядно устройство | {count} зарядни устройства"
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 7c34332b..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"
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 a02c510a..24e48355 100644
--- a/i18n/tr.toml
+++ b/i18n/tr.toml
@@ -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ı"
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/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/mercedes.go b/vehicle/mercedes.go
index cfb461ae..9eeab3f7 100644
--- a/vehicle/mercedes.go
+++ b/vehicle/mercedes.go
@@ -1,6 +1,7 @@
 package vehicle
 
 import (
+	"errors"
 	"time"
 
 	"github.com/evcc-io/evcc/api"
@@ -15,11 +16,16 @@ type Mercedes struct {
 }
 
 func init() {
-	registry.Add("mercedes", NewMercedesFromConfig)
+	registry.Add("mercedes", func(other map[string]interface{}) (api.Vehicle, error) {
+		return newMercedesFromConfig("mercedes", other)
+	})
+	registry.Add("smart-eq", func(other map[string]interface{}) (api.Vehicle, error) {
+		return newMercedesFromConfig("smart-eq", other)
+	})
 }
 
-// NewMercedesFromConfig creates a new vehicle
-func NewMercedesFromConfig(other map[string]interface{}) (api.Vehicle, error) {
+// newMercedesFromConfig creates a new vehicle
+func newMercedesFromConfig(brand string, other map[string]interface{}) (api.Vehicle, error) {
 	cc := struct {
 		embed    `mapstructure:",squash"`
 		Tokens   Tokens
@@ -45,21 +51,28 @@ func NewMercedesFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 		cc.User = cc.Account_
 	}
 
-	log := util.NewLogger("mercedes").Redact(cc.Tokens.Access, cc.Tokens.Refresh)
+	log := util.NewLogger(brand).Redact(cc.Tokens.Access, cc.Tokens.Refresh)
 	identity, err := mercedes.NewIdentity(log, token, cc.User, cc.Region)
 	if err != nil {
 		return nil, err
 	}
 
-	v := &Mercedes{
-		embed: &cc.embed,
-	}
-
 	api := mercedes.NewAPI(log, identity)
 
-	cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles)
-	if err == nil {
-		v.Provider = mercedes.NewProvider(api, cc.VIN, cc.Cache)
+	if brand == "smart-eq" {
+		if cc.VIN == "" {
+			return nil, errors.New("missing VIN")
+		}
+	} else {
+		cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	v := &Mercedes{
+		embed:    &cc.embed,
+		Provider: mercedes.NewProvider(api, cc.VIN, cc.Cache),
 	}
 
 	return v, err
diff --git a/vehicle/smart.go b/vehicle/smart.go
deleted file mode 100644
index 15eecb99..00000000
--- a/vehicle/smart.go
+++ /dev/null
@@ -1,65 +0,0 @@
-package vehicle
-
-import (
-	"fmt"
-	"time"
-
-	"github.com/evcc-io/evcc/api"
-	"github.com/evcc-io/evcc/util"
-	"github.com/evcc-io/evcc/vehicle/mb"
-	"github.com/evcc-io/evcc/vehicle/smart"
-)
-
-// Smart is an api.Vehicle implementation for Smart cars
-type Smart struct {
-	*embed
-	*smart.Provider
-}
-
-func init() {
-	registry.Add("smart", NewSmartFromConfig)
-}
-
-// NewSmartFromConfig creates a new vehicle
-func NewSmartFromConfig(other map[string]interface{}) (api.Vehicle, error) {
-	cc := struct {
-		embed          `mapstructure:",squash"`
-		User, Password string
-		VIN            string
-		Expiry         time.Duration
-		Cache          time.Duration
-	}{
-		Expiry: expiry,
-		Cache:  interval,
-	}
-
-	if err := util.DecodeOther(other, &cc); err != nil {
-		return nil, err
-	}
-
-	if cc.User == "" || cc.Password == "" {
-		return nil, api.ErrMissingCredentials
-	}
-
-	log := util.NewLogger("smart").Redact(cc.User, cc.Password, cc.VIN)
-
-	v := &Smart{
-		embed: &cc.embed,
-	}
-
-	identity := mb.NewIdentity(log, smart.OAuth2Config)
-	err := identity.Login(cc.User, cc.Password)
-	if err != nil {
-		return v, fmt.Errorf("login failed: %w", err)
-	}
-
-	api := smart.NewAPI(log, identity)
-
-	cc.VIN, err = ensureVehicle(cc.VIN, api.Vehicles)
-
-	if err == nil {
-		v.Provider = smart.NewProvider(log, api, cc.VIN, cc.Expiry, cc.Cache)
-	}
-
-	return v, err
-}


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