diff --git a/meter/sma.go b/meter/sma.go index 2ae5d59fe..26a9f847b 100644 --- a/meter/sma.go +++ b/meter/sma.go @@ -19,6 +19,8 @@ type SMA struct { implement.Caps uri string scale float64 + usage string + dc bool device *sma.Device } @@ -35,7 +37,8 @@ func NewSMAFromConfig(other map[string]any) (api.Meter, error) { URI, Password, Interface string Usage string Serial uint32 - Scale float64 // power only + Scale float64 // scale power, swap energy registers + DC bool // expose DC measurements (pv/battery) }{ Password: "0000", Scale: 1, @@ -45,15 +48,17 @@ func NewSMAFromConfig(other map[string]any) (api.Meter, error) { return nil, err } - return NewSMA(cc.URI, cc.Password, cc.Interface, cc.Serial, cc.Scale, cc.Usage, cc.batteryCapacity.Decorator(), cc.batterySocLimits.Decorator(), cc.batteryPowerLimits.Decorator()) + return NewSMA(cc.URI, cc.Password, cc.Interface, cc.Serial, cc.Scale, cc.Usage, cc.DC, cc.batteryCapacity.Decorator(), cc.batterySocLimits.Decorator(), cc.batteryPowerLimits.Decorator()) } // NewSMA creates an SMA meter -func NewSMA(uri, password, iface string, serial uint32, scale float64, usage string, capacity func() float64, batterySocLimits, batteryPowerLimits func() (float64, float64)) (*SMA, error) { +func NewSMA(uri, password, iface string, serial uint32, scale float64, usage string, dc bool, capacity func() float64, batterySocLimits, batteryPowerLimits func() (float64, float64)) (*SMA, error) { sm := &SMA{ Caps: implement.New(), uri: uri, scale: scale, + usage: usage, + dc: dc, } discoverer, err := sma.GetDiscoverer(iface) @@ -67,13 +72,11 @@ func NewSMA(uri, password, iface string, serial uint32, scale float64, usage str 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") } @@ -86,20 +89,52 @@ func NewSMA(uri, password, iface string, serial uint32, scale float64, usage str // start update loop manually to get values as fast as possible go sm.device.Run() - if usage == "battery" { - implement.May(sm, implement.MeterEnergy(sm.TotalEnergy)) + isInverter := !sm.device.IsEnergyMeter() + isHybridInverter := isInverter && dc + + if isInverter && usage == "battery" { implement.Has(sm, implement.Battery(sm.soc)) implement.May(sm, implement.BatteryCapacity(capacity)) implement.May(sm, implement.BatterySocLimiter(batterySocLimits)) implement.May(sm, implement.BatteryPowerLimiter(batteryPowerLimits)) } + if isHybridInverter && usage == "pv" { + implement.Has(sm, implement.MaxACPowerGetter(sm.maxACPower)) + } + + if !isHybridInverter { + implement.Has(sm, implement.PhaseCurrents(sm.currents)) + implement.Has(sm, implement.PhasePowers(sm.powers)) + } + + if !(isHybridInverter && usage == "pv") { + implement.Has(sm, implement.MeterReturnEnergy(sm.returnEnergy)) + } + return sm, nil } // CurrentPower implements the api.Meter interface func (sm *SMA) CurrentPower() (float64, error) { values, err := sm.device.Values() + + if !sm.device.IsEnergyMeter() { + switch sm.usage { + case "grid": + // grid: import minus export (consumption positive) + return sma.AsFloat(values[sunny.GridPowerImport]) - sma.AsFloat(values[sunny.GridPowerExport]), err + case "pv": + if sm.dc { + return sma.AsFloat(values[sunny.PvPower]), err + } + case "battery": + if sm.dc { + return sma.AsFloat(values[sunny.BatteryVoltage]) * sma.AsFloat(values[sunny.BatteryCurrent]), err + } + } + } + return sm.scale * (sma.AsFloat(values[sunny.ActivePowerPlus]) - sma.AsFloat(values[sunny.ActivePowerMinus])), err } @@ -108,16 +143,67 @@ var _ api.MeterEnergy = (*SMA)(nil) // TotalEnergy implements the api.MeterEnergy interface func (sm *SMA) TotalEnergy() (float64, error) { values, err := sm.device.Values() + + if !sm.device.IsEnergyMeter() { + switch sm.usage { + case "grid": + return sma.AsFloat(values[sunny.GridEnergyImportKWh]), err + case "pv": + if sm.dc { + return sma.AsFloat(values[sunny.PvEnergyTotalKWh]), err + } + case "battery": + if sm.dc { + return sma.AsFloat(values[sunny.BatteryEnergyDischargeKWh]), err + } + } + } + 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) +// returnEnergy implements the api.MeterReturnEnergy interface +func (sm *SMA) returnEnergy() (float64, error) { + values, err := sm.device.Values() -// Currents implements the api.PhaseCurrents interface -func (sm *SMA) Currents() (float64, float64, float64, error) { + if !sm.device.IsEnergyMeter() { + switch sm.usage { + case "grid": + return sma.AsFloat(values[sunny.GridEnergyExportKWh]), err + case "battery": + if sm.dc { + return sma.AsFloat(values[sunny.BatteryEnergyChargeKWh]), err + } + } + } + + if sm.scale < 0 { + return sma.AsFloat(values[sunny.ActiveEnergyPlus]) / 3600000, err + } + + return sma.AsFloat(values[sunny.ActiveEnergyMinus]) / 3600000, 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 +} + +// currents implements the api.PhaseCurrents interface +func (sm *SMA) currents() (float64, float64, float64, error) { values, err := sm.device.Values() var powers [3]float64 @@ -135,24 +221,8 @@ func (sm *SMA) Currents() (float64, float64, float64, error) { 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) { +// powers implements the api.PhasePowers interface +func (sm *SMA) powers() (float64, float64, float64, error) { values, err := sm.device.Values() var res [3]float64 @@ -166,16 +236,28 @@ func (sm *SMA) Powers() (float64, float64, float64, error) { return sm.scale * res[0], sm.scale * res[1], sm.scale * res[2], err } +// maxACPower returns the inverter's nominal max active power (LRI 0x411E) +func (sm *SMA) maxACPower() float64 { + values, err := sm.device.Values() + if err != nil { + return 0 + } + + return sma.AsFloat(values[sunny.ActivePowerMax]) +} + // 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 } @@ -209,5 +291,6 @@ func (sm *SMA) Diagnose() { } } } + w.Flush() } diff --git a/templates/definition/meter/sma-hybrid-speedwire.yaml b/templates/definition/meter/sma-hybrid-speedwire.yaml new file mode 100644 index 000000000..d0c55315d --- /dev/null +++ b/templates/definition/meter/sma-hybrid-speedwire.yaml @@ -0,0 +1,31 @@ +template: sma-hybrid-speedwire +products: + - brand: SMA + description: + de: Smart Energy Hybrid-Wechselrichter (Speedwire) + en: Smart Energy Hybrid Inverter (Speedwire) +requirements: + description: + de: | + Für aktive Batteriesteuerung den Modbus-basierten Eintrag SMA Smart Energy Hybrid Wechselrichter verwenden. + en: | + For active battery control use the Modbus-based SMA Smart Energy Hybrid Inverter template instead. + evcc: ["skiptest"] +params: + - name: usage + choice: ["pv", "battery", "grid"] + - name: host + - name: password + help: + en: Password for user group Standard + de: Passwort für Benutzergruppe Benutzer + - preset: battery-params +render: | + type: sma + usage: {{ .usage }} + dc: true + uri: {{ .host }} # IP address or hostname + password: {{ .password }} # optional + {{- if eq .usage "battery" }} + {{- include "battery-params" . }} + {{- end }}