SMA Speedwire: support hybrid inverters (#30084)
This commit is contained in:
parent
8278bd79c9
commit
ee8cb8db06
2 changed files with 142 additions and 28 deletions
139
meter/sma.go
139
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()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue