Add SMA Sunny Home Manager 2.0 ModbusTCP with curtailment (#32091)

This commit is contained in:
Frank Behrens 2026-07-29 10:37:52 +02:00 • committed by GitHub
parent 19ed27fa54
commit 0a98d57cec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 77 additions and 13 deletions

View file

@ -69,7 +69,7 @@ type Site struct {
// meters
circuit api.Circuit // Circuit
hems api.HEMS // HEMS (set by configureHEMS at boot)
gridMeter api.Meter // Grid usage meter
gridMeter config.Device[api.Meter] // Grid usage meter
pvMeters []config.Device[api.Meter] // PV generation meters
batteryMeters []config.Device[api.Meter] // Battery charging meters
extMeters []config.Device[api.Meter] // External meters - for monitoring only
@ -206,10 +206,7 @@ func (site *Site) Boot(log *util.Logger, loadpoints []*Loadpoint, tariffs *tarif
return err
}
site.gridMeter = dev.Instance()
if site.gridMeter == nil {
return errors.New("missing grid meter instance")
}
site.gridMeter = dev
me, err := metrics.NewCollector(metrics.Grid, site.Meters.GridMeterRef, metrics.Grid)
if err != nil {
@ -456,7 +453,7 @@ func (site *Site) DumpConfig() {
)
if site.gridMeter != nil {
site.log.INFO.Println(meterCapabilities("grid", site.gridMeter))
site.log.INFO.Println(meterCapabilities("grid", site.gridMeter.Instance()))
}
if len(site.pvMeters) > 0 {
@ -864,7 +861,9 @@ func (site *Site) updateGridMeter() error {
mm := types.Measurement{Name: site.Meters.GridMeterRef}
if res, err := backoff.RetryWithData(site.gridMeter.CurrentPower, modbus.Backoff()); err == nil {
meter := site.gridMeter.Instance()
if res, err := backoff.RetryWithData(meter.CurrentPower, modbus.Backoff()); err == nil {
mm.Power = res
site.gridPower = res
site.log.DEBUG.Printf("grid power: %.0fW", res)
@ -873,10 +872,10 @@ func (site *Site) updateGridMeter() error {
}
// grid phase currents (signed)
if phaseMeter, ok := api.Cap[api.PhaseCurrents](site.gridMeter); ok {
if phaseMeter, ok := api.Cap[api.PhaseCurrents](meter); ok {
// grid phase powers
var p1, p2, p3 float64
if phaseMeter, ok := api.Cap[api.PhasePowers](site.gridMeter); ok {
if phaseMeter, ok := api.Cap[api.PhasePowers](meter); ok {
var err error // phases needed for signed currents
if p1, p2, p3, err = phaseMeter.Powers(); err == nil {
mm.Powers = []float64{p1, p2, p3}
@ -896,7 +895,7 @@ func (site *Site) updateGridMeter() error {
// grid energy (import); nil when the device has no MeterEnergy capability or the read fails
// ignore spurious zero readings (NaN-derived or nightly reset, #30950)
if energyMeter, ok := api.Cap[api.MeterEnergy](site.gridMeter); ok {
if energyMeter, ok := api.Cap[api.MeterEnergy](meter); ok {
if f, err := nonZeroEnergy(energyMeter.TotalEnergy()); err == nil {
mm.Energy = &f
} else if !errors.Is(err, api.ErrNotAvailable) {
@ -906,7 +905,7 @@ func (site *Site) updateGridMeter() error {
// grid return energy (export); nil when the device has no MeterReturnEnergy capability or the read fails
// ignore spurious zero readings as above
if returnEnergyMeter, ok := api.Cap[api.MeterReturnEnergy](site.gridMeter); ok {
if returnEnergyMeter, ok := api.Cap[api.MeterReturnEnergy](meter); ok {
if f, err := nonZeroEnergy(returnEnergyMeter.ReturnEnergy()); err == nil {
mm.ReturnEnergy = &f
} else if !errors.Is(err, api.ErrNotAvailable) {

View file

@ -97,7 +97,7 @@ func (site *Site) dimMeters(dim bool) error {
return errs
}
// curtailPV applies the HEMS curtailment percent to all curtailable pv meters.
// curtailPV applies the HEMS curtailment percent to all curtailable pv or grid meters.
// Devices are only queried when the percent changes or after a failed attempt.
func (site *Site) curtailPV(percent *int) error {
if percent == nil || site.curtailPercent != nil && *site.curtailPercent == *percent {
@ -107,8 +107,13 @@ func (site *Site) curtailPV(percent *int) error {
// invalidate until successfully applied
site.curtailPercent = nil
meters := slices.Clone(site.pvMeters)
if site.gridMeter != nil {
meters = append(meters, site.gridMeter)
}
var errs error
for _, dev := range site.pvMeters {
for _, dev := range meters {
m, ok := api.Cap[api.Curtailer](dev.Instance())
if !ok {
continue