diff --git a/detect/definitions.go b/detect/definitions.go index a0ce49f1a..250b60074 100644 --- a/detect/definitions.go +++ b/detect/definitions.go @@ -22,7 +22,7 @@ const ( // private task ids const ( taskOpenwb = "openwb" - taskSMA = "shm" + taskSMA = "sma" taskKEBA = "keba" taskE3DC = "e3dc_simple" taskSonnen = "sonnen" diff --git a/detect/tasks/sma.go b/detect/tasks/sma.go index e5eadd2f5..dd363c7ac 100644 --- a/detect/tasks/sma.go +++ b/detect/tasks/sma.go @@ -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 diff --git a/detect/tasks/types.go b/detect/tasks/types.go index 4df2ae22a..3fdd5db57 100644 --- a/detect/tasks/types.go +++ b/detect/tasks/types.go @@ -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 { diff --git a/go.mod b/go.mod index 45668649b..d72a78206 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index ebb90953b..12fd46337 100644 --- a/go.sum +++ b/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= diff --git a/meter/sma.go b/meter/sma.go index d9399e187..bb02c1020 100644 --- a/meter/sma.go +++ b/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 + } +} diff --git a/meter/sma/listener.go b/meter/sma/listener.go deleted file mode 100644 index 13161b2be..000000000 --- a/meter/sma/listener.go +++ /dev/null @@ -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 = "" -) - -// 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 - } - } -} diff --git a/meter/sma/listener_test.go b/meter/sma/listener_test.go deleted file mode 100644 index cf0eb0482..000000000 --- a/meter/sma/listener_test.go +++ /dev/null @@ -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) - } - }) - } -} diff --git a/meter/sma_decorators.go b/meter/sma_decorators.go index 3fd077d4e..297a5d8ed 100644 --- a/meter/sma_decorators.go +++ b/meter/sma_decorators.go @@ -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() } diff --git a/meter/sma_test.go b/meter/sma_test.go deleted file mode 100644 index 6b4e295a3..000000000 --- a/meter/sma_test.go +++ /dev/null @@ -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) - } - - }) - } -}