From 4982bcc025d38d4a7cc6be8280af561231877a3e Mon Sep 17 00:00:00 2001 From: andig Date: Sun, 14 Jun 2026 11:21:01 +0200 Subject: [PATCH] Revert "Optimizer: derive per-device action suggestions from result" (#30833) --- core/site_optimizer.go | 65 ++----------------------------------- core/site_optimizer_test.go | 35 -------------------- 2 files changed, 3 insertions(+), 97 deletions(-) diff --git a/core/site_optimizer.go b/core/site_optimizer.go index 0553833db..ab52df9ee 100644 --- a/core/site_optimizer.go +++ b/core/site_optimizer.go @@ -71,61 +71,8 @@ type batteryDetail struct { type batteryResult struct { batteryDetail - Full time.Time `json:"full,omitzero"` - Empty time.Time `json:"empty,omitzero"` - Suggestion batterySuggestion `json:"suggestion,omitzero"` -} - -// batterySuggestion is the advisory action derived from the optimizer corner result for the -// current slot. It is published for visibility only and not yet wired into control. -type batterySuggestion struct { - // Action is the recommended action for the current slot. - // home battery: normal|hold|charge|holdcharge; loadpoint/vehicle: charge|stop - Action string `json:"action,omitempty"` - Charge float64 `json:"charge,omitempty"` // recommended charge power, W - Discharge float64 `json:"discharge,omitempty"` // recommended discharge power, W -} - -// suggestionThreshold ignores numerical noise around zero power (W) -const suggestionThreshold = 50 - -// currentSlotSuggestion maps the optimizer's first-slot corner result onto an advisory action. -// Because the optimization is linear, the first slot is at an operating-range extreme, so it -// 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) batterySuggestion { - if slotHours <= 0 || len(res.ChargingPower) == 0 || len(res.DischargingPower) == 0 { - return batterySuggestion{} - } - - charge := float64(res.ChargingPower[0]) / slotHours - discharge := float64(res.DischargingPower[0]) / slotHours - - s := batterySuggestion{Charge: charge, Discharge: discharge} - - if detail.Type == batteryTypeBattery { - idle := charge <= suggestionThreshold && discharge <= suggestionThreshold - switch { - case charge > suggestionThreshold && gridImporting: - // charging while importing means grid charging - s.Action = api.BatteryCharge.String() - case idle && gridImporting: - // idle while importing: discharge is deliberately withheld - s.Action = api.BatteryHold.String() - case idle && gridExporting: - // idle while exporting: surplus is exported instead of charged - s.Action = api.BatteryHoldCharge.String() - default: - s.Action = api.BatteryNormal.String() - } - } else if charge > suggestionThreshold { - s.Action = "charge" - } else { - s.Action = "stop" - } - - return s + Full time.Time `json:"full,omitzero"` + Empty time.Time `json:"empty,omitzero"` } type requestDetails struct { @@ -316,24 +263,18 @@ func (site *Site) optimizerUpdate(battery []types.Measurement) error { Details: details, }) - slotHours := firstSlotDuration.Hours() - gridImporting := len(resp.JSON200.GridImport) > 0 && resp.JSON200.GridImport[0] > 0 - gridExporting := len(resp.JSON200.GridExport) > 0 && resp.JSON200.GridExport[0] > 0 - var batteries []batteryResult for i, batReq := range req.Batteries { batResp := resp.JSON200.Batteries[i] - detail := details.BatteryDetails[i] batResult := batteryResult{ - batteryDetail: detail, + batteryDetail: details.BatteryDetails[i], Full: matchSoc(batResp.StateOfCharge, func(soc float32) bool { return soc >= batReq.SMax }), Empty: matchSoc(batResp.StateOfCharge, func(soc float32) bool { return soc <= batReq.SMin }), - Suggestion: currentSlotSuggestion(detail, batResp, gridImporting, gridExporting, slotHours), } batteries = append(batteries, batResult) diff --git a/core/site_optimizer_test.go b/core/site_optimizer_test.go index 9395379e9..9c5091985 100644 --- a/core/site_optimizer_test.go +++ b/core/site_optimizer_test.go @@ -148,38 +148,3 @@ func TestBatteryForecastSocExtremes(t *testing.T) { }) } } - -func TestCurrentSlotSuggestion(t *testing.T) { - // slotHours 1 makes the per-slot Wh values map 1:1 to W - for _, tc := range []struct { - name string - typ batteryType - charge, disch float32 - importing, export bool - want string - }{ - {"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) - assert.Equal(t, tc.want, s.Action) - 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.Equal(t, batterySuggestion{}, currentSlotSuggestion(batteryDetail{Type: batteryTypeBattery}, optimizer.BatteryResult{}, true, false, 1)) -}