Skip to content
Merged
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
2 changes: 0 additions & 2 deletions contract/registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 1 addition & 3 deletions src/lib/format/plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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', () => {
Expand Down
19 changes: 3 additions & 16 deletions src/lib/sim/box.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -780,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,
Expand Down Expand Up @@ -830,7 +817,7 @@ export class SimBox {
})
return
}
if (this.#evHold || this.#evSurplusOnly) {
if (this.#evSurplusOnly) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Expose boost while Charge now is active

When a manual charge is already running, this change makes the simulator accept a boost, but EvPanel.svelte still branches on lp.manualActive to hide the “Boost from the house battery” button and display “Available after returning to the plan”; its existing test at EvPanel.svelte.test.ts:908-917 enforces that stale behavior. Consequently, owners cannot initiate the newly supported boost-under-Charge-now flow through the product UI, despite the store and box accepting it; update that UI guard and test alongside this condition.

Useful? React with 👍 / 👎.

this.#cmdResult(cmd.cmdId, 'rejected', {
code: 'E_UNAVAILABLE',
args: { op: cmd.op },
Expand Down
17 changes: 9 additions & 8 deletions src/lib/state/loadpoints.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -236,28 +236,29 @@ 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()

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)
})

Expand Down
Loading