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
5 changes: 5 additions & 0 deletions .changeset/boost-during-charge-now.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ftw-webapp": patch
---

Offer home battery boost while Charge now runs, matching Core's rules. Starting either keeps the other active, and stopping the boost leaves Charge now running.
12 changes: 4 additions & 8 deletions src/views/EvPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -745,12 +745,10 @@
{#if site.canConfigure && lp.pluggedIn}
<details class="extras">
<summary>Home battery boost</summary>
<!-- The boost: a bounded lease the box caps at four hours. The
box refuses one while a hold runs or the charger is on spare
solar only, so the offer says so up front from what the box
served, instead of drawing a button that leads to a refusal. -->
{#if !lp.boostActive}
{#if boostDraft?.lpId === lp.id}
{#if isPaused(lp)}
<p class="hint">Resume charging before starting a boost.</p>
{:else if boostDraft?.lpId === lp.id}
<div class="editor">
<div class="row">
<span class="label">Battery boost</span>
Expand Down Expand Up @@ -803,9 +801,7 @@
{:else}
<div class="row">
<span class="label">Battery boost</span>
{#if lp.manualActive}
<span class="hint">Available after returning to the plan.</span>
{:else if surplusFor(lp)}
{#if surplusFor(lp)}

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 Keep boost unavailable while charging is paused

When the charger is paused, pauseCharging() represents that as manualActive with a zero hold (isPaused(lp)), but this condition now exposes Start boost for that state. A boost only supplies the battery behind the active loadpoint hold; it does not replace the zero hold, so clicking it starts and consumes the bounded lease while the car remains paused despite the copy promising it will charge. Preserve a paused-state guard while allowing nonzero Charge now holds.

Useful? React with 👍 / 👎.

<span class="hint">Not while the charger uses spare solar only.</span>
{:else}
<button class="quiet edit" onclick={() => beginBoost(lp)}>
Expand Down
50 changes: 46 additions & 4 deletions src/views/EvPanel.svelte.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -905,16 +905,58 @@ describe('the charger behind its bubble', () => {
expect(button('Boost from the house battery')).toBeDefined()
})

it('does not offer a boost while a manual charge runs, and says why', async () => {
it.each([false, true])('keeps boost unavailable while paused, including an open draft: %s', async (openDraft) => {
vi.useFakeTimers()
vi.setSystemTime(CHARGING_EVENING)
await openedFor()
const { site } = await openedFor()
const sent = vi.spyOn(site, 'command')
if (openDraft) {
button('Boost from the house battery')!.click()
await vi.advanceTimersByTimeAsync(50)
}
button('Pause charging')!.click()
await vi.advanceTimersByTimeAsync(1_000)
expect(button('Boost from the house battery')).toBeUndefined()
expect(button('Start boost')).toBeUndefined()
expect(document.body.textContent).toContain('Resume charging before starting a boost.')
expect(sent.mock.calls.some(([op]) => op === OP_LOADPOINT_BOOST)).toBe(false)

button('Charge now')!.click()
await vi.advanceTimersByTimeAsync(1_000)
expect(button(openDraft ? 'Start boost' : 'Boost from the house battery')).toBeDefined()
})

expect(button('Boost from the house battery')).toBeUndefined()
expect(document.body.textContent).toContain('Available after returning to the plan')
it.each(['charge first', 'boost first'])('keeps Charge now and a battery boost together: %s', async (order) => {
vi.useFakeTimers()
vi.setSystemTime(CHARGING_EVENING)
const { site } = await openedFor()
const sent = vi.spyOn(site, 'command')

if (order === 'charge first') {
button('Charge now')!.click()
await vi.advanceTimersByTimeAsync(1_000)
}
button('Boost from the house battery')!.click()
await vi.advanceTimersByTimeAsync(50)
button('Start boost')!.click()
await vi.advanceTimersByTimeAsync(1_000)
expect(sent).toHaveBeenCalledWith(OP_LOADPOINT_BOOST, {
id: 'carport', min_battery_soc_pct: 30, duration_s: 3600,
})
if (order === 'boost first') {
button('Charge now')!.click()
await vi.advanceTimersByTimeAsync(1_000)
}
expect(document.body.textContent).toContain('Charge now is active')
expect(document.body.textContent).toContain('Battery boost is on')
expect(button('Return to plan')).toBeDefined()
expect(button('Stop boost')).toBeDefined()

button('Stop boost')!.click()
await vi.advanceTimersByTimeAsync(1_000)
expect(document.body.textContent).toContain('Charge now is active')
expect(document.body.textContent).not.toContain('Battery boost is on')
expect(button('Boost from the house battery')).toBeDefined()
})

it('explains where to connect a first charger instead of showing an empty panel', async () => {
Expand Down
Loading