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() }