Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SMA/Speedwire Plugin support #1173

Merged
merged 6 commits into from
Jul 4, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
reworked SMA and added SMA provider
  • Loading branch information
bboehmke committed Jun 24, 2021
commit c3db3032ce7d70bc5e41e108ba117711e8492745
178 changes: 18 additions & 160 deletions meter/sma.go
Original file line number Diff line number Diff line change
@@ -1,88 +1,25 @@
package meter

import (
"context"
"errors"
"fmt"
"os"
"sort"
"sync"
"sync/atomic"
"text/tabwriter"
"time"

"github.com/andig/evcc/api"
"github.com/andig/evcc/provider/sma"
"github.com/andig/evcc/util"
"github.com/imdario/mergo"
"gitlab.com/bboehmke/sunny"
)

const udpTimeout = 10 * time.Second

// smaDiscoverer discovers SMA devices in background while providing already found devices
type smaDiscoverer struct {
conn *sunny.Connection
devices map[uint32]*sunny.Device
mux sync.RWMutex
done uint32
}

// run discover and store found devices
func (d *smaDiscoverer) run() {
devices := make(chan *sunny.Device)

go func() {
for device := range devices {
d.mux.Lock()
d.devices[device.SerialNumber()] = device
d.mux.Unlock()
}
}()

// discover devices and wait for results
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
d.conn.DiscoverDevices(ctx, devices, "")
cancel()
close(devices)

// mark discover as done
atomic.AddUint32(&d.done, 1)
}

func (d *smaDiscoverer) get(serial uint32) *sunny.Device {
d.mux.RLock()
defer d.mux.RUnlock()
return d.devices[serial]
}

// deviceBySerial with the given serial number
func (d *smaDiscoverer) deviceBySerial(serial uint32) *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
if device := d.get(serial); device != nil {
return device
}

time.Sleep(time.Millisecond * 10)
}
return d.get(serial)
}

// SMA supporting SMA Home Manager 2.0 and SMA Energy Meter 30
type SMA struct {
log *util.Logger
mux *util.Waiter
uri string
iface string
values map[sunny.ValueID]interface{}
scale float64
device *sunny.Device
device *sma.Device
}

func init() {
Expand Down Expand Up @@ -114,58 +51,32 @@ func NewSMAFromConfig(other map[string]interface{}) (api.Meter, error) {
return NewSMA(cc.URI, cc.Password, cc.Interface, cc.Serial, cc.Scale)
}

// map of created discover instances
var discoverers = make(map[string]*smaDiscoverer)

// initialize sunny logger only once
var once sync.Once

// NewSMA creates a SMA Meter
func NewSMA(uri, password, iface string, serial uint32, scale float64) (api.Meter, error) {
log := util.NewLogger("sma")
once.Do(func() {
sunny.Log = log.TRACE
})

sm := &SMA{
mux: util.NewWaiter(udpTimeout, func() { log.TRACE.Println("wait for initial value") }),
log: log,
uri: uri,
iface: iface,
values: make(map[sunny.ValueID]interface{}),
scale: scale,
log: util.NewLogger("sma"),
uri: uri,
iface: iface,
scale: scale,
}

conn, err := sunny.NewConnection(iface)
discoverer, err := sma.GetDiscoverer(iface)
if err != nil {
return nil, fmt.Errorf("connection failed: %w", err)
return nil, fmt.Errorf("failed to get discoverer failed: %w", err)
}

switch {
case uri != "":
sm.device, err = conn.NewDevice(uri, password)
sm.device, err = discoverer.DeviceByIP(uri, password)
if err != nil {
return nil, err
}

case serial > 0:
discoverer, ok := discoverers[iface]
if !ok {
discoverer = &smaDiscoverer{
conn: conn,
devices: make(map[uint32]*sunny.Device),
}

go discoverer.run()

discoverers[iface] = discoverer
}

sm.device = discoverer.deviceBySerial(serial)
sm.device = discoverer.DeviceBySerial(serial, password)
if sm.device == nil {
return nil, fmt.Errorf("device not found: %d", serial)
}
sm.device.SetPassword(password)

default:
return nil, errors.New("missing uri or serial")
Expand All @@ -184,71 +95,38 @@ func NewSMA(uri, password, iface string, serial uint32, scale float64) (api.Mete
}
}

go func() {
for range time.NewTicker(time.Second).C {
sm.updateValues()
}
}()

return decorateSMA(sm, soc), nil
}

func (sm *SMA) updateValues() {
sm.mux.Lock()
defer sm.mux.Unlock()

values, err := sm.device.GetValues()
if err == nil {
err = mergo.Merge(&sm.values, values, mergo.WithOverride)
}

if err == nil {
sm.mux.Update()
} else {
sm.log.ERROR.Println(err)
}
}

func (sm *SMA) hasValue() (map[sunny.ValueID]interface{}, error) {
elapsed := sm.mux.LockWithTimeout()
defer sm.mux.Unlock()

if elapsed > 0 {
return nil, fmt.Errorf("update timeout: %v", elapsed.Truncate(time.Second))
}

return sm.values, nil
}

// CurrentPower implements the api.Meter interface
func (sm *SMA) CurrentPower() (float64, error) {
values, err := sm.hasValue()
return sm.scale * (sm.asFloat(values[sunny.ActivePowerPlus]) - sm.asFloat(values[sunny.ActivePowerMinus])), err
values, err := sm.device.GetValues()
return sm.scale * (sma.AsFloat(values[sunny.ActivePowerPlus]) - sma.AsFloat(values[sunny.ActivePowerMinus])), err
}

// TotalEnergy implements the api.MeterEnergy interface
func (sm *SMA) TotalEnergy() (float64, error) {
values, err := sm.hasValue()
return sm.asFloat(values[sunny.ActiveEnergyPlus]) / 3600000, err
values, err := sm.device.GetValues()
return sma.AsFloat(values[sunny.ActiveEnergyPlus]) / 3600000, err
}

// Currents implements the api.MeterCurrent interface
func (sm *SMA) Currents() (float64, float64, float64, error) {
values, err := sm.hasValue()
values, err := sm.device.GetValues()

measurements := []sunny.ValueID{sunny.CurrentL1, sunny.CurrentL2, sunny.CurrentL3}
andig marked this conversation as resolved.
Show resolved Hide resolved
var vals [3]float64
for i := 0; i < 3; i++ {
vals[i] = sm.asFloat(values[measurements[i]])
vals[i] = sma.AsFloat(values[measurements[i]])
}

return vals[0], vals[1], vals[2], err
}

// soc implements the api.Battery interface
func (sm *SMA) soc() (float64, error) {
values, err := sm.hasValue()
return sm.asFloat(values[sunny.BatteryCharge]), err
values, err := sm.device.GetValues()
return sma.AsFloat(values[sunny.BatteryCharge]), err
}

// Diagnose implements the api.Diagnosis interface
Expand Down Expand Up @@ -290,23 +168,3 @@ func (sm *SMA) Diagnose() {
}
andig marked this conversation as resolved.
Show resolved Hide resolved
w.Flush()
}

func (sm *SMA) asFloat(value interface{}) float64 {
switch v := value.(type) {
case float64:
return v
case int32:
return float64(v)
case int64:
return float64(v)
case uint32:
return float64(v)
case uint64:
return float64(v)
case nil:
return 0
default:
sm.log.WARN.Printf("unknown value type: %T", value)
return 0
}
}
98 changes: 98 additions & 0 deletions provider/sma.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package provider

import (
"errors"
"fmt"

"github.com/andig/evcc/provider/sma"
"github.com/andig/evcc/util"
"gitlab.com/bboehmke/sunny"
)

// SMA provider
type SMA struct {
device *sma.Device
value sunny.ValueID
scale float64
}

func init() {
registry.Add("sma", NewSMAFromConfig)
}

// NewSMAFromConfig creates SMA provider
func NewSMAFromConfig(other map[string]interface{}) (IntProvider, error) {
cc := struct {
URI, Password, Interface string
Serial uint32
Value string
Scale float64
}{
Password: "0000",
Scale: 1,
}

if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}

discoverer, err := sma.GetDiscoverer(cc.Interface)
if err != nil {
return nil, fmt.Errorf("failed to get discoverer failed: %w", err)
}

var provider = &SMA{
scale: cc.Scale,
}
switch {
case cc.URI != "":
provider.device, err = discoverer.DeviceByIP(cc.URI, cc.Password)
if err != nil {
return nil, err
}

case cc.Serial > 0:
provider.device = discoverer.DeviceBySerial(cc.Serial, cc.Password)
if provider.device == nil {
return nil, fmt.Errorf("device not found: %d", cc.Serial)
}

default:
return nil, errors.New("missing uri or serial")
}

provider.value, err = sunny.ValueIDString(cc.Value)
if err != nil {
return nil, err
}

return provider, err
}

var _ FloatProvider = (*Mqtt)(nil)
andig marked this conversation as resolved.
Show resolved Hide resolved

// FloatGetter creates handler for float64
func (p *SMA) FloatGetter() func() (float64, error) {
return func() (float64, error) {
values, err := p.device.GetValues()
if err != nil {
return 0, err
}

return sma.AsFloat(values[p.value]) * p.scale, nil
}
}

// IntGetter creates handler for int64
func (p *SMA) IntGetter() func() (int64, error) {
fl := p.FloatGetter()

return func() (int64, error) {
f, err := fl()
if err != nil {
return 0, err
}

return int64(f), nil
}
}
Loading