Skip to content
Open
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/ev-warning-pilot.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions web/chart-palette.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 10 additions & 2 deletions web/control-feedback.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.`;
Expand All @@ -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) {
Expand Down
13 changes: 13 additions & 0 deletions web/control-feedback.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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/);
});
2 changes: 1 addition & 1 deletion web/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
Expand Down
Loading