From f9d7a15adb6aefc7368cc6ed1ea0765ea8d82c8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20B=C3=B6hmke?= Date: Sat, 12 Jun 2021 20:34:15 +0200 Subject: [PATCH] SMA: speed up device discovery (#1147) --- detect/tasks/sma.go | 4 +- go.mod | 2 +- go.sum | 4 +- meter/sma.go | 121 ++++++++++++++++++++++++++++++++++---------- 4 files changed, 101 insertions(+), 30 deletions(-) diff --git a/detect/tasks/sma.go b/detect/tasks/sma.go index dd363c7ac..98736e2b5 100644 --- a/detect/tasks/sma.go +++ b/detect/tasks/sma.go @@ -68,11 +68,13 @@ func (h *SMAHandler) Test(log *util.Logger, in ResultDetails) (res []ResultDetai return nil } - devices, err := sunny.DiscoverDevices(h.Password) + connection, err := sunny.NewConnection("") if err != nil { log.ERROR.Println("sma:", err) return nil } + + devices := connection.SimpleDiscoverDevices(h.Password) h.handled = true h.mux.Unlock() diff --git a/go.mod b/go.mod index d72a78206..226ff0a3f 100644 --- a/go.mod +++ b/go.mod @@ -70,7 +70,7 @@ require ( github.com/thoas/go-funk v0.8.0 github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c github.com/volkszaehler/mbmd v0.0.0-20210117183837-59dcc46d62d4 - gitlab.com/bboehmke/sunny v0.11.1 + gitlab.com/bboehmke/sunny v0.12.1 golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf // indirect golang.org/x/mod v0.4.2 // indirect golang.org/x/net v0.0.0-20210508051633-16afe75a6701 diff --git a/go.sum b/go.sum index 12fd46337..b2b3ec9f2 100644 --- a/go.sum +++ b/go.sum @@ -721,8 +721,8 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -gitlab.com/bboehmke/sunny v0.11.1 h1:AmGqCXAgDbQFwGjNQ/+K1jGDwAPDL4q6zGtW+ux7zlI= -gitlab.com/bboehmke/sunny v0.11.1/go.mod h1:o0e0jA5xTQ7JpQ2FO/C8N21gSV47g64EC+dRUQui+CM= +gitlab.com/bboehmke/sunny v0.12.1 h1:SGFssDJNenC5csLHjtYsuFpOO+2Wd0woJyLBwfygr9g= +gitlab.com/bboehmke/sunny v0.12.1/go.mod h1:o0e0jA5xTQ7JpQ2FO/C8N21gSV47g64EC+dRUQui+CM= go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= diff --git a/meter/sma.go b/meter/sma.go index bb02c1020..0c3d5babc 100644 --- a/meter/sma.go +++ b/meter/sma.go @@ -1,12 +1,15 @@ package meter import ( + "context" "errors" "fmt" "reflect" "sort" "strconv" "strings" + "sync" + "sync/atomic" "time" "github.com/andig/evcc/api" @@ -16,6 +19,76 @@ import ( const udpTimeout = 10 * time.Second +// SMADiscoverHelper discovers SMA devices in background while providing already found devices +type SMADiscoverHelper struct { + conn *sunny.Connection + devices map[string]*sunny.Device + devicesMutex sync.RWMutex + done uint32 +} + +// NewSMADiscoverHelper creates discovery helper for given connection (normally one per interface) +func NewSMADiscoverHelper(conn *sunny.Connection) *SMADiscoverHelper { + discover := SMADiscoverHelper{ + conn: conn, + devices: make(map[string]*sunny.Device), + } + + go discover.run() + + return &discover +} + +// run discover and store found devices +func (d *SMADiscoverHelper) run() { + devices := make(chan *sunny.Device, 10) + + var wg sync.WaitGroup + wg.Add(1) + go func() { + for device := range devices { + d.devicesMutex.Lock() + d.devices[strconv.FormatInt(int64(device.SerialNumber()), 10)] = device + d.devicesMutex.Unlock() + } + wg.Done() + }() + + // discover devices and wait for results + ctx, cancel := context.WithTimeout(context.Background(), time.Second*3) + d.conn.DiscoverDevices(ctx, devices, "") + cancel() + close(devices) + wg.Wait() + + // mark discover as done + atomic.AddUint32(&d.done, 1) +} + +func (d *SMADiscoverHelper) get(serial string) *sunny.Device { + d.devicesMutex.RLock() + defer d.devicesMutex.RUnlock() + return d.devices[serial] +} + +// GetDevice with the given serial number +func (d *SMADiscoverHelper) GetDevice(serial string) *sunny.Device { + start := time.Now() + for time.Since(start) < time.Second*3 { + // discover done -> return immediately regardless of result + if atomic.LoadUint32(&d.done) != 0 { + return d.get(serial) + + // device with serial found -> return + } else if device := d.get(serial); device != nil { + return device + } + + time.Sleep(time.Millisecond * 10) + } + return d.get(serial) +} + // values bundles SMA readings type values struct { power float64 @@ -63,6 +136,9 @@ func NewSMAFromConfig(other map[string]interface{}) (api.Meter, error) { return NewSMA(cc.URI, cc.Password, cc.Serial, cc.Interface, cc.Power, cc.Energy, cc.Scale) } +// map of created discover instances +var discovers = make(map[string]*SMADiscoverHelper) + // NewSMA creates a SMA Meter func NewSMA(uri, password, serial, iface, power, energy string, scale float64) (api.Meter, error) { log := util.NewLogger("sma") @@ -76,12 +152,6 @@ func NewSMA(uri, password, serial, iface, power, energy string, scale float64) ( log.WARN.Println("SMA energy not supported -> ignoring") } - if iface != "" { - if err := sunny.SetMulticastInterface(iface); err != nil { - return nil, err - } - } - sm := &SMA{ mux: util.NewWaiter(udpTimeout, func() { log.TRACE.Println("wait for initial value") }), log: log, @@ -92,42 +162,41 @@ func NewSMA(uri, password, serial, iface, power, energy string, scale float64) ( scale: scale, } - var err error + conn, err := sunny.NewConnection(iface) + if err != nil { + return nil, fmt.Errorf("failed to get SMA connection: %w", err) + } + if uri != "" { - sm.device, err = sunny.NewDevice(uri, password) + sm.device, err = conn.NewDevice(uri, password) if err != nil { return nil, err } } else if serial != "" { - // list all devices - devices, err := sunny.DiscoverDevices(password) - if err != nil { - return nil, err - } - - // check if device with serial number is present - for _, device := range devices { - if serial == strconv.FormatInt(int64(device.SerialNumber()), 10) { - sm.device = device - } + if _, ok := discovers[iface]; !ok { + discovers[iface] = NewSMADiscoverHelper(conn) } + sm.device = discovers[iface].GetDevice(serial) if sm.device == nil { return nil, fmt.Errorf("failed to find device with serial: %s", serial) } + sm.device.SetPassword(password) } else { return nil, errors.New("missing uri or serial") } - vals, err := sm.device.GetValues() - if err != nil { - return nil, err - } - // decorate api.Battery var soc func() (float64, error) - if _, ok := vals["battery_charge"]; ok { - soc = sm.soc + if !sm.device.IsEnergyMeter() { // only for inverters possible + vals, err := sm.device.GetValues() + if err != nil { + return nil, err + } + + if _, ok := vals["battery_charge"]; ok { + soc = sm.soc + } } go func() {