Repository: evcc-io/evcc
Pull request #19072 (2025-02-22 16:59:42)

Tasmota: enable more usages for SML
- Tasmota SML reader can be used now with the Tasmota standard template for more usage scenarios: pv, battery, charge
Refer to #19041
- PV usage logic for CurrentPower was integrated in `/meter/shelly.go`
- Tasmota templates were updated correspondigly

andig: Done
thierolm: @jeriox bitte teste jetzt nochmal den PR
jeriox: > @jeriox bitte teste jetzt nochmal den PR

funktioniert einwandfrei!
andig: @thierolm könnten wir es hier analog Shelly machen und die `usage` Logik aus der Connection raus halten?

Patch:
diff --git a/assets/js/components/Loadpoints/SessionInfo.vue b/assets/js/components/Loadpoints/SessionInfo.vue
index 526be471..22ecb62d 100644
--- a/assets/js/components/Loadpoints/SessionInfo.vue
+++ b/assets/js/components/Loadpoints/SessionInfo.vue
@@ -64,6 +64,11 @@ export default {
 					value: this.fmtDuration(this.chargeRemainingDurationInterpolated),
 					available: this.chargeRemainingDurationInterpolated > 0,
 				},
+				{
+					key: "finished",
+					value: this.fmtHourMinute(this.finishTime),
+					available: this.chargeRemainingDurationInterpolated > 0,
+				},
 				{
 					key: "duration",
 					value: this.fmtDuration(this.chargeDurationInterpolated),
@@ -119,6 +124,14 @@ export default {
 		showSm() {
 			return this.valueSm !== undefined;
 		},
+		finishTime() {
+			const remainingSeconds = this.chargeRemainingDurationInterpolated;
+			if (remainingSeconds > 0) {
+				const now = new Date();
+				return new Date(now.getTime() + remainingSeconds * 1000);
+			}
+			return undefined;
+		},
 		solarFormatted() {
 			return this.fmtPercentage(this.sessionSolarPercentage, 1);
 		},
diff --git a/assets/js/mixins/formatter.ts b/assets/js/mixins/formatter.ts
index 3a5cd98e..8b39150f 100644
--- a/assets/js/mixins/formatter.ts
+++ b/assets/js/mixins/formatter.ts
@@ -201,6 +201,12 @@ export default defineComponent({
 
 			return `${weekday} ${hour}`.trim();
 		},
+		fmtHourMinute(date: Date) {
+			return new Intl.DateTimeFormat(this.$i18n?.locale, {
+				hour: "numeric",
+				minute: "numeric",
+			}).format(date);
+		},
 		fmtFullDateTime(date: Date, short: boolean) {
 			return new Intl.DateTimeFormat(this.$i18n?.locale, {
 				weekday: short ? undefined : "short",
diff --git a/charger/shelly.go b/charger/shelly.go
index e992bb42..018b9f92 100644
--- a/charger/shelly.go
+++ b/charger/shelly.go
@@ -2,6 +2,7 @@ package charger
 
 import (
 	"fmt"
+	"time"
 
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/meter/shelly"
@@ -10,7 +11,7 @@ import (
 
 // Shelly charger implementation
 type Shelly struct {
-	conn *shelly.Switch
+	conn *shelly.Connection
 	*switchSocket
 }
 
@@ -18,33 +19,50 @@ func init() {
 	registry.Add("shelly", NewShellyFromConfig)
 }
 
+//go:generate go tool decorate -f decorateShelly -b *Shelly -r api.Charger -t "api.PhaseVoltages,Voltages,func() (float64, float64, float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)" -t "api.PhasePowers,Powers,func() (float64, float64, float64, error)"
+
 // NewShellyFromConfig creates a Shelly charger from generic config
-func NewShellyFromConfig(other map[string]interface{}) (api.Charger, error) {
-	var cc struct {
+func NewShellyFromConfig(other map[string]any) (api.Charger, error) {
+	cc := struct {
 		embed        `mapstructure:",squash"`
 		URI          string
 		User         string
 		Password     string
 		Channel      int
 		StandbyPower float64
+		Cache        time.Duration
+	}{
+		Cache: time.Second,
 	}
 
 	if err := util.DecodeOther(other, &cc); err != nil {
 		return nil, err
 	}
 
-	return NewShelly(cc.embed, cc.URI, cc.User, cc.Password, cc.Channel, cc.StandbyPower)
+	c, err := NewShelly(cc.embed, cc.URI, cc.User, cc.Password, cc.Channel, cc.StandbyPower, cc.Cache)
+	if err != nil {
+		return nil, err
+	}
+
+	var vol, cur, pow func() (float64, float64, float64, error)
+	if phases, ok := c.conn.Generation.(shelly.Phases); ok {
+		vol = phases.Voltages
+		cur = phases.Currents
+		pow = phases.Powers
+	}
+
+	return decorateShelly(c, vol, cur, pow), nil
 }
 
 // NewShelly creates Shelly charger
-func NewShelly(embed embed, uri, user, password string, channel int, standbypower float64) (*Shelly, error) {
-	conn, err := shelly.NewConnection(uri, user, password, channel)
+func NewShelly(embed embed, uri, user, password string, channel int, standbypower float64, cache time.Duration) (*Shelly, error) {
+	conn, err := shelly.NewConnection(uri, user, password, channel, cache)
 	if err != nil {
 		return nil, err
 	}
 
 	c := &Shelly{
-		conn: shelly.NewSwitch(conn),
+		conn: conn,
 	}
 
 	c.switchSocket = NewSwitchSocket(&embed, c.Enabled, c.conn.CurrentPower, standbypower)
diff --git a/charger/shelly_decorators.go b/charger/shelly_decorators.go
new file mode 100644
index 00000000..0aa4820e
--- /dev/null
+++ b/charger/shelly_decorators.go
@@ -0,0 +1,111 @@
+package charger
+
+// Code generated by github.com/evcc-io/evcc/cmd/tools/decorate.go. DO NOT EDIT.
+
+import (
+	"github.com/evcc-io/evcc/api"
+)
+
+func decorateShelly(base *Shelly, phaseVoltages func() (float64, float64, float64, error), phaseCurrents func() (float64, float64, float64, error), phasePowers func() (float64, float64, float64, error)) api.Charger {
+	switch {
+	case phaseCurrents == nil && phaseVoltages == nil:
+		return base
+
+	case phaseCurrents == nil && phaseVoltages != nil:
+		return &struct {
+			*Shelly
+			api.PhaseVoltages
+		}{
+			Shelly: base,
+			PhaseVoltages: &decorateShellyPhaseVoltagesImpl{
+				phaseVoltages: phaseVoltages,
+			},
+		}
+
+	case phaseCurrents != nil && phasePowers == nil && phaseVoltages == nil:
+		return &struct {
+			*Shelly
+			api.PhaseCurrents
+		}{
+			Shelly: base,
+			PhaseCurrents: &decorateShellyPhaseCurrentsImpl{
+				phaseCurrents: phaseCurrents,
+			},
+		}
+
+	case phaseCurrents != nil && phasePowers == nil && phaseVoltages != nil:
+		return &struct {
+			*Shelly
+			api.PhaseCurrents
+			api.PhaseVoltages
+		}{
+			Shelly: base,
+			PhaseCurrents: &decorateShellyPhaseCurrentsImpl{
+				phaseCurrents: phaseCurrents,
+			},
+			PhaseVoltages: &decorateShellyPhaseVoltagesImpl{
+				phaseVoltages: phaseVoltages,
+			},
+		}
+
+	case phaseCurrents != nil && phasePowers != nil && phaseVoltages == nil:
+		return &struct {
+			*Shelly
+			api.PhaseCurrents
+			api.PhasePowers
+		}{
+			Shelly: base,
+			PhaseCurrents: &decorateShellyPhaseCurrentsImpl{
+				phaseCurrents: phaseCurrents,
+			},
+			PhasePowers: &decorateShellyPhasePowersImpl{
+				phasePowers: phasePowers,
+			},
+		}
+
+	case phaseCurrents != nil && phasePowers != nil && phaseVoltages != nil:
+		return &struct {
+			*Shelly
+			api.PhaseCurrents
+			api.PhasePowers
+			api.PhaseVoltages
+		}{
+			Shelly: base,
+			PhaseCurrents: &decorateShellyPhaseCurrentsImpl{
+				phaseCurrents: phaseCurrents,
+			},
+			PhasePowers: &decorateShellyPhasePowersImpl{
+				phasePowers: phasePowers,
+			},
+			PhaseVoltages: &decorateShellyPhaseVoltagesImpl{
+				phaseVoltages: phaseVoltages,
+			},
+		}
+	}
+
+	return nil
+}
+
+type decorateShellyPhaseCurrentsImpl struct {
+	phaseCurrents func() (float64, float64, float64, error)
+}
+
+func (impl *decorateShellyPhaseCurrentsImpl) Currents() (float64, float64, float64, error) {
+	return impl.phaseCurrents()
+}
+
+type decorateShellyPhasePowersImpl struct {
+	phasePowers func() (float64, float64, float64, error)
+}
+
+func (impl *decorateShellyPhasePowersImpl) Powers() (float64, float64, float64, error) {
+	return impl.phasePowers()
+}
+
+type decorateShellyPhaseVoltagesImpl struct {
+	phaseVoltages func() (float64, float64, float64, error)
+}
+
+func (impl *decorateShellyPhaseVoltagesImpl) Voltages() (float64, float64, float64, error) {
+	return impl.phaseVoltages()
+}
diff --git a/charger/tasmota.go b/charger/tasmota.go
index 47262ee7..37a05fd4 100644
--- a/charger/tasmota.go
+++ b/charger/tasmota.go
@@ -32,6 +32,7 @@ func NewTasmotaFromConfig(other map[string]interface{}) (api.Charger, error) {
 		URI          string
 		User         string
 		Password     string
+		Usage        string
 		StandbyPower float64
 		Channel      []int
 		Cache        time.Duration
@@ -44,11 +45,11 @@ func NewTasmotaFromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewTasmota(cc.embed, cc.URI, cc.User, cc.Password, cc.Channel, cc.StandbyPower, cc.Cache)
+	return NewTasmota(cc.embed, cc.URI, cc.User, cc.Password, cc.Usage, cc.Channel, cc.StandbyPower, cc.Cache)
 }
 
 // NewTasmota creates Tasmota charger
-func NewTasmota(embed embed, uri, user, password string, channels []int, standbypower float64, cache time.Duration) (api.Charger, error) {
+func NewTasmota(embed embed, uri, user, password, usage string, channels []int, standbypower float64, cache time.Duration) (api.Charger, error) {
 	conn, err := tasmota.NewConnection(uri, user, password, channels, cache)
 	if err != nil {
 		return nil, err
diff --git a/core/circuit/circuit.go b/core/circuit/circuit.go
index c3d3f398..d8e39226 100644
--- a/core/circuit/circuit.go
+++ b/core/circuit/circuit.go
@@ -12,6 +12,7 @@ import (
 	"github.com/evcc-io/evcc/plugin"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/config"
+	"github.com/evcc-io/evcc/util/modbus"
 )
 
 var _ api.Circuit = (*Circuit)(nil)
@@ -235,7 +236,7 @@ func (c *Circuit) overloadOnError(t time.Time, val *float64) {
 }
 
 func (c *Circuit) updateMeters() error {
-	if f, err := backoff.RetryWithData(c.meter.CurrentPower, bo()); err == nil {
+	if f, err := backoff.RetryWithData(c.meter.CurrentPower, modbus.Backoff()); err == nil {
 		c.power = f
 		c.powerUpdated = time.Now()
 	} else {
@@ -249,7 +250,7 @@ func (c *Circuit) updateMeters() error {
 			var err error
 			i1, i2, i3, err = phaseMeter.Currents()
 			return err
-		}, bo()); err != nil {
+		}, modbus.Backoff()); err != nil {
 			c.overloadOnError(c.currentUpdated, &c.current)
 			return fmt.Errorf("circuit currents: %w", err)
 		}
diff --git a/core/circuit/helper.go b/core/circuit/helper.go
deleted file mode 100644
index 77c88c8b..00000000
--- a/core/circuit/helper.go
+++ /dev/null
@@ -1,12 +0,0 @@
-package circuit
-
-import (
-	"time"
-
-	"github.com/cenkalti/backoff/v4"
-)
-
-// bo returns an exponential backoff for reading meter power quickly
-func bo() *backoff.ExponentialBackOff {
-	return backoff.NewExponentialBackOff(backoff.WithInitialInterval(20*time.Millisecond), backoff.WithMaxElapsedTime(time.Second))
-}
diff --git a/core/helper.go b/core/helper.go
index d8c2a31a..5f9fedab 100644
--- a/core/helper.go
+++ b/core/helper.go
@@ -2,9 +2,7 @@ package core
 
 import (
 	"fmt"
-	"time"
 
-	"github.com/cenkalti/backoff/v4"
 	"github.com/evcc-io/evcc/util/config"
 )
 
@@ -16,11 +14,6 @@ var (
 	Voltage float64
 )
 
-// bo returns an exponential backoff for reading meter power quickly
-func bo() *backoff.ExponentialBackOff {
-	return backoff.NewExponentialBackOff(backoff.WithInitialInterval(20*time.Millisecond), backoff.WithMaxElapsedTime(2*time.Second))
-}
-
 // powerToCurrent is a helper function to convert power to per-phase current
 func powerToCurrent(power float64, phases int) float64 {
 	if Voltage == 0 {
diff --git a/core/loadpoint.go b/core/loadpoint.go
index 8aa9b9f8..f0ebac21 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -26,6 +26,7 @@ import (
 	"github.com/evcc-io/evcc/push"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/config"
+	"github.com/evcc-io/evcc/util/modbus"
 	"github.com/evcc-io/evcc/util/telemetry"
 )
 
@@ -1461,7 +1462,7 @@ func (lp *Loadpoint) pvMaxCurrent(mode api.ChargeMode, sitePower, batteryBoostPo
 
 // UpdateChargePowerAndCurrents updates charge meter power and currents for load management
 func (lp *Loadpoint) UpdateChargePowerAndCurrents() float64 {
-	power, err := backoff.RetryWithData(lp.chargeMeter.CurrentPower, bo())
+	power, err := backoff.RetryWithData(lp.chargeMeter.CurrentPower, modbus.Backoff())
 	if err == nil {
 		lp.Lock()
 		lp.chargePower = power // update value if no error
@@ -1502,7 +1503,7 @@ func (lp *Loadpoint) UpdateChargePowerAndCurrents() float64 {
 			lp.publish(keys.ChargeCurrents, lp.chargeCurrents)
 
 			return nil
-		}, bo()); err != nil && !errors.Is(err, api.ErrNotAvailable) {
+		}, modbus.Backoff()); err != nil && !errors.Is(err, api.ErrNotAvailable) {
 			lp.log.ERROR.Printf("charge currents: %v", err)
 		}
 	}
diff --git a/core/site.go b/core/site.go
index da95ed7b..922b47e3 100644
--- a/core/site.go
+++ b/core/site.go
@@ -30,6 +30,7 @@ import (
 	"github.com/evcc-io/evcc/tariff"
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/config"
+	"github.com/evcc-io/evcc/util/modbus"
 	"github.com/evcc-io/evcc/util/telemetry"
 	"github.com/samber/lo"
 	"github.com/smallnest/chanx"
@@ -496,7 +497,7 @@ func (site *Site) collectMeters(key string, meters []config.Device[api.Meter]) [
 				fmt.Fprintf(&b, "%v !! %3dms %v\n", start, d.Milliseconds(), err)
 			}
 			return f, err
-		}, bo())
+		}, modbus.Backoff())
 		if err == nil {
 			site.log.DEBUG.Printf("%s %d power: %.0fW", key, i+1, power)
 		} else {
@@ -723,7 +724,7 @@ func (site *Site) updateGridMeter() error {
 
 	var mm measurement
 
-	if res, err := backoff.RetryWithData(site.gridMeter.CurrentPower, bo()); err == nil {
+	if res, err := backoff.RetryWithData(site.gridMeter.CurrentPower, modbus.Backoff()); err == nil {
 		mm.Power = res
 		site.gridPower = res
 		site.log.DEBUG.Printf("grid power: %.0fW", res)
diff --git a/go.mod b/go.mod
index 8592d463..c98dfc88 100644
--- a/go.mod
+++ b/go.mod
@@ -53,6 +53,7 @@ require (
 	github.com/influxdata/influxdb-client-go/v2 v2.14.0
 	github.com/insomniacslk/tapo v1.0.1
 	github.com/itchyny/gojq v0.12.17
+	github.com/jarcoal/httpmock v1.3.0
 	github.com/jeremywohl/flatten v1.0.1
 	github.com/jinzhu/now v1.1.5
 	github.com/joeshaw/carwings v0.0.0-20250124122309-e366d592915c
@@ -218,6 +219,6 @@ tool (
 
 replace gopkg.in/yaml.v3 => github.com/andig/yaml v0.0.0-20240531135838-1ff5761ab467
 
-replace github.com/grid-x/modbus => github.com/evcc-io/modbus v0.0.0-20250403111054-69e330ffbd07
+replace github.com/grid-x/modbus => github.com/evcc-io/modbus v0.0.0-20250420095335-2cd088941a35
 
 replace github.com/lorenzodonini/ocpp-go => github.com/evcc-io/ocpp-go v0.0.0-20250322092544-c0c6094051c0
diff --git a/go.sum b/go.sum
index 69621d48..86132397 100644
--- a/go.sum
+++ b/go.sum
@@ -155,8 +155,8 @@ github.com/enbility/zeroconf/v2 v2.0.0-20240920094356-be1cae74fda6/go.mod h1:Bsz
 github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g=
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/evcc-io/modbus v0.0.0-20250403111054-69e330ffbd07 h1:OQNaVvgKSJabdxrQVkXANhpfsinAil1GzbOwHgghxTo=
-github.com/evcc-io/modbus v0.0.0-20250403111054-69e330ffbd07/go.mod h1:swrNGAVgI1r/3d/sEKE7qgujdRR9aHVPYKyc3gvpVTc=
+github.com/evcc-io/modbus v0.0.0-20250420095335-2cd088941a35 h1:OBKG6g2jjwF93LOWXrHyPBhggJkgInWMWVvA7AkCf7Q=
+github.com/evcc-io/modbus v0.0.0-20250420095335-2cd088941a35/go.mod h1:swrNGAVgI1r/3d/sEKE7qgujdRR9aHVPYKyc3gvpVTc=
 github.com/evcc-io/ocpp-go v0.0.0-20250322092544-c0c6094051c0 h1:Qz34Pm1Wr05jjJia5g2On3zRqdZh+BLTwqHgJGsiIh4=
 github.com/evcc-io/ocpp-go v0.0.0-20250322092544-c0c6094051c0/go.mod h1:2kcukDdhui4u730VfnYVWuwzDLgw+mBRGDir/QAyBhg=
 github.com/evcc-io/rct v0.1.2-0.20250315164247-d2f41b161785 h1:OWCBVMcPsVTffdiZN3VYaq9p4fsWlzd490J6pekaKLA=
@@ -442,6 +442,8 @@ github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m
 github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
 github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
+github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
 github.com/mergermarket/go-pkcs7 v0.0.0-20170926155232-153b18ea13c9 h1:j6boLfPkcFlRVaKbc0hf5PVh3jJrdHv9n6SIPOdVKaU=
 github.com/mergermarket/go-pkcs7 v0.0.0-20170926155232-153b18ea13c9/go.mod h1:GH7jtq102ZiRB7LEKgqP54akN7GOVaNpCJrDWTeWSMY=
 github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
diff --git a/i18n/de.toml b/i18n/de.toml
index b4764812..b267619e 100644
--- a/i18n/de.toml
+++ b/i18n/de.toml
@@ -614,6 +614,7 @@ charged = "Geladen"
 co2 = "⌀ CO₂"
 duration = "Ladedauer"
 fallbackName = "Ladepunkt"
+finished = "Ladeende"
 power = "Leistung"
 price = "Kosten"
 remaining = "Restzeit"
diff --git a/i18n/en.toml b/i18n/en.toml
index 363f0c6d..036574e4 100644
--- a/i18n/en.toml
+++ b/i18n/en.toml
@@ -613,6 +613,7 @@ charged = "Charged"
 co2 = "⌀ CO₂"
 duration = "Duration"
 fallbackName = "Charging point"
+finished = "Finish time"
 power = "Power"
 price = "Cost"
 remaining = "Remaining"
diff --git a/meter/lgpcs/lgpcs.go b/meter/lgpcs/lgpcs.go
index 65fb1598..5068bcb1 100644
--- a/meter/lgpcs/lgpcs.go
+++ b/meter/lgpcs/lgpcs.go
@@ -31,55 +31,49 @@ type Com struct {
 }
 
 var (
-	once     sync.Once
-	instance *Com
+	instances map[string]*Com = map[string]*Com{}
+	mu        sync.Mutex      = sync.Mutex{}
 )
 
-// GetInstance implements the singleton pattern to handle the access via the authkey to the PCS of the LG ESS HOME system
+// GetInstance retrives a singleton per uri from a map to handle the access via the authkey to the PCS of the LG ESS HOME system
 func GetInstance(uri, registration, password string, cache time.Duration, essType Model) (*Com, error) {
-	uri = util.DefaultScheme(strings.TrimSuffix(uri, "/"), "https")
-
-	var err error
-	once.Do(func() {
-		log := util.NewLogger("lgess")
-		instance = &Com{
-			Helper:       request.NewHelper(log),
-			uri:          uri,
-			registration: registration,
-			password:     password,
-			essType:      essType,
-			log:          log,
-		}
-
-		// ignore the self signed certificate
-		instance.Client.Transport = request.NewTripper(log, transport.Insecure())
-
-		// caches the data access for the "cache" time duration
-		// sends a new request to the pcs if the cache is expired and Data() requested
-		instance.dataG = util.Cached(instance.essInfo, cache)
+	mu.Lock()
+	defer mu.Unlock()
 
-		// do first login if no authKey exists and uri and password exist
-		if instance.authKey == "" && instance.uri != "" && (instance.password != "" || instance.registration != "") {
-			err = instance.Login()
-		}
-	})
+	uri = util.DefaultScheme(strings.TrimSuffix(uri, "/"), "https")
 
-	// check if different uris are provided
-	if uri != "" && instance.uri != uri {
-		return nil, fmt.Errorf("uri mismatch: %s vs %s", instance.uri, uri)
+	instance, found := instances[uri]
+	if found {
+		return instance, nil
 	}
 
-	// check if different registrations are provided
-	if registration != "" && instance.registration != registration {
-		return nil, errors.New("registration mismatch")
+	log := util.NewLogger(fmt.Sprintf("lgess-%s", strings.TrimPrefix("https://", uri)))
+	instance = &Com{uri: uri,
+		Helper:       request.NewHelper(log),
+		registration: registration,
+		password:     password,
+		essType:      essType,
+		log:          log,
 	}
+	// put instance into the cache map
+	instances[uri] = instance
+
+	// ignore the self signed certificate
+	instance.Client.Transport = request.NewTripper(log, transport.Insecure())
 
-	// check if different passwords are provided
-	if password != "" && instance.password != password {
-		return nil, errors.New("password mismatch")
+	// caches the data access for the "cache" time duration
+	// sends a new request to the pcs if the cache is expired and Data() requested
+	instance.dataG = util.Cached(instance.essInfo, cache)
+
+	// do first login if no authKey exists and uri and password exist
+	if instance.authKey == "" && instance.uri != "" && (instance.password != "" || instance.registration != "") {
+		if err := instance.Login(); err != nil {
+			return nil, err
+		}
+		return instance, nil
 	}
 
-	return instance, err
+	return nil, errors.New("missing credentials")
 }
 
 // Login calls login and stores the returned authorization key
diff --git a/meter/shelly.go b/meter/shelly.go
index 67ac8f8a..3dd2f54d 100644
--- a/meter/shelly.go
+++ b/meter/shelly.go
@@ -1,33 +1,83 @@
 package meter
 
 import (
+	"math"
+	"strings"
+	"time"
+
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/meter/shelly"
 	"github.com/evcc-io/evcc/util"
 )
 
+// Shelly meter considering usage
+type Shelly struct {
+	shelly.Connection
+	usage string
+}
+
 // Shelly meter implementation
 func init() {
 	registry.Add("shelly", NewShellyFromConfig)
 }
 
+//go:generate go tool decorate -f decorateShelly -b *Shelly -r api.Meter -t "api.PhaseVoltages,Voltages,func() (float64, float64, float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)" -t "api.PhasePowers,Powers,func() (float64, float64, float64, error)"
+
 // NewShellyFromConfig creates a Shelly charger from generic config
-func NewShellyFromConfig(other map[string]interface{}) (api.Meter, error) {
-	var cc struct {
+func NewShellyFromConfig(other map[string]any) (api.Meter, error) {
+	cc := struct {
 		URI      string
 		User     string
 		Password string
 		Channel  int
+		Usage    string
+		Cache    time.Duration
+	}{
+		Cache: time.Second,
 	}
 
 	if err := util.DecodeOther(other, &cc); err != nil {
 		return nil, err
 	}
 
-	conn, err := shelly.NewConnection(cc.URI, cc.User, cc.Password, cc.Channel)
+	c, err := NewShelly(cc.URI, cc.User, cc.Password, strings.ToLower(cc.Usage), cc.Channel, cc.Cache)
+	if err != nil {
+		return nil, err
+	}
+
+	var vol, cur, pow func() (float64, float64, float64, error)
+	if phases, ok := c.Connection.Generation.(shelly.Phases); ok {
+		vol = phases.Voltages
+		cur = phases.Currents
+		pow = phases.Powers
+	}
+
+	return decorateShelly(c, vol, cur, pow), nil
+}
+
+// NewShelly creates Shelly meter
+func NewShelly(uri, user, password, usage string, channel int, cache time.Duration) (*Shelly, error) {
+	conn, err := shelly.NewConnection(uri, user, password, channel, cache)
 	if err != nil {
 		return nil, err
 	}
+	c := &Shelly{
+		Connection: *conn,
+		usage:      usage,
+	}
+	return c, nil
+}
 
-	return shelly.NewSwitch(conn), nil
+var _ api.Meter = (*Shelly)(nil)
+
+// CurrentPower implements the api.Meter interface
+func (c *Shelly) CurrentPower() (float64, error) {
+	power, err := c.Connection.CurrentPower()
+	if err != nil {
+		return 0, err
+	}
+	if c.usage == "pv" {
+		power = math.Abs(power)
+	}
+	return power, nil
 }
diff --git a/meter/shelly/connection.go b/meter/shelly/connection.go
index 4024cb86..01dc0a3c 100644
--- a/meter/shelly/connection.go
+++ b/meter/shelly/connection.go
@@ -3,27 +3,34 @@ package shelly
 import (
 	"errors"
 	"fmt"
-	"net/http"
 	"strings"
+	"time"
 
 	"github.com/evcc-io/evcc/util"
 	"github.com/evcc-io/evcc/util/request"
 	"github.com/evcc-io/evcc/util/transport"
-	"github.com/jpfielding/go-http-digest/pkg/digest"
 )
 
+type Generation interface {
+	CurrentPower() (float64, error)
+	Enabled() (bool, error)
+	Enable(bool) error
+	TotalEnergy() (float64, error)
+}
+
+type Phases interface {
+	Currents() (float64, float64, float64, error)
+	Voltages() (float64, float64, float64, error)
+	Powers() (float64, float64, float64, error)
+}
+
 // Connection is the Shelly connection
 type Connection struct {
-	*request.Helper
-	uri     string
-	channel int
-	gen     int    // Shelly api generation
-	model   string // Shelly device type
-	profile string // Shelly device profile
+	Generation
 }
 
 // NewConnection creates a new Shelly device connection.
-func NewConnection(uri, user, password string, channel int) (*Connection, error) {
+func NewConnection(uri, user, password string, channel int, cache time.Duration) (*Connection, error) {
 	if uri == "" {
 		return nil, errors.New("missing uri")
 	}
@@ -31,13 +38,14 @@ func NewConnection(uri, user, password string, channel int) (*Connection, error)
 	for _, suffix := range []string{"/", "/rcp", "/shelly"} {
 		uri = strings.TrimSuffix(uri, suffix)
 	}
+	uri = util.DefaultScheme(uri, "http")
 
 	log := util.NewLogger("shelly")
 	client := request.NewHelper(log)
 
 	// Shelly Gen1 and Gen2 families expose the /shelly endpoint
 	var resp DeviceInfo
-	if err := client.GetJSON(fmt.Sprintf("%s/shelly", util.DefaultScheme(uri, "http")), &resp); err != nil {
+	if err := client.GetJSON(fmt.Sprintf("%s/shelly", uri), &resp); err != nil {
 		return nil, err
 	}
 
@@ -45,61 +53,30 @@ func NewConnection(uri, user, password string, channel int) (*Connection, error)
 		return nil, fmt.Errorf("%s (%s) missing user/password", resp.Model, resp.Mac)
 	}
 
-	conn := &Connection{
-		Helper:  client,
-		uri:     util.DefaultScheme(uri, "http"),
-		channel: channel,
-		gen:     resp.Gen,
-		model:   strings.Split(resp.Type+resp.Model, "-")[0],
-		profile: resp.Profile,
-	}
-
-	conn.Client.Transport = request.NewTripper(log, transport.Insecure())
-
 	// Set default profile to "monophase" if not provided
 	if resp.Profile == "" {
 		resp.Profile = "monophase"
 	}
 
-	switch conn.gen {
-	case 0, 1:
+	model := strings.Split(resp.Type+resp.Model, "-")[0]
+
+	client.Transport = request.NewTripper(log, transport.Insecure())
+
+	var gen Generation
+	if resp.Gen < 2 {
 		// Shelly GEN 1 API
 		// https://shelly-api-docs.shelly.cloud/gen1/#shelly-family-overview
 		if user != "" {
 			log.Redact(transport.BasicAuthHeader(user, password))
-			conn.Client.Transport = transport.BasicAuth(user, password, conn.Client.Transport)
 		}
-	case 2, 3:
+		gen = newGen1(client, uri, model, channel, user, password, cache)
+	} else {
 		// Shelly GEN 2+ API
 		// https://shelly-api-docs.shelly.cloud/gen2/
-		conn.uri += "/rpc"
-		if user != "" {
-			conn.Client.Transport = digest.NewTransport(user, password, conn.Client.Transport)
-		}
-	default:
-		return conn, fmt.Errorf("%s (%s) unknown api generation (%d)", resp.Type, resp.Model, conn.gen)
+		gen = newGen2(client, uri, model, resp.Profile, channel, user, password, cache)
 	}
 
-	return conn, nil
-}
+	conn := &Connection{gen}
 
-// execGen2Cmd executes a shelly api gen1/gen2 command and provides the response
-func (d *Connection) execGen2Cmd(method string, enable bool, res interface{}) error {
-	// Shelly gen 2 rfc7616 authentication
-	// https://shelly-api-docs.shelly.cloud/gen2/Overview/CommonDeviceTraits#authentication
-	// https://datatracker.ietf.org/doc/html/rfc7616
-
-	data := &Gen2RpcPost{
-		Id:     d.channel,
-		On:     enable,
-		Src:    "evcc",
-		Method: method,
-	}
-
-	req, err := request.New(http.MethodPost, fmt.Sprintf("%s/%s", d.uri, method), request.MarshalJSON(data), request.JSONEncoding)
-	if err != nil {
-		return err
-	}
-
-	return d.DoJSON(req, &res)
+	return conn, nil
 }
diff --git a/meter/shelly/energymeter.go b/meter/shelly/energymeter.go
deleted file mode 100644
index 6de49cb2..00000000
--- a/meter/shelly/energymeter.go
+++ /dev/null
@@ -1,71 +0,0 @@
-package shelly
-
-import "github.com/evcc-io/evcc/api"
-
-type EnergyMeter struct {
-	conn *Connection
-}
-
-func NewEnergyMeter(conn *Connection) *EnergyMeter {
-	res := &EnergyMeter{
-		conn: conn,
-	}
-
-	return res
-}
-
-// CurrentPower implements the api.Meter interface
-func (sh *EnergyMeter) CurrentPower() (float64, error) {
-	var res Gen2StatusResponse
-	if err := sh.conn.execGen2Cmd("EM.GetStatus", false, &res); err != nil {
-		return 0, err
-	}
-
-	return res.TotalPower, nil
-}
-
-// TotalEnergy implements the api.Meter interface
-func (sh *EnergyMeter) TotalEnergy() (float64, error) {
-	var res Gen2EmDataStatusResponse
-	if err := sh.conn.execGen2Cmd("EMData.GetStatus", false, &res); err != nil {
-		return 0, err
-	}
-
-	return res.TotalEnergy / 1000, nil
-}
-
-var _ api.PhaseCurrents = (*EnergyMeter)(nil)
-
-// Currents implements the api.PhaseCurrents interface
-func (sh *EnergyMeter) Currents() (float64, float64, float64, error) {
-	var res Gen2StatusResponse
-	if err := sh.conn.execGen2Cmd("EM.GetStatus", false, &res); err != nil {
-		return 0, 0, 0, err
-	}
-
-	return res.CurrentA, res.CurrentB, res.CurrentC, nil
-}
-
-var _ api.PhaseVoltages = (*EnergyMeter)(nil)
-
-// Voltages implements the api.PhaseVoltages interface
-func (sh *EnergyMeter) Voltages() (float64, float64, float64, error) {
-	var res Gen2StatusResponse
-	if err := sh.conn.execGen2Cmd("EM.GetStatus", false, &res); err != nil {
-		return 0, 0, 0, err
-	}
-
-	return res.VoltageA, res.VoltageB, res.VoltageC, nil
-}
-
-var _ api.PhasePowers = (*EnergyMeter)(nil)
-
-// Powers implements the api.PhasePowers interface
-func (sh *EnergyMeter) Powers() (float64, float64, float64, error) {
-	var res Gen2StatusResponse
-	if err := sh.conn.execGen2Cmd("EM.GetStatus", false, &res); err != nil {
-		return 0, 0, 0, err
-	}
-
-	return res.PowerA, res.PowerB, res.PowerC, nil
-}
diff --git a/meter/shelly/gen1.go b/meter/shelly/gen1.go
index 73b32832..d4ed9d44 100644
--- a/meter/shelly/gen1.go
+++ b/meter/shelly/gen1.go
@@ -1,5 +1,16 @@
 package shelly
 
+import (
+	"errors"
+	"fmt"
+	"strings"
+	"time"
+
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/request"
+	"github.com/evcc-io/evcc/util/transport"
+)
+
 // Gen1API endpoint reference: https://shelly-api-docs.shelly.cloud/gen1/#shelly-family-overview
 
 type Gen1SwitchResponse struct {
@@ -23,3 +34,102 @@ type Gen1Status struct {
 		Total_Returned float64
 	}
 }
+
+var _ Generation = (*gen1)(nil)
+
+type gen1 struct {
+	*request.Helper
+	uri     string
+	channel int
+	model   string
+	status  util.Cacheable[Gen1Status]
+}
+
+// newGen1 initializes the connection to the shelly gen1 api and sets up the cached gen1Status
+func newGen1(client *request.Helper, uri, model string, channel int, user, password string, cache time.Duration) *gen1 {
+	if user != "" {
+		client.Transport = transport.BasicAuth(user, password, client.Transport)
+	}
+
+	c := &gen1{
+		Helper:  client,
+		uri:     uri,
+		model:   model,
+		channel: channel,
+	}
+
+	// Cached gen1Status
+	c.status = util.ResettableCached(func() (Gen1Status, error) {
+		var res Gen1Status
+		if err := c.GetJSON(fmt.Sprintf("%s/status", uri), &res); err != nil {
+			return Gen1Status{}, err
+		}
+		return res, nil
+	}, cache)
+
+	return c
+}
+
+func (c *gen1) CurrentPower() (float64, error) {
+	var power float64
+	res, err := c.status.Get()
+	if err != nil {
+		return 0, err
+	}
+
+	switch {
+	case c.channel < len(res.Meters):
+		power = res.Meters[c.channel].Power
+	case c.channel < len(res.EMeters):
+		power = res.EMeters[c.channel].Power
+	default:
+		return 0, errors.New("invalid channel, missing power meter")
+	}
+
+	return power, nil
+}
+
+func (c *gen1) Enabled() (bool, error) {
+	var res Gen1SwitchResponse
+	uri := fmt.Sprintf("%s/relay/%d", c.uri, c.channel)
+	err := c.GetJSON(uri, &res)
+	return res.Ison, err
+}
+
+func (c *gen1) Enable(enable bool) error {
+	onoff := map[bool]string{true: "on", false: "off"}
+
+	c.status.Reset()
+
+	var res Gen1SwitchResponse
+	uri := fmt.Sprintf("%s/relay/%d?turn=%s", c.uri, c.channel, onoff[enable])
+	return c.GetJSON(uri, &res)
+}
+
+func (c *gen1) TotalEnergy() (float64, error) {
+	var energy float64
+	res, err := c.status.Get()
+	if err != nil {
+		return 0, err
+	}
+
+	switch {
+	case c.channel < len(res.Meters):
+		energy = res.Meters[c.channel].Total - res.Meters[c.channel].Total_Returned
+	case c.channel < len(res.EMeters):
+		energy = res.EMeters[c.channel].Total - res.EMeters[c.channel].Total_Returned
+	default:
+		return 0, errors.New("invalid channel, missing power meter")
+	}
+
+	return c.energy(energy) / 1000, nil
+}
+
+// gen1Energy in kWh
+func (c *gen1) energy(energy float64) float64 {
+	// Gen 1 Shelly EM devices are providing Watt hours, Gen 1 Shelly PM devices are providing Watt minutes
+	if !strings.Contains(c.model, "EM") {
+		energy /= 60
+	}
+	return energy
+}
diff --git a/meter/shelly/gen2.go b/meter/shelly/gen2.go
index 639105c4..5264880b 100644
--- a/meter/shelly/gen2.go
+++ b/meter/shelly/gen2.go
@@ -1,5 +1,17 @@
 package shelly
 
+import (
+	"fmt"
+	"net/http"
+	"slices"
+	"strconv"
+	"time"
+
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/request"
+	"github.com/jpfielding/go-http-digest/pkg/digest"
+)
+
 // Gen2API endpoint reference: https://shelly-api-docs.shelly.cloud/gen2/
 
 type Gen2RpcPost struct {
@@ -51,32 +63,239 @@ type Gen2EM1Data struct {
 	TotalActRetEnergy float64 `json:"total_act_ret_energy"`
 }
 
-type Gen2StatusResponse struct {
-	Switch0 Gen2SwitchStatus `json:"switch:0"`
-	Switch1 Gen2SwitchStatus `json:"switch:1"`
-	Switch2 Gen2SwitchStatus `json:"switch:2"`
-	Pm0     Gen2SwitchStatus `json:"pm1:0"`
-	Pm1     Gen2SwitchStatus `json:"pm2:1"`
-	Pm2     Gen2SwitchStatus `json:"pm3:2"`
-	// additional shelly Pro EM meter JSON response
-	TotalPower float64       `json:"total_act_power"`
-	CurrentA   float64       `json:"a_current"`
-	CurrentB   float64       `json:"b_current"`
-	CurrentC   float64       `json:"c_current"`
-	VoltageA   float64       `json:"a_voltage"`
-	VoltageB   float64       `json:"b_voltage"`
-	VoltageC   float64       `json:"c_voltage"`
-	PowerA     float64       `json:"a_act_power"`
-	PowerB     float64       `json:"b_act_power"`
-	PowerC     float64       `json:"c_act_power"`
-	Em0        Gen2EM1Status `json:"em1:0"`
-	Em1        Gen2EM1Status `json:"em1:1"`
-	Em2        Gen2EM1Status `json:"em1:2"`
-	Em0Data    Gen2EM1Data   `json:"em1data:0"`
-	Em1Data    Gen2EM1Data   `json:"em1data:1"`
-	Em2Data    Gen2EM1Data   `json:"em1data:2"`
-}
-
-type Gen2EmDataStatusResponse struct {
-	TotalEnergy float64 `json:"total_act"`
+var _ Generation = (*gen2)(nil)
+
+type gen2 struct {
+	*request.Helper
+	uri          string
+	channel      int
+	model        string
+	profile      string
+	switchstatus util.Cacheable[Gen2SwitchStatus]
+	em1status    func() (Gen2EM1Status, error)
+	em1data      func() (Gen2EM1Data, error)
+	emstatus     func() (Gen2EMStatus, error)
+	emdata       func() (Gen2EMData, error)
+}
+
+func apiCall[T any](c *gen2, api string) func() (T, error) {
+	return func() (T, error) {
+		var res T
+		if err := c.execCmd(fmt.Sprintf("%s?id=%d", api, c.channel), false, &res); err != nil {
+			return res, err
+		}
+		return res, nil
+	}
+}
+
+// gen2InitApi initializes the connection to the shelly gen2+ api and sets up the cached gen2SwitchStatus, gen2EM1Status and gen2EMStatus
+func newGen2(helper *request.Helper, uri, model, profile string, channel int, user, password string, cache time.Duration) *gen2 {
+	c := &gen2{
+		Helper:  helper,
+		uri:     uri,
+		channel: channel,
+		model:   model,
+		profile: profile,
+	}
+
+	// Shelly GEN 2+ API
+	// https://shelly-api-docs.shelly.cloud/gen2/
+	c.uri = fmt.Sprintf("%s/rpc", util.DefaultScheme(uri, "http"))
+	if user != "" {
+		c.Client.Transport = digest.NewTransport(user, password, c.Client.Transport)
+	}
+
+	c.switchstatus = util.ResettableCached(apiCall[Gen2SwitchStatus](c, "Switch.GetStatus"), cache)
+	c.em1status = util.Cached(apiCall[Gen2EM1Status](c, "EM1.GetStatus"), cache)
+	c.em1data = util.Cached(apiCall[Gen2EM1Data](c, "EM1Data.GetStatus"), cache)
+	c.emstatus = util.Cached(apiCall[Gen2EMStatus](c, "EM.GetStatus"), cache)
+	c.emdata = util.Cached(apiCall[Gen2EMData](c, "EMData.GetStatus"), cache)
+
+	return c
+}
+
+// execCmd executes a shelly api gen2+ command and provides the response
+func (c *gen2) execCmd(method string, enable bool, res any) error {
+	// Shelly gen 2 rfc7616 authentication
+	// https://shelly-api-docs.shelly.cloud/gen2/Overview/CommonDeviceTraits#authentication
+	// https://datatracker.ietf.org/doc/html/rfc7616
+
+	data := &Gen2RpcPost{
+		Id:     c.channel,
+		On:     enable,
+		Src:    "evcc",
+		Method: method,
+	}
+
+	req, err := request.New(http.MethodPost, fmt.Sprintf("%s/%s", c.uri, method), request.MarshalJSON(data), request.JSONEncoding)
+	if err != nil {
+		return err
+	}
+
+	return c.DoJSON(req, &res)
+}
+
+// CurrentPower implements the api.Meter interface
+func (c *gen2) CurrentPower() (float64, error) {
+	switch {
+	// Endpoint Switch.GetStatus
+	case c.hasSwitchEndpoint():
+		res, err := c.switchstatus.Get()
+		return res.Apower, err
+
+		// Endpoint EM1.GetStatus
+	case c.hasEMEndpoint() && c.profile == "monophase":
+		res, err := c.em1status()
+		return res.ActPower, err
+
+		// Endpoint EM.GetStatus
+	case c.hasEMEndpoint() && c.profile == "triphase":
+		res, err := c.emstatus()
+		return res.TotalActPower, err
+
+	default:
+		return 0, fmt.Errorf("unknown shelly model: %s", c.model)
+	}
+}
+
+// Gen2Enabled implements the Gen2 api.Charger interface
+func (c *gen2) Enabled() (bool, error) {
+	if c.hasSwitchEndpoint() {
+		res, err := c.switchstatus.Get()
+		return res.Output, err
+	}
+
+	return false, fmt.Errorf("unknown shelly model: %s", c.model)
+}
+
+// Gen2Enable implements the api.Charger interface
+func (c *gen2) Enable(enable bool) error {
+	var res Gen2SwitchStatus
+	c.switchstatus.Reset()
+	return c.execCmd("Switch.Set?id="+strconv.Itoa(c.channel), enable, &res)
+}
+
+// TotalEnergy implements the api.Meter interface
+func (c *gen2) TotalEnergy() (float64, error) {
+	switch {
+	case c.hasSwitchEndpoint():
+		res, err := c.switchstatus.Get()
+		return res.Aenergy.Total / 1000, err
+
+	case c.hasEMEndpoint() && c.profile == "monophase":
+		res, err := c.em1data()
+		return res.TotalActEnergy / 1000, err
+
+	case c.hasEMEndpoint() && c.profile == "triphase":
+		res, err := c.emdata()
+		return res.TotalAct / 1000, err
+
+	default:
+		return 0, fmt.Errorf("unknown shelly model: %s", c.model)
+	}
+}
+
+// Currents implements the api.PhaseCurrents interface
+func (c *gen2) Currents() (float64, float64, float64, error) {
+	switch {
+	case c.hasSwitchEndpoint():
+		res, err := c.switchstatus.Get()
+		return res.Current, 0, 0, err
+
+	case c.hasEMEndpoint() && c.profile == "monophase":
+		res, err := c.em1status()
+		return res.Current, 0, 0, err
+
+	case c.hasEMEndpoint() && c.profile == "triphase":
+		res, err := c.emstatus()
+		return res.ACurrent, res.BCurrent, res.CCurrent, err
+
+	default:
+		return 0, 0, 0, fmt.Errorf("unknown shelly model: %s", c.model)
+	}
+}
+
+// Voltages implements the api.PhaseVoltages interface
+func (c *gen2) Voltages() (float64, float64, float64, error) {
+	switch {
+	case c.hasSwitchEndpoint():
+		res, err := c.switchstatus.Get()
+		return res.Voltage, 0, 0, err
+
+	case c.hasEMEndpoint() && c.profile == "monophase":
+		res, err := c.em1status()
+		return res.Voltage, 0, 0, err
+
+	case c.hasEMEndpoint() && c.profile == "triphase":
+		res, err := c.emstatus()
+		return res.AVoltage, res.BVoltage, res.CVoltage, err
+
+	default:
+		return 0, 0, 0, fmt.Errorf("unknown shelly model: %s", c.model)
+	}
+}
+
+// Powers implements the api.PhasePowers interface
+func (c *gen2) Powers() (float64, float64, float64, error) {
+	switch {
+	case c.hasSwitchEndpoint():
+		res, err := c.switchstatus.Get()
+		return res.Apower, 0, 0, err
+
+	case c.hasEMEndpoint() && c.profile == "monophase":
+		res, err := c.em1status()
+		return res.ActPower, 0, 0, err
+
+	case c.hasEMEndpoint() && c.profile == "triphase":
+		res, err := c.emstatus()
+		return res.AActPower, res.BActPower, res.CActPower, err
+
+	default:
+		return 0, 0, 0, fmt.Errorf("unknown shelly model: %s", c.model)
+	}
+}
+
+// Gen2+ models using Switch.GetStatus endpoint https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/Switch#switchgetstatus-example
+func (c *gen2) hasSwitchEndpoint() bool {
+	// Generation 2 Devices (Plus Series):
+	// - SNSW-001X16EU: Shelly Plus 1 with 1x relay
+	// - SNSW-001P16EU: Shelly Plus 1PM with 1x relay + power meter
+	// - SNSW-002P16EU: Shelly Plus 2PM with 2x relay + power meter
+	// - SNPL-00112EU: Shelly Plus Plug S (EU)
+	// - SNPL-00110IT: Shelly Plus Plug S (Italy)
+	// - SNPL-00112UK: Shelly Plus Plug S (UK)
+	// - SNPL-00116US: Shelly Plus Plug S (US)
+	// Generation 2 Devices (Pro Series - Hutschiene):
+	// - SPSW-001XE16EU: Shelly Pro 1 with 1x relay
+	// - SPSW-001PE16EU: Shelly Pro 1 PM with 1x relay + power meter
+	// - SPSW-002XE16EU: Shelly Pro 2 with 2x relay
+	// - SPSW-002PE16EU: Shelly Pro 2 PM with 2x relay + power meter
+	// - SPSW-003XE16EU: Shelly Pro 3 with 3x relay
+	// - SPSW-004PE16EU: Shelly Pro 4 PM with 4x relay + power meter
+	// Generation 3 Devices:
+	// - S3SW-001P16EU: Shelly 1PM Gen3 with 1x relay + power meter
+	// Generation 4 Devices:
+	// - S4SW-001X8EU: Shelly 1 Mini Gen4 with 1x relay
+	// - S4SW-001P8EU: Shelly 1PM Mini Gen4 with 1x relay + power meter
+	// - S4SW-001P16EU: Shelly 1PM Gen4 with 1x relay + power meter
+	switchModels := []string{"SNSW", "SNPL", "SPSW", "S3SW", "S4SW"}
+	return slices.Contains(switchModels, c.model)
+}
+
+// Gen2+ models using EM1.GetStatus endpoint for power and EM1Data.GetStatus for energy
+// https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/EM1#em1getstatus-example
+// https://shelly-api-docs.shelly.cloud/gen2/ComponentsAndServices/EM1Data#em1datagetstatus-example
+func (c *gen2) hasEMEndpoint() bool {
+	// Generation 2 Devices (Pro Series - Hutschiene):
+	// - SPEM-002CEBEU50: Shelly Pro EM 50 with 1x relay + 2x energy meter
+	// - SPEM-003CEBEU120: Shelly Pro 3EM with 3x energy meter
+	// - SPEM-003CEBEU63: Shelly Pro 3EM-3CT63 with 3x energy meter
+	// Generation 3 Devices:
+	// - S3EM-002CXCEU: Shelly EM Gen 3 with 1x relay + 2x energy meter
+	// - S3EM-001XCEU: Shelly 3EM Gen 3 with 3x energy meter
+	// - S3EM-003CXCEU63: Shelly 3EM Gen 3 with 3x energy meter
+	// Generation 4 Devices:
+	// - S4EM-001PXCEU16: Shelly EM Mini Gen4 with power meter
+	em1Models := []string{"SPEM", "S3EM", "S4EM"}
+	return slices.Contains(em1Models, c.model)
 }
diff --git a/meter/shelly/switch.go b/meter/shelly/switch.go
deleted file mode 100644
index 466c7eec..00000000
--- a/meter/shelly/switch.go
+++ /dev/null
@@ -1,147 +0,0 @@
-package shelly
-
-import (
-	"errors"
-	"fmt"
-	"math"
-	"strings"
-)
-
-type Switch struct {
-	conn *Connection
-}
-
-func NewSwitch(conn *Connection) *Switch {
-	res := &Switch{
-		conn: conn,
-	}
-
-	return res
-}
-
-// CurrentPower implements the api.Meter interface
-func (sh *Switch) CurrentPower() (float64, error) {
-	var power float64
-
-	switch sh.conn.gen {
-	case 0, 1:
-		var res Gen1Status
-		uri := fmt.Sprintf("%s/status", sh.conn.uri)
-		if err := sh.conn.GetJSON(uri, &res); err != nil {
-			return 0, err
-		}
-
-		switch {
-		case sh.conn.channel < len(res.Meters):
-			power = res.Meters[sh.conn.channel].Power
-		case sh.conn.channel < len(res.EMeters):
-			power = res.EMeters[sh.conn.channel].Power
-		default:
-			return 0, errors.New("invalid channel, missing power meter")
-		}
-
-	default:
-		var res Gen2StatusResponse
-		if err := sh.conn.execGen2Cmd("Shelly.GetStatus", false, &res); err != nil {
-			return 0, err
-		}
-
-		switch sh.conn.channel {
-		case 1:
-			power = res.Switch1.Apower + res.Pm1.Apower + res.Em1.ActPower
-		case 2:
-			power = res.Switch2.Apower + res.Pm2.Apower + res.Em2.ActPower
-		default:
-			power = res.Switch0.Apower + res.Pm0.Apower + res.Em0.ActPower
-		}
-	}
-
-	// Assure positive power response (Gen 1 EM devices can provide negative values)
-	return math.Abs(power), nil
-}
-
-// Enabled implements the api.Charger interface
-func (sh *Switch) Enabled() (bool, error) {
-	switch sh.conn.gen {
-	case 0, 1:
-		var res Gen1SwitchResponse
-		uri := fmt.Sprintf("%s/relay/%d", sh.conn.uri, sh.conn.channel)
-		err := sh.conn.GetJSON(uri, &res)
-		return res.Ison, err
-
-	default:
-		var res Gen2SwitchStatus
-		err := sh.conn.execGen2Cmd("Switch.GetStatus", false, &res)
-		return res.Output, err
-	}
-}
-
-// Enable implements the api.Charger interface
-func (sh *Switch) Enable(enable bool) error {
-	var err error
-	onoff := map[bool]string{true: "on", false: "off"}
-
-	switch sh.conn.gen {
-	case 0, 1:
-		var res Gen1SwitchResponse
-		uri := fmt.Sprintf("%s/relay/%d?turn=%s", sh.conn.uri, sh.conn.channel, onoff[enable])
-		err = sh.conn.GetJSON(uri, &res)
-
-	default:
-		var res Gen2SwitchStatus
-		err = sh.conn.execGen2Cmd("Switch.Set", enable, &res)
-	}
-
-	return err
-}
-
-// TotalEnergy implements the api.Meter interface
-func (sh *Switch) TotalEnergy() (float64, error) {
-	var energy float64
-
-	switch sh.conn.gen {
-	case 0, 1:
-		var res Gen1Status
-		uri := fmt.Sprintf("%s/status", sh.conn.uri)
-		if err := sh.conn.GetJSON(uri, &res); err != nil {
-			return 0, err
-		}
-
-		switch {
-		case sh.conn.channel < len(res.Meters):
-			energy = res.Meters[sh.conn.channel].Total
-		case sh.conn.channel < len(res.EMeters):
-			energy = res.EMeters[sh.conn.channel].Total
-		default:
-			return 0, errors.New("invalid channel, missing power meter")
-		}
-
-		energy = gen1Energy(sh.conn.model, energy)
-
-	default:
-		var res Gen2StatusResponse
-		if err := sh.conn.execGen2Cmd("Shelly.GetStatus", false, &res); err != nil {
-			return 0, err
-		}
-
-		switch sh.conn.channel {
-		case 1:
-			energy = res.Switch1.Aenergy.Total + res.Pm1.Aenergy.Total + res.Em1Data.TotalActEnergy - res.Em1Data.TotalActRetEnergy
-		case 2:
-			energy = res.Switch2.Aenergy.Total + res.Pm2.Aenergy.Total + res.Em2Data.TotalActEnergy - res.Em2Data.TotalActRetEnergy
-		default:
-			energy = res.Switch0.Aenergy.Total + res.Pm0.Aenergy.Total + res.Em0Data.TotalActEnergy - res.Em0Data.TotalActRetEnergy
-		}
-	}
-
-	return energy / 1000, nil
-}
-
-// gen1Energy in kWh
-func gen1Energy(devicetype string, energy float64) float64 {
-	// Gen 1 Shelly EM devices are providing Watt hours, Gen 1 Shelly PM devices are providing Watt minutes
-	if !strings.Contains(devicetype, "EM") {
-		energy /= 60
-	}
-	return energy
-}
diff --git a/meter/shelly_decorators.go b/meter/shelly_decorators.go
new file mode 100644
index 00000000..39357a59
--- /dev/null
+++ b/meter/shelly_decorators.go
@@ -0,0 +1,111 @@
+package meter
+
+// Code generated by github.com/evcc-io/evcc/cmd/tools/decorate.go. DO NOT EDIT.
+
+import (
+	"github.com/evcc-io/evcc/api"
+)
+
+func decorateShelly(base *Shelly, phaseVoltages func() (float64, float64, float64, error), phaseCurrents func() (float64, float64, float64, error), phasePowers func() (float64, float64, float64, error)) api.Meter {
+	switch {
+	case phaseCurrents == nil && phaseVoltages == nil:
+		return base
+
+	case phaseCurrents == nil && phaseVoltages != nil:
+		return &struct {
+			*Shelly
+			api.PhaseVoltages
+		}{
+			Shelly: base,
+			PhaseVoltages: &decorateShellyPhaseVoltagesImpl{
+				phaseVoltages: phaseVoltages,
+			},
+		}
+
+	case phaseCurrents != nil && phasePowers == nil && phaseVoltages == nil:
+		return &struct {
+			*Shelly
+			api.PhaseCurrents
+		}{
+			Shelly: base,
+			PhaseCurrents: &decorateShellyPhaseCurrentsImpl{
+				phaseCurrents: phaseCurrents,
+			},
+		}
+
+	case phaseCurrents != nil && phasePowers == nil && phaseVoltages != nil:
+		return &struct {
+			*Shelly
+			api.PhaseCurrents
+			api.PhaseVoltages
+		}{
+			Shelly: base,
+			PhaseCurrents: &decorateShellyPhaseCurrentsImpl{
+				phaseCurrents: phaseCurrents,
+			},
+			PhaseVoltages: &decorateShellyPhaseVoltagesImpl{
+				phaseVoltages: phaseVoltages,
+			},
+		}
+
+	case phaseCurrents != nil && phasePowers != nil && phaseVoltages == nil:
+		return &struct {
+			*Shelly
+			api.PhaseCurrents
+			api.PhasePowers
+		}{
+			Shelly: base,
+			PhaseCurrents: &decorateShellyPhaseCurrentsImpl{
+				phaseCurrents: phaseCurrents,
+			},
+			PhasePowers: &decorateShellyPhasePowersImpl{
+				phasePowers: phasePowers,
+			},
+		}
+
+	case phaseCurrents != nil && phasePowers != nil && phaseVoltages != nil:
+		return &struct {
+			*Shelly
+			api.PhaseCurrents
+			api.PhasePowers
+			api.PhaseVoltages
+		}{
+			Shelly: base,
+			PhaseCurrents: &decorateShellyPhaseCurrentsImpl{
+				phaseCurrents: phaseCurrents,
+			},
+			PhasePowers: &decorateShellyPhasePowersImpl{
+				phasePowers: phasePowers,
+			},
+			PhaseVoltages: &decorateShellyPhaseVoltagesImpl{
+				phaseVoltages: phaseVoltages,
+			},
+		}
+	}
+
+	return nil
+}
+
+type decorateShellyPhaseCurrentsImpl struct {
+	phaseCurrents func() (float64, float64, float64, error)
+}
+
+func (impl *decorateShellyPhaseCurrentsImpl) Currents() (float64, float64, float64, error) {
+	return impl.phaseCurrents()
+}
+
+type decorateShellyPhasePowersImpl struct {
+	phasePowers func() (float64, float64, float64, error)
+}
+
+func (impl *decorateShellyPhasePowersImpl) Powers() (float64, float64, float64, error) {
+	return impl.phasePowers()
+}
+
+type decorateShellyPhaseVoltagesImpl struct {
+	phaseVoltages func() (float64, float64, float64, error)
+}
+
+func (impl *decorateShellyPhaseVoltagesImpl) Voltages() (float64, float64, float64, error) {
+	return impl.phaseVoltages()
+}
diff --git a/meter/shelly_energymeter.go b/meter/shelly_energymeter.go
deleted file mode 100644
index 090a12bb..00000000
--- a/meter/shelly_energymeter.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package meter
-
-import (
-	"github.com/evcc-io/evcc/api"
-	"github.com/evcc-io/evcc/meter/shelly"
-	"github.com/evcc-io/evcc/util"
-)
-
-// Shelly meter implementation
-func init() {
-	registry.Add("shelly-energymeter", NewShellyEnergyMeterFromConfig)
-}
-
-// NewShellyFromConfig creates a Shelly charger from generic config
-func NewShellyEnergyMeterFromConfig(other map[string]interface{}) (api.Meter, error) {
-	var cc struct {
-		URI      string
-		User     string
-		Password string
-		Channel  int
-	}
-
-	if err := util.DecodeOther(other, &cc); err != nil {
-		return nil, err
-	}
-
-	conn, err := shelly.NewConnection(cc.URI, cc.User, cc.Password, cc.Channel)
-	if err != nil {
-		return nil, err
-	}
-
-	return shelly.NewEnergyMeter(conn), nil
-}
diff --git a/meter/tasmota.go b/meter/tasmota.go
index acb1578c..04a69807 100644
--- a/meter/tasmota.go
+++ b/meter/tasmota.go
@@ -1,6 +1,7 @@
 package meter
 
 import (
+	"math"
 	"strings"
 	"time"
 
@@ -23,7 +24,7 @@ func init() {
 //go:generate go tool decorate -f decorateTasmota -b *Tasmota -r api.Meter -t "api.PhaseVoltages,Voltages,func() (float64, float64, float64, error)" -t "api.PhaseCurrents,Currents,func() (float64, float64, float64, error)"
 
 // NewTasmotaFromConfig creates a Tasmota meter from generic config
-func NewTasmotaFromConfig(other map[string]interface{}) (api.Meter, error) {
+func NewTasmotaFromConfig(other map[string]any) (api.Meter, error) {
 	cc := struct {
 		URI      string
 		User     string
@@ -68,19 +69,21 @@ var _ api.Meter = (*Tasmota)(nil)
 
 // CurrentPower implements the api.Meter interface
 func (c *Tasmota) CurrentPower() (float64, error) {
-	if c.usage == "grid" {
-		return c.conn.SmlPower()
+	power, err := c.conn.CurrentPower()
+	if err != nil {
+		return 0, err
+	}
+	// positive power for pv usage
+	if c.usage == "pv" {
+		return math.Abs(power), nil
 	}
-	return c.conn.CurrentPower()
+	return power, nil
 }
 
 var _ api.MeterEnergy = (*Tasmota)(nil)
 
 // TotalEnergy implements the api.MeterEnergy interface
 func (c *Tasmota) TotalEnergy() (float64, error) {
-	if c.usage == "grid" {
-		return c.conn.SmlTotalEnergy()
-	}
 	return c.conn.TotalEnergy()
 }
 
diff --git a/meter/tasmota/connection.go b/meter/tasmota/connection.go
index 4986a4de..078aaecd 100644
--- a/meter/tasmota/connection.go
+++ b/meter/tasmota/connection.go
@@ -208,13 +208,13 @@ func (c *Connection) CurrentPower() (float64, error) {
 		}
 		res += power
 	}
-	return res, nil
+	return res + float64(s.StatusSNS.SML.PowerCurr), nil
 }
 
 // TotalEnergy implements the api.MeterEnergy interface
 func (c *Connection) TotalEnergy() (float64, error) {
 	res, err := c.statusSnsG.Get()
-	return res.StatusSNS.Energy.Total, err
+	return res.StatusSNS.Energy.Total + res.StatusSNS.SML.TotalOut, err
 }
 
 // Currents implements the api.PhaseCurrents interface
@@ -250,15 +250,3 @@ func (c *Connection) getPhaseValues(fun func(StatusSNSResponse) Channels) (float
 
 	return res[0], res[1], res[2], nil
 }
-
-// SmlPower provides the sml sensor power
-func (c *Connection) SmlPower() (float64, error) {
-	res, err := c.statusSnsG.Get()
-	return float64(res.StatusSNS.SML.PowerCurr), err
-}
-
-// SmlTotalEnergy provides the sml sensor total import energy
-func (c *Connection) SmlTotalEnergy() (float64, error) {
-	res, err := c.statusSnsG.Get()
-	return res.StatusSNS.SML.TotalIn, err
-}
diff --git a/server/http.go b/server/http.go
index 3c6572cd..90ee3ce2 100644
--- a/server/http.go
+++ b/server/http.go
@@ -39,17 +39,6 @@ type HTTPd struct {
 	*http.Server
 }
 
-// loggingResponseWriter wraps http.ResponseWriter to capture status code
-type loggingResponseWriter struct {
-	http.ResponseWriter
-	statusCode int
-}
-
-func (lrw *loggingResponseWriter) WriteHeader(code int) {
-	lrw.statusCode = code
-	lrw.ResponseWriter.WriteHeader(code)
-}
-
 // NewHTTPd creates HTTP server with configured routes for loadpoint
 func NewHTTPd(addr string, hub *SocketHub) *HTTPd {
 	router := mux.NewRouter().StrictSlash(true)
@@ -59,17 +48,8 @@ func NewHTTPd(addr string, hub *SocketHub) *HTTPd {
 	// log all requests
 	router.Use(func(next http.Handler) http.Handler {
 		return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-			// simple logging for websocket connections
-			if r.Header.Get("Upgrade") == "websocket" {
-				log.TRACE.Printf("%s %s", r.Method, r.URL.Path)
-				next.ServeHTTP(w, r)
-				return
-			}
-
-			// capture status code
-			lrw := &loggingResponseWriter{ResponseWriter: w, statusCode: http.StatusOK}
-			next.ServeHTTP(lrw, r)
-			log.TRACE.Printf("%s %s %d", r.Method, r.URL.Path, lrw.statusCode)
+			log.TRACE.Printf("%s %s", r.Method, r.URL.Path)
+			next.ServeHTTP(w, r)
 		})
 	})
 
diff --git a/templates/definition/charger/vaillant.yaml b/templates/definition/charger/vaillant.yaml
index 18cd14f3..8e637977 100644
--- a/templates/definition/charger/vaillant.yaml
+++ b/templates/definition/charger/vaillant.yaml
@@ -12,6 +12,10 @@ requirements:
 params:
   - name: user
   - name: password
+  - name: realm
+    type: choice
+    choice: [DE, AT]
+    default: DE
   - name: zone
     type: int
     help:
@@ -28,5 +32,6 @@ render: |
   type: vaillant
   user: {{ .user }}
   password: {{ .password }}
+  realm: {{ if eq .realm "AT" -}} vaillant-austria-b2c {{- end }}
   heatingzone: {{ .zone }}
   heatingsetpoint: {{ .setpoint }}
diff --git a/templates/definition/meter/shelly-1pm.yaml b/templates/definition/meter/shelly-1pm.yaml
index bb638349..404b03a4 100644
--- a/templates/definition/meter/shelly-1pm.yaml
+++ b/templates/definition/meter/shelly-1pm.yaml
@@ -21,4 +21,5 @@ render: |
   {{- if .password }}
   password: {{ .password }}
   {{- end }}
+  usage: {{ .usage }}
   channel: {{ .channel }}  # shelly device relay channel
diff --git a/templates/definition/meter/shelly-pro-3em.yaml b/templates/definition/meter/shelly-pro-3em.yaml
index 30d3a55a..8ae0d48e 100644
--- a/templates/definition/meter/shelly-pro-3em.yaml
+++ b/templates/definition/meter/shelly-pro-3em.yaml
@@ -10,7 +10,7 @@ params:
   - name: user
   - name: password
 render: |
-  type: shelly-energymeter
+  type: shelly
   uri: http://{{ .host }}  # shelly device ip address (local)
   {{- if .user }}
   user: {{ .user }}
@@ -18,4 +18,5 @@ render: |
   {{- if .password }}
   password: {{ .password }}
   {{- end }}
+  usage: {{ .usage }}
   channel: 0  # shelly device relay channel
diff --git a/templates/definition/meter/tasmota-sml.yaml b/templates/definition/meter/tasmota-sml.yaml
index 1ff9a4bb..73a09abf 100644
--- a/templates/definition/meter/tasmota-sml.yaml
+++ b/templates/definition/meter/tasmota-sml.yaml
@@ -56,7 +56,7 @@ requirements:
       ```
 params:
   - name: usage
-    choice: ["grid"]
+    choice: ["grid", "pv", "battery", "charge"]
   - name: host
   - name: user
     required: false
diff --git a/templates/definition/meter/tasmota.yaml b/templates/definition/meter/tasmota.yaml
index fd71e6ee..792c7916 100644
--- a/templates/definition/meter/tasmota.yaml
+++ b/templates/definition/meter/tasmota.yaml
@@ -1,11 +1,12 @@
 template: tasmota
 products:
-  - description:
-      generic: Tasmota (1 Phase Meter)
+  - brand: Tasmota
+    description:
+      generic: Tasmota (1 Phase + SML Meter)
 group: switchsockets
 params:
   - name: usage
-    choice: ["pv", "battery", "charge"]
+    choice: ["grid", "pv", "battery", "charge"]
   - name: host
   - name: user
     required: false
diff --git a/templates/definition/tariff/green-grid-compass.yaml b/templates/definition/tariff/green-grid-compass.yaml
index ebae8515..19d514ac 100644
--- a/templates/definition/tariff/green-grid-compass.yaml
+++ b/templates/definition/tariff/green-grid-compass.yaml
@@ -28,6 +28,7 @@ params:
     default: DE_LU
 render: |
   type: custom
+  tariff: solar
   forecast:
     source: http
     uri: https://eco2grid.com/green-grid-compass/v2/forecast/hourly?zone_code={{ .zone }}&start={{ `{{ now | date "2006-01-02T15" }}` }}:00:00&end={{ `{{ addDate now 0 0 2 | date "2006-01-02T15" }}` }}:00:00&apikey={{ .apiKey }}
diff --git a/templates/definition/tariff/nordpool.yaml b/templates/definition/tariff/nordpool.yaml
index 10d5700d..2a36914b 100644
--- a/templates/definition/tariff/nordpool.yaml
+++ b/templates/definition/tariff/nordpool.yaml
@@ -46,6 +46,7 @@ params:
   - preset: tariff-base
 render: |
   type: custom
+  {{ include "tariff-base" . }}
   forecast:
     source: go
     script: |
@@ -79,4 +80,3 @@ render: |
                 "value": .entryPerArea.{{ .region }} / 1000
               }
             ] | tostring
-  {{ include "tariff-base" . }}
diff --git a/util/modbus/functions.go b/util/modbus/functions.go
index d5c0bc87..5c745896 100644
--- a/util/modbus/functions.go
+++ b/util/modbus/functions.go
@@ -7,8 +7,15 @@ import (
 	"fmt"
 	"strconv"
 	"strings"
+	"time"
+
+	"github.com/cenkalti/backoff/v4"
 )
 
+func Backoff() *backoff.ExponentialBackOff {
+	return backoff.NewExponentialBackOff(backoff.WithInitialInterval(20*time.Millisecond), backoff.WithMaxElapsedTime(5*time.Second))
+}
+
 // decodeMask converts a bit mask in decimal or hex format to uint64
 func decodeMask(mask string) (uint64, error) {
 	mask = strings.ToLower(mask)
diff --git a/vehicle/tesla.go b/vehicle/tesla.go
index 21e7da42..59765df4 100644
--- a/vehicle/tesla.go
+++ b/vehicle/tesla.go
@@ -47,7 +47,7 @@ func NewTeslaFromConfig(other map[string]interface{}) (api.Vehicle, error) {
 	}
 
 	if cc.Credentials.ID == "" {
-		return nil, errors.New("missing client id, see https://github.com/evcc-io/evcc/discussions/17501")
+		return nil, errors.New("missing client id, see https://docs.evcc.io/en/docs/devices/vehicles#tesla")
 	}
 
 	token, err := cc.Tokens.Token()


Test patch:
diff --git a/charger/easee_test.go b/charger/easee_test.go
index 29dd4965..809c28ef 100644
--- a/charger/easee_test.go
+++ b/charger/easee_test.go
@@ -2,11 +2,16 @@ package charger
 
 import (
 	"encoding/json"
+	"fmt"
+	"net/http"
 	"testing"
 	"time"
 
+	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/charger/easee"
 	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/request"
+	"github.com/jarcoal/httpmock"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -22,12 +27,18 @@ func createPayload(id easee.ObservationID, timestamp time.Time, dataType easee.D
 	return json.RawMessage(out)
 }
 
-func newEasee() Easee {
-	return Easee{
+func newEasee() *Easee {
+	log := util.NewLogger("easee")
+	e := Easee{
+		Helper:    request.NewHelper(log),
 		obsTime:   make(map[easee.ObservationID]time.Time),
-		log:       util.NewLogger("easee"),
+		log:       log,
 		startDone: func() {},
+		cmdC:      make(chan easee.SignalRCommandResponse),
+		obsC:      make(chan easee.Observation),
 	}
+	e.Client.Timeout = 500 * time.Millisecond //aggressive timeout to accelerate testing
+	return &e
 }
 
 func TestProductUpdate_IgnoreOutdatedProductUpdate(t *testing.T) {
@@ -49,3 +60,282 @@ func TestProductUpdate_IgnoreOutdatedProductUpdate(t *testing.T) {
 	assert.Equal(t, now, e.obsTime[easee.CHARGER_OP_MODE])
 	assert.Equal(t, 2, e.opMode)
 }
+
+func TestProductUpdate_InitialStateCheck(t *testing.T) {
+	now := time.Now().UTC().Truncate(0) //truncate removes sub nanos
+
+	e := newEasee()
+
+	assert.False(t, e.initialStatePresent)
+
+	tc := []struct {
+		obsId           easee.ObservationID
+		dataType        easee.DataType
+		value           string
+		expectInitState bool
+	}{
+		{easee.CHARGER_OP_MODE, easee.Integer, "2", false},
+		{easee.TOTAL_POWER, easee.Double, "11.0", false},
+		{easee.SESSION_ENERGY, easee.Double, "22.0", false},
+		{easee.LIFETIME_ENERGY, easee.Double, "1000.0", true},
+	}
+
+	for _, tc := range tc {
+		t.Logf("%+v", tc)
+
+		e.ProductUpdate(createPayload(tc.obsId, now, tc.dataType, tc.value))
+		assert.Equal(t, tc.expectInitState, e.initialStatePresent)
+	}
+}
+
+// TestInExpectedOpMode tests the inExpectedOpMode function with different scenarios
+func TestInExpectedOpMode(t *testing.T) {
+	tc := []struct {
+		opMode int
+		enable bool
+		expect bool
+	}{
+		{easee.ModeDisconnected, false, false},
+		{easee.ModeAwaitingAuthentication, false, true},
+		{easee.ModeAwaitingStart, false, true},
+		{easee.ModeOffline, false, false},
+
+		//enable cases
+		{easee.ModeAwaitingAuthentication, true, false},
+		{easee.ModeOffline, true, false},
+		{easee.ModeCharging, true, true},
+		{easee.ModeCompleted, true, true},
+		{easee.ModeAwaitingStart, true, true},
+		{easee.ModeReadyToCharge, true, true},
+	}
+	for _, tc := range tc {
+		t.Logf("%+v", tc)
+
+		e := newEasee()
+		e.opMode = tc.opMode
+		res := e.inExpectedOpMode(tc.enable)
+		assert.Equal(t, tc.expect, res)
+	}
+}
+
+func TestEasee_waitForTickResponse(t *testing.T) {
+	testCases := []struct {
+		name         string
+		expectedTick int64
+		cmdCValue    *easee.SignalRCommandResponse
+		expectedErr  error
+	}{
+		{
+			name:         "Success - Tick Found",
+			expectedTick: 123,
+			cmdCValue:    &easee.SignalRCommandResponse{Ticks: 123, WasAccepted: true},
+			expectedErr:  nil,
+		},
+		{
+			name:         "Success - Tick Found, but Rejected",
+			expectedTick: 456,
+			cmdCValue:    &easee.SignalRCommandResponse{Ticks: 456, WasAccepted: false},
+			expectedErr:  fmt.Errorf("command rejected: %d", 456),
+		},
+		{
+			name:         "Timeout",
+			expectedTick: 789,
+			expectedErr:  api.ErrTimeout,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			t.Logf("%+v", tc)
+
+			e := newEasee()
+
+			// Set up the command channel for test and Easee to share
+			cmdC := make(chan easee.SignalRCommandResponse, 1) // make it buffered for ease of testing
+			e.cmdC = cmdC
+
+			if tc.cmdCValue != nil {
+				cmdC <- *tc.cmdCValue
+			}
+
+			err := e.waitForTickResponse(tc.expectedTick)
+
+			// Assert the result
+			if tc.expectedErr != nil {
+				assert.EqualError(t, err, tc.expectedErr.Error())
+			} else {
+				assert.NoError(t, err)
+			}
+		})
+	}
+}
+
+func TestEasee_postJsonAndWait(t *testing.T) {
+	const chargerID string = "TESTTEST"
+	const ticks int64 = 638798974487432600
+
+	settingsUri := fmt.Sprintf("%s/chargers/%s/settings", easee.API, chargerID)
+	commandUri := fmt.Sprintf("%s/chargers/%s/commands/resume_charging", easee.API, chargerID)
+
+	settingsReply := fmt.Sprintf("{\"device\":\"%s\",\"commandId\":48,\"ticks\":%d}", chargerID, ticks)
+
+	cmdResponse := easee.SignalRCommandResponse{
+		WasAccepted: true,
+		Ticks:       ticks,
+	}
+
+	testCases := []struct {
+		uri      string
+		httpRc   int
+		respBody string
+		cmdResp  *easee.SignalRCommandResponse
+		noop     bool
+		err      error
+	}{
+		{settingsUri, 200, "", nil, false, nil},                                   //sync reply
+		{settingsUri, 202, "[]", nil, true, nil},                                  //noop reply
+		{settingsUri, 202, "[" + settingsReply + "]", nil, false, api.ErrTimeout}, //timeout
+		{commandUri, 202, "{}", nil, true, nil},                                   //noop command reply
+		{commandUri, 202, settingsReply, &cmdResponse, false, nil},                //command reply
+		{commandUri, 400, "", nil, false, fmt.Errorf("invalid status: %d", 400)},  //unexpected result
+	}
+
+	for _, tc := range testCases {
+		t.Logf("%+v", tc)
+
+		e := newEasee()
+
+		httpmock.ActivateNonDefault(e.Client)
+		httpmock.RegisterResponder(http.MethodPost, tc.uri,
+			httpmock.NewStringResponder(tc.httpRc, tc.respBody))
+
+		if tc.cmdResp != nil {
+			go func() {
+				e.cmdC <- *tc.cmdResp
+			}()
+		}
+
+		noop, err := e.postJSONAndWait(tc.uri, nil)
+
+		assert.Equal(t, tc.noop, noop)
+		assert.Equal(t, tc.err, err)
+
+		httpmock.Reset()
+	}
+}
+
+func TestEasee_waitForChargerEnabledState(t *testing.T) {
+	testCases := []struct {
+		expEnabled  bool
+		updateState bool
+		sendObs     bool
+		expectErr   error
+	}{
+		{false, false, false, nil},           // short circuit, already in target state
+		{true, true, true, nil},              // normal flow
+		{true, false, false, api.ErrTimeout}, // missing state change
+		{true, true, false, nil},             // late landing state change (transition without Obs)
+	}
+
+	for _, tc := range testCases {
+		t.Logf("%+v", tc)
+
+		e := newEasee()
+		e.opMode = easee.ModeAwaitingAuthentication
+
+		if tc.updateState { // simulate state changes
+			go func() {
+				e.opMode = easee.ModeCharging // transition to charging
+				if tc.sendObs {
+					e.obsC <- easee.Observation{
+						ID: easee.CHARGER_OP_MODE,
+					}
+				}
+			}()
+		}
+
+		err := e.waitForChargerEnabledState(tc.expEnabled)
+
+		if tc.expectErr != nil {
+			assert.EqualError(t, err, tc.expectErr.Error())
+		} else {
+			assert.NoError(t, err)
+		}
+	}
+}
+
+func TestEasee_waitForDynamicChargerCurrent(t *testing.T) {
+	testCases := []struct {
+		expectedDcc float64
+		updateState bool
+		sendObs     bool
+		expectErr   error
+	}{
+		{6, false, false, nil},             // short circuit, already at target dcc
+		{32, true, true, nil},              // normal flow
+		{32, false, false, api.ErrTimeout}, // missing state change
+		{32, true, false, nil},             // late landing state change (transition without Obs)
+	}
+
+	for _, tc := range testCases {
+		t.Logf("%+v", tc)
+
+		e := newEasee()
+		e.dynamicChargerCurrent = 6
+
+		if tc.updateState { // simulate state changes
+			go func() {
+				e.dynamicChargerCurrent = 32 // transition to 32A
+				if tc.sendObs {
+					e.obsC <- easee.Observation{
+						ID:       easee.DYNAMIC_CHARGER_CURRENT,
+						DataType: easee.Double,
+						Value:    "32",
+					}
+				}
+			}()
+		}
+
+		err := e.waitForDynamicChargerCurrent(tc.expectedDcc)
+
+		if tc.expectErr != nil {
+			assert.EqualError(t, err, tc.expectErr.Error())
+		} else {
+			assert.NoError(t, err)
+		}
+	}
+}
+
+func TestEasee_MaxCurrent(t *testing.T) {
+	testCases := []struct {
+		targetCurrent int64
+		expectCurrent float64
+	}{
+		{6, 6},   // short circuit
+		{32, 16}, // target above max
+		{10, 10}, // normal case
+	}
+
+	for _, tc := range testCases {
+		t.Logf("%+v", tc)
+
+		e := newEasee()
+		e.charger = "CHARGERID"
+		e.maxChargerCurrent = 16
+		e.dynamicChargerCurrent = 6
+
+		uriPattern := fmt.Sprintf("=~%s.*", easee.API)
+
+		//register mock NoOp reply, suffices for this test case
+		httpmock.ActivateNonDefault(e.Client)
+		httpmock.RegisterResponder(http.MethodPost, uriPattern,
+			httpmock.NewStringResponder(202, "[]"))
+
+		err := e.MaxCurrent(tc.targetCurrent)
+
+		assert.NoError(t, err)
+		assert.Equal(t, tc.expectCurrent, e.current)
+		//TODO this fails, either current or dynamicChargerCurrent need to go
+		//assert.Equal(t, e.current, e.dynamicChargerCurrent)
+	}
+}
diff --git a/meter/shelly/gen1_test.go b/meter/shelly/gen1_test.go
index ac1f50fa..7f9dc6b2 100644
--- a/meter/shelly/gen1_test.go
+++ b/meter/shelly/gen1_test.go
@@ -17,7 +17,8 @@ func TestUnmarshalGen1Status(t *testing.T) {
 		jsonstr := `{"wifi_sta":{"connected":true,"ssid":"XXXX-WLAN","ip":"192.168.178.XXX","rssi":-54},"cloud":{"enabled":false,"connected":false},"mqtt":{"connected":false},"time":"17:59","unixtime":1676134770,"serial":2437,"has_update":true,"mac":"84CCA8XXXXXXX","cfg_changed_cnt":1,"actions_stats":{"skipped":0},"relays":[{"ison":false,"has_timer":false,"timer_started":0,"timer_duration":0,"timer_remaining":0,"overpower":false,"source":"http"}],"meters":[{"power":4711.12,"overpower":0.00,"is_valid":true,"timestamp":1676138370,"counters":[0.000, 0.000, 0.000],"total":6472513}],"inputs":[{"input":0,"event":"","event_cnt":0}],"temperature":16.79,"overtemperature":false,"tmp":{"tC":16.79,"tF":62.22, "is_valid":true},"temperature_status":"Normal","ext_sensors":{},"ext_temperature":{},"ext_humidity":{},"update":{"status":"pending","has_update":true,"new_version":"20221108-153925/v1.12.1-1PM-fix-g2821131","old_version":"20220209-094317/v1.11.8-g8c7bb8d"},"ram_total":50456,"ram_free":37056,"fs_size":233681,"fs_free":149094,"uptime":17284290}`
 		require.NoError(t, json.Unmarshal([]byte(jsonstr), &res))
 
-		assert.Equal(t, 107875.21666666666, gen1Energy("SHSW-PM", res.Meters[0].Total))
+		g := &gen1{model: "SHSW-PM"}
+		assert.Equal(t, 107875.21666666666, g.energy(res.Meters[0].Total))
 		assert.Equal(t, 4711.12, res.Meters[0].Power)
 	}
 
@@ -28,7 +29,8 @@ func TestUnmarshalGen1Status(t *testing.T) {
 		jsonstr := `{"wifi_sta":{"connected":true,"ssid":"XXXX-WLAN","ip":"192.168.178.XXX","rssi":-57},"cloud":{"enabled":false,"connected":false},"mqtt":{"connected":false},"time":"19:25","unixtime":1676139913,"serial":959,"has_update":true,"mac":"E8DB8XXXXXX","cfg_changed_cnt":1,"actions_stats":{"skipped":0},"relays":[{"ison":false,"has_timer":false,"timer_started":0,"timer_duration":0,"timer_remaining":0,"source":"timer"}],"meters":[{"power":81.5,"is_valid":true}],"inputs":[{"input":0,"event":"","event_cnt":0}],"ext_sensors":{},"ext_temperature":{},"ext_humidity":{},"update":{"status":"pending","has_update":true,"new_version":"20221027-091427/v1.12.1-ga9117d3","old_version":"20211109-124958/v1.11.7-g682a0db"},"ram_total":50880,"ram_free":38796,"fs_size":233681,"fs_free":151102,"uptime":20319391}`
 		require.NoError(t, json.Unmarshal([]byte(jsonstr), &res))
 
-		assert.Equal(t, 0.0, gen1Energy("SHSW-1", res.Meters[0].Total))
+		g := &gen1{model: "SHSW-1"}
+		assert.Equal(t, 0.0, g.energy(res.Meters[0].Total))
 		assert.Equal(t, 81.5, res.Meters[0].Power)
 	}
 
@@ -39,7 +41,8 @@ func TestUnmarshalGen1Status(t *testing.T) {
 		jsonstr := `{"wifi_sta":{"connected":true,"ssid":"XXXX","ip":"192.168.178.XXX","rssi":-55},"cloud":{"enabled":false,"connected":false},"mqtt":{"connected":false},"time":"11:16","unixtime":1676110566,"serial":21580,"has_update":false,"mac":"C45BXXXXX","cfg_changed_cnt":0,"actions_stats":{"skipped":0},"relays":[{"ison":false,"has_timer":false,"timer_started":0,"timer_duration":0,"timer_remaining":0,"overpower":false,"is_valid":true,"source":"input"}],"emeters":[{"power":-620.34,"reactive":714.48,"pf":-0.66,"voltage":235.68,"is_valid":true,"total":401472.9,"total_returned":653673.7},{"power":0.00,"reactive":0.00,"pf":0.00,"voltage":235.68,"is_valid":true,"total":173411.3,"total_returned":294.2}],"update":{"status":"idle","has_update":false,"new_version":"20221027-105518/v1.12.1-ga9117d3","old_version":"20221027-105518/v1.12.1-ga9117d3"},"ram_total":51072,"ram_free":35660,"fs_size":233681,"fs_free":156373,"uptime":2226140}`
 		require.NoError(t, json.Unmarshal([]byte(jsonstr), &res))
 
-		assert.Equal(t, 401472.9, gen1Energy("SHEM", res.EMeters[0].Total))
+		g := &gen1{model: "SHEM"}
+		assert.Equal(t, 401472.9, g.energy(res.EMeters[0].Total))
 		assert.Equal(t, -620.34, res.EMeters[0].Power)
 	}
 }
diff --git a/meter/shelly/gen2_test.go b/meter/shelly/gen2_test.go
index efeb1de8..b12ac0c8 100644
--- a/meter/shelly/gen2_test.go
+++ b/meter/shelly/gen2_test.go
@@ -11,70 +11,30 @@ import (
 // Test Gen2+ status responses
 func TestUnmarshalGen2StatusResponse(t *testing.T) {
 	{
-		// Shelly Pro 1PM channel 0 (1)
-		var res Gen2StatusResponse
-
-		jsonstr := `{"ble":{},"cloud":{"connected":true},"eth":{"ip":null},"input:0":{"id":0,"state":false},"input:1":{"id":1,"state":false},"mqtt":{"connected":false},"switch:0":{"id":0, "source":"HTTP", "output":false, "apower":47.11, "voltage":232.0, "current":0.000, "pf":0.00, "aenergy":{"total":5.125,"by_minute":[0.000,0.000,0.000],"minute_ts":1675718520},"temperature":{"tC":25.3, "tF":77.5}},"sys":{"mac":"30C6F78BB4D8","restart_required":false,"time":"22:22","unixtime":1675718522,"uptime":45070,"ram_size":234204,"ram_free":137716,"fs_size":524288,"fs_free":172032,"cfg_rev":13,"kvs_rev":1,"schedule_rev":0,"webhook_rev":0,"available_updates":{"beta":{"version":"0.13.0-beta3"}}},"wifi":{"sta_ip":"192.168.178.64","status":"got ip","ssid":"***","rssi":-62},"ws":{"connected":false}}`
-		require.NoError(t, json.Unmarshal([]byte(jsonstr), &res))
-
-		assert.Equal(t, 5.125, res.Switch0.Aenergy.Total)
-		assert.Equal(t, 47.11, res.Switch0.Apower)
-	}
-
-	{
-		// Shelly PM Mini Gen3 channel 0 (1)
-		var res Gen2StatusResponse
-
-		jsonstr := `{"ble":{},"cloud":{"connected":true},"mqtt":{"connected":false},"pm1:0":{"id":0, "voltage":239.9, "current":7.434, "apower":1780.1 ,"freq":50.1,"aenergy":{"total":3551.682,"by_minute":[15234.772,29611.247,29825.821],"minute_ts":1719917850},"ret_aenergy":{"total":0.000,"by_minute":[0.000,0.000,0.000],"minute_ts":1719917850}},"sys":{"mac":"84FCE638D818","restart_required":false,"time":"12:57","unixtime":1719917851,"uptime":62328,"ram_size":261744,"ram_free":151436,"fs_size":1048576,"fs_free":712704,"cfg_rev":10,"kvs_rev":1,"schedule_rev":0,"webhook_rev":0,"available_updates":{"stable":{"version":"1.3.3"}},"reset_reason":1},"wifi":{"sta_ip":"192.168.178.89","status":"got ip","ssid":"FritzBox 8 2.4","rssi":-62},"ws":{"connected":false}}`
+		// Switch.GetStatus Endpoint
+		var res Gen2SwitchStatus
+		jsonstr := `{"id":0, "source":"HTTP", "output":false, "apower":47.11, "voltage":232.0, "current":0.000, "pf":0.00, "aenergy":{"total":5.125,"by_minute":[0.000,0.000,0.000],"minute_ts":1675718520},"temperature":{"tC":25.3, "tF":77.5}}`
 		require.NoError(t, json.Unmarshal([]byte(jsonstr), &res))
-
-		assert.Equal(t, 3551.682, res.Pm0.Aenergy.Total)
-		assert.Equal(t, 1780.1, res.Pm0.Apower)
+		assert.Equal(t, 5.125, res.Aenergy.Total)
+		assert.Equal(t, 47.11, res.Apower)
 	}
 
 	{
-		// Shelly Pro 3EM
-		var res Gen2StatusResponse
-
-		jsonstr := `{"ble":{},"bthome":{"errors":["bluetooth_disabled"]},"cloud":{"connected":true},"em1:0":{"id":0,"current":3.705,"voltage":242.8,"act_power":598.9,"aprt_power":900.6,"pf":0.66,"freq":50.0,"calibration":"factory"},"em1:1":{"id":1,"current":0.194,"voltage":242.8,"act_power":0.0,"aprt_power":47.2,"pf":0.00,"freq":50.0,"calibration":"factory"},"em1:2":{"id":2,"current":0.027,"voltage":242.8,"act_power":0.0,"aprt_power":6.6,"pf":0.00,"freq":50.0,"calibration":"factory"},"em1data:0":{"id":0,"total_act_energy":3458.24,"total_act_ret_energy":1605.24},"em1data:1":{"id":1,"total_act_energy":2768.67,"total_act_ret_energy":25.49},"em1data:2":{"id":2,"total_act_energy":3.09,"total_act_ret_energy":0.71},"eth":{"ip":null},"modbus":{},"mqtt":{"connected":false},"sys":{"mac":"FCE8C0DBA850","restart_required":false,"time":"19:46","unixtime":1731404780,"uptime":563,"ram_size":247148,"ram_free":110596,"fs_size":524288,"fs_free":176128,"cfg_rev":21,"kvs_rev":0,"schedule_rev":3,"webhook_rev":1,"available_updates":{},"reset_reason":3},"temperature:0":{"id": 0,"tC":39.0, "tF":102.2},"wifi":{"sta_ip":"192.168.40.174","status":"got ip","ssid":"IoT","rssi":-67},"ws":{"connected":false}}`
+		// EM1.GetStatus Endpoint
+		var res Gen2EM1Status
+		jsonstr := `{"id":"0","current":1.473,"voltage":226.9,"act_power":-332.2,"aprt_power":335,"pf":0.99,"freq":50,"calibration":"factory"}`
 		require.NoError(t, json.Unmarshal([]byte(jsonstr), &res))
-		// Channel 0 (1)
-		assert.Equal(t, 598.9, res.Em0.ActPower)
-		assert.Equal(t, 3.705, res.Em0.Current)
-		assert.Equal(t, 242.8, res.Em0.Voltage)
-		assert.Equal(t, 3458.24, res.Em0Data.TotalActEnergy)
-		assert.Equal(t, 1605.24, res.Em0Data.TotalActRetEnergy)
-		// Channel 1 (2)
-		assert.Equal(t, 0.0, res.Em1.ActPower)
-		assert.Equal(t, 0.194, res.Em1.Current)
-		assert.Equal(t, 242.8, res.Em1.Voltage)
-		assert.Equal(t, 2768.67, res.Em1Data.TotalActEnergy)
-		assert.Equal(t, 25.49, res.Em1Data.TotalActRetEnergy)
-		// Channel 2 (3)
-		assert.Equal(t, 0.0, res.Em2.ActPower)
-		assert.Equal(t, 0.027, res.Em2.Current)
-		assert.Equal(t, 242.8, res.Em2.Voltage)
-		assert.Equal(t, 3.09, res.Em2Data.TotalActEnergy)
-		assert.Equal(t, 0.71, res.Em2Data.TotalActRetEnergy)
+		assert.Equal(t, -332.2, res.ActPower)
+		assert.Equal(t, 1.473, res.Current)
+		assert.Equal(t, 226.9, res.Voltage)
 	}
 
 	{
-		// Shelly Pro EM-50 channel 0 + 1
-		var res Gen2StatusResponse
-
-		jsonstr := `{"ble":{},"bthome":{"errors":["bluetooth_disabled"]},"cloud":{"connected":true},"em1:0":{"id":0,"current":1.473,"voltage":226.9,"act_power":-332.2,"aprt_power":335.0,"pf":0.99,"freq":50.0,"calibration":"factory"},"em1:1":{"id":1,"current":0.428,"voltage":227.0,"act_power":-38.5,"aprt_power":97.4,"pf":0.38,"freq":50.0,"calibration":"factory"},"em1data:0":{"id":0,"total_act_energy":1264.15,"total_act_ret_energy":144792.28},"em1data:1":{"id":1,"total_act_energy":48002.83,"total_act_ret_energy":33241.59},"eth":{"ip":null},"modbus":{},"mqtt":{"connected":false},"switch:0":{"id":0, "source":"HTTP_in", "output":false,"temperature":{"tC":46.4, "tF":115.5}},"sys":{"mac":"08F9E0E8AF2C","restart_required":false,"time":"10:42","unixtime":1742809323,"uptime":3671372,"ram_size":249680,"ram_free":107492,"fs_size":524288,"fs_free":188416,"cfg_rev":12,"kvs_rev":0,"schedule_rev":1,"webhook_rev":0,"available_updates":{"beta":{"version":"1.5.1-beta2"}},"reset_reason":3},"wifi":{"sta_ip":"192.168.1.120","status":"got ip","ssid":"Spaetzlewerk","rssi":-61},"ws":{"connected":false}}`
+		// EM1Data.GetStatus Endpoint
+		var res Gen2EM1Data
+		jsonstr := `{"id":"0","total_act_energy":1264.15,"total_act_ret_energy":144792.28}`
 		require.NoError(t, json.Unmarshal([]byte(jsonstr), &res))
-		// Channel 0 (1)
-		assert.Equal(t, -332.2, res.Em0.ActPower)
-		assert.Equal(t, 1.473, res.Em0.Current)
-		assert.Equal(t, 226.9, res.Em0.Voltage)
-		assert.Equal(t, 1264.15, res.Em0Data.TotalActEnergy)
-		assert.Equal(t, 144792.28, res.Em0Data.TotalActRetEnergy)
-		// Channel 1 (2)
-		assert.Equal(t, -38.5, res.Em1.ActPower)
-		assert.Equal(t, 0.428, res.Em1.Current)
-		assert.Equal(t, 227.0, res.Em1.Voltage)
-		assert.Equal(t, 48002.83, res.Em1Data.TotalActEnergy)
-		assert.Equal(t, 33241.59, res.Em1Data.TotalActRetEnergy)
+		assert.Equal(t, 1264.15, res.TotalActEnergy)
+		assert.Equal(t, 144792.28, res.TotalActRetEnergy)
 	}
 }
diff --git a/tests/simulator/api.js b/tests/simulator/api.js
index c8e0ef8e..c55e8fed 100644
--- a/tests/simulator/api.js
+++ b/tests/simulator/api.js
@@ -72,15 +72,12 @@ const teslaloggerMiddleware = (req, res, next) => {
 };
 
 const shellyMiddleware = (req, res, next) => {
-  // simulate a shelly gen1 device api. implement power and energy
+  // simulate a shelly gen2 device api. implement power and energy
   if (req.method === "GET" && req.originalUrl === "/shelly") {
-    res.end(JSON.stringify({ gen: 2 }));
-  } else if (req.originalUrl === "/rpc/Shelly.GetStatus") {
+    res.end(JSON.stringify({ model: "SNSW-001P16EU", gen: 2 }));
+  } else if (req.originalUrl === "/rpc/Switch.GetStatus?id=0") {
     res.end(
-      JSON.stringify({
-        "switch:0": { apower: state.site.pv.power, aenergy: { total: state.site.pv.energy } },
-      })
-    );
+      JSON.stringify({ apower: state.site.pv.power, aenergy: { total: state.site.pv.energy } }));
   } else {
     next();
   }