evcc-io/meter/sma.go

176 lines
3.8 KiB
Go

package meter
import (
"errors"
"fmt"
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/meter/sma"
"github.com/andig/evcc/util"
)
const udpTimeout = 10 * time.Second
// values bundles SMA readings
type values struct {
power float64
energy float64
currentL1 float64
currentL2 float64
currentL3 float64
}
// SMA supporting SMA Home Manager 2.0 and SMA Energy Meter 30
type SMA struct {
log *util.Logger
mux *util.Waiter
uri string
serial string
values values
powerO sma.Obis
energyO sma.Obis
recv chan sma.Telegram
}
// NewSMAFromConfig creates a SMA Meter from generic config
func NewSMAFromConfig(other map[string]interface{}) (api.Meter, error) {
cc := struct {
URI, Serial, Power, Energy string
}{}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
return NewSMA(cc.URI, cc.Serial, cc.Power, cc.Energy)
}
// NewSMA creates a SMA Meter
func NewSMA(uri, serial, power, energy string) (api.Meter, error) {
log := util.NewLogger("sma")
sm := &SMA{
mux: util.NewWaiter(udpTimeout, func() { log.TRACE.Println("wait for initial value") }),
log: log,
uri: uri,
serial: serial,
powerO: sma.Obis(power),
energyO: sma.Obis(energy),
recv: make(chan sma.Telegram),
}
if sma.Instance == nil {
sma.Instance = sma.New(log)
}
// we only need to subscribe to one of the two possible identifiers
if uri != "" {
sma.Instance.Subscribe(uri, sm.recv)
} else if serial != "" {
sma.Instance.Subscribe(serial, sm.recv)
} else {
return nil, errors.New("missing uri or serial")
}
go sm.receive()
// decorate api.MeterEnergy
if energy != "" {
return &SMAEnergy{SMA: sm}, nil
}
return sm, nil
}
// update the actual meter data
func (sm *SMA) updateMeterValues(msg sma.Telegram) {
sm.mux.Lock()
defer sm.mux.Unlock()
if sm.powerO != "" {
// use user-defined obis
if power, ok := msg.Values[sm.powerO]; ok {
sm.values.power = power
sm.mux.Update()
}
} else {
sm.values.power = msg.Values[sma.ImportPower] - msg.Values[sma.ExportPower]
sm.mux.Update()
}
if sm.energyO != "" {
if energy, ok := msg.Values[sm.energyO]; ok {
sm.values.energy = energy
sm.mux.Update()
} else {
sm.log.WARN.Println("missing obis for energy")
}
}
if currentL1, ok := msg.Values[sma.CurrentL1]; ok {
sm.values.currentL1 = currentL1
sm.mux.Update()
} else {
sm.log.WARN.Println("missing obis for currentL1")
}
if currentL2, ok := msg.Values[sma.CurrentL2]; ok {
sm.values.currentL2 = currentL2
sm.mux.Update()
} else {
sm.log.WARN.Println("missing obis for currentL2")
}
if currentL3, ok := msg.Values[sma.CurrentL3]; ok {
sm.values.currentL3 = currentL3
sm.mux.Update()
} else {
sm.log.WARN.Println("missing obis for currentL3")
}
}
// receive processes the channel message containing the multicast data
func (sm *SMA) receive() {
for msg := range sm.recv {
if msg.Values == nil {
continue
}
sm.updateMeterValues(msg)
}
}
func (sm *SMA) hasValue() (values, error) {
elapsed := sm.mux.LockWithTimeout()
defer sm.mux.Unlock()
if elapsed > 0 {
return values{}, fmt.Errorf("recv timeout: %v", elapsed.Truncate(time.Second))
}
return sm.values, nil
}
// CurrentPower implements the Meter.CurrentPower interface
func (sm *SMA) CurrentPower() (float64, error) {
values, err := sm.hasValue()
return values.power, err
}
// Currents implements the MeterCurrent interface
func (sm *SMA) Currents() (float64, float64, float64, error) {
values, err := sm.hasValue()
return values.currentL1, sm.values.currentL2, sm.values.currentL3, err
}
// SMAEnergy decorates SMA with api.MeterEnergy interface
type SMAEnergy struct {
*SMA
}
// TotalEnergy implements the api.MeterEnergy interface
func (sm *SMAEnergy) TotalEnergy() (float64, error) {
values, err := sm.hasValue()
return values.energy, err
}