From cc6bef6b82dca6d4b848cafbedd7666c29bc7080 Mon Sep 17 00:00:00 2001 From: premultiply <4681172+premultiply@users.noreply.github.com> Date: Sun, 19 Jul 2026 12:44:32 +0200 Subject: [PATCH] Modbus: handle NaN values (#31011) --- plugin/modbus.go | 8 +++++++- util/modbus/functions.go | 7 ++++--- util/modbus/register.go | 3 ++- util/modbus/register_test.go | 17 +++++++++++++---- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/plugin/modbus.go b/plugin/modbus.go index 9eae8a9e4..bfc78e616 100644 --- a/plugin/modbus.go +++ b/plugin/modbus.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/evcc-io/evcc/api" "github.com/evcc-io/evcc/util" "github.com/evcc-io/evcc/util/modbus" gridx "github.com/grid-x/modbus" @@ -156,7 +157,12 @@ func (m *Modbus) FloatGetter() (func() (f float64, err error), error) { return 0, fmt.Errorf("read failed: %w", err) } - return m.scale * decode(bytes), nil + // a "*nan" sentinel decodes to NaN - the device reports no valid value + if res := m.scale * decode(bytes); !math.IsNaN(res) { + return res, nil + } + + return 0, api.ErrNotAvailable }, nil } diff --git a/util/modbus/functions.go b/util/modbus/functions.go index 867ad8746..faea9066e 100644 --- a/util/modbus/functions.go +++ b/util/modbus/functions.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "errors" "fmt" + "math" "slices" "strconv" "strings" @@ -81,7 +82,7 @@ func decodeNaN16(f func(b []byte) float64, nan ...uint16) func(b []byte) float64 return func(b []byte) float64 { u := binary.BigEndian.Uint16(b) if slices.Contains(nan, u) { - return 0 + return math.NaN() } return f(b) } @@ -91,7 +92,7 @@ func decodeNaN32(f func(b []byte) float64, nan ...uint32) func(b []byte) float64 return func(b []byte) float64 { u := binary.BigEndian.Uint32(b) if slices.Contains(nan, u) { - return 0 + return math.NaN() } return f(b) } @@ -101,7 +102,7 @@ func decodeNaN64(f func(b []byte) float64, nan ...uint64) func(b []byte) float64 return func(b []byte) float64 { u := binary.BigEndian.Uint64(b) if slices.Contains(nan, u) { - return 0 + return math.NaN() } return f(b) } diff --git a/util/modbus/register.go b/util/modbus/register.go index 676fc2f82..4120b1209 100644 --- a/util/modbus/register.go +++ b/util/modbus/register.go @@ -249,7 +249,8 @@ func (r Register) Operation() (RegisterOperation, error) { func asFloat64[T constraints.Signed | constraints.Unsigned | constraints.Float](f func([]byte) T) func([]byte) float64 { return func(v []byte) float64 { res := float64(f(v)) - if math.IsNaN(res) || math.IsInf(res, 0) { + // keep NaN so callers can map it to "not available"; sanitize Inf to 0 + if math.IsInf(res, 0) { res = 0 } return res diff --git a/util/modbus/register_test.go b/util/modbus/register_test.go index a30740fba..ba1a030f4 100644 --- a/util/modbus/register_test.go +++ b/util/modbus/register_test.go @@ -71,10 +71,13 @@ func TestDecoding(t *testing.T) { out float64 }{ {Register{Decode: "float32"}, []byte{0x4b, 0x3c, 0x61, 0x4e}, 12345678}, - {Register{Decode: "float32"}, []byte{0xff, 0xff, 0xff, 0x7f}, 0}, // NaN + {Register{Decode: "float32"}, []byte{0xff, 0xff, 0xff, 0x7f}, math.NaN()}, // IEEE NaN preserved {Register{Decode: "float32s"}, []byte{0x61, 0x4e, 0x4b, 0x3c}, 12345678}, - {Register{Decode: "float32s"}, []byte{0xff, 0x7f, 0xff, 0xff}, 0}, // NaN swapped - {Register{Decode: "float32nans"}, []byte{0xff, 0xff, 0xff, 0x7f}, 0}, // NaN + {Register{Decode: "float32s"}, []byte{0xff, 0x7f, 0xff, 0xff}, math.NaN()}, // IEEE NaN swapped preserved + {Register{Decode: "float32nans"}, []byte{0xff, 0xff, 0xff, 0x7f}, math.NaN()}, + {Register{Decode: "uint16nan"}, []byte{0xff, 0xff}, math.NaN()}, + {Register{Decode: "uint32nan"}, []byte{0xff, 0xff, 0xff, 0xff}, math.NaN()}, + {Register{Decode: "uint64nan"}, []byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, math.NaN()}, {Register{Decode: "uint64"}, []byte{0x00, 0x04, 0x00, 0x03, 0x00, 0x02, 0x00, 0x01}, 0x0004000300020001}, {Register{Decode: "uint64s"}, []byte{0x00, 0x04, 0x00, 0x03, 0x00, 0x02, 0x00, 0x01}, 0x0001000200030004}, } @@ -82,6 +85,12 @@ func TestDecoding(t *testing.T) { for _, tc := range tc { fun, err := tc.r.DecodeFunc() require.NoError(t, err, tc) - require.Equal(t, tc.out, fun(tc.in), tc) + + out := fun(tc.in) + if math.IsNaN(tc.out) { + require.True(t, math.IsNaN(out), tc) + continue + } + require.Equal(t, tc.out, out, tc) } }