Repository: evcc-io/evcc
Pull request #8901 (2023-07-10 19:43:13)

Add M-TEC Energy Butler
Fixes #8601 #8036

@1979ck @thomas-hutterer-tik @sirwastl bitte testen.
Wir freuen uns über zahlreiche neue Sponsoren!


Patch:
diff --git a/charger/easee.go b/charger/easee.go
index 04eacb88..580c8dd4 100644
--- a/charger/easee.go
+++ b/charger/easee.go
@@ -399,7 +399,6 @@ func (c *Easee) Enable(enable bool) error {
 		targetCurrent = 32
 	}
 
-	c.log.DEBUG.Printf("send command: %s", action)
 	uri := fmt.Sprintf("%s/chargers/%s/commands/%s", easee.API, c.charger, action)
 	if err := c.postJSONAndWait(uri, nil); err != nil {
 		return err
@@ -409,13 +408,7 @@ func (c *Easee) Enable(enable bool) error {
 		return err
 	}
 
-	c.mux.Lock()
-	dynamicChargerCurrent := c.dynamicChargerCurrent
-	c.mux.Unlock()
-
-	c.log.DEBUG.Printf("DCC update received, current: %.3f, dynamicChargerCurrent: %.3f", c.current, dynamicChargerCurrent)
 	if enable {
-		c.log.DEBUG.Printf("send enable, reset current: %.3f", c.current)
 		// reset currents after enable, as easee automatically resets to maxA
 		return c.MaxCurrent(int64(c.current))
 	}
@@ -465,7 +458,6 @@ func (c *Easee) postJSONAndWait(uri string, data any) error {
 }
 
 func (c *Easee) waitForTickResponse(expectedTick int64) error {
-	c.log.TRACE.Printf("wait for tick response: %d", expectedTick)
 	for {
 		select {
 		case cmdResp := <-c.cmdC:
@@ -473,11 +465,9 @@ func (c *Easee) waitForTickResponse(expectedTick int64) error {
 				if !cmdResp.WasAccepted {
 					return fmt.Errorf("command rejected: %d", cmdResp.Ticks)
 				}
-				c.log.TRACE.Printf("received tick response: %d", cmdResp.Ticks)
 				return nil
 			}
 		case <-time.After(10 * time.Second):
-			c.log.TRACE.Printf("tick response timed out: %d", expectedTick)
 			return api.ErrTimeout
 		}
 	}
@@ -485,8 +475,6 @@ func (c *Easee) waitForTickResponse(expectedTick int64) error {
 
 // wait for up to 3s for current become targetCurrent
 func (c *Easee) waitForDynamicChargerCurrent(targetCurrent float64) error {
-	c.log.DEBUG.Printf("wait for DCC update: %.3f", targetCurrent)
-
 	// check any updates received meanwhile
 	c.mux.Lock()
 	if c.dynamicChargerCurrent == targetCurrent {
@@ -506,7 +494,6 @@ func (c *Easee) waitForDynamicChargerCurrent(targetCurrent float64) error {
 			if err != nil {
 				continue
 			}
-			c.log.TRACE.Printf("DCC update received: %.3f (want: %.3f)", value.(float64), targetCurrent)
 			if value.(float64) == targetCurrent {
 				return nil
 			}
@@ -670,8 +657,7 @@ func (c *Easee) updateSmartCharging() {
 
 		uri := fmt.Sprintf("%s/chargers/%s/settings", easee.API, c.charger)
 
-		err := c.postJSONAndWait(uri, data)
-		if err != nil {
+		if err := c.postJSONAndWait(uri, data); err != nil {
 			c.log.WARN.Printf("smart charging: %v", err)
 			return
 		}
diff --git "a/charger/weidm\303\274ller.go" "b/charger/weidm\303\274ller.go"
new file mode 100644
index 00000000..dd06d59e
--- /dev/null
+++ "b/charger/weidm\303\274ller.go"
@@ -0,0 +1,243 @@
+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"
+
+	"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"
+)
+
+// Weidmüller charger implementation
+type Weidmüller struct {
+	log  *util.Logger
+	conn *modbus.Connection
+	curr uint16
+}
+
+const (
+	wmRegCarStatus    = 301  // GD_ID_EVCC_CAR_STATE CHAR
+	wmRegEvccStatus   = 302  // GD_ID_EVCC_EVSE_STATE UINT16
+	wmRegPhases       = 317  // GD_ID_EVCC_PHASES UINT16
+	wmRegVoltages     = 400  // GD_ID_CM_VOLTAGE_PHASE UINT32
+	wmRegCurrents     = 406  // GD_ID_CM_CURRENT_PHASE UINT32
+	wmRegActivePower  = 418  // GD_ID_CM_ACTIVE_POWER UINT32
+	wmRegTotalEnergy  = 457  // GD_ID_CM_CONSUMED_ENERGY_TOTAL_WH UINT64
+	wmRegCurrentLimit = 702  // GD_ID_AUT_USER_CURRENT_LIMIT UINT16
+	wmRegCardId       = 1000 // GD_ID_RFID_TAG_UID CHAR[21]
+)
+
+func init() {
+	registry.Add("weidmüller", NewWeidmüllerFromConfig)
+}
+
+// NewWeidmüllerFromConfig creates a Weidmüller charger from generic config
+func NewWeidmüllerFromConfig(other map[string]interface{}) (api.Charger, error) {
+	cc := modbus.TcpSettings{
+		ID: 255,
+	}
+
+	if err := util.DecodeOther(other, &cc); err != nil {
+		return nil, err
+	}
+
+	return NewWeidmüller(cc.URI, cc.ID)
+}
+
+// NewWeidmüller creates Weidmüller charger
+func NewWeidmüller(uri string, id uint8) (api.Charger, error) {
+	conn, err := modbus.NewConnection(uri, "", "", 0, modbus.Tcp, id)
+	if err != nil {
+		return nil, err
+	}
+
+	if !sponsor.IsAuthorized() {
+		return nil, api.ErrSponsorRequired
+	}
+
+	log := util.NewLogger("weidmüller")
+	conn.Logger(log.TRACE)
+
+	wb := &Weidmüller{
+		log:  log,
+		conn: conn,
+		curr: 6, // assume min current
+	}
+
+	// get initial state from charger
+	curr, err := wb.getCurrent()
+	if err != nil {
+		return nil, fmt.Errorf("current limit: %w", err)
+	}
+	if curr != 0 {
+		wb.curr = curr
+	}
+
+	return wb, err
+}
+
+func (wb *Weidmüller) setCurrent(current uint16) error {
+	b := make([]byte, 2)
+	binary.BigEndian.PutUint16(b, current)
+
+	_, err := wb.conn.WriteMultipleRegisters(wmRegCurrentLimit, 1, b)
+
+	return err
+}
+
+func (wb *Weidmüller) getCurrent() (uint16, error) {
+	b, err := wb.conn.ReadHoldingRegisters(wmRegCurrentLimit, 1)
+	if err != nil {
+		return 0, err
+	}
+
+	return binary.BigEndian.Uint16(b), nil
+}
+
+// getPhaseValues returns 3 sequential register values
+func (wb *Weidmüller) getPhaseValues(reg uint16) (float64, float64, float64, error) {
+	b, err := wb.conn.ReadHoldingRegisters(reg, 6)
+	if err != nil {
+		return 0, 0, 0, err
+	}
+
+	var res [3]float64
+	for i := 0; i < 3; i++ {
+		res[i] = float64(binary.BigEndian.Uint32(b[4*i:])) / 1e3
+	}
+
+	return res[0], res[1], res[2], nil
+}
+
+// Status implements the api.Charger interface
+func (wb *Weidmüller) Status() (api.ChargeStatus, error) {
+	b, err := wb.conn.ReadHoldingRegisters(wmRegCarStatus, 1)
+	if err != nil {
+		return api.StatusNone, err
+	}
+
+	switch s := string(b[0]); s {
+	case "A", "B", "C":
+		return api.ChargeStatus(s), nil
+	default:
+		return api.StatusNone, fmt.Errorf("invalid status: %s", s)
+	}
+}
+
+// Enabled implements the api.Charger interface
+func (wb *Weidmüller) Enabled() (bool, error) {
+	curr, err := wb.getCurrent()
+
+	return curr != 0, err
+}
+
+// Enable implements the api.Charger interface
+func (wb *Weidmüller) Enable(enable bool) error {
+	var current uint16
+	if enable {
+		current = wb.curr
+	}
+
+	return wb.setCurrent(current)
+}
+
+// MaxCurrent implements the api.Charger interface
+func (wb *Weidmüller) MaxCurrent(current int64) error {
+	if current < 6 {
+		return fmt.Errorf("invalid current %d", current)
+	}
+
+	wb.curr = uint16(current * 10)
+
+	return wb.setCurrent(wb.curr)
+}
+
+var _ api.Meter = (*Weidmüller)(nil)
+
+// CurrentPower implements the api.Meter interface
+func (wb *Weidmüller) CurrentPower() (float64, error) {
+	b, err := wb.conn.ReadHoldingRegisters(wmRegActivePower, 2)
+	if err != nil {
+		return 0, err
+	}
+
+	return float64(binary.BigEndian.Uint32(b)) / 1e3, err
+}
+
+var _ api.MeterEnergy = (*Weidmüller)(nil)
+
+// TotalEnergy implements the api.MeterEnergy interface
+func (wb *Weidmüller) TotalEnergy() (float64, error) {
+	b, err := wb.conn.ReadHoldingRegisters(wmRegTotalEnergy, 2)
+	if err != nil {
+		return 0, err
+	}
+
+	return float64(binary.BigEndian.Uint32(b)) / 1e3, err
+}
+
+var _ api.PhaseCurrents = (*Weidmüller)(nil)
+
+// Currents implements the api.PhaseCurrents interface
+func (wb *Weidmüller) Currents() (float64, float64, float64, error) {
+	return wb.getPhaseValues(wmRegCurrents)
+}
+
+var _ api.PhaseVoltages = (*Weidmüller)(nil)
+
+// Voltages implements the api.PhaseVoltages interface
+func (wb *Weidmüller) Voltages() (float64, float64, float64, error) {
+	return wb.getPhaseValues(wmRegVoltages)
+}
+
+var _ api.Identifier = (*Weidmüller)(nil)
+
+// Identify implements the api.Identifier interface
+func (wb *Weidmüller) Identify() (string, error) {
+	b, err := wb.conn.ReadHoldingRegisters(wmRegCardId, 11)
+	if err != nil {
+		return "", err
+	}
+	return bytesAsString(b), nil
+}
+
+var _ api.PhaseSwitcher = (*Weidmüller)(nil)
+
+// Phases1p3p implements the api.PhaseSwitcher interface
+func (wb *Weidmüller) Phases1p3p(phases int) error {
+	b := make([]byte, 2)
+
+	if phases == 3 {
+		binary.BigEndian.PutUint16(b, 1)
+	}
+
+	_, err := wb.conn.WriteMultipleRegisters(wmRegPhases, 1, b)
+
+	return err
+}
+
+var _ api.Diagnosis = (*Weidmüller)(nil)
+
+// Diagnose implements the api.Diagnosis interface
+func (wb *Weidmüller) Diagnose() {
+
+}
diff --git a/core/loadpoint.go b/core/loadpoint.go
index 145f8a3a..b9ee3080 100644
--- a/core/loadpoint.go
+++ b/core/loadpoint.go
@@ -315,7 +315,7 @@ func (lp *Loadpoint) collectDefaults() {
 		*actionCfg.MaxCurrent = lp.GetMaxCurrent()
 		*actionCfg.MinSoc = lp.GetMinSoc()
 		*actionCfg.TargetSoc = lp.GetTargetSoc()
-		*actionCfg.Priority = lp.Priority()
+		*actionCfg.Priority = lp.GetPriority()
 	} else {
 		lp.log.ERROR.Printf("error allocating action config: %v", err)
 	}
@@ -557,6 +557,9 @@ func (lp *Loadpoint) applyAction(actionCfg api.ActionConfig) {
 	if actionCfg.TargetSoc != nil {
 		lp.SetTargetSoc(*actionCfg.TargetSoc)
 	}
+	if actionCfg.Priority != nil {
+		lp.SetPriority(*actionCfg.Priority)
+	}
 }
 
 // Prepare loadpoint configuration by adding missing helper elements
@@ -603,6 +606,7 @@ func (lp *Loadpoint) Prepare(uiChan chan<- util.Param, pushChan chan<- push.Even
 	}
 
 	lp.publish("mode", lp.GetMode())
+	lp.publish("priority", lp.GetPriority())
 	lp.publish(targetSoc, lp.GetTargetSoc())
 	lp.publish(minSoc, lp.GetMinSoc())
 
diff --git a/core/loadpoint/api.go b/core/loadpoint/api.go
index 1e2a33b9..226f505b 100644
--- a/core/loadpoint/api.go
+++ b/core/loadpoint/api.go
@@ -17,7 +17,6 @@ type Controller interface {
 type API interface {
 	// Title returns the defined loadpoint title
 	Title() string
-	Priority() int
 
 	//
 	// status
@@ -30,6 +29,11 @@ type API interface {
 	// settings
 	//
 
+	// GetPriority returns the loadpoint priority
+	GetPriority() int
+	// SetPriority sets the loadpoint priority
+	SetPriority(int)
+
 	// GetMode returns the charge mode
 	GetMode() api.ChargeMode
 	// SetMode sets the charge mode
diff --git a/core/loadpoint/mock.go b/core/loadpoint/mock.go
index 5bc56870..a2c2addb 100644
--- a/core/loadpoint/mock.go
+++ b/core/loadpoint/mock.go
@@ -205,6 +205,20 @@ func (mr *MockAPIMockRecorder) GetPlan(arg0, arg1 interface{}) *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPlan", reflect.TypeOf((*MockAPI)(nil).GetPlan), arg0, arg1)
 }
 
+// GetPriority mocks base method.
+func (m *MockAPI) GetPriority() int {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetPriority")
+	ret0, _ := ret[0].(int)
+	return ret0
+}
+
+// GetPriority indicates an expected call of GetPriority.
+func (mr *MockAPIMockRecorder) GetPriority() *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPriority", reflect.TypeOf((*MockAPI)(nil).GetPriority))
+}
+
 // GetRemainingDuration mocks base method.
 func (m *MockAPI) GetRemainingDuration() time.Duration {
 	m.ctrl.T.Helper()
@@ -317,20 +331,6 @@ func (mr *MockAPIMockRecorder) HasChargeMeter() *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasChargeMeter", reflect.TypeOf((*MockAPI)(nil).HasChargeMeter))
 }
 
-// Priority mocks base method.
-func (m *MockAPI) Priority() int {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Priority")
-	ret0, _ := ret[0].(int)
-	return ret0
-}
-
-// Priority indicates an expected call of Priority.
-func (mr *MockAPIMockRecorder) Priority() *gomock.Call {
-	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Priority", reflect.TypeOf((*MockAPI)(nil).Priority))
-}
-
 // RemoteControl mocks base method.
 func (m *MockAPI) RemoteControl(arg0 string, arg1 RemoteDemand) {
 	m.ctrl.T.Helper()
@@ -429,6 +429,18 @@ func (mr *MockAPIMockRecorder) SetPhases(arg0 interface{}) *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPhases", reflect.TypeOf((*MockAPI)(nil).SetPhases), arg0)
 }
 
+// SetPriority mocks base method.
+func (m *MockAPI) SetPriority(arg0 int) {
+	m.ctrl.T.Helper()
+	m.ctrl.Call(m, "SetPriority", arg0)
+}
+
+// SetPriority indicates an expected call of SetPriority.
+func (mr *MockAPIMockRecorder) SetPriority(arg0 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetPriority", reflect.TypeOf((*MockAPI)(nil).SetPriority), arg0)
+}
+
 // SetTargetEnergy mocks base method.
 func (m *MockAPI) SetTargetEnergy(arg0 float64) {
 	m.ctrl.T.Helper()
diff --git a/core/loadpoint_api.go b/core/loadpoint_api.go
index d313b530..07552264 100644
--- a/core/loadpoint_api.go
+++ b/core/loadpoint_api.go
@@ -18,11 +18,6 @@ func (lp *Loadpoint) Title() string {
 	return lp.Title_
 }
 
-// Priority returns the loadpoint priority
-func (lp *Loadpoint) Priority() int {
-	return lp.Priority_
-}
-
 // GetStatus returns the charging status
 func (lp *Loadpoint) GetStatus() api.ChargeStatus {
 	lp.Lock()
@@ -102,6 +97,26 @@ func (lp *Loadpoint) SetTargetEnergy(energy float64) {
 	}
 }
 
+// GetPriority returns the loadpoint priority
+func (lp *Loadpoint) GetPriority() int {
+	lp.Lock()
+	defer lp.Unlock()
+	return lp.Priority_
+}
+
+// SetPriority sets the loadpoint priority
+func (lp *Loadpoint) SetPriority(prio int) {
+	lp.Lock()
+	defer lp.Unlock()
+
+	lp.log.DEBUG.Println("set priority:", prio)
+
+	if lp.Priority_ != prio {
+		lp.Priority_ = prio
+		lp.publish("priority", prio)
+	}
+}
+
 // GetTargetSoc returns loadpoint charge target soc
 func (lp *Loadpoint) GetTargetSoc() int {
 	lp.Lock()
diff --git a/core/prioritizer/prioritizer.go b/core/prioritizer/prioritizer.go
index 334dac20..346bcfe7 100644
--- a/core/prioritizer/prioritizer.go
+++ b/core/prioritizer/prioritizer.go
@@ -1,15 +1,20 @@
 package prioritizer
 
 import (
+	"fmt"
+
 	"github.com/evcc-io/evcc/core/loadpoint"
+	"github.com/evcc-io/evcc/util"
 )
 
 type Prioritizer struct {
+	log    *util.Logger
 	demand map[loadpoint.API]float64
 }
 
-func New() *Prioritizer {
+func New(log *util.Logger) *Prioritizer {
 	return &Prioritizer{
+		log:    log,
 		demand: make(map[loadpoint.API]float64),
 	}
 }
@@ -21,14 +26,23 @@ func (p *Prioritizer) UpdateChargePowerFlexibility(lp loadpoint.API) {
 }
 
 func (p *Prioritizer) GetChargePowerFlexibility(lp loadpoint.API) float64 {
-	prio := lp.Priority()
+	prio := lp.GetPriority()
+
+	var (
+		reduceBy float64
+		msg      string
+	)
 
-	var reduceBy float64
 	for lp, power := range p.demand {
-		if lp.Priority() < prio {
+		if lp.GetPriority() < prio {
 			reduceBy += power
+			msg += fmt.Sprintf("%.0fW from %s at prio %d, ", power, lp.Title(), lp.GetPriority())
 		}
 	}
 
+	if p.log != nil && reduceBy > 0 {
+		p.log.DEBUG.Printf("lp %s at prio %d gets additional %stotal %.0fW\n", lp.Title(), lp.GetPriority(), msg, reduceBy)
+	}
+
 	return reduceBy
 }
diff --git a/core/site.go b/core/site.go
index dd76a415..390d82b1 100644
--- a/core/site.go
+++ b/core/site.go
@@ -113,7 +113,7 @@ func NewSiteFromConfig(
 	site.loadpoints = loadpoints
 	site.tariffs = tariffs
 	site.coordinator = coordinator.New(log, vehicles)
-	site.prioritizer = prioritizer.New()
+	site.prioritizer = prioritizer.New(log)
 	site.savings = NewSavings(tariffs)
 
 	site.restoreSettings()
diff --git a/i18n/es.toml b/i18n/es.toml
index 585940aa..e3700a6f 100644
--- a/i18n/es.toml
+++ b/i18n/es.toml
@@ -111,12 +111,12 @@ modalTitle = "¿Necesitas ayuda?"
 primaryActions = "Si algo no funciona como debería, puedes obtener ayuda aqui."
 restartButton = "Reiniciar"
 restartDescription = "¿Has probado de apagar y volver a encender"
-secondaryActions = "¿Sigues sin poder resolver tu problema? Aquí tienes otras opciones más contundentes"
+secondaryActions = "¿Sigues sin poder resolver tu problema? Aquí tienes otras opciones más contundentes."
 
 [help.restart]
 cancel = "Cancelar"
 confirm = "¡Sí, reinicia!"
-description = "Bajo circunstancias normales no debería ser necesario reiniciar. Por favor, considera la posibilidad de reportar un error si necesitas reiniciar evcc regularmente."
+description = "En circunstancias normales no debería ser necesario reiniciar. Por favor, considera la posibilidad de presentar un error si necesita reiniciar evcc de forma regular."
 disclaimer = "Nota: evcc terminará y dependerá del sistema operativo para reiniciar el servicio"
 modalTitle = "¿Seguro que quieres volver a empezar?"
 
diff --git a/package-lock.json b/package-lock.json
index 2bd8dcf4..852c27d2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3789,9 +3789,9 @@
       }
     },
     "node_modules/editorconfig/node_modules/semver": {
-      "version": "5.7.1",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
-      "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
+      "version": "5.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
+      "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
       "bin": {
         "semver": "bin/semver"
       }
@@ -6986,9 +6986,9 @@
       }
     },
     "node_modules/semver": {
-      "version": "6.3.0",
-      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
-      "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
+      "version": "6.3.1",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+      "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
       "bin": {
         "semver": "bin/semver.js"
       }
diff --git a/templates/definition/meter/mtec-eb-gen2.yaml b/templates/definition/meter/mtec-eb-gen2.yaml
new file mode 100644
index 00000000..5834ac8d
--- /dev/null
+++ b/templates/definition/meter/mtec-eb-gen2.yaml
@@ -0,0 +1,47 @@
+template: mtec-eb-gen2
+products:
+  - brand: M-TEC
+    description:
+      generic: Energy Butler GEN2
+params:
+  - name: usage
+    choice: ["grid", "pv", "battery"]
+  - name: modbus
+    choice: ["rs485"]
+render: |
+  type: custom
+  {{- if eq .usage "grid" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 11000 # Zähler (NVP) Leistung
+      type: holding
+      decode: int32
+  {{- end }}
+  {{- if eq .usage "pv" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 11028 # PV Leistung
+      type: holding
+      decode: uint32
+  {{- end }}
+  {{- if eq .usage "battery" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 40258 # Batterie Leistung
+      type: holding
+      decode: int32
+  soc:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 43000 # State of Charge (SOC)
+      type: holding
+      decode: uint16
+      scale: 0.01
+  {{- end }}
diff --git a/templates/definition/meter/mtec-eb-gen3.yaml b/templates/definition/meter/mtec-eb-gen3.yaml
new file mode 100644
index 00000000..5d2fde70
--- /dev/null
+++ b/templates/definition/meter/mtec-eb-gen3.yaml
@@ -0,0 +1,47 @@
+template: mtec-eb-gen3
+products:
+  - brand: M-TEC
+    description:
+      generic: Energy Butler GEN3
+params:
+  - name: usage
+    choice: ["grid", "pv", "battery"]
+  - name: modbus
+    choice: ["rs485"]
+render: |
+  type: custom
+  {{- if eq .usage "grid" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 11000 # Zähler (NVP) Leistung
+      type: holding
+      decode: int32
+  {{- end }}
+  {{- if eq .usage "pv" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 11028 # PV Leistung
+      type: holding
+      decode: uint32
+  {{- end }}
+  {{- if eq .usage "battery" }}
+  power:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 30258 # Batterie Leistung
+      type: holding
+      decode: int32
+  soc:
+    source: modbus
+    {{- include "modbus" . | indent 2 }}
+    register:
+      address: 33000 # State of Charge (SOC)
+      type: holding
+      decode: uint16
+      scale: 0.01
+  {{- end }}
diff --git a/templates/definition/vehicle/offline.yaml b/templates/definition/vehicle/offline.yaml
index 31ae4e9a..9c7d9f17 100644
--- a/templates/definition/vehicle/offline.yaml
+++ b/templates/definition/vehicle/offline.yaml
@@ -11,15 +11,6 @@ params:
   - name: capacity
   - name: phases
     advanced: true
-  - name: mode
-    advanced: true
-  - name: minCurrent
-    advanced: true
-  - name: maxCurrent
-    advanced: true
-  - name: identifiers
-    advanced: true
-    type: stringlist
   - preset: vehicle-identify
 render: |
   type: custom
@@ -37,21 +28,4 @@ render: |
   soc:
     source: const
     value: 0
-  onIdentify:
-  {{- if .mode }}
-    mode: {{ .mode }}
-  {{- end }}
-    minSoc: 0 # fixed - no soc available
-    targetSoc: 100 # fixed - no soc available
-  {{- if .minCurrent }}
-    minCurrent: {{ .minCurrent }}
-  {{- end }}
-  {{- if .maxCurrent }}
-    maxCurrent: {{ .maxCurrent }}
-  {{- end }}
-  {{- if (len .identifiers) }}
-  identifiers:
-  {{-   range .identifiers }}
-  - {{ . }}
-  {{-   end }}
-  {{- end }}
+  {{ include "vehicle-identify" . }}
diff --git a/templates/definition/vehicle/seat-cupra.yaml b/templates/definition/vehicle/seat-cupra.yaml
index 4e747b76..3ebd497e 100644
--- a/templates/definition/vehicle/seat-cupra.yaml
+++ b/templates/definition/vehicle/seat-cupra.yaml
@@ -2,7 +2,7 @@ template: cupra
 products:
   - brand: Seat
     description:
-      generic: Cupra Born, My Cupra
+      generic: CupraConnect Gen4 (Born)
 params:
   - preset: vehicle-base
   - preset: vehicle-identify
diff --git a/templates/definition/vehicle/seat.yaml b/templates/definition/vehicle/seat.yaml
index 62b99403..c96cc2b0 100644
--- a/templates/definition/vehicle/seat.yaml
+++ b/templates/definition/vehicle/seat.yaml
@@ -2,8 +2,7 @@ template: seat
 products:
   - brand: Seat
     description:
-      de: Andere
-      en: Others
+      generic: CupraConnect Gen3 (Ateca, Leon, Formentor)
 params:
   - preset: vehicle-base
   - preset: vehicle-identify
diff --git a/templates/docs/meter/mtec-eb-gen2_0.yaml b/templates/docs/meter/mtec-eb-gen2_0.yaml
new file mode 100644
index 00000000..5f76fb89
--- /dev/null
+++ b/templates/docs/meter/mtec-eb-gen2_0.yaml
@@ -0,0 +1,58 @@
+product:
+  brand: M-TEC
+  description: Energy Butler GEN2
+render:
+  - usage: grid
+    default: |
+      type: template
+      template: mtec-eb-gen2
+      usage: grid
+
+      # RS485 via adapter (Modbus RTU)
+      modbus: rs485serial
+      id: 1
+      device: /dev/ttyUSB0 # USB-RS485 Adapter Adresse
+      baudrate: 9600 # Prüfe die Geräteeinstellungen, typische Werte sind 9600, 19200, 38400, 57600, 115200
+      comset: "8N1" # Kommunikationsparameter für den Adapter
+
+      # RS485 via TCP/IP (Modbus RTU)
+      modbus: rs485tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 502 # Port
+  - usage: pv
+    default: |
+      type: template
+      template: mtec-eb-gen2
+      usage: pv
+
+      # RS485 via adapter (Modbus RTU)
+      modbus: rs485serial
+      id: 1
+      device: /dev/ttyUSB0 # USB-RS485 Adapter Adresse
+      baudrate: 9600 # Prüfe die Geräteeinstellungen, typische Werte sind 9600, 19200, 38400, 57600, 115200
+      comset: "8N1" # Kommunikationsparameter für den Adapter
+
+      # RS485 via TCP/IP (Modbus RTU)
+      modbus: rs485tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 502 # Port
+  - usage: battery
+    default: |
+      type: template
+      template: mtec-eb-gen2
+      usage: battery
+
+      # RS485 via adapter (Modbus RTU)
+      modbus: rs485serial
+      id: 1
+      device: /dev/ttyUSB0 # USB-RS485 Adapter Adresse
+      baudrate: 9600 # Prüfe die Geräteeinstellungen, typische Werte sind 9600, 19200, 38400, 57600, 115200
+      comset: "8N1" # Kommunikationsparameter für den Adapter
+
+      # RS485 via TCP/IP (Modbus RTU)
+      modbus: rs485tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 502 # Port
diff --git a/templates/docs/meter/mtec-eb-gen3_0.yaml b/templates/docs/meter/mtec-eb-gen3_0.yaml
new file mode 100644
index 00000000..850d6ee2
--- /dev/null
+++ b/templates/docs/meter/mtec-eb-gen3_0.yaml
@@ -0,0 +1,58 @@
+product:
+  brand: M-TEC
+  description: Energy Butler GEN3
+render:
+  - usage: grid
+    default: |
+      type: template
+      template: mtec-eb-gen3
+      usage: grid
+
+      # RS485 via adapter (Modbus RTU)
+      modbus: rs485serial
+      id: 1
+      device: /dev/ttyUSB0 # USB-RS485 Adapter Adresse
+      baudrate: 9600 # Prüfe die Geräteeinstellungen, typische Werte sind 9600, 19200, 38400, 57600, 115200
+      comset: "8N1" # Kommunikationsparameter für den Adapter
+
+      # RS485 via TCP/IP (Modbus RTU)
+      modbus: rs485tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 502 # Port
+  - usage: pv
+    default: |
+      type: template
+      template: mtec-eb-gen3
+      usage: pv
+
+      # RS485 via adapter (Modbus RTU)
+      modbus: rs485serial
+      id: 1
+      device: /dev/ttyUSB0 # USB-RS485 Adapter Adresse
+      baudrate: 9600 # Prüfe die Geräteeinstellungen, typische Werte sind 9600, 19200, 38400, 57600, 115200
+      comset: "8N1" # Kommunikationsparameter für den Adapter
+
+      # RS485 via TCP/IP (Modbus RTU)
+      modbus: rs485tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 502 # Port
+  - usage: battery
+    default: |
+      type: template
+      template: mtec-eb-gen3
+      usage: battery
+
+      # RS485 via adapter (Modbus RTU)
+      modbus: rs485serial
+      id: 1
+      device: /dev/ttyUSB0 # USB-RS485 Adapter Adresse
+      baudrate: 9600 # Prüfe die Geräteeinstellungen, typische Werte sind 9600, 19200, 38400, 57600, 115200
+      comset: "8N1" # Kommunikationsparameter für den Adapter
+
+      # RS485 via TCP/IP (Modbus RTU)
+      modbus: rs485tcpip
+      id: 1
+      host: 192.0.2.2 # Hostname
+      port: 502 # Port
diff --git a/templates/docs/vehicle/cupra_0.yaml b/templates/docs/vehicle/cupra_0.yaml
index b577cb02..c5997bf2 100644
--- a/templates/docs/vehicle/cupra_0.yaml
+++ b/templates/docs/vehicle/cupra_0.yaml
@@ -1,6 +1,6 @@
 product:
   brand: Seat
-  description: Cupra Born, My Cupra
+  description: CupraConnect Gen4 (Born)
 render:
   - default: |
       type: template
diff --git a/templates/docs/vehicle/offline_0.yaml b/templates/docs/vehicle/offline_0.yaml
index d97bed53..407e748f 100644
--- a/templates/docs/vehicle/offline_0.yaml
+++ b/templates/docs/vehicle/offline_0.yaml
@@ -15,10 +15,6 @@ render:
       capacity: 50 # Akkukapazität in kWh (Optional)
       phases: 3 # Die maximale Anzahl der Phasen welche genutzt werden können (Optional)
       mode: # Möglich sind Off, Now, MinPV und PV, oder leer wenn keiner definiert werden soll (Optional)
-      minCurrent: 6 # Definiert die minimale Stromstärke pro angeschlossener Phase mit welcher das Fahrzeug geladen werden soll (Optional)
-      maxCurrent: 16 # Definiert die maximale Stromstärke pro angeschlossener Phase mit welcher das Fahrzeug geladen werden soll (Optional)
-      identifiers: # Kann meist erst später eingetragen werden, siehe: https://docs.evcc.io/docs/guides/vehicles/#erkennung-des-fahrzeugs-an-der-wallbox (Optional)
-      mode: # Möglich sind Off, Now, MinPV und PV, oder leer wenn keiner definiert werden soll (Optional)
       minSoc: 25 # Ladung mit maximaler Geschwindigkeit bis zu dem angegeben Ladestand unabhängig PV-Erzeugung, wenn der Lademodus nicht auf 'Aus' steht (Optional)
       targetSoc: 80 # Bis zu welchem Ladestand (Soc) soll das Fahrzeug geladen werden (Optional)
       minCurrent: 6 # Definiert die minimale Stromstärke pro angeschlossener Phase mit welcher das Fahrzeug geladen werden soll (Optional)
diff --git a/templates/docs/vehicle/seat_0.yaml b/templates/docs/vehicle/seat_0.yaml
index d82ee71e..2fbbaade 100644
--- a/templates/docs/vehicle/seat_0.yaml
+++ b/templates/docs/vehicle/seat_0.yaml
@@ -1,6 +1,6 @@
 product:
   brand: Seat
-  description: Andere
+  description: CupraConnect Gen3 (Ateca, Leon, Formentor)
 render:
   - default: |
       type: template
diff --git a/templates/evcc.io/brands.json b/templates/evcc.io/brands.json
index 54666919..ed5dc7fe 100644
--- a/templates/evcc.io/brands.json
+++ b/templates/evcc.io/brands.json
@@ -97,6 +97,7 @@
   "Janitza",
   "Kostal",
   "LG",
+  "M-TEC",
   "my-PV",
   "myStrom",
   "OpenEMS",
@@ -147,6 +148,7 @@
   "IGEN Tech",
   "Kostal",
   "LG",
+  "M-TEC",
   "myStrom",
   "OpenEMS",
   "Powerfox",


Test patch:
diff --git a/core/prioritizer/prioritizer_test.go b/core/prioritizer/prioritizer_test.go
index f3d322c6..cb533718 100644
--- a/core/prioritizer/prioritizer_test.go
+++ b/core/prioritizer/prioritizer_test.go
@@ -11,13 +11,15 @@ import (
 func TestPrioritzer(t *testing.T) {
 	ctrl := gomock.NewController(t)
 
-	p := New()
+	p := New(nil)
 
 	lo := loadpoint.NewMockAPI(ctrl)
-	lo.EXPECT().Priority().Return(0).AnyTimes()
+	lo.EXPECT().Title().AnyTimes()
+	lo.EXPECT().GetPriority().Return(0).AnyTimes()
 
 	hi := loadpoint.NewMockAPI(ctrl)
-	hi.EXPECT().Priority().Return(1).AnyTimes()
+	hi.EXPECT().Title().AnyTimes()
+	hi.EXPECT().GetPriority().Return(1).AnyTimes()
 
 	// no additional power available
 	lo.EXPECT().GetChargePowerFlexibility().Return(300.0)