Optimizer: evaluate suggestion actionable flag on publish (#32045)
This commit is contained in:
parent
ea6cc99094
commit
de4c63d182
3 changed files with 103 additions and 50 deletions
|
|
@ -111,6 +111,7 @@ type Site struct {
|
|||
batteryModeExternal api.BatteryMode // Battery mode (external, runtime only, not persisted)
|
||||
batteryModeExternalTimer time.Time // Battery mode timer for external control
|
||||
batterySuggestions map[string]types.Suggestion // Optimizer suggestions by battery meter name
|
||||
loadpointSuggestions map[int]types.Suggestion // Optimizer suggestions by loadpoint id
|
||||
}
|
||||
|
||||
// MetersConfig contains the site's meter configuration
|
||||
|
|
@ -1182,6 +1183,9 @@ func (site *Site) update(lp updater) {
|
|||
site.publish(keys.BatteryGridChargeActive, batteryGridChargeActive)
|
||||
site.updateBatteryMode(batteryGridChargeActive, rate)
|
||||
|
||||
// re-evaluate against the updated loadpoint state
|
||||
site.publishSuggestions()
|
||||
|
||||
site.stats.Update(site)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ const (
|
|||
// maps cleanly onto the discrete battery mode / loadpoint intent that control would later apply.
|
||||
// An idle battery is interpreted from the grid flow: importing means discharge is withheld
|
||||
// (hold), exporting means charging is withheld (holdcharge).
|
||||
func currentSlotSuggestion(detail batteryDetail, res optimizer.BatteryResult, gridImporting, gridExporting bool, slotHours float64, current string) types.Suggestion {
|
||||
func currentSlotSuggestion(detail batteryDetail, res optimizer.BatteryResult, gridImporting, gridExporting bool, slotHours float64) types.Suggestion {
|
||||
if slotHours <= 0 || len(res.ChargingPower) == 0 || len(res.DischargingPower) == 0 {
|
||||
return types.Suggestion{}
|
||||
}
|
||||
|
|
@ -155,9 +155,6 @@ func currentSlotSuggestion(detail batteryDetail, res optimizer.BatteryResult, gr
|
|||
s.Action = actionStop
|
||||
}
|
||||
|
||||
// actionable when the suggested action differs from the current operating mode
|
||||
s.Actionable = s.Action != current
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
|
|
@ -176,33 +173,68 @@ func loadpointCurrentAction(lp *Loadpoint) string {
|
|||
return actionStop
|
||||
}
|
||||
|
||||
// setBatterySuggestions replaces the suggestions applied on each battery publish
|
||||
func (site *Site) setBatterySuggestions(suggestions map[string]types.Suggestion) {
|
||||
// setSuggestions replaces the suggestions applied on each publish
|
||||
func (site *Site) setSuggestions(batteries map[string]types.Suggestion, loadpoints map[int]types.Suggestion) {
|
||||
site.Lock()
|
||||
defer site.Unlock()
|
||||
|
||||
site.batterySuggestions = suggestions
|
||||
site.batterySuggestions = batteries
|
||||
site.loadpointSuggestions = loadpoints
|
||||
}
|
||||
|
||||
// batterySuggestion returns the optimizer suggestion for the given battery meter
|
||||
// batterySuggestion returns the optimizer suggestion for the given battery meter.
|
||||
// The actionable flag is evaluated on read since the battery mode changes between
|
||||
// optimizer runs.
|
||||
func (site *Site) batterySuggestion(name string) *types.Suggestion {
|
||||
mode := site.GetBatteryMode().String()
|
||||
|
||||
site.RLock()
|
||||
defer site.RUnlock()
|
||||
|
||||
if s, ok := site.batterySuggestions[name]; ok {
|
||||
return &s
|
||||
s, ok := site.batterySuggestions[name]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.Actionable = s.Action != mode
|
||||
|
||||
return &s
|
||||
}
|
||||
|
||||
// loadpointSuggestion returns the optimizer suggestion for the given loadpoint.
|
||||
// The actionable flag is evaluated on read since the loadpoint's action changes
|
||||
// between optimizer runs.
|
||||
func (site *Site) loadpointSuggestion(id int) *types.Suggestion {
|
||||
site.RLock()
|
||||
s, ok := site.loadpointSuggestions[id]
|
||||
site.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
s.Actionable = s.Action != loadpointCurrentAction(site.loadpoints[id])
|
||||
|
||||
return &s
|
||||
}
|
||||
|
||||
// publishSuggestions publishes the loadpoints' suggestions
|
||||
func (site *Site) publishSuggestions() {
|
||||
for id := range site.loadpoints {
|
||||
var val any
|
||||
if s := site.loadpointSuggestion(id); s != nil {
|
||||
val = *s
|
||||
}
|
||||
site.publishLoadpoint(id, keys.Suggestion, val)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearSuggestions removes all suggestions when the optimizer result is stale
|
||||
func (site *Site) clearSuggestions() {
|
||||
site.setBatterySuggestions(nil)
|
||||
site.publishBattery()
|
||||
site.setSuggestions(nil, nil)
|
||||
|
||||
for id := range site.Loadpoints() {
|
||||
site.publishLoadpoint(id, keys.Suggestion, nil)
|
||||
}
|
||||
site.publishBattery()
|
||||
site.publishSuggestions()
|
||||
}
|
||||
|
||||
type requestDetails struct {
|
||||
|
|
@ -461,15 +493,7 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error {
|
|||
|
||||
batteries = append(batteries, batResult)
|
||||
|
||||
// current operating mode to detect an actionable change
|
||||
var current string
|
||||
if detail.Type == batteryTypeBattery {
|
||||
current = site.GetBatteryMode().String()
|
||||
} else if detail.loadpoint != nil {
|
||||
current = loadpointCurrentAction(site.loadpoints[*detail.loadpoint])
|
||||
}
|
||||
|
||||
suggestion := currentSlotSuggestion(detail, batResp, gridImporting, gridExporting, slotHours, current)
|
||||
suggestion := currentSlotSuggestion(detail, batResp, gridImporting, gridExporting, slotHours)
|
||||
if suggestion.Action == "" {
|
||||
continue
|
||||
}
|
||||
|
|
@ -485,19 +509,13 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error {
|
|||
|
||||
site.publish("evopt-batteries", batteries)
|
||||
|
||||
site.setBatterySuggestions(suggestions)
|
||||
site.setSuggestions(suggestions, lpSuggestions)
|
||||
site.battery.Forecast = site.addBatteryForecastTotals(req.Batteries, resp.JSON200.Batteries)
|
||||
|
||||
site.publishBattery()
|
||||
|
||||
// publish for all loadpoints so suggestions of dropped-out loadpoints clear
|
||||
for id := range site.Loadpoints() {
|
||||
var val any
|
||||
if s, ok := lpSuggestions[id]; ok {
|
||||
val = s
|
||||
}
|
||||
site.publishLoadpoint(id, keys.Suggestion, val)
|
||||
}
|
||||
site.publishSuggestions()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -289,36 +289,67 @@ func TestCurrentSlotSuggestion(t *testing.T) {
|
|||
typ batteryType
|
||||
charge, disch float32
|
||||
importing, export bool
|
||||
current string // current operating mode
|
||||
want string
|
||||
wantActionable bool
|
||||
}{
|
||||
{"battery grid charge", batteryTypeBattery, 3000, 0, true, false, "normal", "charge", true},
|
||||
{"battery grid charge unchanged", batteryTypeBattery, 3000, 0, true, false, "charge", "charge", false},
|
||||
{"battery pv charge (no import)", batteryTypeBattery, 3000, 0, false, true, "normal", "normal", false},
|
||||
{"battery hold (idle while importing)", batteryTypeBattery, 0, 0, true, false, "normal", "hold", true},
|
||||
{"battery holdcharge (idle while exporting)", batteryTypeBattery, 0, 0, false, true, "normal", "holdcharge", true},
|
||||
{"battery discharge", batteryTypeBattery, 0, 2000, true, false, "normal", "normal", false},
|
||||
{"battery idle balanced", batteryTypeBattery, 0, 0, false, false, "normal", "normal", false},
|
||||
{"loadpoint charge", batteryTypeLoadpoint, 11000, 0, false, false, "stop", "charge", true},
|
||||
{"loadpoint charge unchanged", batteryTypeLoadpoint, 11000, 0, false, false, "charge", "charge", false},
|
||||
{"loadpoint stop", batteryTypeLoadpoint, 0, 0, false, false, "charge", "stop", true},
|
||||
{"loadpoint stop unchanged", batteryTypeLoadpoint, 0, 0, false, false, "stop", "stop", false},
|
||||
{"vehicle below threshold is stop", batteryTypeVehicle, 40, 0, false, false, "charge", "stop", true},
|
||||
{"battery grid charge", batteryTypeBattery, 3000, 0, true, false, "charge"},
|
||||
{"battery pv charge (no import)", batteryTypeBattery, 3000, 0, false, true, "normal"},
|
||||
{"battery hold (idle while importing)", batteryTypeBattery, 0, 0, true, false, "hold"},
|
||||
{"battery holdcharge (idle while exporting)", batteryTypeBattery, 0, 0, false, true, "holdcharge"},
|
||||
{"battery discharge", batteryTypeBattery, 0, 2000, true, false, "normal"},
|
||||
{"battery idle balanced", batteryTypeBattery, 0, 0, false, false, "normal"},
|
||||
{"loadpoint charge", batteryTypeLoadpoint, 11000, 0, false, false, "charge"},
|
||||
{"loadpoint stop", batteryTypeLoadpoint, 0, 0, false, false, "stop"},
|
||||
{"vehicle below threshold is stop", batteryTypeVehicle, 40, 0, false, false, "stop"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
res := optimizer.BatteryResult{
|
||||
ChargingPower: []float32{tc.charge},
|
||||
DischargingPower: []float32{tc.disch},
|
||||
}
|
||||
s := currentSlotSuggestion(batteryDetail{Type: tc.typ}, res, tc.importing, tc.export, 1, tc.current)
|
||||
s := currentSlotSuggestion(batteryDetail{Type: tc.typ}, res, tc.importing, tc.export, 1)
|
||||
assert.Equal(t, tc.want, s.Action)
|
||||
assert.Equal(t, tc.wantActionable, s.Actionable)
|
||||
assert.InDelta(t, tc.charge, s.Charge, 1e-3)
|
||||
assert.InDelta(t, tc.disch, s.Discharge, 1e-3)
|
||||
})
|
||||
}
|
||||
|
||||
// no result yields an empty suggestion
|
||||
assert.Empty(t, currentSlotSuggestion(batteryDetail{Type: batteryTypeBattery}, optimizer.BatteryResult{}, true, false, 1, ""))
|
||||
assert.Empty(t, currentSlotSuggestion(batteryDetail{Type: batteryTypeBattery}, optimizer.BatteryResult{}, true, false, 1))
|
||||
}
|
||||
|
||||
// TestSuggestionActionable ensures the actionable flag follows the current state
|
||||
// instead of the state at optimizer run time
|
||||
func TestSuggestionActionable(t *testing.T) {
|
||||
lp := NewLoadpoint(util.NewLogger("foo"), nil)
|
||||
|
||||
site := &Site{
|
||||
batteryMode: api.BatteryNormal,
|
||||
loadpoints: []*Loadpoint{lp},
|
||||
}
|
||||
site.setSuggestions(
|
||||
map[string]types.Suggestion{"bat": {Action: api.BatteryCharge.String()}},
|
||||
map[int]types.Suggestion{0: {Action: actionCharge}},
|
||||
)
|
||||
|
||||
// battery mode differs from suggestion
|
||||
s := site.batterySuggestion("bat")
|
||||
require.NotNil(t, s)
|
||||
assert.True(t, s.Actionable)
|
||||
|
||||
site.batteryMode = api.BatteryCharge
|
||||
assert.False(t, site.batterySuggestion("bat").Actionable)
|
||||
|
||||
assert.Nil(t, site.batterySuggestion("unknown"))
|
||||
|
||||
// loadpoint stopped, suggestion is to charge
|
||||
s = site.loadpointSuggestion(0)
|
||||
require.NotNil(t, s)
|
||||
assert.True(t, s.Actionable)
|
||||
|
||||
// loadpoint charging matches the suggestion
|
||||
lp.enabled = true
|
||||
lp.status = api.StatusC
|
||||
assert.False(t, site.loadpointSuggestion(0).Actionable)
|
||||
|
||||
assert.Nil(t, site.loadpointSuggestion(1))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue