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/no-raw-minus-charts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"ftw": patch
---

The dashboard no longer prints a minus sign on grid and battery power. Charts label their axes with sizes and name what sits on each side of zero: above it what adds to the grid meter's reading (import, charge, load, the car), below it what takes away (export, discharge, solar). Grid and battery values say their direction in a word, such as "500 W export" or "1.4 kW discharge", in tooltips, the live strip, targets, driver cards, the charger card's plan table, the per-phase watts under the fuse bars and the plan's safety note. A battery that misses its target reads "short" or "over" along the target's direction. The Overview flow shows each device's size with "charging" or "discharging" under it, and a battery FTW only observes says so under its title. Home use keeps its sign for now.
8 changes: 8 additions & 0 deletions web/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -1637,6 +1637,14 @@ body.ftw-app .live-stat-value.is-export { color: var(--green-e); }
body.ftw-app .live-stat-value.is-charging { color: var(--cyan); }
body.ftw-app .live-stat-value.is-discharging { color: var(--energy-gives); }
body.ftw-app .live-stat-value.is-neutral { color: var(--fg-dim); }
/* The direction word under a two-way value, so a narrow cell keeps the
size on one line. */
body.ftw-app .live-stat-dir {
display: block;
font-size: 10px;
font-weight: 500;
letter-spacing: 0.06em;
}

/* 24 h battery + SoC history block — sits below the live power chart.
* Section label matches the Plan card's chart-section labels. */
Expand Down
197 changes: 136 additions & 61 deletions web/app.js

Large diffs are not rendered by default.

11 changes: 5 additions & 6 deletions web/components/energy-flow-readings.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,11 +73,9 @@ function placeholderPlanet(partial) {
* @param {object} status GET /api/status body
* @param {object} [opts]
* @param {number} [opts.idleW]
* @param {(name: string, driver: object, batW: number) => string} [opts.batterySub]
*/
export function flowReadingsFromStatus(status, opts) {
const thresholdW = (opts && opts.idleW) || idleW();
const batterySub = opts && opts.batterySub;
const planets = [];
const today = (status && status.energy && status.energy.today) || {};
const importWh = num(today.import_wh);
Expand Down Expand Up @@ -155,10 +153,10 @@ export function flowReadingsFromStatus(status, opts) {
if (batW !== null) {
const bIdle = isIdle(batW, thresholdW);
const soc = num(d.bat_soc);
const defaultSub = d.observe_only === true
? "observe only"
: bIdle ? "idle" : batW >= 0 ? "charging" : "discharging";
const sub = batterySub ? batterySub(name, d, batW) : defaultSub;
// The bubble prints a size, so this line says the direction. The
// bubble has no room for "observe only" on the same line; the
// component puts it under the title.
const sub = bIdle ? "idle" : batW >= 0 ? "charging" : "discharging";
const planet = {
id: `bat-${name}`, corner: "top-right", title: "BATTERY", role: "battery", name,
kw: batW / 1000, toHub: batW < 0,
Expand All @@ -169,6 +167,7 @@ export function flowReadingsFromStatus(status, opts) {
dailyScope: "aggregate",
dailyAggregateMembers: batDailyMembers,
clickable: d.observe_only !== true,
observeOnly: d.observe_only === true,
};
if (online) {
live.battery = true;
Expand Down
20 changes: 15 additions & 5 deletions web/components/ftw-energy-flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -653,12 +653,21 @@ class FtwEnergyFlow extends FtwElement {
!p.aggregated &&
(layerClass === "ind" || aggregateMemberCount > 1) &&
Math.max(p._groupSize || 1, aggregateMemberCount || 1) > 1;
const grouped = p._groupSize > 1 && !!p.name;
return renderCircleNode({
pos: p._pos,
value: p.placeholder ? "—" : fmtKw(p.kw),
// A planet prints its size; the word under it gives the direction,
// because the UI never shows a raw minus. The hub keeps Core's sign
// for Home use (#1768).
value: p.placeholder ? "—" : fmtKw(Math.abs(p.kw)),
title: p.title,
nameLabel: p._groupSize > 1 && p.name ? p.name.toUpperCase() : null,
sub: p.sub,
// The title's second line names the device in a group, or says
// that FTW only observes it; a phone's larger title font has room
// for one word. A grouped device keeps its name there and says
// "observe only" under the size instead.
nameLabel: grouped ? p.name.toUpperCase()
: p.observeOnly ? (this._compact ? "OBSERVED" : "OBSERVE ONLY") : null,
sub: grouped && p.observeOnly ? "observe only" : p.sub,
color: p.color,
soc: p.placeholder ? null : p.soc,
chargeLimit: p.placeholder ? null : p.chargeLimit,
Expand Down Expand Up @@ -1662,8 +1671,9 @@ function renderDailyParts(parts) {
}

function fmtKw(kw) {
// Input is kilowatts. Sub-kW values render as plain integer watts
// (no decimals) so a "30 W" import or "−12 W" battery trickle stays
// Input is kilowatts: a size for a planet, Home use as Core signs it
// for the hub. Sub-kW values render as plain integer watts (no
// decimals) so a "30 W" import or "12 W" battery trickle stays
// visible as the real number. The visual "idle / balanced"
// affordances (beam dimming, sub-label text) live on a separate
// ±FLOW_IDLE_W threshold; this formatter only handles display.
Expand Down
11 changes: 9 additions & 2 deletions web/diagnose.js
Original file line number Diff line number Diff line change
Expand Up @@ -601,9 +601,12 @@
ctx.textAlign = 'right';
ctx.fillText(maxPrice.toFixed(0) + 'ö', pad.l - 4, priceY0 + 10);
ctx.fillText('0', pad.l - 4, priceY0 + priceH - 2);
ctx.fillText('+' + (maxAbsPower / 1000).toFixed(1) + 'kW', pad.l - 4, powerY0 + 10);
ctx.fillText(powerAxisLabel(maxAbsPower), pad.l - 4, powerY0 + 10);
ctx.fillText('0', pad.l - 4, powerMidY + 3);
ctx.fillText('−' + (maxAbsPower / 1000).toFixed(1) + 'kW', pad.l - 4, powerY0 + powerH - 2);
ctx.fillText(powerAxisLabel(-maxAbsPower), pad.l - 4, powerY0 + powerH - 2);
// The power labels are sizes; these words say what sits on each side.
const words = globalThis.ftwPowerWords;
if (words) words.drawSides(ctx, pad.l + 6, powerMidY, words.SIDES.diagnose, C.dim, C.panel);
ctx.fillText('100%', pad.l - 4, socY0 + 10);
ctx.fillText('0', pad.l - 4, socY0 + socH - 2);

Expand All @@ -616,6 +619,10 @@
}

// ---- Helpers ----
// The chart's power axis shows sizes, never a raw minus.
function powerAxisLabel(w) {
return (Math.abs(w) / 1000).toFixed(1) + 'kW';
}
function fmtHHMM(ts) {
const d = new Date(ts);
return d.getHours().toString().padStart(2, '0') + ':' +
Expand Down
1 change: 1 addition & 0 deletions web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -996,6 +996,7 @@ <h3>Price bars (top of the chart)</h3>
<script src="/project-statistics.js?v=terms2"></script>
<script src="/lan-auth.js?v=password1"></script>
<script type="module" src="/control-feedback-view.js"></script>
<script src="/power-words.js"></script>
<script src="/app.js?v=noask1"></script>
<script src="/settings.js?v=diag3"></script>
<script src="/settings/tabs/control.js?v=tabsplit1"></script>
Expand Down
10 changes: 9 additions & 1 deletion web/loadpoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,14 @@
return Math.round(w) + ' W';
}

// The plan's battery power as a size and its word ("1.2 kW discharge"),
// never a raw minus; signed, as before, only if power-words.js is missing.
function batteryW(w) {
const words = globalThis.ftwPowerWords;
if (w == null || !isFinite(w)) return '—';
return words ? words.formatPower(w, 'battery') : fmtW(w);
}

function fmtPct(frac) {
if (frac == null || !isFinite(frac)) return '—';
return (frac * 100).toFixed(1) + '%';
Expand Down Expand Up @@ -215,7 +223,7 @@
`<td>${fmtPriceOre(a.price_ore)}</td>` +
`<td>${fmtW(a.loadpoint_w || 0)}</td>` +
`<td>${fmtPct(a.loadpoint_soc)}</td>` +
`<td>${fmtW(a.battery_w)}</td>` +
`<td>${batteryW(a.battery_w)}</td>` +
`<td>${escapeHtml(a.reason || '')}</td>` +
'</tr>';
}).join('');
Expand Down
Loading
Loading