Repository: evcc-io/evcc
Pull request #11537 (2024-01-07 10:04:45)

Renault: add alternative wakeup
Refs https://github.com/evcc-io/evcc/discussions/10749


Patch:
diff --git a/api/mock.go b/api/mock.go
index 0fb7eb35..20516734 100644
--- a/api/mock.go
+++ b/api/mock.go
@@ -1,5 +1,10 @@
 // Code generated by MockGen. DO NOT EDIT.
 // Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery,Tariff,BatteryController)
+//
+// Generated by this command:
+//
+//	mockgen -package api -destination mock.go github.com/evcc-io/evcc/api Charger,ChargeState,CurrentLimiter,PhaseSwitcher,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery,Tariff,BatteryController
+//
 
 // Package api is a generated GoMock package.
 package api
@@ -7,7 +12,7 @@ package api
 import (
 	reflect "reflect"
 
-	gomock "github.com/golang/mock/gomock"
+	gomock "go.uber.org/mock/gomock"
 )
 
 // MockCharger is a mock of Charger interface.
@@ -42,7 +47,7 @@ func (m *MockCharger) Enable(arg0 bool) error {
 }
 
 // Enable indicates an expected call of Enable.
-func (mr *MockChargerMockRecorder) Enable(arg0 interface{}) *gomock.Call {
+func (mr *MockChargerMockRecorder) Enable(arg0 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Enable", reflect.TypeOf((*MockCharger)(nil).Enable), arg0)
 }
@@ -71,7 +76,7 @@ func (m *MockCharger) MaxCurrent(arg0 int64) error {
 }
 
 // MaxCurrent indicates an expected call of MaxCurrent.
-func (mr *MockChargerMockRecorder) MaxCurrent(arg0 interface{}) *gomock.Call {
+func (mr *MockChargerMockRecorder) MaxCurrent(arg0 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaxCurrent", reflect.TypeOf((*MockCharger)(nil).MaxCurrent), arg0)
 }
@@ -200,7 +205,7 @@ func (m *MockPhaseSwitcher) Phases1p3p(arg0 int) error {
 }
 
 // Phases1p3p indicates an expected call of Phases1p3p.
-func (mr *MockPhaseSwitcherMockRecorder) Phases1p3p(arg0 interface{}) *gomock.Call {
+func (mr *MockPhaseSwitcherMockRecorder) Phases1p3p(arg0 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Phases1p3p", reflect.TypeOf((*MockPhaseSwitcher)(nil).Phases1p3p), arg0)
 }
@@ -419,7 +424,7 @@ func (m *MockVehicle) SetTitle(arg0 string) {
 }
 
 // SetTitle indicates an expected call of SetTitle.
-func (mr *MockVehicleMockRecorder) SetTitle(arg0 interface{}) *gomock.Call {
+func (mr *MockVehicleMockRecorder) SetTitle(arg0 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetTitle", reflect.TypeOf((*MockVehicle)(nil).SetTitle), arg0)
 }
@@ -613,7 +618,7 @@ func (m *MockBatteryController) SetBatteryMode(arg0 BatteryMode) error {
 }
 
 // SetBatteryMode indicates an expected call of SetBatteryMode.
-func (mr *MockBatteryControllerMockRecorder) SetBatteryMode(arg0 interface{}) *gomock.Call {
+func (mr *MockBatteryControllerMockRecorder) SetBatteryMode(arg0 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBatteryMode", reflect.TypeOf((*MockBatteryController)(nil).SetBatteryMode), arg0)
 }
diff --git a/assets/js/components/Loadpoint.vue b/assets/js/components/Loadpoint.vue
index 2fe96f11..62a0fe62 100644
--- a/assets/js/components/Loadpoint.vue
+++ b/assets/js/components/Loadpoint.vue
@@ -244,8 +244,19 @@ export default {
 		socBasedCharging: function () {
 			return (!this.vehicleFeatureOffline && this.vehiclePresent) || this.vehicleSoc > 0;
 		},
+		knownVehicle: function () {
+			return !!this.vehicleName;
+		},
+		vehicleHasSoc: function () {
+			return this.knownVehicle && !this.vehicleFeatureOffline;
+		},
 		socBasedPlanning: function () {
-			return this.vehicleSoc > 0 && this.vehicleCapacity > 0;
+			// TODO: deduplicate business logic. see also: socBasedPlanning() in loadpoint.go
+			return (
+				this.knownVehicle &&
+				this.vehicleCapacity > 0 &&
+				(this.vehicleHasSoc || this.vehicleSoc > 0)
+			);
 		},
 	},
 	watch: {
diff --git a/assets/js/i18n.js b/assets/js/i18n.js
index e6ac9348..99fdbdb4 100644
--- a/assets/js/i18n.js
+++ b/assets/js/i18n.js
@@ -8,9 +8,8 @@ import settings from "./settings";
 // https://github.com/joker-x/languages.js/blob/master/languages.json
 export const LOCALES = {
   ar: ["Arabic", "العربية"],
-  bh: ["Bihari", "भोजपुरी"],
+  bg: ["Bulgarian", "Български"],
   ca: ["Catalan", "Català"],
-  "zh-Hans": ["Chinese (Simplified)", "简体中文"],
   cs: ["Czech", "Česky"],
   da: ["Danish", "Dansk"],
   de: ["German", "Deutsch"],
@@ -30,7 +29,9 @@ export const LOCALES = {
   ru: ["Russian", "Русский"],
   sl: ["Slovenian", "Slovenščina"],
   sv: ["Swedish", "Svenska"],
+  tr: ["Turkish", "Türkçe"],
   uk: ["Ukrainian", "Українська"],
+  "zh-Hans": ["Chinese (Simplified)", "简体中文"],
 };
 
 function getBrowserLocale() {
diff --git a/assets/js/views/ChargingSessions.vue b/assets/js/views/ChargingSessions.vue
index 13fca909..fd228c87 100644
--- a/assets/js/views/ChargingSessions.vue
+++ b/assets/js/views/ChargingSessions.vue
@@ -8,7 +8,7 @@
 				<div size="s" class="mx-2 flex-grow-0 flex-shrink-0 fw-normal">/</div>
 				<span class="text-truncate">{{ $t("sessions.title") }}</span>
 			</h1>
-			<TopNavigation />
+			<TopNavigation v-bind="topNavigation" />
 		</header>
 
 		<div class="row">
@@ -263,6 +263,7 @@ import CustomSelect from "../components/CustomSelect.vue";
 import ChargingSessionModal from "../components/ChargingSessionModal.vue";
 import breakpoint from "../mixins/breakpoint";
 import settings from "../settings";
+import collector from "../mixins/collector";
 
 const COLUMNS_PER_BREAKPOINT = {
 	xs: 1,
@@ -276,7 +277,7 @@ const COLUMNS_PER_BREAKPOINT = {
 export default {
 	name: "ChargingSessions",
 	components: { TopNavigation, ChargingSessionModal, CustomSelect },
-	mixins: [formatter, breakpoint],
+	mixins: [formatter, breakpoint, collector],
 	props: {
 		notifications: Array,
 		month: { type: Number, default: () => new Date().getMonth() + 1 },
@@ -295,6 +296,10 @@ export default {
 		return { title: `${this.$t("sessions.title")} | evcc` };
 	},
 	computed: {
+		topNavigation: function () {
+			const vehicleLogins = store.state.auth ? store.state.auth.vehicles : {};
+			return { vehicleLogins, ...this.collectProps(TopNavigation, store.state) };
+		},
 		currentSessions() {
 			const sessionsWithDefaults = this.sessions.map((session) => {
 				const loadpoint = session.loadpoint || this.$t("main.loadpoint.fallbackName");
diff --git a/assets/js/views/Config.vue b/assets/js/views/Config.vue
index 115f7070..71c63362 100644
--- a/assets/js/views/Config.vue
+++ b/assets/js/views/Config.vue
@@ -7,7 +7,7 @@
 				</router-link>
 				Configuration 🧪
 			</h1>
-			<TopNavigation />
+			<TopNavigation v-bind="topNavigation" />
 		</header>
 
 		<div class="alert alert-danger mb-5" role="alert">
@@ -189,10 +189,13 @@ import Modal from "bootstrap/js/dist/modal";
 import api from "../api";
 import VehicleIcon from "../components/VehicleIcon";
 import VehicleModal from "../components/Config/VehicleModal.vue";
+import store from "../store";
+import collector from "../mixins/collector";
 
 export default {
 	name: "Config",
 	components: { TopNavigation, VehicleIcon, VehicleModal },
+	mixins: [collector],
 	data() {
 		return {
 			vehicles: [],
@@ -203,6 +206,12 @@ export default {
 			vehicleId: undefined,
 		};
 	},
+	computed: {
+		topNavigation: function () {
+			const vehicleLogins = store.state.auth ? store.state.auth.vehicles : {};
+			return { vehicleLogins, ...this.collectProps(TopNavigation, store.state) };
+		},
+	},
 	mounted() {
 		this.loadVehicles();
 		this.loadLoadpoints();
diff --git a/charger/eebus.go b/charger/eebus.go
index 5a562cc4..7b855d7b 100644
--- a/charger/eebus.go
+++ b/charger/eebus.go
@@ -16,6 +16,8 @@ import (
 	"github.com/evcc-io/evcc/util"
 )
 
+//go:generate mockgen -package charger -destination eebus_test_mock.go github.com/enbility/cemd/emobility EmobilityI
+
 const (
 	maxIdRequestTimespan         = time.Second * 120
 	idleFactor                   = 0.6
diff --git a/charger/mennekes.go b/charger/mennekes.go
index 6c55f0ca..9c400ea3 100644
--- a/charger/mennekes.go
+++ b/charger/mennekes.go
@@ -188,7 +188,7 @@ func (wb *Mennekes) CurrentPower() (float64, error) {
 		return 0, err
 	}
 
-	return float64(encoding.Float32(b)) * 1e3, nil
+	return float64(encoding.Float32(b)), nil
 }
 
 var _ api.MeterEnergy = (*Mennekes)(nil)
@@ -263,7 +263,7 @@ var _ api.PhaseSwitcher = (*Mennekes)(nil)
 // Phases1p3p implements the api.PhaseSwitcher interface
 func (wb *Mennekes) Phases1p3p(phases int) error {
 	var u uint16
-	if phases == 3 {
+	if phases == 1 {
 		u = 1
 	}
 
diff --git a/charger/openwb-2.0.go b/charger/openwb-2.0.go
index 224d5aa0..86fb4da9 100644
--- a/charger/openwb-2.0.go
+++ b/charger/openwb-2.0.go
@@ -11,9 +11,10 @@ import (
 
 // OpenWB20 charger implementation
 type OpenWB20 struct {
-	conn *modbus.Connection
-	curr uint16
-	base uint16
+	conn    *modbus.Connection
+	enabled bool
+	curr    uint16
+	base    uint16
 }
 
 const (
@@ -106,12 +107,7 @@ func (wb *OpenWB20) Status() (api.ChargeStatus, error) {
 
 // Enabled implements the api.Charger interface
 func (wb *OpenWB20) Enabled() (bool, error) {
-	b, err := wb.conn.ReadInputRegisters(wb.base+openwbRegActualAmps, 1)
-	if err != nil {
-		return false, err
-	}
-
-	return binary.BigEndian.Uint16(b) == 1, nil
+	return verifyEnabled(wb, wb.enabled)
 }
 
 func (wb *OpenWB20) setCurrent(u uint16) error {
@@ -125,8 +121,11 @@ func (wb *OpenWB20) Enable(enable bool) error {
 	if enable {
 		u = wb.curr
 	}
-
-	return wb.setCurrent(u)
+	err := wb.setCurrent(u)
+	if err == nil {
+		wb.enabled = enable
+	}
+	return err
 }
 
 // MaxCurrent implements the api.Charger interface
diff --git a/charger/pulsatrix.go b/charger/pulsatrix.go
new file mode 100644
index 00000000..5ff0ee44
--- /dev/null
+++ b/charger/pulsatrix.go
@@ -0,0 +1,297 @@
+package charger
+
+/*
+MIT License
+
+Copyright (c) 2023-2024 pulsatrix gmbh
+Copyright (c) 2019-2024 andig
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+*/
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"strconv"
+	"sync"
+	"time"
+
+	"github.com/cenkalti/backoff/v4"
+	"github.com/evcc-io/evcc/api"
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/request"
+	"github.com/evcc-io/evcc/util/sponsor"
+	"nhooyr.io/websocket"
+)
+
+// pulsatrix charger implementation
+type Pulsatrix struct {
+	log     *util.Logger
+	mu      sync.Mutex
+	conn    *websocket.Conn
+	uri     string
+	enabled bool
+	quit    chan struct{}
+	data    *util.Monitor[pulsatrixData]
+}
+
+type pulsatrixData struct {
+	VehicleStatus   string     `json:"vehicleStatus"`
+	LastActivePower float64    `json:"lastActivePower"`
+	PhaseVoltage    [3]float64 `json:"voltage"`
+	PhaseAmperage   [3]float64 `json:"amperage"`
+	AmperageLimit   float64    `json:"amperageLimit"`
+	EnergyImported  float64    `json:"energyImported"`
+}
+
+func init() {
+	registry.Add("pulsatrix", NewPulsatrixFromConfig)
+}
+
+// NewPulsatrixtFromConfig creates a pulsatrix charger from generic config
+func NewPulsatrixFromConfig(other map[string]interface{}) (api.Charger, error) {
+	var cc struct {
+		Host string
+	}
+
+	if err := util.DecodeOther(other, &cc); err != nil {
+		return nil, err
+	}
+
+	return NewPulsatrix(cc.Host)
+}
+
+// NewPulsatrix creates pulsatrix charger
+func NewPulsatrix(hostname string) (*Pulsatrix, error) {
+	wb := Pulsatrix{
+		log:  util.NewLogger("pulsatrix"),
+		uri:  fmt.Sprintf("ws://%s/api/ws", hostname),
+		data: util.NewMonitor[pulsatrixData](15 * time.Second),
+	}
+
+	if err := wb.connectWs(); err != nil {
+		return nil, err
+	}
+
+	if !sponsor.IsAuthorized() {
+		return nil, api.ErrSponsorRequired
+	}
+
+	return &wb, nil
+}
+
+// ConnectWs connects to a pulsatrix SECC websocket
+func (c *Pulsatrix) connectWs() error {
+	ctx, cancel := context.WithTimeout(context.Background(), request.Timeout)
+	defer cancel()
+
+	c.log.TRACE.Printf("connecting to %s", c.uri)
+	conn, _, err := websocket.Dial(ctx, c.uri, nil)
+	if err != nil {
+		return err
+	}
+
+	c.conn = conn
+
+	// ensure evcc and SECC are in sync
+	if err := c.Enable(false); err != nil {
+		c.log.ERROR.Println(err)
+	}
+	c.quit = make(chan struct{})
+	go c.wsReader()
+	go c.heartbeat()
+
+	return nil
+}
+
+// ReconnectWs reconnects to a pulsatrix SECC websocket
+func (c *Pulsatrix) reconnectWs() {
+	bo := backoff.NewExponentialBackOff()
+	bo.InitialInterval = time.Second
+	bo.MaxInterval = 1 * time.Minute
+	bo.MaxElapsedTime = 0 * time.Second // retry forever; default is 15 min
+	if err := backoff.Retry(c.connectWs, bo); err != nil {
+		c.log.ERROR.Println(err)
+	}
+}
+
+// WsReader runs a loop that reads messages from the websocket
+func (c *Pulsatrix) wsReader() {
+	for {
+		ctx, cancel := context.WithTimeout(context.Background(), request.Timeout)
+		defer cancel()
+
+		messageType, message, err := c.conn.Read(ctx)
+		if err != nil {
+			c.log.ERROR.Println("read message:", err)
+			break
+		} else {
+			c.parseWsMessage(messageType, message)
+		}
+	}
+
+	c.mu.Lock()
+	c.conn.Close(websocket.StatusNormalClosure, "Reconnecting")
+	c.conn = nil
+	close(c.quit)
+	c.mu.Unlock()
+
+	c.reconnectWs()
+}
+
+// wsWrite writes a message to the websocket
+func (c *Pulsatrix) write(message string) error {
+	c.mu.Lock()
+	defer c.mu.Unlock()
+	if c.conn != nil {
+		ctx, cancel := context.WithTimeout(context.Background(), request.Timeout)
+		defer cancel()
+
+		if err := c.conn.Write(ctx, websocket.MessageText, []byte(message)); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// ParseWsMessage parses a message from the websocket
+func (c *Pulsatrix) parseWsMessage(messageType websocket.MessageType, message []byte) {
+	if messageType == websocket.MessageText {
+		b := bytes.ReplaceAll(message, []byte(":NaN"), []byte(":null"))
+		var parsedMessage struct {
+			Message json.RawMessage `json:"message"`
+		}
+
+		if err := json.Unmarshal(b, &parsedMessage); err != nil {
+			c.log.ERROR.Println(err)
+			return
+		}
+
+		val, _ := c.data.Get()
+		if err := json.Unmarshal(parsedMessage.Message, &val); err != nil {
+			c.log.ERROR.Println(err)
+		} else {
+			c.data.Set(val)
+		}
+	}
+}
+
+// Heartbeat sends a heartbeat to the pulsatrix SECC
+func (c *Pulsatrix) heartbeat() {
+	ticker := time.NewTicker(3 * time.Minute)
+	defer ticker.Stop()
+
+	for {
+		select {
+		case <-ticker.C:
+			if err := c.Enable(c.enabled); err != nil {
+				c.log.ERROR.Println(err)
+			}
+		case <-c.quit:
+			return
+		}
+	}
+}
+
+// Status implements the api.Charger interface
+func (c *Pulsatrix) Status() (api.ChargeStatus, error) {
+	res, err := c.data.Get()
+	if err != nil {
+		return api.StatusNone, err
+	}
+
+	return api.ChargeStatusString(res.VehicleStatus)
+}
+
+// Enabled implements the api.Charger interface
+func (c *Pulsatrix) Enabled() (bool, error) {
+	return verifyEnabled(c, c.enabled)
+}
+
+// Enable implements the api.Charger interface
+func (c *Pulsatrix) Enable(enable bool) error {
+	err := c.write("setEnabled\n" + strconv.FormatBool(enable))
+	if err == nil {
+		c.enabled = enable
+	}
+	return err
+}
+
+// MaxCurrent implements the api.CurrentLimiter interface
+func (c *Pulsatrix) MaxCurrent(current int64) error {
+	return c.MaxCurrentMillis(float64(current))
+}
+
+// MaxCurrentMillis implements the api.ChargerEx interface
+func (c *Pulsatrix) MaxCurrentMillis(current float64) error {
+	return c.write("setCurrentLimit\n" + strconv.FormatFloat(current, 'f', 10, 64))
+}
+
+var _ api.CurrentGetter = (*Pulsatrix)(nil)
+
+// GetMaxCurrent implements the api.CurrentGetter interface
+func (c *Pulsatrix) GetMaxCurrent() (float64, error) {
+	res, err := c.data.Get()
+	return res.AmperageLimit, err
+}
+
+// CurrentPower implements the api.Meter interface
+func (c *Pulsatrix) CurrentPower() (float64, error) {
+	res, err := c.data.Get()
+	return res.LastActivePower, err
+}
+
+var _ api.MeterEnergy = (*Pulsatrix)(nil)
+
+// TotalEnergy implements the api.MeterEnergy interface
+func (c *Pulsatrix) TotalEnergy() (float64, error) {
+	res, err := c.data.Get()
+	return res.EnergyImported, err
+}
+
+// Phases1p3p implements the api.PhaseSwitcher interface
+func (c *Pulsatrix) Phases1p3p(phases int) error {
+	return c.write("set1p3p\n" + strconv.FormatBool(phases == 1))
+}
+
+var _ api.PhaseCurrents = (*Pulsatrix)(nil)
+
+// Currents implements the api.PhaseCurrents interface
+func (c *Pulsatrix) Currents() (float64, float64, float64, error) {
+	res, err := c.data.Get()
+	if err != nil {
+		return 0, 0, 0, err
+	}
+
+	return res.PhaseAmperage[0], res.PhaseAmperage[1], res.PhaseAmperage[2], nil
+}
+
+var _ api.PhaseVoltages = (*Pulsatrix)(nil)
+
+// Voltages implements the api.PhaseVoltages interface
+func (c *Pulsatrix) Voltages() (float64, float64, float64, error) {
+	res, err := c.data.Get()
+	if err != nil {
+		return 0, 0, 0, err
+	}
+
+	return res.PhaseVoltage[0], res.PhaseVoltage[1], res.PhaseVoltage[2], nil
+}
diff --git a/cmd/meter.go b/cmd/meter.go
index 2abd4a7c..babb2a12 100644
--- a/cmd/meter.go
+++ b/cmd/meter.go
@@ -48,9 +48,11 @@ func runMeter(cmd *cobra.Command, args []string) {
 
 	meters := config.Meters().Devices()
 
+	var flagUsed bool
 	if mode != api.BatteryUnknown {
-		for _, dev := range meters {
-			v := dev.Instance()
+		flagUsed = true
+
+		for _, v := range config.Instances(meters) {
 			if b, ok := v.(api.BatteryController); ok {
 				if err := b.SetBatteryMode(mode); err != nil {
 					log.FATAL.Fatalln("set battery mode:", err)
@@ -64,11 +66,13 @@ func runMeter(cmd *cobra.Command, args []string) {
 		}
 	}
 
-	d := dumper{len: len(meters)}
-	for _, dev := range meters {
-		v := dev.Instance()
+	if !flagUsed {
+		d := dumper{len: len(meters)}
+		for _, dev := range meters {
+			v := dev.Instance()
 
-		d.DumpWithHeader(dev.Config().Name, v)
+			d.DumpWithHeader(dev.Config().Name, v)
+		}
 	}
 
 	// wait for shutdown
diff --git a/core/loadpoint/mock.go b/core/loadpoint/mock.go
index 8138ff1a..3d69145f 100644
--- a/core/loadpoint/mock.go
+++ b/core/loadpoint/mock.go
@@ -1,5 +1,10 @@
 // Code generated by MockGen. DO NOT EDIT.
 // Source: github.com/evcc-io/evcc/core/loadpoint (interfaces: API)
+//
+// Generated by this command:
+//
+//	mockgen -package loadpoint -destination mock.go -mock_names API=MockAPI github.com/evcc-io/evcc/core/loadpoint API
+//
 
 // Package loadpoint is a generated GoMock package.
 package loadpoint
@@ -9,7 +14,7 @@ import (
 	time "time"
 
 	api "github.com/evcc-io/evcc/api"
-	gomock "github.com/golang/mock/gomock"
+	gomock "go.uber.org/mock/gomock"
 )
 
 // MockAPI is a mock of API interface.
@@ -255,7 +260,7 @@ func (m *MockAPI) GetPlan(arg0 time.Time, arg1 time.Duration) (api.Rates, error)
 }
 
 // GetPlan indicates an expected call of GetPlan.
-func (mr *MockAPIMockRecorder) GetPlan(arg0, arg1 interface{}) *gomock.Call {
+func (mr *MockAPIMockRecorder) GetPlan(arg0, arg1 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlan", reflect.TypeOf((*MockAPI)(nil).GetPlan), arg0, arg1)
 }
@@ -313,7 +318,7 @@ func (m *MockAPI) GetPlanRequiredDuration(arg0, arg1 float64) time.Duration {
 }
 
 // GetPlanRequiredDuration indicates an expected call of GetPlanRequiredDuration.
-func (mr *MockAPIMockRecorder) GetPlanRequiredDuration(arg0, arg1 interface{}) *gomock.Call {
+func (mr *MockAPIMockRecorder) GetPlanRequiredDuration(arg0, arg1 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlanRequiredDuration", reflect.TypeOf((*MockAPI)(nil).GetPlanRequiredDuration), arg0, arg1)
 }
@@ -421,7 +426,7 @@ func (m *MockAPI) RemoteControl(arg0 string, arg1 RemoteDemand) {
 }
 
 // RemoteControl indicates an expected call of RemoteControl.
-func (mr *MockAPIMockRecorder) RemoteControl(arg0, arg1 interface{}) *gomock.Call {
+func (mr *MockAPIMockRecorder) RemoteControl(arg0, arg1 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoteControl", reflect.TypeOf((*MockAPI)(nil).RemoteControl), arg0, arg1)
 }
@@ -433,7 +438,7 @@ func (m *MockAPI) SetDisableThreshold(arg0 float64) {
 }
 
 // SetDisableThreshold indicates an expected call of SetDisableThreshold.
-func (mr *MockAPIMockRecorder) SetDisableThreshold(arg0 interface{}) *gomock.Call {
+func (mr *MockAPIMockRecorder) SetDisableThreshold(arg0 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDisableThreshold", reflect.TypeOf((*MockAPI)(nil).SetDisableThreshold), arg0)
 }
@@ -445,7 +450,7 @@ func (m *MockAPI) SetEnableThreshold(arg0 float64) {
 }
 
 // SetEnableThreshold indicates an expected call of SetEnableThreshold.
-func (mr *MockAPIMockRecorder) SetEnableThreshold(arg0 interface{}) *gomock.Call {
+func (mr *MockAPIMockRecorder) SetEnableThreshold(arg0 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetEnableThreshold", reflect.TypeOf((*MockAPI)(nil).SetEnableThreshold), arg0)
 }
@@ -457,7 +462,7 @@ func (m *MockAPI) SetLimitEnergy(arg0 float64) {
 }
 
 // SetLimitEnergy indicates an expected call of SetLimitEnergy.
-func (mr *MockAPIMockRecorder) SetLimitEnergy(arg0 interface{}) *gomock.Call {
+func (mr *MockAPIMockRecorder) SetLimitEnergy(arg0 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLimitEnergy", reflect.TypeOf((*MockAPI)(nil).SetLimitEnergy), arg0)
 }
@@ -469,7 +474,7 @@ func (m *MockAPI) SetLimitSoc(arg0 int) {
 }
 
 // SetLimitSoc indicates an expected call of SetLimitSoc.
-func (mr *MockAPIMockRecorder) SetLimitSoc(arg0 interface{}) *gomock.Call {
+func (mr *MockAPIMockRecorder) SetLimitSoc(arg0 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLimitSoc", reflect.TypeOf((*MockAPI)(nil).SetLimitSoc), arg0)
 }
@@ -481,7 +486,7 @@ func (m *MockAPI) SetMaxCurrent(arg0 float64) {
 }
 
 // SetMaxCurrent indicates an expected call of SetMaxCurrent.
-func (mr *MockAPIMockRecorder) SetMaxCurrent(arg0 interface{}) *gomock.Call {
+func (mr *MockAPIMockRecorder) SetMaxCurrent(arg0 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMaxCurrent", reflect.TypeOf((*MockAPI)(nil).SetMaxCurrent), arg0)
 }
@@ -493,7 +498,7 @@ func (m *MockAPI) SetMinCurrent(arg0 float64) {
 }
 
 // SetMinCurrent indicates an expected call of SetMinCurrent.
-func (mr *MockAPIMockRecorder) SetMinCurrent(arg0 interface{}) *gomock.Call {
+func (mr *MockAPIMockRecorder) SetMinCurrent(arg0 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMinCurrent", reflect.TypeOf((*MockAPI)(nil).SetMinCurrent), arg0)
 }
@@ -505,7 +510,7 @@ func (m *MockAPI) SetMode(arg0 api.ChargeMode) {
 }
 
 // SetMode indicates an expected call of SetMode.
-func (mr *MockAPIMockRecorder) SetMode(arg0 interface{}) *gomock.Call {
+func (mr *MockAPIMockRecorder) SetMode(arg0 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMode", reflect.TypeOf((*MockAPI)(nil).SetMode), arg0)
 }
@@ -519,7 +524,7 @@ func (m *MockAPI) SetPhases(arg0 int) error {
 }
 
 // SetPhases indicates an expected call of SetPhases.
-func (mr *MockAPIMockRecorder) SetPhases(arg0 interface{}) *gomock.Call {
+func (mr *MockAPIMockRecorder) SetPhases(arg0 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPhases", reflect.TypeOf((*MockAPI)(nil).SetPhases), arg0)
 }
@@ -533,7 +538,7 @@ func (m *MockAPI) SetPlanEnergy(arg0 time.Time, arg1 float64) error {
 }
 
 // SetPlanEnergy indicates an expected call of SetPlanEnergy.
-func (mr *MockAPIMockRecorder) SetPlanEnergy(arg0, arg1 interface{}) *gomock.Call {
+func (mr *MockAPIMockRecorder) SetPlanEnergy(arg0, arg1 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPlanEnergy", reflect.TypeOf((*MockAPI)(nil).SetPlanEnergy), arg0, arg1)
 }
@@ -545,7 +550,7 @@ func (m *MockAPI) SetPriority(arg0 int) {
 }
 
 // SetPriority indicates an expected call of SetPriority.
-func (mr *MockAPIMockRecorder) SetPriority(arg0 interface{}) *gomock.Call {
+func (mr *MockAPIMockRecorder) SetPriority(arg0 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPriority", reflect.TypeOf((*MockAPI)(nil).SetPriority), arg0)
 }
@@ -557,7 +562,7 @@ func (m *MockAPI) SetVehicle(arg0 api.Vehicle) {
 }
 
 // SetVehicle indicates an expected call of SetVehicle.
-func (mr *MockAPIMockRecorder) SetVehicle(arg0 interface{}) *gomock.Call {
+func (mr *MockAPIMockRecorder) SetVehicle(arg0 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetVehicle", reflect.TypeOf((*MockAPI)(nil).SetVehicle), arg0)
 }
diff --git a/core/site.go b/core/site.go
index 612e1ef2..323c2d62 100644
--- a/core/site.go
+++ b/core/site.go
@@ -790,8 +790,11 @@ func (site *Site) update(lp Updater) {
 		homePower = max(homePower, 0)
 		site.publish(keys.HomePower, homePower)
 
+		// add battery charging power to homePower to ignore all consumption which does not occur on loadpoints
+		// fix for: https://github.com/evcc-io/evcc/issues/11032
+		nonChargePower := homePower + max(0, -site.batteryPower)
 		greenShareHome := site.greenShare(0, homePower)
-		greenShareLoadpoints := site.greenShare(homePower, homePower+totalChargePower)
+		greenShareLoadpoints := site.greenShare(nonChargePower, nonChargePower+totalChargePower)
 
 		lp.Update(sitePower, smartCostActive, batteryBuffered, batteryStart, greenShareLoadpoints, site.effectivePrice(greenShareLoadpoints), site.effectiveCo2(greenShareLoadpoints))
 
diff --git a/core/vehicle/mock.go b/core/vehicle/mock.go
index 4da01bc2..a2fa746e 100644
--- a/core/vehicle/mock.go
+++ b/core/vehicle/mock.go
@@ -1,5 +1,10 @@
 // Code generated by MockGen. DO NOT EDIT.
 // Source: github.com/evcc-io/evcc/core/vehicle (interfaces: API)
+//
+// Generated by this command:
+//
+//	mockgen -package vehicle -destination mock.go -mock_names API=MockAPI github.com/evcc-io/evcc/core/vehicle API
+//
 
 // Package vehicle is a generated GoMock package.
 package vehicle
@@ -9,7 +14,7 @@ import (
 	time "time"
 
 	api "github.com/evcc-io/evcc/api"
-	gomock "github.com/golang/mock/gomock"
+	gomock "go.uber.org/mock/gomock"
 )
 
 // MockAPI is a mock of API interface.
@@ -113,7 +118,7 @@ func (m *MockAPI) SetLimitSoc(arg0 int) {
 }
 
 // SetLimitSoc indicates an expected call of SetLimitSoc.
-func (mr *MockAPIMockRecorder) SetLimitSoc(arg0 interface{}) *gomock.Call {
+func (mr *MockAPIMockRecorder) SetLimitSoc(arg0 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetLimitSoc", reflect.TypeOf((*MockAPI)(nil).SetLimitSoc), arg0)
 }
@@ -125,7 +130,7 @@ func (m *MockAPI) SetMinSoc(arg0 int) {
 }
 
 // SetMinSoc indicates an expected call of SetMinSoc.
-func (mr *MockAPIMockRecorder) SetMinSoc(arg0 interface{}) *gomock.Call {
+func (mr *MockAPIMockRecorder) SetMinSoc(arg0 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetMinSoc", reflect.TypeOf((*MockAPI)(nil).SetMinSoc), arg0)
 }
@@ -139,7 +144,7 @@ func (m *MockAPI) SetPlanSoc(arg0 time.Time, arg1 int) error {
 }
 
 // SetPlanSoc indicates an expected call of SetPlanSoc.
-func (mr *MockAPIMockRecorder) SetPlanSoc(arg0, arg1 interface{}) *gomock.Call {
+func (mr *MockAPIMockRecorder) SetPlanSoc(arg0, arg1 any) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPlanSoc", reflect.TypeOf((*MockAPI)(nil).SetPlanSoc), arg0, arg1)
 }
diff --git a/go.mod b/go.mod
index 267ccdbe..41d40f6a 100644
--- a/go.mod
+++ b/go.mod
@@ -37,7 +37,6 @@ require (
 	github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
 	github.com/godbus/dbus/v5 v5.1.0
 	github.com/gokrazy/updater v0.0.0-20230215172637-813ccc7f21e2
-	github.com/golang/mock v1.6.0
 	github.com/google/go-github/v32 v32.1.0
 	github.com/google/uuid v1.5.0
 	github.com/gorilla/handlers v1.5.2
@@ -90,6 +89,7 @@ require (
 	github.com/volkszaehler/mbmd v0.0.0-20231215091549-af16b1f597b9
 	github.com/writeas/go-strip-markdown/v2 v2.1.1
 	gitlab.com/bboehmke/sunny v0.16.0
+	go.uber.org/mock v0.4.0
 	golang.org/x/crypto/x509roots/fallback v0.0.0-20231218163308-9d2ee975ef9f
 	golang.org/x/exp v0.0.0-20231219180239-dc181d75b848
 	golang.org/x/net v0.19.0
@@ -130,6 +130,7 @@ require (
 	github.com/gobwas/httphead v0.1.0 // indirect
 	github.com/gobwas/pool v0.2.1 // indirect
 	github.com/gobwas/ws v1.3.1 // indirect
+	github.com/golang/mock v1.6.0 // indirect
 	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/google/go-querystring v1.1.0 // indirect
 	github.com/gorilla/websocket v1.5.1 // indirect
diff --git a/go.sum b/go.sum
index 770dfe38..0e0f167b 100644
--- a/go.sum
+++ b/go.sum
@@ -703,6 +703,8 @@ go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
 go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
 go.uber.org/goleak v1.1.10 h1:z+mqJhf6ss6BSfSM671tgKyZBFPTTJM+HLxnhPC3wu0=
 go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
+go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
+go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
 go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
 go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
diff --git a/templates/definition/charger/pulsatrix.yaml b/templates/definition/charger/pulsatrix.yaml
new file mode 100644
index 00000000..f9d79399
--- /dev/null
+++ b/templates/definition/charger/pulsatrix.yaml
@@ -0,0 +1,14 @@
+template: pulsatrix
+products:
+  - brand: Pulsatrix
+requirements:
+  evcc: ["sponsorship"]
+params:
+  - name: host #IP address or hostname (can be found on 3rd page of SECC display)
+    required: true
+    description:
+      en: IP address or hostname (can be found on 3rd page of SECC display)
+      de: IP-Adresse oder Hostname (wird auf der dritten Displayseite des SECC angezeigt)
+render: |
+  type: pulsatrix
+  host: {{ .host }}
diff --git a/templates/definition/meter/sma-hybrid.yaml b/templates/definition/meter/sma-hybrid.yaml
index 23a83021..db12fb90 100644
--- a/templates/definition/meter/sma-hybrid.yaml
+++ b/templates/definition/meter/sma-hybrid.yaml
@@ -20,18 +20,26 @@ params:
     advanced: true
   - name: watchdog
     type: duration
-    default: 60s
+    default: 30s
     advanced: true
 render: |
   type: custom
   {{- if eq .usage "pv" }}
   power:
-    source: modbus
-    {{- include "modbus" . | indent 2 }}
-    register:
-      address: 30773 # SMA Modbus Profile: DcMs.Watt
-      type: input
-      decode: int32nan
+    source: calc
+    add:
+      - source: modbus
+        {{- include "modbus" . | indent 6 }}
+        register:
+          address: 30773 # SMA Modbus Profile: DcMs.Watt [1]
+          type: holding
+          decode: int32nan
+      - source: modbus
+        {{- include "modbus" . | indent 6 }}
+        register:
+          address: 30961 # SMA Modbus Profile: DcMs.Watt [2]
+          type: holding
+          decode: int32nan
   energy:
     source: modbus
     {{- include "modbus" . | indent 2 }}
@@ -83,39 +91,58 @@ render: |
       - case: 1 # normal
         set:
           source: const
-          value: 10000 # Maximale Wirkleistung
+          value: 803 # inaktiv (Ina)
           set:
             source: modbus
             {{- include "modbus" . | indent 10 }}
-            timeout: {{ .timeout }}
             register:
-              address: 44039 # SMA Modbus Profile: Inverter.WModCfg.WCtlComCfg.WSptMaxNom
+              address: 40151 # SMA Modbus Profile: Inverter.WModCfg.WCtlComCfg.WCtlComAct
               type: writemultiple
-              decode: int32
+              decode: uint32
       - case: 2 # hold
         set:
-          source: const
-          value: 0 # Maximale Wirkleistung
+          source: sequence
           set:
-            source: modbus
-            {{- include "modbus" . | indent 10 }}
-            timeout: {{ .timeout }}
-            register:
-              address: 44039 # SMA Modbus Profile: Inverter.WModCfg.WCtlComCfg.WSptMaxNom
-              type: writemultiple
-              decode: int32
+          - source: const
+            value: 0 # Wirkleistungsvorgabe
+            set:
+              source: modbus
+              {{- include "modbus" . | indent 12 }}
+              register:
+                address: 40149 # SMA Modbus Profile: Inverter.WModCfg.WCtlComCfg.WSpt
+                type: writemultiple
+                decode: int32
+          - source: const
+            value: 802 # aktiv (Act)
+            set:
+              source: modbus
+              {{- include "modbus" . | indent 12 }}
+              register:
+                address: 40151 # SMA Modbus Profile: Inverter.WModCfg.WCtlComCfg.WCtlComAct
+                type: writemultiple
+                decode: uint32
       - case: 3 # charge
         set:
-          source: const
-          value: -10000 # Maximale Wirkleistung
+          source: sequence
           set:
-            source: modbus
-            {{- include "modbus" . | indent 10 }}
-            timeout: {{ .timeout }}
-            register:
-              address: 44039 # SMA Modbus Profile: Inverter.WModCfg.WCtlComCfg.WSptMaxNom
-              type: writemultiple
-              decode: int32
+          - source: const
+            value: -2147483648 # Wirkleistungsvorgabe
+            set:
+              source: modbus
+              {{- include "modbus" . | indent 12 }}
+              register:
+                address: 40149 # SMA Modbus Profile: Inverter.WModCfg.WCtlComCfg.WSpt
+                type: writemultiple
+                decode: int32
+          - source: const
+            value: 802 # aktiv (Act)
+            set:
+              source: modbus
+              {{- include "modbus" . | indent 12 }}
+              register:
+                address: 40151 # SMA Modbus Profile: Inverter.WModCfg.WCtlComCfg.WCtlComAct
+                type: writemultiple
+                decode: uint32
   {{- if .capacity }}
   capacity: {{ .capacity }} # kWh
   {{- end }}
diff --git a/templates/definition/meter/sma-inverter-modbus.yaml b/templates/definition/meter/sma-inverter-modbus.yaml
index 410353d2..3cac3e92 100644
--- a/templates/definition/meter/sma-inverter-modbus.yaml
+++ b/templates/definition/meter/sma-inverter-modbus.yaml
@@ -62,7 +62,6 @@ render: |
           set:
             source: modbus
             {{- include "modbus" . | indent 10 }}
-            timeout: {{ .timeout }}
             register:
               address: 44039 # SMA Modbus Profile: Inverter.WModCfg.WCtlComCfg.WSptMaxNom
               type: writemultiple
@@ -74,7 +73,6 @@ render: |
           set:
             source: modbus
             {{- include "modbus" . | indent 10 }}
-            timeout: {{ .timeout }}
             register:
               address: 44039 # SMA Modbus Profile: Inverter.WModCfg.WCtlComCfg.WSptMaxNom
               type: writemultiple
@@ -86,7 +84,6 @@ render: |
           set:
             source: modbus
             {{- include "modbus" . | indent 10 }}
-            timeout: {{ .timeout }}
             register:
               address: 44039 # SMA Modbus Profile: Inverter.WModCfg.WCtlComCfg.WSptMaxNom
               type: writemultiple
diff --git a/templates/definition/meter/solarman.yaml b/templates/definition/meter/solarman.yaml
index a5bb6860..55afbcf9 100644
--- a/templates/definition/meter/solarman.yaml
+++ b/templates/definition/meter/solarman.yaml
@@ -4,15 +4,6 @@ products:
   - brand: IGEN Tech
     description:
       generic: Solarman Logger
-  - brand: Deye
-    description:
-      generic: SUN series micro inverter
-  - brand: Bosswerk
-    description:
-      generic: MI series micro inverter
-  - brand: Ginlong
-    description:
-      generic: Solis inverter
 params:
   - name: usage
     choice: ["pv"]
diff --git a/templates/definition/meter/solis-hybrid.yaml b/templates/definition/meter/solis-hybrid.yaml
new file mode 100644
index 00000000..9d2a09af
--- /dev/null
+++ b/templates/definition/meter/solis-hybrid.yaml
@@ -0,0 +1,122 @@
+template: solis-hybrid
+products:
+  - brand: Ginlong
+    description:
+      generic: Solis Hybrid Inverter
+params:
+  - name: usage
+    choice: ["grid", "pv", "battery"]
+    allinone: true
+  - name: modbus
+    choice: ["rs485"]
+    baudrate: 9600
+    id: 1
+  - name: capacity
+    advanced: true
+render: |
+  type: custom
+  {{- if eq .usage "grid" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33263 # Meter total active power
+      type: input
+      decode: int32
+    scale: -1
+  energy:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33283 # Meter total active energy from grid
+      type: input
+      decode: uint32
+    scale: 0.01
+  currents:
+  - source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33252 # Meter ac current A
+      type: input
+      decode: uint16
+    scale: 0.01
+  - source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33254 # Meter ac current B
+      type: input
+      decode: uint16
+    scale: 0.01
+  - source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33256 # Meter ac current C
+      type: input
+      decode: uint16
+    scale: 0.01
+  powers:
+  - source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33257 # Meter active power A
+      type: input
+      decode: int32
+    scale: -1
+  - source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33259 # Meter active power B
+      type: input
+      decode: int32
+    scale: -1
+  - source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33261 # Meter active power C
+      type: input
+      decode: int32
+    scale: -1
+  {{- end }}
+  {{- if eq .usage "pv" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      type: input
+      address: 33057 # Total DC output power (PV Power)
+      decode: uint32
+  energy:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33029 # Total energy generation
+      type: input
+      decode: uint32
+  {{- end }}
+  {{- if eq .usage "battery" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      type: input
+      address: 33149 # Battery power
+      decode: int32
+    scale: -1      
+  energy:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33165 # Battery total discharge energy
+      type: input
+      decode: uint32
+  soc:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33139 # Battery capacity SOC
+      type: input
+      decode: uint16
+  {{- if .capacity }}
+  capacity: {{ .capacity }} # kWh
+  {{- end }}
+  {{- end }}
diff --git a/templates/definition/meter/solis.yaml b/templates/definition/meter/solis.yaml
new file mode 100644
index 00000000..a34a9f28
--- /dev/null
+++ b/templates/definition/meter/solis.yaml
@@ -0,0 +1,48 @@
+template: solis
+products:
+  - brand: Ginlong
+    description:
+      generic: Solis Inverter
+params:
+  - name: usage
+    choice: ["grid", "pv"]
+  - name: modbus
+    choice: ["rs485"]
+    baudrate: 9600
+    id: 1
+render: |
+  type: custom
+  {{- if eq .usage "grid" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 3262 # Meter Total P
+      type: input
+      decode: int32
+    scale: -1
+  energy:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 3282 # Meter grid import active energy
+      type: input
+      decode: uint32
+    scale: 0.01
+  {{- end }}
+  {{- if eq .usage "pv" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      type: input
+      address: 3004 # Active power
+      decode: uint32
+  energy:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 3008 # Total energy
+      type: input
+      decode: uint32
+  {{- end }}
diff --git a/templates/definition/vehicle/renault.yaml b/templates/definition/vehicle/renault.yaml
index 721f8889..8c921f3f 100644
--- a/templates/definition/vehicle/renault.yaml
+++ b/templates/definition/vehicle/renault.yaml
@@ -6,7 +6,14 @@ params:
   - preset: vehicle-identify
   - name: vin
     example: WREN...
+  - name: alternativewakeup
+    type: bool
+    description:
+      de: Alternativer Aufweckmechanismus
+      en: Alternative wakeup mechanism
+    advanced: true
 render: |
   type: renault
   {{ include "vehicle-base" . }}
   {{ include "vehicle-identify" . }}
+  alternativewakeup: {{ .alternativewakeup }}
diff --git a/tools.go b/tools.go
index 03087581..d27705ad 100644
--- a/tools.go
+++ b/tools.go
@@ -4,5 +4,5 @@ package main
 
 import (
 	_ "github.com/dmarkham/enumer"
-	_ "github.com/golang/mock/mockgen"
+	_ "go.uber.org/mock/mockgen"
 )
diff --git a/vehicle/renault.go b/vehicle/renault.go
index 7aa410f1..141bebe7 100644
--- a/vehicle/renault.go
+++ b/vehicle/renault.go
@@ -40,6 +40,7 @@ func NewRenaultDaciaFromConfig(brand string, other map[string]interface{}) (api.
 	cc := struct {
 		embed                       `mapstructure:",squash"`
 		User, Password, Region, VIN string
+		AlternativeWakeup           bool
 		Cache                       time.Duration
 		Timeout                     time.Duration
 	}{
@@ -93,7 +94,7 @@ func NewRenaultDaciaFromConfig(brand string, other map[string]interface{}) (api.
 		err = vehicle.Available()
 	}
 
-	v.Provider = renault.NewProvider(api, accountID, vehicle.VIN, cc.Cache)
+	v.Provider = renault.NewProvider(api, accountID, vehicle.VIN, cc.AlternativeWakeup, cc.Cache)
 
 	return v, err
 }
diff --git a/vehicle/renault/provider.go b/vehicle/renault/provider.go
index 4a84f36d..c302a51a 100644
--- a/vehicle/renault/provider.go
+++ b/vehicle/renault/provider.go
@@ -23,7 +23,7 @@ type Provider struct {
 }
 
 // NewProvider creates a vehicle api provider
-func NewProvider(api *kamereon.API, accountID, vin string, cache time.Duration) *Provider {
+func NewProvider(api *kamereon.API, accountID, vin string, alternativeWakeup bool, cache time.Duration) *Provider {
 	impl := &Provider{
 		batteryG: provider.Cached(func() (kamereon.Response, error) {
 			return api.Battery(accountID, vin)
@@ -35,6 +35,9 @@ func NewProvider(api *kamereon.API, accountID, vin string, cache time.Duration)
 			return api.Hvac(accountID, vin)
 		}, cache),
 		wakeup: func() (kamereon.Response, error) {
+			if alternativeWakeup {
+				return api.Action(accountID, kamereon.ActionStart, vin)
+			}
 			return api.WakeUp(accountID, vin)
 		},
 		position: func() (kamereon.Response, error) {


Test patch:
diff --git a/charger/eebus_test.go b/charger/eebus_test.go
index 687a2f8f..68570b29 100644
--- a/charger/eebus_test.go
+++ b/charger/eebus_test.go
@@ -3,8 +3,7 @@ package charger
 import (
 	"testing"
 
-	"github.com/enbility/cemd/emobility"
-	"github.com/golang/mock/gomock"
+	"go.uber.org/mock/gomock"
 )
 
 func TestEEBusIsCharging(t *testing.T) {
@@ -117,7 +116,7 @@ func TestEEBusIsCharging(t *testing.T) {
 			for index, m := range tc.measurements {
 				ctrl := gomock.NewController(t)
 
-				emobilityMock := emobility.NewMockEmobilityI(ctrl)
+				emobilityMock := NewMockEmobilityI(ctrl)
 				eebus := &EEBus{
 					emobility: emobilityMock,
 				}
diff --git a/charger/eebus_test_mock.go b/charger/eebus_test_mock.go
new file mode 100644
index 00000000..bc42eaae
--- /dev/null
+++ b/charger/eebus_test_mock.go
@@ -0,0 +1,350 @@
+// Code generated by MockGen. DO NOT EDIT.
+// Source: github.com/enbility/cemd/emobility (interfaces: EmobilityI)
+//
+// Generated by this command:
+//
+//	mockgen -package charger -destination eebus_test_mock.go github.com/enbility/cemd/emobility EmobilityI
+//
+
+// Package charger is a generated GoMock package.
+package charger
+
+import (
+	reflect "reflect"
+
+	emobility "github.com/enbility/cemd/emobility"
+	gomock "go.uber.org/mock/gomock"
+)
+
+// MockEmobilityI is a mock of EmobilityI interface.
+type MockEmobilityI struct {
+	ctrl     *gomock.Controller
+	recorder *MockEmobilityIMockRecorder
+}
+
+// MockEmobilityIMockRecorder is the mock recorder for MockEmobilityI.
+type MockEmobilityIMockRecorder struct {
+	mock *MockEmobilityI
+}
+
+// NewMockEmobilityI creates a new mock instance.
+func NewMockEmobilityI(ctrl *gomock.Controller) *MockEmobilityI {
+	mock := &MockEmobilityI{ctrl: ctrl}
+	mock.recorder = &MockEmobilityIMockRecorder{mock}
+	return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockEmobilityI) EXPECT() *MockEmobilityIMockRecorder {
+	return m.recorder
+}
+
+// EVChargeStrategy mocks base method.
+func (m *MockEmobilityI) EVChargeStrategy() emobility.EVChargeStrategyType {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "EVChargeStrategy")
+	ret0, _ := ret[0].(emobility.EVChargeStrategyType)
+	return ret0
+}
+
+// EVChargeStrategy indicates an expected call of EVChargeStrategy.
+func (mr *MockEmobilityIMockRecorder) EVChargeStrategy() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVChargeStrategy", reflect.TypeOf((*MockEmobilityI)(nil).EVChargeStrategy))
+}
+
+// EVChargedEnergy mocks base method.
+func (m *MockEmobilityI) EVChargedEnergy() (float64, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "EVChargedEnergy")
+	ret0, _ := ret[0].(float64)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// EVChargedEnergy indicates an expected call of EVChargedEnergy.
+func (mr *MockEmobilityIMockRecorder) EVChargedEnergy() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVChargedEnergy", reflect.TypeOf((*MockEmobilityI)(nil).EVChargedEnergy))
+}
+
+// EVCommunicationStandard mocks base method.
+func (m *MockEmobilityI) EVCommunicationStandard() (emobility.EVCommunicationStandardType, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "EVCommunicationStandard")
+	ret0, _ := ret[0].(emobility.EVCommunicationStandardType)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// EVCommunicationStandard indicates an expected call of EVCommunicationStandard.
+func (mr *MockEmobilityIMockRecorder) EVCommunicationStandard() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVCommunicationStandard", reflect.TypeOf((*MockEmobilityI)(nil).EVCommunicationStandard))
+}
+
+// EVConnected mocks base method.
+func (m *MockEmobilityI) EVConnected() bool {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "EVConnected")
+	ret0, _ := ret[0].(bool)
+	return ret0
+}
+
+// EVConnected indicates an expected call of EVConnected.
+func (mr *MockEmobilityIMockRecorder) EVConnected() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVConnected", reflect.TypeOf((*MockEmobilityI)(nil).EVConnected))
+}
+
+// EVConnectedPhases mocks base method.
+func (m *MockEmobilityI) EVConnectedPhases() (uint, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "EVConnectedPhases")
+	ret0, _ := ret[0].(uint)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// EVConnectedPhases indicates an expected call of EVConnectedPhases.
+func (mr *MockEmobilityIMockRecorder) EVConnectedPhases() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVConnectedPhases", reflect.TypeOf((*MockEmobilityI)(nil).EVConnectedPhases))
+}
+
+// EVCoordinatedChargingSupported mocks base method.
+func (m *MockEmobilityI) EVCoordinatedChargingSupported() (bool, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "EVCoordinatedChargingSupported")
+	ret0, _ := ret[0].(bool)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// EVCoordinatedChargingSupported indicates an expected call of EVCoordinatedChargingSupported.
+func (mr *MockEmobilityIMockRecorder) EVCoordinatedChargingSupported() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVCoordinatedChargingSupported", reflect.TypeOf((*MockEmobilityI)(nil).EVCoordinatedChargingSupported))
+}
+
+// EVCurrentChargeState mocks base method.
+func (m *MockEmobilityI) EVCurrentChargeState() (emobility.EVChargeStateType, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "EVCurrentChargeState")
+	ret0, _ := ret[0].(emobility.EVChargeStateType)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// EVCurrentChargeState indicates an expected call of EVCurrentChargeState.
+func (mr *MockEmobilityIMockRecorder) EVCurrentChargeState() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVCurrentChargeState", reflect.TypeOf((*MockEmobilityI)(nil).EVCurrentChargeState))
+}
+
+// EVCurrentLimits mocks base method.
+func (m *MockEmobilityI) EVCurrentLimits() ([]float64, []float64, []float64, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "EVCurrentLimits")
+	ret0, _ := ret[0].([]float64)
+	ret1, _ := ret[1].([]float64)
+	ret2, _ := ret[2].([]float64)
+	ret3, _ := ret[3].(error)
+	return ret0, ret1, ret2, ret3
+}
+
+// EVCurrentLimits indicates an expected call of EVCurrentLimits.
+func (mr *MockEmobilityIMockRecorder) EVCurrentLimits() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVCurrentLimits", reflect.TypeOf((*MockEmobilityI)(nil).EVCurrentLimits))
+}
+
+// EVCurrentsPerPhase mocks base method.
+func (m *MockEmobilityI) EVCurrentsPerPhase() ([]float64, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "EVCurrentsPerPhase")
+	ret0, _ := ret[0].([]float64)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// EVCurrentsPerPhase indicates an expected call of EVCurrentsPerPhase.
+func (mr *MockEmobilityIMockRecorder) EVCurrentsPerPhase() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVCurrentsPerPhase", reflect.TypeOf((*MockEmobilityI)(nil).EVCurrentsPerPhase))
+}
+
+// EVEnergyDemand mocks base method.
+func (m *MockEmobilityI) EVEnergyDemand() (emobility.EVDemand, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "EVEnergyDemand")
+	ret0, _ := ret[0].(emobility.EVDemand)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// EVEnergyDemand indicates an expected call of EVEnergyDemand.
+func (mr *MockEmobilityIMockRecorder) EVEnergyDemand() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVEnergyDemand", reflect.TypeOf((*MockEmobilityI)(nil).EVEnergyDemand))
+}
+
+// EVGetIncentiveConstraints mocks base method.
+func (m *MockEmobilityI) EVGetIncentiveConstraints() emobility.EVIncentiveSlotConstraints {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "EVGetIncentiveConstraints")
+	ret0, _ := ret[0].(emobility.EVIncentiveSlotConstraints)
+	return ret0
+}
+
+// EVGetIncentiveConstraints indicates an expected call of EVGetIncentiveConstraints.
+func (mr *MockEmobilityIMockRecorder) EVGetIncentiveConstraints() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVGetIncentiveConstraints", reflect.TypeOf((*MockEmobilityI)(nil).EVGetIncentiveConstraints))
+}
+
+// EVGetPowerConstraints mocks base method.
+func (m *MockEmobilityI) EVGetPowerConstraints() emobility.EVTimeSlotConstraints {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "EVGetPowerConstraints")
+	ret0, _ := ret[0].(emobility.EVTimeSlotConstraints)
+	return ret0
+}
+
+// EVGetPowerConstraints indicates an expected call of EVGetPowerConstraints.
+func (mr *MockEmobilityIMockRecorder) EVGetPowerConstraints() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVGetPowerConstraints", reflect.TypeOf((*MockEmobilityI)(nil).EVGetPowerConstraints))
+}
+
+// EVIdentification mocks base method.
+func (m *MockEmobilityI) EVIdentification() (string, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "EVIdentification")
+	ret0, _ := ret[0].(string)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// EVIdentification indicates an expected call of EVIdentification.
+func (mr *MockEmobilityIMockRecorder) EVIdentification() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVIdentification", reflect.TypeOf((*MockEmobilityI)(nil).EVIdentification))
+}
+
+// EVLoadControlObligationLimits mocks base method.
+func (m *MockEmobilityI) EVLoadControlObligationLimits() ([]float64, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "EVLoadControlObligationLimits")
+	ret0, _ := ret[0].([]float64)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// EVLoadControlObligationLimits indicates an expected call of EVLoadControlObligationLimits.
+func (mr *MockEmobilityIMockRecorder) EVLoadControlObligationLimits() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVLoadControlObligationLimits", reflect.TypeOf((*MockEmobilityI)(nil).EVLoadControlObligationLimits))
+}
+
+// EVOptimizationOfSelfConsumptionSupported mocks base method.
+func (m *MockEmobilityI) EVOptimizationOfSelfConsumptionSupported() (bool, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "EVOptimizationOfSelfConsumptionSupported")
+	ret0, _ := ret[0].(bool)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// EVOptimizationOfSelfConsumptionSupported indicates an expected call of EVOptimizationOfSelfConsumptionSupported.
+func (mr *MockEmobilityIMockRecorder) EVOptimizationOfSelfConsumptionSupported() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVOptimizationOfSelfConsumptionSupported", reflect.TypeOf((*MockEmobilityI)(nil).EVOptimizationOfSelfConsumptionSupported))
+}
+
+// EVPowerPerPhase mocks base method.
+func (m *MockEmobilityI) EVPowerPerPhase() ([]float64, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "EVPowerPerPhase")
+	ret0, _ := ret[0].([]float64)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// EVPowerPerPhase indicates an expected call of EVPowerPerPhase.
+func (mr *MockEmobilityIMockRecorder) EVPowerPerPhase() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVPowerPerPhase", reflect.TypeOf((*MockEmobilityI)(nil).EVPowerPerPhase))
+}
+
+// EVSoC mocks base method.
+func (m *MockEmobilityI) EVSoC() (float64, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "EVSoC")
+	ret0, _ := ret[0].(float64)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// EVSoC indicates an expected call of EVSoC.
+func (mr *MockEmobilityIMockRecorder) EVSoC() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVSoC", reflect.TypeOf((*MockEmobilityI)(nil).EVSoC))
+}
+
+// EVSoCSupported mocks base method.
+func (m *MockEmobilityI) EVSoCSupported() (bool, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "EVSoCSupported")
+	ret0, _ := ret[0].(bool)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// EVSoCSupported indicates an expected call of EVSoCSupported.
+func (mr *MockEmobilityIMockRecorder) EVSoCSupported() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVSoCSupported", reflect.TypeOf((*MockEmobilityI)(nil).EVSoCSupported))
+}
+
+// EVWriteIncentives mocks base method.
+func (m *MockEmobilityI) EVWriteIncentives(arg0 []emobility.EVDurationSlotValue) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "EVWriteIncentives", arg0)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// EVWriteIncentives indicates an expected call of EVWriteIncentives.
+func (mr *MockEmobilityIMockRecorder) EVWriteIncentives(arg0 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVWriteIncentives", reflect.TypeOf((*MockEmobilityI)(nil).EVWriteIncentives), arg0)
+}
+
+// EVWriteLoadControlLimits mocks base method.
+func (m *MockEmobilityI) EVWriteLoadControlLimits(arg0, arg1 []float64) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "EVWriteLoadControlLimits", arg0, arg1)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// EVWriteLoadControlLimits indicates an expected call of EVWriteLoadControlLimits.
+func (mr *MockEmobilityIMockRecorder) EVWriteLoadControlLimits(arg0, arg1 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVWriteLoadControlLimits", reflect.TypeOf((*MockEmobilityI)(nil).EVWriteLoadControlLimits), arg0, arg1)
+}
+
+// EVWritePowerLimits mocks base method.
+func (m *MockEmobilityI) EVWritePowerLimits(arg0 []emobility.EVDurationSlotValue) error {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "EVWritePowerLimits", arg0)
+	ret0, _ := ret[0].(error)
+	return ret0
+}
+
+// EVWritePowerLimits indicates an expected call of EVWritePowerLimits.
+func (mr *MockEmobilityIMockRecorder) EVWritePowerLimits(arg0 any) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EVWritePowerLimits", reflect.TypeOf((*MockEmobilityI)(nil).EVWritePowerLimits), arg0)
+}
diff --git a/core/coordinator/coordinator_test.go b/core/coordinator/coordinator_test.go
index 5b3dd9d1..5af8eec1 100644
--- a/core/coordinator/coordinator_test.go
+++ b/core/coordinator/coordinator_test.go
@@ -6,7 +6,7 @@ import (
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/core/loadpoint"
 	"github.com/evcc-io/evcc/util"
-	"github.com/golang/mock/gomock"
+	"go.uber.org/mock/gomock"
 )
 
 func TestVehicleDetectByStatus(t *testing.T) {
diff --git a/core/loadpoint_effective_test.go b/core/loadpoint_effective_test.go
index 060a836f..52e2957a 100644
--- a/core/loadpoint_effective_test.go
+++ b/core/loadpoint_effective_test.go
@@ -5,8 +5,8 @@ import (
 
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util"
-	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
+	"go.uber.org/mock/gomock"
 )
 
 func TestEffectiveLimitSoc(t *testing.T) {
diff --git a/core/loadpoint_phases_test.go b/core/loadpoint_phases_test.go
index 2c92c068..363aa2e4 100644
--- a/core/loadpoint_phases_test.go
+++ b/core/loadpoint_phases_test.go
@@ -9,7 +9,7 @@ import (
 	"github.com/benbjohnson/clock"
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util"
-	"github.com/golang/mock/gomock"
+	"go.uber.org/mock/gomock"
 )
 
 type testCase struct {
diff --git a/core/loadpoint_session_test.go b/core/loadpoint_session_test.go
index 2b2a4840..412d4f5f 100644
--- a/core/loadpoint_session_test.go
+++ b/core/loadpoint_session_test.go
@@ -9,9 +9,9 @@ import (
 	"github.com/evcc-io/evcc/core/session"
 	serverdb "github.com/evcc-io/evcc/server/db"
 	"github.com/evcc-io/evcc/util"
-	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	"go.uber.org/mock/gomock"
 )
 
 func TestSession(t *testing.T) {
diff --git a/core/loadpoint_sync_test.go b/core/loadpoint_sync_test.go
index 26096c87..995d28aa 100644
--- a/core/loadpoint_sync_test.go
+++ b/core/loadpoint_sync_test.go
@@ -6,9 +6,9 @@ import (
 	"github.com/benbjohnson/clock"
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util"
-	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	"go.uber.org/mock/gomock"
 )
 
 func TestSyncCharger(t *testing.T) {
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index a08ab6c4..d5c12983 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -10,7 +10,7 @@ import (
 	"github.com/evcc-io/evcc/core/soc"
 	"github.com/evcc-io/evcc/push"
 	"github.com/evcc-io/evcc/util"
-	"github.com/golang/mock/gomock"
+	"go.uber.org/mock/gomock"
 )
 
 const (
diff --git a/core/loadpoint_vehicle_test.go b/core/loadpoint_vehicle_test.go
index 9597d4cb..600105e3 100644
--- a/core/loadpoint_vehicle_test.go
+++ b/core/loadpoint_vehicle_test.go
@@ -11,8 +11,8 @@ import (
 	"github.com/evcc-io/evcc/core/coordinator"
 	"github.com/evcc-io/evcc/core/soc"
 	"github.com/evcc-io/evcc/util"
-	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
+	"go.uber.org/mock/gomock"
 )
 
 func TestPublishSocAndRange(t *testing.T) {
diff --git a/core/planner/planner_test.go b/core/planner/planner_test.go
index 507311fd..05f27fa9 100644
--- a/core/planner/planner_test.go
+++ b/core/planner/planner_test.go
@@ -8,9 +8,9 @@ import (
 	"github.com/benbjohnson/clock"
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util"
-	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	"go.uber.org/mock/gomock"
 )
 
 func rates(prices []float64, start time.Time, slotDuration time.Duration) api.Rates {
diff --git a/core/prioritizer/prioritizer_test.go b/core/prioritizer/prioritizer_test.go
index 2e45bd8d..870b3aba 100644
--- a/core/prioritizer/prioritizer_test.go
+++ b/core/prioritizer/prioritizer_test.go
@@ -4,8 +4,8 @@ import (
 	"testing"
 
 	"github.com/evcc-io/evcc/core/loadpoint"
-	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
+	"go.uber.org/mock/gomock"
 )
 
 func TestPrioritzer(t *testing.T) {
diff --git a/core/site_battery_test.go b/core/site_battery_test.go
index 6cd68636..e2ecda52 100644
--- a/core/site_battery_test.go
+++ b/core/site_battery_test.go
@@ -6,9 +6,9 @@ import (
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/core/loadpoint"
 	"github.com/evcc-io/evcc/util"
-	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+	"go.uber.org/mock/gomock"
 )
 
 func TestDetermineBatteryMode(t *testing.T) {
diff --git a/core/site_test.go b/core/site_test.go
index 539025d0..d03a7801 100644
--- a/core/site_test.go
+++ b/core/site_test.go
@@ -105,12 +105,17 @@ func TestGreenShare(t *testing.T) {
 		{
 			"half grid, half pv, battery charge, no lp",
 			1000, 1000, -1000, 1000, 0,
-			1, 1, 0,
+			0.5, 1, 0,
 		},
 		{
 			"half grid, half pv, battery charge, home, lp",
 			1000, 1000, -1000, 500, 500,
-			1, 1, 1,
+			0.5, 1, 0,
+		},
+		{
+			"pv ac limited, battery charge & grid import",
+			1000, 3000, -1000, 1000, 2000,
+			0.75, 1, 0.5,
 		},
 	}
 
@@ -123,7 +128,8 @@ func TestGreenShare(t *testing.T) {
 			batteryPower: tc.battery,
 		}
 
-		greenShareTotal := s.greenShare(0, tc.home+tc.lp)
+		totalPower := tc.grid + tc.pv + max(0, tc.battery)
+		greenShareTotal := s.greenShare(0, totalPower)
 		if greenShareTotal != tc.greenShareTotal {
 			t.Errorf("greenShareTotal wanted %.3f, got %.3f", tc.greenShareTotal, greenShareTotal)
 		}
@@ -131,7 +137,7 @@ func TestGreenShare(t *testing.T) {
 		if greenShareHome != tc.greenShareHome {
 			t.Errorf("greenShareHome wanted %.3f, got %.3f", tc.greenShareHome, greenShareHome)
 		}
-		greenShareLoadpoints := s.greenShare(tc.home, tc.home+tc.lp)
+		greenShareLoadpoints := s.greenShare(tc.home+max(0, -tc.battery), totalPower)
 		if greenShareLoadpoints != tc.greenShareLoadpoints {
 			t.Errorf("greenShareLoadpoints wanted %.3f, got %.3f", tc.greenShareLoadpoints, greenShareLoadpoints)
 		}
diff --git a/core/soc/estimator_test.go b/core/soc/estimator_test.go
index 4d5fe8bf..9c30eeef 100644
--- a/core/soc/estimator_test.go
+++ b/core/soc/estimator_test.go
@@ -7,8 +7,8 @@ import (
 
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util"
-	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
+	"go.uber.org/mock/gomock"
 )
 
 func TestRemainingChargeDuration(t *testing.T) {
diff --git a/core/wrapper/chargemeter_test.go b/core/wrapper/chargemeter_test.go
index a02bd233..26276ccf 100644
--- a/core/wrapper/chargemeter_test.go
+++ b/core/wrapper/chargemeter_test.go
@@ -3,7 +3,7 @@ package wrapper
 import (
 	"testing"
 
-	"github.com/golang/mock/gomock"
+	"go.uber.org/mock/gomock"
 )
 
 func TestProxyChargeMeter(t *testing.T) {
diff --git a/core/wrapper/chargerater_test.go b/core/wrapper/chargerater_test.go
index 5fea988a..8af799d1 100644
--- a/core/wrapper/chargerater_test.go
+++ b/core/wrapper/chargerater_test.go
@@ -7,7 +7,7 @@ import (
 	"github.com/benbjohnson/clock"
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/util"
-	"github.com/golang/mock/gomock"
+	"go.uber.org/mock/gomock"
 )
 
 func TestNoMeter(t *testing.T) {
diff --git a/tests/modals.spec.js b/tests/modals.spec.js
index 2eeac459..59f19195 100644
--- a/tests/modals.spec.js
+++ b/tests/modals.spec.js
@@ -5,6 +5,8 @@ const { startSimulator, stopSimulator } = require("./simulator");
 const BASICS_CONFIG = "basics.evcc.yaml";
 const SIMULATOR_CONFIG = "simulator.evcc.yaml";
 
+const UI_ROUTES = ["/", "/#/sessions", "/#/config"];
+
 test.describe("Basics", async () => {
   test.beforeAll(async () => {
     await start(BASICS_CONFIG);
@@ -15,13 +17,15 @@ test.describe("Basics", async () => {
   });
 
   test("Menu options. No battery and grid.", async ({ page }) => {
-    await page.goto("/");
-
-    await page.getByTestId("topnavigation-button").click();
-    await expect(page.getByRole("button", { name: "General Settings" })).toBeVisible();
-    await expect(page.getByRole("button", { name: "Battery Settings" })).not.toBeVisible();
-    await expect(page.getByRole("button", { name: "Smart Grid Charging" })).not.toBeVisible();
-    await expect(page.getByRole("button", { name: "Need help?" })).toBeVisible();
+    for (const route of UI_ROUTES) {
+      await page.goto(route);
+
+      await page.getByTestId("topnavigation-button").click();
+      await expect(page.getByRole("button", { name: "General Settings" })).toBeVisible();
+      await expect(page.getByRole("button", { name: "Battery Settings" })).not.toBeVisible();
+      await expect(page.getByRole("button", { name: "Smart Grid Charging" })).not.toBeVisible();
+      await expect(page.getByRole("button", { name: "Need help?" })).toBeVisible();
+    }
   });
 
   test("Need help?", async ({ page }) => {
@@ -54,13 +58,15 @@ test.describe("Advanced", async () => {
   });
 
   test("Menu options. All available.", async ({ page }) => {
-    await page.goto("/");
-
-    await page.getByTestId("topnavigation-button").click();
-    await expect(page.getByRole("button", { name: "General Settings" })).toBeVisible();
-    await expect(page.getByRole("button", { name: "Battery Settings" })).toBeVisible();
-    await expect(page.getByRole("button", { name: "Smart Grid Charging" })).toBeVisible();
-    await expect(page.getByRole("button", { name: "Need help?" })).toBeVisible();
+    for (const route of UI_ROUTES) {
+      await page.goto(route);
+
+      await page.getByTestId("topnavigation-button").click();
+      await expect(page.getByRole("button", { name: "General Settings" })).toBeVisible();
+      await expect(page.getByRole("button", { name: "Battery Settings" })).toBeVisible();
+      await expect(page.getByRole("button", { name: "Smart Grid Charging" })).toBeVisible();
+      await expect(page.getByRole("button", { name: "Need help?" })).toBeVisible();
+    }
   });
 
   test("Battery Settings from top navigation", async ({ page }) => {