Repository: evcc-io/evcc
Pull request #4207 (2022-08-23 16:55:12)

Add support for Elli chargers
- Generic template schema fixes
- Added multiple workarounds for Elli charger specifics (define IP address in the configuration, use of `hasMeter` and `chargedEnergy` in the Elli templated marking lacking support of those)
- Add EEBUS template presets, one with meter and one without
- Move EEBUS `ski` definition to default params
- Add `ip` to default params
- Add 2 Elli templates, one for Connect (without meter) and one for Connect (with meter) and Pro
- Set the chargers voltage per charger. Currently hard coded, in the future this should be taken from the `site`
- Fixed excessive polling of external meters, will be fetched every 10s (could be improved by coupling this to the `site` `interval`)
- Fixed check for detecting if the EV is charging
- Fixed issue with self consumption handling
- Use evcc provided `machineid`/`plantid` when available
- Added patching of go library to accept buggy Elli EEBUS certificate


DerAndereAndi: Mein Ziel ist hier dass es funktioniert. Die EEBUS Implementierung und das momentane "Interface" sind für mich ein dead end. Möchte hier nicht mehr Zeit als notwendig einsetzen.
DerAndereAndi: Der Elli Charger Connect hat kein eingebautes Messgerät. Man kann dies selbst nachrüsten, dafür gibt es in Foren eine Anleitung. Der Benefit ist dass gemessene Stromstärken pro Phase gemeldet werden. Die Wallbox selbst liefert ansonsten einfach die vorgegebene Stromstärke zurück. Momentan wird über das Template das jedoch deaktiviert und der Anwender sieht keine Ladeleistung. Er könnte "einfach" das andere Template nehmen und würde dann Werte sehen.

Die Wallboxen sind alle installiert und ob der Besitzer jetzt noch einen Modbus Zähler mit mehr Daten einsetzt um dann auch Leistung und geladene Energiemenge zu erhalten, ist wohl nicht sehr wahrscheinlich. Die meisten Anwender sind keine DIY und haben wohl mit der Einrichtung von evcc schon genug Schwierigkeiten vor sich.

Meine Frage wäre nun: sollte man das im Template der Connect nicht einfach auch "erlauben", dass die theoretischen Werte geliefert werden?

@andig @premultiply was meint ihr?
andig: Auf keinen Fall falls sie das nicht phasenrichtig macht.  
DerAndereAndi: Was meinst du damit? Es werden pro Phase die Stromstärke zurückgegeben, die als Limit gesetzt ist. Es ist ein theoretischer Wert.  Wenn das nicht erwünscht ist, bleibt es dann so wie es ist. Aber wie gesagt, der Anwender kann einfach das andere Template wählen und dann passiert genau das beschriebene. Es ist nicht möglich festzustellen ob da ein richtiger Zähler eingebaut ist oder nicht.
andig: Wir verwenden die Ströme zur Phasenerkennung für den PV Modus. Die müssen stimmen, wenns nur L1 gibt darf auch nur auf L1 was kommen sonst funktioniert die Erkennung nicht. 
premultiply: Genau wie @andig es beschrieben hat. Auch wenn es auf den ersten Blick so aussieht wie dass was evcc intern dann auch macht.
Aber der entscheidende Unterschied ist, dass evcc in dem Fall dann glaubt, dass die übermittelten Phasenströme echt sind und damit die wichtige Phasenstromerkennung getäuscht wird und dann die ganze Regelung usw. nicht mehr richtig funktioniert.
andig: Bzgl. ASN: jetzt fehlt noch Docker?
DerAndereAndi: Wieso? Der läuft als Teil des GitHub Workflows, und da werden die Jobs in `default.yaml` durchgeführt in der go installiert und dann gepatched wird.
andig: > Wieso? Der läuft als Teil des GitHub Workflows, und da werden die Jobs in `default.yaml` durchgeführt in der go installiert und dann gepatched wird.

Nein. Der Docker Build läuft in einem Docker Container.
andig: Nämlich hier: https://github.com/evcc-io/evcc/blob/master/Dockerfile#L23
DerAndereAndi: Ok, was genau soll da jetzt wo rein? Ich kenne diesen Teil nicht.
andig: > Ok, was genau soll da jetzt wo rein?

Done

Patch:
diff --git a/.github/workflows/default.yml b/.github/workflows/default.yml
index 24e24e5a..e3d1e2b7 100644
--- a/.github/workflows/default.yml
+++ b/.github/workflows/default.yml
@@ -30,6 +30,9 @@ jobs:
       - name: Porcelain
         run: make porcelain
 
+      - name: Patch ASN1
+        run: sudo make patch-asn1
+
   build:
     name: Build
     runs-on: ubuntu-latest
@@ -43,6 +46,9 @@ jobs:
           cache: true
         id: go
 
+      - name: Patch ASN1
+        run: sudo make patch-asn1
+
       - name: Build
         run: make build
 
diff --git a/Dockerfile b/Dockerfile
index 1276b95e..49bebb5f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,22 +1,22 @@
-# STEP 1 build ui
-FROM --platform=$BUILDPLATFORM node:16-alpine as node
+# # STEP 1 build ui
+# FROM --platform=$BUILDPLATFORM node:16-alpine as node
 
-RUN apk update && apk add --no-cache make alpine-sdk
+# RUN apk update && apk add --no-cache make alpine-sdk
 
-WORKDIR /build
+# WORKDIR /build
 
-# install node tools
-COPY package*.json ./
-RUN npm ci
+# # install node tools
+# COPY package*.json ./
+# RUN npm ci
 
-# build ui
-COPY Makefile .
-COPY assets assets
-COPY vite.config.js vite.config.js
-COPY .eslintrc.js .eslintrc.js
-COPY postcss.config.js postcss.config.js
+# # build ui
+# COPY Makefile .
+# COPY assets assets
+# COPY vite.config.js vite.config.js
+# COPY .eslintrc.js .eslintrc.js
+# COPY postcss.config.js postcss.config.js
 
-RUN make clean ui
+# RUN make clean ui
 
 
 # STEP 2 build executable binary
@@ -42,10 +42,11 @@ RUN go mod download
 
 # prepare
 COPY . .
+RUN make patch-asn1
 RUN make assets
 
 # copy ui
-COPY --from=node /build/dist /build/dist
+# COPY --from=node /build/dist /build/dist
 
 # build
 ARG TARGETOS
diff --git a/Makefile b/Makefile
index bbb67c9a..0deeab0f 100644
--- a/Makefile
+++ b/Makefile
@@ -29,6 +29,12 @@ IMAGE_OPTIONS := -hostname evcc -http_port 8080 github.com/gokrazy/serial-busybo
 # deb
 PACKAGES = ./release
 
+# patch asn1.go
+CURRENT_DIR := $(shell pwd)
+GO_ROOT := $(shell go env GOROOT)
+ASN1_PATH := $(GO_ROOT)/src/vendor/golang.org/x/crypto/cryptobyte
+ASN1_FILE := $(ASN1_PATH)/asn1.go
+
 default: build
 
 all: clean install install-ui ui assets lint test-ui lint-ui test build
@@ -129,3 +135,7 @@ image-update:
 soc:
 	@echo Version: $(VERSION) $(SHA) $(BUILD_DATE)
 	go build $(BUILD_TAGS) $(BUILD_ARGS) github.com/evcc-io/evcc/cmd/soc
+
+# patch asn1.go to allow Elli buggy certificates to be accepted with EEBUS
+patch-asn1:
+	test -e $(ASN1_FILE) && patch -N -t -d $(ASN1_PATH) -i $(CURRENT_DIR)/patch/asn1.diff || true
diff --git a/charger/eebus.go b/charger/eebus.go
index 0403c241..93eca20d 100644
--- a/charger/eebus.go
+++ b/charger/eebus.go
@@ -32,6 +32,9 @@ type EEBus struct {
 	connected           bool
 	expectedEnableState bool
 
+	lastIsChargingCheck  time.Time
+	lastIsChargingResult bool
+
 	evConnectedTime time.Time
 }
 
@@ -42,18 +45,25 @@ func init() {
 // NewEEBusFromConfig creates an EEBus charger from generic config
 func NewEEBusFromConfig(other map[string]interface{}) (api.Charger, error) {
 	cc := struct {
-		Ski string
-	}{}
+		Ski           string
+		Ip            string
+		Meter         bool
+		ChargedEnergy bool
+	}{
+		ChargedEnergy: true,
+	}
 
 	if err := util.DecodeOther(other, &cc); err != nil {
 		return nil, err
 	}
 
-	return NewEEBus(cc.Ski)
+	return NewEEBus(cc.Ski, cc.Ip, cc.Meter, cc.ChargedEnergy)
 }
 
+//go:generate go run ../cmd/tools/decorate.go -f decorateEEBus -b *EEBus -r api.Charger -t "api.Meter,CurrentPower,func() (float64, error)" -t "api.MeterCurrent,Currents,func() (float64, float64, float64, error)" -t "api.ChargeRater,ChargedEnergy,func() (float64, error)"
+
 // NewEEBus creates EEBus charger
-func NewEEBus(ski string) (*EEBus, error) {
+func NewEEBus(ski, ip string, hasMeter, hasChargedEnergy bool) (api.Charger, error) {
 	log := util.NewLogger("eebus")
 
 	if server.EEBusInstance == nil {
@@ -65,7 +75,15 @@ func NewEEBus(ski string) (*EEBus, error) {
 		communicationStandard: communication.EVCommunicationStandardEnumTypeUnknown,
 	}
 
-	server.EEBusInstance.Register(ski, c.onConnect, c.onDisconnect)
+	server.EEBusInstance.Register(ski, ip, c.onConnect, c.onDisconnect)
+
+	if hasMeter {
+		if hasChargedEnergy {
+			return decorateEEBus(c, c.currentPower, c.currents, c.chargedEnergy), nil
+		} else {
+			return decorateEEBus(c, c.currentPower, c.currents, nil), nil
+		}
+	}
 
 	return c, nil
 }
@@ -76,6 +94,7 @@ func (c *EEBus) onConnect(ski string, conn ship.Conn) error {
 	eebusDevice := app.HEMS(server.EEBusInstance.DeviceInfo())
 	c.cc = communication.NewConnectionController(c.log.TRACE, conn, eebusDevice)
 	c.cc.SetDataUpdateHandler(c.dataUpdateHandler)
+	c.cc.Voltage = 230.0 // TODO value should be provided from site
 
 	c.setDefaultValues()
 	c.setConnected(true)
@@ -97,6 +116,8 @@ func (c *EEBus) setDefaultValues() {
 	c.communicationStandard = communication.EVCommunicationStandardEnumTypeUnknown
 	c.socSupportAvailable = false
 	c.selfConsumptionSupportAvailable = false
+	c.lastIsChargingCheck = time.Now().Add(-time.Hour * 1)
+	c.lastIsChargingResult = false
 }
 
 func (c *EEBus) setConnected(connected bool) {
@@ -124,10 +145,6 @@ func (c *EEBus) setLoadpointMinMaxLimits(data *communication.EVSEClientDataType)
 	if c.lp.GetMaxCurrent() != newMax && newMax > 0 {
 		c.lp.SetMaxCurrent(newMax)
 	}
-
-	if err := c.lp.SetPhases(int(data.EVData.ConnectedPhases)); err != nil {
-		c.log.ERROR.Printf("!! cannot set %dp", data.EVData.ConnectedPhases)
-	}
 }
 
 func (c *EEBus) showCurrentChargingSetup() {
@@ -142,20 +159,17 @@ func (c *EEBus) showCurrentChargingSetup() {
 
 	if prevComStandard != data.EVData.CommunicationStandard {
 		c.communicationStandard = data.EVData.CommunicationStandard
-		timestamp := time.Now()
-		c.log.WARN.Println("!! ", timestamp.Format("2006-01-02 15:04:05"), " ev-charger-communication changed from ", prevComStandard, " to ", data.EVData.CommunicationStandard)
+		c.log.TRACE.Println("ev-charger-communication changed from ", prevComStandard, " to ", data.EVData.CommunicationStandard)
 	}
 
 	if prevSoCSupport != data.EVData.UCSoCAvailable {
 		c.socSupportAvailable = data.EVData.UCSoCAvailable
-		timestamp := time.Now()
-		c.log.WARN.Println("!! ", timestamp.Format("2006-01-02 15:04:05"), " ev-charger-soc support changed from ", prevSoCSupport, " to ", data.EVData.UCSoCAvailable)
+		c.log.TRACE.Println("ev-charger-soc support changed from ", prevSoCSupport, " to ", data.EVData.UCSoCAvailable)
 	}
 
 	if prevSelfConsumptionSupport != data.EVData.UCSelfConsumptionAvailable {
 		c.selfConsumptionSupportAvailable = data.EVData.UCSelfConsumptionAvailable
-		timestamp := time.Now()
-		c.log.WARN.Println("!! ", timestamp.Format("2006-01-02 15:04:05"), " ev-charger-self-consumption-support support changed from ", prevSelfConsumptionSupport, " to ", data.EVData.UCSelfConsumptionAvailable)
+		c.log.TRACE.Println("ev-charger-self-consumption-support support changed from ", prevSelfConsumptionSupport, " to ", data.EVData.UCSelfConsumptionAvailable)
 	}
 }
 
@@ -163,14 +177,17 @@ func (c *EEBus) dataUpdateHandler(dataType communication.EVDataElementUpdateType
 	// we receive data, so it is connected
 	c.setConnected(true)
 
+	prevSelfConsumptionSupport := c.selfConsumptionSupportAvailable
 	c.showCurrentChargingSetup()
 
 	switch dataType {
 	case communication.EVDataElementUpdateUseCaseSelfConsumption:
 		// if availability of self consumption use case changes, resend the current charging limit
-		err := c.writeCurrentLimitData([]float64{c.maxCurrent, c.maxCurrent, c.maxCurrent})
-		if err != nil {
-			c.log.ERROR.Println("failed to send current limit data: ", err)
+		// but only if the support value actually changed
+		if prevSelfConsumptionSupport != c.selfConsumptionSupportAvailable {
+			if err := c.writeCurrentLimitData([]float64{c.maxCurrent, c.maxCurrent, c.maxCurrent}); err != nil {
+				c.log.WARN.Println("failed to send current limit data: ", err)
+			}
 		}
 	// case communication.EVDataElementUpdateUseCaseSoC:
 	case communication.EVDataElementUpdateEVConnectionState:
@@ -188,7 +205,7 @@ func (c *EEBus) dataUpdateHandler(dataType communication.EVDataElementUpdateType
 	// case communication.EVDataElementUpdateChargingStrategy:
 	case communication.EVDataElementUpdateChargingPlanRequired:
 		if err := c.writeChargingPlan(); err != nil {
-			c.log.ERROR.Println("failed to send charging plan: ", err)
+			c.log.INFO.Println("failed to send charging plan: ", err)
 		}
 	case communication.EVDataElementUpdateConnectedPhases:
 		c.setLoadpointMinMaxLimits(data)
@@ -199,11 +216,39 @@ func (c *EEBus) dataUpdateHandler(dataType communication.EVDataElementUpdateType
 	}
 }
 
-// we assume that if any current power value of any phase is >50W, then charging is active and enabled is true
-func isCharging(d communication.EVDataType) bool {
-	return d.Measurements.Power[1] > d.Limits[1].Min*idleFactor ||
-		d.Measurements.Power[2] > d.Limits[2].Min*idleFactor ||
-		d.Measurements.Power[3] > d.Limits[3].Min*idleFactor
+// we assume that if any phase current value is > idleFactor * min Current, then charging is active and enabled is true
+func (c *EEBus) isCharging(d *communication.EVSEClientDataType) bool {
+	// check if an external physical meter is assigned
+	// we only want this for configured meters and not for internal meters!
+	// right now it works as expected
+	if c.lp != nil && c.lp.HasChargeMeter() {
+		// we only check ever 10 seconds, maybe we can use the config interval duration
+		timeDiff := time.Since(c.lastIsChargingCheck)
+		if timeDiff.Seconds() >= 10.0 {
+			c.lastIsChargingCheck = time.Now()
+			c.lastIsChargingResult = false
+			if c.lp.GetChargePower() > c.lp.GetMinPower()*idleFactor {
+				c.lastIsChargingResult = true
+				return true
+			}
+		} else if c.lastIsChargingResult {
+			return true
+		}
+	}
+
+	// The above doesn't (yet) work for built in meters, so check the EEBUS measurements also
+	var phase uint
+	for phase = 1; phase <= d.EVData.ConnectedPhases; phase++ {
+		if phaseCurrent, ok := d.EVData.Measurements.Current.Load(phase); ok {
+			if _, ok := phaseCurrent.(float64); ok {
+				if phaseCurrent.(float64) > d.EVData.Limits[phase].Min*idleFactor {
+					return true
+				}
+			}
+		}
+	}
+
+	return false
 }
 
 func (c *EEBus) updateState() (api.ChargeStatus, error) {
@@ -225,7 +270,7 @@ func (c *EEBus) updateState() (api.ChargeStatus, error) {
 	case communication.EVChargeStateEnumTypeFinished, communication.EVChargeStateEnumTypePaused: // Finished, Paused
 		return api.StatusB, nil
 	case communication.EVChargeStateEnumTypeActive: // Active
-		if isCharging(data.EVData) {
+		if c.isCharging(data) {
 			// we might already be enabled and charging due to connection issues
 			c.expectedEnableState = true
 			return api.StatusC, nil
@@ -458,7 +503,7 @@ func (c *EEBus) writeCurrentLimitData(currents []float64) error {
 
 	// set overload protection limits and self consumption limits to identical values
 	// so if the EV supports self consumption it will be used automatically
-	return c.cc.WriteCurrentLimitData(currents, currents, data.EVData)
+	return c.cc.WriteCurrentLimitData(currents, currents, &data.EVData)
 }
 
 // MaxCurrent implements the api.Charger interface
@@ -499,10 +544,8 @@ func (c *EEBus) MaxCurrentMillis(current float64) error {
 	return c.writeCurrentLimitData(currents)
 }
 
-var _ api.Meter = (*EEBus)(nil)
-
 // CurrentPower implements the api.Meter interface
-func (c *EEBus) CurrentPower() (float64, error) {
+func (c *EEBus) currentPower() (float64, error) {
 	data, err := c.cc.GetData()
 	if err != nil {
 		return 0, err
@@ -513,20 +556,19 @@ func (c *EEBus) CurrentPower() (float64, error) {
 	}
 
 	var power float64
-	var phase uint
-	for phase = 1; phase <= data.EVData.ConnectedPhases; phase++ {
-		if phasePower, ok := data.EVData.Measurements.Power[phase]; ok {
-			power += phasePower
+	for phase := uint(1); phase <= data.EVData.ConnectedPhases; phase++ {
+		if phasePower, ok := data.EVData.Measurements.Power.Load(phase); ok {
+			if _, ok := phasePower.(float64); ok {
+				power += phasePower.(float64)
+			}
 		}
 	}
 
 	return power, nil
 }
 
-var _ api.ChargeRater = (*EEBus)(nil)
-
 // ChargedEnergy implements the api.ChargeRater interface
-func (c *EEBus) ChargedEnergy() (float64, error) {
+func (c *EEBus) chargedEnergy() (float64, error) {
 	data, err := c.cc.GetData()
 	if err != nil {
 		return 0, err
@@ -541,23 +583,8 @@ func (c *EEBus) ChargedEnergy() (float64, error) {
 	return energy, nil
 }
 
-// var _ api.ChargeTimer = (*EEBus)(nil)
-
-// // ChargingTime implements the api.ChargeTimer interface
-// func (c *EEBus) ChargingTime() (time.Duration, error) {
-// 	// var currentSession MCCCurrentSession
-// 	// if err := mcc.getEscapedJSON(mcc.apiURL(mccAPICurrentSession), &currentSession); err != nil {
-// 	// 	return 0, err
-// 	// }
-
-// 	// return time.Duration(currentSession.Duration * time.Second), nil
-// 	return 0, nil
-// }
-
-var _ api.MeterCurrent = (*EEBus)(nil)
-
 // Currents implements the api.MeterCurrent interface
-func (c *EEBus) Currents() (float64, float64, float64, error) {
+func (c *EEBus) currents() (float64, float64, float64, error) {
 	data, err := c.cc.GetData()
 	if err != nil {
 		return 0, 0, 0, err
@@ -569,10 +596,12 @@ func (c *EEBus) Currents() (float64, float64, float64, error) {
 
 	var currents []float64
 
-	for phase := 1; phase <= 3; phase++ {
+	for phase := uint(1); phase <= 3; phase++ {
 		current := 0.0
-		if value, ok := data.EVData.Measurements.Current[uint(phase)]; ok {
-			current = value
+		if value, ok := data.EVData.Measurements.Current.Load(phase); ok {
+			if _, ok := value.(float64); ok {
+				current = value.(float64)
+			}
 		}
 		currents = append(currents, current)
 	}
diff --git a/charger/eebus_decorators.go b/charger/eebus_decorators.go
new file mode 100644
index 00000000..ab845db2
--- /dev/null
+++ b/charger/eebus_decorators.go
@@ -0,0 +1,137 @@
+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 decorateEEBus(base *EEBus, meter func() (float64, error), meterCurrent func() (float64, float64, float64, error), chargeRater func() (float64, error)) api.Charger {
+	switch {
+	case chargeRater == nil && meter == nil && meterCurrent == nil:
+		return base
+
+	case chargeRater == nil && meter != nil && meterCurrent == nil:
+		return &struct {
+			*EEBus
+			api.Meter
+		}{
+			EEBus: base,
+			Meter: &decorateEEBusMeterImpl{
+				meter: meter,
+			},
+		}
+
+	case chargeRater == nil && meter == nil && meterCurrent != nil:
+		return &struct {
+			*EEBus
+			api.MeterCurrent
+		}{
+			EEBus: base,
+			MeterCurrent: &decorateEEBusMeterCurrentImpl{
+				meterCurrent: meterCurrent,
+			},
+		}
+
+	case chargeRater == nil && meter != nil && meterCurrent != nil:
+		return &struct {
+			*EEBus
+			api.Meter
+			api.MeterCurrent
+		}{
+			EEBus: base,
+			Meter: &decorateEEBusMeterImpl{
+				meter: meter,
+			},
+			MeterCurrent: &decorateEEBusMeterCurrentImpl{
+				meterCurrent: meterCurrent,
+			},
+		}
+
+	case chargeRater != nil && meter == nil && meterCurrent == nil:
+		return &struct {
+			*EEBus
+			api.ChargeRater
+		}{
+			EEBus: base,
+			ChargeRater: &decorateEEBusChargeRaterImpl{
+				chargeRater: chargeRater,
+			},
+		}
+
+	case chargeRater != nil && meter != nil && meterCurrent == nil:
+		return &struct {
+			*EEBus
+			api.ChargeRater
+			api.Meter
+		}{
+			EEBus: base,
+			ChargeRater: &decorateEEBusChargeRaterImpl{
+				chargeRater: chargeRater,
+			},
+			Meter: &decorateEEBusMeterImpl{
+				meter: meter,
+			},
+		}
+
+	case chargeRater != nil && meter == nil && meterCurrent != nil:
+		return &struct {
+			*EEBus
+			api.ChargeRater
+			api.MeterCurrent
+		}{
+			EEBus: base,
+			ChargeRater: &decorateEEBusChargeRaterImpl{
+				chargeRater: chargeRater,
+			},
+			MeterCurrent: &decorateEEBusMeterCurrentImpl{
+				meterCurrent: meterCurrent,
+			},
+		}
+
+	case chargeRater != nil && meter != nil && meterCurrent != nil:
+		return &struct {
+			*EEBus
+			api.ChargeRater
+			api.Meter
+			api.MeterCurrent
+		}{
+			EEBus: base,
+			ChargeRater: &decorateEEBusChargeRaterImpl{
+				chargeRater: chargeRater,
+			},
+			Meter: &decorateEEBusMeterImpl{
+				meter: meter,
+			},
+			MeterCurrent: &decorateEEBusMeterCurrentImpl{
+				meterCurrent: meterCurrent,
+			},
+		}
+	}
+
+	return nil
+}
+
+type decorateEEBusChargeRaterImpl struct {
+	chargeRater func() (float64, error)
+}
+
+func (impl *decorateEEBusChargeRaterImpl) ChargedEnergy() (float64, error) {
+	return impl.chargeRater()
+}
+
+type decorateEEBusMeterImpl struct {
+	meter func() (float64, error)
+}
+
+func (impl *decorateEEBusMeterImpl) CurrentPower() (float64, error) {
+	return impl.meter()
+}
+
+type decorateEEBusMeterCurrentImpl struct {
+	meterCurrent func() (float64, float64, float64, error)
+}
+
+func (impl *decorateEEBusMeterCurrentImpl) Currents() (float64, float64, float64, error) {
+	return impl.meterCurrent()
+}
diff --git a/go.mod b/go.mod
index c9e6af2c..8a2261c5 100644
--- a/go.mod
+++ b/go.mod
@@ -25,7 +25,7 @@ require (
 	github.com/dylanmei/iso8601 v0.1.0
 	github.com/eclipse/paho.mqtt.golang v1.4.1
 	github.com/emirpasic/gods v1.18.1
-	github.com/evcc-io/eebus v0.0.0-20220815121332-bbb3cd4260e0
+	github.com/evcc-io/eebus v0.0.0-20220929204231-3e14a14e2b1c
 	github.com/fatih/structs v1.1.0
 	github.com/foogod/go-powerwall v0.2.0
 	github.com/go-ping/ping v1.1.0
diff --git a/go.sum b/go.sum
index 10abb86d..c793832b 100644
--- a/go.sum
+++ b/go.sum
@@ -224,8 +224,8 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.m
 github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
 github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/evcc-io/eebus v0.0.0-20220815121332-bbb3cd4260e0 h1:Hy0S1srOZXi/kd9gx5tdu5bFmCuz3GED9KkLvnOlYXU=
-github.com/evcc-io/eebus v0.0.0-20220815121332-bbb3cd4260e0/go.mod h1:7TpZYb1fTnptzT5zpq46bU6K2LI3bH+YckOmDtcHXyY=
+github.com/evcc-io/eebus v0.0.0-20220929204231-3e14a14e2b1c h1:WVQ0xywNGq+v+npAoHLNhN8Mb+ePD0t0JImCbplz3Wc=
+github.com/evcc-io/eebus v0.0.0-20220929204231-3e14a14e2b1c/go.mod h1:kkKB+GC+F1XOFdy68BCwRbLPuFUqAZP3UIk4Vhr/FYA=
 github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM=
diff --git a/patch/asn1.diff b/patch/asn1.diff
new file mode 100644
index 00000000..71b8571a
--- /dev/null
+++ b/patch/asn1.diff
@@ -0,0 +1,11 @@
+--- asn1.go	2022-09-15 13:21:17.000000000 +0200
++++ asn1_new.go	2022-09-15 13:22:12.000000000 +0200
+@@ -255,7 +255,7 @@
+ 	switch bytes[0] {
+ 	case 0:
+ 		*out = false
+-	case 0xff:
++	case 1, 0xff:
+ 		*out = true
+ 	default:
+ 		return false
diff --git a/server/eebus.go b/server/eebus.go
index 84666b9c..c1f6e9c2 100644
--- a/server/eebus.go
+++ b/server/eebus.go
@@ -6,6 +6,7 @@ import (
 	"crypto/x509"
 	"errors"
 	"fmt"
+	"net"
 	"strings"
 	"sync"
 
@@ -16,6 +17,7 @@ import (
 	"github.com/evcc-io/eebus/ship"
 	"github.com/evcc-io/eebus/spine/model"
 	"github.com/evcc-io/evcc/util"
+	"github.com/evcc-io/evcc/util/machine"
 	"github.com/libp2p/zeroconf/v2"
 )
 
@@ -38,6 +40,7 @@ type EEBus struct {
 	id                 string
 	zc                 *zeroconf.Server
 	clients            map[string]EEBusClientCBs
+	ipaddress          map[string]string
 	connectedClients   map[string]ship.Conn
 	discoveredClients  map[string]*zeroconf.ServiceEntry
 	clientInConnection map[string]bool
@@ -73,7 +76,12 @@ func NewEEBus(other map[string]interface{}) (*EEBus, error) {
 
 	if len(cc.ShipID) == 0 {
 		var err error
-		cc.ShipID, err = ship.UniqueID(details.BrandName, "evcc-eebus")
+		protectedID, err := machine.ProtectedID("evcc-eebus")
+		if err != nil {
+			return nil, err
+		}
+
+		cc.ShipID, err = ship.UniqueIDWithProtectedID(details.BrandName, protectedID)
 		if err != nil {
 			return nil, err
 		}
@@ -108,6 +116,7 @@ func NewEEBus(other map[string]interface{}) (*EEBus, error) {
 		srv:                srv,
 		id:                 cc.ShipID,
 		clients:            make(map[string]EEBusClientCBs),
+		ipaddress:          make(map[string]string),
 		connectedClients:   make(map[string]ship.Conn),
 		discoveredClients:  make(map[string]*zeroconf.ServiceEntry),
 		clientInConnection: make(map[string]bool),
@@ -120,7 +129,7 @@ func (c *EEBus) DeviceInfo() communication.ManufacturerDetails {
 	return EEBUSDetails
 }
 
-func (c *EEBus) Register(ski string, shipConnectHandler func(string, ship.Conn) error, shipDisconnectHandler func(string)) {
+func (c *EEBus) Register(ski, ip string, shipConnectHandler func(string, ship.Conn) error, shipDisconnectHandler func(string)) {
 	ski = strings.ReplaceAll(ski, "-", "")
 	ski = strings.ReplaceAll(ski, " ", "")
 	ski = strings.ToLower(ski)
@@ -128,6 +137,9 @@ func (c *EEBus) Register(ski string, shipConnectHandler func(string, ship.Conn)
 
 	c.mux.Lock()
 	c.clients[ski] = EEBusClientCBs{onConnect: shipConnectHandler, onDisconnect: shipDisconnectHandler}
+	if ip != "" {
+		c.ipaddress[ski] = ip
+	}
 	c.mux.Unlock()
 
 	// maybe the SKI is already discovered
@@ -200,6 +212,8 @@ func (c *EEBus) addDisoveredEntry(entry *zeroconf.ServiceEntry) {
 			return
 		}
 
+		c.patchMdnsEntryWithProvidedIP(svc.SKI, entry)
+
 		if entry.AddrIPv4 == nil && entry.AddrIPv6 == nil {
 			c.log.TRACE.Printf("Ignoring discovered mDNS entry as it has no IPv4 and IPv6 address: %s", entry.HostName)
 			return
@@ -245,7 +259,19 @@ func (c *EEBus) handleDiscoveredSKI(ski string) error {
 	return nil
 }
 
+// add the IP address from the charger configuration to the mDNS entry if it is missing
+func (c *EEBus) patchMdnsEntryWithProvidedIP(ski string, entry *zeroconf.ServiceEntry) {
+	address, exists := c.ipaddress[ski]
+	if entry.AddrIPv4 == nil && exists && address != "" {
+		if ip := net.ParseIP(address); ip != nil {
+			entry.AddrIPv4 = []net.IP{ip}
+		}
+	}
+}
+
 func (c *EEBus) connectDiscoveredEntry(ski string, entry *zeroconf.ServiceEntry) error {
+	c.patchMdnsEntryWithProvidedIP(ski, entry)
+
 	svc, err := mdns.NewFromDNSEntry(entry)
 
 	var conn ship.Conn
diff --git a/templates/definition/charger/eebus.yaml b/templates/definition/charger/eebus.yaml
index a085a9ff..f3a0ceeb 100644
--- a/templates/definition/charger/eebus.yaml
+++ b/templates/definition/charger/eebus.yaml
@@ -4,15 +4,10 @@ products:
       de: EEBUS kompatible Wallbox
       en: EEBUS compatible Wallbox
 group: generic
-capabilities: ["iso151182", "mA"]
+capabilities: ["mA"]
 requirements:
   evcc: ["eebus"]
 params:
-  - name: ski
-    required: true
-    help:
-      en: The SKI of the wallbox, can usually be found on the web interface of the wallbox
-      de: Die SKI der Wallbox, ist üblicherweise im Web Interface der Wallbox zu finden
+  - preset: eebusmeter
 render: |
-  type: eebus
-  ski: {{ .ski }}
+  {{ include "eebus-meter" . }}
diff --git a/templates/definition/charger/elli-charger-connect.yaml b/templates/definition/charger/elli-charger-connect.yaml
new file mode 100644
index 00000000..d748bfe0
--- /dev/null
+++ b/templates/definition/charger/elli-charger-connect.yaml
@@ -0,0 +1,24 @@
+template: elliconnect
+products:
+  - brand: Elli
+    description:
+      de: Charger Connect (ohne Meter) (Beta)
+      en: Charger Connect (without Meter) (Beta)
+capabilities: ["mA"]
+requirements:
+  evcc: ["eebus"]
+  description:
+    de: |
+      Dem Gerät muss eine feste IP Adresse zugewiesen sein (Manuell oder per DHCP).
+
+      Die Unterstützung ist im Beta Stadium und es kann noch zu Problemen kommen!
+    en: |
+      The device has to have a fix IP address (manuall or via DHCP).
+
+      The support is in beta state and problems can occur!
+params:
+  - preset: eebusnometer
+  - name: ip
+    required: true
+render: |
+  {{ include "eebus-no-meter" . }}
diff --git a/templates/definition/charger/elli-charger-pro.yaml b/templates/definition/charger/elli-charger-pro.yaml
new file mode 100644
index 00000000..b7132e2b
--- /dev/null
+++ b/templates/definition/charger/elli-charger-pro.yaml
@@ -0,0 +1,25 @@
+template: ellipro
+products:
+  - brand: Elli
+    description:
+      de: Charger Connect (mit Meter)/Pro (Beta)
+      en: Charger Connect (with Meter)/Pro (Beta)
+capabilities: ["mA"]
+requirements:
+  evcc: ["eebus"]
+  description:
+    de: |
+      Dem Gerät muss eine feste IP Adresse zugewiesen sein (Manuell oder per DHCP).
+
+      Die Unterstützung ist im Beta Stadium und es kann noch zu Problemen kommen!
+    en: |
+      The device has to have a fix IP address (manuall or via DHCP).
+
+      The support is in beta state and problems can occur!
+params:
+  - preset: eebusmeter
+  - name: ip
+    required: true
+render: |
+  {{ include "eebus-meter" . }}
+  chargedEnergy: false
diff --git a/templates/definition/charger/porsche-pmcc.yaml b/templates/definition/charger/porsche-pmcc.yaml
index 8691c832..d27525b3 100644
--- a/templates/definition/charger/porsche-pmcc.yaml
+++ b/templates/definition/charger/porsche-pmcc.yaml
@@ -3,15 +3,10 @@ products:
   - brand: Porsche
     description:
       generic: Mobile Charger Connect
-capabilities: ["iso151182"]
+capabilities: ["iso151182", "mA"]
 requirements:
   evcc: ["eebus"]
 params:
-  - name: ski
-    required: true
-    help:
-      en: The SKI of the wallbox, can usually be found on the web interface of the wallbox
-      de: Die SKI der Wallbox, ist üblicherweise im Web Interface der Wallbox zu finden
+  - preset: eebusmeter
 render: |
-  type: eebus
-  ski: {{ .ski }}
+  {{ include "eebus-meter" . }}
diff --git a/templates/definition/charger/porsche-pmcp.yaml b/templates/definition/charger/porsche-pmcp.yaml
index 6a1d1779..46d3768b 100644
--- a/templates/definition/charger/porsche-pmcp.yaml
+++ b/templates/definition/charger/porsche-pmcp.yaml
@@ -6,11 +6,6 @@ products:
 requirements:
   evcc: ["eebus"]
 params:
-  - name: ski
-    required: true
-    help:
-      en: The SKI of the wallbox, can usually be found on the web interface of the wallbox
-      de: Die SKI der Wallbox, ist üblicherweise im Web Interface der Wallbox zu finden
+  - preset: eebusmeter
 render: |
-  type: eebus
-  ski: {{ .ski }}
+  {{ include "eebus-meter" . }}
diff --git a/templates/definition/defaults-schema.json b/templates/definition/defaults-schema.json
index acd82e9b..a444cfd9 100644
--- a/templates/definition/defaults-schema.json
+++ b/templates/definition/defaults-schema.json
@@ -43,12 +43,16 @@
         },
         "sockets": {
           "$ref": "#/definitions/Generic"
+        },
+        "scooter": {
+          "$ref": "#/definitions/Generic"
         }
       },
       "required": [
         "generic",
         "sockets",
-        "switchsockets"
+        "switchsockets",
+        "scooter"
       ],
       "title": "Devicegroups"
     },
diff --git a/templates/definition/defaults.yaml b/templates/definition/defaults.yaml
index fe10b680..443bb5c8 100644
--- a/templates/definition/defaults.yaml
+++ b/templates/definition/defaults.yaml
@@ -45,6 +45,14 @@ params:
       de: IP-Adresse oder Hostname
       en: IP-Address or Hostname
     example: 192.0.2.2
+  - name: ip
+    description:
+      de: IP-Adresse
+      en: IP-Address
+    help:
+      de: IP-Adresse
+      en: IP-Address
+    example: 192.0.2.2
   - name: port
     description:
       de: Port
@@ -216,6 +224,11 @@ params:
       en: "'en' for English and 'de' for German"
     default: en
     validvalues: ["de", "en"]
+  - name: ski
+    required: true
+    help:
+      en: The SKI of the wallbox, can usually be found on the web interface of the wallbox
+      de: Die SKI der Wallbox, ist üblicherweise im Web Interface der Wallbox zu finden
 
 presets:
   vehiclebase:
@@ -301,6 +314,29 @@ presets:
       {{define "vehicle-language"}}
       language: {{ .language }}
       {{end}}
+  eebusmeter:
+    params:
+      - name: ski
+      - name: ip
+        required: false
+    render: |
+      {{define "eebus-meter"}}
+      type: eebus
+      ski: {{ .ski }}
+      {{ if ne .ip "" }}ip: {{ .ip }}{{ end }}
+      meter: true
+      {{end}}
+  eebusnometer:
+    params:
+      - name: ski
+      - name: ip
+        required: false
+    render: |
+      {{define "eebus-no-meter"}}
+      type: eebus
+      ski: {{ .ski }}
+      {{ if ne .ip "" }}ip: {{ .ip }}{{ end }}
+      {{end}}
 
 modbus:
   interfaces:
diff --git a/templates/definition/devices-schema.json b/templates/definition/devices-schema.json
index b0c5193e..4960463c 100644
--- a/templates/definition/devices-schema.json
+++ b/templates/definition/devices-schema.json
@@ -78,7 +78,7 @@
     },
     "Groups": {
       "type": "string",
-      "enum": ["generic", "switchsockets", "sockets"]
+      "enum": ["generic", "switchsockets", "scooter", "sockets"]
     },
     "GuidedSetup": {
       "type": "object",
@@ -120,7 +120,7 @@
     },
     "Capability": {
       "type": "string",
-      "enum": ["1p3p", "iso151182", "rfid", "smahems"]
+      "enum": ["1p3p", "iso151182", "rfid", "smahems", "mA"]
     }
   }
 }
\ No newline at end of file
diff --git a/templates/definition/meter/sungrow-hybrid.yaml b/templates/definition/meter/sungrow-hybrid.yaml
index c34f75b4..055a9637 100644
--- a/templates/definition/meter/sungrow-hybrid.yaml
+++ b/templates/definition/meter/sungrow-hybrid.yaml
@@ -34,7 +34,6 @@ render: |
       address: 13036 # Total Import Energy
       type: input
       decode: uint32s
-    scale: 0.1
   {{- end }}
   {{- if eq .usage "pv" }}
   power:
@@ -51,7 +50,6 @@ render: |
       address: 13002 # Total PV Generation
       type: input
       decode: uint32s
-    scale: 0.1
   {{- end }}
   {{- if eq .usage "battery" }}
   power:
@@ -87,7 +85,6 @@ render: |
       address: 13026 # Total battery discharge energy
       type: input
       decode: uint32s
-    scale: 0.1
   soc:
     source: modbus
     {{- include "modbus" . | indent 2 }}
diff --git a/templates/definition/meter/sungrow-inverter.yaml b/templates/definition/meter/sungrow-inverter.yaml
index b16fdaed..cdafc0fa 100644
--- a/templates/definition/meter/sungrow-inverter.yaml
+++ b/templates/definition/meter/sungrow-inverter.yaml
@@ -32,7 +32,6 @@ render: |
       address: 5098 # Total import energy
       type: input
       decode: uint32s
-    scale: 0.1
   {{- end }}
   {{- if eq .usage "pv" }}
   power:
@@ -49,5 +48,4 @@ render: |
       address: 5003 # Total power yields
       type: input
       decode: uint32s
-    scale: 0.1
   {{- end }}
diff --git a/templates/docs/charger/eebus_0.yaml b/templates/docs/charger/eebus_0.yaml
index a8d84a97..297d126f 100644
--- a/templates/docs/charger/eebus_0.yaml
+++ b/templates/docs/charger/eebus_0.yaml
@@ -1,10 +1,11 @@
 product:
   description: EEBUS kompatible Wallbox
   group: Generische Unterstützung
-capabilities: ["iso151182", "mA"]
+capabilities: ["mA"]
 requirements: ["eebus"]
 render:
   - default: |
       type: template
       template: eebus
       ski: # Die SKI der Wallbox, ist üblicherweise im Web Interface der Wallbox zu finden
+      ip: 192.0.2.2 # IP-Adresse # Optional
diff --git a/templates/docs/charger/elliconnect_0.yaml b/templates/docs/charger/elliconnect_0.yaml
new file mode 100644
index 00000000..7c75eb3b
--- /dev/null
+++ b/templates/docs/charger/elliconnect_0.yaml
@@ -0,0 +1,16 @@
+product:
+  brand: Elli
+  description: Charger Connect (ohne Meter) (Beta)
+capabilities: ["mA"]
+requirements: ["eebus"]
+description: |
+  Dem Gerät muss eine feste IP Adresse zugewiesen sein (Manuell oder per DHCP).
+
+  Die Unterstützung ist im Beta Stadium und es kann noch zu Problemen kommen!
+
+render:
+  - default: |
+      type: template
+      template: elliconnect
+      ski: # Die SKI der Wallbox, ist üblicherweise im Web Interface der Wallbox zu finden
+      ip: 192.0.2.2 # IP-Adresse
diff --git a/templates/docs/charger/ellipro_0.yaml b/templates/docs/charger/ellipro_0.yaml
new file mode 100644
index 00000000..91a24638
--- /dev/null
+++ b/templates/docs/charger/ellipro_0.yaml
@@ -0,0 +1,16 @@
+product:
+  brand: Elli
+  description: Charger Connect (mit Meter)/Pro (Beta)
+capabilities: ["mA"]
+requirements: ["eebus"]
+description: |
+  Dem Gerät muss eine feste IP Adresse zugewiesen sein (Manuell oder per DHCP).
+
+  Die Unterstützung ist im Beta Stadium und es kann noch zu Problemen kommen!
+
+render:
+  - default: |
+      type: template
+      template: ellipro
+      ski: # Die SKI der Wallbox, ist üblicherweise im Web Interface der Wallbox zu finden
+      ip: 192.0.2.2 # IP-Adresse
diff --git a/templates/docs/charger/pmcc_0.yaml b/templates/docs/charger/pmcc_0.yaml
index 44c6d60a..91f46329 100644
--- a/templates/docs/charger/pmcc_0.yaml
+++ b/templates/docs/charger/pmcc_0.yaml
@@ -1,10 +1,11 @@
 product:
   brand: Porsche
   description: Mobile Charger Connect
-capabilities: ["iso151182"]
+capabilities: ["iso151182", "mA"]
 requirements: ["eebus"]
 render:
   - default: |
       type: template
       template: pmcc
       ski: # Die SKI der Wallbox, ist üblicherweise im Web Interface der Wallbox zu finden
+      ip: 192.0.2.2 # IP-Adresse # Optional
diff --git a/templates/docs/charger/pmcp_0.yaml b/templates/docs/charger/pmcp_0.yaml
index f00cc79c..e3976e1c 100644
--- a/templates/docs/charger/pmcp_0.yaml
+++ b/templates/docs/charger/pmcp_0.yaml
@@ -7,3 +7,4 @@ render:
       type: template
       template: pmcp
       ski: # Die SKI der Wallbox, ist üblicherweise im Web Interface der Wallbox zu finden
+      ip: 192.0.2.2 # IP-Adresse # Optional
diff --git a/templates/evcc.io/brands.json b/templates/evcc.io/brands.json
index fa8554ba..4a0b2b08 100644
--- a/templates/evcc.io/brands.json
+++ b/templates/evcc.io/brands.json
@@ -12,6 +12,7 @@
   "Easee",
   "Ebee",
   "echarge",
+  "Elli",
   "Ensto",
   "Etrel",
   "Garo",


Test patch:
diff --git a/charger/eebus_test.go b/charger/eebus_test.go
new file mode 100644
index 00000000..f087f122
--- /dev/null
+++ b/charger/eebus_test.go
@@ -0,0 +1,136 @@
+package charger
+
+import (
+	"testing"
+
+	"github.com/evcc-io/eebus/communication"
+)
+
+type limitStruct struct {
+	phase           uint
+	min, max, pause float64
+}
+
+type measurementStruct struct {
+	phase   uint
+	current float64
+}
+
+type testMeasurementStruct struct {
+	expected bool
+	data     []measurementStruct
+}
+
+func TestEEBusIsCharging(t *testing.T) {
+	tests := []struct {
+		name         string
+		limits       []limitStruct
+		measurements []testMeasurementStruct
+	}{
+		{
+			"3 phase IEC",
+			[]limitStruct{
+				{1, 6.0, 16.0, 0.0},
+				{2, 6.0, 16.0, 0.0},
+				{3, 6.0, 16.0, 0.0},
+			},
+			[]testMeasurementStruct{
+				{
+					false,
+					[]measurementStruct{
+						{1, 0.0},
+						{2, 3.0},
+						{3, 0.0},
+					},
+				},
+				{
+					true,
+					[]measurementStruct{
+						{1, 6.0},
+						{2, 0.0},
+						{3, 1.0},
+					},
+				},
+			},
+		},
+		{
+			"1 phase IEC",
+			[]limitStruct{
+				{1, 6.0, 16.0, 0.0},
+			},
+			[]testMeasurementStruct{
+				{
+					false,
+					[]measurementStruct{
+						{1, 2.0},
+					},
+				},
+				{
+					true,
+					[]measurementStruct{
+						{1, 6.0},
+					},
+				},
+			},
+		},
+		{
+			"3 phase ISO",
+			[]limitStruct{
+				{1, 2.2, 16.0, 0.1},
+				{2, 2.2, 16.0, 0.1},
+				{3, 2.2, 16.0, 0.1},
+			},
+			[]testMeasurementStruct{
+				{
+					false,
+					[]measurementStruct{
+						{1, 1.0},
+						{2, 0.0},
+						{3, 0.0},
+					},
+				},
+				{
+					true,
+					[]measurementStruct{
+						{1, 1.8},
+						{2, 1.0},
+						{3, 3.0},
+					},
+				},
+			},
+		},
+	}
+
+	eebus := &EEBus{}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			data := &communication.EVSEClientDataType{
+				EVData: communication.EVDataType{
+					ConnectedPhases: 3,
+					Limits:          make(map[uint]communication.EVCurrentLimitType),
+					Measurements:    communication.EVMeasurementsType{},
+				},
+			}
+
+			for _, limit := range tc.limits {
+				data.EVData.Limits[limit.phase] = communication.EVCurrentLimitType{
+					Min:     limit.min,
+					Max:     limit.max,
+					Default: limit.pause,
+				}
+			}
+
+			for index, m := range tc.measurements {
+				for _, d := range m.data {
+					data.EVData.Measurements.Current.Store(d.phase, d.current)
+				}
+
+				result := eebus.isCharging(data)
+				if result != m.expected {
+					t.Errorf("Failure: test %s, series %d, expected %v, got %v", tc.name, index, m.expected, result)
+				}
+			}
+		})
+	}
+}