HEMS: redesign dim and curtail api as tristate (#30116)

This commit is contained in:
andig 2026-05-23 11:42:29 +02:00 • committed by GitHub
parent 3d59da473b
commit 8dc0edd8d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 67 additions and 54 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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