Releases: tesseractAZ/power
Release list
v1.180.0
An announced outage survives an add-on restart
- 1.178.0 made the panel's "no grid" reading override a grid declared present and hold until
the panel reports Grid OK again, but the reading lived only in the running add-on. If the
add-on restarted during an outage while the panel was off the cloud (an outage that also takes
the internet down), the new process never saw a reading: a panel listed offline is never
polled. The declaration then stood again. The runway alarm's spoken warning was held back,
off_gridread OFF and battery-level announcements said the house was drawing from grid
power, for as long as the panel stayed dark. The last "no grid" reading is now saved to a small
file next to the database and read back as the add-on starts. It applies whether or not the
add-on can reach the EcoFlow cloud after the restart: the resolver finds the panel before it
has sent anything, or, while the add-on has no device list at all, uses the saved reading
directly. The file is written only when the reading changes and is cleared the moment the panel
reports Grid OK, so a "Grid OK" is never saved; replacing the panel drops the old one's
reading. The trade-off: if the grid returned while the
panel was dark, "no grid" persists until the panel reports. That is an early alarm, not a missed
one; deleting/data/grid-reading.jsonand restarting the add-on clears it by hand.
New harness scripts/mutate-grid-reading-persist.mjs (15 anchor-asserted mutants).
v1.179.0
Old readings from the panel and the Cores no longer count as the grid being there
- The panel's own grid flag counted as grid presence whenever the panel was online, however old
the reading. Two ways left an old "Grid OK" standing through an outage: the panel dropping off
the cloud and coming back before its next reading (the reconnect notice arrives first and
carries no data), and the panel's readings failing to download while it stayed listed online
— indefinitely. Either way the add-on believed the grid was there when it was not: the runway
alarm's spoken warning was held back, battery-level announcements said the house was drawing
from grid power,off_gridread OFF andshp2_grid_connectedread ON. The same was true,
and worse, of the measured grid power: a panel reading frozen mid-charge (7–8 kW at the
reserve floor) counted as grid flowing, which overrides even the checks made at the floor,
and a Core whose data had stopped kept its last grid draw. All three now count only while
fresh — downloaded in the last five minutes, from a device that is online and not replaying
old data — the same test the overnight charging controls already use. Readings arrive about
once a minute, so normal operation, including the gaps between the panel's charging bursts,
is unchanged, and a reconnect of a few seconds does not discard a reading that is still fresh.
A reading from before an outage can still count until the next successful download, at most
five minutes after the last good one.shp2_grid_connectedreads unknown while stale. The
other direction is accepted deliberately: if the cloud is unreachable for more than five
minutes while the batteries sit at the reserve floor and the grid is charging them, the floor
alarm speaks, because nothing can vouch for the grid. - When nothing can be heard at all, the overnight charging no longer treats that as the grid
being lost: it holds rather than ending the night's buy or switching force-charge off. - Reads from the EcoFlow cloud now give up after 30 seconds. They had no limit short of five
minutes — as long as the freshness window — so a single stuck read could have stalled every
device's update past it. Commands keep the longer limit: a slow reply can still mean the
command arrived, and treating it as failed would send it again. - The Energy flow card shows the house load as not reporting while the panel's readings have
stopped, instead of drawing its last load next to a grid reading the add-on no longer trusts. - The v1.178.0 rule that a "no grid" reading overrides a grid declared present is unaffected.
New harness scripts/mutate-presence-fresh-readback.mjs (13 anchor-asserted mutants).
v1.178.1
Battery net and panel load no longer publish 0 at a restart
- 1.178.0 stopped model-less zeros reaching Home Assistant after a restart, and at its own
deploy fifteen of the governed sensors did go to unknown and back. Two did not:Battery Net
andPanel Loadstill went X → 0 → X. Both publishers added up the fleet flows before waiting
on the analytics reports, and judged whether those sums were ready after waiting. The first
device poll lands during that wait, so readiness saw real devices and passed sums that had been
taken before any device had reported. The sums are now taken after the wait, from the same
device state readiness judges, so the two cannot disagree. They are also a few seconds fresher.
Harness scripts/mutate-grid-veto-boot-zero.mjs: three mutants added (32).
v1.178.0
A measured "no grid" outranks a declared grid; boot placeholders are published as unknown
- The panel's "grid not detected" now overrides a grid declared present. With
input_boolean.grid_available(orGRID_AVAILABLE) ON, the grid resolver trusted that
declaration over the panel's own reading everywhere except at the reserve floor. In an outage
with the toggle left ON the grid kept "backstopping": the runway alarm's spoken warning stayed
gated silent,runway_projection_islanded_onlyread ON,off_gridread OFF and the Runway card
said "not a live countdown" — until the pool neared the floor, by which time the hours in which
to shed load or start a generator had passed. An online panel reportinggridSta0 ("grid not
detected") or 2 (out of spec, islanded onto the batteries) now vetoes the declaration at any
state of charge, and the resolver's reason names the code the panel reported. The veto holds
on the panel's last reading until a newer one says Grid OK (grid power measured flowing still
counts as the grid while it flows): a panel that goes quiet — cloud-offline, replaying stale
data, not refreshing, briefly reconnecting, or sending a reply without its grid status — does
not turn an announced outage back into "grid present". Each of those,
if allowed to lift the veto, droppedoff_gridandload_shed_recommendedmid-outage and
gated the runway warning again. If the grid returns while the panel is dark, "no grid"
persists until the panel reports: an early alarm, not a missed one. A panel that has never
reportedgridStavetoes nothing, and the reading is kept in memory only: after an add-on
restart with the panel dark, the declaration stands again until the panel reports. In three
weeks of recorded history (2026-09-01 to 09-22) the panel never reported 0 or 2 while the
grid was up, and a false reading would sound an alarm early rather than silence one. - No more model-less zeros in Home Assistant after a restart. The first state publish runs
on broker connect, about a second before the first device poll, and the next one about 75 s
later. Everything computed in between came out 0 and was published as a reading: across the
day's four restarts, 16 sensors went X → 0 → X each time — fleet PV and battery net, panel
load, five alarm counts, the usable-speaker count, CO2 avoided (7 d), tariff today and
7 d, curtailment (7 d), array peak and the next-24 h PV forecast. Measurement sensors took a
false minimum of 0 into every restart hour, and the Energy dashboard's solar and battery rates
dropped to 0.pv_curtailment_kwh_todayistotal_increasing, so its dip (5.44 → 0 →
5.44 kWh) read as a meter reset and the day's curtailment was counted again — 8 times since
09-01. Each group of fields now publishes null (unknown) until its own input exists: fleet
flows until an online Core is projected, panel load until the panel reports a channel, alarm
counts once the monitor has run, the speaker count after the first probe, the forecast when
there is PV history to project from, and the clipping, curtailment, carbon and tariff figures
when their report ran on a real basis — for curtailment that includes weather, which is
missing after a restart until the first forecast fetch succeeds and would otherwise have
produced the same 0 by another path. Both the MQTT publisher and/api/ha-stateapply the
same rule, and the "PV Curtailment Active" binary sensor reads unknown rather than off while
withheld. A DPU-only install (no panel) keeps publishing its grid cost. Genuine zeros — no PV at night, no active alarms — publish as before. Lifetime
counters, which come from persisted totals, are unaffected. - The console's plant PV view shows "—" for the next-24 h forecast while it has no basis.
New harness scripts/mutate-grid-veto-boot-zero.mjs (29 anchor-asserted mutants).
v1.177.0
The Runway card says what its projection shows
- "No dip in 24 h — forecast PV keeps up with load" is gone. It was printed whenever the
islanded pool did not cross the reserve floor inside 24 hours — the only condition the
projection tested. On 2026-09-22 it sat above a projection that fell from 78 to 26 kWh, with
solar covering 57% of the modelled load. The projection now reports its lowest point and when
it happens, and the headline reads "reserve holds 24 h" labelled with that lowest point and its
margin over the floor ("lowest ≈ 26.0 kWh around Wed 6:00 AM, 11.3 kWh above the reserve
floor"). "Forecast PV keeps up with the load" appears only when the pool never falls below where
it is now. A lowest point within 15% of full above the floor is shown neutral rather than green
(never more alarming than a reserve crossing 12–24 hours out, which is also neutral). - "Grid is carrying the load" only when it is. The note read "grid is carrying the load"
whenever the grid was backstopping, including with 0 W imported while solar carried the house,
beside an Energy flow card reading GRID STANDBY. It now says so only when grid power is flowing
and reads "grid available as a backstop" otherwise. The note — "these are islanded projections,
not a live countdown" — is still shown only while the grid resolver says the grid is
backstopping: a grid that is merely reported present at the reserve floor (a declared grid with
no measured flow, or a panel reporting "Grid OK" while the pool keeps discharging) is exactly the
state in which the projection is the live countdown and the alarms speak critical, and the card
says nothing to the contrary. - The header names the load model in use. "Last-hour load + next-24h forecast PV" described
only the degraded fallback. The projection normally runs the day-of-week load curve (without
predicted EV charging) with the last hour's load blended into the first four hours — live, 2.2×
the last hour alone. The header reads "typical load" normally and "last-hour load" in the
fallback. - "Recent load" is captioned by what it is. Every fallback — a live reading after a restart,
a single recorded sample, a value carried forward from the previous compute — was labelled
"1-hour average". The caption now follows the projection'srecentLoadBasis. - The two forecast-load figures say why they differ. The Dashboard's runway load leaves out
the predicted-EV layer (the alarm path is evidence-based; past EV charging is still averaged
into the typical load curve, and a car charging now shows in the recent load); the Solar tab's
forecast load adds it (live: 93–94 vs 96.3 kWh). Each now says which, and the Solar tab's
projected low SoC notes when EV load predicted before that low is included. - Home Assistant and the Dashboard agree on the next-24 h solar forecast. With every Core
reporting the two are meant to be one number, and had drifted apart twice: the value published
to Home Assistant skipped the learned bias correction (51.4 vs 52.9 kWh live, exactly the
factor), and its solar model was refit on hours the Dashboard's model excludes after any
partial-fleet day (79.4 vs 76.7 kWh reproduced). With no Core missing the published figure now
uses the Dashboard's model and correction. The runway and its alarms are unaffected: they never
read the display figure. - The projection's lowest point is timestamped at the empty crossing when the pool empties,
matchingemptyAtMsinstead of the end of that hour. - "of 92 full" keeps the tile's own decimal ("of 92.2 full").
New harness scripts/mutate-runway-card.mjs (15 anchor-asserted mutants).
v1.176.0
The dashboard says how old its data is — and stops saying "live" over stale data
- The LIVE pill needs fresh readings, not just an open link. It was the WebSocket's state
alone, and a socket stays open while the server has nothing new to send: it read LIVE, green,
through the 2026-09-22 03:17–03:28 EcoFlow cloud outage. It now reads live only when every
home reading — the panel and each online Core wired to it — is under three minutes old (the
alarm engine's own "Telemetry stale" threshold), and stale otherwise. - "updated N ago" is the age of the oldest reading, and keeps counting. It printed the
snapshot'sgeneratedAt, which the server bumps on every poll FAILURE — the harder the cloud
failed, the fresher the header looked — and it only re-rendered when a new snapshot arrived.
It now reads a new server clock,lastTelemetryAtMs, moved only when telemetry content lands
(not by a/statusonline flip, a failed poll or an empty payload), and ticks every 5 s. - A panel replaying a stale cloud copy is stale. The EcoFlow cloud can answer the panel's
poll with 200 OK and a replayed body; the server flags it and raises "Panel data is stale"
(21 episodes 2026-09-13..21), but every replay refreshed the panel's clock. Its reading time
is now capped at when the replay began. A panel that has not reported since a restart counts
as never reported, found by identity even before its first projection arrives. - Ages are measured on the server's clock. Every reading time is server-stamped, so
comparing them with the browser's clock made the pill wrong by any skew between the viewing
device and the host (this host has no RTC and boots on a baked-in date until NTP). Each
WebSocket frame now carries the server's time at send, and the browser measures ages against
it, taking the least-delayed frame so a backlog shows as age rather than skew. - Runway, Today and Curtailment say when their figures are old. Each kept its last payload
indefinitely when refreshes failed — Runway set an error flag nothing read after its first
success, Today returned silently — with nothing on screen to say so. They now show "stale · as
of " (naming the day when it is not today) once they have missed their polls. The Today
card and the Solar tab's Today tile drop a payload whose day is over instead of showing
yesterday's totals under "since 12:00 AM";/api/summary/todaynow returnsdayEndMs, exact
across daylight-saving changes. - Two pages no longer take the dashboard down on a server error. The Solar tab and the
circuit popup stored an HTTP 500 body as data, and the render then threw — the whole dashboard
fell to its error screen, e.g. when the Solar tab polled during an add-on restart. A failed
Solar history request now keeps the last good chart instead of blanking the day. The Insights
sections no longer render an error body as their payload.
Pure logic in web/src/freshness.ts, run by the server suite; new harness
scripts/mutate-dashboard-freshness.mjs (23 anchor-asserted mutants).
v1.175.0
The Energy flow card draws the grid where it goes, and a silent panel is no longer a 0 W house
- Grid power is drawn to its real destination. The SHP2's grid reading is the total at the
main, and the card drew all of it as one edge into the Batteries; the only edge into Loads came
out of the Batteries. A grid carrying the house was therefore drawn as the house running on
battery. At 03:30 on 2026-09-22 the grid supplied 1901 W of a 1904 W house while the home Cores
output 0 W and their packs moved 16 W — the card showed 1.9 kW flowing into batteries nothing was
charging and back out of inverters producing nothing. The card now draws grid → Batteries for
the Cores' own AC input only (grid charging them) and a separate grid → Loads edge, routed below
the battery node, for the rest, capped at the house load. During a force charge both appear
(04:05: 15,969 W into the Cores, 1,996 W to the house); the grid node keeps the metered total. - The arrows into Loads sum to the Loads box, attributed from the fresher meter. The
Batteries → Loads edge wasMath.max(load, acOut): the Cores' inverter meter against the
panel's own total, two meters that differ by tens of watts (the reported view showed 1925 W
into a "1.89 kW" box; live 14,427 W into "14.36 kW"; about 30% of overnight 5-minute buckets
disagreed). The panel reports in one frame every ~60 s and the Cores every ~10 s, so every
transition — a charge ramp, a charge ending, the Cores handing the house to the grid — has up
to a minute in which they disagree. The Cores now count as delivering only when their
inverters report output. When they are the house's only source, their edge is the house total;
when they are idle, the grid carries the whole house (including in the minute after they stop,
before the panel's next frame shows the main rising); with both, the Cores' share comes from
their own meter and the grid takes the rest, so the two edges always sum to the box. The Grid
node shows the main meter unless the faster meters show it is a frame behind (it read 3.41 kW
beside a 16.3 kW charge at a ramp, and 18.4 kW beside a 2.1 kW flow as a charge ended).
Replayed over 190.5 h of recorded meter history (150,466 instants), the card now draws no
flow out of Cores whose inverters report nothing, never loses the Cores' delivery, never shows
the house with nothing feeding it, and never shows a Grid node its edges contradict; the first
cut of this change had drawn phantom battery output for 593 minutes of that history. - A panel that is offline or replaying a cloud shadow is not read as live. The server already
zeroes such a panel's grid reading; its channels still held the frozen house load, which would
have drawn a grid-fed house flowing out of idle batteries. The Loads node reads "—" with "panel
data stale", and the Cores' grid draw is counted over the same Cores the Batteries node shows
(the server's import figure also counts a source slot whose Core is not connected). With no
connection table yet (cold boot) the server's fail-safe import figure is used, so a bench
spare's wall charge is not drawn as live grid. - A panel that reports no channel watts reads "—", and records nothing. The SHP2 projection
always carries twelve channel entries, withwatts: nullfor any the payload omitted. The card
summed them to 0 W, and the recorder started itspanel_loadsum at 0 and stored that as a
measured "house drew 0 W" — a row the Today tiles integrate and the night-charge load model and
band calibration learn from. The card now shows "—" with "panel not reporting", and the
recorder writes no row, so the gap is a coverage gap every consumer already handles. The
runway's last-resort fallback re-used its own previous recent load, which each compute wrote
back, so with no rows it would carry a pre-silence load (an EV charging) forward indefinitely;
the carry now lasts only while a real reading is under two hours old.
The card's numbers now come from a pure module, web/src/cards/energyFlowModel.ts, which the
server test suite imports and runs against recorded scenes from 2026-09-22 (the 03:30 backstop, the
03:43 charge ramp, the 04:05 and 04:35 steady charge, midday solar). New harness
scripts/mutate-energy-flow.mjs (15 anchor-asserted mutants). The Solar and Batteries nodes are
not balanced against the house: PV is metered on the DC side and the house on the AC side, and the
MPPT, charger and inverter losses between them are not drawn.
v1.174.0
Two transient conditions no longer speak: a sunrise solar code, and a brief cell-spread excursion
-
The MPPT string error code must now stand for three minutes. Error code 457 is the benign
standby status a Delta Pro Ultra reports on a shedding string; the existing guard rejected it by
requiring the string to be producing, a rule derived entirely from sunset, where a shedding
string makes no watts. At sunrise the same code rides a string that IS producing: on 2026-09-22
one home Core reported HV code 457 at 407 W / 301 V / 1.38 A for about 60 seconds at 06:58 and
again at 07:33, and both were announced over the house before clearing on the next tick (an
identical 60-second blip on 2026-08-30 at 06:43).dpu-pvh-err/dpu-pvl-errnow also require
the SAME code to stand forMPPT_ERR_DEBOUNCE_MS(three minutes, the window the inverter error
code and the SHP2 source error already use). A string genuinely faulting while producing still
alarms — three minutes later.The clock behind that window (
SnapshotStore.trackMpptErrOnsets, keyed<sn>:hv/<sn>:lv)
advances only while the code is non-zero AND the string is producing, which is the exact
condition the alarm fires on. A code standing overnight on a dark string therefore banks no
time, and the sunrise ramp that carries it into real watts starts the window from zero. The
producing test itself moves toserver/src/mppt.tsso the alarm engine and the clock read one
definition. -
A cell-imbalance warning waits ten minutes before it is spoken. The cell-spread warning fires
at 24 mV and holds at 20 mV, close enough to normal working spread that packs cross it and settle
back within minutes: a six-minute excursion at 21:14 on 2026-09-21 was announced, as were
episodes through the night. Nothing in the first minutes is actionable.vdiff-warn-*and the
peer-voldiff-*report of the same event now stay off the audible path until the spread has
stood forIMBALANCE_SPEAK_HOLD_MS; the card, the push and the digest are unchanged, and the
CRITICAL imbalance still speaks immediately. Age is read from the restart-persistent onset
sidecar, so the hold is not reset by the roughly daily host restart.
New harness scripts/mutate-alarm-transients.mjs (12 anchor-asserted mutants) covers both guards,
including the two ways each could ship inert: a debounce clock that counts a dark string, and a
speak hold that is exported, tested and never wired into the tick.
v1.173.3
Source comments, test descriptions and design docs in the spec register
Source comments, test descriptions, mutation-harness notes and the design documents under
docs/ carried quoted request text and personal attributions for decisions. Each is restated as
a plain policy or requirement — for example, the stale-data alarm's rule now reads "Policy: the
stale-data alarm sounds only after the immediate remediation has failed." One runtime string
changes with it: the write-readiness gate's blocking text now says re-scoping the cushion is "a
policy decision" (PERFORMANCE.md quotes it and is updated to match). No behaviour, identifier,
threshold or mutation anchor changes; "owner" is kept where it names the user-configured reserve
floor (ownerReserveFloorPct) and the manual-cancel control.
v1.173.2
Release notes in the spec register; one log line per held boot yellow
- Release notes no longer quote or attribute requests. CHANGELOG.md, CHANGELOG-ARCHIVE.md
and the release paragraphs in DOCS.md carried quoted request text and "Owner (date):" style
labels. Each is rewritten as a plain statement of the problem, the measurement and the change;
every version, number, identifier and requirement is kept. Quotations of log lines, UI text and
announcement text are unchanged. - The boot-yellow hold logs once per episode. v1.173.1 wrote "yellow held for boot
confirmation" on every 10-second tick while a startup yellow waited out its confirmation window
(eight identical lines on the first restart). It now writes one line per held episode.