From 522331f29fe6a249ad19727c61a0f3b34ca7e63e Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Sun, 27 Sep 2026 08:29:57 +0200 Subject: [PATCH 1/2] Drop the priority and weighted modes with Core Core removes ModePriority and ModeWeighted: nothing ever set a priority order, so neither regulated. The registry loses both keys, byte for byte the copy in srcfl/ftw, and the demo box and the mode test follow. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MuerPFZFG88kgu8sWVHeq7 --- contract/registry.yaml | 2 -- src/lib/format/plan.test.ts | 4 +--- src/lib/sim/box.ts | 12 ------------ 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/contract/registry.yaml b/contract/registry.yaml index dfb20e2..c7fe978 100644 --- a/contract/registry.yaml +++ b/contract/registry.yaml @@ -147,8 +147,6 @@ modes: - { key: charge, tier: advanced } - { key: planner_self, tier: hidden } - { key: planner_cheap, tier: hidden } - - { key: priority, tier: hidden } - - { key: weighted, tier: hidden } # --------------------------------------------------------------------------- # Error codes the box sends. The box sends the code and args; this app owns all diff --git a/src/lib/format/plan.test.ts b/src/lib/format/plan.test.ts index b62084c..6568e94 100644 --- a/src/lib/format/plan.test.ts +++ b/src/lib/format/plan.test.ts @@ -26,8 +26,6 @@ const FTW_MODES = [ 'charge', 'planner_self', 'planner_cheap', - 'priority', - 'weighted', ] as const const T0 = Date.UTC(2026, 6, 15, 12, 0, 0) @@ -205,7 +203,7 @@ describe('mode wording comes from the box', () => { // divergence in the contract file fails a test. expect(FTW_MODES).toContain('planner_passive_arbitrage') expect(FTW_MODES).toContain('planner_arbitrage') - expect(FTW_MODES).toHaveLength(10) + expect(FTW_MODES).toHaveLength(8) }) it('has words for every reason the box can send', () => { diff --git a/src/lib/sim/box.ts b/src/lib/sim/box.ts index 37a7914..209c28b 100644 --- a/src/lib/sim/box.ts +++ b/src/lib/sim/box.ts @@ -209,18 +209,6 @@ const MODE_CATALOG: ModeInfo[] = [ tooltip: 'Forecast-driven \u2014 grid-charges during cheap hours, never exports.', tier: 'hidden', }, - { - key: 'priority', - label: 'Priority', - tooltip: 'Fill the highest-priority battery first.', - tier: 'hidden', - }, - { - key: 'weighted', - label: 'Weighted', - tooltip: 'Distribute dispatch across batteries by configured weights.', - tier: 'hidden', - }, ] const MODE_KEYS = MODE_CATALOG.map((m) => m.key) From b6a8589f6f6a5f3e3ea7b2923e7a5586c483169b Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Sun, 27 Sep 2026 08:32:27 +0200 Subject: [PATCH 2/2] Let a boost run beside Charge now, as the box does Core now keeps a battery boost when Charge now starts and accepts one asked for while it runs (srcfl/ftw#1450): Charge now sets what the car draws, the boost lets the house battery cover it. The demo box follows, and the test says so. The operator_hold label stays for older boxes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MuerPFZFG88kgu8sWVHeq7 --- src/lib/sim/box.ts | 7 +++---- src/lib/state/loadpoints.test.ts | 17 +++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/lib/sim/box.ts b/src/lib/sim/box.ts index 209c28b..9cc0fe7 100644 --- a/src/lib/sim/box.ts +++ b/src/lib/sim/box.ts @@ -768,10 +768,9 @@ export class SimBox { }) return } + // A hold sets what the car draws; a running boost keeps letting the + // house battery cover it, as the box does. this.#evHold = { powerW: Math.round(w) } - // The box's own tick withdraws a boost the moment an operator hold - // appears, and remembers why. - this.#stopBoost('operator_hold') } this.#cmdResult(cmd.cmdId, 'applied', undefined, { value: this.#evHold?.powerW ?? 0, @@ -818,7 +817,7 @@ export class SimBox { }) return } - if (this.#evHold || this.#evSurplusOnly) { + if (this.#evSurplusOnly) { this.#cmdResult(cmd.cmdId, 'rejected', { code: 'E_UNAVAILABLE', args: { op: cmd.op }, diff --git a/src/lib/state/loadpoints.test.ts b/src/lib/state/loadpoints.test.ts index a2db69f..d03ba6f 100644 --- a/src/lib/state/loadpoints.test.ts +++ b/src/lib/state/loadpoints.test.ts @@ -236,7 +236,7 @@ describe('the charger over the wire', () => { expect(lp.boostStopReason).toBe('cancelled') }) - it('reports a hold ending a boost, and the box refusing a boost under a hold', async () => { + it('keeps a boost beside Charge now, and takes one asked for under it', async () => { vi.useFakeTimers() vi.setSystemTime(CHARGING_EVENING) const { store } = await loadedStore() @@ -244,20 +244,21 @@ describe('the charger over the wire', () => { await settled(store.boost(store.points[0]!, 30, 3600)) expect(store.points[0]!.boostActive).toBe(true) - // A hold takes priority: the box withdraws the boost and keeps the why. + // Charge now sets what the car draws; the boost keeps letting the house + // battery cover it. await settled(store.chargeNow(store.points[0]!, 16)) let lp = store.points[0]! expect(lp.manualActive).toBe(true) + expect(lp.boostActive).toBe(true) + + await settled(store.stopBoost(lp)) + lp = store.points[0]! expect(lp.boostActive).toBe(false) - expect(lp.boostStopReason).toBe('operator_hold') - // And a boost asked for under that hold is refused, in a sentence about - // the boost rather than about the charger being out of reach. + // A boost asked for while Charge now runs is taken. await settled(store.boost(lp, 30, 3600)) - expect(store.command.kind).toBe('failed') - expect(store.command.kind === 'failed' && store.command.help).toMatch(/won't boost/) lp = store.points[0]! - expect(lp.boostActive).toBe(false) + expect(lp.boostActive).toBe(true) expect(lp.manualActive).toBe(true) })