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
4 changes: 2 additions & 2 deletions go/test/site/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,8 @@ the fuse and 1.2 × the fuse as Core does: it counts only once its 60 s heat
| `s14-25` One-phase car | an 11 kW charger with a one-phase car, a plan it cannot meet, 20 min | 3.4–3.8 kW over the last 10 min; never called not following | |
| `r1-25` Meter silent, then offline | 25 A, 15 A per phase, battery at 50 %, Charge now 16 A; the site meter silent (Modbus timeouts) for 120 s, then gone for 90 s. **Skipped**: the pinned simulator cannot silence the site meter | the battery relieved the fuse (setup); while silent no offer rises and the battery order only moves toward 0 (**known**: A1); a 0 W order and the battery back on its own within 64 s; the meter never shows live; Charge now back within 95 s of each return and held until the next event; fuse holds | |
| `r2-25`, `r2-60-25` Battery drops out | 25 A, 20 A per phase, the battery held at a 3 kW discharge, a car taking 10 A on Charge now, the charger at once or 60 s late; the inverter gone for 180 s. The site stays under 1.2 × the fuse, where Core cuts the car on its 60 s heat budget, not at once | the battery discharged (setup); fuse holds; over 1.2 × the fuse at most the charger's delay + 7 s; the 60 s heat under 1.13 × the fuse; no offer rises while it is gone; Home use shown as estimated throughout. The time over the fuse and when Core sends the cut are recorded | |
| `r3-25` Planner floor | 25 A, 15 A per phase, battery at 12 %, planner `soc_min` 10 %, inverter floor 5 %, charger 60 s late, Charge now 16 A for 600 s | the battery reaches the floor (setup); no discharge ordered below it (**known**: A4); the battery order swings at most 100 W over the last 5 min (**known**: A3, A4); over the fuse at most the delay + 7 s; fuse holds | |
| `r4-25` Hard kill | 25 A, 12 A per phase, Charge now 16 A; once the day's Home use is known and 10 min more, the battery held at a 3 kW discharge; 5 min later SIGKILL for 20 s, and 2 min after that for 120 s while the house steps to 16 A | Charge now back at its minimum within 60 s of each restart and held until the next kill or the end; the day's Home use kept within 1 % across the first kill; session energy counts again (**known**: E1); no state.db recovery in the log; fuse holds | |
| `r3-25` Planner floor | 25 A, 15 A per phase, battery at 12 %, planner `soc_min` 10 %, inverter floor 5 %, charger 60 s late, Charge now 16 A for 600 s | the battery reaches the floor (setup); no discharge ordered below it; the measured SoC stays at or above it; the battery order swings at most 100 W over the last 5 min; over the fuse at most the delay + 7 s; fuse holds | |
| `r4-25` Hard kill | 25 A, 12 A per phase, Charge now 16 A; once the day's Home use is known and 25 min more, the battery held at a 3 kW discharge; 5 min later SIGKILL for 20 s, and 2 min after that for 120 s while the house steps to 16 A | Charge now back at its minimum within 60 s of each restart and held until the next kill or the end; the day's Home use kept within 1 % across the first kill; session energy counts again; no state.db recovery in the log; fuse holds | |
| `r5a-25` Start beside a charging car | 25 A, 14 A per phase, battery at its 5 % floor, a car charging on its own at 16 A: 30 A per phase for 60 s before Core starts | the car charged and the site was over the fuse (setup); under the fuse within 30 s of Core answering, and over it at most 3 s after; Home use shown within 30 s; fuse holds | |
| `r5b-25` Start on a stale plan | a goal 40 → 60 % 8 h out, Core stopped for 20 min | no positive order in the first 30 s before a fresh plan; Home use shown within 30 s; fuse holds. It does not reach D6 on master: the saved plan is younger than 30 min and a fresh one lands within seconds | |
| `r5c-25` Start into a new session | Charge now, Core stopped for 10 min while the car leaves and another plugs in | Core sees the car within 60 s; Home use shown within 30 s; fuse holds. Whether Charge now carries over is recorded, not judged: ev-088 waits for the owner | |
Expand Down
19 changes: 9 additions & 10 deletions go/test/site/scenarios_p0_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,9 @@ func init() {
scenario{name: "r2-60-25", dir: "r2-battery-offline-slow60-25A", minutes: 10, run: func(c *runCtx) (map[string]any, error) { return r2(c, 25, 60) },
judge: judgeR2},
scenario{name: "r3-25", dir: "r3-planner-floor-25A", minutes: 13, run: func(c *runCtx) (map[string]any, error) { return r3(c, 25) },
judge: judgeR3,
known: map[string]string{
"no discharge ordered below the planner floor": "A4: fuse relief discharges to 5 %, below planner.soc_min (dispatch.go:4113-4124, 4495-4499)",
"the battery order swings at most 100 W over the last 5 min": "A3, A4: at the inverter's empty 5 % Core keeps ordering 3.9-5 kW of discharge (the slew step reopens it, dispatch.go:2148-2219)",
}},
scenario{name: "r4-25", dir: "r4-sigkill-25A", minutes: 40, run: func(c *runCtx) (map[string]any, error) { return r4(c, 25) },
judge: judgeR4,
known: map[string]string{"session energy counts again after each restart": "E1: a 1.6 transaction is not taken up after a restart, so session_wh stays 0 (handlers.go:615-617, #1787)"}},
judge: judgeR3},
scenario{name: "r4-25", dir: "r4-sigkill-25A", minutes: 41, run: func(c *runCtx) (map[string]any, error) { return r4(c, 25) },
judge: judgeR4},
scenario{name: "r5a-25", dir: "r5a-start-car-charging-25A", minutes: 5, run: func(c *runCtx) (map[string]any, error) { return r5a(c, 25) },
judge: judgeR5a},
scenario{name: "r5b-25", dir: "r5b-start-stale-plan-25A", minutes: 27, run: func(c *runCtx) (map[string]any, error) { return r5b(c, 25) },
Expand Down Expand Up @@ -158,7 +153,11 @@ func r3(c *runCtx, fuse float64) (map[string]any, error) {
// 20 s; two minutes later the second lasts 120 s, and the house steps to
// 16 A per phase while Core is gone. Like s7, the run first waits until the
// day's Home use is known, and long enough after it that one restart stays
// under the 10 % unknown-time rule. Core holds a battery for at most
// under the 10 % unknown-time rule. The rule counts whole ledger minutes,
// so a restart whose unknown car reading crosses a minute boundary marks
// two. With the 42 s Core was down or starting, that came to 128 s of a
// 1 054 s day when the hold came 10 min after, and failed 1 run in 5.
// 25 min leaves room for it. Core holds a battery for at most
// 1800 s, so the hold starts 300 s before the first kill and outlasts both.
func r4(c *runCtx, fuse float64) (map[string]any, error) {
var serial string
Expand All @@ -177,7 +176,7 @@ func r4(c *runCtx, fuse float64) (map[string]any, error) {
}) {
st.mark("today_known")
}
sleepS(600)
sleepS(1500)
st.mark("battery_hold")
if err := st.post("/api/battery/manual_hold", map[string]any{"direction": "discharge", "power_w": 3000, "hold_s": 1800}, nil); err != nil {
return err
Expand Down
10 changes: 10 additions & 0 deletions go/test/site/scenarios_p0_unit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,16 @@ func TestJudgeR3(t *testing.T) {
if v["the battery order swings at most 100 W over the last 5 min"] {
t.Fatal("an order hunting between 0 and -3 kW passes")
}
// No order at all, as when Core hands the battery back to the inverter,
// while it runs on from 12 % to the inverter's 5 %.
v = run(func(i int, r *sample) { r.Bat.SoCPct = 12 - 7*float64(min(i, 200))/200 })
if !v["no discharge ordered below the planner floor"] || v["the battery stays at or above the planner floor"] {
t.Fatalf("a battery left to run to 5 %%: %v", v)
}
v = run(func(i int, r *sample) { r.Bat.SoCPct = max(10, soc(i)) })
if !v["the battery stays at or above the planner floor"] || !v["the battery reached the planner floor"] {
t.Fatalf("a battery held at the floor: %v", v)
}
}

func TestJudgeR4SessionEnergy(t *testing.T) {
Expand Down
5 changes: 4 additions & 1 deletion go/test/site/verdicts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,9 @@ func judgeR2(d *runData) (metrics, []criterion) {
}

// judgeR3: Core orders no discharge below the planner's floor, its battery
// order settles, and the car answers when the battery stops.
// order settles, and the car answers when the battery stops. The measured
// SoC must hold the floor too: an order of 0 W also reads from a battery
// handed back to the inverter, which would run on to its own 5 %.
func judgeR3(d *runData) (metrics, []criterion) {
fuse, delay, floor := d.metaF("fuse"), d.metaF("charger_delay_s"), d.metaF("planner_floor_pct")
t0, tCN, t1 := d.marks["plug"], d.marks["charge_now"], d.marks["end"]
Expand Down Expand Up @@ -681,6 +683,7 @@ func judgeR3(d *runData) (metrics, []criterion) {
return m, []criterion{
crit("the battery reached the planner floor", len(run) > 0 && minSoC <= floor+0.5), // the setup
crit("no discharge ordered below the planner floor", below <= 10),
crit("the battery stays at or above the planner floor", len(run) > 0 && minSoC >= floor-0.5),
crit("the battery order swings at most 100 W over the last 5 min", len(last) > 0 && swing <= 100),
crit("over the fuse no longer than the charger's delay + 7 s", over <= allowed),
crit("fuse holds", !anyBlown(run)),
Expand Down
Loading