SAFETY-136: the line said 10 to 15 s late is not caught, then that 15 s late
is caught after about 4 minutes. It now says 10 s is not caught and 15 s only
after about 4 minutes, as the card says.
SAFETY-138: the 30 s timer that zeroes a late write is the ESPHome board's
watchdog (README), not the inverter's own.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Minor bump, no option changes. Folds "Not in a release yet" into a new
0.21.0 section, adds the contents table for the nine merges on release/1.0
since master 0.20.1, updates the known issues, and rewrites the 0.20.1
SAFETY-128 known issue to say it ships in 0.21.0.
Co-Authored-By: Claude <noreply@anthropic.com>
The 0 W of shutdown and of the Stop button now waits (bounded, 2 s) for a
write already on its way to the same unit, for every driver, GoodWe included.
Once shutdown has begun, cycle, heartbeat and safety-stop writes are dropped
right before the write is created. One shared wait (_await_inflight) serves
the 0 W and SMA-10's _release_last.
Co-Authored-By: Claude <noreply@anthropic.com>
A driver now says whether its release() hands the inverter back
(Driver.hands_back; GoodWe is False, so release() stays write(0) and the
Controller never calls it). command_site_release() sits next to
command_site_zero(): the Stop button and shutdown() (so SIGTERM, restart,
control task death and a late config refusal) hand a hand-back unit back,
once, and zero every other unit as before. Safety stops and refusals still
hold 0 W for every unit. Shadow mode releases a hand-back unit. After a
release nothing is written to the unit (heartbeat and cycle included) until
control starts again. release() is bounded by RELEASE_BUDGET_S (= the write
budget), runs concurrently per unit, and a raising or hanging release on one
unit does not stop the others.
The "set to 0 W" sentences for Stop, restart, shadow mode and the stopped
banner are built from the roster (app/handback.py, EN and NL, words from the
driver); a GoodWe-only roster keeps every old literal. A handed-back unit
shows "Back under <its control>" on Home.
Existing test edited: test_messages.py (two classification entries for the
CRITICAL line that moved into a roster builder, and the new release bound).
Co-Authored-By: Claude <noreply@anthropic.com>
Owner decision 2026-11-07: clients see nothing new until SMA-4. config.yaml,
translations, labels and the page layout are back to 23ff9d0's. The validation
of the keys stays for callers that skip the schema.
Co-Authored-By: Claude <noreply@anthropic.com>
app/drivers.py holds one Driver per roster entry (configured, actuator_id,
limits, read, write, release, probe, link_stale_s, batt_bound, settle_s, brand,
words). GoodWeEsphomeDriver makes the calls the code it replaced made, in the
same order. Inverter gets driver; has_writer, is_live and link_note read it. The
SoC/power read, the write, the device range (run_control, amain, the settings
panel), rule 2 per unit and the SAFETY-28 census go through it.
setpoint_entity is optional per entry beside driver, host, port, unit_id and
maintenance; only goodwe_esphome is selectable, the rest are checked and unused.
Co-Authored-By: Claude <noreply@anthropic.com>
One stale Forecast.Solar tomorrow sensor dropped the whole OPT-17 sum from
17:54 to 20:00. Today readings and the OPT-34 morning rules are unchanged.
Co-Authored-By: Claude <noreply@anthropic.com>
The meter's delay fit needed 16 new values in 60 s, so at the owner's 5 s P1
only the batteries' fit ran and 30 s late was found after about 3 minutes
(export to -420 W). A meter that slow is now also fitted over 120 s; it calls
a delay late only over 16 s, on two fits in a row, with the same corroboration
as before, and never ends a verdict, so a slow battery (R2-1) runs as before.
Found about 90 s after the start, export above -200 W. 10 s late at a 5 s
meter is not found (nothing swings; it cannot be told from a 10 s battery).
Co-Authored-By: Claude <noreply@anthropic.com>
The cheap run is sized at the full rating, so under the peak limit a battery is
still short when it ends, and control plans the next run then. Home drew only
the run under way. `cheaproll` rolls the run on the way control does
(`modetrade.energy()` + `priceruns.plan()` at each run end, lifted from
`dayview._levels()`), reading the levels at the charge power the peak limit
leaves (peak aim less the house, else the rating). `homeview.mode_runs()` and
`price_windows()` use it for today: the grid block, the Today line and the
chart's cheap bars run to where control stops, each battery's block to its
own stop. Display only: no control change.
test_ux54: today's grid block of the night-before scenario now follows the roll.
Co-Authored-By: Claude <noreply@anthropic.com>
Brings OPT-36 (#229) and TEST-37 (#231) onto release/1.0, which keeps
SAFETY-128 (#228). No textual conflicts. CHANGELOG: the SAFETY-128 entry
moved out of the released 0.20.0 section into "Not in a release yet" above
## 0.20.1; the 0.20.0 and 0.20.1 sections are identical to master.
Co-Authored-By: Claude <noreply@anthropic.com>
SAFETY-132: the 9.6 kW is the charge the add-on asked for in a test,
not a measured grid figure. SAFETY-133: the quarter also ends over the
3 500 W limit; the guard sees the load late (0.20.1 has no lateness check).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Patch bump: OPT-36 (Leftover solar counts Cheap hours only and Solar first batteries). No option changes.
Co-Authored-By: Claude <noreply@anthropic.com>
Review F1: a slot after B's run counts only if no slot from the expensive
hours on is cheaper (control plans B's charge left in the cheapest window
ahead), and x 0.95 of B's charge is stored. Three-price night: 1ac7fe9 left
the expensive hours 2.61 kWh short; now 0.
Review F2: a Solar first battery's drain until the expensive hours reads each
hour at no less than the night base (the profile is net of the sun). Read net,
a dark morning drained the battery the plan still counted; A's level fell,
the cheap run moved and was lost before it started (+0.40 EUR per 3 days).
The two real-price replays now cost what 0.20.0 does.
F3: the ux54 sweep's peak limit is 15 kW (the option's top). F4: the rest
comes from the grid (EN/NL). F5: CHANGELOG and DECISIONS wording.
Co-Authored-By: Claude <noreply@anthropic.com>
Live 0.20.0: A (Leftover solar, 60 %) charged to 95 % while B (Cheap hours,
94 %) already covered the expensive hours. dayplan.plan() now takes off the
shortfall what Cheap hours batteries will hold when the hours start (the
ceiling or what their cheap run reaches under the peak aim) and what Solar
first batteries will still hold after the night's use. Home reads a give
block after the planned charge, plan_cover() is the plan's own figure, and
the Why line says other batteries cover.
Co-Authored-By: Claude <noreply@anthropic.com>
Readings that Home Assistant delivers late but still changing passed
every staleness gate, so the loop corrected again each cycle an error
its last command had already fixed (10 s late: the give run swung and
exported to -1.9 kW), and the peak guard pushed to the inverter limit
on the same late figures (OPT-34 F-2).
app/lateness.py fits how long after a power request the meter shows
it. Over 8 s the readings are late: a request the battery reading has
not answered holds the loop (it may not lead the reading by more than
100 W) and caps the peak guard at what stands; unanswered for 23 s it
holds, then 0 W after stale_input_s. An amber row and one log line per
episode say so. With readings on time nothing changes (digests equal).
DECISIONS/SAFETY-128-late-readings.md; test_safety128.py.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
- The OPT-23 headline said a daily-total forecast is now counted right by
day; OPT-30 (known issue) still over-counts by day. Say what changed.
- CI-15: 16 min was one run; runs took 16 to 24 min.
- UX-75: quote the page text as it reads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a fresh today reading in the night (02:00 on the rig) went stale
(05:00), Hold.stand_in(as_today=True) served the 17:05 "tomorrow" figure
again until 06:00: the plan went back to 95 %, the latch took it, and
Leftover solar charged back up. read_sum() now skips that stand-in once
every today sensor has given a fresh reading for the day
(Hold.replaced()); the stale newer reading follows the OPT-27 rules.
Co-Authored-By: Claude <noreply@anthropic.com>
While a forecast is present and the cheap run holds, the night-charge latch is
rebuilt from the plan: a latched battery takes its current level whatever its
role this tick (a fresh figure may lower it below the SoC), one that left the
plan is dropped, a new one comes in only while it charges from the grid.
Test: a fresh brighter figure lowers A below its SoC, the sensors go quiet, A
does not charge back up. CHANGELOG summary says two fixes.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
With the forecast sensors silent from 17:05 the kept evening figure ended at
midnight + 3 h, in the middle of the night grid charge, and Leftover solar's
planned level went with it (rig: A 49.8 % against 79.9 %).
- A night grid charge that has started keeps its planned level when the forecast
goes (Controller._latch_charge, store key night_charge_latch), and the narrow
price gap with it (weather_held). A fresh figure still moves the level.
- The evening's tomorrow figure is today's until the bell starts (06:00 local),
up to 15 h old but only if it was usable at midnight (solarfc.Hold.stand_in).
- Screen: "No solar forecast now. The charge under way goes on to its planned
level." (EN/NL) while a latched battery is below its level.
- test_opt34.py (whole nights through the real control cycle, mutants), test_opt27
and test_modes_p4 adjusted to the new window and the kept level.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
Text only (DECISIONS, DOCS, CHANGELOG): a step load alarms on the same cycle
as before; a steadily rising load alarms a little later; with peak_margin_w 0
a period can end up to 10 W over the limit without this alarm, as for 'Load
too high' (SAFETY-81). Owner accepted 2026-10-31.
test_safety126.py part 2: after a real overrun ends, _peak_landing()'s latch
is reset and the night's later landings are quiet (review F3).
Co-Authored-By: Claude <noreply@anthropic.com>
During the night grid charge under the peak reserve the guard alarmed
(ERROR PEAK_GUARD_FLOOR_LIMITED, "Battery stopped limiting the peak") and the
Batteries "Why" said "The peak limit needs power" at quarter ends, while every
quarter landed on the aim. The charge lands the period 0-7 W over the aim and
excess_w()'s 900 / remaining turns that into tens of kW in the last second;
with nothing able to give, check_peak_floor_limited() alarmed on any
wanted > 0.
Each cycle the guard wants power, _peak_landing() asks misses_aim(): with the
batteries at 0 W, does the period end more than ALARM_MISS_W over the aim? If
not, the decision is within_aim: no reserve alarm, no red peak row, no "Why"
peak. The claim and the commands are unchanged. A real overrun under the
reserve alarms on the same cycle as before.
Co-Authored-By: Claude <noreply@anthropic.com>
- TARIFF-24 DECISIONS: the hold keeps a Cheap hours charge; a Leftover solar
battery still stops without a forecast (it has no planned level, still true
after OPT-28: `dayplan.plan()` returns None and `charge_to()` gives None).
Adds "a run that has started runs to its end" for a give run begun in a hold.
- CHANGELOG 0.19.2: "when the period is over" -> "when the period would go
over the limit".
Co-Authored-By: Claude <noreply@anthropic.com>
Owner 2026-10-29: fix N=1 in this hotfix. _charge_headroom() no longer
returns early at one inverter, so the N=1 cheap-window maintenance charge
is held to charge_room_w() as at N=2; SAFETY-84's site-stop rule stays N>=2
only (at N=1 such a stop outranks the shaping anyway). The guard no longer
steps in and gives power at each quarter's end during that charge.
test_safety125.py: N=1 maintenance on 5 s and 10 s meters with late
readings, with and without a banked peak; the row in an N=2 maintenance
charge and in shadow mode; edge cases (blind past the hold, a start
mid-quarter, the summer-time change, the forecast in W, a 10 s meter, the
last second). test_safety84/87: three N=1 pins move. test_safety119: its
quarter-edge hand-back runs keep the old N=1 sizing as a fixture, plus a
check that with the new sizing nothing hands back. CHANGELOG, DECISIONS,
DOCS follow.
Co-Authored-By: Claude <noreply@anthropic.com>
- The over-the-limit rows ran over STYLE's 15 words (EN 16/19, NL 16/21)
and two Dutch Home sentences over 12 words; they were not in the samples,
so test_messages/test_ux46 never saw them. Now:
"Peak guard: grid charge limited, at this rate 5 814 W, over 5 413 W" /
"Piekbewaking: laden beperkt, aan dit tempo 5 814 W, boven 5 413 W",
"...idle, at this rate ..., over ..." / "...in rust, aan dit tempo ...",
NL Home "Laadt minder van het net om onder 5,4 kW te blijven." and
"Zou minder van het net laden om onder 5,4 kW te blijven."
The rows and the line are added to the samples. The under row is unchanged.
- CHANGELOG known issues and the DECISIONS note: at N=1 the maintenance
charge is not sized to the room, so SAFETY-99 still applies to it.
- DOCS: the peak guard row's new words.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A meter reading late near a quarter's end left seconds the projection did
not count (SAFETY-99), and charge_headroom_w() read them as room: the cheap
run (and price trading, and the N=2 maintenance charge) went back to full
power in the quarter's last seconds and the quarter could land over a banked
month peak or the cap. peakguard.charge_room_w() counts those seconds at the
held reading for the charge only; the guard's floor is unchanged.
The guard row said "idle, heading for 5 814 W, under this month's 5 413 W
peak". Over the limit at the current rate the row and the month-peak line
now say so, and that the grid charge is cut back.
Version 0.19.2, CHANGELOG, DECISIONS. test_safety125.py new; the lag pins
in test_price_run_fixes.py and test_opt20.py and the AST pin in
test_arbitrage.py follow the change (see the pull request).
Co-Authored-By: Claude <noreply@anthropic.com>
With battery modes on, the price gap a mode needs was always widened x1.5 by
Uncertainty.NO_WEATHER, even with a solar forecast read, so a 0.08 day/night
gap never traded at the shipped cycle cost. The doubt is now added only when
no usable forecast is read (solar_fc is None, the day plan's own test), in the
run plan (_price_runs), the charge gate (_mode_roles) and Home's mirror
(homeview.trade_verdict). The rule is one helper, arbitrage.weather_uncertainty();
the modes-off trade path (main.py cycle) keeps its literal NO_WEATHER.
DOCS "Battery modes" sentence, CHANGELOG 0.19.1 line and the DECISIONS note
follow. Tests: test_tariff24_weather.py.
Co-Authored-By: Claude <noreply@anthropic.com>
With battery modes on, a day/night or fixed static_profile, no price feed and
arbitrage off, tariff_refresh_wanted() was false: no tariff task, no
tariff_plan, mode_runs None, so Leftover solar / Cheap hours never charged from
the grid and the Batteries screen said "No prices". packmode.enabled(opts) is
now a third term of the gate. With price_provider off the task is pure
arithmetic on static_profile, so no HTTPS call is added.
Also: version 0.19.1, CHANGELOG entry, DECISIONS note, test_tariff24.py, and a
shorter tariff_refresh_wanted() docstring (history stays in DECISIONS).
Co-Authored-By: Claude <noreply@anthropic.com>
Owner chose 0.19.0 (2026-10-26) because PRICE-02 changes trading behaviour.
Version and CHANGELOG heading/first line only; no option changes.
Co-Authored-By: Claude <noreply@anthropic.com>
Bump version to 0.18.3 and add the 0.18.3 CHANGELOG section: DOC-8, UX-45,
SAFETY-119, SAFETY-121, UX-46, PRICE-02. A patch bump: no option added,
removed or renamed, no default changed.
Co-Authored-By: Claude <noreply@anthropic.com>
Review of PR #199. "Warns once" left out the Home row and the Settings
note, which stay while the fault lasts. "No option change" now points to
the one case that needs a look.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SAFETY-118 line: the add-on already turns a kW forecast into W
(main.py read_inputs, abs(raw) < 50), so only the settings-page warning is
wrong. Rounding note and rows: the new settings page; UX-44 also changed
one DOCS line.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
OPT-15 and OPT-20 rows: the retry is two inverters only, and OPT-20 no
longer claims no other battery moves. SAFETY-111 added to known issues.
MAINT-19 marked as from code reading. Wording fixes in the SAFETY-112 and
MAINT-11 rows, UX-38 and the price service note.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
config.yaml version 0.17.0 -> 0.18.0 and a closed ## 0.18.0 CHANGELOG
section for OPT-17, SAFETY-112, OPT-15, MAINT-11, PRICE-01 and OPT-20.
No code change.
Co-Authored-By: Claude <noreply@anthropic.com>
SAFETY-107 row names the tested base; MAINT-17 says code reading only;
SAFETY-105 figure is from a simulation; SAFETY-97 edge is 175 W; the
maintenance note cites MAINT-14 and MAINT-15; SAFETY-108 "about 15 s".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bump version to 0.17.0 and add the closed 0.17.0 CHANGELOG section.
No code change, no option change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SAFETY-103 swing 'by about 800 W for about 5 s'; SAFETY-99 'late, by less
than 30 s'; MAINT-10 row: one-at-a-time is not new; SAFETY-96: drop the
test-only 800 W; SAFETY-90 and MAINT-10 rows trimmed to two sentences.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
config.yaml version 0.14.0 -> 0.15.0 (minor: behaviour change from
SAFETY-94/97, no option change). New `## 0.15.0` section with the contents
since 0.14.0 (cf705a9#171, 3129dae#172) and the known issues.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Slew step not one house's 400 W; three two-inverter known issues, not
four; commissioning reference covers a new install; SoC example holds
for decimal sensors; SAFETY-95 states the jump, not the export.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two inverters are now supported for maintenance and the peak guard
(SAFETY-91, SAFETY-93). No option changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Crash, not stop, for SAFETY-61; Watchdog off by default; its own
warning for the 50 W peak margin (also under a higher month peak);
"refuses power" for SAFETY-52/73; no one-house options claim.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bump config.yaml version 0.12.0 -> 0.13.0 and close a ## 0.13.0
CHANGELOG section covering every merge since 0.12.0 (a45ed06), with the
known issues SAFETY-90/91/92/93/94. Option surface 77 -> 83, none removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>