-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
hems/fnn: add failsafeConsumptionActivePowerLimit and failsafeDurationMinimum support #31928
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
b30fa57
9d44aad
0fac98a
e65d376
043964f
3521b65
3d1966e
6d8054b
446e6ba
adbbc83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,11 +1,14 @@ | ||||||||||||||||
| package fnn | ||||||||||||||||
|
|
||||||||||||||||
| import ( | ||||||||||||||||
| "errors" | ||||||||||||||||
| "testing" | ||||||||||||||||
| "time" | ||||||||||||||||
|
|
||||||||||||||||
| "github.com/evcc-io/evcc/core/site" | ||||||||||||||||
| "github.com/evcc-io/evcc/hems/hems" | ||||||||||||||||
| "github.com/evcc-io/evcc/server/db" | ||||||||||||||||
| "github.com/evcc-io/evcc/util" | ||||||||||||||||
| "github.com/stretchr/testify/assert" | ||||||||||||||||
| "github.com/stretchr/testify/require" | ||||||||||||||||
| ) | ||||||||||||||||
|
|
@@ -21,12 +24,16 @@ func boolG(v bool) func() (bool, error) { | |||||||||||||||
| return func() (bool, error) { return v, nil } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| func errG() func() (bool, error) { | ||||||||||||||||
| return func() (bool, error) { return false, errors.New("read error") } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // TestCurtailmentNotConfigured verifies that without W3 no curtailment | ||||||||||||||||
| // statement is made, while dimming via W4 remains available. | ||||||||||||||||
| func TestCurtailmentNotConfigured(t *testing.T) { | ||||||||||||||||
| require.NoError(t, db.NewInstance("sqlite", ":memory:")) | ||||||||||||||||
|
|
||||||||||||||||
| fnn, err := NewFnn(&stubSite{}, 1e3, 1e3, nil, nil, nil, boolG(true), 0) | ||||||||||||||||
| fnn, err := NewFnn(&stubSite{}, 1e3, 1e3, nil, nil, nil, boolG(true), 0, 0, 0) | ||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||
|
|
||||||||||||||||
| assert.Nil(t, fnn.CurtailedPercent()) | ||||||||||||||||
|
|
@@ -40,7 +47,7 @@ func TestCurtailmentNotConfigured(t *testing.T) { | |||||||||||||||
| // TestDimmingNotConfigured verifies that without W4 no dimming statement is | ||||||||||||||||
| // made, while curtailment via W3 remains available. | ||||||||||||||||
| func TestDimmingNotConfigured(t *testing.T) { | ||||||||||||||||
| fnn, err := NewFnn(&stubSite{}, 0, 1e3, boolG(false), nil, nil, nil, 0) | ||||||||||||||||
| fnn, err := NewFnn(&stubSite{}, 0, 1e3, boolG(false), nil, nil, nil, 0, 0, 0) | ||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||
|
|
||||||||||||||||
| assert.Nil(t, fnn.MaxConsumptionPower()) | ||||||||||||||||
|
|
@@ -49,3 +56,88 @@ func TestDimmingNotConfigured(t *testing.T) { | |||||||||||||||
| assert.NotNil(t, fnn.CurtailedPercent()) | ||||||||||||||||
| assert.NotNil(t, hems.Curtailed(fnn)) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // TestDecodeConfig verifies that failsafe config keys are accepted and decoded. | ||||||||||||||||
| func TestDecodeConfig(t *testing.T) { | ||||||||||||||||
| require.NoError(t, db.NewInstance("sqlite", ":memory:")) | ||||||||||||||||
| _ = util.NewLogger("fnn") | ||||||||||||||||
|
|
||||||||||||||||
| other := map[string]any{ | ||||||||||||||||
| "maxDimPower": 4200, | ||||||||||||||||
| "failsafeConsumptionActivePowerLimit": 4200, | ||||||||||||||||
| "failsafeDurationMinimum": "30m", | ||||||||||||||||
| "w4": map[string]any{ | ||||||||||||||||
| "source": "const", | ||||||||||||||||
| "value": false, | ||||||||||||||||
| }, | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| f, err := NewFromConfig(t.Context(), other, &stubSite{}) | ||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||
| assert.Equal(t, 4200.0, f.failsafeConsumptionLimit) | ||||||||||||||||
| assert.Equal(t, 30*time.Minute, f.failsafeDurationMinimum) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // TestFailsafeActivatesOnReadError verifies that a W4 read error triggers failsafe | ||||||||||||||||
| // mode and the configured failsafe consumption limit is applied. | ||||||||||||||||
| func TestFailsafeActivatesOnReadError(t *testing.T) { | ||||||||||||||||
| require.NoError(t, db.NewInstance("sqlite", ":memory:")) | ||||||||||||||||
|
|
||||||||||||||||
| const failsafeLimit = 4200.0 | ||||||||||||||||
| fnn, err := NewFnn(&stubSite{}, failsafeLimit, 0, nil, nil, nil, errG(), 0, failsafeLimit, 0) | ||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||
|
|
||||||||||||||||
| // construction calls runDim once — failsafe should already be active | ||||||||||||||||
| assert.True(t, fnn.failsafeActive) | ||||||||||||||||
| require.NotNil(t, fnn.MaxConsumptionPower()) | ||||||||||||||||
| assert.Equal(t, failsafeLimit, *fnn.MaxConsumptionPower()) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // TestFailsafeExitsAfterDuration verifies that failsafe is released once | ||||||||||||||||
| // failsafeDurationMinimum has elapsed and a successful read follows. | ||||||||||||||||
| func TestFailsafeExitsAfterDuration(t *testing.T) { | ||||||||||||||||
| require.NoError(t, db.NewInstance("sqlite", ":memory:")) | ||||||||||||||||
|
|
||||||||||||||||
| const failsafeLimit = 4200.0 | ||||||||||||||||
| // duration of 0 means failsafe exits on next successful read | ||||||||||||||||
| fnn, err := NewFnn(&stubSite{}, failsafeLimit, 0, nil, nil, nil, errG(), 0, failsafeLimit, 0) | ||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||
| assert.True(t, fnn.failsafeActive) | ||||||||||||||||
|
|
||||||||||||||||
| // switch to successful read | ||||||||||||||||
| fnn.w4 = boolG(false) | ||||||||||||||||
| require.NoError(t, fnn.runDim()) | ||||||||||||||||
|
|
||||||||||||||||
| assert.False(t, fnn.failsafeActive) | ||||||||||||||||
| require.NotNil(t, fnn.MaxConsumptionPower()) | ||||||||||||||||
| assert.Equal(t, 0.0, *fnn.MaxConsumptionPower()) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // TestFailsafeRemainsActiveDuringDuration verifies that failsafe stays active | ||||||||||||||||
| // while failsafeDurationMinimum has not yet elapsed, even when reads succeed. | ||||||||||||||||
| func TestFailsafeRemainsActiveDuringDuration(t *testing.T) { | ||||||||||||||||
| require.NoError(t, db.NewInstance("sqlite", ":memory:")) | ||||||||||||||||
|
|
||||||||||||||||
| const failsafeLimit = 4200.0 | ||||||||||||||||
| fnn, err := NewFnn(&stubSite{}, failsafeLimit, 0, nil, nil, nil, errG(), 0, failsafeLimit, time.Hour) | ||||||||||||||||
| require.NoError(t, err) | ||||||||||||||||
| assert.True(t, fnn.failsafeActive) | ||||||||||||||||
|
|
||||||||||||||||
| // switch to successful read, but duration not yet elapsed | ||||||||||||||||
| fnn.w4 = boolG(false) | ||||||||||||||||
| require.NoError(t, fnn.runDim()) | ||||||||||||||||
|
|
||||||||||||||||
| assert.True(t, fnn.failsafeActive) | ||||||||||||||||
| require.NotNil(t, fnn.MaxConsumptionPower()) | ||||||||||||||||
| assert.Equal(t, failsafeLimit, *fnn.MaxConsumptionPower()) | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| // TestFailsafeNotConfiguredPropagatesError verifies that without a configured | ||||||||||||||||
| // failsafe limit, a W4 read error is returned to the caller unchanged. | ||||||||||||||||
| func TestFailsafeNotConfiguredPropagatesError(t *testing.T) { | ||||||||||||||||
|
Comment on lines
+144
to
+146
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (testing): Test description suggests propagation from runDim, but the test only covers NewFnn failing The comment for TestFailsafeNotConfiguredPropagatesError says the W4 read error is “returned to the caller unchanged”, which implies it is validating runDim’s error propagation. In reality, this test only checks that NewFnn fails during construction because its internal runDim call sees a W4 read error with no failsafe limit. Please either update the test name/comment to clearly state it covers construction-time behavior, or add coverage for a direct runDim call (if that path is used) that asserts the original error is propagated as described.
Suggested change
|
||||||||||||||||
| require.NoError(t, db.NewInstance("sqlite", ":memory:")) | ||||||||||||||||
|
|
||||||||||||||||
| // no failsafe limit: construction error because runDim is called in NewFnn | ||||||||||||||||
| _, err := NewFnn(&stubSite{}, 1e3, 0, nil, nil, nil, errG(), 0, 0, 0) | ||||||||||||||||
| assert.Error(t, err) | ||||||||||||||||
| } | ||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
suggestion (bug_risk): On w4 read error, consider logging the error details and ensuring setConsumptionLimit interactions are safe under concurrent use.
The WARN log in the error path omits
err; including it would significantly improve troubleshootability. Also,setConsumptionLimitis invoked afterc.muis unlocked. Please confirm thatsetConsumptionLimitis safe to call concurrently and does not depend on the same invariants protected byc.mu(e.g.,failsafeActive), or otherwise consider keeping the call under the lock or documenting the assumptions.Suggested implementation:
setConsumptionLimitdoes not itself acquirec.mu; if it does, adjust the locking strategy to avoid self-deadlock (e.g., keepsetConsumptionLimitlock-free and rely on the caller's lock).setConsumptionLimitis intended to be callable concurrently withoutc.mu, instead leave the call outside the critical section and add a comment documenting that it is concurrency-safe and does not rely onfailsafeActive/failsafeEnteredAtinvariants.runDimwith injectedw4errors to verify that failsafe activation andsetConsumptionLimitbehavior remain consistent under concurrent use.