SMA Speedwire: support hybrid inverters (#30084)

This commit is contained in:
premultiply 2026-06-11 01:51:24 +02:00 • committed by GitHub
parent 8278bd79c9
commit ee8cb8db06
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 142 additions and 28 deletions

View file

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