From 8dc0edd8d9d21830c53db92824621a5f4283060c Mon Sep 17 00:00:00 2001 From: andig Date: Sat, 23 May 2026 11:42:29 +0200 Subject: [PATCH] HEMS: redesign dim and curtail api as tristate (#30116) --- api/api.go | 4 ++-- api/mock.go | 8 ++++---- core/circuit/circuit.go | 24 ++++++++++++------------ core/helper.go | 8 ++++---- core/loadpoint.go | 22 +++++++++++----------- core/site_battery.go | 2 +- core/site_circuit_test.go | 23 +++++++++++++++-------- core/site_circuits.go | 30 ++++++++++++++++++------------ 8 files changed, 67 insertions(+), 54 deletions(-) diff --git a/api/api.go b/api/api.go index d92a7e65b..f2be451dc 100644 --- a/api/api.go +++ b/api/api.go @@ -280,11 +280,11 @@ type Circuit interface { // EnWG §14a - reduce demand/consumption Dim(bool) - Dimmed() bool + Dimmed() *bool // EEG §9 - reduce feed-in to the grid Curtail(bool) - Curtailed() bool + Curtailed() *bool } // Redactor is an interface to redact sensitive data diff --git a/api/mock.go b/api/mock.go index 4591c1e38..937dd667f 100644 --- a/api/mock.go +++ b/api/mock.go @@ -869,10 +869,10 @@ func (mr *MockCircuitMockRecorder) Curtail(arg0 any) *gomock.Call { } // Curtailed mocks base method. -func (m *MockCircuit) Curtailed() bool { +func (m *MockCircuit) Curtailed() *bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Curtailed") - ret0, _ := ret[0].(bool) + ret0, _ := ret[0].(*bool) return ret0 } @@ -895,10 +895,10 @@ func (mr *MockCircuitMockRecorder) Dim(arg0 any) *gomock.Call { } // Dimmed mocks base method. -func (m *MockCircuit) Dimmed() bool { +func (m *MockCircuit) Dimmed() *bool { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Dimmed") - ret0, _ := ret[0].(bool) + ret0, _ := ret[0].(*bool) return ret0 } diff --git a/core/circuit/circuit.go b/core/circuit/circuit.go index d31670bdf..6a018477e 100644 --- a/core/circuit/circuit.go +++ b/core/circuit/circuit.go @@ -36,8 +36,8 @@ type Circuit struct { current float64 power float64 - dimmed bool - curtailed bool + dimmed *bool + curtailed *bool currentUpdated time.Time powerUpdated time.Time @@ -394,19 +394,19 @@ func (c *Circuit) ValidateCurrent(old, new float64) float64 { func (c *Circuit) Dim(dim bool) { c.mu.Lock() defer c.mu.Unlock() - c.dimmed = dim + c.dimmed = &dim } -func (c *Circuit) Dimmed() bool { +func (c *Circuit) Dimmed() *bool { c.mu.RLock() defer c.mu.RUnlock() - if c.dimmed { - return true + if c.dimmed != nil { + return c.dimmed } if c.parent == nil { - return false + return nil } return c.parent.Dimmed() @@ -415,19 +415,19 @@ func (c *Circuit) Dimmed() bool { func (c *Circuit) Curtail(curtail bool) { c.mu.Lock() defer c.mu.Unlock() - c.curtailed = curtail + c.curtailed = &curtail } -func (c *Circuit) Curtailed() bool { +func (c *Circuit) Curtailed() *bool { c.mu.RLock() defer c.mu.RUnlock() - if c.curtailed { - return true + if c.curtailed != nil { + return c.curtailed } if c.parent == nil { - return false + return nil } return c.parent.Curtailed() diff --git a/core/helper.go b/core/helper.go index 37b006b4f..2db51326e 100644 --- a/core/helper.go +++ b/core/helper.go @@ -82,18 +82,18 @@ func circuitMaxPower(circuit api.Circuit) float64 { } // circuitDimmed returns a circuits dim status -func circuitDimmed(circuit api.Circuit) bool { +func circuitDimmed(circuit api.Circuit) *bool { if circuit == nil { - return false + return nil } return circuit.Dimmed() } // circuitCurtailed returns a circuit's curtail status -func circuitCurtailed(circuit api.Circuit) bool { +func circuitCurtailed(circuit api.Circuit) *bool { if circuit == nil { - return false + return nil } return circuit.Curtailed() diff --git a/core/loadpoint.go b/core/loadpoint.go index 17de6ba7e..fba363314 100644 --- a/core/loadpoint.go +++ b/core/loadpoint.go @@ -1963,20 +1963,20 @@ func (lp *Loadpoint) Update(sitePower, batteryBoostPower float64, consumption, f return } - dim := lp.circuit != nil && lp.circuit.Dimmed() + if dim := circuitDimmed(lp.circuit); dim != nil { + if *dim != dimmed { + if err := dimmer.Dim(*dim); err != nil { + lp.log.ERROR.Printf("dim: %v", err) + return + } - if dim != dimmed { - if err := dimmer.Dim(dim); err != nil { - lp.log.ERROR.Printf("dim: %v", err) - return + lp.publish(keys.Dimmed, *dim) + lp.log.INFO.Printf("§14a dim: %t", *dim) } - lp.publish(keys.Dimmed, dim) - lp.log.INFO.Printf("§14a dim: %t", dim) - } - - if dim { - return + if *dim { + return + } } } diff --git a/core/site_battery.go b/core/site_battery.go index b22d4d7ce..a052bc74c 100644 --- a/core/site_battery.go +++ b/core/site_battery.go @@ -57,7 +57,7 @@ func (site *Site) updateBatteryMode(batteryGridChargeActive bool, rate api.Rate) // put battery into hold mode when charging is active and circuit dimmed fromToCharge := batteryMode == api.BatteryCharge || batteryMode == api.BatteryUnknown && site.batteryMode == api.BatteryCharge - if fromToCharge && circuitDimmed(site.circuit) { + if dimmed := circuitDimmed(site.circuit); fromToCharge && dimmed != nil && *dimmed { site.log.DEBUG.Println("battery mode: circuit dimmed") batteryMode = api.BatteryHold } diff --git a/core/site_circuit_test.go b/core/site_circuit_test.go index 49481139f..d36256417 100644 --- a/core/site_circuit_test.go +++ b/core/site_circuit_test.go @@ -29,18 +29,25 @@ func TestDimming(t *testing.T) { } for _, tc := range []struct { - has, want bool + has bool + want *bool }{ - {has: false, want: false}, - {has: false, want: true}, - {has: true, want: false}, - {has: true, want: true}, + // nil: circuit has no opinion - the device must not be touched, so a + // limit configured outside evcc is preserved (issue #30068) + {has: false, want: nil}, + {has: true, want: nil}, + {has: false, want: new(false)}, + {has: false, want: new(true)}, + {has: true, want: new(false)}, + {has: true, want: new(true)}, } { t.Logf("%+v", tc) - dimmer.EXPECT().Dimmed().Return(tc.has, nil) - if tc.has != tc.want { - dimmer.EXPECT().Dim(tc.want).Return(nil) + if tc.want != nil { + dimmer.EXPECT().Dimmed().Return(tc.has, nil) + if tc.has != *tc.want { + dimmer.EXPECT().Dim(*tc.want).Return(nil) + } } require.NoError(t, s.dimMeters(tc.want)) diff --git a/core/site_circuits.go b/core/site_circuits.go index 4d9372ff0..f1dc2c800 100644 --- a/core/site_circuits.go +++ b/core/site_circuits.go @@ -19,8 +19,8 @@ type circuitStruct struct { Current *float64 `json:"current,omitempty"` MaxPower float64 `json:"maxPower,omitempty"` MaxCurrent float64 `json:"maxCurrent,omitempty"` - Dimmed bool `json:"dimmed"` - Curtailed bool `json:"curtailed"` + Dimmed *bool `json:"dimmed,omitempty"` + Curtailed *bool `json:"curtailed,omitempty"` } // publishCircuits returns a list of circuit titles @@ -58,9 +58,12 @@ func (site *Site) publishCircuits() { site.publish(keys.Circuits, res) } -func (site *Site) dimMeters(dim bool) error { - var errs error +func (site *Site) dimMeters(dim *bool) error { + if dim == nil { + return nil + } + var errs error for _, dev := range slices.Concat(site.auxMeters, site.extMeters) { m, ok := api.Cap[api.Dimmer](dev.Instance()) if !ok { @@ -68,7 +71,7 @@ func (site *Site) dimMeters(dim bool) error { } if dimmed, err := m.Dimmed(); err == nil { - if dim == dimmed { + if *dim == dimmed { continue } } else { @@ -78,8 +81,8 @@ func (site *Site) dimMeters(dim bool) error { continue } - if err := m.Dim(dim); err == nil { - site.log.DEBUG.Printf("%s dim: %t", dev.Config().Name, dim) + if err := m.Dim(*dim); err == nil { + site.log.DEBUG.Printf("%s dim: %t", dev.Config().Name, *dim) } else if !errors.Is(err, api.ErrNotAvailable) { errs = errors.Join(errs, fmt.Errorf("%s dim: %w", dev.Config().Name, err)) } @@ -88,9 +91,12 @@ func (site *Site) dimMeters(dim bool) error { return errs } -func (site *Site) curtailPV(curtail bool) error { - var errs error +func (site *Site) curtailPV(curtail *bool) error { + if curtail == nil { + return nil + } + var errs error for _, dev := range site.pvMeters { m, ok := api.Cap[api.Curtailer](dev.Instance()) if !ok { @@ -98,7 +104,7 @@ func (site *Site) curtailPV(curtail bool) error { } if curtailed, err := m.Curtailed(); err == nil { - if curtail == curtailed { + if *curtail == curtailed { continue } } else { @@ -108,8 +114,8 @@ func (site *Site) curtailPV(curtail bool) error { continue } - if err := m.Curtail(curtail); err == nil { - site.log.DEBUG.Printf("%s curtail: %t", dev.Config().Name, curtail) + if err := m.Curtail(*curtail); err == nil { + site.log.DEBUG.Printf("%s curtail: %t", dev.Config().Name, *curtail) } else if !errors.Is(err, api.ErrNotAvailable) { errs = errors.Join(errs, fmt.Errorf("%s curtail: %w", dev.Config().Name, err)) }