evcc icon indicating copy to clipboard operation
evcc copied to clipboard

feat: Add support for Lambda excess energy specified in negative numbers

Open anbie opened this issue 6 months ago • 8 comments

#20238

This PR introduces two enhancements to the lambda-zewotherm template: 1. Inverted Power Write for Negative Excess Mode Adds support for inverting the power value written via Modbus when operating in "negative excess" mode. 2. Customizable Modbus TCP Port Introduces a new port parameter, allowing users to override the default Modbus TCP port 502.

As noted in #20238, there is still an outstanding issue when using the "negative" mode: negative values are being overwritten with 0 during processing. Further investigation is needed.

anbie avatar Jun 21 '25 21:06 anbie

Wenn ich in die plugin/modbus.go ein Print mit einbaue sieht man, dass da schon nur noch -0 ankommt:

	return func(val float64) error {
		val *= m.scale

		fmt.Println(val)

		switch op.FuncCode {
		case gridx.FuncCodeWriteSingleCoil:
			var uval uint16
			if val != 0 {
				uval = 0xFF00
			}
			_, err = m.conn.WriteSingleCoil(op.Addr, uval)
			return err

Vmtl. passiert der Fehler vorher im Template.

andig avatar Jun 23 '25 08:06 andig

Gute Frage – ich bin leider auch noch nicht weitergekommen, was genau mit dem Wert passiert. Testweise habe ich die „-1“ im „negative“-Fall mal durch eine „2“ ersetzt. Damit konnte ich sehen, dass ein verdoppelter Wert durchgereicht wird – der Pfad scheint also grundsätzlich zu funktionieren. Bessere Debug-Möglichkeiten (z. B. Traces o. Ä.) habe ich bisher leider nicht gefunden. Ich bin aber noch am Lernen und weiter am Forschen.

anbie avatar Jun 23 '25 12:06 anbie

Da ich in Go nicht ganz zu Hause bin, habe ich etwas mit Trace-Statements in der writeFunc() experimentiert, um das Verhalten besser zu verstehen.

Was mir dabei aufgefallen ist:

  • Beim Anlegen bzw. Validieren der Lambda wird initial der Wert 0 geschrieben.
  • Erst wenn der Charger bereits existiert und tatsächlich „geladen“ wird, erscheinen reale Leistungswerte (Watt). Ich lade hier mit "fast" und sehe also grob 11kW.
  • Die modbus "payload" ist dann weiterhin 0x0000
	return func(val float64) error {

		fmt.Println("▶ writeFunc called")
		fmt.Println(" raw val:", val)


		fmt.Println(" m.scale:", m.scale)


		val *= m.scale
		fmt.Println(" scaled val:", val)
▶ writeFunc called
 raw val: 11040
 m.scale: -1
 scaled val: -11040
[db:1  ] TRACE 2025/06/23 23:46:33 modbus: send 00 31 00 00 00 09 01 10 00 66 00 01 02 00 00
[db:1  ] TRACE 2025/06/23 23:46:33 modbus: recv 00 31 00 00 00 06 01 10 00 66 00 01
[site  ] DEBUG 2025/06/23 23:46:49 ----

anbie avatar Jun 23 '25 21:06 anbie

Es sieht so aus, als würde encode Probleme haben mit negativen Werten. (bin erstmal busy für die nächsten Stunden - deswegen hier nur ein kurzer Zwischenbericht)

Trace Output

▶ writeFunc called
 raw val: 11040
 m.scale: -1
 scaled val: -11040
▶ func called
 v: 0
 b: [0 0]
Call to WriteMultipleRegisters
 b: [0 0]
 length 1
[db:1  ] TRACE 2025/06/24 08:46:02 modbus: send 00 0e 00 00 00 09 01 10 00 66 00 01 02 00 00
[db:1  ] TRACE 2025/06/24 08:46:02 modbus: recv 00 0e 00 00 00 06 01 10 00 66 00 01

modbus.go:

		fmt.Println("▶ writeFunc called")
		fmt.Println(" raw val:", val)

		fmt.Println(" m.scale:", m.scale)

		val *= m.scale
		fmt.Println(" scaled val:", val)
		[...]
		case gridx.FuncCodeWriteMultipleRegisters:
			b, err := encode(val)
			if err == nil {
				fmt.Println("Call to WriteMultipleRegisters")
				fmt.Println(" b:", b)
				fmt.Println(" length", op.Length)
				_, err = m.conn.WriteMultipleRegisters(op.Addr, op.Length, b)
			}
			return err

register.go:

	return func(f float64) ([]byte, error) {
		fmt.Println("▶ func called")
		v := fun(f)
		b := make([]byte, 2*length)
		fmt.Println(" v:", v)
		fmt.Println(" b:", b)

Wenn ich val = 5 mache, dann sieht das so aus:

▶ func called
 v: 5
 b: [0 0]
Call to WriteMultipleRegisters
 b: [0 5]
 length 1
[db:1  ] TRACE 2025/06/24 08:43:06 modbus: send 00 2c 00 00 00 09 01 10 00 66 00 01 02 00 05
[db:1  ] TRACE 2025/06/24 08:43:06 modbus: recv 00 2c 00 00 00 06 01 10 00 66 00 01

anbie avatar Jun 24 '25 06:06 anbie

Es liegt also an der übergebenen encode Funktion. Bzw. an der inneren fun()

andig avatar Jun 24 '25 07:06 andig

Ich habe in dem Zusammenhang einiges über Go dazugelernt – das Problem versteckt sich in util/modbus/register.go in der EncodeFunc():

func (r Register) EncodeFunc() (func(float64) ([]byte, error), error) {
	enc := strings.ToLower(r.encoding())

	switch {
	case strings.HasPrefix(enc, "bool"):
		fallthrough

	case strings.HasPrefix(enc, "int") || strings.HasPrefix(enc, "uint"):
		return r.encodeToBytes(func(v float64) uint64 {
			return uint64(v)
		})

Ich habe das mal ersetzt mit diesem Code, um auch negative Werte korrekt in die Lambda zu bekommen:

	case strings.HasPrefix(enc, "int16"):
		// get register length up front
		length, err := r.Length()
		if err != nil {
			return nil, err
		}

		return r.encodeToBytes(func(v float64) uint64 {
			// Round to nearest integer
			i := int64(math.Round(v))

			// Compute bit-width for int16
			bits := 16 * int(length)
			min := -(1 << (bits - 1))
			max := (1 << (bits - 1)) - 1

			// Two’s-complement mask for signed
			mask := uint64((1 << bits) - 1)
			return uint64(uint64(i) & mask)
		})

	case strings.HasPrefix(enc, "uint16"):
        ...

Vollständige Transparenz: Ich bin in Go nicht wirklich tief drin und habe mir bei der Umsetzung Unterstützung von ChatGTP geholt. Daher kann ich schwer einschätzen, ob der Ansatz hier „gut“ und elegant gelöst ist – Feedback, Anmerkungen oder Verbesserungsvorschläge sind sehr willkommen! 🙏

anbie avatar Jun 25 '25 11:06 anbie

Vorschlag für die EncodeFunc:

	case strings.HasPrefix(enc, "int"):
		return r.encodeToBytes(func(v float64) uint64 {
			return uint64(int64(v))
		})

	case strings.HasPrefix(enc, "uint"):
		return r.encodeToBytes(func(v float64) uint64 {
			return uint64(v)
		})

ob/wie das downscaling des Vorzeichens funktioniert bräuchte einen Unit Test, aber erstmal reichts das auszuprobieren ;)

andig avatar Jun 25 '25 18:06 andig

Hab Deinen Vorschlag eingebaut und kurzen Unit-Test gemacht: das sieht vielverspechend aus.

▶ func called
 f -11040
 fun() points to: github.com/evcc-io/evcc/util/modbus.Register.EncodeFunc.func1
 b: [0 0]
 Call to WriteMultipleRegisters
 b: [212 224]
[db:1  ] TRACE 2025/06/25 20:36:40 modbus: send 00 1a 00 00 00 09 01 10 00 66 00 01 02 d4 e0
[db:1  ] TRACE 2025/06/25 20:36:40 modbus: recv 00 1a 00 00 00 06 01 10 00 66 00 01

Die Data Payload, die zur Lambda Register 102 geschickt wird, ist 0xD4E0 -> "-11008".

anbie avatar Jun 25 '25 18:06 anbie