From 21941ca3cbebfe8a19ca83db664538bad71a1d70 Mon Sep 17 00:00:00 2001 From: Segran Date: Thu, 12 Nov 2026 13:44:10 +0100 Subject: [PATCH 1/2] fix(ev): show pilot current in charging warnings --- .changeset/ev-warning-pilot.md | 5 +++++ web/control-feedback.js | 12 ++++++++++-- web/control-feedback.test.mjs | 13 +++++++++++++ 3 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 .changeset/ev-warning-pilot.md diff --git a/.changeset/ev-warning-pilot.md b/.changeset/ev-warning-pilot.md new file mode 100644 index 000000000..c9f655aaa --- /dev/null +++ b/.changeset/ev-warning-pilot.md @@ -0,0 +1,5 @@ +--- +"ftw": patch +--- + +Show requested current, charger pilot current and measured power in EV charging warnings when fresh pilot data is available. diff --git a/web/control-feedback.js b/web/control-feedback.js index c3c7cbaec..ce356726b 100644 --- a/web/control-feedback.js +++ b/web/control-feedback.js @@ -111,9 +111,9 @@ function sentence(row) { case 'load_balancer_limit': return `${row.make ? `${row.make} limits the charger to ${amps(row.limit_a)} (its load balancing)` : `The charger’s load balancing limits it to ${amps(row.limit_a)}`}. FTW asked for ${amps(row.requested_a)}; the car takes ${controlPower(actual)}.`; case 'one_phase': return `The car charges on ${row.charging_phase || 'one phase'} only${num(row.charging_phase_max_w) ? `, at most ${controlPower(row.charging_phase_max_w)}` : ''}. FTW plans with what it takes.`; - case 'power_below_target': case 'power_above_target': return `Asked for ${target(row)}, delivering ${controlPower(actual)}.`; + case 'power_below_target': case 'power_above_target': return evPowerWarning(row) || `Asked for ${target(row)}, delivering ${controlPower(actual)}.`; case 'power_wrong_direction': return `Asked for ${target(row)}, but it is ${doing(row, actual)} ${controlPower(actual)}.`; - case 'no_power_response': return `Asked for ${target(row)}, but it delivers no power.`; + case 'no_power_response': return evPowerWarning(row) || `Asked for ${target(row)}, but it delivers no power.`; case 'power_while_idle': return `Asked to stay idle, but it is ${doing(row, actual)} ${controlPower(actual)}.`; case 'setpoint_changed': return `The ${name} reports a target of ${controlFlow(row, row.readback_w)}, not the ${target(row)} FTW sent.`; case 'device_fault': return row.device_reason ? `The ${name} reports a fault: ${row.device_reason}.` : `The ${name} reports a fault that stops control.`; @@ -134,6 +134,14 @@ function sentence(row) { } } +function evPowerWarning(row) { + if (row.kind !== 'ev' || !row.readings_fresh || + !num(row.requested_a) || !num(row.pilot_a) || !num(row.actual_w)) return ''; + const request = `FTW asked for ${amps(row.requested_a)}`; + const pilot = `the charger signalled ${amps(row.pilot_a)} to the car`; + return `${request} and ${pilot}. Measured charging: ${controlPower(row.actual_w)}.`; +} + function nextStep(row) { const name = noun(row); switch (row.reason) { diff --git a/web/control-feedback.test.mjs b/web/control-feedback.test.mjs index 561fabfbd..26c8c0fc9 100644 --- a/web/control-feedback.test.mjs +++ b/web/control-feedback.test.mjs @@ -236,3 +236,16 @@ test('the plan and what happens now, in words', () => { assert.equal(planNowWords({...morning, why: 'other'}).why, 'FTW follows what the home and the sun do now, not the forecast.'); assert.equal(planNowWords({...morning, planned_battery_w: -3000, battery_w: -1000, why: 'less_home'}).then, 'The battery gives only what the home needs.'); }); + +test('EV warnings show fresh pilot evidence with the measured charging power', () => { + const ev = row({kind: 'ev', status: 'not_following', severity: 'warning', + requested_a: 15, sent_w: 10350, pilot_a: 11, actual_w: 7489, reason: 'power_below_target'}); + assert.equal(controlStatus(ev).text, + 'FTW asked for 15 A and the charger signalled 11 A to the car. Measured charging: 7.5 kW.'); + const stopped = {...ev, pilot_a: 12, actual_w: 0, reason: 'no_power_response'}; + assert.equal(controlStatus(stopped).text, + 'FTW asked for 15 A and the charger signalled 12 A to the car. Measured charging: 0 W.'); + assert.equal(controlStatus(stopped).tone, 'warning'); + assert.doesNotMatch(controlStatus({...stopped, readings_fresh: false}).text, /signalled/); + assert.doesNotMatch(controlStatus({...stopped, pilot_a: null}).text, /signalled/); +}); From 73ca1ac5b5e093c56094eb4c82851b371730f890 Mon Sep 17 00:00:00 2001 From: Segran Date: Thu, 12 Nov 2026 13:48:40 +0100 Subject: [PATCH 2/2] fix(web): restore chart palette checks --- web/chart-palette.test.mjs | 1 + web/style.css | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/web/chart-palette.test.mjs b/web/chart-palette.test.mjs index 649950cc0..37ac1ef6e 100644 --- a/web/chart-palette.test.mjs +++ b/web/chart-palette.test.mjs @@ -33,6 +33,7 @@ async function drawHistory(days) { const legend = { innerHTML: "" }; const els = { "energy-history-chart": canvas, "energy-history-legend": legend }; const context = { + URLSearchParams, document: { getElementById: (id) => els[id] || null }, window: { devicePixelRatio: 1, diff --git a/web/style.css b/web/style.css index 4169cf66a..601949e6f 100644 --- a/web/style.css +++ b/web/style.css @@ -191,7 +191,7 @@ header h1 { .energy-history-ev-cost span { color: var(--text-dim); font-size: 0.78rem; } .energy-history-ev-cost strong { font-size: 1.05rem; } .energy-history-ev-cost small { color: var(--text-dim); font-size: 0.7rem; } -.energy-history-ev-cost .incomplete { color: var(--warning, #f59e0b); } +.energy-history-ev-cost .incomplete { color: var(--alarm); } @media (max-width: 720px) { .energy-history-ev-cost { grid-template-columns: 1fr; } }