Add SMA speedwire support for inverters (#1114)

This commit is contained in:
Benjamin Böhmke 2021-06-11 07:20:49 +02:00 • committed by GitHub
parent b8c07beba5
commit 80418daa1b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 259 additions and 654 deletions

View file

@ -22,7 +22,7 @@ const (
// private task ids
const (
taskOpenwb = "openwb"
taskSMA = "shm"
taskSMA = "sma"
taskKEBA = "keba"
taskE3DC = "e3dc_simple"
taskSonnen = "sonnen"

View file

@ -4,27 +4,29 @@ import (
"crypto/tls"
"fmt"
"net/http"
"strconv"
"sync"
"time"
"github.com/andig/evcc/meter/sma"
"github.com/andig/evcc/util"
"gitlab.com/bboehmke/sunny"
)
const Sma TaskType = "shm"
const Sma TaskType = "sma"
func init() {
registry.Add(Sma, SMAHandlerFactory)
}
type ShmResult struct {
type SmaResult struct {
Serial string
Http bool
}
func SMAHandlerFactory(conf map[string]interface{}) (TaskHandler, error) {
handler := SMAHandler{
Timeout: 5 * time.Second,
Timeout: 5 * time.Second,
Password: "0000",
}
err := util.DecodeOther(conf, &handler)
@ -34,8 +36,9 @@ func SMAHandlerFactory(conf map[string]interface{}) (TaskHandler, error) {
type SMAHandler struct {
mux sync.Mutex
listener *sma.Listener
handled bool
Timeout time.Duration
Password string
}
func (h *SMAHandler) httpAvailable(ip string) bool {
@ -60,46 +63,27 @@ func (h *SMAHandler) httpAvailable(ip string) bool {
func (h *SMAHandler) Test(log *util.Logger, in ResultDetails) (res []ResultDetails) {
h.mux.Lock()
if h.listener != nil {
if h.handled {
h.mux.Unlock()
return nil
}
var err error
if h.listener, err = sma.New(log, ""); err != nil {
log.ERROR.Println("shm:", err)
devices, err := sunny.DiscoverDevices(h.Password)
if err != nil {
log.ERROR.Println("sma:", err)
return nil
}
h.handled = true
h.mux.Unlock()
resC := make(chan sma.Telegram)
h.listener.Subscribe(sma.Any, resC)
timer := time.NewTimer(h.Timeout)
WAIT:
for {
select {
case t := <-resC:
// eliminate duplicates
for _, r := range res {
if r.ShmResult != nil && r.ShmResult.Serial == t.Serial {
continue WAIT
}
}
out := ResultDetails{
IP: t.Addr,
ShmResult: &ShmResult{
Serial: t.Serial,
Http: h.httpAvailable(t.Addr),
},
}
res = append(res, out)
case <-timer.C:
break WAIT
}
for _, device := range devices {
res = append(res, ResultDetails{
IP: device.Address().IP.String(),
SmaResult: &SmaResult{
Serial: strconv.FormatInt(int64(device.SerialNumber()), 10),
Http: h.httpAvailable(device.Address().IP.String()),
},
})
}
return res

View file

@ -11,7 +11,7 @@ type ResultDetails struct {
Topic string `json:",omitempty"`
ModbusResult *ModbusResult `json:",omitempty"`
KebaResult *KebaResult `json:",omitempty"`
ShmResult *ShmResult `json:",omitempty"`
SmaResult *SmaResult `json:",omitempty"`
}
func (d *ResultDetails) Clone() ResultDetails {

1
go.mod
View file

@ -70,6 +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
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

3
go.sum
View file

@ -721,6 +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=
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=
@ -1128,6 +1130,7 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=

View file

@ -3,11 +3,15 @@ package meter
import (
"errors"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
"time"
"github.com/andig/evcc/api"
"github.com/andig/evcc/meter/sma"
"github.com/andig/evcc/util"
"gitlab.com/bboehmke/sunny"
)
const udpTimeout = 10 * time.Second
@ -19,138 +23,202 @@ type values struct {
currentL1 float64
currentL2 float64
currentL3 float64
soc 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
iface string
values values
powerO sma.Obis
energyO sma.Obis
recv chan sma.Telegram
log *util.Logger
mux *util.Waiter
uri string
serial string
iface string
values values
scale float64
device *sunny.Device
updateTicker *time.Ticker
}
func init() {
registry.Add("sma", NewSMAFromConfig)
}
//go:generate go run ../cmd/tools/decorate.go -f decorateSMA -b api.Meter -t "api.MeterEnergy,TotalEnergy,func() (float64, error)"
//go:generate go run ../cmd/tools/decorate.go -f decorateSMA -r api.Meter -b *SMA -t "api.Battery,SoC,func() (float64, error)"
// NewSMAFromConfig creates a SMA Meter from generic config
func NewSMAFromConfig(other map[string]interface{}) (api.Meter, error) {
cc := struct {
URI, Serial, Interface, Power, Energy string
}{}
URI, Password, Serial, Interface, Power, Energy string
Scale float64
}{
Password: "0000",
Scale: 1,
}
if err := util.DecodeOther(other, &cc); err != nil {
return nil, err
}
return NewSMA(cc.URI, cc.Serial, cc.Interface, cc.Power, cc.Energy)
return NewSMA(cc.URI, cc.Password, cc.Serial, cc.Interface, cc.Power, cc.Energy, cc.Scale)
}
// NewSMA creates a SMA Meter
func NewSMA(uri, serial, iface, power, energy string) (api.Meter, error) {
func NewSMA(uri, password, serial, iface, power, energy string, scale float64) (api.Meter, error) {
log := util.NewLogger("sma")
sunny.Log = log.TRACE
sm := &SMA{
mux: util.NewWaiter(udpTimeout, func() { log.TRACE.Println("wait for initial value") }),
log: log,
uri: uri,
serial: serial,
iface: iface,
powerO: sma.Obis(power),
energyO: sma.Obis(energy),
recv: make(chan sma.Telegram),
// print warnings for unused config
if power != "" {
log.WARN.Println("SMA power not supported -> ignoring")
}
if energy != "" {
log.WARN.Println("SMA energy not supported -> ignoring")
}
if sma.Instance == nil {
instance, err := sma.New(log, iface)
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,
uri: uri,
serial: serial,
iface: iface,
updateTicker: time.NewTicker(time.Second),
scale: scale,
}
var err error
if uri != "" {
sm.device, err = sunny.NewDevice(uri, password)
if err != nil {
return nil, err
}
sma.Instance = instance
}
// 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)
// 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 sm.device == nil {
return nil, fmt.Errorf("failed to find device with serial: %s", serial)
}
} else {
return nil, errors.New("missing uri or serial")
}
// decorate api.MeterEnergy
var totalEnergy func() (float64, error)
if energy != "" {
totalEnergy = sm.totalEnergy
vals, err := sm.device.GetValues()
if err != nil {
return nil, err
}
go sm.receive()
// decorate api.Battery
var soc func() (float64, error)
if _, ok := vals["battery_charge"]; ok {
soc = sm.soc
}
return decorateSMA(sm, totalEnergy), nil
go func() {
for range sm.updateTicker.C {
sm.updateValues()
}
}()
return decorateSMA(sm, soc), nil
}
// update the actual meter data
func (sm *SMA) updateMeterValues(msg sma.Telegram) {
func (sm *SMA) updateValues() {
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()
vals, err := sm.device.GetValues()
if err != nil {
sm.log.ERROR.Printf("failed to get values: %v", err)
return
}
if sm.energyO != "" {
if energy, ok := msg.Values[sm.energyO]; ok {
sm.values.energy = energy
if sm.device.IsEnergyMeter() {
powerP, ok1 := vals["active_power_plus"]
powerM, ok2 := vals["active_power_minus"]
if ok1 && ok2 {
sm.values.power = sm.scale * (sm.convertValue(powerP) - sm.convertValue(powerM))
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.log.ERROR.Println("missing value for power")
}
sm.updateMeterValues(msg)
if currentL1, ok := vals["l1_current"]; ok {
sm.values.currentL1 = sm.convertValue(currentL1)
sm.mux.Update()
} else {
sm.log.ERROR.Println("missing value for currentL1")
}
if currentL2, ok := vals["l2_current"]; ok {
sm.values.currentL2 = sm.convertValue(currentL2)
sm.mux.Update()
} else {
sm.log.ERROR.Println("missing value for currentL2")
}
if currentL3, ok := vals["l3_current"]; ok {
sm.values.currentL3 = sm.convertValue(currentL3)
sm.mux.Update()
} else {
sm.log.ERROR.Println("missing value for currentL3")
}
if energyTotal, ok := vals["active_energy_plus"]; ok {
sm.values.energy = sm.convertValue(energyTotal) / 3600000
sm.mux.Update()
}
} else {
if power, ok := vals["power_ac_total"]; ok {
sm.values.power = sm.convertValue(power)
sm.mux.Update()
} else {
sm.log.DEBUG.Println("missing value for power -> set to 0")
sm.values.power = 0
sm.mux.Update()
}
if currentL1, ok := vals["current_ac1"]; ok {
sm.values.currentL1 = sm.convertValue(currentL1) / 1000
sm.mux.Update()
}
if currentL2, ok := vals["current_ac2"]; ok {
sm.values.currentL2 = sm.convertValue(currentL2) / 1000
sm.mux.Update()
}
if currentL3, ok := vals["current_ac3"]; ok {
sm.values.currentL3 = sm.convertValue(currentL3) / 1000
sm.mux.Update()
}
if soc, ok := vals["battery_charge"]; ok {
sm.values.soc = sm.convertValue(soc)
sm.mux.Update()
}
if energyTotal, ok := vals["energy_total"]; ok {
sm.values.energy = sm.convertValue(energyTotal) / 1000
sm.mux.Update()
}
}
}
@ -159,7 +227,7 @@ func (sm *SMA) hasValue() (values, error) {
defer sm.mux.Unlock()
if elapsed > 0 {
return values{}, fmt.Errorf("recv timeout: %v", elapsed.Truncate(time.Second))
return values{}, fmt.Errorf("update timeout: %v", elapsed.Truncate(time.Second))
}
return sm.values, nil
@ -174,11 +242,74 @@ func (sm *SMA) CurrentPower() (float64, error) {
// Currents implements the api.MeterCurrent interface
func (sm *SMA) Currents() (float64, float64, float64, error) {
values, err := sm.hasValue()
return values.currentL1, sm.values.currentL2, sm.values.currentL3, err
return values.currentL1, values.currentL2, values.currentL3, err
}
// totalEnergy implements the api.MeterEnergy interface
func (sm *SMA) totalEnergy() (float64, error) {
// TotalEnergy implements the api.MeterEnergy interface
func (sm *SMA) TotalEnergy() (float64, error) {
values, err := sm.hasValue()
return values.energy, err
}
// soc implements the api.Battery interface
func (sm *SMA) soc() (float64, error) {
values, err := sm.hasValue()
return values.soc, err
}
// Diagnose implements the api.Diagnosis interface
func (sm *SMA) Diagnose() {
fmt.Printf(" IP: %s\n", sm.device.Address())
fmt.Printf(" Serial: %d\n", sm.device.SerialNumber())
fmt.Printf(" Is EnergyMeter: %v\n", sm.device.IsEnergyMeter())
fmt.Printf("\n")
name, err := sm.device.GetDeviceName()
if err != nil {
fmt.Printf(" ERROR: %v\n", err)
} else {
fmt.Printf(" Name: %s\n", name)
}
devClass, err := sm.device.GetDeviceClass()
if err != nil {
fmt.Printf(" ERROR: %v\n", err)
} else {
fmt.Printf(" Device Class: 0x%X\n", devClass)
}
fmt.Printf("\n")
values, err := sm.device.GetValues()
if err != nil {
fmt.Printf(" ERROR: %v\n", err)
} else {
keys := make([]string, 0, len(values))
keyLength := 0
for k := range values {
keys = append(keys, k)
if len(k) > keyLength {
keyLength = len(k)
}
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf(" %s:%s %v %s\n", k, strings.Repeat(" ", keyLength-len(k)), values[k], sm.device.GetValueInfo(k).Unit)
}
}
}
func (sm *SMA) convertValue(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)
default:
sm.log.WARN.Printf("unknown value type: %s", reflect.TypeOf(value).Name())
return 0
}
}

View file

@ -1,226 +0,0 @@
package sma
import (
"encoding/binary"
"errors"
"fmt"
"net"
"strconv"
"sync"
"github.com/andig/evcc/util"
)
const (
multicastAddr = "239.12.255.254:9522"
udpBufferSize = 8192
msgSerial = 20 // start of serial in preamble
msgPreamble = 28 // preamble size in bytes
msgCodeLength = 4 // length in bytes
// Any subscriber receives all messages
Any = "<any>"
)
// Obis defines an Obis code as understood my the EMETER protocol
type Obis = string
// obis code definitions
const (
ImportPower Obis = "1:1.4.0" // Wirkleistung (W)
ExportPower Obis = "1:2.4.0" // Wirkleistung (W)
ImportEnergy Obis = "1:1.8.0" // Wirkarbeit (Ws) +
ExportEnergy Obis = "1:2.8.0" // Wirkarbeit (Ws) −
CurrentL1 Obis = "1:31.4.0" // Strom (A)
CurrentL2 Obis = "1:51.4.0" // Strom (A)
CurrentL3 Obis = "1:71.4.0" // Strom (A)
)
// obisDefinition defines the properties needed to parse the SMA multicast telegram values
type obisDefinition struct {
length int // data size in bytes of the return value
factor float64 // the factor to multiply the value by to get the proper value in the given unit
}
// list of Obis codes and their properties as defined in the SMA EMETER-Protokoll-TI-de-10.pdf document
var knownObisCodes = map[Obis]obisDefinition{
// Overall sums
ImportPower: {4, 0.1}, ImportEnergy: {8, 1}, // Wirkleistung (W)/-arbeit (Ws) +
ExportPower: {4, 0.1}, ExportEnergy: {8, 1}, // Wirkleistung (W)/-arbeit (Ws) −
"1:3.4.0": {4, 0.1}, "1:3.8.0": {8, 1}, // Blindleistung (W)/-arbeit (Ws) +
"1:4.4.0": {4, 0.1}, "1:4.8.0": {8, 1}, // Blindleistung (W)/-arbeit (Ws) −
"1:9.4.0": {4, 0.1}, "1:9.8.0": {8, 1}, // Scheinleistung (W)/-arbeit (Ws) +
"1:10.4.0": {4, 0.1}, "1:10.8.0": {8, 1}, // Scheinleistung (W)/-arbeit (Ws) −
"1:13.4.0": {4, 0.001}, // Leistungsfaktor (Φ)
"1:14.4.0": {4, 0.001}, // Frequenz (Hz)
// Phase 1: {
"1:21.4.0": {4, 0.1}, "1:21.8.0": {8, 1}, // Wirkleistung (W)/-arbeit (Ws) +
"1:22.4.0": {4, 0.1}, "1:22.8.0": {8, 1}, // Wirkleistung (W)/-arbeit (Ws) −
"1:23.4.0": {4, 0.1}, "1:23.8.0": {8, 1}, // Blindleistung (W)/-arbeit (Ws) +
"1:24.4.0": {4, 0.1}, "1:24.8.0": {8, 1}, // Blindleistung (W)/-arbeit (Ws) −
"1:29.4.0": {4, 0.1}, "1:29.8.0": {8, 1}, // Scheinleistung (W)/-arbeit (Ws) +
"1:30.4.0": {4, 0.1}, "1:30.8.0": {8, 1}, // Scheinleistung (W)/-arbeit (Ws) −
CurrentL1: {4, 0.001}, // Strom (A)
"1:32.4.0": {4, 0.001}, // Spannung (V
// Phase 2: {
"1:41.4.0": {4, 0.1}, "1:41.8.0": {8, 1}, // Wirkleistung (W)/-arbeit (Ws) +
"1:42.4.0": {4, 0.1}, "1:42.8.0": {8, 1}, // Wirkleistung (W)/-arbeit (Ws) −
"1:43.4.0": {4, 0.1}, "1:43.8.0": {8, 1}, // Blindleistung (W)/-arbeit (Ws) +
"1:44.4.0": {4, 0.1}, "1:44.8.0": {8, 1}, // Blindleistung (W)/-arbeit (Ws) −
"1:49.4.0": {4, 0.1}, "1:49.8.0": {8, 1}, // Scheinleistung (W)/-arbeit (Ws) +
"1:50.4.0": {4, 0.1}, "1:50.8.0": {8, 1}, // Scheinleistung (W)/-arbeit (Ws) −
CurrentL2: {4, 0.001}, // Strom (A)
"1:52.4.0": {4, 0.001}, // Spannung (V)
// Phase 3: {
"1:61.4.0": {4, 0.1}, "1:61.8.0": {8, 1}, // Wirkleistung (W)/-arbeit (Ws) +
"1:62.4.0": {4, 0.1}, "1:62.8.0": {8, 1}, // Wirkleistung (W)/-arbeit (Ws) −
"1:63.4.0": {4, 0.1}, "1:63.8.0": {8, 1}, // Blindleistung (W)/-arbeit (Ws) +
"1:64.4.0": {4, 0.1}, "1:64.8.0": {8, 1}, // Blindleistung (W)/-arbeit (Ws) −
"1:69.4.0": {4, 0.1}, "1:69.8.0": {8, 1}, // Scheinleistung (W)/-arbeit (Ws) +
"1:70.4.0": {4, 0.1}, "1:70.8.0": {8, 1}, // Scheinleistung (W)/-arbeit (Ws) −
CurrentL3: {4, 0.001}, // Strom (A)
"1:72.4.0": {4, 0.001}, // Spannung (V)
// Others
"144:0.0.0": {4, 1}, // SW Version
}
// Instance is the Listener singleton
var Instance *Listener
// Telegram defines the data structure of a SMA multicast data package
type Telegram struct {
Addr string
Serial string
Values map[Obis]float64
}
// Listener for receiving SMA multicast data packages
type Listener struct {
mux sync.Mutex
log *util.Logger
conn *net.UDPConn
clients map[string]chan<- Telegram
}
// New creates a Listener
func New(log *util.Logger, ifaceName string) (*Listener, error) {
var iface *net.Interface
if ifaceName != "" {
var err error
if iface, err = net.InterfaceByName(ifaceName); err != nil {
return nil, fmt.Errorf("error resolving network interface '%s': %w", ifaceName, err)
}
log.DEBUG.Printf("listening on network interface %s for multicasts", iface.Name)
}
// parse address
gaddr, err := net.ResolveUDPAddr("udp4", multicastAddr)
if err != nil {
return nil, fmt.Errorf("error resolving udp address: %w", err)
}
// open connection
conn, err := net.ListenMulticastUDP("udp4", iface, gaddr)
if err != nil {
return nil, fmt.Errorf("error opening connecting: %w", err)
}
if err := conn.SetReadBuffer(udpBufferSize); err != nil {
return nil, fmt.Errorf("error setting read buffer: %w", err)
}
l := &Listener{
log: log,
conn: conn,
clients: make(map[string]chan<- Telegram),
}
go l.listen()
return l, nil
}
// processMessage converts a SMA multicast data package into Telegram
func (l *Listener) processMessage(src *net.UDPAddr, b []byte) (Telegram, error) {
numBytes := len(b)
if numBytes <= msgPreamble {
return Telegram{}, errors.New("received data package is too small")
}
obisValues := make(map[string]float64)
var obisDef obisDefinition
for i := msgPreamble; i < numBytes-msgCodeLength; i += msgCodeLength + obisDef.length {
// spec says value should be 1, but reading contains 0
b0 := b[i+0]
if b0 == 0 {
b0 = 1
}
code := fmt.Sprintf("%d:%d.%d.%d", b0, b[i+1], b[i+2], b[i+3])
if obisDef, ok := knownObisCodes[code]; ok {
switch obisDef.length {
case 4:
obisValues[code] = obisDef.factor * float64(binary.BigEndian.Uint32(b[i+msgCodeLength:]))
case 8:
obisValues[code] = obisDef.factor * float64(binary.BigEndian.Uint64(b[i+msgCodeLength:]))
}
}
}
serial := strconv.FormatUint(uint64(binary.BigEndian.Uint32(b[msgSerial:])), 10)
msg := Telegram{
Addr: src.IP.String(),
Serial: serial,
Values: obisValues,
}
// l.log.TRACE.Printf("recv %v", msg.Values)
return msg, nil
}
// listen for multicast data packages
func (l *Listener) listen() {
buffer := make([]byte, udpBufferSize)
for {
read, src, err := l.conn.ReadFromUDP(buffer)
if err != nil {
l.log.WARN.Printf("udp read failed: %s", err)
continue
}
if msg, err := l.processMessage(src, buffer[:read-1]); err == nil {
l.send(msg)
}
}
}
// Subscribe adds a client address and message channel
func (l *Listener) Subscribe(identifier string, c chan<- Telegram) {
l.mux.Lock()
defer l.mux.Unlock()
l.clients[identifier] = c
}
func (l *Listener) send(msg Telegram) {
l.mux.Lock()
defer l.mux.Unlock()
for identifier, client := range l.clients {
if identifier == msg.Addr || identifier == msg.Serial || identifier == Any {
select {
case client <- msg:
default:
l.log.TRACE.Println("recv: listener blocked")
}
break
}
}
}

View file

@ -1,210 +0,0 @@
package sma
import (
"net"
"reflect"
"testing"
"github.com/andig/evcc/util"
)
func TestListenerProcessMessage(t *testing.T) {
tests := []struct {
name string
ip net.IP
port int
response []byte
wantErr bool
want Telegram
}{
{
"SMA Sunny Home Manager - success",
net.ParseIP("192.0.2.2"),
33168,
[]byte{
0x53, 0x4d, 0x41, 0x00, 0x00, 0x04, 0x02, 0xa0, 0x00, 0x00, 0x00, 0x01, 0x02, 0x4c, 0x00, 0x10,
0x60, 0x69, 0x01, 0x74, 0x00, 0x00, 0x00, 0x00, 0xb2, 0x79, 0xb9, 0x63, 0x00, 0x01, 0x04, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x01, 0x9a, 0xc1, 0x2f, 0x28,
0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x01, 0x7b, 0x00, 0x02, 0x08, 0x00, 0x00, 0x00, 0x00, 0x05,
0x85, 0x61, 0x66, 0x80, 0x00, 0x03, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x08, 0x00,
0x00, 0x00, 0x00, 0x01, 0x77, 0xbb, 0xd1, 0x90, 0x00, 0x04, 0x04, 0x00, 0x00, 0x00, 0x1b, 0x17,
0x00, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00, 0x05, 0x1c, 0xea, 0x4c, 0x98, 0x00, 0x09, 0x04, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x08, 0x00, 0x00, 0x00, 0x00, 0x04, 0x35, 0xe1, 0xe4, 0x00,
0x00, 0x0a, 0x04, 0x00, 0x00, 0x00, 0x1b, 0x22, 0x00, 0x0a, 0x08, 0x00, 0x00, 0x00, 0x00, 0x07,
0xd1, 0x54, 0xcd, 0xc8, 0x00, 0x0d, 0x04, 0x00, 0x00, 0x00, 0x00, 0x37, 0x00, 0x0e, 0x04, 0x00,
0x00, 0x00, 0xc3, 0x50, 0x00, 0x15, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x08, 0x00,
0x00, 0x00, 0x00, 0x01, 0x70, 0x4b, 0x83, 0xb0, 0x00, 0x16, 0x04, 0x00, 0x00, 0x00, 0x01, 0x1e,
0x00, 0x16, 0x08, 0x00, 0x00, 0x00, 0x00, 0x05, 0x66, 0x12, 0xc5, 0x08, 0x00, 0x17, 0x04, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x17, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73, 0x37, 0xec, 0x68,
0x00, 0x18, 0x04, 0x00, 0x00, 0x00, 0x14, 0x64, 0x00, 0x18, 0x08, 0x00, 0x00, 0x00, 0x00, 0x02,
0x20, 0x3b, 0x02, 0xa0, 0x00, 0x1d, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1d, 0x08, 0x00,
0x00, 0x00, 0x00, 0x02, 0x20, 0x65, 0xfa, 0x50, 0x00, 0x1e, 0x04, 0x00, 0x00, 0x00, 0x14, 0x6b,
0x00, 0x1e, 0x08, 0x00, 0x00, 0x00, 0x00, 0x06, 0x11, 0x8d, 0x2a, 0x40, 0x00, 0x1f, 0x04, 0x00,
0x00, 0x00, 0x09, 0xf5, 0x00, 0x20, 0x04, 0x00, 0x00, 0x03, 0xa8, 0xf3, 0x00, 0x21, 0x04, 0x00,
0x00, 0x00, 0x00, 0x37, 0x00, 0x29, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0x08, 0x00,
0x00, 0x00, 0x00, 0x01, 0x5c, 0x87, 0xe4, 0x90, 0x00, 0x2a, 0x04, 0x00, 0x00, 0x00, 0x00, 0x38,
0x00, 0x2a, 0x08, 0x00, 0x00, 0x00, 0x00, 0x01, 0x4a, 0x41, 0xd0, 0xc0, 0x00, 0x2b, 0x04, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x95, 0x17, 0x47, 0x48,
0x00, 0x2c, 0x04, 0x00, 0x00, 0x00, 0x01, 0xf9, 0x00, 0x2c, 0x08, 0x00, 0x00, 0x00, 0x00, 0x02,
0x08, 0xf0, 0x1a, 0x90, 0x00, 0x31, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x08, 0x00,
0x00, 0x00, 0x00, 0x01, 0xf6, 0x0b, 0xe2, 0x38, 0x00, 0x32, 0x04, 0x00, 0x00, 0x00, 0x01, 0xfc,
0x00, 0x32, 0x08, 0x00, 0x00, 0x00, 0x00, 0x02, 0x2d, 0x8f, 0x04, 0x88, 0x00, 0x33, 0x04, 0x00,
0x00, 0x00, 0x01, 0x8d, 0x00, 0x34, 0x04, 0x00, 0x00, 0x03, 0xa6, 0xb4, 0x00, 0x35, 0x04, 0x00,
0x00, 0x00, 0x00, 0x6f, 0x00, 0x3d, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3d, 0x08, 0x00,
0x00, 0x00, 0x00, 0x00, 0xcf, 0x2b, 0x4b, 0xc8, 0x00, 0x3e, 0x04, 0x00, 0x00, 0x00, 0x00, 0x25,
0x00, 0x3e, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0xd6, 0x4a, 0x54, 0x30, 0x00, 0x3f, 0x04, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x84, 0x69, 0xbf, 0xe8,
0x00, 0x40, 0x04, 0x00, 0x00, 0x00, 0x04, 0xbb, 0x00, 0x40, 0x08, 0x00, 0x00, 0x00, 0x00, 0x01,
0x08, 0xbc, 0x51, 0x70, 0x00, 0x45, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x45, 0x08, 0x00,
0x00, 0x00, 0x00, 0x01, 0xa2, 0x02, 0x34, 0x08, 0x00, 0x46, 0x04, 0x00, 0x00, 0x00, 0x04, 0xbb,
0x00, 0x46, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0xee, 0x6d, 0xbe, 0x30, 0x00, 0x47, 0x04, 0x00,
0x00, 0x00, 0x02, 0x66, 0x00, 0x48, 0x04, 0x00, 0x00, 0x03, 0xa8, 0x0f, 0x00, 0x49, 0x04, 0x00,
0x00, 0x00, 0x00, 0x1e, 0x90, 0x00, 0x00, 0x00, 0x02, 0x03, 0x05, 0x52, 0x00, 0x00, 0x00, 0x00,
},
false,
Telegram{
Addr: "192.0.2.2",
Serial: "0",
Values: map[string]float64{
"1:1.4.0": 0, "1:1.8.0": 6.89131908e+09,
"1:2.4.0": 37.9, "1:2.8.0": 2.371258944e+10,
"1:3.4.0": 0, "1:3.8.0": 6.30376488e+09,
"1:4.4.0": 693.5, "1:4.8.0": 2.195995356e+10,
"1:9.4.0": 0, "1:9.8.0": 1.80838656e+10,
"1:10.4.0": 694.6, "1:10.8.0": 3.357676692e+10,
"1:13.4.0": 0.055,
"1:14.4.0": 50.0,
"1:21.4.0": 0, "1:21.8.0": 6.1789644e+09,
"1:22.4.0": 28.6, "1:22.8.0": 2.31873426e+10,
"1:23.4.0": 0, "1:23.8.0": 1.93304484e+09,
"1:24.4.0": 522, "1:24.8.0": 9.1306728e+09,
"1:29.4.0": 0, "1:29.8.0": 9.13348872e+09,
"1:30.4.0": 522.7, "1:30.8.0": 2.606426784e+10,
"1:31.4.0": 2.549,
"1:32.4.0": 239.859,
"1:41.4.0": 0, "1:41.8.0": 5.84737704e+09,
"1:42.4.0": 5.6000000000000005, "1:42.8.0": 5.54079456e+09,
"1:43.4.0": 0, "1:43.8.0": 2.50133076e+09,
"1:44.4.0": 50.5, "1:44.8.0": 8.73988776e+09,
"1:49.4.0": 0, "1:49.8.0": 8.42294124e+09,
"1:50.4.0": 50.800000000000004, "1:50.8.0": 9.35428212e+09,
"1:51.4.0": 0.397,
"1:52.4.0": 239.284,
"1:61.4.0": 0, "1:61.8.0": 3.47572116e+09,
"1:62.4.0": 3.7, "1:62.8.0": 3.59519544e+09,
"1:63.4.0": 0, "1:63.8.0": 2.22152292e+09,
"1:64.4.0": 121.10000000000001, "1:64.8.0": 4.44152664e+09,
"1:69.4.0": 0, "1:69.8.0": 7.01302068e+09,
"1:70.4.0": 121.10000000000001, "1:70.8.0": 4.00016952e+09,
"1:71.4.0": 0.614,
"1:72.4.0": 239.631,
"144:0.0.0": 3.3752402e+07,
},
},
},
{
"SMA Energy Manager - success",
net.ParseIP("192.0.2.2"),
33168,
[]byte{
0x53, 0x4d, 0x41, 0x00, 0x00, 0x04, 0x02, 0xa0, 0x00, 0x00, 0x00, 0x01, 0x02, 0x44, 0x00, 0x10,
0x60, 0x69, 0x01, 0x5d, 0x00, 0x00, 0x00, 0x00, 0xb2, 0x79, 0xba, 0x32, 0x00, 0x01, 0x04, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x61, 0xcd, 0x60,
0x00, 0x02, 0x04, 0x00, 0x00, 0x00, 0x08, 0xac, 0x00, 0x02, 0x08, 0x00, 0x00, 0x00, 0x00, 0x09,
0xa2, 0x63, 0x4e, 0xc0, 0x00, 0x03, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x08, 0x00,
0x00, 0x00, 0x00, 0x00, 0x63, 0x57, 0x7c, 0xc8, 0x00, 0x04, 0x04, 0x00, 0x00, 0x00, 0x00, 0x01,
0x00, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00, 0x01, 0x56, 0x41, 0x9e, 0xc8, 0x00, 0x09, 0x04, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x09, 0x08, 0x00, 0x00, 0x00, 0x00, 0x01, 0x07, 0x51, 0x3b, 0x20,
0x00, 0x0a, 0x04, 0x00, 0x00, 0x00, 0x08, 0xac, 0x00, 0x0a, 0x08, 0x00, 0x00, 0x00, 0x00, 0x09,
0xed, 0x57, 0xc3, 0xc0, 0x00, 0x0d, 0x04, 0x00, 0x00, 0x00, 0x03, 0xe8, 0x00, 0x15, 0x04, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x15, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0xa2, 0x73, 0x88,
0x00, 0x16, 0x04, 0x00, 0x00, 0x00, 0x05, 0x20, 0x00, 0x16, 0x08, 0x00, 0x00, 0x00, 0x00, 0x07,
0x7d, 0x59, 0x22, 0xf0, 0x00, 0x17, 0x04, 0x00, 0x00, 0x00, 0x00, 0x12, 0x00, 0x17, 0x08, 0x00,
0x00, 0x00, 0x00, 0x00, 0x20, 0xd6, 0xb0, 0x58, 0x00, 0x18, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x18, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8a, 0x16, 0x93, 0x80, 0x00, 0x1d, 0x04, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x1d, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6f, 0x58, 0x9a, 0xe8,
0x00, 0x1e, 0x04, 0x00, 0x00, 0x00, 0x05, 0x20, 0x00, 0x1e, 0x08, 0x00, 0x00, 0x00, 0x00, 0x07,
0x91, 0xf3, 0x65, 0xe0, 0x00, 0x1f, 0x04, 0x00, 0x00, 0x00, 0x02, 0x8e, 0x00, 0x20, 0x04, 0x00,
0x00, 0x03, 0xa8, 0xdc, 0x00, 0x21, 0x04, 0x00, 0x00, 0x00, 0x03, 0xe8, 0x00, 0x29, 0x04, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x29, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xf1, 0xb0,
0x00, 0x2a, 0x04, 0x00, 0x00, 0x00, 0x01, 0xc1, 0x00, 0x2a, 0x08, 0x00, 0x00, 0x00, 0x00, 0x01,
0x12, 0x13, 0x13, 0x30, 0x00, 0x2b, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x2b, 0x08, 0x00,
0x00, 0x00, 0x00, 0x00, 0x21, 0xf4, 0xf7, 0x10, 0x00, 0x2c, 0x04, 0x00, 0x00, 0x00, 0x00, 0x07,
0x00, 0x2c, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x66, 0xde, 0x75, 0x90, 0x00, 0x31, 0x04, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x31, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x28, 0xa4, 0x20,
0x00, 0x32, 0x04, 0x00, 0x00, 0x00, 0x01, 0xc1, 0x00, 0x32, 0x08, 0x00, 0x00, 0x00, 0x00, 0x01,
0x78, 0x16, 0x49, 0x18, 0x00, 0x33, 0x04, 0x00, 0x00, 0x00, 0x00, 0xf5, 0x00, 0x34, 0x04, 0x00,
0x00, 0x03, 0xa6, 0xb3, 0x00, 0x35, 0x04, 0x00, 0x00, 0x00, 0x03, 0xe8, 0x00, 0x3d, 0x04, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x3d, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x71, 0x38,
0x00, 0x3e, 0x04, 0x00, 0x00, 0x00, 0x01, 0xcb, 0x00, 0x3e, 0x08, 0x00, 0x00, 0x00, 0x00, 0x01,
0x13, 0x47, 0x1e, 0xe0, 0x00, 0x3f, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, 0x08, 0x00,
0x00, 0x00, 0x00, 0x00, 0x21, 0x45, 0x0d, 0x50, 0x00, 0x40, 0x04, 0x00, 0x00, 0x00, 0x00, 0x0c,
0x00, 0x40, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x66, 0x05, 0xcc, 0x40, 0x00, 0x45, 0x04, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x45, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1d, 0x95, 0xe0, 0x30,
0x00, 0x46, 0x04, 0x00, 0x00, 0x00, 0x01, 0xcb, 0x00, 0x46, 0x08, 0x00, 0x00, 0x00, 0x00, 0x01,
0x5e, 0xad, 0xe3, 0xf0, 0x00, 0x47, 0x04, 0x00, 0x00, 0x00, 0x00, 0xe7, 0x00, 0x48, 0x04, 0x00,
0x00, 0x03, 0xa7, 0xa9, 0x00, 0x49, 0x04, 0x00, 0x00, 0x00, 0x03, 0xe8, 0x90, 0x00, 0x00, 0x00,
0x02, 0x00, 0x12, 0x52, 0x00, 0x00, 0x00, 0x00,
},
false,
Telegram{
Addr: "192.0.2.2",
Serial: "0",
Values: map[string]float64{
"1:1.4.0": 0, "1:1.8.0": 1.2385008e+08,
"1:2.4.0": 222, "1:2.8.0": 4.137912288e+10,
"1:3.4.0": 0, "1:3.8.0": 1.66667796e+09,
"1:4.4.0": 0.1, "1:4.8.0": 5.74210836e+09,
"1:9.4.0": 0, "1:9.8.0": 4.41773136e+09,
"1:10.4.0": 222, "1:10.8.0": 4.26366576e+10,
"1:13.4.0": 1,
"1:21.4.0": 0, "1:21.8.0": 1.2808692e+08,
"1:22.4.0": 131.20000000000002, "1:22.8.0": 3.216776472e+10,
"1:23.4.0": 1.8, "1:23.8.0": 5.5094076e+08,
"1:24.4.0": 0, "1:24.8.0": 2.31673536e+09,
"1:29.4.0": 0, "1:29.8.0": 1.8680778e+09,
"1:30.4.0": 131.20000000000002, "1:30.8.0": 3.251341872e+10,
"1:31.4.0": 0.654,
"1:32.4.0": 239.836,
"1:41.4.0": 0, "1:41.8.0": 258480,
"1:42.4.0": 44.900000000000006, "1:42.8.0": 4.59820728e+09,
"1:43.4.0": 0, "1:43.8.0": 5.6970216e+08,
"1:44.4.0": 0.7000000000000001, "1:44.8.0": 1.72585512e+09,
"1:49.4.0": 0, "1:49.8.0": 6.977232e+07,
"1:50.4.0": 44.900000000000006, "1:50.8.0": 6.30969372e+09,
"1:51.4.0": 0.245,
"1:52.4.0": 239.28300000000002,
"1:61.4.0": 0, "1:61.8.0": 749880,
"1:62.4.0": 45.900000000000006, "1:62.8.0": 4.61839536e+09,
"1:63.4.0": 0, "1:63.8.0": 5.5817352e+08,
"1:64.4.0": 1.2000000000000002, "1:64.8.0": 1.711656e+09,
"1:69.4.0": 0, "1:69.8.0": 4.9636152e+08,
"1:70.4.0": 45.900000000000006, "1:70.8.0": 5.88342168e+09,
"1:71.4.0": 0.231,
"1:72.4.0": 239.529,
"144:0.0.0": 3.3559122e+07,
},
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
l := &Listener{
log: util.NewLogger("foo"),
}
buffer := tc.response
read := len(buffer)
src := &net.UDPAddr{IP: tc.ip, Port: tc.port}
got, err := l.processMessage(src, buffer[:read-1])
if (err != nil) != tc.wantErr {
t.Errorf("Listener.processMessage() error = %v, wantErr %v", err, tc.wantErr)
return
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("Listener.processMessage() got %v, want %v", got, tc.want)
}
})
}
}

View file

@ -6,19 +6,19 @@ import (
"github.com/andig/evcc/api"
)
func decorateSMA(base api.Meter, meterEnergy func() (float64, error)) api.Meter {
func decorateSMA(base *SMA, battery func() (float64, error)) api.Meter {
switch {
case meterEnergy == nil:
case battery == nil:
return base
case meterEnergy != nil:
case battery != nil:
return &struct {
api.Meter
api.MeterEnergy
*SMA
api.Battery
}{
Meter: base,
MeterEnergy: &decorateSMAMeterEnergyImpl{
meterEnergy: meterEnergy,
SMA: base,
Battery: &decorateSMABatteryImpl{
battery: battery,
},
}
}
@ -26,10 +26,10 @@ func decorateSMA(base api.Meter, meterEnergy func() (float64, error)) api.Meter
return nil
}
type decorateSMAMeterEnergyImpl struct {
meterEnergy func() (float64, error)
type decorateSMABatteryImpl struct {
battery func() (float64, error)
}
func (impl *decorateSMAMeterEnergyImpl) TotalEnergy() (float64, error) {
return impl.meterEnergy()
func (impl *decorateSMABatteryImpl) SoC() (float64, error) {
return impl.battery()
}

View file

@ -1,78 +0,0 @@
package meter
import (
"testing"
"github.com/andig/evcc/meter/sma"
"github.com/andig/evcc/util"
)
func TestSMAUpdateMeterValues(t *testing.T) {
tests := []struct {
name string
message sma.Telegram
wantPower float64
wantCurrentL1 float64
wantCurrentL2 float64
wantCurrentL3 float64
}{
{
"success export",
sma.Telegram{
Values: map[string]float64{
"1:1.4.0": 0,
"1:2.4.0": 37.9,
"1:31.4.0": 2.549,
"1:51.4.0": 0.397,
"1:71.4.0": 0.614,
},
},
-37.9,
2.549,
0.397,
0.614,
},
{
"success import",
sma.Telegram{
Values: map[string]float64{
"1:1.4.0": 20,
"1:2.4.0": 0,
"1:31.4.0": 0.654,
"1:51.4.0": 0.245,
"1:71.4.0": 0.231,
},
},
20,
0.654,
0.245,
0.231,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sm := &SMA{
log: util.NewLogger("foo"),
mux: util.NewWaiter(udpTimeout, func() {}),
}
sm.updateMeterValues(tt.message)
if sm.values.power != tt.wantPower {
t.Errorf("Listener.processMessage() got Power %v, want %v", sm.values.power, tt.wantPower)
}
if sm.values.currentL1 != tt.wantCurrentL1 {
t.Errorf("Listener.processMessage() got CurrentL1 %v, want %v", sm.values.currentL1, tt.wantCurrentL1)
}
if sm.values.currentL2 != tt.wantCurrentL2 {
t.Errorf("Listener.processMessage() got CurrentL2 %v, want %v", sm.values.currentL2, tt.wantCurrentL2)
}
if sm.values.currentL3 != tt.wantCurrentL3 {
t.Errorf("Listener.processMessage() got CurrentL3 %v, want %v", sm.values.currentL3, tt.wantCurrentL3)
}
})
}
}