Changelog¶
Every release of Smart Battery Pilot, newest first. The same notes are on GitHub, and Home Assistant shows them in the update dialog when HACS offers a new version.
| Version | Released | Headline |
|---|---|---|
| v1.6.2 | 2026-11-05 | Refill the backup reserve where it pays most |
| v1.6.1 | 2026-11-01 | Keep a running charge when a re-plan repeats or fails its script call |
| v1.6.0 | 2026-10-30 | Report when the battery doesn't do what the plan counts on |
| v1.5.0 | 2026-10-30 | Two new card views and a visual polish |
| v1.4.0 | 2026-10-29 | Hourly PV curve from Energy-dashboard forecasts |
| v1.3.1 | 2026-10-18 | README screenshots render in HACS |
| v1.3.0 | 2026-10-18 | Your currency, and export at the best peak |
| v1.2.5 | 2026-10-14 | |
| v1.2.4 | 2026-10-11 | The documentation gets its own pages |
| v1.2.3 | 2026-10-11 | The card says how the charger stands |
| v1.2.2 | 2026-10-11 | A project website and a clearer card |
| v1.2.1 | 2026-10-11 | evcc's limit where it fits, a grid import limit |
| v1.2.0 | 2026-10-11 | Plan around the charging evcc has planned |
| v1.1.0 | 2026-10-11 | EV charging, evcc and other controllers |
| v1.0.0 | 2026-09-29 | Honest accounting: battery benefit and pilot savings |
| v0.9.0 | 2026-09-29 | A backup reserve for grid outages |
| v0.8.0 | 2026-09-29 | Cold-weather charging, and every decision explains itself |
| v0.7.2 | 2026-09-14 | The card's tooltip and curve labels are back |
| v0.7.1 | 2026-09-14 | The card draws at any size and takes a height |
| v0.7.0 | 2026-09-10 | A rewritten card, and a forecast that lets go of an old sensor |
| v0.6.7 | 2026-09-05 | Cent prices, export without a script and a stale card |
| v0.6.6 | 2026-09-05 | Ten languages |
| v0.6.5 | 2026-09-05 | Held-back energy no longer displaces free PV |
| v0.6.4 | 2026-09-05 | Cent prices, savings while off and a hanging script |
| v0.6.3 | 2026-09-05 | The master switch comes back, and no slot charges and discharges at once |
| v0.6.2 | 2026-09-04 | Forecast and savings stop trusting impossible data |
| v0.6.1 | 2026-09-03 | Export follows the planned power, no discharging into negative prices |
| v0.6.0 | 2026-09-03 | 19 fixes, and savings measured against doing nothing |
| v0.5.1 | 2026-09-02 | The documentation in English |
| v0.5.0 | 2026-09-02 | Dry-run, stale prices and savings meters handled correctly |
| v0.4.0 | 2026-09-02 | Follow-ups after the HACS listing |
| v0.3.1 | 2026-08-21 | The card speaks your language |
| v0.3.0 | 2026-08-21 | English states, Home Assistant 2024.11 minimum |
| v0.2.0 | 2026-06-12 | The new sensors, released as a minor version |
| v0.1.4 | 2026-06-12 | Sensors for price, plan status and savings |
| v0.1.0 | 2026-06-11 | Initial release |
v1.6.2 — Refill the backup reserve where it pays most¶
Released 2026-11-05 · on GitHub
This release plans a backup reserve refill around the price peaks in front of it, and makes the consistency checks more accurate and easier to follow (#69, thanks to @emilsundstrom83-spec for the detailed question).
When the SOC is below the backup reserve, the battery may spend nothing until the reserve is refilled. The refill went to the cheapest slot before the deadline, chosen on price alone. If that slot came after a price peak, the peak was lost: no arbitrage and no idle before the refill, only auto. This was easy to run into with a reserve raised at runtime, for example on a storm warning.
- Refill where it pays most: the planner now also tries finishing the refill earlier, in front of each slot worth more than the energy it could buy before it. It plans the whole day around each of these refills and keeps the cheapest plan. If nothing is gained, the refill stays in the cheapest slot as before (#71, #72).
- Honest savings: only the cheapest refill counts as mandatory. If the refill moves earlier, its extra cost comes off the estimated savings.
reserve_refill_cost_eurshows what the chosen refill really costs. - More accurate auto-slot checks: the checks "PV surplus doesn't reach the battery" and "battery doesn't cover the house" now sample PV and house load every minute, instead of once per refresh or slot boundary. On a variable sky the old readings could overstate the surplus or deficit and push towards a false message (#73).
- A week of check history in the diagnostics: each consistency check keeps its daily verdict for the last 7 days under
runtime.health.history. Strikes, and a good day resetting a series, are logged at info level (#70). - Docs: the BYD + Fronius Gen24 example explains the inverter's revert timeout (
InOutWRte_RvrtTms) and why the example scripts assume0(#68).
Upgrading¶
Nothing to configure. With a backup reserve, a refill can now show up earlier in the plan than before, together with a charge for the following peak. The minute sampling for the auto-slot checks only runs when a PV power or consumption entity is configured.
Full changelog: https://github.com/cygnusb/ha-smart-battery-pilot/compare/v1.6.1...v1.6.2
v1.6.1 — Keep a running charge when a re-plan repeats or fails its script call¶
Released 2026-11-01 · on GitHub
This release fixes a charge slot that could be cancelled seconds after it started (#65, thanks to @emilsundstrom83-spec for the detailed report).
The charge and export scripts ran again on every re-plan, even with identical values. A re-plan often lands right at a slot boundary, for example when the price sensor updates at that moment, so the script could run twice within seconds. If the integration behind it limits how often a register can be written, the second call failed. The pilot then fell back to auto and ended the charge the first call had started.
- No repeated calls: within a slot, a charge or export script runs again only when
power_worreserve_socchanged. Every new slot still calls it, even with the same values. - A failed update no longer cancels the slot: if an update fails while the mode is already running in this slot, the mode keeps running and the next re-plan or slot boundary tries again. Until then, the battery continues with the previous values.
- The fail-safe stays: if the first call of a slot fails, the pilot still falls back to auto mode. A script that keeps failing still raises its message under Repairs.
Two calls can still come close together when the power really changes, so a script should tolerate a rate limit or skip writes that wouldn't change anything. See Control scripts.
Upgrading¶
Nothing to configure. If a script relied on being called again by a re-plan within a slot, it now runs only at the start of the slot and when its values change. The diagnostics download shows the new outcome failed_kept_running for an update that failed.
Full changelog: https://github.com/cygnusb/ha-smart-battery-pilot/compare/v1.6.0...v1.6.1
v1.6.0 — Report when the battery doesn't do what the plan counts on¶
Released 2026-10-30 · on GitHub
The pilot now notices when the battery doesn't do what the plan counts on. Until now, a failed assumption caused no error: a charge script that barely charges, an idle slot that leaks or PV surplus that never reaches the battery simply let the plan and the battery drift apart. The first symptom was the bill.
Eleven read-only checks now compare the two and raise a message under Settings → Repairs:
- The pilot isn't steering: a control script keeps failing, there has been no valid plan for 12 hours, or another controller has held the battery for 24 hours.
- A sensor or setting is off: the SOC stands still while energy moves, the PV forecast for today is really a "remaining today" sensor, or the configured capacity is more than 25 % off.
- An action doesn't work: charge, idle and export slots that don't do their part, and auto slots that don't cover the house or don't take PV surplus (#58).
The checks only report; they never change a plan or a script call. Most need three days with enough energy at stake before they fire, and a good day clears the message again. Only slots the pilot really drives count, and surplus and deficit are measured, so a wrong PV forecast can't raise a message. All details are in Consistency checks. The current state of the checks is in the diagnostics download under runtime.health.
The limits are first values. If a message is wrong for your installation, please open an issue with the diagnostics attached. If it describes something you do on purpose, use Ignore on the message.
The docs also gain What an auto slot assumes and an example for a command-driven AC battery (Zendure SolarFlow 2400 AC+, thanks to @flatpackfan).
Upgrading¶
If you update from v1.4.0, you also get the card changes of v1.5.0: the bars and flow views and a visual polish.
Nothing to configure. New messages may appear under Repairs in the days after the update. The check for PV surplus needs the optional live PV power entity; without it, it stays off.
Full changelog: https://github.com/cygnusb/ha-smart-battery-pilot/compare/v1.5.0...v1.6.0
v1.5.0 — Two new card views and a visual polish¶
Released 2026-10-30 · on GitHub
Two things for the dashboard card: the existing charts get a visual polish, and two new views draw the plan differently.
This release was published after v1.6.0, to complete the version history. v1.6.0 contains everything here, so there's no reason to go back to this version.
New views¶
view: bars: The price as one bar per slot in the colour of the planned action, which replaces the ribbon. The energy and SOC panels are as intracks.view: flow: The price as a band of one orange that darkens with the price, with the cheapest and dearest slot labeled above it and the action ribbon directly under it. PV is drawn upwards and consumption downwards around zero. The SOC runs across 0–100 %, withmin_socandmax_socas dashed lines.- EV charging: With EV charging on the plan, both views add a lane under the price with the car's kWh per slot, on a scale of its own.
barsdraws evcc's price limit as a line;flowputs a dot on every slot under it.
Polish in all views¶
- Areas: Fills fade from the curve to the baseline, and orange and amber no longer turn into a brown block on dark themes.
- Grid: The tinted plot boxes are gone, and the grid is a hairline in the theme's divider colour.
- Now marker: The red "now" line is replaced by a playhead, a pointer above the action ribbon with a hairline through the panels.
- Hover: Hovering puts a dot on every curve, and the tooltip opens each series row with its colour.
- Price labels: The cheapest slot is now labeled next to the most expensive one.
Upgrading¶
Nothing to configure. To try a new view, set view: bars or view: flow on the card.
Full changelog: https://github.com/cygnusb/ha-smart-battery-pilot/compare/v1.4.0...v1.5.0
v1.4.0 — Hourly PV curve from Energy-dashboard forecasts¶
Released 2026-10-29 · on GitHub
Until now, the daily PV forecast was spread over the daylight hours as an even, symmetric curve. That misplaces much of the yield of east- or west-facing arrays, and of days whose clouds clear or build up during the day. For an east roof, the planner expected an afternoon yield that never came.
Forecast integrations that feed the Energy dashboard (Forecast.Solar, Open-Meteo Solar Forecast, Solcast, Helios and others) already know the hourly shape of the day. The PV step now has an optional Hourly forecast field: pick one config entry per roof face, and their hourly curves are added up and used slot by slot. The card's PV curve follows automatically.
- Fallback, never a missing roof. If one selected entry delivers no curve, the whole cycle plans with the daily sensors and the log says so once. Slots beyond the end of the shortest curve also use the daily sensors, so keep them configured.
- Startup. Forecast integrations often load after this one. While Home Assistant is starting, a missing curve is not reported; the plan is redone once startup has finished.
- Forecast.Solar stamps each value with the end of its hour, other integrations with the start. Both are read correctly.
- Local hour of day. A price source that stamps its slots in UTC no longer shifts the PV curve and the consumption profile by the UTC offset.
- The diagnostics download shows under
inputs.pv_hourlywhich entries were used, which failed, and how many slots came from the curve.
The docs now also point out two common pitfalls with the daily sensors. Enter the forecast for the whole day, never "remaining today". Helios Forecast numbers its days from today: energy_day_1 is today, energy_day_2 is tomorrow.
Upgrading¶
Nothing changes on its own: existing installations keep planning exactly as before. To use the hourly curve, open Settings → Devices & services → Smart Battery Pilot → Configure → PV forecast and pick your forecast entries under Hourly forecast, then Apply. A new setup pre-selects the entries your Energy dashboard already uses.
Full changelog: https://github.com/cygnusb/ha-smart-battery-pilot/compare/v1.3.1...v1.4.0
v1.3.1 — README screenshots render in HACS¶
Released 2026-10-18 · on GitHub
The README screenshots were broken in the HACS frontend: it does not support <picture> and does not resolve relative image paths. The screenshots now use plain <img> tags with absolute URLs, so they show up in HACS as well as on GitHub.
No functional changes to the integration or the card.
Upgrading¶
Nothing to do. Update through HACS as usual; the README in HACS refreshes with the new release.
Full changelog: https://github.com/cygnusb/ha-smart-battery-pilot/compare/v1.3.0...v1.3.1
v1.3.0 — Your currency, and export at the best peak¶
Released 2026-10-18 · on GitHub
This release brings two changes: prices are counted in your Home Assistant currency, and export mode gives stored energy to the most valuable peak.
Prices in your currency (#50, #52)¶
Until now, prices were passed through unconverted, but everything around them assumed euros: sensor units, config-flow labels and limits, the default spread and feed-in tariff, the plausibility bound, and the card's price axis. A price curve in SEK/kWh was planned correctly but labelled EUR, got defaults about ten times too small, and a price spike above 10 SEK/kWh made the pilot refuse to plan.
The currency now comes from Home Assistant's own setting. Price and savings sensors carry that unit, the config and options flows label and bound the price fields in it, and a new entry starts with a spread and feed-in tariff sized for it. The plausibility bound and evcc's limit tolerance scale with it as well. If your price entity declares a different currency than Home Assistant, planning carries on and the mismatch is shown in the price step of the flow, as a repair issue under Settings → Repairs, and in the log. The card follows the plan's currency.
Export mode: the best peak gets the energy (#49, #51)¶
In export mode the optimizer used to assign self-consumption for every slot before it looked at export at all. A cheap night slot could therefore spend stored energy that an evening price peak would have sold for far more, and on some days the plan exported nothing. In synthetic winter scenarios this left 1.7–2.2 EUR per day on the table compared with an exact optimum of the same model.
Covering demand and exporting now compete for stored energy in one list, ordered by value. Covering demand is worth the import price, exporting is worth the sell price minus your spread. The spread discount means a slot's demand stays covered unless selling the energy elsewhere clearly pays more.
Self-consumption mode plans exactly as before.
Upgrading¶
Update through HACS and restart Home Assistant.
- Export mode: plans now look different. The battery may be held (
idle) for a few hours ahead of a price peak, then sell for several slots in a row at full discharge power, down to the backup reserve or min SOC. After the peak, the house is supplied from the grid when that is cheaper than the energy was worth at the peak. Your export script must keep serving the house while it discharges into the grid; this matters more now that export runs can last several hours. If your inverter cannot do both, switch to self-consumption mode. To make export more selective, raise the spread threshold. - Home Assistant currency EUR: no visible change from the currency work.
- Home Assistant currency other than EUR: the price and savings sensors change their unit from EUR to your currency. Home Assistant may show a one-time repair about a changed statistics unit for these sensors; the old unit was wrong, so accept the new one. Stored values for price offset, feed-in tariff and spread are not changed (only new entries get scaled defaults), so review them once in the options. A later change of Home Assistant's currency takes effect after reloading the integration.
Entity IDs, unique IDs and attribute names stay the same, so history, statistics and templates carry on.
Full changelog: https://github.com/cygnusb/ha-smart-battery-pilot/compare/v1.2.5...v1.3.0
v1.2.5¶
Released 2026-10-14 · on GitHub
Fix: the card is now registered as a Lovelace resource (storage mode) instead of only via extra_js_url. This avoids a race with the scoped custom element registry polyfill that made the card intermittently missing (#47, home-assistant/frontend#53890).
YAML dashboards (resource_mode: yaml): the resource list is read-only, add /smart_battery_pilot/smart-battery-pilot-card.js as a module resource manually (see README).
v1.2.4 — The documentation gets its own pages¶
Released 2026-10-11 · on GitHub
The documentation now has its own pages on the project website: https://cygnusb.github.io/ha-smart-battery-pilot/docs/
They are built from the same Markdown files you find in docs/ on GitHub, and every change to them is published automatically. On the website you get search, navigation, a table of contents for each page and a dark mode, in the same look as the project page. Links to missing pages or anchors are now caught in CI before they reach the site.
The "Documentation" link on the integration's page in Home Assistant, the README and the project page now lead to these pages.
This release does not change how the integration plans or controls your battery.
Upgrading¶
Nothing to do: update through HACS and restart Home Assistant. The Documentation link in Settings → Devices & services now opens the new pages.
Full changelog: https://github.com/cygnusb/ha-smart-battery-pilot/compare/v1.2.3...v1.2.4
v1.2.3 — The card says how the charger stands¶
Released 2026-10-11 · on GitHub
The card always shows whether evcc is being read. Until now, a card with evcc as its source looked exactly like one without whenever nothing was planned for the car, for example with the car full or unplugged. The status line now says how the charger stands:
EV: 100 % · no charging planned: a car is plugged in, at that SOC, and evcc has nothing to charge.EV: not plugged inEV: evcc not reachable
When evcc plans a session, the status line shows that session, as before.
The plan sensor carries the data as ev_status and ev_loadpoints: connected, charging, mode, SOC and limit for each loadpoint, without vehicle or loadpoint names. Both stay out of the recorder.
Upgrading¶
Nothing to configure. Reload the card (browser cache) after the update.
Full changelog: https://github.com/cygnusb/ha-smart-battery-pilot/compare/v1.2.2...v1.2.3
v1.2.2 — A project website and a clearer card¶
Released 2026-10-11 · on GitHub
A project website. Smart Battery Pilot now has a home at https://cygnusb.github.io/ha-smart-battery-pilot/. It shows how the planner reads a winter day, what the card looks like, and how to install the integration.
New screenshots. The README images show the whole card, uncropped, in light and dark to match your GitHub theme. Two new scenarios show planned evcc charging.
Card fixes.
- The "EV … kWh" label in a charging window now sits above the price curve and evcc's limit line instead of on them.
- The compact view's short price strip draws the limit line without a label; the legend names it.
Fix: Configuration sensor after a restart. The Configuration sensor showed "inactive" after a restart until the next re-plan, even when the pilot was switched on. It now shows the right state right away. Only the display was affected; the pilot steered as configured.
Docs. The energy-system reference is now a generic walk-through of a BYD + Fronius Gen24 setup.
Upgrading¶
Nothing to do. Reload the card (browser cache) to get the label changes.
Full changelog: https://github.com/cygnusb/ha-smart-battery-pilot/compare/v1.2.1...v1.2.2
v1.2.1 — evcc's limit where it fits, a grid import limit¶
Released 2026-10-11 · on GitHub
Follow-ups to the planned EV sessions of 1.2.0.
evcc's price limit is drawn only where it fits. evcc compares its smart cost limit with its own prices, which may leave out fees, VAT or a markup your plan's prices include. The card used to shift the limit onto your price axis in every case. Now it does so only when the two price series are a constant offset apart. Otherwise there is no line, because no single line would separate the slots evcc picks. A slot evcc picked for its price names evcc's own limit in the tooltip, and the line's label quotes the same figure.
An EV tile in the compact view. The next session's energy, time and reason, laid out 2×2 on narrow cards. The charging band is a bluer teal now, easier to tell from the consumption line.
Grid import limit (optional). Set your grid connection's limit, and the pilot's grid charge only uses what the house and a charging car leave below it. It never throttles either of them, and a cold battery's charge request stays within the limit too. The diagnostics count the charge slots the limit capped.
Upgrading¶
Nothing to do. The grid import limit is under Configure → EV charging & other controllers and is off by default. Reload the card (browser cache) to see the tile and the new colour.
Full changelog: https://github.com/cygnusb/ha-smart-battery-pilot/compare/v1.2.0...v1.2.1
v1.2.0 — Plan around the charging evcc has planned¶
Released 2026-10-11 · on GitHub
The plan now knows when the car will charge. With Planned EV sessions set to evcc, the pilot reads what evcc has already decided and plans around it, ahead of time instead of when the session starts:
- evcc's charge plan (target time): its slots, at the power evcc sized the plan with;
- fast mode: from now until the car reaches its limit;
- cheap-price charging: every slot at or below evcc's smart cost limit, until the car reaches its limit.
The energy is bounded by what the car still needs. A session running for any other reason, such as PV mode, counts for the slot in progress. The pilot only reads evcc's REST API, once a minute, and re-plans when the expected sessions change. If evcc is unreachable, it plans without sessions.
What the plan does with it depends on your EV policy:
- Allow: the car's grid energy is demand, and the planner decides whether the battery serves it.
- Block and External: those slots are held, with no discharge and no planned grid charge under External, so the SOC forecast and the savings estimate match what really happens.
- In every case: the car takes PV surplus before the battery, so the SOC forecast no longer runs high on days the car charges from the sun.
In the card. Charging windows run as a band across the panels. evcc's price limit appears as a dashed line, shifted onto your price basis. A thick SOC segment marks where the battery is held for the car, and the status line shows the next session. Without planned sessions the card looks exactly as before.
Also in this release:
- A hold set for a car is no longer counted as pilot savings.
- After a restart the pilot plans as soon as the SOC reads, instead of waiting up to 30 minutes on "no SOC".
Details in docs/ev-charging.md and docs/optimizer.md.
Upgrading¶
Nothing changes until you choose an EV source. In Configure → EV charging & other controllers, set Planned EV sessions to evcc and check the evcc address. It is pre-filled when evcc's Home Assistant integration is installed, and checked on save. Keep the wallbox power sensors set, so a house meter that includes the wallbox does not count the car twice. Reload the card (browser cache) to see the new layers.
Full changelog: https://github.com/cygnusb/ha-smart-battery-pilot/compare/v1.1.0...v1.2.0
v1.1.0 — EV charging, evcc and other controllers¶
Released 2026-10-11 · on GitHub
A wallbox changes two things for the pilot, and this release handles both in a new options section, EV charging & other controllers. It works with any wallbox Home Assistant can see. When evcc is installed, the section fills itself in from it.
The forecast learns the house without the car. A whole-house meter counts the wallbox too. One evening session outweighs a day of household load, and the model used to learn it as a daily habit at that hour. Configured wallbox power sensors (W, kW or energy meters) are now subtracted from each hour of the consumption history before training.
The battery stays out of EV charging. Choose what the house battery does during a session:
- Allow: nothing changes, as before.
- Block: while the wallboxes draw more than a threshold, the pilot blocks discharging. It reacts to the wallbox sensor at once, with hysteresis so ramping does not flip the inverter.
- External: another controller such as evcc decides while it is active.
No more tug of war with evcc. An external control entity, for example evcc's sensor.evcc_battery_mode, tells the pilot when another controller holds the battery. During that time the pilot calls no script. When the controller lets go, the pilot sends its current action once more. Before, whatever evcc left in the inverter stayed there until the plan changed. Only clearly active states count (on, hold, charge …), so a translated or unknown state never pauses the pilot.
The diagnostics show the new decisions ev_block and external_control, plus live wallbox and controller state.
The full guide, including the recommended evcc settings and two look-alike entities to avoid, is in docs/ev-charging.md.
Upgrading¶
Nothing changes until you open Configure → EV charging & other controllers. For evcc: enable the evcc integration's Battery mode [CODE] sensor (sensor.evcc_battery_mode, disabled by default). Then pick your loadpoints' …_charge_power sensors and the policy External. The cleaner forecast takes effect at the next daily retrain or after a restart.
Full changelog: https://github.com/cygnusb/ha-smart-battery-pilot/compare/v1.0.0...v1.1.0
v1.0.0 — Honest accounting: battery benefit and pilot savings¶
Released 2026-09-29 · on GitHub
1.0.0. Smart Battery Pilot now covers what it set out to do. It plans battery charging against dynamic prices and learns your consumption. It takes PV forecasts, cold battery temperatures and a backup reserve into account, and it explains every plan and decision in the logs and diagnostics. This release completes it with honest accounting.
The "actual savings" figure answered the wrong question. It is now split into what the battery is worth and what the pilot itself added.
What was wrong¶
A user had the pilot switched on and dry-run off, but every slot planned so far had been auto. The pilot had never charged from the grid or held the battery back. The actual savings sensor still showed 50 EUR.
The sensor credited every evening's ordinary self-consumption: discharge at the import price, minus PV charge at the feed-in tariff. That is roughly 3 EUR a day of value the inverter delivers with or without the pilot. The v0.6.4 guard only stopped the counting while the pilot was off. It could not tell a pilot that steers from one that merely runs.
Three figures, three questions¶
| Sensor | Question it answers |
|---|---|
| Battery benefit (net), previously Actual savings | What is the battery worth? Discharge at the import price, minus what the charge cost: grid charge at the import price, PV charge at the feed-in it displaced. |
| Battery benefit (gross), new | What did the battery displace? Discharge at the grid price only, with no PV opportunity cost deducted. |
| Pilot savings, new | What did the pilot add? |
How pilot savings are counted:
- Only energy the pilot moved differently from the inverter counts. That is grid charge in a
chargeslot, and demand anidleslot kept the battery from covering (estimated from the plan's forecast). - Each such amount is kept with the price it stood at. When the battery later discharges, the oldest amounts are used first. The pilot is credited with what that energy was worth then, minus its price and conversion losses.
- Everything else is plain self-consumption and earns the pilot nothing. A pilot that only ever sends
autoshows 0.00. - A trade that did not pay off lowers the figure.
- The energy not yet used shows as
pending_kwhon the sensor and in the diagnostics dump.
Upgrading¶
- Your existing total is kept. Battery benefit (net) keeps the old sensor's unique id, so its entity id, history and long-term statistics carry on; only the displayed name changes. Its value up to now is what your battery was worth while the pilot was switched on.
- Net and gross now count all the time, whether or not the pilot steers, because the battery's worth does not depend on it. With the pilot off or in dry-run, the inverter runs in its own mode, and energy is priced as in
auto. - Pilot savings start at 0.00 and grow only when the pilot actually charges from the grid or holds the battery back.
- Two new sensors appear. If you had a dashboard card or automation on Actual savings meaning "what the pilot saved", point it at Pilot savings instead.
Full changelog: https://github.com/cygnusb/ha-smart-battery-pilot/compare/v0.9.0...v1.0.0
v0.9.0 — A backup reserve for grid outages¶
Released 2026-09-29 · on GitHub
A backup reserve for grid outages: energy the pilot never spends on price arbitrage, brought back up by a deadline when the battery is below it, and handed to your inverter so it holds even while Home Assistant is down.
Why this was needed¶
min_soc has always been a planning assumption. The optimizer did not count on energy below it, but nothing enforced it on the hardware. In auto the inverter discharges down to its own reserve; the Fronius GEN24 example scripts wrote a fixed 5 % into MinRsvPct. So a user who set min_soc = 30 % still ended up at 5 %. And since the planner only charges from the grid when the price spread pays for it, nothing ever brought a battery back up to a reserve once it had dropped below.
Backup reserve (optional, off by default)¶
Configure → Backup reserve (options menu only):
| Setting | Default |
|---|---|
| Backup reserve | 0 % (off; at or below the minimum SOC it stays off) |
| Reserve entity | – (optional input_number/number/sensor in % that replaces the fixed value while it reads a number) |
| Refill deadline | 12 h |
| Block discharging at the reserve | off |
- Arbitrage never spends it. The planner now counts stored energy above the reserve instead of above the minimum SOC. No discharge slot and no export slot takes the SOC below it. That includes the hours while the reserve is still being refilled.
- It is refilled by the deadline. If the SOC is below the reserve, a refill pass runs before any arbitrage. It uses forecast PV surplus first, then the cheapest grid slots up to the deadline, whether or not the price spread pays for it.
- The refill is mandatory, so its cost is not counted against the estimated savings. The plan sensor reports it separately as
reserve_refill_kwhandreserve_refill_cost_eur. - If the deadline is too short for your charge power, the plan charges what fits and carries the warning
reserve_refill_incomplete, logged once per episode. - Cold-weather charging applies to refill slots too.
- The refill is mandatory, so its cost is not counted against the estimated savings. The plan sensor reports it separately as
- Your inverter gets it. Every control script now receives the variable
reserve_soc(whole percent: the reserve, or the minimum SOC while it is off).- When the reserve changes, for example because an automation raised the entity on a storm warning, the pilot runs the current action's script again, so the new value reaches the inverter straight away.
- The value last sent is persisted, so a restart only re-sends it when it actually changed.
- Scripts that ignore the variable keep working.
- A fallback for inverters without a reserve setting. Block discharging at the reserve makes the pilot run your block discharge script instead of auto when the SOC reaches the reserve, and release it 2 percentage points above. It reacts to SOC changes immediately, not at the next re-plan. It only works while Home Assistant runs, so it is a fallback and not a substitute.
Also fixed¶
nanandinfentity states are now treated as unreadable for every numeric entity the integration reads. Python'sfloat()accepts them. A reserve entity readingnanwould have been clamped to max SOC, and the refill would have charged the battery full from the grid.
Upgrading¶
Nothing changes until you set a reserve: with it off, plans are identical to v0.8.0.
If you want the inverter to hold the reserve (you should, for a real outage), have your auto and idle scripts write reserve_soc into the inverter's minimum reserve. For the Fronius GEN24 (see docs/examples/byd-fronius-gen24.md):
- service: modbus.write_register
data: { hub: gen24, slave: 1, address: 40350, value: "{{ ((reserve_soc | default(5)) * 100) | int }}" }
Your force-charge script keeps its 99 % (9900) on purpose. Note that once you adopt this, your inverter's reserve follows the minimum SOC whenever no backup reserve is set, instead of the fixed 5 % the old example used.
The actual savings sensors count what the meters measured, so energy bought for a refill shows up there, while the estimated savings exclude it. The two figures drift apart by the refill cost.
Full changelog: https://github.com/cygnusb/ha-smart-battery-pilot/compare/v0.8.0...v0.9.0
v0.8.0 — Cold-weather charging, and every decision explains itself¶
Released 2026-09-29 · on GitHub
Two things: the planner can now account for a cold battery, and every plan and every executor decision now explains itself — in the diagnostics dump even when debug logging was never switched on. This release also carries the logging work from v0.7.3, which was merged but not released on its own.
Cold-weather charging (optional, off by default)¶
A cold LFP battery takes far less than its nominal charge power: the BMS throttles below roughly 15 °C and nearly stops near 0 °C. The planner did not know that. On a cold winter night it booked two cheap hours of grid charging that in reality delivered a third of the energy, and the expensive morning found the battery half empty. The SOC forecast and the savings estimate were wrong in the same way.
Configure → Cold-weather charging (options menu only; the initial setup is unchanged):
| Setting | Default |
|---|---|
| Plan with reduced charge power in the cold | off |
| Battery temperature entity | – (cell/module temperature, not the outdoor sensor; °F is converted) |
| Charge power at 0 / 5 / 10 / 15 / 20 °C | 10 / 20 / 50 / 80 / 100 % |
The sliders are the planner's assumption, linear in between and clamped outside 0–20 °C. With it the planner spreads the charge over more (or longer) cheap slots.
The pilot never throttles the battery itself. A slot planned up to the cold limit still asks your charge script for the full power, and the BMS does the limiting. A slot that only needs part of it asks for exactly that part, as it always did. A cold BMS caps the current; it does not take a share of the request. So asking a partial slot for more would overshoot the plan, and near the top, your max SOC.
It learns. With a battery charge energy meter configured, every forced charge slot the pilot really runs is measured against what it asked for. Per 5 °C band, once six slots have shown the battery taking clearly less than requested, the measured value replaces the slider for that band. These readings are ignored:
- slots near max SOC, where the battery tapers because it is full
- slots under 10 minutes
- small requests
- readings below meter resolution
- anything in dry-run, where no charge happens
The configuration sensor shows the current charge_factor, whether it comes from the curve or was learned, and how many bands are learned. The diagnostics dump lists every band.
Every decision explains itself¶
A support report usually arrives as "it charged at 3 am, why?", with a diagnostics download attached and debug logging never switched on. Until now, neither could answer it:
- the optimizer did not log at all,
- the executor skipped script calls silently,
- an invalid plan (SOC or price entity unavailable) handed the battery back to auto without a word.
Diagnostics now carry what the planner was fed (SOC, horizon, price range, battery limits, tunables, charge factor) and the executor's last 50 decisions with outcome and reason. The outcomes are: applied, unchanged, dry run, disabled, no live plan (with the reason), script failed, failed and fell back to auto. Identical decisions in a row collapse into one entry with a repeat count.
Debug logging (device page → Enable debug logging) now shows:
- every planning run's inputs,
- how the price adapter parsed the entity,
- every charge/discharge pairing the optimizer made,
- every candidate it rejected, with the reason and the arithmetic:
pair charge Thu 00:00 @ 0.1000 -> discharge Thu 05:00 @ 0.5000: 1.581 kWh stored Thu 02:00 @ 0.1000: spread not reached - cheapest open charge slot Thu 01:00 @ 0.1000 costs 0.2111 with losses + spread Plan result: auto=5, charge=2, idle=1; grid charge 8.333 kWh, battery delivers 7.500 kWh, cost 1.2833 EUR vs. baseline 3.6000 EUR (saves 2.3167)
A no-plan state is now logged. When planning stops working, the log shows one warning per new reason. While the reason persists, the messages drop to debug, and an info line follows when planning works again.
Fixed: the options dialog on Home Assistant 2024.11¶
On Home Assistant 2024.11, the oldest core this integration supports, Configure crashed on its first step with AttributeError: 'SBPOptionsFlow' object has no attribute 'config_entry'. That core never sets config_entry on an options flow, and newer cores refuse any assignment to it, so the flow now receives its entry through the constructor. The unit tests had set the attribute themselves, which is why this went unnoticed. It was found by a new test that walks the options dialog in a real Home Assistant on every supported core.
Upgrading¶
Nothing to do. Cold-weather charging is off until you switch it on, and with it off, plans are identical to v0.7.2. The existing store gains one key (charge_rate); older stores load unchanged. No entity changes. The configuration sensor gains a few attributes.
The only visible difference without the new option: if your SOC or price entity drops out, you now get one warning in the log where there used to be silence.
Full changelog: https://github.com/cygnusb/ha-smart-battery-pilot/compare/v0.7.2...v0.8.0
v0.7.2 — The card's tooltip and curve labels are back¶
Released 2026-09-14 · on GitHub
Two fixes for v0.7.1, both reported from a live dashboard.
The tooltip was gone¶
The chart drew correctly at every size, but hovering it did nothing.
The chrome measurement that v0.7.1's percentage height needs was placed at the top of the method that also attaches the pointer listeners, and returned early when the card had no layout to measure:
const cardH = this.getBoundingClientRect().height;
const chartH = svg.getBoundingClientRect().height;
if (cardH <= 0 || chartH <= 0) return; // also skips the listeners below
...
svg.addEventListener("pointermove", onMove); // never runs
A card first drawn without a layout — on a dashboard tab that is not in front, inside a collapsed section, or before the first layout pass — therefore never got a pointermove handler at all. And because a later resize only redraws when the width changes, the tooltip stayed dead for the rest of that card's life. Nothing logged, nothing looked wrong.
The measurement is now a method of its own that runs last: nothing depends on it, so nothing may be skipped by it. The pointer mapping also refuses a zero-width box, which was the other way a card with no layout produced NaN instead of a slot.
A card that renders correctly is not evidence that it is wired up, and the string-rendering test shims could never have seen this. The new test reports zero for every box and asserts on the handlers themselves — against the v0.7.1 source it fails with assert 'svg:pointermove' in ['.viewtog:click'].
The numbers printed on the curves were unreadable¶
The peak price, the SOC endpoint and the balance view's two direction labels sat 6 px above their own marker dot. At a 10 px font that is close enough for the dot to merge into the digits, with the curve running through them as well — it reads as a rendering artifact rather than as a label.
They now clear the dot by 9 px and carry a halo: paint-order: stroke fill draws a stroke in the card's own background colour behind the fill, knocking whatever passes underneath out of the glyphs instead of letting it cross them. The stroke is var(--card-background-color), so it follows light and dark themes.
Upgrading¶
Nothing to do. No configuration changes, no entity changes, no stored data touched. If the tooltip was dead for you on v0.7.1, it comes back after the update — clear the browser cache if the card looks unchanged.
Full changelog: https://github.com/cygnusb/ha-smart-battery-pilot/compare/v0.7.1...v0.7.2
v0.7.1 — The card draws at any size and takes a height¶
Released 2026-09-14 · on GitHub
The dashboard card no longer has a preferred width. It draws at whatever size the dashboard gives it, and it takes a height:.
"Bad resolution" was zoom¶
The card rendered its whole chart into a fixed viewBox="0 0 480 H" scaled to 100 % width. That makes the chart an image, not a layout: stretch the card to 1400 px and everything is magnified 2.9x — 9 px axis labels become 26 px, 2 px strokes become 6 px, the three-hour grid drifts apart into an empty axis. It is not blur, it is zoom, and no amount of care in the stylesheet could help, because the stylesheet's pixels were not the screen's pixels.
The same constraint worked the other way in a narrow phone column, where the labels shrank below legibility. And because preserveAspectRatio ties the height to the width, the height could not be chosen at all — a 1400 px card was 817 px tall whether or not anyone wanted that.
The card now measures the box the dashboard gives it and puts that pixel width into the viewBox, so the scale factor is exactly 1 and every font size and stroke width in the stylesheet means what it says. A ResizeObserver redraws on resize.
LAYOUTS loses its hardcoded panel y/h. The fixed chrome — action ribbon, per-panel caption, hour-label strip — costs a constant number of pixels at any size; the remainder is shared out by weight with a 30 px floor per panel. So extra height buys taller curves, and extra width buys a denser hour grid (chosen from 1/2/3/6/12 h by the room the plot actually has) and more y-axis ticks (about one per 29 px of panel), instead of magnifying what is already there.
At 480 px the card reproduces its previous geometry exactly — same 60/58/58 panels, same three-hour grid — so nothing changes for anyone who was already happy with it.
height:¶
type: custom:smart-battery-pilot-card
height: auto # auto (default) | <pixels> | <percent>
| value | effect |
|---|---|
omitted, or auto |
Grows gently with width and then stops: roughly 236–440 px in tracks and balance, 158–300 px in compact. |
height: 320 |
Exactly that many pixels for the chart. "320px" works too. |
height: "100%" |
Fills the box the dashboard offers, minus the status line and legend. Useful where that box has a height of its own — a sections-view card with grid_options.rows set, or a card_mod wrapper; elsewhere it falls back to auto. |
Smaller things¶
- Edge labels are pinned inside the plot instead of running off it — the closing
Thu, 1/15used to lose half its characters — and the weekday is dropped when a multi-day plan crowds the day separators together. - The compact view's three figures are set smaller below 420 px, so
01:00 PMstays on one line instead of wrapping across two and pushing the chart down. getGridOptions()is declared, so the card can be dragged narrower in a sections view. Nothing is requested that would re-flow an existing dashboard: to use the full width of acolumn_span: 2or wider section, setgrid_options: {columns: full}on the card yourself.
Upgrading¶
Nothing to do. No configuration changes, no entity changes, no stored data touched. Clear the browser cache if the card looks unchanged after the update.
Full changelog: https://github.com/cygnusb/ha-smart-battery-pilot/compare/v0.7.0...v0.7.1
v0.7.0 — A rewritten card, and a forecast that lets go of an old sensor¶
Released 2026-09-10 · on GitHub
A rewritten dashboard card, and a fix for a consumption forecast that could quietly keep predicting the wrong household.
The consumption model could get stuck on a sensor you no longer use¶
ConsumptionForecaster.train() only ever replaced the ridge regression's weights. The branch that fits it is guarded by a minimum sample count and had no else, so once the guard stopped firing the old weights survived — and prediction prefers weights over the hourly profile whenever they exist.
Changing the configured consumption sensor is exactly that case. The new entity's long-term statistics start at zero rows: the hourly profile is correctly refitted from the handful of new samples, while the regression fitted to the previous sensor keeps answering every prediction. Nothing logged, nothing failed.
On the instance this was found on, the stale model forecast 9.0 kWh a day where the refitted profile said 23.3 — and its negative intercept ran into the zero clamp from 16:00 on, so demand was exactly 0.00 kWh through the 0.88 EUR/kWh evening hours. With no demand there, the planner assigned no discharge at all and the SOC forecast sat flat on max SOC for the whole horizon: 0.24 kWh of planned discharge instead of 3.09.
How to tell if this hit you: on sensor.…_consumption_forecast_24h (or the plan status sensor), model_type: ridge_regression next to a training_samples count below 336 is impossible and is the tell. After updating, a restart is enough — stored weights whose own sample count cannot justify them are now dropped on load, instead of waiting up to 24 h for the next training run. The same guard covers a recorder purge, a shortened training window, and a filter that starts dropping samples.
The card: one scale per drawing surface¶
The old card drew price in EUR/kWh, SOC in percent and energy in kWh on a single plot. Price and SOC got an axis each; PV and consumption got neither — they shared the lower 45 % of the price plot, normalized to the PV maximum. On a day with 64 kWh of PV against a 12 kWh household, a perfectly correct consumption forecast is pressed onto the baseline and reads as broken.
The SOC line had the opposite problem: 0–100 % against a battery that lives between 10 % and 95 % spends a third of the panel on states it can never reach, and a full battery saturates along the top edge — exactly where the interesting question is how deep the evening takes it.
Everything now gets its own panel, its own scale and its own axis, and the planned actions move out of the background into a labeled band above them. (They used to be four translucent washes behind the curves, two of which differed by a few percent of opacity.) The most expensive slot and the SOC endpoint carry the only two numbers printed on the curves.
Three views¶
type: custom:smart-battery-pilot-card
view: tracks # tracks | balance | compact
tracks(default) — price, PV against consumption, SOC, stacked on one time axis.balance— same three panels, but the middle one shows PV minus consumption as an area diverging around zero. That is the quantity the optimizer actually plans against, and it answers "why is the battery doing nothing" without arithmetic: while the surplus stands, there is nothing to spend.compact— status, three figures (SOC now → at horizon end, next change, planned discharge) and a smaller SOC + price chart, at roughly two thirds the height. For overview dashboards and phones.
An unrecognised value draws the default rather than an empty card. The button at the right of the legend flips tracks/balance without a dashboard edit; the config wins again as soon as it is re-applied. See docs/configuration.md.
Nothing to do on update — an existing card config keeps working and gets the new default view.
Also¶
- The plan sensor publishes
min_socandmax_soc, which is what lets the card draw the SOC track against the battery's real operating window. A card talking to an older backend falls back to the data's own range. - Six new card strings across all ten languages.
- 271 → 277 tests. The card's new tests render the plan in every view and check what came out — including the invariant behind a bug the rewrite hit on the way: a filled SVG area whose steps begin with
Mopens a second subpath, and the closingZthen draws a diagonal across the panel instead of running along the baseline. - README and card screenshots regenerated from the real optimizer, with one shot per view.
Full changelog: https://github.com/cygnusb/ha-smart-battery-pilot/compare/v0.6.7...v0.7.0
v0.6.7 — Cent prices, export without a script and a stale card¶
Released 2026-09-05 · on GitHub
Four fixes from a full review of the integration.
Cent price units were read as euros¶
Two independent paths read a price in cents as EUR/kWh — prices a hundred times too large, which makes every hour look like a giant arbitrage opportunity and force-charges the battery from the grid.
- The unit check matched three exact spellings, so
Cent/kWh,cents/kWhandc€/kWhfell through as euros. It now matches a prefix of the unit's numerator, which also keeps a currency that merely begins with ac(CZK, CHF, CAD) from being mistaken for cents. - The ENTSO-E adapter did not consult the unit at all. A sensor configured for cents yielded
5.0 EUR/kWhwhere Nordpool yielded0.05. The plausibility guard turned that into a hardimplausible_priceson any day with a slot above 10 ct, and let a curve peaking below it through to be planned on.
aWATTar is deliberately unchanged: there marketprice is raw API data in EUR/MWh while the entity's unit describes its state, so scaling by it would make a correct sensor ten times too expensive.
If your price sensor reports cents, check sensor.…_current_price after updating — it should now match your tariff.
Export mode could be selected without an export script¶
The planner kept scheduling export slots, the executor refused them and fell back to auto mode at every slot boundary — the setting looked active while doing nothing but filling the log. The options flow now refuses export mode without a force-discharge script, and refuses clearing that script while export mode is active.
The dashboard card went stale between slot boundaries¶
The plan sensor's state and attributes are identical on both sides of a slot boundary, so Home Assistant fires no state change and the card kept showing the previous slot's action chip and "now" marker until the next replan — up to 30 minutes into a 15-minute slot.
Documentation¶
- What an export slot assumes of your export script: the
power_wit receives is the household load share plus the exported share, and the cost model credits both. - What curtailed PV is worth: it is valued at zero even with a feed-in tariff configured. This changes no decision, it only makes
estimated_savingsslightly optimistic on sunny days.
Under the hood¶
229 → 269 tests. The Lovelace card gains behavioural tests of its own — it is loaded into a DOM shim under node, which no previous test could do.
v0.6.6 — Ten languages¶
Released 2026-09-05 · on GitHub
The setup flow, the entity states and the dashboard card now speak ten languages: Dutch, Swedish, Norwegian (Bokmål), Danish, Finnish, Estonian, Latvian and Lithuanian join English and German.
The picks follow the supported price sources rather than raw population. Nordpool covers the Nordics and the Baltics, Tibber adds the Netherlands, and the Netherlands, Finland and Norway have among the highest dynamic-contract shares in Europe — exactly the households this integration is for. Austria and Switzerland come along with German. Home Assistant falls back to English everywhere else, as before.
Translations lived in two places, and only one was checked¶
The Lovelace card is plain JavaScript served to the browser. It cannot read the integration's translation files, so it carries its own string table — and nothing tied the two together. Adding nl.json on its own would have produced a Dutch setup flow and an English card, with a completely green test suite. test_translations.py also listed ["en", "de"] by hand, so a new file would not have been examined at all.
Languages are now discovered from the directory, and four checks were added: key parity against English, nothing left untranslated, the card covering the same languages with the same keys, and the card's {entity} / {error} / {action} / {time} placeholders surviving translation. That last one matters because a dropped placeholder produces a message with a hole in it, in one language only.
Adding a language is now copy, translate, add a card block. The tests say what is missing.
Norwegian resolved to English¶
Home Assistant labels the language nb, but browsers report no. The card fell back to English with the Norwegian strings sitting right there. It now aliases no and nn to nb.
A currency assumption, flagged rather than fixed¶
The recommended price spread of 0.10–0.25 is calibrated in euros, which is off by roughly a factor of ten in SEK, NOK and DKK — a Swedish household entering 0.20 would get a threshold that never blocks anything.
The Swedish, Norwegian and Danish help texts therefore say "per kWh in your price sensor's currency" instead of naming EUR. The defaults themselves are untouched: changing them is a real decision about behaviour and deserves its own release, not a quiet edit buried in a translation change.
On the quality of these translations¶
Everything except English and German was written without a native speaker's review, and the vocabulary here is dense with inverter jargon — state of charge, roundtrip efficiency, feed-in tariff, price spread. The terminology follows what Home Assistant Core and inverter vendors use in each market, but that is not the same as a native speaker having read it.
Corrections are very welcome, and cheap to make. The README names both files a language lives in and points at the tests, so a correction PR is safe to review and merge. If something reads wrong in your language, please open one.
Upgrade note¶
No configuration change required. Home Assistant picks the language from your user profile; the card follows Home Assistant and falls back to the browser.
v0.6.5 — Held-back energy no longer displaces free PV¶
Released 2026-09-05 · on GitHub
The optimizer could plan a day that was worse than leaving the inverter alone — on ordinary sunny summer days, not adversarial price curves. One fix, in the place the plan is actually decided.
Reservations ignored that the PV they displace gets thrown away¶
Energy held back for a later peak occupies capacity that midday PV surplus would otherwise fill. Once the battery reaches max SOC the surplus is simply discarded, so the reservation adds nothing to the day — it replaces free PV with energy the household paid for.
Nothing in the planner knew this. assign_discharge() sourced already-stored energy with min(levels[d:]), which assumes a withdrawal now lowers every later SOC level by the same amount. It does not: a slot that curtails PV absorbs the shortfall up to what it was about to waste.
On a 10 kWh battery at 60 %, with a morning peak, a PV bell over 09–16 h and an evening peak, the plan held 1.02 kWh back overnight, idled hours 00–04, and then sat pinned at 95 % SOC for five hours of curtailed sun. It delivered 1.02 kWh less to the house than plain self-consumption and reported estimated_savings_eur = -0.10. The savings figure was not wrong — the cost model correctly reported the loss. The assignment was.
timeline() now reports the PV curtailed in each slot, and a withdrawal is capped at levels[j] + Σ curtailed[d..j] over every later slot j. The old expression is the no-curtailment case of that. Same scenario after: +0.17 EUR, and the battery delivers 12.81 kWh — exactly what an untouched inverter would.
Note what did not change: hours 02–04 stay idle. That reservation is real — the battery genuinely reaches min SOC at 08 h, before any PV arrives, so there is no surplus to release it against. The fix releases the energy that curtailment makes free, and only that.
idle is now a consequence of the plan, not a label applied beside it¶
The rule that produced the visible idle marker was a separate heuristic — block discharge whenever any pricier future slot discharges — with no notion of scarcity of its own. It is now derived from the assignment: a slot is idle exactly when it has demand, the battery holds energy, and the plan did not spend it there.
This also closes a quieter gap. Under the old rule a slot could be labelled auto while holding energy the plan had not assigned — so the inverter would discharge it, while the cost model booked zero for that slot. The plan and the number it reports now describe the same day.
A plan that loses money is never shipped¶
Behind both fixes: if the finished plan's estimated savings still come out negative, it is discarded and the all-auto plan runs instead. Doing nothing is always available and is the very baseline the figure is measured against. It surfaces as a plan_worse_than_baseline warning in sensor.…_plan's attributes and a log line pointing at the issue tracker.
It is a guard, not the fix. Across 20 000 random scenarios it never fires — the assignment gets it right on its own — and the new property test asserts exactly that, so the fallback cannot quietly paper over a future regression.
Tests¶
187 (was 184). All three new tests were verified to fail against v0.6.4:
- the reproducing summer day, asserting the plan beats doing nothing and delivers at least as much to the house as an untouched inverter;
- a 2000-scenario property sweep over battery sizes, efficiencies, spreads and both discharge modes;
- the fallback's own branch, which nothing reaches naturally any more.
Re-fuzzed over 20 000 scenarios: no negative plan, and SOC stays inside the min/max window with per-slot power limits respected throughout.
Upgrade note¶
No configuration change required. Plans are recomputed on the next cycle. If you have been watching sensor.…_estimated_savings go negative on sunny days, that is the symptom this release removes.
v0.6.4 — Cent prices, savings while off and a hanging script¶
Released 2026-09-05 · on GitHub
Findings from a full review pass over the whole integration. Two of them produce numbers that are simply wrong, one can wedge Home Assistant.
Cent prices were read as euros when a sensor degraded¶
A Nordpool sensor whose raw_today is momentarily empty — right after a restart, or when the upstream API hiccups — falls through to the generic hourly_arrays adapter, and that one read the plain today list as EUR/kWh. Only the Nordpool adapter knew about price_in_cents and öre.
raw_today = [] -> adapter hourly_arrays, first price 30.0 EUR/kWh
raw_today = [...] -> adapter nordpool, first price 0.30 EUR/kWh
A plan built on prices a hundred times too large makes every hour look like a huge arbitrage opportunity, and the planner grid-charges accordingly. This affects any Nordpool installation configured in cents or öre — Nordic setups in particular. The unit detection now lives in price_adapters/base.py and every adapter that reads a bare number uses it.
As a second line of defence the coordinator now refuses a price curve whose worst value exceeds 10 EUR/kWh instead of planning on it. The entry stays loaded so sensor.…_plan_status can report the new implausible_prices state and the diagnostics dump can say what went wrong, while the executor fails safe to auto mode. A real 0.95 EUR/kWh spike still plans normally.
Actual savings accrued while the integration was switched off¶
sensor.…_actual_savings_eur accumulated regardless of whether the integration was steering. With the master switch off and dry-run on — the state every installation ships in — a battery cycling under the inverter's own control reported 4.19 EUR/day of "savings" from an integration that had not called a single script.
Two compounding causes: nothing gated the accounting on the steering state, and charge in a mode not yet on record (last_applied is None) was valued at the feed-in tariff rather than the import price — 0.79 EUR instead of the 1.98 EUR that energy actually cost, over that same day.
Accounting now runs only while the pilot steers. The meter baselines keep advancing while it is paused, so switching on does not settle everything that moved in the meantime as one enormous delta.
What the figure is, now stated plainly in the docs: the measured value of the battery's energy flows while the pilot is in charge. It is not a counterfactual against the inverter's own behaviour — a true do-nothing baseline cannot be reconstructed from meter deltas. estimated_savings is the one with that baseline, and the two deliberately answer different questions.
A hanging control script wedged reloads and shutdown¶
Scripts are awaited with blocking: true while the executor holds its lock, and config entry unload waits for that same lock. An unbounded retry loop inside a script — which the Fronius Gen24 example actively recommends for the Modbus writes — hung integration reloads and Home Assistant's shutdown indefinitely. The call now times out after 120 s, the action counts as failed, and the battery is handed back to auto mode.
Also fixed¶
binary_sensor.…_plan_probleminherited its availability from the coordinator, so it wentunavailablein exactly the situation it exists to report. An automation waiting foronwould never have fired.- The options flow now refuses an export mode that could never fire (feed-in tariff at or below the minimum spread) instead of letting the setting look active while doing nothing.
- An energy meter with an unexpected unit logged the same warning twice an hour, forever. Now once per entity.
PlanSlot.charge_power_wcarries the discharge power in export slots. The field is nowpower_w; the published attribute keepscharge_power_was an alias, so existing templates and dashboard cards keep working unchanged.
Tests¶
The review found two areas essentially unprotected, and both are now covered:
- The 424-line config flow had almost no coverage.
tests/test_config_flow.pyadds 19 tests, including the coupling where the options flow writesdatakeys intooptions— the reason the unique-id repair on startup exists at all. - The fast suite runs against hand-written stubs of Home Assistant, so by construction it cannot notice core changing an API underneath us.
tests_ha/adds five smoke tests against a real Home Assistant viapytest-homeassistant-custom-component, with its own CI job. Verified green on homeassistant 2026.2.3.
184 tests (was 152) plus 5 against real Home Assistant. Every new regression test was verified to fail against the unfixed code, and the negative controls — a normal tariff, a record price spike, a reachable export mode, plain EUR/kWh arrays — to pass both before and after. The optimizer was re-fuzzed over 4000 random scenarios: no SOC or power-limit violations.
Upgrade note¶
No configuration change is required. If sensor.…_actual_savings_eur has been running while the master switch was off or dry-run was on, its total carries energy the integration did not move. The totals are not corrected retroactively — reset them from the entity's settings if the number matters to you.
v0.6.3 — The master switch comes back, and no slot charges and discharges at once¶
Released 2026-09-05 · on GitHub
Seven findings from a full review pass. Two of them could silently disable the integration or make it plan the exact opposite of what it should.
The executor stayed dead after the master switch went off and on¶
async_stop() raises an internal stop flag that turns every queued apply and every slot-boundary timer into a no-op. async_start() never cleared it — and the master switch calls exactly that pair when you turn it off. Switching off once left the integration silent until the next reload or Home Assistant restart, with the switch showing "on", no scripts running, and no boundary timer armed.
If you have ever toggled switch.…_enabled off and back on, this is why nothing happened afterwards.
The optimizer planned charge and a discharge in the same slot¶
The greedy pairing handed a cheap slot to a pricier one as a charge candidate after that same slot had already been credited with covering its own demand. The plan then asked the inverter to force-charge while the cost model booked a discharge that could never happen.
In a realistic winter curve (export mode, no feed-in tariff, spread 0.05) three consecutive hours came out as charge on a battery already at 95 %: nothing is charged, self-consumption is blocked, and the house draws its full load from the grid at 0.32 EUR/kWh — while the plan claims the battery delivered it.
Across 4000 random scenarios this affected 52 % of export-mode plans and 0.8 % of self-consumption plans; in the worst case 20.5 of 92.9 planned discharge kWh were credited to slots the executor would force-charge. estimated_savings_eur and battery_discharge_kwh were inflated to match. Twenty thousand scenarios now produce zero such slots.
Exported energy was credited at the import price¶
In export mode the accumulated savings booked every discharged kWh at the import price — market price plus grid fees, levies and taxes. Energy sent to the grid earns the feed-in tariff, or the raw market price when the tariff is 0. For a German household that is a factor of three to four. Discharge is now valued by where the energy actually went.
Also fixed¶
- The two accumulated-savings sensors dropped to
unknownon every failed update, tearing holes in their long-term statistics whenever the price entity blinked. They are running totals and now keep their value. - A consumption entity without long-term statistics made every 30-minute update re-query weeks of recorder history and log the same warning, forever. A fruitless run now backs off for six hours.
sensor.…_current_actiondeclaredunknownas an enum option — a state Home Assistant reserves for itself, indistinguishable from a genuinely unknown state, and its translation was never shown.- The unique id did not follow a SOC entity changed in the options flow, leaving the "one battery, one plan" duplicate guard pointing at the old inverter.
- A trigger arriving while an apply waited on a script could still queue a second run and call the charge script twice.
Upgrade note¶
sensor.…_current_action now reports no_plan instead of unknown when no slot covers the current time. Automations comparing against unknown need adjusting — though that value was never distinguishable from Home Assistant's own unknown state anyway.
152 tests (was 136). Every new test was verified to fail against the unfixed code.
v0.6.2 — Forecast and savings stop trusting impossible data¶
Released 2026-09-04 · on GitHub
Two independent problems, both about trusting data that was not what it claimed to be.
The consumption forecast trained on impossible readings¶
A meter that loses its source often reports a constant 0 instead of going unavailable. Fronius P_Load does this for whole hours while the rest of the inverter data keeps flowing, and the recorder averages those zeroes into the long-term statistics as genuine measurements. train() only rejected negatives, so the model learned an afternoon hole that is not there.
On a real 90-day history (141 of 2158 hours pinned at 0 W, every one of them in daylight) the forecast under-estimated consumption by 12.5 % overall and 28.6 % between 12:00 and 20:00 — exactly the window where the planner decides whether to hold SOC for the evening peak. Implausible samples are now dropped, with a threshold relative to the household's own median rather than a fixed wattage.
If your consumption sensor was affected, the forecast corrects itself over the next fortnight as the model retrains.
Savings accounting priced energy with data it could not have¶
_price_for_interval averaged over plan.slots, but past slots are dropped from the plan, so the "time-weighted mean" always collapsed to the current price. _charge_unit_price read the momentary last_applied, which the executor has already advanced by the time the meter is read, so grid charging was billed at the feed-in tariff — roughly 0.96 EUR of phantom savings per charge slot on an hourly grid, compounding daily.
Both values are now recorded as they happen and integrated over.
Also fixed¶
- The slot-boundary timer called the charge/export script twice per boundary.
async_stopdid not stop already-queued applies, which could re-arm the timer and re-apply a forced mode behind an unloaded entry.- Actual-savings accumulators kept filling with only one energy meter configured, poisoning the totals once the second was added.
- Entries created before 0.6 were not covered by the "one battery, one plan" duplicate guard.
136 tests (was 124). Every new test was verified to fail against the unfixed code.
v0.6.1 — Export follows the planned power, no discharging into negative prices¶
Released 2026-09-03 · on GitHub
Second full review of v0.6.0. Four correctness holes and two savings-accounting gaps.
Upgrade notes¶
If you use export mode, update the force-discharge script so it honours power_w. The shipped Fronius example now writes OutWRte from that value instead of dumping at 100 %. Charge scripts already received power_w; export scripts do too.
Actual-savings EUR now prices PV charging (auto/idle) at the feed-in tariff, not the import price. Grid-charge slots are unchanged. Wh meters are converted to kWh automatically.
No configuration migration is needed.
Fixed¶
- Saving options dropped
smart_battery_pilot.replan. On a single-entry install, options reload unloads the last entry and never re-runsasync_setup, so the service vanished until a Home Assistant restart. Automations calling it failed silently. Unload now keeps the service during reload, andasync_setup_entryre-registers it if it is missing. - A crash could still leave the inverter force-charging.
last_appliedwas only delay-saved (60 s). Power loss in that window left the store atauto, so the nextConfigEntryNotReady(tariff entity not up yet) did not release the battery._remembernow persists immediately. - The optimizer discharged stored energy into negative prices. Export and grid-charge pairing already refused that; self-consumption did not. On a fully negative day the battery was emptied for a loss. Negative-price demand slots now idle so the house takes the paid import and SOC is kept for a later positive hour. Charging at negative prices to serve later peaks is unchanged.
exportran with no power setpoint. Charge passedpower_w; export passed{}. The Fronius example then wrote OutWRte=10000 for the whole slot, even when the plan only allocated a slice. Export now gets a planned discharge power, consecutive export slots re-apply when that power changes, and the example honourspower_w.
Improved¶
- Actual savings averages slot prices over the coordinator interval instead of attributing a 30-minute meter delta to a single 15-minute slot
- Charge/discharge energy meters in Wh are converted to kWh before accumulating
- Docs, translations, and the Fronius example document
power_won export
Tests: 112 → 124.
v0.6.0 — 19 fixes, and savings measured against doing nothing¶
Released 2026-09-03 · on GitHub
Full review of the integration turned up 19 issues; this release fixes all of them. Three affected day-to-day operation, and all three sat on the time axis.
Upgrade notes¶
Two changes are visible in your dashboard:
estimated_savingsis now measured against doing nothing (plain self consumption) rather than against buying everything from the grid. A plan that changes nothing reports0.00. Expect a smaller — and honest — number than before.- The actual-savings sensors need both energy meters. Configure battery charge and discharge energy, or both sensors stay unavailable. With only one meter the EUR sensor previously published a permanent
0.00.
No configuration migration is needed.
Fixed¶
- Entities did not refresh at slot boundaries, only on the 30-minute coordinator tick. With 15-minute price slots a whole
chargeslot could pass withoutsensor.…_current_actionever showing it, so automations built on it fired late or not at all. Sensors, card and inverter now change in the same moment. - Hourly price arrays broke on both clock-change days. A 23-value spring day matched no adapter at all and left the plan invalid for the whole day; a 25-value autumn day produced 58-minute slots with prices attached to the wrong hours, so the inverter switched at the wrong times. Slot length now derives from the real length of the local day.
- A restart could leave the battery in a forced mode indefinitely. The last applied action lived only in memory, and setup aborts before the executor even starts when the tariff integration is still loading. The action is persisted now, and an aborted setup releases the battery back to auto mode.
| before | after | |
|---|---|---|
| 23-value day (spring) | no adapter, plan invalid all day | 23 slots at 1.000 h |
| 25-value day (autumn) | 15 slots at 0.967 h, prices shifted | 25 slots at 1.000 h |
| savings on a do-nothing plan | 3.08 EUR | 0.00 EUR |
| sensor lag at a slot change | up to 30 min | none |
The three DST symptoms shared one root cause: Python treats subtraction and comparison of two timezone-aware datetimes that share the same tzinfo object as naive, and ZoneInfo caches its instances — so the day-length calculation returned 24 h on every day of the year. The same trap sat in the slot duration, in the dedup key (which merged the two 02:00 slots of an autumn day, despite their different prices) and in every slot lookup.
Also fixed¶
- Concurrent triggers could leave the executor's recorded state describing a mode the inverter was not in (
asyncio.Lock) estimated_savingsno longer carries themonetarydevice class, which Home Assistant only accepts together withTOTALand logged an error about- Timezone-less timestamps from template price sensors no longer fail with a generic parse error
- Saving options no longer bounces the inverter out of and back into its mode
- Removing the integration cleans up its stored model and savings totals
- A second config entry on the same battery is blocked
Improved¶
- English UI: 51 field help texts that only German users saw are translated; a test now keeps every language in lockstep with the config flow's schemas
- PV forecast follows
sun.suninstead of a fixed 06:00–21:00 window — a December day is modelled as the ~8 hours it really is - Dashboard card renders in Home Assistant's timezone rather than the browser's, and no longer scans every state in the system on each update
- Diagnostics download on the device page: configuration, matched price adapter, forecast model and the first day of the plan
- The plan's slot list is kept out of the recorder database
ruffadded to CI
Tests grew from 70 to 112, covering both clock-change days, restart and concurrency behaviour, the savings baseline, and translation parity.
v0.5.1 — The documentation in English¶
Released 2026-09-02 · on GitHub
Documentation follow-up to v0.5.0. No behaviour change.
Docs¶
- House energy notes are English now (
HA_Energie_Dokumentation.md→docs/energy-system.md) - Original
spec.mdbrief translated to English - README entity table includes
current_price,plan_status,configuration, and the actual-savings sensors - Going-live order is enable first, then turn dry-run off
- Configuration docs cover energy meters, Nordpool cents, live PV, and dry-run restoring auto
- Fronius example: idle verification (StorCtl_Mod=0, MinRsvPct=5%) plus optional PV/meter fields
UI strings in translations/de.json stay German on purpose. Script aliases in the Fronius example (SBP: Laden erzwingen, …) are the names from that Home Assistant install.
v0.5.0 — Dry-run, stale prices and savings meters handled correctly¶
Released 2026-09-02 · on GitHub
Review follow-up: control, savings, and price-adapter correctness, plus the ha-netze-bw README presentation.
Fixed¶
- Dry-run actually stops and starts inverter scripts.
_last_appliedis only set after a real script call. Leaving dry-run applies the current slot; entering dry-run (or unloading while live) restores auto. A dry-run-only session no longer fires auto on unload. - Failed price refresh no longer keeps a stale plan in control. After the first successful update, unavailable prices return
valid=Falseso the executor fails safe to auto.last_update_success=Falseis treated the same way. smart_battery_pilot.replansurvives options reload. The service is registered once fromasync_setupand refreshes every loaded entry's coordinator.- Actual savings no longer explode on a meter glitch. Unavailable energy entities leave the baseline unchanged. EUR savings require both charge and discharge meters; a discharge-only setup no longer counts full discharge value as profit.
- Actual savings use the current slot price, not the unweighted mean of all remaining charge/auto slots in the horizon.
- PV surplus and grid charge share the inverter charge cap, so negative-price sunny hours cannot store beyond max charge power.
- Nordpool cents/öre (
price_in_centsor ct/cEUR/öre units) are divided by 100 before the spread check. - Import offset is not treated as export revenue. Grid export is valued at the feed-in tariff, or at the raw market price when feed-in is 0.
- Tibber-style hourly arrays are laid out in civil time, so spring-forward days no longer produce two slots at the same instant.
- Fronius idle example writes StorCtl_Mod=0, OutWRte=0 and MinRsvPct=5%, so a preceding 99% charge reserve cannot leak into idle hours.
Changed¶
has_heat_pumpenables the heating-demand forecast feature as soon as any temperature history exists.- Configured current PV power is shown live on the Lovelace card.
sensor.*_estimated_savingsisMEASUREMENT(a jumping horizon estimate, not a meter). Existing long-term statistics for that entity may still contain bogus resets — clear them under Developer Tools → Statistics if the graph looks wrong.- Export mode with a feed-in below the spread (stock 0.08 vs 0.20) sets plan warning
export_spread_unreachableand logs why nothing is exported. Set feed-in to 0 for market-price export, or lower the spread. - Scripts are invoked
blocking=True. A missing or failed script does not update_last_applied; live control fails safe to auto.
Docs¶
- README uses the wordmark via an absolute GitHub URL (so it renders in the HACS readme view) and the same badge set as ha-netze-bw.
- Brand images were already in
custom_components/smart_battery_pilot/brand/; no path move was needed.
Upgrade notes¶
Dry-run behaviour is stricter: if you were in dry-run with a leftover force-charge on the inverter, turning dry-run off now applies the planned action, and turning it on restores auto. Watch the first slot after updating.
If Nordpool is configured in cents, plans will stop treating every spread as profitable.
The Fronius idle script in docs/examples/byd-fronius-gen24.md changed — copy the new registers if you used that example.
v0.4.0 — Follow-ups after the HACS listing¶
Released 2026-09-02 · on GitHub
Follow-ups from the review round after the HACS listing.
Fixed¶
- Long-term statistics of the actual-savings sensor.
sensor.*_actual_savings_kwhreportsdischarge - charge, which drops whenever the battery charges, but it carried state classTOTAL_INCREASING. The recorder reads every decrease as a meter reset, so each cheap-charging night wrote a phantom reset and a spurious positive delta into the statistics. The sensor isTOTALnow, matching the EUR sensor. Existing statistics for that entity may still contain the bad deltas — clear them under Developer Tools → Statistics if the graph looks wrong. - The configuration sensor is no longer German on English installs. It returned the literal strings
aktiv/inaktivwithout a device class, so they showed up verbatim. It is an enum sensor now with translatedactive/inactivestates, and its nine attributes use English keys (capacity_kwh,min_soc_percent,efficiency_percent,min_price_spread_eur_kwh,discharge_mode,price_offset_eur_kwh,feed_in_tariff_eur_kwh,training_days,dry_run). - Card title escaping. The Lovelace card interpolated its configured title into the
ha-cardheader without the escape helper it already used at every other interpolation site.
Changed¶
- Test dependencies now live in
tests/requirements.txtrather than inline in the CI workflow, so a local checkout reproduces CI withpip install -r tests/requirements.txt.
Upgrade notes¶
The configuration sensor's unique_id changed along with its translation key. After updating you get a new sensor.<device>_configuration, and the previous konfiguration entity stays behind in the entity registry as unavailable — delete it once under Settings → Devices & Services → Entities. Only diagnostic data is affected; no automation state is lost.
If you read the configuration sensor's attributes in a template or automation, update the key names as listed above.
v0.3.1 — The card speaks your language¶
Released 2026-08-21 · on GitHub
Completes the localisation work started in v0.3.0.
Changed¶
- The bundled Lovelace card is no longer hardcoded German. Action labels, legend, empty states, hover tooltip and date/time formatting now follow the Home Assistant user's language (
hass.locale.language→ browser language → English). English and German ship with the card; seedocs/configuration.mdfor adding a language. - The card re-renders on a language change, and interpolated entity ids / error codes are HTML-escaped.
v0.3.0 — English states, Home Assistant 2024.11 minimum¶
Released 2026-08-21 · on GitHub
Addresses HACS default-repository review feedback.
Breaking¶
sensor.*_next_actionandsensor.*_plan_statusnow use English enum states:no_change,no_price_data,no_soc,no_price_adapter,error(previouslykeine_aenderung,kein_preis,kein_soc,kein_preisadapter,fehler). Update any automation that matched the old state strings.- Minimum Home Assistant version raised to 2024.11.0, matching the APIs actually used (
StaticPathConfig, implicitOptionsFlow.config_entry).
Fixed¶
async_setupno longer stats the bundled card file on the event loop.logo.png/logo@2x.pngare a real wordmark instead of copies of the icon.
v0.2.0 — The new sensors, released as a minor version¶
Released 2026-06-12 · on GitHub
What's new in v0.2.0¶
New sensors¶
- Current price (
sensor.…_current_price) — live electricity price from the active plan slot in EUR/kWh - Plan status (
sensor.…_plan_status) — diagnostic sensor:ok,kein_preis,kein_soc,fehler - Configuration (
sensor.…_konfiguration) — diagnostic entity exposing all active integration parameters (excluded from recorder) - Actual savings EUR — accumulated real-world savings since start; requires optional battery energy meter entities
- Actual savings kWh — net kWh arbitrage benefit (discharge − charge)
Sensor fixes¶
charge_planstate is now the count of actively planned (non-auto) slotsnext_actionnow showskeine_aenderunginstead ofunknownwhen no change is pending; exposesin_minutesattribute- Fixed HA startup warnings: monetary sensors now use
state_class: TOTALas required by Home Assistant
Config flow¶
- Optional battery energy meter entities (
battery_charge_energy_entity,battery_discharge_energy_entity) to enable actual savings tracking - Detailed
data_descriptionhelp texts for all config/options flow fields (German) - Feed-in tariff default corrected to 0.08 EUR/kWh (was 0.9)
Dashboard card¶
- Auto mode now shows a subtle blue background in the graph (was transparent)
- Consumption forecast overlay on the price chart (shared kWh scale with PV)
- Screenshots added to README
CI & metadata¶
- GitHub Actions: Node.js 24 (Node 20 deprecated June 2026)
- GitHub repository topics added for discoverability (Tibber, aWATTar, Nordpool, EPEX, Fronius, BYD, …)
v0.1.4 — Sensors for price, plan status and savings¶
Released 2026-06-12 · on GitHub
What's new¶
New sensors¶
- Current price — live electricity price from the active plan slot (EUR/kWh)
- Plan status — diagnostic sensor:
ok,kein_preis,kein_soc,fehler - Configuration — diagnostic entity showing all active integration parameters (not recorded)
- Actual savings EUR — accumulated real savings since start, requires optional energy meter entities
- Actual savings kWh — net kWh arbitrage benefit (discharge − charge)
- Next action now shows
keine_aenderunginstead ofunknownwhen no change is pending, and exposesin_minutesattribute
Fixes¶
charge_planstate is now the count of actively planned (non-auto) slots — previously always showed the raw plan object- Fixed HA startup warnings:
SavingsSensorandActualSavingsEurSensornow usestate_class: TOTAL(required fordevice_class: monetary) - Auto mode now shows a subtle blue background in the card graph (was transparent)
- Feed-in tariff default corrected from 0.9 → 0.08 EUR/kWh
Config flow improvements¶
- Optional battery energy meter entities (
battery_charge_energy_entity,battery_discharge_energy_entity) to enable actual savings tracking - Detailed
data_descriptionhelp texts for all config/options flow fields (German)
Dashboard card¶
- Consumption forecast overlay on the price chart (shared kWh scale with PV)
- Screenshots added to README for all three action scenarios
CI¶
- GitHub Actions now uses Node.js 24 (Node 20 deprecated June 2026)
v0.1.0 — Initial release¶
Released 2026-06-11 · on GitHub
First release of Smart Battery Pilot — charge your home battery when electricity is cheap, use it when it's expensive.
Features¶
- Vendor-neutral battery control via user-defined Home Assistant scripts (charge / idle / auto / export)
- Price forecast auto-detection: Nordpool, EPEX Spot, ENTSO-E, aWATTar, hourly price arrays
- Self-learning consumption forecast (hourly profile + ridge regression, zero external dependencies)
- Optional PV forecast integration (net-demand planning)
- Deterministic optimizer with SOC simulation, efficiency losses and configurable price spread
- Dry-run mode and master switch for safe rollout, fail-safe back to auto mode
- Bundled Lovelace card (price curve, action bands, SOC forecast)
- Vendor example: BYD Battery-Box Premium HVM + Fronius Gen24 (Modbus TCP)
Installation¶
HACS → Custom repositories → cygnusb/ha-smart-battery-pilot (Integration)
🤖 Generated with Claude Code