Skip to content

Commit

Permalink
SMA: speed up device discovery (#1147)
Browse files Browse the repository at this point in the history
  • Loading branch information
bboehmke authored Jun 12, 2021
1 parent bbc79cb commit f9d7a15
Show file tree
Hide file tree
Showing 4 changed files with 101 additions and 30 deletions.
4 changes: 3 additions & 1 deletion detect/tasks/sma.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
121 changes: 95 additions & 26 deletions meter/sma.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package meter

import (
"context"
"errors"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"

"github.com/andig/evcc/api"
Expand All @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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,
Expand All @@ -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() {
Expand Down

0 comments on commit f9d7a15

Please sign in to comment.