diff --git a/.changeset/core-owns-house-load.md b/.changeset/core-owns-house-load.md new file mode 100644 index 0000000..3982ec1 --- /dev/null +++ b/.changeset/core-owns-house-load.md @@ -0,0 +1,5 @@ +--- +"ftw-webapp": patch +--- + +When a box does not report the car on its live stream, the app still shows the charger's power on the car, but it no longer takes that power off what the home uses. The box decides what the home's use holds, as the phone app already does. diff --git a/src/lib/state/flow.test.ts b/src/lib/state/flow.test.ts index 1dcc684..5296b54 100644 --- a/src/lib/state/flow.test.ts +++ b/src/lib/state/flow.test.ts @@ -151,23 +151,32 @@ describe('withLoadpointEv', () => { expect(flowReadings(out).planets.find((p) => p.id === 'ev')!.kw).toBeCloseTo(7.2) }) - it('splits a silent stream so the car is not the house', () => { - // The phone bug: field 10 at 0 W, house holding house+car (~12.7 kW). - const folded = fields([ + it('adds the charger to a silent stream and leaves Core’s load alone', () => { + // Field 10 at 0 W while a loadpoint charges: the car gets its node, and + // the house keeps the load the box sent. Core decides what that load + // holds; the app subtracting the car from it guessed for the box. + const silent = fields([ [FID.GRID_W, 0], [FID.PV_W, -3300], [FID.BATTERY_W, -9900], [FID.LOAD_W, 12_700], [FID.EV_W, 0], ]) - const out = withLoadpointEv(folded, 11_400) + const out = withLoadpointEv(silent, 11_400) const r = flowReadings(out) - expect(r.load).toBeCloseTo(1.3) + expect(r.load).toBeCloseTo(12.7) const ev = r.planets.find((p) => p.id === 'ev')! expect(ev.kw).toBeCloseTo(11.4) expect(ev.sub).toBe('charging') }) + it('keeps a missing or zero load as the box sent it', () => { + const missing = fields([[FID.GRID_W, 0], [FID.EV_W, 0]]) + expect(withLoadpointEv(missing, 7_200).has(FID.LOAD_W)).toBe(false) + const zero = fields([[FID.GRID_W, 0], [FID.LOAD_W, 0], [FID.EV_W, 0]]) + expect(withLoadpointEv(zero, 7_200).get(FID.LOAD_W)).toBe(0) + }) + it('draws the EV node when the stream never sent field 10', () => { const without = fields(FULL.filter(([fid]) => fid !== FID.EV_W)) const r = flowReadings(withLoadpointEv(without, 7200)) diff --git a/src/lib/state/flow.ts b/src/lib/state/flow.ts index 1a6a8b3..df8b565 100644 --- a/src/lib/state/flow.ts +++ b/src/lib/state/flow.ts @@ -220,12 +220,12 @@ export function loadpointChargeW(points: readonly { powerW: number }[]): number } /** - * Put the car back on its own node when the 1 Hz stream folded it into the - * house. + * Add the charger's reading when the stream has no active EV field. * * The overlay fires only when the stream is silent or idle and a loadpoint * is not. A box that already sends field 10 is left alone, so the two - * sources cannot fight. + * sources cannot fight. Core owns the house load: it stays as the box sent + * it, missing and zero included, as on the phone. */ export function withLoadpointEv( fields: ReadonlyMap, @@ -236,9 +236,7 @@ export function withLoadpointEv( if (wire !== undefined && Math.abs(wire) > FLOW_IDLE_W) return fields const out = new Map(fields) - const load = out.get(FID.LOAD_W) ?? 0 out.set(FID.EV_W, Math.round(evW)) - out.set(FID.LOAD_W, Math.max(0, load - evW)) return out }