Repository: evcc-io/evcc
Pull request #10558 (2023-10-29 14:21:49)

docs: document that NRGkick support only exists for older models
The list of supported charging cables has been updated to include only older NRGkick charging cables.

Currently, DiniTech GmbH refuses to publish the specification of the current communication protocol. According to discussions with NRGkick Support, it is also not guaranteed that a release is planned.

Further info:
- https://github.com/evcc-io/docs/pull/464
- https://github.com/evcc-io/evcc/discussions/4048


Patch:
diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml
index 25fce64a..825810b3 100644
--- a/.github/workflows/default.yml
+++ b/.github/workflows/default.yml
@@ -53,7 +53,7 @@ jobs:
           cache-key-prefix: build-cache-
         id: go
 
-      - uses: actions/setup-node@v3
+      - uses: actions/setup-node@v4
         with:
           node-version: "18"
           cache: "npm"
@@ -108,7 +108,7 @@ jobs:
     steps:
       - uses: actions/checkout@v4
 
-      - uses: actions/setup-node@v3
+      - uses: actions/setup-node@v4
         with:
           node-version: "18"
           cache: "npm"
@@ -149,7 +149,7 @@ jobs:
           cache-key-prefix: integration-cache-
         id: go
 
-      - uses: actions/setup-node@v3
+      - uses: actions/setup-node@v4
         with:
           node-version: "18"
           cache: "npm"
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
index cbd57f51..432535d2 100644
--- a/.github/workflows/nightly.yml
+++ b/.github/workflows/nightly.yml
@@ -63,7 +63,7 @@ jobs:
         uses: docker/setup-buildx-action@v3
 
       - name: Publish
-        uses: docker/build-push-action@v4
+        uses: docker/build-push-action@v5
         with:
           context: .
           platforms: linux/amd64,linux/arm64,linux/arm/v6
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 8dd4dd5b..7e6c2f47 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -41,7 +41,7 @@ jobs:
             evcc/evcc
 
       - name: Publish
-        uses: docker/build-push-action@v4
+        uses: docker/build-push-action@v5
         with:
           context: .
           platforms: linux/amd64,linux/arm64,linux/arm/v6
diff --git a/README.md b/README.md
index b0856581..39677be2 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ evcc is an extensible EV Charge Controller with PV integration implemented in [G
 
 - simple and clean user interface
 - wide range of supported [chargers](https://docs.evcc.io/docs/devices/chargers):
-  - ABL eMH1, Alfen (Eve), Bender (CC612/613), cFos (PowerBrain), Daheimladen, Ebee (Wallbox), Ensto (Chago Wallbox), [EVSEWifi/ smartWB](https://www.evse-wifi.de), Garo (GLB, GLB+, LS4), go-eCharger, HardyBarth (eCB1, cPH1, cPH2), Heidelberg (Energy Control), Innogy (eBox), Juice (Charger Me), KEBA/BMW, Menneckes (Amedio, Amtron Premium/Xtra, Amtron ChargeConrol), NRGkick, [openWB (includes Pro)](https://openwb.de/), Optec (Mobility One), PC Electric (includes Garo), Siemens, TechniSat (Technivolt), [Tinkerforge Warp Charger](https://www.warp-charger.com), Ubitricity (Heinz), Vestel, Wallbe, Webasto (Live), Mobile Charger Connect and many more
+  - ABL eMH1, Alfen (Eve), Bender (CC612/613), cFos (PowerBrain), Daheimladen, Ebee (Wallbox), Ensto (Chago Wallbox), [EVSEWifi/ smartWB](https://www.evse-wifi.de), Garo (GLB, GLB+, LS4), go-eCharger, HardyBarth (eCB1, cPH1, cPH2), Heidelberg (Energy Control), Innogy (eBox), Juice (Charger Me), KEBA/BMW, Menneckes (Amedio, Amtron Premium/Xtra, Amtron ChargeConrol), older NRGkicks (before 2022/2023), [openWB (includes Pro)](https://openwb.de/), Optec (Mobility One), PC Electric (includes Garo), Siemens, TechniSat (Technivolt), [Tinkerforge Warp Charger](https://www.warp-charger.com), Ubitricity (Heinz), Vestel, Wallbe, Webasto (Live), Mobile Charger Connect and many more
   - experimental EEBus support (Elli, PMCC)
   - experimental OCPP support
   - Build-your-own: Phoenix Contact (includes ESL Walli), [EVSE DIN](http://evracing.cz/simple-evse-wallbox)
diff --git a/api/api.go b/api/api.go
index 5cd88569..67dc3ccd 100644
--- a/api/api.go
+++ b/api/api.go
@@ -9,7 +9,7 @@ import (
 	"time"
 )
 
-//go:generate mockgen -package mock -destination ../mock/mock_api.go github.com/evcc-io/evcc/api Charger,ChargeState,PhaseSwitcher,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery,Tariff
+//go:generate mockgen -package api -destination api.go github.com/evcc-io/evcc/api Charger,ChargeState,PhaseSwitcher,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery,Tariff
 
 // ChargeMode is the charge operation mode. Valid values are off, now, minpv and pv
 type ChargeMode string
diff --git a/mock/mock_api.go b/api/mock.go
similarity index 96%
rename from mock/mock_api.go
rename to api/mock.go
index a52dabde..c46974f9 100644
--- a/mock/mock_api.go
+++ b/api/mock.go
@@ -1,13 +1,12 @@
 // Code generated by MockGen. DO NOT EDIT.
 // Source: github.com/evcc-io/evcc/api (interfaces: Charger,ChargeState,PhaseSwitcher,Identifier,Meter,MeterEnergy,Vehicle,ChargeRater,Battery,Tariff)
 
-// Package mock is a generated GoMock package.
-package mock
+// Package api is a generated GoMock package.
+package api
 
 import (
 	reflect "reflect"
 
-	api "github.com/evcc-io/evcc/api"
 	gomock "github.com/golang/mock/gomock"
 )
 
@@ -78,10 +77,10 @@ func (mr *MockChargerMockRecorder) MaxCurrent(arg0 interface{}) *gomock.Call {
 }
 
 // Status mocks base method.
-func (m *MockCharger) Status() (api.ChargeStatus, error) {
+func (m *MockCharger) Status() (ChargeStatus, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Status")
-	ret0, _ := ret[0].(api.ChargeStatus)
+	ret0, _ := ret[0].(ChargeStatus)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
@@ -116,10 +115,10 @@ func (m *MockChargeState) EXPECT() *MockChargeStateMockRecorder {
 }
 
 // Status mocks base method.
-func (m *MockChargeState) Status() (api.ChargeStatus, error) {
+func (m *MockChargeState) Status() (ChargeStatus, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Status")
-	ret0, _ := ret[0].(api.ChargeStatus)
+	ret0, _ := ret[0].(ChargeStatus)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
@@ -347,10 +346,10 @@ func (mr *MockVehicleMockRecorder) Identifiers() *gomock.Call {
 }
 
 // OnIdentified mocks base method.
-func (m *MockVehicle) OnIdentified() api.ActionConfig {
+func (m *MockVehicle) OnIdentified() ActionConfig {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "OnIdentified")
-	ret0, _ := ret[0].(api.ActionConfig)
+	ret0, _ := ret[0].(ActionConfig)
 	return ret0
 }
 
@@ -515,10 +514,10 @@ func (m *MockTariff) EXPECT() *MockTariffMockRecorder {
 }
 
 // Rates mocks base method.
-func (m *MockTariff) Rates() (api.Rates, error) {
+func (m *MockTariff) Rates() (Rates, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Rates")
-	ret0, _ := ret[0].(api.Rates)
+	ret0, _ := ret[0].(Rates)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
@@ -530,10 +529,10 @@ func (mr *MockTariffMockRecorder) Rates() *gomock.Call {
 }
 
 // Type mocks base method.
-func (m *MockTariff) Type() api.TariffType {
+func (m *MockTariff) Type() TariffType {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "Type")
-	ret0, _ := ret[0].(api.TariffType)
+	ret0, _ := ret[0].(TariffType)
 	return ret0
 }
 
diff --git a/charger/alphatec.go b/charger/alphatec.go
index b6b04ed2..fcb3a1d2 100644
--- a/charger/alphatec.go
+++ b/charger/alphatec.go
@@ -126,7 +126,7 @@ func (wb *Alphatec) Status() (api.ChargeStatus, error) {
 	switch u := binary.BigEndian.Uint16(b); u {
 	case 1:
 		return api.StatusA, nil
-	case 2:
+	case 2, 8:
 		return api.StatusB, nil
 	case 3:
 		return api.StatusC, nil
diff --git a/charger/amperfied.go b/charger/amperfied.go
new file mode 100644
index 00000000..de14e0c6
--- /dev/null
+++ b/charger/amperfied.go
@@ -0,0 +1,322 @@
+package charger
+
+// LICENSE
+
+// Copyright (c) 2023 premultiply
+
+// This module is NOT covered by the MIT license. All rights reserved.
+
+// 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 (
+	"encoding/binary"
+	"fmt"
+	"time"
+
+	"github.com/evcc-io/evcc/api"
+	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/modbus"
+	"github.com/evcc-io/evcc/util/sponsor"
+)
+
+// Amperfied charger implementation
+type Amperfied struct {
+	log     *util.Logger
+	conn    *modbus.Connection
+	current uint16
+	wakeup  bool
+}
+
+const (
+	ampRegChargingState  = 5    // Input
+	ampRegCurrents       = 6    // Input 6,7,8
+	ampRegTemperature    = 9    // Input
+	ampRegVoltages       = 10   // Input 10,11,12
+	ampRegPower          = 14   // Input
+	ampRegEnergy         = 17   // Input
+	ampRegTimeoutConfig  = 257  // Holding
+	ampRegRemoteLock     = 259  // Holding
+	ampRegAmpsConfig     = 261  // Holding
+	ampRegFailSafeConfig = 262  // Holding
+	ampRegRfidUID        = 2002 // Input
+)
+
+func init() {
+	registry.Add("amperfied", NewAmperfiedFromConfig)
+}
+
+// NewAmperfiedFromConfig creates a Amperfied charger from generic config
+func NewAmperfiedFromConfig(other map[string]interface{}) (api.Charger, error) {
+	cc := modbus.TcpSettings{
+		ID: 255,
+	}
+
+	if err := util.DecodeOther(other, &cc); err != nil {
+		return nil, err
+	}
+
+	return NewAmperfied(cc.URI, cc.ID)
+}
+
+// NewAmperfied creates Amperfied charger
+func NewAmperfied(uri string, slaveID uint8) (api.Charger, error) {
+	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, slaveID)
+	if err != nil {
+		return nil, err
+	}
+
+	if !sponsor.IsAuthorized() {
+		return nil, api.ErrSponsorRequired
+	}
+
+	log := util.NewLogger("amperfied")
+	conn.Logger(log.TRACE)
+
+	wb := &Amperfied{
+		log:     log,
+		conn:    conn,
+		current: 60, // assume min current
+	}
+
+	// get failsafe timeout from charger
+	b, err := wb.conn.ReadHoldingRegisters(ampRegTimeoutConfig, 1)
+	if err != nil {
+		return nil, fmt.Errorf("failsafe timeout: %w", err)
+	}
+	if u := binary.BigEndian.Uint16(b); u > 0 {
+		go wb.heartbeat(time.Duration(u) * time.Millisecond / 2)
+	}
+
+	return wb, nil
+}
+
+func (wb *Amperfied) heartbeat(timeout time.Duration) {
+	for range time.Tick(timeout) {
+		if _, err := wb.Status(); err != nil {
+			wb.log.ERROR.Println("heartbeat:", err)
+		}
+	}
+}
+
+func (wb *Amperfied) set(reg, val uint16) error {
+	b := make([]byte, 2)
+	binary.BigEndian.PutUint16(b, val)
+
+	_, err := wb.conn.WriteMultipleRegisters(reg, 1, b)
+
+	return err
+}
+
+// Status implements the api.Charger interface
+func (wb *Amperfied) Status() (api.ChargeStatus, error) {
+	b, err := wb.conn.ReadInputRegisters(ampRegChargingState, 1)
+	if err != nil {
+		return api.StatusNone, err
+	}
+
+	sb := binary.BigEndian.Uint16(b)
+
+	if sb != 10 {
+		wb.wakeup = false
+	}
+
+	switch sb {
+	case 2, 3:
+		return api.StatusA, nil
+	case 4, 5:
+		return api.StatusB, nil
+	case 6, 7:
+		return api.StatusC, nil
+	case 8:
+		return api.StatusD, nil
+	case 9:
+		return api.StatusE, nil
+	case 10:
+		// ensure RemoteLock is disabled after wake-up
+		b, err := wb.conn.ReadHoldingRegisters(ampRegRemoteLock, 1)
+		if err != nil {
+			return api.StatusNone, err
+		}
+
+		// unlock
+		if binary.BigEndian.Uint16(b) != 1 {
+			if err := wb.set(ampRegRemoteLock, 1); err != nil {
+				return api.StatusNone, err
+			}
+		}
+
+		// keep status B2 during wakeup
+		if wb.wakeup {
+			return api.StatusB, nil
+		}
+
+		return api.StatusF, nil
+	default:
+		return api.StatusNone, fmt.Errorf("invalid status: %d", sb)
+	}
+}
+
+// Enabled implements the api.Charger interface
+func (wb *Amperfied) Enabled() (bool, error) {
+	b, err := wb.conn.ReadHoldingRegisters(ampRegAmpsConfig, 1)
+	if err != nil {
+		return false, err
+	}
+
+	cur := binary.BigEndian.Uint16(b)
+
+	enabled := cur != 0
+	if enabled {
+		wb.current = cur
+	}
+
+	return enabled, nil
+}
+
+// Enable implements the api.Charger interface
+func (wb *Amperfied) Enable(enable bool) error {
+	var cur uint16
+	if enable {
+		cur = wb.current
+	}
+
+	b := make([]byte, 2)
+	binary.BigEndian.PutUint16(b, cur)
+
+	_, err := wb.conn.WriteMultipleRegisters(ampRegAmpsConfig, 1, b)
+
+	return err
+}
+
+// MaxCurrent implements the api.Charger interface
+func (wb *Amperfied) MaxCurrent(current int64) error {
+	return wb.MaxCurrentMillis(float64(current))
+}
+
+var _ api.ChargerEx = (*Amperfied)(nil)
+
+// MaxCurrentMillis implements the api.ChargerEx interface
+func (wb *Amperfied) MaxCurrentMillis(current float64) error {
+	if current < 6 {
+		return fmt.Errorf("invalid current %.1f", current)
+	}
+
+	cur := uint16(10 * current)
+
+	b := make([]byte, 2)
+	binary.BigEndian.PutUint16(b, cur)
+
+	_, err := wb.conn.WriteMultipleRegisters(ampRegAmpsConfig, 1, b)
+	if err == nil {
+		wb.current = cur
+	}
+
+	return err
+}
+
+var _ api.Meter = (*Amperfied)(nil)
+
+// CurrentPower implements the api.Meter interface
+func (wb *Amperfied) CurrentPower() (float64, error) {
+	b, err := wb.conn.ReadInputRegisters(ampRegPower, 1)
+	if err != nil {
+		return 0, err
+	}
+
+	return float64(binary.BigEndian.Uint16(b)), nil
+}
+
+var _ api.MeterEnergy = (*Amperfied)(nil)
+
+// TotalEnergy implements the api.MeterEnergy interface
+func (wb *Amperfied) TotalEnergy() (float64, error) {
+	b, err := wb.conn.ReadInputRegisters(ampRegEnergy, 2)
+	if err != nil {
+		return 0, err
+	}
+
+	return float64(binary.BigEndian.Uint32(b)) / 1e3, nil
+}
+
+// getPhaseValues returns 3 sequential register values
+func (wb *Amperfied) getPhaseValues(reg uint16, divider float64) (float64, float64, float64, error) {
+	b, err := wb.conn.ReadInputRegisters(reg, 3)
+	if err != nil {
+		return 0, 0, 0, err
+	}
+
+	var res [3]float64
+	for i := 0; i < 3; i++ {
+		res[i] = float64(binary.BigEndian.Uint16(b[2*i:])) / divider
+	}
+
+	return res[0], res[1], res[2], nil
+}
+
+var _ api.PhaseCurrents = (*Amperfied)(nil)
+
+// Currents implements the api.PhaseCurrents interface
+func (wb *Amperfied) Currents() (float64, float64, float64, error) {
+	return wb.getPhaseValues(ampRegCurrents, 10)
+}
+
+var _ api.PhaseVoltages = (*Amperfied)(nil)
+
+// Voltages implements the api.PhaseVoltages interface
+func (wb *Amperfied) Voltages() (float64, float64, float64, error) {
+	return wb.getPhaseValues(ampRegVoltages, 1)
+}
+
+var _ api.Identifier = (*Amperfied)(nil)
+
+// identify implements the api.Identifier interface
+func (wb *Amperfied) Identify() (string, error) {
+	b, err := wb.conn.ReadInputRegisters(ampRegRfidUID, 6)
+	if err != nil {
+		return "", err
+	}
+
+	return bytesAsString(b), nil
+}
+
+var _ api.Diagnosis = (*Amperfied)(nil)
+
+// Diagnose implements the api.Diagnosis interface
+func (wb *Amperfied) Diagnose() {
+	if b, err := wb.conn.ReadInputRegisters(ampRegTemperature, 1); err == nil {
+		fmt.Printf("Temperature:\t%.1fC\n", float64(int16(binary.BigEndian.Uint16(b)))/10)
+	}
+	if b, err := wb.conn.ReadHoldingRegisters(ampRegTimeoutConfig, 1); err == nil {
+		fmt.Printf("Timeout:\t%d\n", binary.BigEndian.Uint16(b))
+	}
+	if b, err := wb.conn.ReadHoldingRegisters(ampRegRemoteLock, 1); err == nil {
+		fmt.Printf("Remote Lock:\t%d\n", binary.BigEndian.Uint16(b))
+	}
+	if b, err := wb.conn.ReadHoldingRegisters(ampRegFailSafeConfig, 1); err == nil {
+		fmt.Printf("FailSafe:\t%d\n", binary.BigEndian.Uint16(b))
+	}
+}
+
+var _ api.Resurrector = (*Amperfied)(nil)
+
+// WakeUp implements the api.Resurrector interface
+func (wb *Amperfied) WakeUp() error {
+	// force status F by locking
+	if err := wb.set(ampRegRemoteLock, 0); err == nil {
+		// Takes at least ~10 sec to return to normal operation
+		// after locking even if unlocking immediately.
+		wb.wakeup = true
+	}
+
+	// return to normal operation by unlocking after ~10 sec
+	return wb.set(ampRegRemoteLock, 1)
+}
diff --git a/charger/openevse.go b/charger/openevse.go
index bf4729fe..26c5d32e 100644
--- a/charger/openevse.go
+++ b/charger/openevse.go
@@ -103,8 +103,9 @@ func (c *OpenEVSE) setOverride() error {
 	if c.enabled {
 		state = openevse.Enabled
 	}
-	data.State = &state
-	data.MaxCurrent = &c.current
+
+	data.State = state
+	data.MaxCurrent = c.current
 
 	req, err := request.New(http.MethodPost, uri, request.MarshalJSON(data), request.JSONEncoding)
 	if err == nil {
@@ -153,11 +154,11 @@ func (c *OpenEVSE) Status() (api.ChargeStatus, error) {
 		255: "disabled"
 	*/
 
-	switch state := *res.State; state {
+	switch state := res.State; state {
 	case 1:
 		return api.StatusA, err
 	case 2, 254, 255:
-		if *res.Vehicle == 1 {
+		if res.Vehicle == 1 {
 			return api.StatusB, err
 		}
 		return api.StatusA, err
@@ -175,7 +176,7 @@ func (c *OpenEVSE) Status() (api.ChargeStatus, error) {
 // Enabled implements the api.Charger interface
 func (c *OpenEVSE) Enabled() (bool, error) {
 	res, err := c.statusG.Get()
-	return *res.Status == openevse.Enabled, err
+	return res.Status == openevse.Enabled, err
 }
 
 // Enable implements the api.Charger interface
@@ -199,7 +200,7 @@ func (c *OpenEVSE) ChargedEnergy() (float64, error) {
 		return 0, err
 	}
 
-	return *res.SessionEnergy / 1e3, err
+	return res.SessionEnergy / 1e3, err
 }
 
 var _ api.ChargeTimer = (*OpenEVSE)(nil)
@@ -209,7 +210,8 @@ func (c *OpenEVSE) ChargingTime() (time.Duration, error) {
 	if err != nil {
 		return 0, err
 	}
-	return time.Duration(*res.SessionElapsed) * time.Second, err
+
+	return time.Duration(res.SessionElapsed) * time.Second, err
 }
 
 var _ api.MeterEnergy = (*OpenEVSE)(nil)
@@ -221,7 +223,7 @@ func (c *OpenEVSE) TotalEnergy() (float64, error) {
 		return 0, err
 	}
 
-	return *res.TotalEnergy, err
+	return res.TotalEnergy, err
 }
 
 var _ api.Meter = (*OpenEVSE)(nil)
@@ -231,7 +233,8 @@ func (c *OpenEVSE) CurrentPower() (float64, error) {
 	if err != nil {
 		return 0, err
 	}
-	return *res.Power, err
+
+	return res.Power, err
 }
 
 // phases1p3p implements the api.ChargePhases interface
diff --git a/charger/openevse/types.go b/charger/openevse/types.go
index 11a3492f..b471adc7 100644
--- a/charger/openevse/types.go
+++ b/charger/openevse/types.go
@@ -7,24 +7,24 @@ const (
 
 // Only keep the interesting properties from the status endpoint
 type Status struct {
-	Amp            *float64 `json:"amp,omitempty"`             // the value of the charge current in mA
-	Elapsed        *float64 `json:"elapsed,omitempty"`         // current session duration in seconds
-	ManualOverride *int     `json:"manual_override,omitempty"` // 1 = active, 0 = default
-	Mode           *string  `json:"mode,omitempty"`            // The current mode of the EVSE
-	Pilot          *int     `json:"pilot,omitempty"`           // the pilot value, in amps
-	Power          *float64 `json:"power,omitempty"`           // apparent power in watts
-	SessionEnergy  *float64 `json:"session_energy"`            // The total amount of energy accumulated for current session (in wh)
-	SessionElapsed *int     `json:"session_elapsed"`           // duration of this charging session in seconds
-	State          *int     `json:"state,omitempty"`           // evse state 1=A 2=B 3=C 4=D 5-11=F 254=sleeping 255=disabled
-	Status         *string  `json:"status,omitempty"`          // active, disabled, none, unknown
-	TotalEnergy    *float64 `json:"total_energy"`              // The total amount of energy accumulated (in kwh)
-	Vehicle        *int     `json:"vehicle,omitempty"`         // 0=not connected, 1=connected
-	Voltage        *float64 `json:"voltage,omitempty"`         // supplied via MQTT/Tesla/HTTP or assume a default
+	Amp            float64 `json:"amp,omitempty"`             // the value of the charge current in mA
+	Elapsed        float64 `json:"elapsed,omitempty"`         // current session duration in seconds
+	ManualOverride int     `json:"manual_override,omitempty"` // 1 = active, 0 = default
+	Mode           string  `json:"mode,omitempty"`            // The current mode of the EVSE
+	Pilot          int     `json:"pilot,omitempty"`           // the pilot value, in amps
+	Power          float64 `json:"power,omitempty"`           // apparent power in watts
+	SessionEnergy  float64 `json:"session_energy"`            // The total amount of energy accumulated for current session (in wh)
+	SessionElapsed int     `json:"session_elapsed"`           // duration of this charging session in seconds
+	State          int     `json:"state,omitempty"`           // evse state 1=A 2=B 3=C 4=D 5-11=F 254=sleeping 255=disabled
+	Status         string  `json:"status,omitempty"`          // active, disabled, none, unknown
+	TotalEnergy    float64 `json:"total_energy"`              // The total amount of energy accumulated (in kwh)
+	Vehicle        int     `json:"vehicle,omitempty"`         // 0=not connected, 1=connected
+	Voltage        float64 `json:"voltage,omitempty"`         // supplied via MQTT/Tesla/HTTP or assume a default
 }
 
 type Override struct {
-	State         *string `json:"state,omitempty"`          // Either enable charging (active) or block charging (disabled)
-	ChargeCurrent *int    `json:"charge_current,omitempty"` // Specify the active charge current in Amps >= 0
-	MaxCurrent    *int    `json:"max_current,omitempty"`    // Maximum current, primarily for load sharing situations
-	AutoRelease   *bool   `json:"auto_release,omitempty"`
+	State         string `json:"state,omitempty"`          // Either enable charging (active) or block charging (disabled)
+	ChargeCurrent int    `json:"charge_current,omitempty"` // Specify the active charge current in Amps >= 0
+	MaxCurrent    int    `json:"max_current,omitempty"`    // Maximum current, primarily for load sharing situations
+	AutoRelease   bool   `json:"auto_release,omitempty"`
 }
diff --git a/charger/openwb-2.0.go b/charger/openwb-2.0.go
index e182a8d6..bf24a16e 100644
--- a/charger/openwb-2.0.go
+++ b/charger/openwb-2.0.go
@@ -38,6 +38,8 @@ func init() {
 
 // https://openwb.de/main/wp-content/uploads/2023/10/ModbusTCP-openWB-series2-Pro-1.pdf
 
+//go:generate go run ../cmd/tools/decorate.go -f decorateOpenWB20 -b *OpenWB20 -r api.Charger -t "api.Identifier,Identify,func() (string, error)"
+
 // NewOpenWB20FromConfig creates a OpenWB20 charger from generic config
 func NewOpenWB20FromConfig(other map[string]interface{}) (api.Charger, error) {
 	cc := modbus.TcpSettings{
@@ -48,7 +50,17 @@ func NewOpenWB20FromConfig(other map[string]interface{}) (api.Charger, error) {
 		return nil, err
 	}
 
-	return NewOpenWB20(cc.URI, cc.ID)
+	wb, err := NewOpenWB20(cc.URI, cc.ID)
+	if err != nil {
+		return nil, err
+	}
+
+	var identify func() (string, error)
+	if _, err := wb.identify(); err == nil {
+		identify = wb.identify
+	}
+
+	return decorateOpenWB20(wb, identify), nil
 }
 
 // NewOpenWB20 creates OpenWB20 charger
@@ -180,10 +192,8 @@ func (wb *OpenWB20) WakeUp() error {
 	return err
 }
 
-var _ api.Identifier = (*OpenWB20)(nil)
-
 // Identify implements the api.Identifier interface
-func (wb *OpenWB20) Identify() (string, error) {
+func (wb *OpenWB20) identify() (string, error) {
 	b, err := wb.conn.ReadInputRegisters(openwbRegRfid, 10)
 	if err != nil {
 		return "", err
diff --git a/charger/openwb-2.0_decorators.go b/charger/openwb-2.0_decorators.go
new file mode 100644
index 00000000..4887e8db
--- /dev/null
+++ b/charger/openwb-2.0_decorators.go
@@ -0,0 +1,35 @@
+package charger
+
+// Code generated by github.com/evcc-io/evcc/cmd/tools/decorate.go. DO NOT EDIT.
+
+import (
+	"github.com/evcc-io/evcc/api"
+)
+
+func decorateOpenWB20(base *OpenWB20, identifier func() (string, error)) api.Charger {
+	switch {
+	case identifier == nil:
+		return base
+
+	case identifier != nil:
+		return &struct {
+			*OpenWB20
+			api.Identifier
+		}{
+			OpenWB20: base,
+			Identifier: &decorateOpenWB20IdentifierImpl{
+				identifier: identifier,
+			},
+		}
+	}
+
+	return nil
+}
+
+type decorateOpenWB20IdentifierImpl struct {
+	identifier func() (string, error)
+}
+
+func (impl *decorateOpenWB20IdentifierImpl) Identify() (string, error) {
+	return impl.identifier()
+}
diff --git a/charger/tasmota.go b/charger/tasmota.go
index a0f0a317..5c0b13ed 100644
--- a/charger/tasmota.go
+++ b/charger/tasmota.go
@@ -15,8 +15,7 @@ import (
 
 // Tasmota charger implementation
 type Tasmota struct {
-	conn    *tasmota.Connection
-	channel int
+	conn *tasmota.Connection
 	*switchSocket
 }
 
@@ -32,10 +31,10 @@ func NewTasmotaFromConfig(other map[string]interface{}) (api.Charger, error) {
 		User         string
 		Password     string
 		StandbyPower float64
-		Channel      int
+		Channel      []int
 		Cache        time.Duration
 	}{
-		Channel: 1,
+		Channel: []int{1},
 		Cache:   time.Second,
 	}
 
@@ -47,20 +46,19 @@ func NewTasmotaFromConfig(other map[string]interface{}) (api.Charger, error) {
 }
 
 // NewTasmota creates Tasmota charger
-func NewTasmota(embed embed, uri, user, password string, channel int, standbypower float64, cache time.Duration) (*Tasmota, error) {
-	conn, err := tasmota.NewConnection(uri, user, password, channel, cache)
+func NewTasmota(embed embed, uri, user, password string, channels []int, standbypower float64, cache time.Duration) (*Tasmota, error) {
+	conn, err := tasmota.NewConnection(uri, user, password, channels, cache)
 	if err != nil {
 		return nil, err
 	}
 
 	c := &Tasmota{
-		conn:    conn,
-		channel: channel,
+		conn: conn,
 	}
 
 	c.switchSocket = NewSwitchSocket(&embed, c.Enabled, c.conn.CurrentPower, standbypower)
 
-	return c, c.conn.ChannelExists(channel)
+	return c, c.conn.RelayExists()
 }
 
 // Enabled implements the api.Charger interface
@@ -79,3 +77,17 @@ var _ api.MeterEnergy = (*Tasmota)(nil)
 func (c *Tasmota) TotalEnergy() (float64, error) {
 	return c.conn.TotalEnergy()
 }
+
+var _ api.PhaseCurrents = (*Tasmota)(nil)
+
+// Currents implements the api.PhaseCurrents interface
+func (c *Tasmota) Currents() (float64, float64, float64, error) {
+	return c.conn.Currents()
+}
+
+var _ api.PhaseVoltages = (*Tasmota)(nil)
+
+// Voltages implements the api.PhaseVoltages interface
+func (c *Tasmota) Voltages() (float64, float64, float64, error) {
+	return c.conn.Voltages()
+}
diff --git a/charger/vestel.go b/charger/vestel.go
index 9d4c8df7..6545d034 100644
--- a/charger/vestel.go
+++ b/charger/vestel.go
@@ -55,6 +55,7 @@ var (
 type Vestel struct {
 	log     *util.Logger
 	conn    *modbus.Connection
+	enabled bool
 	current uint16
 }
 
@@ -95,13 +96,25 @@ func NewVestel(uri string, id uint8) (*Vestel, error) {
 		current: 6,
 	}
 
-	go wb.heartbeat()
+	// get failsafe timeout from charger
+	b, err := wb.conn.ReadInputRegisters(vestelRegFailsafeTimeout, 1)
+	if err != nil {
+		return nil, fmt.Errorf("failsafe timeout: %w", err)
+	}
+	timeout := 10 * time.Second
+	if u := binary.BigEndian.Uint16(b); u > 0 {
+		timeout = time.Duration(u) * time.Second / 4
+	}
+	if timeout < 3*time.Second {
+		timeout = 3 * time.Second
+	}
+	go wb.heartbeat(timeout)
 
 	return wb, nil
 }
 
-func (wb *Vestel) heartbeat() {
-	for range time.Tick(time.Second * 3) {
+func (wb *Vestel) heartbeat(timeout time.Duration) {
+	for range time.Tick(timeout) {
 		if _, err := wb.conn.WriteSingleRegister(vestelRegAlive, 1); err != nil {
 			wb.log.ERROR.Println("heartbeat:", err)
 		}
@@ -127,12 +140,14 @@ func (wb *Vestel) Status() (api.ChargeStatus, error) {
 
 // Enabled implements the api.Charger interface
 func (wb *Vestel) Enabled() (bool, error) {
-	b, err := wb.conn.ReadHoldingRegisters(vestelRegMaxCurrent, 1)
-	if err != nil {
-		return false, err
-	}
+	return verifyEnabled(wb, wb.enabled)
+
+	// b, err := wb.conn.ReadHoldingRegisters(vestelRegMaxCurrent, 1)
+	// if err != nil {
+	// 	return false, err
+	// }
 
-	return binary.BigEndian.Uint16(b) > 0, nil
+	// return binary.BigEndian.Uint16(b) > 0, nil
 }
 
 // Enable implements the api.Charger interface
@@ -143,6 +158,9 @@ func (wb *Vestel) Enable(enable bool) error {
 	}
 
 	_, err := wb.conn.WriteSingleRegister(vestelRegMaxCurrent, u)
+	if err == nil {
+		wb.enabled = enable
+	}
 
 	return err
 }
diff --git a/charger/webasto-next.go b/charger/webasto-next.go
index 57871405..731346ce 100644
--- a/charger/webasto-next.go
+++ b/charger/webasto-next.go
@@ -150,7 +150,7 @@ func (wb *WebastoNext) Enable(enable bool) error {
 
 // Enabled implements the api.Charger interface
 func (wb *WebastoNext) Enabled() (bool, error) {
-	return wb.enabled, nil
+	return verifyEnabled(wb, wb.enabled)
 
 	// b, err := wb.conn.ReadHoldingRegisters(1104, 1)
 	// if err != nil {
diff --git a/core/loadpoint/api.go b/core/loadpoint/api.go
index 226f505b..82936697 100644
--- a/core/loadpoint/api.go
+++ b/core/loadpoint/api.go
@@ -6,7 +6,7 @@ import (
 	"github.com/evcc-io/evcc/api"
 )
 
-//go:generate mockgen -package loadpoint -destination mock.go -mock_names API=MockAPI github.com/evcc-io/evcc/core/loadpoint API
+//go:generate mockgen -package loadpoint -destination api.go -mock_names API=MockAPI github.com/evcc-io/evcc/core/loadpoint API
 
 // Controller gives access to loadpoint
 type Controller interface {
diff --git a/core/site_api.go b/core/site_api.go
index 6be6cdeb..7df9be2d 100644
--- a/core/site_api.go
+++ b/core/site_api.go
@@ -10,6 +10,8 @@ import (
 
 var _ site.API = (*Site)(nil)
 
+var ErrBatteryNotConfigured = errors.New("battery not configured")
+
 const (
 	GridTariff    = "grid"
 	FeedinTariff  = "feedin"
@@ -29,13 +31,17 @@ func (site *Site) SetPrioritySoc(soc float64) error {
 	defer site.Unlock()
 
 	if len(site.batteryMeters) == 0 {
-		return errors.New("battery not configured")
+		return ErrBatteryNotConfigured
 	}
 
-	site.PrioritySoc = soc
-	settings.SetFloat("site.prioritySoc", site.PrioritySoc)
-	site.publish("prioritySoc", site.PrioritySoc)
+	site.log.DEBUG.Println("set priority soc:", soc)
+
+	if site.PrioritySoc != soc {
+		site.PrioritySoc = soc
+		settings.SetFloat("site.prioritySoc", site.PrioritySoc)
+		site.publish("prioritySoc", site.PrioritySoc)
 
+	}
 	return nil
 }
 
@@ -52,12 +58,16 @@ func (site *Site) SetBufferSoc(soc float64) error {
 	defer site.Unlock()
 
 	if len(site.batteryMeters) == 0 {
-		return errors.New("battery not configured")
+		return ErrBatteryNotConfigured
 	}
 
-	site.BufferSoc = soc
-	settings.SetFloat("site.bufferSoc", site.BufferSoc)
-	site.publish("bufferSoc", site.BufferSoc)
+	site.log.DEBUG.Println("set buffer soc:", soc)
+
+	if site.BufferSoc != soc {
+		site.BufferSoc = soc
+		settings.SetFloat("site.bufferSoc", site.BufferSoc)
+		site.publish("bufferSoc", site.BufferSoc)
+	}
 
 	return nil
 }
@@ -75,12 +85,16 @@ func (site *Site) SetBufferStartSoc(soc float64) error {
 	defer site.Unlock()
 
 	if len(site.batteryMeters) == 0 {
-		return errors.New("battery not configured")
+		return ErrBatteryNotConfigured
 	}
 
-	site.BufferStartSoc = soc
-	settings.SetFloat("site.bufferStartSoc", site.BufferStartSoc)
-	site.publish("bufferStartSoc", site.BufferStartSoc)
+	site.log.DEBUG.Println("set buffer start soc:", soc)
+
+	if site.BufferStartSoc != soc {
+		site.BufferStartSoc = soc
+		settings.SetFloat("site.bufferStartSoc", site.BufferStartSoc)
+		site.publish("bufferStartSoc", site.BufferStartSoc)
+	}
 
 	return nil
 }
@@ -97,8 +111,12 @@ func (site *Site) SetResidualPower(power float64) error {
 	site.Lock()
 	defer site.Unlock()
 
-	site.ResidualPower = power
-	site.publish("residualPower", site.ResidualPower)
+	site.log.DEBUG.Println("set residual power:", power)
+
+	if site.ResidualPower != power {
+		site.ResidualPower = power
+		site.publish("residualPower", site.ResidualPower)
+	}
 
 	return nil
 }
@@ -115,9 +133,13 @@ func (site *Site) SetSmartCostLimit(val float64) error {
 	site.Lock()
 	defer site.Unlock()
 
-	site.SmartCostLimit = val
-	settings.SetFloat("site.smartCostLimit", site.SmartCostLimit)
-	site.publish("smartCostLimit", site.SmartCostLimit)
+	site.log.DEBUG.Println("set smart cost limit:", val)
+
+	if site.SmartCostLimit != val {
+		site.SmartCostLimit = val
+		settings.SetFloat("site.smartCostLimit", site.SmartCostLimit)
+		site.publish("smartCostLimit", site.SmartCostLimit)
+	}
 
 	return nil
 }
diff --git a/go.mod b/go.mod
index 3a7239ea..a8e2262e 100644
--- a/go.mod
+++ b/go.mod
@@ -59,7 +59,7 @@ require (
 	github.com/korylprince/ipnetgen v1.0.1
 	github.com/kr/pretty v0.3.1
 	github.com/libp2p/zeroconf/v2 v2.2.0
-	github.com/lorenzodonini/ocpp-go v0.17.1-0.20231022204909-85c930b1a567
+	github.com/lorenzodonini/ocpp-go v0.17.1-0.20231031074144-d214d7c42cbc
 	github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40
 	github.com/mabunixda/wattpilot v1.6.2
 	github.com/manifoldco/promptui v0.9.0
@@ -169,6 +169,7 @@ require (
 	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
 	github.com/prometheus/client_model v0.5.0 // indirect
 	github.com/prometheus/procfs v0.12.0 // indirect
+	github.com/relvacode/iso8601 v1.3.0 // indirect
 	github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
 	github.com/rickb777/date v1.20.5 // indirect
 	github.com/rickb777/plural v1.4.1 // indirect
diff --git a/go.sum b/go.sum
index e43c923a..13636069 100644
--- a/go.sum
+++ b/go.sum
@@ -527,8 +527,8 @@ github.com/libp2p/zeroconf/v2 v2.2.0 h1:Cup06Jv6u81HLhIj1KasuNM/RHHrJ8T7wOTS4+Tv
 github.com/libp2p/zeroconf/v2 v2.2.0/go.mod h1:fuJqLnUwZTshS3U/bMRJ3+ow/v9oid1n0DmyYyNO1Xs=
 github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM=
 github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4=
-github.com/lorenzodonini/ocpp-go v0.17.1-0.20231022204909-85c930b1a567 h1:+qBZ5h4KmEG5IGmlDYwvjjm/ywlHarGOOrih/GbUz3w=
-github.com/lorenzodonini/ocpp-go v0.17.1-0.20231022204909-85c930b1a567/go.mod h1:jVzgk5k3JVGltWMU/QGlU7chojlCd/bJYc4WYl8R2M4=
+github.com/lorenzodonini/ocpp-go v0.17.1-0.20231031074144-d214d7c42cbc h1:T18nkqs09wcmipKdYNTtN2xT0tvOq+b7bapjD8ZHV1I=
+github.com/lorenzodonini/ocpp-go v0.17.1-0.20231031074144-d214d7c42cbc/go.mod h1:ZynYDWGw6CslG3vyPuucLsy6AyE+h3XXYlr39jhNiQY=
 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
 github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
 github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ=
@@ -699,6 +699,8 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1
 github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
 github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
 github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
+github.com/relvacode/iso8601 v1.3.0 h1:HguUjsGpIMh/zsTczGN3DVJFxTU/GX+MMmzcKoMO7ko=
+github.com/relvacode/iso8601 v1.3.0/go.mod h1:FlNp+jz+TXpyRqgmM7tnzHHzBnz776kmAH2h3sZCn0I=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
 github.com/rickb777/date v1.20.5 h1:Ybjz7J7ga9ui4VJizQpil0l330r6wkn6CicaoattIxQ=
diff --git a/meter/tasmota.go b/meter/tasmota.go
index e069bbd1..f046b808 100644
--- a/meter/tasmota.go
+++ b/meter/tasmota.go
@@ -1,6 +1,7 @@
 package meter
 
 import (
+	"strings"
 	"time"
 
 	"github.com/evcc-io/evcc/api"
@@ -19,17 +20,19 @@ func init() {
 	registry.Add("tasmota", NewTasmotaFromConfig)
 }
 
+//go:generate go run ../cmd/tools/decorate.go -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) {
 	cc := struct {
 		URI      string
 		User     string
 		Password string
-		Channel  int
+		Channel  []int
 		Usage    string
 		Cache    time.Duration
 	}{
-		Channel: 1,
+		Channel: []int{1},
 		Cache:   time.Second,
 	}
 
@@ -37,12 +40,12 @@ func NewTasmotaFromConfig(other map[string]interface{}) (api.Meter, error) {
 		return nil, err
 	}
 
-	return NewTasmota(cc.URI, cc.User, cc.Password, cc.Usage, cc.Channel, cc.Cache)
+	return NewTasmota(cc.URI, cc.User, cc.Password, strings.ToLower(cc.Usage), cc.Channel, cc.Cache)
 }
 
 // NewTasmota creates Tasmota meter
-func NewTasmota(uri, user, password, usage string, channel int, cache time.Duration) (*Tasmota, error) {
-	conn, err := tasmota.NewConnection(uri, user, password, channel, cache)
+func NewTasmota(uri, user, password, usage string, channels []int, cache time.Duration) (api.Meter, error) {
+	conn, err := tasmota.NewConnection(uri, user, password, channels, cache)
 	if err != nil {
 		return nil, err
 	}
@@ -52,7 +55,13 @@ func NewTasmota(uri, user, password, usage string, channel int, cache time.Durat
 		usage: usage,
 	}
 
-	return c, err
+	var currents, voltages func() (float64, float64, float64, error)
+	if usage != "grid" {
+		currents = c.currents
+		voltages = c.voltages
+	}
+
+	return decorateTasmota(c, currents, voltages), nil
 }
 
 var _ api.Meter = (*Tasmota)(nil)
@@ -74,3 +83,13 @@ func (c *Tasmota) TotalEnergy() (float64, error) {
 	}
 	return c.conn.TotalEnergy()
 }
+
+// currents implements the api.PhaseCurrents interface
+func (c *Tasmota) currents() (float64, float64, float64, error) {
+	return c.conn.Currents()
+}
+
+// voltages implements the api.PhaseVoltages interface
+func (c *Tasmota) voltages() (float64, float64, float64, error) {
+	return c.conn.Voltages()
+}
diff --git a/meter/tasmota/connection.go b/meter/tasmota/connection.go
index 9803765d..b23215e3 100644
--- a/meter/tasmota/connection.go
+++ b/meter/tasmota/connection.go
@@ -17,24 +17,35 @@ import (
 type Connection struct {
 	*request.Helper
 	uri, user, password string
-	channel             int
+	channels            []int
 	statusSnsG          provider.Cacheable[StatusSNSResponse]
 	statusStsG          provider.Cacheable[StatusSTSResponse]
 }
 
 // NewConnection creates a Tasmota connection
-func NewConnection(uri, user, password string, channel int, cache time.Duration) (*Connection, error) {
+func NewConnection(uri, user, password string, channels []int, cache time.Duration) (*Connection, error) {
 	if uri == "" {
 		return nil, errors.New("missing uri")
 	}
 
+	used := make(map[int]bool)
+	for _, c := range channels {
+		if c < 1 || c > 8 {
+			return nil, fmt.Errorf("invalid channel: %d", c)
+		}
+		if used[c] {
+			return nil, fmt.Errorf("duplicate channel: %d", c)
+		}
+		used[c] = true
+	}
+
 	log := util.NewLogger("tasmota")
 	c := &Connection{
 		Helper:   request.NewHelper(log),
 		uri:      util.DefaultScheme(strings.TrimRight(uri, "/"), "http"),
 		user:     user,
 		password: password,
-		channel:  channel,
+		channels: channels,
 	}
 
 	c.Client.Transport = request.NewTripper(log, transport.Insecure())
@@ -65,34 +76,36 @@ func NewConnection(uri, user, password string, channel int, cache time.Duration)
 }
 
 // channelExists checks the existence of the configured relay channel interface
-func (c *Connection) ChannelExists(channel int) error {
+func (c *Connection) RelayExists() error {
 	res, err := c.statusStsG.Get()
 	if err != nil {
 		return err
 	}
 
 	var ok bool
-	switch channel {
-	case 1:
-		ok = res.StatusSTS.Power != "" || res.StatusSTS.Power1 != ""
-	case 2:
-		ok = res.StatusSTS.Power2 != ""
-	case 3:
-		ok = res.StatusSTS.Power3 != ""
-	case 4:
-		ok = res.StatusSTS.Power4 != ""
-	case 5:
-		ok = res.StatusSTS.Power5 != ""
-	case 6:
-		ok = res.StatusSTS.Power6 != ""
-	case 7:
-		ok = res.StatusSTS.Power7 != ""
-	case 8:
-		ok = res.StatusSTS.Power8 != ""
-	}
+	for _, channel := range c.channels {
+		switch channel {
+		case 1:
+			ok = res.StatusSTS.Power != "" || res.StatusSTS.Power1 != ""
+		case 2:
+			ok = res.StatusSTS.Power2 != ""
+		case 3:
+			ok = res.StatusSTS.Power3 != ""
+		case 4:
+			ok = res.StatusSTS.Power4 != ""
+		case 5:
+			ok = res.StatusSTS.Power5 != ""
+		case 6:
+			ok = res.StatusSTS.Power6 != ""
+		case 7:
+			ok = res.StatusSTS.Power7 != ""
+		case 8:
+			ok = res.StatusSTS.Power8 != ""
+		}
 
-	if !ok {
-		return fmt.Errorf("invalid relay channel: %d", channel)
+		if !ok {
+			return fmt.Errorf("invalid relay channel: %d", channel)
+		}
 	}
 
 	return nil
@@ -100,53 +113,56 @@ func (c *Connection) ChannelExists(channel int) error {
 
 // Enable implements the api.Charger interface
 func (c *Connection) Enable(enable bool) error {
-	cmd := fmt.Sprintf("Power%d off", c.channel)
-	if enable {
-		cmd = fmt.Sprintf("Power%d on", c.channel)
-	}
+	for _, channel := range c.channels {
 
-	parameters := url.Values{
-		"user":     []string{c.user},
-		"password": []string{c.password},
-		"cmnd":     []string{cmd},
-	}
+		cmd := fmt.Sprintf("Power%d off", channel)
+		if enable {
+			cmd = fmt.Sprintf("Power%d on", channel)
+		}
 
-	var res PowerResponse
-	if err := c.GetJSON(fmt.Sprintf("%s/cm?%s", c.uri, parameters.Encode()), &res); err != nil {
-		return err
-	}
+		parameters := url.Values{
+			"user":     []string{c.user},
+			"password": []string{c.password},
+			"cmnd":     []string{cmd},
+		}
 
-	var on bool
-	switch c.channel {
-	case 2:
-		on = strings.ToUpper(res.Power2) == "ON"
-	case 3:
-		on = strings.ToUpper(res.Power3) == "ON"
-	case 4:
-		on = strings.ToUpper(res.Power4) == "ON"
-	case 5:
-		on = strings.ToUpper(res.Power5) == "ON"
-	case 6:
-		on = strings.ToUpper(res.Power6) == "ON"
-	case 7:
-		on = strings.ToUpper(res.Power7) == "ON"
-	case 8:
-		on = strings.ToUpper(res.Power8) == "ON"
-	default:
-		on = strings.ToUpper(res.Power) == "ON" || strings.ToUpper(res.Power1) == "ON"
+		var res PowerResponse
+		if err := c.GetJSON(fmt.Sprintf("%s/cm?%s", c.uri, parameters.Encode()), &res); err != nil {
+			return err
+		}
+
+		var enabled bool
+		switch channel {
+		case 2:
+			enabled = strings.ToUpper(res.Power2) == "ON"
+		case 3:
+			enabled = strings.ToUpper(res.Power3) == "ON"
+		case 4:
+			enabled = strings.ToUpper(res.Power4) == "ON"
+		case 5:
+			enabled = strings.ToUpper(res.Power5) == "ON"
+		case 6:
+			enabled = strings.ToUpper(res.Power6) == "ON"
+		case 7:
+			enabled = strings.ToUpper(res.Power7) == "ON"
+		case 8:
+			enabled = strings.ToUpper(res.Power8) == "ON"
+		default:
+			enabled = strings.ToUpper(res.Power) == "ON" || strings.ToUpper(res.Power1) == "ON"
+		}
+
+		switch {
+		case enable && !enabled:
+			return errors.New("switchOn failed")
+		case !enable && enabled:
+			return errors.New("switchOff failed")
+		}
 	}
 
 	c.statusSnsG.Reset()
 	c.statusStsG.Reset()
 
-	switch {
-	case enable && !on:
-		return errors.New("switchOn failed")
-	case !enable && on:
-		return errors.New("switchOff failed")
-	default:
-		return nil
-	}
+	return nil
 }
 
 // Enabled implements the api.Charger interface
@@ -156,33 +172,46 @@ func (c *Connection) Enabled() (bool, error) {
 		return false, err
 	}
 
-	switch c.channel {
-	case 2:
-		return strings.ToUpper(res.StatusSTS.Power2) == "ON", err
-	case 3:
-		return strings.ToUpper(res.StatusSTS.Power3) == "ON", err
-	case 4:
-		return strings.ToUpper(res.StatusSTS.Power4) == "ON", err
-	case 5:
-		return strings.ToUpper(res.StatusSTS.Power5) == "ON", err
-	case 6:
-		return strings.ToUpper(res.StatusSTS.Power6) == "ON", err
-	case 7:
-		return strings.ToUpper(res.StatusSTS.Power7) == "ON", err
-	case 8:
-		return strings.ToUpper(res.StatusSTS.Power8) == "ON", err
-	default:
-		return strings.ToUpper(res.StatusSTS.Power) == "ON" || strings.ToUpper(res.StatusSTS.Power1) == "ON", err
+	var enabled bool
+	for _, channel := range c.channels {
+		switch channel {
+		case 2:
+			enabled = strings.ToUpper(res.StatusSTS.Power2) == "ON"
+		case 3:
+			enabled = strings.ToUpper(res.StatusSTS.Power3) == "ON"
+		case 4:
+			enabled = strings.ToUpper(res.StatusSTS.Power4) == "ON"
+		case 5:
+			enabled = strings.ToUpper(res.StatusSTS.Power5) == "ON"
+		case 6:
+			enabled = strings.ToUpper(res.StatusSTS.Power6) == "ON"
+		case 7:
+			enabled = strings.ToUpper(res.StatusSTS.Power7) == "ON"
+		case 8:
+			enabled = strings.ToUpper(res.StatusSTS.Power8) == "ON"
+		default:
+			enabled = strings.ToUpper(res.StatusSTS.Power) == "ON" || strings.ToUpper(res.StatusSTS.Power1) == "ON"
+
+		}
 	}
+	return enabled, nil
 }
 
 // CurrentPower implements the api.Meter interface
 func (c *Connection) CurrentPower() (float64, error) {
-	res, err := c.statusSnsG.Get()
+	s, err := c.statusSnsG.Get()
 	if err != nil {
 		return 0, err
 	}
-	return res.StatusSNS.Energy.Power.Channel(c.channel)
+	var res float64
+	for _, channel := range c.channels {
+		power, err := s.StatusSNS.Energy.Power.Channel(channel)
+		if err != nil {
+			return 0, err
+		}
+		res += power
+	}
+	return res, nil
 }
 
 // TotalEnergy implements the api.MeterEnergy interface
@@ -191,6 +220,42 @@ func (c *Connection) TotalEnergy() (float64, error) {
 	return res.StatusSNS.Energy.Total, err
 }
 
+// Currents implements the api.PhaseCurrents interface
+func (c *Connection) Currents() (float64, float64, float64, error) {
+	s, err := c.statusSnsG.Get()
+	if err != nil {
+		return 0, 0, 0, err
+	}
+
+	var res [3]float64
+	for i, c := range c.channels {
+		res[i], err = s.StatusSNS.Energy.Current.Channel(c)
+		if err != nil {
+			return 0, 0, 0, err
+		}
+	}
+
+	return res[0], res[1], res[2], err
+}
+
+// Voltages implements the api.PhaseVoltages interface
+func (c *Connection) Voltages() (float64, float64, float64, error) {
+	s, err := c.statusSnsG.Get()
+	if err != nil {
+		return 0, 0, 0, err
+	}
+
+	var res [3]float64
+	for i, c := range c.channels {
+		res[i], err = s.StatusSNS.Energy.Voltage.Channel(c)
+		if err != nil {
+			return 0, 0, 0, err
+		}
+	}
+
+	return res[0], res[1], res[2], err
+}
+
 // SmlPower provides the sml sensor power
 func (c *Connection) SmlPower() (float64, error) {
 	res, err := c.statusSnsG.Get()
diff --git a/meter/tasmota_decorators.go b/meter/tasmota_decorators.go
new file mode 100644
index 00000000..466fb6d1
--- /dev/null
+++ b/meter/tasmota_decorators.go
@@ -0,0 +1,69 @@
+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 decorateTasmota(base *Tasmota, phaseVoltages func() (float64, float64, float64, error), phaseCurrents func() (float64, float64, float64, error)) api.Meter {
+	switch {
+	case phaseCurrents == nil && phaseVoltages == nil:
+		return base
+
+	case phaseCurrents == nil && phaseVoltages != nil:
+		return &struct {
+			*Tasmota
+			api.PhaseVoltages
+		}{
+			Tasmota: base,
+			PhaseVoltages: &decorateTasmotaPhaseVoltagesImpl{
+				phaseVoltages: phaseVoltages,
+			},
+		}
+
+	case phaseCurrents != nil && phaseVoltages == nil:
+		return &struct {
+			*Tasmota
+			api.PhaseCurrents
+		}{
+			Tasmota: base,
+			PhaseCurrents: &decorateTasmotaPhaseCurrentsImpl{
+				phaseCurrents: phaseCurrents,
+			},
+		}
+
+	case phaseCurrents != nil && phaseVoltages != nil:
+		return &struct {
+			*Tasmota
+			api.PhaseCurrents
+			api.PhaseVoltages
+		}{
+			Tasmota: base,
+			PhaseCurrents: &decorateTasmotaPhaseCurrentsImpl{
+				phaseCurrents: phaseCurrents,
+			},
+			PhaseVoltages: &decorateTasmotaPhaseVoltagesImpl{
+				phaseVoltages: phaseVoltages,
+			},
+		}
+	}
+
+	return nil
+}
+
+type decorateTasmotaPhaseCurrentsImpl struct {
+	phaseCurrents func() (float64, float64, float64, error)
+}
+
+func (impl *decorateTasmotaPhaseCurrentsImpl) Currents() (float64, float64, float64, error) {
+	return impl.phaseCurrents()
+}
+
+type decorateTasmotaPhaseVoltagesImpl struct {
+	phaseVoltages func() (float64, float64, float64, error)
+}
+
+func (impl *decorateTasmotaPhaseVoltagesImpl) Voltages() (float64, float64, float64, error) {
+	return impl.phaseVoltages()
+}
diff --git a/mock/mock_loadpoint.go b/mock/mock_loadpoint.go
deleted file mode 100644
index 7f488e1a..00000000
--- a/mock/mock_loadpoint.go
+++ /dev/null
@@ -1,46 +0,0 @@
-// Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/evcc-io/evcc/core (interfaces: Updater)
-
-// Package mock is a generated GoMock package.
-package mock
-
-import (
-	reflect "reflect"
-
-	gomock "github.com/golang/mock/gomock"
-)
-
-// MockUpdater is a mock of Updater interface.
-type MockUpdater struct {
-	ctrl     *gomock.Controller
-	recorder *MockUpdaterMockRecorder
-}
-
-// MockUpdaterMockRecorder is the mock recorder for MockUpdater.
-type MockUpdaterMockRecorder struct {
-	mock *MockUpdater
-}
-
-// NewMockUpdater creates a new mock instance.
-func NewMockUpdater(ctrl *gomock.Controller) *MockUpdater {
-	mock := &MockUpdater{ctrl: ctrl}
-	mock.recorder = &MockUpdaterMockRecorder{mock}
-	return mock
-}
-
-// EXPECT returns an object that allows the caller to indicate expected use.
-func (m *MockUpdater) EXPECT() *MockUpdaterMockRecorder {
-	return m.recorder
-}
-
-// Update mocks base method.
-func (m *MockUpdater) Update(arg0 float64, arg1, arg2 bool) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "Update", arg0, arg1, arg2)
-}
-
-// Update indicates an expected call of Update.
-func (mr *MockUpdaterMockRecorder) Update(arg0, arg1, arg2 interface{}) *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockUpdater)(nil).Update), arg0, arg1, arg2)
-}
diff --git a/server/http_handler.go b/server/http_handler.go
index 042412f0..814b0f4c 100644
--- a/server/http_handler.go
+++ b/server/http_handler.go
@@ -8,6 +8,7 @@ import (
 	"math"
 	"net/http"
 	"strconv"
+	"strings"
 	"text/template"
 	"time"
 
@@ -16,7 +17,9 @@ import (
 	"github.com/evcc-io/evcc/core/site"
 	"github.com/evcc-io/evcc/server/assets"
 	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/jq"
 	"github.com/gorilla/mux"
+	"github.com/itchyny/gojq"
 )
 
 var ignoreState = []string{"releaseNotes"} // excessive size
@@ -164,7 +167,34 @@ func stateHandler(cache *util.Cache) http.HandlerFunc {
 		for _, k := range ignoreState {
 			delete(res, k)
 		}
+
 		encodeFloats(res)
+
+		if q := r.URL.Query().Get("jq"); q != "" {
+			q = strings.TrimPrefix(q, ".result")
+
+			query, err := gojq.Parse(q)
+			if err != nil {
+				jsonError(w, http.StatusBadRequest, err)
+				return
+			}
+
+			b, err := json.Marshal(res)
+			if err != nil {
+				jsonError(w, http.StatusBadRequest, err)
+				return
+			}
+
+			res, err := jq.Query(query, b)
+			if err != nil {
+				jsonError(w, http.StatusBadRequest, err)
+				return
+			}
+
+			jsonWrite(w, res)
+			return
+		}
+
 		jsonResult(w, res)
 	}
 }
diff --git a/server/modbus/handler.go b/server/modbus/handler.go
index 66b3368b..035ef9fa 100644
--- a/server/modbus/handler.go
+++ b/server/modbus/handler.go
@@ -14,8 +14,7 @@ import (
 type handler struct {
 	log      *util.Logger
 	readOnly bool
-	mbserver.RequestHandler
-	conn *modbus.Connection
+	conn     *modbus.Connection
 }
 
 func bytesAsUint16(b []byte) []uint16 {
@@ -73,7 +72,7 @@ func coilsToBytes(b []bool) []byte {
 	return res
 }
 
-func (h *handler) coilsToResult(op string, qty uint16, b []byte, err error) ([]bool, error) {
+func (h *handler) bytesToBoolResult(op string, qty uint16, b []byte, err error) ([]bool, error) {
 	h.logResult(op, b, err)
 
 	var modbusError *gridx.Error
@@ -97,6 +96,12 @@ LOOP:
 	return res, err
 }
 
+func (h *handler) HandleDiscreteInputs(req *mbserver.DiscreteInputsRequest) ([]bool, error) {
+	h.log.TRACE.Printf("read discrete: id %d addr %d qty %d", req.UnitId, req.Addr, req.Quantity)
+	b, err := h.conn.ReadDiscreteInputsWithSlave(req.UnitId, req.Addr, req.Quantity)
+	return h.bytesToBoolResult("read discrete", req.Quantity, b, err)
+}
+
 func (h *handler) HandleCoils(req *mbserver.CoilsRequest) ([]bool, error) {
 	if req.IsWrite {
 		if h.readOnly {
@@ -104,34 +109,34 @@ func (h *handler) HandleCoils(req *mbserver.CoilsRequest) ([]bool, error) {
 		}
 
 		if req.WriteFuncCode == gridx.FuncCodeWriteSingleCoil {
-			h.log.TRACE.Printf("write single coil: id %d addr %d val %t", req.UnitId, req.Addr, req.Args[0])
+			h.log.TRACE.Printf("write coil: id %d addr %d val %t", req.UnitId, req.Addr, req.Args[0])
 			var u uint16
 			if req.Args[0] {
 				u = 0xFF00
 			}
 
 			b, err := h.conn.WriteSingleCoilWithSlave(req.UnitId, req.Addr, u)
-			return h.coilsToResult("write coil", req.Quantity, b, err)
+			return h.bytesToBoolResult("write coil", req.Quantity, b, err)
 		}
 
 		h.log.TRACE.Printf("write coils: id %d addr %d qty %d val %v", req.UnitId, req.Addr, req.Quantity, req.Args)
 		args := coilsToBytes(req.Args)
 		b, err := h.conn.WriteMultipleCoilsWithSlave(req.UnitId, req.Addr, req.Quantity, args)
-		return h.coilsToResult("write coils", req.Quantity, b, err)
+		return h.bytesToBoolResult("write coils", req.Quantity, b, err)
 	}
 
-	h.log.TRACE.Printf("read coil: id %d addr %d qty %d", req.UnitId, req.Addr, req.Quantity)
+	h.log.TRACE.Printf("read coils: id %d addr %d qty %d", req.UnitId, req.Addr, req.Quantity)
 	b, err := h.conn.ReadCoilsWithSlave(req.UnitId, req.Addr, req.Quantity)
-	return h.coilsToResult("read coil", req.Quantity, b, err)
+	return h.bytesToBoolResult("read coils", req.Quantity, b, err)
 }
 
-func (h *handler) HandleInputRegisters(req *mbserver.InputRegistersRequest) (res []uint16, err error) {
+func (h *handler) HandleInputRegisters(req *mbserver.InputRegistersRequest) ([]uint16, error) {
 	h.log.TRACE.Printf("read input: id %d addr %d qty %d", req.UnitId, req.Addr, req.Quantity)
 	b, err := h.conn.ReadInputRegistersWithSlave(req.UnitId, req.Addr, req.Quantity)
 	return h.exceptionToUint16AndError("read input", b, err)
 }
 
-func (h *handler) HandleHoldingRegisters(req *mbserver.HoldingRegistersRequest) (res []uint16, err error) {
+func (h *handler) HandleHoldingRegisters(req *mbserver.HoldingRegistersRequest) ([]uint16, error) {
 	if req.IsWrite {
 		if h.readOnly {
 			return nil, mbserver.ErrIllegalFunction
@@ -143,12 +148,12 @@ func (h *handler) HandleHoldingRegisters(req *mbserver.HoldingRegistersRequest)
 			return h.exceptionToUint16AndError("write holding", b, err)
 		}
 
-		h.log.TRACE.Printf("write holding: id %d addr %d qty %d val %0x", req.UnitId, req.Addr, req.Quantity, asBytes(req.Args))
+		h.log.TRACE.Printf("write holdings: id %d addr %d qty %d val %0x", req.UnitId, req.Addr, req.Quantity, asBytes(req.Args))
 		b, err := h.conn.WriteMultipleRegistersWithSlave(req.UnitId, req.Addr, req.Quantity, asBytes(req.Args))
 		return h.exceptionToUint16AndError("write multiple holding", b, err)
 	}
 
-	h.log.TRACE.Printf("read holding: id %d addr %d qty %d", req.UnitId, req.Addr, req.Quantity)
+	h.log.TRACE.Printf("read holdings: id %d addr %d qty %d", req.UnitId, req.Addr, req.Quantity)
 	b, err := h.conn.ReadHoldingRegistersWithSlave(req.UnitId, req.Addr, req.Quantity)
 	return h.exceptionToUint16AndError("read holding", b, err)
 }
diff --git a/server/modbus/proxy.go b/server/modbus/proxy.go
index 2271175d..dc964bad 100644
--- a/server/modbus/proxy.go
+++ b/server/modbus/proxy.go
@@ -22,10 +22,9 @@ func StartProxy(port int, config modbus.Settings, readOnly bool) error {
 	}
 
 	h := &handler{
-		log:            util.NewLogger(fmt.Sprintf("proxy-%d", port)),
-		readOnly:       readOnly,
-		RequestHandler: new(mbserver.DummyHandler), // supplies HandleDiscreteInputs
-		conn:           conn,
+		log:      util.NewLogger(fmt.Sprintf("proxy-%d", port)),
+		readOnly: readOnly,
+		conn:     conn,
 	}
 
 	l, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
diff --git a/templates/definition/charger/amperfied.yaml b/templates/definition/charger/amperfied.yaml
index ab1a7c1e..61b1121b 100644
--- a/templates/definition/charger/amperfied.yaml
+++ b/templates/definition/charger/amperfied.yaml
@@ -14,5 +14,5 @@ params:
     choice: ["tcpip"]
     id: 255
 render: |
-  type: heidelberg
+  type: amperfied
   {{- include "modbus" . }}
diff --git a/templates/definition/charger/nrgkick-bluetooth.yaml b/templates/definition/charger/nrgkick-bluetooth.yaml
index 70bec8ed..e9b1e576 100644
--- a/templates/definition/charger/nrgkick-bluetooth.yaml
+++ b/templates/definition/charger/nrgkick-bluetooth.yaml
@@ -3,6 +3,10 @@ products:
   - brand: NRGKick
     description:
       generic: Bluetooth
+requirements:
+  description:
+    de: NRGkick Ladeeinheit via Bluetooth (älter als 2022/2023)
+    en: NRGkick charging unit via Bluetooth (older than 2022/2023)
 params:
   - name: mac
     required: true
diff --git a/templates/definition/charger/nrgkick-connect.yaml b/templates/definition/charger/nrgkick-connect.yaml
index 4565d6ac..c7190bf4 100644
--- a/templates/definition/charger/nrgkick-connect.yaml
+++ b/templates/definition/charger/nrgkick-connect.yaml
@@ -3,6 +3,10 @@ products:
   - brand: NRGKick
     description:
       generic: Connect
+requirements:
+  description:
+    de: NRGkick Ladeeinheit via HTTP (älter als 2022/2023)
+    en: NRGkick charging unit via HTTP (older than 2022/2023)
 params:
   - name: host
   - name: mac
diff --git a/templates/definition/charger/tasmota.yaml b/templates/definition/charger/tasmota.yaml
index 2ef4c522..3400451e 100644
--- a/templates/definition/charger/tasmota.yaml
+++ b/templates/definition/charger/tasmota.yaml
@@ -1,6 +1,6 @@
 template: tasmota
 products:
-  - brand: Tasmota
+  - brand: Tasmota (1 Phase Charger)
 group: switchsockets
 params:
   - name: host
@@ -16,11 +16,11 @@ params:
     default: 1
     required: true
     description:
-      de: Nummer des Schaltkanals
-      en: Device relay channel number
+      de: Schaltkanal Nummer
+      en: Relaychannel number
     help:
-      de: Nummer des Schaltkanals (1-8), bei Geräten mit mehr als einem Schalter
-      en: Device relay channel number (allowed range 1-8)
+      de: Schaltkanal (1-8)
+      en: Relaychannel number (1-8)
   - preset: switchsocket
 render: |
   type: tasmota
@@ -31,5 +31,5 @@ render: |
   {{- if .password }}
   password: {{ .password }}
   {{- end }}
-  channel: {{ .channel }}  # relay channel (allowed range: 1-8)
+  channel: [{{ .channel }}]  # list of relay channels [1,2,....,8]
   {{ include "switchsocket" . }}
diff --git a/templates/definition/meter/enphase.yaml b/templates/definition/meter/enphase.yaml
index 2b94cea1..3e8bdf9b 100644
--- a/templates/definition/meter/enphase.yaml
+++ b/templates/definition/meter/enphase.yaml
@@ -23,7 +23,6 @@ render: |
   power:
     source: http
     uri: http://{{ .host }}/production.json
-    method: GET
     {{- if .token }}
     auth:
       type: bearer
@@ -36,7 +35,6 @@ render: |
   power:
     source: http
     uri: http://{{ .host }}/production.json
-    method: GET
     {{- if .token }}
     auth:
       type: bearer
@@ -47,7 +45,6 @@ render: |
   energy:
     source: http
     uri: http://{{ .host }}/production.json
-    method: GET
     {{- if .token }}
     auth:
       type: bearer
@@ -61,7 +58,6 @@ render: |
   power:
     source: http
     uri: http://{{ .host }}/production.json
-    method: GET
     {{- if .token }}
     auth:
       type: bearer
@@ -72,7 +68,6 @@ render: |
   soc:
     source: http
     uri: http://{{ .host }}/ivp/ensemble/inventory
-    method: GET
     {{- if .token }}
     auth:
       type: bearer
diff --git a/templates/definition/meter/loxone.yaml b/templates/definition/meter/loxone.yaml
new file mode 100644
index 00000000..ff9c209e
--- /dev/null
+++ b/templates/definition/meter/loxone.yaml
@@ -0,0 +1,56 @@
+template: loxone
+products:
+  - brand: Loxone
+    description:
+      generic: Miniserver
+params:
+  - name: usage
+    choice: ["grid", "pv", "battery", "charge"]
+  - name: host
+  - name: user
+  - name: password
+  - name: meterblock
+    required: true
+    description:
+      de: Zählerbaustein
+      en: Meter block
+    help:
+      de: Bezeichnung aus Loxone Config
+      en: Name from Loxone Config
+  - name: socblock
+    required: false
+    description:
+      de: Bausteinbezeichnung für Ladezustand
+      en: Function block name for state of charge
+    help:
+      de: Bezeichnung aus Loxone Config, nur für Batterie
+      en: Name from Loxone Config, only for battery
+render: |
+  type: custom
+  power:
+    source: http
+    uri: http://{{ .host }}/jdev/sps/io/{{ .meterblock }}
+    auth:
+      type: basic
+      user: {{ .user }}
+      password: {{ .password }}
+    jq: .LL.value
+    scale: 1000
+  energy:
+    source: http
+    uri: http://{{ .host }}/jdev/sps/io/{{ .meterblock }}/all
+    auth:
+      type: basic
+      user: {{ .user }}
+      password: {{ .password }}
+    jq: first(.LL[] | select(type == "object" and (.name == "Mr" or .name == "Mrc")) .value)
+  {{- if and (eq .usage "battery") .socblock }}
+  soc:
+    source: http
+    uri: http://{{ .host }}/jdev/sps/io/{{ .socblock }}
+    auth:
+      type: basic
+      user: {{ .user }}
+      password: {{ .password }}
+    jq: .LL.value
+  {{- end }}
diff --git a/templates/definition/meter/saj-h2.yaml b/templates/definition/meter/saj-h2.yaml
new file mode 100644
index 00000000..60b50cd8
--- /dev/null
+++ b/templates/definition/meter/saj-h2.yaml
@@ -0,0 +1,100 @@
+template: saj-h2
+products:
+  - brand: SAJ
+    description:
+      generic: H2 Series Hybrid Solar Inverter
+params:
+  - name: usage
+    choice: ["grid", "pv", "battery"]
+  - name: modbus
+    choice: ["rs485"]
+    baudrate: 115200
+    comset: 8N1
+  - name: capacity
+    advanced: true
+render: |
+  type: custom
+  {{- if eq .usage "grid" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 0x40A7 # TotalGridPowerWatt
+      type: holding
+      decode: int16
+  currents:
+  - source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 0x4032 # RGridCurr
+      type: holding
+      decode: int16
+    scale: 0.01
+  - source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 0x4039 # SGridCurr
+      type: holding
+      decode: int16
+    scale: 0.01
+  - source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 0x4040 # TGridCurr
+      type: holding
+      decode: int16
+    scale: 0.01
+  energy:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 0x40FD # Total_FeedInEnergy
+      type: holding
+      decode: uint32
+    scale: 0.01
+  {{- end }}
+  {{- if eq .usage "pv" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 0x40A5 # TotalPVPower
+      type: holding
+      decode: int16
+  energy:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 0x40C5 # Total_PVEnergy
+      type: holding
+      decode: uint32
+    scale: 0.01
+  {{- end }}
+  {{- if eq .usage "battery" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 0x40A6 # TotalBatteryPower
+      type: holding
+      decode: int16
+  energy:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 0x40D5 # Total_BatDisEnergy
+      type: holding
+      decode: uint32
+    scale: 0.01
+  soc:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 0xA00C # Bat1SOC
+      type: holding
+      decode: uint16
+    scale: 0.01
+  {{- if .capacity }}
+  capacity: {{ .capacity }} # kWh
+  {{- end }}
+  {{- end }}
diff --git a/templates/definition/meter/slimmelezer.yaml b/templates/definition/meter/slimmelezer.yaml
index f473addf..608e79f4 100644
--- a/templates/definition/meter/slimmelezer.yaml
+++ b/templates/definition/meter/slimmelezer.yaml
@@ -17,14 +17,12 @@ render: |
     add:
     - source: http
       uri: http://{{ .host }}/sensor/power_delivered
-      method: GET
       headers:
       - content-type: application/json
       jq: .value
       scale: 1000
     - source: http
       uri: http://{{ .host }}/sensor/power_returned
-      method: GET
       headers:
       - content-type: application/json
       jq: .value
@@ -34,32 +32,27 @@ render: |
     add:
     - source: http
       uri: http://{{ .host }}/sensor/energy_delivered_tariff1
-      method: GET
       headers:
       - content-type: application/json
       jq: .value
     - source: http
       uri: http://{{ .host }}/sensor/energy_delivered_tariff2
-      method: GET
       headers:
       - content-type: application/json
       jq: .value
   currents:
   - source: http
     uri: http://{{ .host }}/sensor/current_l1
-    method: GET
     headers:
     - content-type: application/json
     jq: .value
   - source: http
     uri: http://{{ .host }}/sensor/current_l2
-    method: GET
     headers:
     - content-type: application/json
     jq: .value
   - source: http
     uri: http://{{ .host }}/sensor/current_l3
-    method: GET
     headers:
     - content-type: application/json
     jq: .value
@@ -68,14 +61,12 @@ render: |
     add:
     - source: http
       uri: http://{{ .host }}/sensor/power_delivered_l1
-      method: GET
       headers:
       - content-type: application/json
       jq: .value
       scale: 1000
     - source: http
       uri: http://{{ .host }}/sensor/power_returned_l1
-      method: GET
       headers:
       - content-type: application/json
       jq: .value
@@ -84,14 +75,12 @@ render: |
     add:
     - source: http
       uri: http://{{ .host }}/sensor/power_delivered_l2
-      method: GET
       headers:
       - content-type: application/json
       jq: .value
       scale: 1000
     - source: http
       uri: http://{{ .host }}/sensor/power_returned_l2
-      method: GET
       headers:
       - content-type: application/json
       jq: .value
@@ -100,14 +89,12 @@ render: |
     add:
     - source: http
       uri: http://{{ .host }}/sensor/power_delivered_l3
-      method: GET
       headers:
       - content-type: application/json
       jq: .value
       scale: 1000
     - source: http
       uri: http://{{ .host }}/sensor/power_returned_l3
-      method: GET
       headers:
       - content-type: application/json
       jq: .value
diff --git a/templates/definition/meter/storaxe.yaml b/templates/definition/meter/storaxe.yaml
index a86703d7..949f0117 100644
--- a/templates/definition/meter/storaxe.yaml
+++ b/templates/definition/meter/storaxe.yaml
@@ -16,7 +16,7 @@ render: |
     {{- include "modbus" . | indent 2 }}
     register:
       address: 2 # RealPower
-      type: holding
+      type: input
       decode: int16
     scale: 100
   energy:
@@ -24,27 +24,56 @@ render: |
     {{- include "modbus" . | indent 2 }}
     register:
       address: 115 # EnergyExportedAC
-      type: holding
+      type: input
       decode: uint32
+  voltages:
+    - source: modbus
+      {{- include "modbus" . | indent 4 }}
+      register:
+        address: 7 # ACVoltageL1
+        type: input
+        decode: int16
+      scale: 0.1
+    - source: modbus
+      {{- include "modbus" . | indent 4 }}
+      register:
+        address: 8 # ACVoltageL2
+        type: input
+        decode: int16
+      scale: 0.1
+    - source: modbus
+      {{- include "modbus" . | indent 4 }}
+      register:
+        address: 9 # ACVoltageL3
+        type: input
+        decode: int16
+      scale: 0.1  
   currents:
     - source: modbus
       {{- include "modbus" . | indent 4 }}
       register:
         address: 10 # ACCurrentL1
-        type: holding
+        type: input
         decode: int16
       scale: 0.1
     - source: modbus
       {{- include "modbus" . | indent 4 }}
       register:
         address: 11 # ACCurrentL2
-        type: holding
+        type: input
         decode: int16
       scale: 0.1
     - source: modbus
       {{- include "modbus" . | indent 4 }}
       register:
         address: 12 # ACCurrentL3
-        type: holding
+        type: input
         decode: int16
       scale: 0.1
+  soc:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 126 # SXSSOC
+      type: input
+      decode: int16
diff --git a/templates/definition/meter/tasmota-3p.yaml b/templates/definition/meter/tasmota-3p.yaml
new file mode 100644
index 00000000..60a9786c
--- /dev/null
+++ b/templates/definition/meter/tasmota-3p.yaml
@@ -0,0 +1,24 @@
+template: tasmota-3p
+products:
+  - description:
+      generic: Tasmota (3 Phase Meter - meter channels 1,2+3 must be used)
+group: switchsockets
+params:
+  - name: usage
+    choice: ["grid", "pv", "battery", "charge"]
+  - name: host
+  - name: user
+    required: false
+    help:
+      de: Standard-User ist admin
+      en: admin is default
+  - name: password
+    required: false
+    mask: true
+render: |
+  type: tasmota
+  uri: http://{{ .host }}
+  usage: {{ .usage }}
+  user: {{ .user }}
+  password: {{ .password }}
+  channel: [1,2,3]  # list of meter channels [1,2,....,8]
diff --git a/templates/definition/meter/tasmota.yaml b/templates/definition/meter/tasmota.yaml
index 65d888b1..20274ad7 100644
--- a/templates/definition/meter/tasmota.yaml
+++ b/templates/definition/meter/tasmota.yaml
@@ -1,11 +1,11 @@
 template: tasmota
 products:
   - description:
-      generic: Tasmota
+      generic: Tasmota (1 Phase Meter)
 group: switchsockets
 params:
   - name: usage
-    choice: ["grid", "pv", "battery"]
+    choice: ["grid", "pv", "battery", "charge"]
   - name: host
   - name: user
     required: false
@@ -15,9 +15,19 @@ params:
   - name: password
     required: false
     mask: true
+  - name: channel
+    default: 1
+    required: true
+    description:
+      de: Kanal Nummer
+      en: Channel number
+    help:
+      de: Meterkanal (1-8)
+      en: Meterchannel number (1-8)
 render: |
   type: tasmota
   uri: http://{{ .host }}
   usage: {{ .usage }}
   user: {{ .user }}
   password: {{ .password }}
+  channel: [{{ .channel }}]  # list of meter channels [1,2,....,8]
diff --git a/templates/definition/meter/youless.yaml b/templates/definition/meter/youless.yaml
index de8cc30a..65f820bc 100644
--- a/templates/definition/meter/youless.yaml
+++ b/templates/definition/meter/youless.yaml
@@ -19,23 +19,19 @@ render: |
   power:
     source: http
     uri: http://{{ .host }}/e
-    method: GET
     jq: .[0]|.pwr
   energy:
     source: http
     uri: http://{{ .host }}/e
-    method: GET
     jq: .[0]|.p1+.p2
   {{- end }}
   {{- if eq .usage "pv" }}
   power:
     source: http
     uri: http://{{ .host }}/e
-    method: GET
     jq: .[0].ps0
   energy:
     source: http
     uri: http://{{ .host }}/e
-    method: GET
     jq: .[0].cs0
   {{- end }}
diff --git a/templates/definition/vehicle/evnotify.yaml b/templates/definition/vehicle/evnotify.yaml
index 331c38f1..1583078b 100644
--- a/templates/definition/vehicle/evnotify.yaml
+++ b/templates/definition/vehicle/evnotify.yaml
@@ -29,6 +29,5 @@ render: |
   soc:
     source: http
     uri: https://app.evnotify.de/soc?akey={{ urlEncode .akey }}&token={{ urlEncode .token }} # evNotify Server + AKEY
-    method: GET
     jq: .soc_display
   {{ include "vehicle-identify" . }}
diff --git a/templates/evcc.io/brands.json b/templates/evcc.io/brands.json
index 1f8bfbb2..71e74228 100644
--- a/templates/evcc.io/brands.json
+++ b/templates/evcc.io/brands.json
@@ -78,7 +78,7 @@
   "HomeWizard",
   "myStrom",
   "Shelly",
-  "Tasmota",
+  "Tasmota (1 Phase Charger)",
   "TP-Link"
  ],
  "Meters": [
@@ -109,6 +109,7 @@
   "Janitza",
   "Kostal",
   "LG",
+  "Loxone",
   "M-TEC",
   "my-PV",
   "myStrom",
@@ -119,6 +120,7 @@
   "Qcells",
   "RCT",
   "Saia-Burgess Controls",
+  "SAJ",
   "SAX",
   "Schneider Electric",
   "SENEC",
@@ -168,12 +170,14 @@
   "IGEN Tech",
   "Kostal",
   "LG",
+  "Loxone",
   "M-TEC",
   "myStrom",
   "OpenEMS",
   "Powerfox",
   "Qcells",
   "RCT",
+  "SAJ",
   "SAX",
   "SENEC",
   "Senergy",
diff --git a/util/jq/jq.go b/util/jq/jq.go
index c58241a3..c8a1fd61 100644
--- a/util/jq/jq.go
+++ b/util/jq/jq.go
@@ -8,7 +8,7 @@ import (
 	"github.com/itchyny/gojq"
 )
 
-// Query executes a compiled jq query against given input. It expects a single result only.
+// Query executes a compiled jq query against given json. It expects a single result only.
 func Query(query *gojq.Query, input []byte) (interface{}, error) {
 	var j interface{}
 	if err := json.Unmarshal(input, &j); err != nil {
diff --git a/util/state.go b/util/state.go
index 1cef42f2..0b0f60e8 100644
--- a/util/state.go
+++ b/util/state.go
@@ -85,7 +85,7 @@ func (c *State) Encrypt(key []byte) string {
 }
 
 func (c *State) Validate() error {
-	if c.Time.Add(stateValidity).After(time.Now()) {
+	if time.Since(c.Time) > stateValidity {
 		return nil
 	}
 


Test patch:
diff --git a/core/coordinator/coordinator_test.go b/core/coordinator/coordinator_test.go
index 376bc9eb..5b3dd9d1 100644
--- a/core/coordinator/coordinator_test.go
+++ b/core/coordinator/coordinator_test.go
@@ -5,7 +5,6 @@ import (
 
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/core/loadpoint"
-	"github.com/evcc-io/evcc/mock"
 	"github.com/evcc-io/evcc/util"
 	"github.com/golang/mock/gomock"
 )
@@ -14,12 +13,12 @@ func TestVehicleDetectByStatus(t *testing.T) {
 	ctrl := gomock.NewController(t)
 
 	type vehicle struct {
-		*mock.MockVehicle
-		*mock.MockChargeState
+		*api.MockVehicle
+		*api.MockChargeState
 	}
 
-	v1 := &vehicle{mock.NewMockVehicle(ctrl), mock.NewMockChargeState(ctrl)}
-	v2 := &vehicle{mock.NewMockVehicle(ctrl), mock.NewMockChargeState(ctrl)}
+	v1 := &vehicle{api.NewMockVehicle(ctrl), api.NewMockChargeState(ctrl)}
+	v2 := &vehicle{api.NewMockVehicle(ctrl), api.NewMockChargeState(ctrl)}
 
 	type testcase struct {
 		string
diff --git a/core/loadpoint_phases_test.go b/core/loadpoint_phases_test.go
index efd740ce..65dc6173 100644
--- a/core/loadpoint_phases_test.go
+++ b/core/loadpoint_phases_test.go
@@ -8,7 +8,6 @@ import (
 	evbus "github.com/asaskevich/EventBus"
 	"github.com/benbjohnson/clock"
 	"github.com/evcc-io/evcc/api"
-	"github.com/evcc-io/evcc/mock"
 	"github.com/evcc-io/evcc/util"
 	"github.com/golang/mock/gomock"
 )
@@ -76,15 +75,15 @@ func TestMaxActivePhases(t *testing.T) {
 
 			t.Log(dflt, tc)
 
-			plainCharger := mock.NewMockCharger(ctrl)
+			plainCharger := api.NewMockCharger(ctrl)
 
 			// 1p3p
-			var phaseCharger *mock.MockPhaseSwitcher
+			var phaseCharger *api.MockPhaseSwitcher
 			if tc.capable == 0 {
-				phaseCharger = mock.NewMockPhaseSwitcher(ctrl)
+				phaseCharger = api.NewMockPhaseSwitcher(ctrl)
 			}
 
-			vehicle := mock.NewMockVehicle(ctrl)
+			vehicle := api.NewMockVehicle(ctrl)
 			vehicle.EXPECT().Phases().Return(tc.vehicle).MinTimes(1)
 
 			lp := &Loadpoint{
@@ -96,14 +95,14 @@ func TestMaxActivePhases(t *testing.T) {
 
 			if phaseCharger != nil {
 				lp.charger = struct {
-					*mock.MockCharger
-					*mock.MockPhaseSwitcher
+					*api.MockCharger
+					*api.MockPhaseSwitcher
 				}{
 					plainCharger, phaseCharger,
 				}
 			} else {
 				lp.charger = struct {
-					*mock.MockCharger
+					*api.MockCharger
 				}{
 					plainCharger,
 				}
@@ -162,17 +161,17 @@ func TestPvScalePhases(t *testing.T) {
 	for _, tc := range phaseTests {
 		t.Log(tc)
 
-		plainCharger := mock.NewMockCharger(ctrl)
+		plainCharger := api.NewMockCharger(ctrl)
 		plainCharger.EXPECT().Enabled().Return(true, nil)
 		plainCharger.EXPECT().MaxCurrent(int64(minA)).Return(nil) // MaxCurrentEx not implemented
 
 		// 1p3p
-		var phaseCharger *mock.MockPhaseSwitcher
+		var phaseCharger *api.MockPhaseSwitcher
 		if tc.capable == 0 {
-			phaseCharger = mock.NewMockPhaseSwitcher(ctrl)
+			phaseCharger = api.NewMockPhaseSwitcher(ctrl)
 		}
 
-		vehicle := mock.NewMockVehicle(ctrl)
+		vehicle := api.NewMockVehicle(ctrl)
 		vehicle.EXPECT().Phases().Return(tc.vehicle).MinTimes(1)
 
 		lp := &Loadpoint{
@@ -195,14 +194,14 @@ func TestPvScalePhases(t *testing.T) {
 
 		if phaseCharger != nil {
 			lp.charger = struct {
-				*mock.MockCharger
-				*mock.MockPhaseSwitcher
+				*api.MockCharger
+				*api.MockPhaseSwitcher
 			}{
 				plainCharger, phaseCharger,
 			}
 		} else {
 			lp.charger = struct {
-				*mock.MockCharger
+				*api.MockCharger
 			}{
 				plainCharger,
 			}
@@ -259,11 +258,11 @@ func TestPvScalePhases(t *testing.T) {
 func TestPvScalePhasesTimer(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	charger := &struct {
-		*mock.MockCharger
-		*mock.MockPhaseSwitcher
+		*api.MockCharger
+		*api.MockPhaseSwitcher
 	}{
-		mock.NewMockCharger(ctrl),
-		mock.NewMockPhaseSwitcher(ctrl),
+		api.NewMockCharger(ctrl),
+		api.NewMockPhaseSwitcher(ctrl),
 	}
 
 	dt := time.Minute
@@ -416,15 +415,15 @@ func TestScalePhasesIfAvailable(t *testing.T) {
 	for _, tc := range tc {
 		t.Log(tc)
 
-		plainCharger := mock.NewMockCharger(ctrl)
-		phaseCharger := mock.NewMockPhaseSwitcher(ctrl)
+		plainCharger := api.NewMockCharger(ctrl)
+		phaseCharger := api.NewMockPhaseSwitcher(ctrl)
 
 		lp := &Loadpoint{
 			log:   util.NewLogger("foo"),
 			clock: clock.NewMock(),
 			charger: struct {
-				*mock.MockCharger
-				*mock.MockPhaseSwitcher
+				*api.MockCharger
+				*api.MockPhaseSwitcher
 			}{
 				plainCharger,
 				phaseCharger,
diff --git a/core/loadpoint_session_test.go b/core/loadpoint_session_test.go
index fdc9a1e7..a5e7f9b3 100644
--- a/core/loadpoint_session_test.go
+++ b/core/loadpoint_session_test.go
@@ -7,7 +7,6 @@ import (
 	"github.com/benbjohnson/clock"
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/core/session"
-	"github.com/evcc-io/evcc/mock"
 	serverdb "github.com/evcc-io/evcc/server/db"
 	"github.com/evcc-io/evcc/util"
 	"github.com/golang/mock/gomock"
@@ -27,8 +26,8 @@ func TestSession(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	mm := mock.NewMockMeter(ctrl)
-	me := mock.NewMockMeterEnergy(ctrl)
+	mm := api.NewMockMeter(ctrl)
+	me := api.NewMockMeterEnergy(ctrl)
 
 	type EnergyDecorator struct {
 		api.Meter
@@ -114,11 +113,11 @@ func TestCloseSessionsOnStartup(t *testing.T) {
 
 	clock := clock.NewMock()
 
-	//test data, creates 6 sessions for each loadpoint, 3rd and 6th are "unfinished"
+	// test data, creates 6 sessions for each loadpoint, 3rd and 6th are "unfinished"
 	var sessions1 []*session.Session = createMockSessions(db1, clock)
 	var sessions2 []*session.Session = createMockSessions(db2, clock)
 
-	//write interleaved for two loadpoints
+	// write interleaved for two loadpoints
 	for index, session := range sessions1 {
 		db1.Persist(session)
 		db2.Persist(sessions2[index])
@@ -127,24 +126,24 @@ func TestCloseSessionsOnStartup(t *testing.T) {
 	err = db1.ClosePendingSessionsInHistory(1000)
 	assert.NoError(t, err)
 
-	//check fixed sessions for db1
+	// check fixed sessions for db1
 	var db1Sessions session.Sessions
 	err = serverdb.Instance.Where("Loadpoint = ?", "foo").Order("ID").Find(&db1Sessions).Error
 	assert.NoError(t, err)
 	assert.Len(t, db1Sessions, 6)
 
-	//check fixed history
+	// check fixed history
 	for _, s := range db1Sessions[:5] {
 		assert.NotEmpty(t, s.MeterStop)
 		assert.Equal(t, float64(10), s.ChargedEnergy)
 		t.Logf("session: %+v", s)
 	}
 
-	//check fixed most recent record
+	// check fixed most recent record
 	assert.NotEmpty(t, db1Sessions[5].MeterStop)
 	assert.Equal(t, float64(940), db1Sessions[5].ChargedEnergy)
 
-	//ensure no side effects on loadpoint 2 data, i.e. data left unfixed
+	// ensure no side effects on loadpoint 2 data, i.e. data left unfixed
 	var db2Sessions session.Sessions
 	err = serverdb.Instance.Where("Loadpoint = ?", "bar").Order("ID").Find(&db2Sessions).Error
 	assert.NoError(t, err)
@@ -169,7 +168,7 @@ func createMockSessions(db *session.DB, clock *clock.Mock) []*session.Session {
 		session := db.New(meter1Start)
 		session.Created = clock.Now().Add(1 * time.Minute)
 
-		//create every third session as incomplete
+		// create every third session as incomplete
 		if i%3 == 0 {
 			sessions = append(sessions, session)
 			continue
diff --git a/core/loadpoint_sync_test.go b/core/loadpoint_sync_test.go
index 0c10d4ef..3d5df219 100644
--- a/core/loadpoint_sync_test.go
+++ b/core/loadpoint_sync_test.go
@@ -5,7 +5,6 @@ import (
 
 	"github.com/benbjohnson/clock"
 	"github.com/evcc-io/evcc/api"
-	"github.com/evcc-io/evcc/mock"
 	"github.com/evcc-io/evcc/util"
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
@@ -28,7 +27,7 @@ func TestSyncCharger(t *testing.T) {
 	for _, tc := range tc {
 		t.Logf("%+v", tc)
 
-		charger := mock.NewMockCharger(ctrl)
+		charger := api.NewMockCharger(ctrl)
 		charger.EXPECT().Enabled().Return(tc.actual, nil).AnyTimes()
 
 		if tc.status == api.StatusC {
diff --git a/core/loadpoint_test.go b/core/loadpoint_test.go
index 5e8d23d5..a3b9baf4 100644
--- a/core/loadpoint_test.go
+++ b/core/loadpoint_test.go
@@ -8,7 +8,6 @@ import (
 	"github.com/benbjohnson/clock"
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/core/soc"
-	"github.com/evcc-io/evcc/mock"
 	"github.com/evcc-io/evcc/push"
 	"github.com/evcc-io/evcc/util"
 	"github.com/golang/mock/gomock"
@@ -71,7 +70,7 @@ func attachChannels(lp *Loadpoint, uiChan chan util.Param, pushChan chan push.Ev
 func attachListeners(t *testing.T, lp *Loadpoint) {
 	Voltage = 230 // V
 
-	if charger, ok := lp.charger.(*mock.MockCharger); ok && charger != nil {
+	if charger, ok := lp.charger.(*api.MockCharger); ok && charger != nil {
 		charger.EXPECT().Enabled().Return(true, nil)
 		charger.EXPECT().MaxCurrent(int64(lp.MinCurrent)).Return(nil)
 	}
@@ -104,46 +103,46 @@ func TestUpdatePowerZero(t *testing.T) {
 	tc := []struct {
 		status api.ChargeStatus
 		mode   api.ChargeMode
-		expect func(h *mock.MockCharger)
+		expect func(h *api.MockCharger)
 	}{
-		{api.StatusA, api.ModeOff, func(h *mock.MockCharger) {
+		{api.StatusA, api.ModeOff, func(h *api.MockCharger) {
 			h.EXPECT().Enable(false)
 		}},
-		{api.StatusA, api.ModeNow, func(h *mock.MockCharger) {
+		{api.StatusA, api.ModeNow, func(h *api.MockCharger) {
 			h.EXPECT().Enable(false)
 		}},
-		{api.StatusA, api.ModeMinPV, func(h *mock.MockCharger) {
+		{api.StatusA, api.ModeMinPV, func(h *api.MockCharger) {
 			h.EXPECT().Enable(false)
 		}},
-		{api.StatusA, api.ModePV, func(h *mock.MockCharger) {
+		{api.StatusA, api.ModePV, func(h *api.MockCharger) {
 			h.EXPECT().Enable(false) // zero since update called with 0
 		}},
 
-		{api.StatusB, api.ModeOff, func(h *mock.MockCharger) {
+		{api.StatusB, api.ModeOff, func(h *api.MockCharger) {
 			h.EXPECT().Enable(false)
 		}},
-		{api.StatusB, api.ModeNow, func(h *mock.MockCharger) {
+		{api.StatusB, api.ModeNow, func(h *api.MockCharger) {
 			h.EXPECT().MaxCurrent(int64(maxA)) // true
 		}},
-		{api.StatusB, api.ModeMinPV, func(h *mock.MockCharger) {
+		{api.StatusB, api.ModeMinPV, func(h *api.MockCharger) {
 			// MaxCurrent omitted since identical value
 		}},
-		{api.StatusB, api.ModePV, func(h *mock.MockCharger) {
+		{api.StatusB, api.ModePV, func(h *api.MockCharger) {
 			// zero since update called with 0
 			// force = false due to pv mode climater check
 			h.EXPECT().Enable(false)
 		}},
 
-		{api.StatusC, api.ModeOff, func(h *mock.MockCharger) {
+		{api.StatusC, api.ModeOff, func(h *api.MockCharger) {
 			h.EXPECT().Enable(false)
 		}},
-		{api.StatusC, api.ModeNow, func(h *mock.MockCharger) {
+		{api.StatusC, api.ModeNow, func(h *api.MockCharger) {
 			h.EXPECT().MaxCurrent(int64(maxA)) // true
 		}},
-		{api.StatusC, api.ModeMinPV, func(h *mock.MockCharger) {
+		{api.StatusC, api.ModeMinPV, func(h *api.MockCharger) {
 			// MaxCurrent omitted since identical value
 		}},
-		{api.StatusC, api.ModePV, func(h *mock.MockCharger) {
+		{api.StatusC, api.ModePV, func(h *api.MockCharger) {
 			// omitted since PV balanced
 		}},
 	}
@@ -153,7 +152,7 @@ func TestUpdatePowerZero(t *testing.T) {
 
 		clck := clock.NewMock()
 		ctrl := gomock.NewController(t)
-		charger := mock.NewMockCharger(ctrl)
+		charger := api.NewMockCharger(ctrl)
 
 		lp := &Loadpoint{
 			log:           util.NewLogger("foo"),
@@ -305,7 +304,7 @@ func TestPVHysteresis(t *testing.T) {
 
 			clck := clock.NewMock()
 			ctrl := gomock.NewController(t)
-			charger := mock.NewMockCharger(ctrl)
+			charger := api.NewMockCharger(ctrl)
 
 			Voltage = 100
 			lp := &Loadpoint{
@@ -383,8 +382,8 @@ func TestPVHysteresisForStatusOtherThanC(t *testing.T) {
 func TestDisableAndEnableAtTargetSoc(t *testing.T) {
 	clock := clock.NewMock()
 	ctrl := gomock.NewController(t)
-	charger := mock.NewMockCharger(ctrl)
-	vehicle := mock.NewMockVehicle(ctrl)
+	charger := api.NewMockCharger(ctrl)
+	vehicle := api.NewMockVehicle(ctrl)
 
 	// wrap vehicle with estimator
 	vehicle.EXPECT().Capacity().Return(float64(10))
@@ -465,7 +464,7 @@ func TestDisableAndEnableAtTargetSoc(t *testing.T) {
 func TestSetModeAndSocAtDisconnect(t *testing.T) {
 	clock := clock.NewMock()
 	ctrl := gomock.NewController(t)
-	charger := mock.NewMockCharger(ctrl)
+	charger := api.NewMockCharger(ctrl)
 
 	lp := &Loadpoint{
 		log:           util.NewLogger("foo"),
@@ -537,8 +536,8 @@ func cacheExpecter(t *testing.T, lp *Loadpoint) (*util.Cache, func(key string, v
 func TestChargedEnergyAtDisconnect(t *testing.T) {
 	clock := clock.NewMock()
 	ctrl := gomock.NewController(t)
-	charger := mock.NewMockCharger(ctrl)
-	rater := mock.NewMockChargeRater(ctrl)
+	charger := api.NewMockCharger(ctrl)
+	rater := api.NewMockChargeRater(ctrl)
 
 	lp := &Loadpoint{
 		log:           util.NewLogger("foo"),
@@ -615,7 +614,7 @@ func TestChargedEnergyAtDisconnect(t *testing.T) {
 
 func TestTargetSoc(t *testing.T) {
 	ctrl := gomock.NewController(t)
-	vhc := mock.NewMockVehicle(ctrl)
+	vhc := api.NewMockVehicle(ctrl)
 
 	tc := []struct {
 		vehicle api.Vehicle
@@ -742,10 +741,10 @@ func TestSocPoll(t *testing.T) {
 
 func TestMinSoc(t *testing.T) {
 	ctrl := gomock.NewController(t)
-	vhc := mock.NewMockVehicle(ctrl)
+	vhc := api.NewMockVehicle(ctrl)
 
 	tc := []struct {
-		vehicle *mock.MockVehicle
+		vehicle *api.MockVehicle
 		min     int
 		soc     float64
 		energy  float64
diff --git a/core/loadpoint_vehicle_test.go b/core/loadpoint_vehicle_test.go
index 9e7473d8..5e3822be 100644
--- a/core/loadpoint_vehicle_test.go
+++ b/core/loadpoint_vehicle_test.go
@@ -10,7 +10,6 @@ import (
 	"github.com/evcc-io/evcc/api"
 	"github.com/evcc-io/evcc/core/coordinator"
 	"github.com/evcc-io/evcc/core/soc"
-	"github.com/evcc-io/evcc/mock"
 	"github.com/evcc-io/evcc/util"
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
@@ -20,11 +19,11 @@ func TestPublishSocAndRange(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	clck := clock.NewMock()
 
-	charger := mock.NewMockCharger(ctrl)
+	charger := api.NewMockCharger(ctrl)
 	charger.EXPECT().MaxCurrent(int64(maxA)).AnyTimes()
 	charger.EXPECT().Enabled().Return(true, nil).AnyTimes()
 
-	vehicle := mock.NewMockVehicle(ctrl)
+	vehicle := api.NewMockVehicle(ctrl)
 	vehicle.EXPECT().Title().Return("target").AnyTimes()
 	vehicle.EXPECT().Capacity().AnyTimes()
 	vehicle.EXPECT().Phases().AnyTimes()
@@ -82,8 +81,8 @@ func TestPublishSocAndRange(t *testing.T) {
 func TestVehicleDetectByID(t *testing.T) {
 	ctrl := gomock.NewController(t)
 
-	v1 := mock.NewMockVehicle(ctrl)
-	v2 := mock.NewMockVehicle(ctrl)
+	v1 := api.NewMockVehicle(ctrl)
+	v2 := api.NewMockVehicle(ctrl)
 
 	type testcase struct {
 		string
@@ -150,7 +149,7 @@ func TestDefaultVehicle(t *testing.T) {
 	mode := api.ModePV
 	targetsoc := 80
 
-	dflt := mock.NewMockVehicle(ctrl)
+	dflt := api.NewMockVehicle(ctrl)
 	dflt.EXPECT().Title().Return("default").AnyTimes()
 	dflt.EXPECT().Icon().Return("").AnyTimes()
 	dflt.EXPECT().Capacity().AnyTimes()
@@ -160,7 +159,7 @@ func TestDefaultVehicle(t *testing.T) {
 		TargetSoc: &targetsoc,
 	}).AnyTimes()
 
-	vehicle := mock.NewMockVehicle(ctrl)
+	vehicle := api.NewMockVehicle(ctrl)
 	vehicle.EXPECT().Title().Return("target").AnyTimes()
 	vehicle.EXPECT().Icon().Return("").AnyTimes()
 	vehicle.EXPECT().Capacity().AnyTimes()
@@ -244,7 +243,7 @@ func TestApplyVehicleDefaults(t *testing.T) {
 	// onDefault config
 	od := newConfig(api.ModeOff, 6, 16, nil)
 
-	vehicle := mock.NewMockVehicle(ctrl)
+	vehicle := api.NewMockVehicle(ctrl)
 	vehicle.EXPECT().Title().Return("it's me").AnyTimes()
 	vehicle.EXPECT().Icon().Return("").AnyTimes()
 	vehicle.EXPECT().Capacity().AnyTimes()
@@ -275,11 +274,11 @@ func TestApplyVehicleDefaults(t *testing.T) {
 
 	// identify vehicle by id
 	charger := struct {
-		*mock.MockCharger
-		*mock.MockIdentifier
+		*api.MockCharger
+		*api.MockIdentifier
 	}{
-		MockCharger:    mock.NewMockCharger(ctrl),
-		MockIdentifier: mock.NewMockIdentifier(ctrl),
+		MockCharger:    api.NewMockCharger(ctrl),
+		MockIdentifier: api.NewMockIdentifier(ctrl),
 	}
 
 	lp.charger = charger
@@ -308,11 +307,11 @@ func TestReconnectVehicle(t *testing.T) {
 			clck := clock.NewMock()
 
 			type vehicleT struct {
-				*mock.MockVehicle
-				*mock.MockChargeState
+				*api.MockVehicle
+				*api.MockChargeState
 			}
 
-			vehicle := &vehicleT{mock.NewMockVehicle(ctrl), mock.NewMockChargeState(ctrl)}
+			vehicle := &vehicleT{api.NewMockVehicle(ctrl), api.NewMockChargeState(ctrl)}
 			vehicle.MockVehicle.EXPECT().Title().Return("vehicle").AnyTimes()
 			vehicle.MockVehicle.EXPECT().Icon().Return("").AnyTimes()
 			vehicle.MockVehicle.EXPECT().Capacity().AnyTimes()
@@ -321,7 +320,7 @@ func TestReconnectVehicle(t *testing.T) {
 			vehicle.MockVehicle.EXPECT().Identifiers().AnyTimes().Return(tc.vehicleId)
 			vehicle.MockVehicle.EXPECT().Soc().Return(0.0, nil).AnyTimes()
 
-			charger := mock.NewMockCharger(ctrl)
+			charger := api.NewMockCharger(ctrl)
 			charger.EXPECT().Status().Return(api.StatusB, nil).AnyTimes()
 
 			lp := &Loadpoint{
diff --git a/core/planner/planner_test.go b/core/planner/planner_test.go
index 97f83de6..f2de7355 100644
--- a/core/planner/planner_test.go
+++ b/core/planner/planner_test.go
@@ -7,7 +7,6 @@ import (
 
 	"github.com/benbjohnson/clock"
 	"github.com/evcc-io/evcc/api"
-	"github.com/evcc-io/evcc/mock"
 	"github.com/evcc-io/evcc/util"
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
@@ -35,7 +34,7 @@ func TestPlan(t *testing.T) {
 	clock := clock.NewMock()
 	ctrl := gomock.NewController(t)
 
-	trf := mock.NewMockTariff(ctrl)
+	trf := api.NewMockTariff(ctrl)
 	trf.EXPECT().Rates().AnyTimes().Return(rates([]float64{20, 60, 10, 80, 40, 90}, clock.Now(), time.Hour), nil)
 
 	p := &Planner{
@@ -146,7 +145,7 @@ func TestFlatTariffTargetInThePast(t *testing.T) {
 	clock := clock.NewMock()
 	ctrl := gomock.NewController(t)
 
-	trf := mock.NewMockTariff(ctrl)
+	trf := api.NewMockTariff(ctrl)
 	trf.EXPECT().Rates().AnyTimes().Return(rates([]float64{0}, clock.Now(), time.Hour), nil)
 
 	p := &Planner{
@@ -168,7 +167,7 @@ func TestFlatTariffLongSlots(t *testing.T) {
 	clock := clock.NewMock()
 	ctrl := gomock.NewController(t)
 
-	trf := mock.NewMockTariff(ctrl)
+	trf := api.NewMockTariff(ctrl)
 	trf.EXPECT().Rates().AnyTimes().Return(rates([]float64{0}, clock.Now(), 24*time.Hour), nil)
 
 	p := &Planner{
@@ -196,7 +195,7 @@ func TestTargetAfterKnownPrices(t *testing.T) {
 	clock := clock.NewMock()
 	ctrl := gomock.NewController(t)
 
-	trf := mock.NewMockTariff(ctrl)
+	trf := api.NewMockTariff(ctrl)
 	trf.EXPECT().Rates().AnyTimes().Return(rates([]float64{0}, clock.Now(), time.Hour), nil)
 
 	p := &Planner{
@@ -218,7 +217,7 @@ func TestChargeAfterTargetTime(t *testing.T) {
 	clock := clock.NewMock()
 	ctrl := gomock.NewController(t)
 
-	trf := mock.NewMockTariff(ctrl)
+	trf := api.NewMockTariff(ctrl)
 	trf.EXPECT().Rates().AnyTimes().Return(rates([]float64{0, 0, 0, 0}, clock.Now(), time.Hour), nil)
 
 	p := &Planner{
diff --git a/core/soc/estimator_test.go b/core/soc/estimator_test.go
index 66240fe0..4d5fe8bf 100644
--- a/core/soc/estimator_test.go
+++ b/core/soc/estimator_test.go
@@ -6,7 +6,6 @@ import (
 	"time"
 
 	"github.com/evcc-io/evcc/api"
-	"github.com/evcc-io/evcc/mock"
 	"github.com/evcc-io/evcc/util"
 	"github.com/golang/mock/gomock"
 	"github.com/stretchr/testify/assert"
@@ -14,8 +13,8 @@ import (
 
 func TestRemainingChargeDuration(t *testing.T) {
 	ctrl := gomock.NewController(t)
-	charger := mock.NewMockCharger(ctrl)
-	vehicle := mock.NewMockVehicle(ctrl)
+	charger := api.NewMockCharger(ctrl)
+	vehicle := api.NewMockVehicle(ctrl)
 	// 9 kWh userBatCap => 10 kWh virtualBatCap
 	vehicle.EXPECT().Capacity().Return(float64(9))
 
@@ -32,13 +31,13 @@ func TestRemainingChargeDuration(t *testing.T) {
 
 func TestSocEstimation(t *testing.T) {
 	type chargerStruct struct {
-		*mock.MockCharger
-		*mock.MockBattery
+		*api.MockCharger
+		*api.MockBattery
 	}
 
 	ctrl := gomock.NewController(t)
-	vehicle := mock.NewMockVehicle(ctrl)
-	charger := &chargerStruct{mock.NewMockCharger(ctrl), mock.NewMockBattery(ctrl)}
+	vehicle := api.NewMockVehicle(ctrl)
+	charger := &chargerStruct{api.NewMockCharger(ctrl), api.NewMockBattery(ctrl)}
 
 	// 9 kWh user battery capacity is converted to initial value of 10 kWh virtual capacity
 	var capacity float64 = 9
@@ -116,13 +115,13 @@ func TestSocEstimation(t *testing.T) {
 
 func TestSocFromChargerAndVehicleWithErrors(t *testing.T) {
 	type chargerStruct struct {
-		*mock.MockCharger
-		*mock.MockBattery
+		*api.MockCharger
+		*api.MockBattery
 	}
 
 	ctrl := gomock.NewController(t)
-	vehicle := mock.NewMockVehicle(ctrl)
-	charger := &chargerStruct{mock.NewMockCharger(ctrl), mock.NewMockBattery(ctrl)}
+	vehicle := api.NewMockVehicle(ctrl)
+	charger := &chargerStruct{api.NewMockCharger(ctrl), api.NewMockBattery(ctrl)}
 
 	// 9 kWh user battery capacity is converted to initial value of 10 kWh virtual capacity
 	var capacity float64 = 9
@@ -211,8 +210,8 @@ func TestSocFromChargerAndVehicleWithErrors(t *testing.T) {
 
 func TestImprovedEstimatorRemainingChargeDuration(t *testing.T) {
 	ctrl := gomock.NewController(t)
-	charger := mock.NewMockCharger(ctrl)
-	vehicle := mock.NewMockVehicle(ctrl)
+	charger := api.NewMockCharger(ctrl)
+	vehicle := api.NewMockVehicle(ctrl)
 
 	// https://github.com/evcc-io/evcc/pull/7510#issuecomment-1512688548
 	tc := []struct {
@@ -228,7 +227,7 @@ func TestImprovedEstimatorRemainingChargeDuration(t *testing.T) {
 		{17, 50, 100, 7 * 1e3, 1*time.Hour + 28*time.Minute + 23*time.Second},
 		{50, 10, 60, 11 * 1e3, 2*time.Hour + 31*time.Minute + 31*time.Second},
 		{50, 50, 100, 11 * 1e3, 2*time.Hour + 57*time.Minute + 17*time.Second},
-		{80, 10, 60, 22 * 1e3, 2*time.Hour + 01*time.Minute + 13*time.Second},
+		{80, 10, 60, 22 * 1e3, 2*time.Hour + 0o1*time.Minute + 13*time.Second},
 		{80, 50, 100, 22 * 1e3, 2*time.Hour + 48*time.Minute + 39*time.Second},
 	}
 
diff --git a/core/wrapper/chargerater_test.go b/core/wrapper/chargerater_test.go
index a36f79fb..5fea988a 100644
--- a/core/wrapper/chargerater_test.go
+++ b/core/wrapper/chargerater_test.go
@@ -6,7 +6,6 @@ import (
 
 	"github.com/benbjohnson/clock"
 	"github.com/evcc-io/evcc/api"
-	"github.com/evcc-io/evcc/mock"
 	"github.com/evcc-io/evcc/util"
 	"github.com/golang/mock/gomock"
 )
@@ -61,8 +60,8 @@ func TestWrappedMeter(t *testing.T) {
 	ctrl := gomock.NewController(t)
 	defer ctrl.Finish()
 
-	mm := mock.NewMockMeter(ctrl)
-	me := mock.NewMockMeterEnergy(ctrl)
+	mm := api.NewMockMeter(ctrl)
+	me := api.NewMockMeterEnergy(ctrl)
 
 	type EnergyDecorator struct {
 		api.Meter
diff --git a/server/modbus/proxy_test.go b/server/modbus/proxy_test.go
index 83951dcd..588584e6 100644
--- a/server/modbus/proxy_test.go
+++ b/server/modbus/proxy_test.go
@@ -81,9 +81,8 @@ func TestReadCoils(t *testing.T) {
 	assert.NoError(t, err)
 
 	proxy, _ := mbserver.New(&handler{
-		log:            util.NewLogger("foo"),
-		RequestHandler: new(mbserver.DummyHandler),
-		conn:           downstreamConn,
+		log:  util.NewLogger("foo"),
+		conn: downstreamConn,
 	})
 	assert.NoError(t, proxy.Start(pl))
 	defer func() { _ = proxy.Stop() }()