core: apply dim/curtail to devices that cannot report their state (#32198)

This commit is contained in:
andig 2026-07-27 19:05:34 +02:00 • committed by GitHub
parent 8bbbc2497d
commit 608575ffec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 55 additions and 24 deletions

View file

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

View file

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