diff --git a/charger/eebus-ohpcf.go b/charger/eebus-ohpcf.go index 540e1a95d..ff145c03b 100644 --- a/charger/eebus-ohpcf.go +++ b/charger/eebus-ohpcf.go @@ -148,7 +148,7 @@ func (c *EEBusOHPCF) UseCaseEvent(_ spineapi.DeviceRemoteInterface, entity spine // react immediately to a freshly announced schedule/resume opportunity // instead of waiting for the next reboost tick, which may miss it (#31549) if c.lastEnabled() { - if err := c.apply(); err != nil { + if err := c.apply(true); err != nil { c.log.DEBUG.Printf("apply: %v", err) } } @@ -252,13 +252,19 @@ func (c *EEBusOHPCF) Enabled() (bool, error) { // Enable schedules/resumes the optional consumption when on, pauses/aborts it // when off; while on a reboost loop reschedules newly announced consumption. func (c *EEBusOHPCF) Enable(enable bool) error { + // record the intent only once accepted, otherwise Enabled() would report a + // state the compressor never reached and the loadpoint runs out of sync + if err := c.apply(enable); err != nil { + return err + } + c.setEnabled(enable) if enable { c.startReboost() } - return c.apply() + return nil } // startReboost launches the reboost loop, unless one is already running or no @@ -296,7 +302,7 @@ func (c *EEBusOHPCF) reboostLoop() { if !c.lastEnabled() { return } - if err := c.apply(); err != nil { + if err := c.apply(true); err != nil { c.log.DEBUG.Printf("reboost: %v", err) } } @@ -355,7 +361,7 @@ func (c *EEBusOHPCF) stop(entity spineapi.EntityRemoteInterface) error { // MaxCurrent implements the api.Charger interface. OHPCF is on/off and cannot // be modulated, so the offered current is ignored. func (c *EEBusOHPCF) MaxCurrent(int64) error { - return c.apply() + return c.apply(c.lastEnabled()) } var _ api.Dimmer = (*EEBusOHPCF)(nil) @@ -406,7 +412,7 @@ func (c *EEBusOHPCF) Dim(dim bool) error { // apply issues the command to align the optional consumption with the on/off // intent. It is idempotent: ohpcfControlAction only acts on a state transition. -func (c *EEBusOHPCF) apply() error { +func (c *EEBusOHPCF) apply(enable bool) error { entity, ok := c.connectedCompressor() if !ok { return errNotConnected @@ -418,7 +424,7 @@ func (c *EEBusOHPCF) apply() error { return nil } - switch ohpcfControlAction(state, c.lastEnabled()) { + switch ohpcfControlAction(state, enable) { case ohpcfSchedule: return eebus.Await(func(cb func(model.ResultDataType, model.MsgCounterType)) (*model.MsgCounterType, error) { // 0 = start immediately (relative schedule, see SchedulePowerConsumptionProcess) diff --git a/charger/eebus-ohpcf_test.go b/charger/eebus-ohpcf_test.go index 78711b080..19dfe7697 100644 --- a/charger/eebus-ohpcf_test.go +++ b/charger/eebus-ohpcf_test.go @@ -30,6 +30,15 @@ func TestEEBusOHPCFNotConnected(t *testing.T) { require.ErrorIs(t, c.Dim(true), api.ErrNotAvailable) } +// a failed enable must not persist the intent, otherwise Enabled() reports a +// state the compressor never accepted and the loadpoint runs out of sync (#32252). +func TestOHPCFEnableFailureKeepsState(t *testing.T) { + c := &EEBusOHPCF{} + + require.ErrorIs(t, c.Enable(true), errNotConnected) + assert.False(t, c.lastEnabled()) +} + // status mapping: running is C, every other connected state (incl. completed // and stopped after a boost) is standby B, never disconnected. func TestOHPCFStatus(t *testing.T) { diff --git a/go.mod b/go.mod index 8cb2730f2..be3f2a2e7 100644 --- a/go.mod +++ b/go.mod @@ -268,6 +268,6 @@ replace github.com/grid-x/modbus => github.com/evcc-io/modbus v0.0.0-20250501165 replace github.com/lorenzodonini/ocpp-go => github.com/evcc-io/ocpp-go v0.0.0-20260727074919-195c10b8758d -replace github.com/enbility/spine-go => github.com/andig/spine-go v0.7.1-0.20260725155511-6f83690e6238 +replace github.com/enbility/spine-go => github.com/andig/spine-go v0.7.1-0.20260729105813-e3e33d07b702 replace github.com/enbility/eebus-go => github.com/andig/eebus-go v0.0.0-20260725155950-e735091ff165 diff --git a/go.sum b/go.sum index 26ee57ee3..cf5623454 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ github.com/andig/gosunspec v0.0.0-20260705113727-6d585e133512 h1:1y8dS4GaBB9WUu/ github.com/andig/gosunspec v0.0.0-20260705113727-6d585e133512/go.mod h1:c6P6szcR+ROkqZruOR4f6qbDKFjZX6OitPpj+yJ/r8k= github.com/andig/mbserver v0.0.0-20230310211055-1d29cbb5820e h1:m/NTP3JWpR7M0ljLxiQU4fzR25jjhe1LDtxLMNcoNJQ= github.com/andig/mbserver v0.0.0-20230310211055-1d29cbb5820e/go.mod h1:4VtYzTm//oUipwvO3yh0g/udTE7pYJM+U/kyAuFDsgM= -github.com/andig/spine-go v0.7.1-0.20260725155511-6f83690e6238 h1:VoAFMoBmdIhC6ki4IirAQVd7OPI3xhJG4wJIQpuDSts= -github.com/andig/spine-go v0.7.1-0.20260725155511-6f83690e6238/go.mod h1:ddWZU5BQGyzRGF20Z7rUKFFzbCp+cfu9mZgYtOMW/fM= +github.com/andig/spine-go v0.7.1-0.20260729105813-e3e33d07b702 h1:aSME6GPyu2LeTp282plM4nrCFBwBiNnnjWxuAIYGnhs= +github.com/andig/spine-go v0.7.1-0.20260729105813-e3e33d07b702/go.mod h1:ddWZU5BQGyzRGF20Z7rUKFFzbCp+cfu9mZgYtOMW/fM= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=