Skip to content

Releases: tesseractAZ/power

v1.180.0

Choose a tag to compare

@github-actions github-actions released this 23 Sep 03:35
Immutable release. Only release title and notes can be modified.
2034430

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_grid read 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.json and restarting the add-on clears it by hand.

New harness scripts/mutate-grid-reading-persist.mjs (15 anchor-asserted mutants).

v1.179.0

Choose a tag to compare

@github-actions github-actions released this 23 Sep 02:56
Immutable release. Only release title and notes can be modified.
fdb188b

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_grid read OFF and shp2_grid_connected read 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_connected reads 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

Choose a tag to compare

@github-actions github-actions released this 23 Sep 02:28
Immutable release. Only release title and notes can be modified.
802f85e

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
    and Panel Load still 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

Choose a tag to compare

@github-actions github-actions released this 23 Sep 02:16
Immutable release. Only release title and notes can be modified.
0fbaad3

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 (or GRID_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_only read ON, off_grid read 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 reporting gridSta 0 ("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, dropped off_grid and load_shed_recommended mid-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
    reported gridSta vetoes 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_today is total_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-state apply 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

Choose a tag to compare

@github-actions github-actions released this 23 Sep 00:11
Immutable release. Only release title and notes can be modified.
a86f346

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's recentLoadBasis.
  • 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,
    matching emptyAtMs instead 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

Choose a tag to compare

@github-actions github-actions released this 22 Sep 22:10
Immutable release. Only release title and notes can be modified.
c4f7ead

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's generatedAt, 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 /status online 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/today now returns dayEndMs, 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

Choose a tag to compare

@github-actions github-actions released this 22 Sep 22:05
Immutable release. Only release title and notes can be modified.
69e31b4

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 was Math.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, with watts: null for any the payload omitted. The card
    summed them to 0 W, and the recorder started its panel_load sum 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

Choose a tag to compare

@github-actions github-actions released this 22 Sep 18:25
Immutable release. Only release title and notes can be modified.
05ceb35

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-err now also require
    the SAME code to stand for MPPT_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 to server/src/mppt.ts so 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 for IMBALANCE_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

Choose a tag to compare

@github-actions github-actions released this 22 Sep 02:54
Immutable release. Only release title and notes can be modified.
c06d489

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

Choose a tag to compare

@github-actions github-actions released this 22 Sep 02:37
Immutable release. Only release title and notes can be modified.
53c49df

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.