feat: Add support for Lambda excess energy specified in negative numbers
#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.
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.
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.
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 ----
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
Es liegt also an der übergebenen encode Funktion. Bzw. an der inneren fun()
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! 🙏
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 ;)
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".