diff --git a/core/site_circuits.go b/core/site_circuits.go index 6aceb79fe..5e97c2f91 100644 --- a/core/site_circuits.go +++ b/core/site_circuits.go @@ -73,14 +73,13 @@ func (site *Site) dimMeters(dim bool) error { continue } - if dimmed, err := backoff.RetryWithData(m.Dimmed, modbus.Backoff()); err == nil { - if dim == dimmed { - continue - } - } else { - if !errors.Is(err, api.ErrNotAvailable) { - errs = errors.Join(errs, fmt.Errorf("%s dimmed: %w", deviceTitleOrName(dev), err)) - } + // unreadable state: apply unconditionally + dimmed, err := backoff.RetryWithData(m.Dimmed, modbus.Backoff()) + if err != nil && !errors.Is(err, api.ErrNotAvailable) { + errs = errors.Join(errs, fmt.Errorf("%s dimmed: %w", deviceTitleOrName(dev), err)) + continue + } + if err == nil && dim == dimmed { continue } @@ -115,14 +114,13 @@ func (site *Site) curtailPV(percent *int) error { continue } - if curtailed, err := backoff.RetryWithData(m.CurtailedPercent, modbus.Backoff()); err == nil { - if curtailed == *percent { - continue - } - } else { - if !errors.Is(err, api.ErrNotAvailable) { - errs = errors.Join(errs, fmt.Errorf("%s curtailed: %w", deviceTitleOrName(dev), err)) - } + // unreadable state: apply unconditionally + curtailed, err := backoff.RetryWithData(m.CurtailedPercent, modbus.Backoff()) + if err != nil && !errors.Is(err, api.ErrNotAvailable) { + errs = errors.Join(errs, fmt.Errorf("%s curtailed: %w", deviceTitleOrName(dev), err)) + continue + } + if err == nil && curtailed == *percent { continue } diff --git a/core/site_circuits_test.go b/core/site_circuits_test.go index 7e7e26e60..e18aa53c9 100644 --- a/core/site_circuits_test.go +++ b/core/site_circuits_test.go @@ -15,19 +15,18 @@ import ( type curtailableMeter struct { api.Meter percent int + getErr error setErr error gets int - sets int setCalls []int } func (m *curtailableMeter) CurtailedPercent() (int, error) { m.gets++ - return m.percent, nil + return m.percent, m.getErr } func (m *curtailableMeter) SetCurtailPercent(percent int) error { - m.sets++ m.setCalls = append(m.setCalls, percent) if m.setErr != nil { return m.setErr @@ -93,10 +92,26 @@ func TestCurtailPVNoStatement(t *testing.T) { assert.Equal(t, []int{60}, m.setCalls) } +// A device that cannot report its state is written once, not on every cycle. +func TestCurtailPVNotAvailable(t *testing.T) { + m := &curtailableMeter{getErr: api.ErrNotAvailable} + site := curtailSite(m) + + for range 3 { + require.NoError(t, site.curtailPV(new(60))) + } + assert.Equal(t, []int{60}, m.setCalls) + + // changed: applied again + require.NoError(t, site.curtailPV(new(30))) + assert.Equal(t, []int{60, 30}, m.setCalls) +} + // dimmableMeter counts device interactions to verify caching. type dimmableMeter struct { api.Meter dimmed bool + getErr error dimErr error gets int dimCalls []bool @@ -104,7 +119,7 @@ type dimmableMeter struct { func (m *dimmableMeter) Dimmed() (bool, error) { m.gets++ - return m.dimmed, nil + return m.dimmed, m.getErr } func (m *dimmableMeter) Dim(dim bool) error { @@ -116,12 +131,16 @@ func (m *dimmableMeter) Dim(dim bool) error { return nil } +func dimSite(m api.Meter) *Site { + return &Site{ + log: util.NewLogger("foo"), + auxMeters: []config.Device[api.Meter]{config.NewStaticDevice(config.Named{}, m)}, + } +} + func TestDimMetersCache(t *testing.T) { m := &dimmableMeter{} - site := &Site{ - log: util.NewLogger("foo"), - auxMeters: []config.Device[api.Meter]{config.NewStaticDevice(config.Named{}, api.Meter(m))}, - } + site := dimSite(m) require.NoError(t, site.dimMeters(true)) assert.Equal(t, []bool{true}, m.dimCalls) @@ -142,3 +161,17 @@ func TestDimMetersCache(t *testing.T) { require.Error(t, site.dimMeters(true)) assert.Equal(t, []bool{true, false, true, true}, m.dimCalls) } + +// A device that cannot report its state is written once, not on every cycle. +func TestDimMetersNotAvailable(t *testing.T) { + m := &dimmableMeter{getErr: api.ErrNotAvailable} + site := dimSite(m) + + for range 3 { + require.NoError(t, site.dimMeters(true)) + } + assert.Equal(t, []bool{true}, m.dimCalls) + + require.NoError(t, site.dimMeters(false)) + assert.Equal(t, []bool{true, false}, m.dimCalls) +}