Add SMA speedwire support for inverters (#1114)
This commit is contained in:
parent
b8c07beba5
commit
80418daa1b
10 changed files with 259 additions and 654 deletions
|
|
@ -22,7 +22,7 @@ const (
|
|||
// private task ids
|
||||
const (
|
||||
taskOpenwb = "openwb"
|
||||
taskSMA = "shm"
|
||||
taskSMA = "sma"
|
||||
taskKEBA = "keba"
|
||||
taskE3DC = "e3dc_simple"
|
||||
taskSonnen = "sonnen"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
1
go.mod
|
|
@ -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
3
go.sum
|
|
@ -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=
|
||||
|
|
|
|||
309
meter/sma.go
309
meter/sma.go
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue