Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/published-prices-only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ftw": minor
---

The planner uses the electricity prices that are published, and no longer estimates prices beyond them. The plan reaches as far as the day-ahead prices, which is 11–35 hours: tomorrow's prices usually arrive around 13:00. The Plan view has no "predicted" zone any more. It says how far the plan reaches, for example "Plan until Sat 24:00", and the chart ends there. The Home Assistant plan and price attributes no longer include `confidence`, which was always 1 outside the removed forecast. An EV target later than the last published price is planned within the known prices until the next day's prices arrive.
26 changes: 1 addition & 25 deletions go/cmd/ftw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ import (
"github.com/srcfl/ftw/go/internal/notifications"
"github.com/srcfl/ftw/go/internal/nova"
"github.com/srcfl/ftw/go/internal/ocpp"
"github.com/srcfl/ftw/go/internal/priceforecast"
"github.com/srcfl/ftw/go/internal/prices"
"github.com/srcfl/ftw/go/internal/proxy"
"github.com/srcfl/ftw/go/internal/pvmodel"
Expand Down Expand Up @@ -930,7 +929,7 @@ func main() {

// Forward-declared so the hot-reload closure below can push
// capacity changes into the running planner. Assigned at line
// ~450 after all its dependencies (pvSvc, loadSvc, priceFc) are
// ~450 after all its dependencies (pvSvc, loadSvc) are
// wired up. nil until that point — the reload closure guards.
var mpcSvc *mpc.Service

Expand Down Expand Up @@ -1131,7 +1130,6 @@ func main() {
mpcSvc.ExportBonusOreKwh = newCfg.Price.ExportBonusOreKwh
mpcSvc.ExportFeeOreKwh = newCfg.Price.ExportFeeOreKwh
mpcSvc.ExportFloorOreKwh = newCfg.Price.ExportFloorOreKwh
mpcSvc.GridTariffOreKwh = newCfg.Price.GridTariffOreKwh
mpcSvc.VATPercent = newCfg.Price.VATPercent
mpcSvc.DemandPricePerKW = newCfg.Price.DemandPricePerKW
mpcSvc.DemandTopN = newCfg.Price.DemandTopN
Expand Down Expand Up @@ -1268,25 +1266,6 @@ func main() {

priceSvc = prices.FromConfig(cfg.Price, st, fxSvc)

// ---- Price forecaster (fills in beyond day-ahead publication) ----
zones := []string{"SE3"}
if cfg.Price != nil && cfg.Price.Zone != "" {
zones = []string{cfg.Price.Zone}
}
priceFc := priceforecast.NewService(st, zones)
// Optional: seed from bundled CSV on first boot. Idempotent so safe
// to call every boot — no-op once data is already in the store.
seedPath := filepath.Join(filepath.Dir(*configPath), "seed", "prices.csv")
if _, err := os.Stat(seedPath); err == nil {
n, err := priceFc.SeedFromCSV(seedPath)
if err != nil {
slog.Warn("priceforecast seed failed", "path", seedPath, "err", err)
} else if n > 0 {
slog.Info("priceforecast seeded", "rows", n, "path", seedPath)
}
}
priceFc.Start(ctx)
defer priceFc.Stop()
if priceSvc != nil {
priceSvc.Start(ctx)
defer priceSvc.Stop()
Expand Down Expand Up @@ -1650,7 +1629,6 @@ func main() {
mpcSvc.MinArbitrageSpreadOreKwh = cfg.Planner.MinArbitrageSpreadOreKwh
}
mpcSvc.Load = loadSvc.Predict
mpcSvc.Price = priceFc.Predict
mpcSvc.SiteMeter = cfg.SiteMeterDriver()
// The mathematical planner co-optimizes every scheduled loadpoint.
// The service retains the first entry for its Go-DP emergency fallback.
Expand Down Expand Up @@ -1837,7 +1815,6 @@ func main() {
mpcSvc.ExportBonusOreKwh = cfg.Price.ExportBonusOreKwh
mpcSvc.ExportFeeOreKwh = cfg.Price.ExportFeeOreKwh
mpcSvc.ExportFloorOreKwh = cfg.Price.ExportFloorOreKwh
mpcSvc.GridTariffOreKwh = cfg.Price.GridTariffOreKwh
mpcSvc.VATPercent = cfg.Price.VATPercent
mpcSvc.DemandPricePerKW = cfg.Price.DemandPricePerKW
mpcSvc.DemandTopN = cfg.Price.DemandTopN
Expand Down Expand Up @@ -4312,7 +4289,6 @@ func (b mpcPlanBridge) LatestActions() []ha.PlanAction {
PriceOre: a.PriceOre,
SpotOre: a.SpotOre,
CostOre: a.CostOre,
Confidence: a.Confidence,
Reason: a.Reason,
EMSMode: a.EMSMode,
PVW: a.PVW,
Expand Down
6 changes: 2 additions & 4 deletions go/internal/control/ev_site_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,10 @@ func TestEVSiteOptimizeThenDispatchChargesEVFromPVBesideBatteryImport(t *testing
slots := []mpc.Slot{
{
StartMs: slot.UnixMilli(), LenMin: 60,
PriceOre: 20, SpotOre: 10, LoadW: evComboLoadW, PVW: evComboPVW, Confidence: 1,
},
PriceOre: 20, SpotOre: 10, LoadW: evComboLoadW, PVW: evComboPVW},
{
StartMs: slot.Add(time.Hour).UnixMilli(), LenMin: 60,
PriceOre: 300, SpotOre: 240, LoadW: 2500, PVW: 0, Confidence: 1,
},
PriceOre: 300, SpotOre: 240, LoadW: 2500, PVW: 0},
}
params := mpc.Params{
Mode: mpc.ModeArbitrage,
Expand Down
18 changes: 8 additions & 10 deletions go/internal/ha/bridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ type PlanAction struct {
PriceOre float64 // total consumer price (öre/kWh)
SpotOre float64 // raw wholesale spot price (öre/kWh)
CostOre float64 // expected cost this slot (öre, negative = revenue)
Confidence float64 // forecast confidence 0–1
Reason string // human-readable DP reason
EMSMode string // effective EMS mode the planner chose
PVW float64 // planned PV output (site-sign, ≤ 0)
Expand Down Expand Up @@ -623,7 +622,7 @@ func (b *Bridge) publishDiscovery() {
total++

// Current electricity price sensor: state = total consumer price (öre/kWh),
// attrs = spot_ore, cost_ore, confidence, reason, ems_mode.
// attrs = spot_ore, cost_ore, reason, ems_mode.
priceMsg := b.withAvail(map[string]any{
"name": "Electricity Price",
"unique_id": b.deviceID + "_price_ore",
Expand Down Expand Up @@ -1151,7 +1150,7 @@ func (b *Bridge) announceVehicleDriver(dev map[string]any, driver string) {
// Assistant's recorder keeps it (#1296)
// - plan_schedule_json: the full schedule with every field, own topic
// - price_ore: current consumer electricity price (öre/kWh)
// - price_json: price attributes (spot_ore, cost_ore, confidence, reason, ems_mode)
// - price_json: price attributes (spot_ore, cost_ore, reason, ems_mode)
func (b *Bridge) publishPlan() {
actions := b.plan.LatestActions()
now := time.Now()
Expand All @@ -1160,7 +1159,7 @@ func (b *Bridge) publishPlan() {
currentAction := snapshot.Action
cur := snapshot.Current
curPVW, curLoadW := cur.PVW, cur.LoadW
curPriceOre, curSpotOre, curCostOre, curConfidence := cur.PriceOre, cur.SpotOre, cur.CostOre, cur.Confidence
curPriceOre, curSpotOre, curCostOre := cur.PriceOre, cur.SpotOre, cur.CostOre
curReason, curEMSMode := cur.Reason, cur.EMSMode

b.publishString("plan_action", currentAction)
Expand All @@ -1172,12 +1171,11 @@ func (b *Bridge) publishPlan() {
// Price sensor: standalone value + rich attributes for HA energy dashboard.
b.publishValue("price_ore", curPriceOre)
priceAttrs := map[string]any{
"price_ore": curPriceOre,
"spot_ore": curSpotOre,
"cost_ore": curCostOre,
"confidence": curConfidence,
"reason": curReason,
"ems_mode": curEMSMode,
"price_ore": curPriceOre,
"spot_ore": curSpotOre,
"cost_ore": curCostOre,
"reason": curReason,
"ems_mode": curEMSMode,
}
if d, err := json.Marshal(priceAttrs); err == nil {
b.publish(b.stateTopic("price_json"), d, false)
Expand Down
46 changes: 21 additions & 25 deletions go/internal/ha/plan_payload.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,33 +20,31 @@ const planAttributesBudget = 14 * 1024
// dashboard or automation needs per slot, rounded to one decimal. The full
// slot detail goes to plan_schedule_json.
type planSlot struct {
Start string `json:"start"`
Action string `json:"action"`
BatteryW float64 `json:"battery_w"`
GridW float64 `json:"grid_w"`
SoCPct float64 `json:"soc_pct"`
PriceOre float64 `json:"price_ore"`
Confidence float64 `json:"confidence"`
Start string `json:"start"`
Action string `json:"action"`
BatteryW float64 `json:"battery_w"`
GridW float64 `json:"grid_w"`
SoCPct float64 `json:"soc_pct"`
PriceOre float64 `json:"price_ore"`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Update the MQTT payload documentation after removing confidence

Removing confidence from both schedule payload structs makes the maintained integration guide incorrect: docs/ha-integration.md lines 49–55 still promises confidence in plan_json and describes plan_schedule_json as containing every field. Home Assistant or raw MQTT consumers following that contract will wait for a field that is no longer emitted, so update the documented payload alongside this wire-format change.

AGENTS.md reference: AGENTS.md:L321-L322

Useful? React with 👍 / 👎.

}

// planScheduleSlot is one entry of plan_schedule_json, the full schedule for
// MQTT consumers. It is not attached to a Home Assistant entity because it
// can exceed the recorder limit.
type planScheduleSlot struct {
Start string `json:"start"`
End string `json:"end"`
Action string `json:"action"`
BatteryW float64 `json:"battery_w"`
GridW float64 `json:"grid_w"`
SoCPct float64 `json:"soc_pct"`
PVW float64 `json:"pv_w,omitempty"`
LoadW float64 `json:"load_w,omitempty"`
PriceOre float64 `json:"price_ore,omitempty"`
SpotOre float64 `json:"spot_ore,omitempty"`
CostOre float64 `json:"cost_ore,omitempty"`
Confidence float64 `json:"confidence,omitempty"`
Reason string `json:"reason,omitempty"`
EMSMode string `json:"ems_mode,omitempty"`
Start string `json:"start"`
End string `json:"end"`
Action string `json:"action"`
BatteryW float64 `json:"battery_w"`
GridW float64 `json:"grid_w"`
SoCPct float64 `json:"soc_pct"`
PVW float64 `json:"pv_w,omitempty"`
LoadW float64 `json:"load_w,omitempty"`
PriceOre float64 `json:"price_ore,omitempty"`
SpotOre float64 `json:"spot_ore,omitempty"`
CostOre float64 `json:"cost_ore,omitempty"`
Reason string `json:"reason,omitempty"`
EMSMode string `json:"ems_mode,omitempty"`
}

// planSnapshot is everything publishPlan publishes about the plan.
Expand Down Expand Up @@ -86,12 +84,12 @@ func buildPlanSnapshot(actions []PlanAction, now time.Time) planSnapshot {
compact = append(compact, planSlot{
Start: start, Action: label,
BatteryW: round1(a.BatteryW), GridW: round1(a.GridW), SoCPct: round1(a.SoCPct),
PriceOre: round1(a.PriceOre), Confidence: round2(a.Confidence),
PriceOre: round1(a.PriceOre),
})
full = append(full, planScheduleSlot{
Start: start, End: end, Action: label,
BatteryW: a.BatteryW, GridW: a.GridW, SoCPct: a.SoCPct, PVW: a.PVW, LoadW: a.LoadW,
PriceOre: a.PriceOre, SpotOre: a.SpotOre, CostOre: a.CostOre, Confidence: a.Confidence,
PriceOre: a.PriceOre, SpotOre: a.SpotOre, CostOre: a.CostOre,
Reason: a.Reason, EMSMode: a.EMSMode,
})
}
Expand All @@ -107,7 +105,6 @@ func buildPlanSnapshot(actions []PlanAction, now time.Time) planSnapshot {
"price_ore": cur.PriceOre,
"spot_ore": cur.SpotOre,
"cost_ore": cur.CostOre,
"confidence": cur.Confidence,
"reason": cur.Reason,
"ems_mode": cur.EMSMode,
}
Expand Down Expand Up @@ -143,4 +140,3 @@ func marshalPlanAttributes(attrs map[string]any, slots []planSlot, budget int) (
}

func round1(v float64) float64 { return math.Round(v*10) / 10 }
func round2(v float64) float64 { return math.Round(v*100) / 100 }
3 changes: 1 addition & 2 deletions go/internal/ha/plan_payload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ func planFixture(now time.Time, slotMin, slots int) []PlanAction {
PriceOre: 123.456789 + float64(i),
SpotOre: 80.12345,
CostOre: -12.3456,
Confidence: 0.87654321,
Reason: "cheap hour: charge before the evening peak while PV output is still available",
EMSMode: "planner_arbitrage",
PVW: -2000.5,
Expand Down Expand Up @@ -57,7 +56,7 @@ func TestPlanAttributesStayUnderHomeAssistantLimit(t *testing.T) {
t.Fatalf("schedule entries = %d", len(schedule))
}
first, _ := schedule[0].(map[string]any)
for _, key := range []string{"start", "action", "battery_w", "grid_w", "soc_pct", "price_ore", "confidence"} {
for _, key := range []string{"start", "action", "battery_w", "grid_w", "soc_pct", "price_ore"} {
if _, ok := first[key]; !ok {
t.Fatalf("compact slot missing %s: %v", key, first)
}
Expand Down
2 changes: 1 addition & 1 deletion go/internal/mpc/charging_periods_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func TestNativeChargingPeriodsContinueMeasuredRun(t *testing.T) {
start := time.Now().Add(time.Minute).Truncate(time.Minute)
slots := make([]Slot, 12)
for i := range slots {
slots[i] = Slot{StartMs: start.Add(time.Duration(i) * time.Minute).UnixMilli(), LenMin: 1, Confidence: 1, PriceOre: 100, Limits: PowerLimits{MaxImportW: 1000}}
slots[i] = Slot{StartMs: start.Add(time.Duration(i) * time.Minute).UnixMilli(), LenMin: 1, PriceOre: 100, Limits: PowerLimits{MaxImportW: 1000}}
}
p := Params{Mode: ModeArbitrage, CapacityWh: 10000, SoCMin: .1, SoCMax: .9, InitialSoC: .5, ChargeEfficiency: 1, DischargeEfficiency: 1,
Loadpoint: &LoadpointSpec{ID: "car", CapacityWh: 1000, Levels: 11, SoCMax: .1, TargetSoC: .1, TargetSlotIdx: 11, PluggedIn: true,
Expand Down
32 changes: 16 additions & 16 deletions go/internal/mpc/demand_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ func TestBindDemandChargesExpandsWeekdayHoursAndElapsed(t *testing.T) {
now := time.Date(2026, 9, 9, 10, 7, 0, 0, loc) // Wednesday
start := time.Date(2026, 9, 9, 10, 0, 0, 0, loc)
slots := []Slot{
{StartMs: start.UnixMilli(), ExecutionStartMs: now.UnixMilli(), LenMin: 15, PriceOre: 100, Confidence: 1},
{StartMs: start.Add(15 * time.Minute).UnixMilli(), LenMin: 15, PriceOre: 100, Confidence: 1},
{StartMs: start.Add(30 * time.Minute).UnixMilli(), LenMin: 15, PriceOre: 100, Confidence: 1},
{StartMs: start.Add(45 * time.Minute).UnixMilli(), LenMin: 15, PriceOre: 100, Confidence: 1},
{StartMs: start.Add(time.Hour).UnixMilli(), LenMin: 60, PriceOre: 80, Confidence: 1},
{StartMs: start.UnixMilli(), ExecutionStartMs: now.UnixMilli(), LenMin: 15, PriceOre: 100},
{StartMs: start.Add(15 * time.Minute).UnixMilli(), LenMin: 15, PriceOre: 100},
{StartMs: start.Add(30 * time.Minute).UnixMilli(), LenMin: 15, PriceOre: 100},
{StartMs: start.Add(45 * time.Minute).UnixMilli(), LenMin: 15, PriceOre: 100},
{StartMs: start.Add(time.Hour).UnixMilli(), LenMin: 60, PriceOre: 80},
}
importWh := func(intervals [][2]int64) ([]float64, []int64) {
wh := make([]float64, len(intervals))
Expand Down Expand Up @@ -91,9 +91,9 @@ func TestBindDemandChargesGroupsHoursByLocalDay(t *testing.T) {
}
now := time.Date(2026, 9, 9, 6, 0, 0, 0, loc)
slots := []Slot{
{StartMs: now.UnixMilli(), LenMin: 60, PriceOre: 100, Confidence: 1},
{StartMs: now.Add(time.Hour).UnixMilli(), LenMin: 60, PriceOre: 100, Confidence: 1},
{StartMs: time.Date(2026, 9, 10, 6, 0, 0, 0, loc).UnixMilli(), LenMin: 60, PriceOre: 100, Confidence: 1},
{StartMs: now.UnixMilli(), LenMin: 60, PriceOre: 100},
{StartMs: now.Add(time.Hour).UnixMilli(), LenMin: 60, PriceOre: 100},
{StartMs: time.Date(2026, 9, 10, 6, 0, 0, 0, loc).UnixMilli(), LenMin: 60, PriceOre: 100},
}
got := bindDemandCharges(slots, 7000, 3, 0, 0, loc, now, nil)
if len(got) != 1 || len(got[0].Hours) < 3 {
Expand All @@ -118,9 +118,9 @@ func TestEllevioNightWeightIncludesWeekendsAndHalvesNightHours(t *testing.T) {
}
now := time.Date(2026, 9, 5, 10, 0, 0, 0, loc) // Saturday
slots := []Slot{
{StartMs: now.UnixMilli(), LenMin: 60, PriceOre: 100, Confidence: 1},
{StartMs: time.Date(2026, 9, 5, 22, 0, 0, 0, loc).UnixMilli(), LenMin: 60, PriceOre: 80, Confidence: 1},
{StartMs: time.Date(2026, 9, 5, 23, 0, 0, 0, loc).UnixMilli(), LenMin: 60, PriceOre: 80, Confidence: 1},
{StartMs: now.UnixMilli(), LenMin: 60, PriceOre: 100},
{StartMs: time.Date(2026, 9, 5, 22, 0, 0, 0, loc).UnixMilli(), LenMin: 60, PriceOre: 80},
{StartMs: time.Date(2026, 9, 5, 23, 0, 0, 0, loc).UnixMilli(), LenMin: 60, PriceOre: 80},
}
without := bindDemandCharges(slots, 7000, 3, 0, 0, loc, now, nil)
if len(without) != 0 {
Expand Down Expand Up @@ -161,9 +161,9 @@ func TestBindDemandChargesSkipsUnalignedCurrentHour(t *testing.T) {
now := time.Date(2026, 9, 9, 10, 17, 0, 0, loc)
start := time.Date(2026, 9, 9, 10, 15, 0, 0, loc)
slots := []Slot{
{StartMs: start.UnixMilli(), ExecutionStartMs: now.UnixMilli(), LenMin: 15, Confidence: 1},
{StartMs: start.Add(15 * time.Minute).UnixMilli(), LenMin: 45, Confidence: 1},
{StartMs: start.Add(time.Hour).UnixMilli(), LenMin: 60, Confidence: 1},
{StartMs: start.UnixMilli(), ExecutionStartMs: now.UnixMilli(), LenMin: 15},
{StartMs: start.Add(15 * time.Minute).UnixMilli(), LenMin: 45},
{StartMs: start.Add(time.Hour).UnixMilli(), LenMin: 60},
}
got := bindDemandCharges(slots, 7000, 1, 0, 0, loc, now, nil)
if len(got) != 1 || len(got[0].Hours) == 0 {
Expand Down Expand Up @@ -203,9 +203,9 @@ func TestNativeDemandChargesOnWeekdayHour(t *testing.T) {
defer o.Close()
start := time.Date(2026, 9, 9, 10, 0, 0, 0, time.UTC)
slots := []Slot{
{StartMs: start.UnixMilli(), LenMin: 60, PriceOre: 100, SpotOre: 50, Confidence: 1, LoadW: 500,
{StartMs: start.UnixMilli(), LenMin: 60, PriceOre: 100, SpotOre: 50, LoadW: 500,
Limits: PowerLimits{MaxImportW: 8000, MaxExportW: 8000}},
{StartMs: start.Add(time.Hour).UnixMilli(), LenMin: 60, PriceOre: 300, SpotOre: 240, Confidence: 1, LoadW: 500,
{StartMs: start.Add(time.Hour).UnixMilli(), LenMin: 60, PriceOre: 300, SpotOre: 240, LoadW: 500,
Limits: PowerLimits{MaxImportW: 8000, MaxExportW: 8000}},
}
p := Params{
Expand Down
16 changes: 5 additions & 11 deletions go/internal/mpc/diagnose.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ type DiagnosticSlot struct {
LenMin int `json:"len_min"`

// Inputs
PriceOre float64 `json:"price_ore"` // consumer total (spot + tariff + VAT)
SpotOre float64 `json:"spot_ore"` // raw spot — used for export revenue
Confidence float64 `json:"confidence"` // 1.0 = day-ahead, 0.6 = forecast
PVW float64 `json:"pv_w"` // site-signed (≤ 0 when producing)
LoadW float64 `json:"load_w"`
PriceOre float64 `json:"price_ore"` // consumer total (spot + tariff + VAT)
SpotOre float64 `json:"spot_ore"` // raw spot — used for export revenue
PVW float64 `json:"pv_w"` // site-signed (≤ 0 when producing)
LoadW float64 `json:"load_w"`

// Source and local availability time for the rows consulted to build this
// slot. Synthetic price rows use their creation time. WeatherRow is not full
Expand Down Expand Up @@ -130,9 +129,7 @@ type Diagnostic struct {
// The shape matches what the UI renders in the planner inspector so
// operators can audit each slot: "what did the DP see, what did it
// decide, and why". The per-slot `Reason` string already explains the
// decision class; the adjacent inputs show whether the decision was
// grounded in a real day-ahead price (`confidence == 1.0`) or a
// forecasted one (`confidence == 0.6`).
// decision class; the adjacent inputs show what it was based on.
func (s *Service) Diagnose() *Diagnostic {
if s == nil {
return nil
Expand Down Expand Up @@ -178,7 +175,6 @@ func buildDiagnostic(plan *Plan, slots []Slot, p Params, zone string,
ExecutionStartMs: slot.ExecutionStartMs,
PriceOre: slot.PriceOre,
SpotOre: slot.SpotOre,
Confidence: slot.Confidence,
PVW: slot.PVW,
LoadW: slot.LoadW,
PriceInputSource: slot.PriceInputSource,
Expand Down Expand Up @@ -391,7 +387,6 @@ func planFromDiagnostic(d *Diagnostic) (*Plan, []Slot, Params, time.Time, bool)
SpotOre: ds.SpotOre,
PVW: ds.PVW,
LoadW: ds.LoadW,
Confidence: ds.Confidence,
InputProvenanceSchema: d.InputProvenanceSchema,
PriceInputSource: ds.PriceInputSource,
PriceInputAvailableAtMs: ds.PriceInputAvailableAtMs,
Expand All @@ -410,7 +405,6 @@ func planFromDiagnostic(d *Diagnostic) (*Plan, []Slot, Params, time.Time, bool)
GridW: ds.GridW,
SoC: ds.SoC,
CostOre: ds.CostOre,
Confidence: ds.Confidence,
Reason: ds.Reason,
EMSMode: ds.EMSMode,
PVLimitW: ds.PVLimitW,
Expand Down
Loading
Loading