215 lines
5.4 KiB
Go
215 lines
5.4 KiB
Go
package meter
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"sort"
|
|
"text/tabwriter"
|
|
|
|
"github.com/evcc-io/evcc/api"
|
|
"github.com/evcc-io/evcc/plugin/sma"
|
|
"github.com/evcc-io/evcc/util"
|
|
"gitlab.com/bboehmke/sunny"
|
|
)
|
|
|
|
// SMA supporting SMA Home Manager 2.0, SMA Energy Meter 30 and SMA inverter
|
|
type SMA struct {
|
|
uri string
|
|
scale float64
|
|
device *sma.Device
|
|
}
|
|
|
|
func init() {
|
|
registry.Add("sma", NewSMAFromConfig)
|
|
}
|
|
|
|
// NewSMAFromConfig creates an SMA meter from generic config
|
|
func NewSMAFromConfig(other map[string]any) (api.Meter, error) {
|
|
cc := struct {
|
|
batteryCapacity `mapstructure:",squash"`
|
|
batteryPowerLimits `mapstructure:",squash"`
|
|
batterySocLimits `mapstructure:",squash"`
|
|
URI, Password, Interface string
|
|
Usage string
|
|
Serial uint32
|
|
Scale float64 // power only
|
|
}{
|
|
Password: "0000",
|
|
Scale: 1,
|
|
}
|
|
|
|
if err := util.DecodeOther(other, &cc); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sm, err := NewSMA(cc.URI, cc.Password, cc.Interface, cc.Serial, cc.Scale)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if cc.Usage == "battery" {
|
|
return decorateMeterBattery(
|
|
sm, sm.TotalEnergy,
|
|
sm.soc, cc.batteryCapacity.Decorator(),
|
|
cc.batterySocLimits.Decorator(), cc.batteryPowerLimits.Decorator(), nil,
|
|
), nil
|
|
}
|
|
|
|
return sm, nil
|
|
}
|
|
|
|
// NewSMA creates an SMA meter
|
|
func NewSMA(uri, password, iface string, serial uint32, scale float64) (*SMA, error) {
|
|
sm := &SMA{
|
|
uri: uri,
|
|
scale: scale,
|
|
}
|
|
|
|
discoverer, err := sma.GetDiscoverer(iface)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("discoverer: %w", err)
|
|
}
|
|
|
|
switch {
|
|
case uri != "":
|
|
sm.device, err = discoverer.DeviceByIP(uri, password)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
case serial > 0:
|
|
sm.device = discoverer.DeviceBySerial(serial, password)
|
|
if sm.device == nil {
|
|
return nil, fmt.Errorf("device not found: %d", serial)
|
|
}
|
|
|
|
default:
|
|
return nil, errors.New("missing uri or serial")
|
|
}
|
|
|
|
// call UpdateValues first to check if we get an error
|
|
if err := sm.device.UpdateValues(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// start update loop manually to get values as fast as possible
|
|
go sm.device.Run()
|
|
|
|
return sm, nil
|
|
}
|
|
|
|
// CurrentPower implements the api.Meter interface
|
|
func (sm *SMA) CurrentPower() (float64, error) {
|
|
values, err := sm.device.Values()
|
|
return sm.scale * (sma.AsFloat(values[sunny.ActivePowerPlus]) - sma.AsFloat(values[sunny.ActivePowerMinus])), err
|
|
}
|
|
|
|
var _ api.MeterEnergy = (*SMA)(nil)
|
|
|
|
// TotalEnergy implements the api.MeterEnergy interface
|
|
func (sm *SMA) TotalEnergy() (float64, error) {
|
|
values, err := sm.device.Values()
|
|
if sm.scale < 0 {
|
|
return sma.AsFloat(values[sunny.ActiveEnergyMinus]) / 3600000, err
|
|
}
|
|
return sma.AsFloat(values[sunny.ActiveEnergyPlus]) / 3600000, err
|
|
}
|
|
|
|
var _ api.PhaseCurrents = (*SMA)(nil)
|
|
|
|
// Currents implements the api.PhaseCurrents interface
|
|
func (sm *SMA) Currents() (float64, float64, float64, error) {
|
|
values, err := sm.device.Values()
|
|
|
|
var powers [3]float64
|
|
for i, id := range []sunny.ValueID{sunny.ActivePowerMinusL1, sunny.ActivePowerMinusL2, sunny.ActivePowerMinusL3} {
|
|
if p := sma.AsFloat(values[id]); p > 0 {
|
|
powers[i] = -p
|
|
}
|
|
}
|
|
|
|
var res [3]float64
|
|
for i, id := range []sunny.ValueID{sunny.CurrentL1, sunny.CurrentL2, sunny.CurrentL3} {
|
|
res[i] = util.SignFromPower(sma.AsFloat(values[id]), powers[i])
|
|
}
|
|
|
|
return sm.scale * res[0], sm.scale * res[1], sm.scale * res[2], err
|
|
}
|
|
|
|
var _ api.PhaseVoltages = (*SMA)(nil)
|
|
|
|
// Voltages implements the api.PhaseVoltages interface
|
|
func (sm *SMA) Voltages() (float64, float64, float64, error) {
|
|
values, err := sm.device.Values()
|
|
|
|
var res [3]float64
|
|
for i, id := range []sunny.ValueID{sunny.VoltageL1, sunny.VoltageL2, sunny.VoltageL3} {
|
|
res[i] = sma.AsFloat(values[id])
|
|
}
|
|
|
|
return res[0], res[1], res[2], err
|
|
}
|
|
|
|
var _ api.PhasePowers = (*SMA)(nil)
|
|
|
|
// Powers implements the api.PhasePowers interface
|
|
func (sm *SMA) Powers() (float64, float64, float64, error) {
|
|
values, err := sm.device.Values()
|
|
|
|
var res [3]float64
|
|
for i, id := range []sunny.ValueID{sunny.ActivePowerPlusL1, sunny.ActivePowerPlusL2, sunny.ActivePowerPlusL3} {
|
|
res[i] = sma.AsFloat(values[id])
|
|
}
|
|
for i, id := range []sunny.ValueID{sunny.ActivePowerMinusL1, sunny.ActivePowerMinusL2, sunny.ActivePowerMinusL3} {
|
|
res[i] -= sma.AsFloat(values[id])
|
|
}
|
|
|
|
return sm.scale * res[0], sm.scale * res[1], sm.scale * res[2], err
|
|
}
|
|
|
|
// soc implements the api.Battery interface
|
|
func (sm *SMA) soc() (float64, error) {
|
|
values, err := sm.device.Values()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
soc, ok := values[sunny.BatteryCharge]
|
|
if !ok {
|
|
return 0, api.ErrNotAvailable
|
|
}
|
|
return sma.AsFloat(soc), nil
|
|
}
|
|
|
|
var _ api.Diagnosis = (*SMA)(nil)
|
|
|
|
// Diagnose implements the api.Diagnosis interface
|
|
func (sm *SMA) Diagnose() {
|
|
w := tabwriter.NewWriter(os.Stdout, 0, 0, 1, ' ', 0)
|
|
|
|
fmt.Fprintf(w, " IP:\t%s\n", sm.device.Address())
|
|
fmt.Fprintf(w, " Serial:\t%d\n", sm.device.SerialNumber())
|
|
fmt.Fprintf(w, " EnergyMeter:\t%v\n", sm.device.IsEnergyMeter())
|
|
fmt.Fprintln(w)
|
|
|
|
if values, err := sm.device.Values(); err == nil {
|
|
ids := make([]sunny.ValueID, 0, len(values))
|
|
for k := range values {
|
|
ids = append(ids, k)
|
|
}
|
|
|
|
sort.Slice(ids, func(i, j int) bool {
|
|
return ids[i].String() < ids[j].String()
|
|
})
|
|
|
|
for _, id := range ids {
|
|
switch values[id].(type) {
|
|
case float64:
|
|
fmt.Fprintf(w, " %s:\t%f %s\n", id.String(), values[id], sunny.GetValueInfo(id).Unit)
|
|
default:
|
|
fmt.Fprintf(w, " %s:\t%v %s\n", id.String(), values[id], sunny.GetValueInfo(id).Unit)
|
|
}
|
|
}
|
|
}
|
|
w.Flush()
|
|
}
|