Configuration¶
Config flow steps¶
1. Price source¶
| Field | Description |
|---|---|
| Price forecast entity | Entity of your tariff integration holding the price forecast in its attributes. Supported formats are detected automatically: Nordpool (raw_today/raw_tomorrow; price_in_cents or ct/öre/cEUR units are scaled to whole currency units per kWh), EPEX Spot (data with price_eur_per_mwh/price_ct_per_kwh), ENTSO-E (prices with time/price; the declared unit decides the scale, so a sensor configured for cents is converted too), aWATTar (marketprice entries) and plain hourly arrays (today/tomorrow float lists). Arrays may hold 23, 24 or 25 values (96 / 92 / 100 at quarter-hour resolution) — on the two clock-change days a local day is not 24 hours long, and the grid follows the real day. |
| Price offset | Fixed surcharge added to every market price: grid fees, taxes, levies (e.g. 0.187 EUR/kWh in Germany). Self-consumption decisions use the total import price you actually pay. Grid export is valued at the feed-in tariff, or at the raw market price when the tariff is 0 — the import surcharge is not treated as export revenue. |
| Feed-in tariff | What you earn per exported kWh. 0 means the market price is used (dynamic feed-in). Only relevant for the export discharge mode. |
If your integration is not recognized, create a template sensor with today/
tomorrow attributes as plain price arrays — that format is always accepted.
Currency¶
Prices are never converted between currencies. The planner works in whatever currency the price entity delivers — only sub-units such as cents or öre are scaled to whole units — and takes that to be the currency set in Home Assistant (Settings → System → General). Everything is labelled with it: the price fields in the config flow, the current-price and savings sensors, and the card.
Set Home Assistant's currency to the one your tariff is billed in before adding the integration: a new entry then starts with defaults sized to that currency (a spread of 2.2 SEK/kWh instead of 0.20, for example), and the check that refuses implausibly large prices scales with it. An entry created earlier keeps the values it has — review the price offset, feed-in tariff and spread once. A changed currency setting takes effect after reloading the integration.
When the price entity declares a currency of its own (a currency attribute,
or a unit such as SEK/kWh) that differs from Home Assistant's, you are told
in three places: the price step of the config and options flow points it out
once (submitting again goes on anyway), a repair appears under Settings →
Repairs until both currencies match, and the log gets a warning. Planning
carries on unchanged in the meantime.
2. Battery parameters¶
| Field | Description |
|---|---|
| State of charge entity | Live SOC in percent, e.g. sensor.byd_battery_box_premium_hv_ladezustand. |
| Usable capacity | Battery capacity in kWh. |
| Max charge / discharge power | Inverter limits in W. |
| Min / Max SOC | The plan never leaves this window (e.g. 10–95 %). |
| Roundtrip efficiency | Grid → battery → load efficiency, typically 88–92 %. Losses are priced into every charge decision. |
| Battery charge / discharge energy (optional) | Cumulative kWh or Wh meters. Both are required before the battery benefit (net, gross), pilot savings and energy sensors report a value. Unavailable readings are skipped so a glitch cannot inflate the totals. See how the three money totals differ — in short: battery benefit is what the battery is worth (counted whether or not the pilot steers), pilot savings is only what the pilot added by charging from the grid or holding energy back. |
3. Control scripts¶
The integration never talks to your inverter directly — it calls Home Assistant scripts that you provide. This is what makes it vendor neutral.
| Script | Called when | Receives |
|---|---|---|
| Force charge | a charge slot starts |
variables power_w (planned grid charge power), reserve_soc |
| Block discharge (idle) | an idle slot starts |
variable reserve_soc |
| Auto mode | an auto slot starts; also when the integration is disabled, unloaded, the plan becomes invalid, or dry-run is turned on after a live script was applied |
variable reserve_soc |
| Force discharge to grid | an export slot starts |
variables power_w (planned discharge power), reserve_soc |
reserve_soc (whole percent) is the backup reserve, or the
minimum SOC while no reserve is set. A script that writes it into the
inverter's own reserve setting keeps the reserve even when Home Assistant is
down. Scripts that ignore it keep working.
Within a running charge or export slot, a re-plan calls the script again only
when power_w or reserve_soc changed; the next slot always calls it. If such
an update fails, for example because an integration limits how often a register
can be written, the mode keeps running and the next re-plan or slot boundary
tries again. If the first call of a slot fails, the pilot falls back to auto
mode. Two calls can still come close together, so a script should tolerate a
rate limit or skip writes that wouldn't change anything.
Only the force-discharge script is optional — and only while the discharge mode is self-consumption. Selecting export mode without it is refused, because the planner would keep scheduling export slots that the executor can only answer with a warning and a fallback to auto mode. It must keep covering the house from the battery while it exports; see what an export slot assumes.
The auto script has to leave the battery in self-consumption in both directions: it covers the house load, and it charges from PV surplus. The plan counts on both (what an auto slot assumes). A hybrid inverter or an AC battery in its smart-meter mode does this by itself. A battery that only follows charge and discharge commands does not: there, the auto script has to hand over to something that does, such as an automation that follows the grid meter.
Ready-made scripts for specific hardware: see examples.
Each script is awaited (
blocking: true) and must return within 120 s, or the action counts as failed and the battery is handed back to auto mode. Give any retry loop inside a script a bound of its own — an unbounded one would otherwise hold up integration reloads and Home Assistant's shutdown.
4. Household consumption¶
| Field | Description |
|---|---|
| Consumption sensor | Total house consumption. Either a power sensor in W (recommended, e.g. sensor.solarnet_leistung_verbrauch) or an energy sensor in kWh with long-term statistics. Used to train the forecast from recorder history. |
| Outdoor temperature (optional) | Adds a heating-demand feature to the model — recommended for heat pump households. |
| Heat pump present | Forces the heating-demand feature in the consumption model as soon as any outdoor-temperature history exists (instead of waiting until half the samples are tagged). |
The forecast model:
- Day 1+: weighted hourly profile (weekday/weekend, recent days count more).
- After ~14 days of history: ridge regression with cyclic hour-of-day, weekend and temperature features (pure Python, no extra dependencies).
- After ~4 weeks of history: whichever of the two forecasts better. Both
are fitted on everything but the last 14 days and scored on those days by
their mean absolute error per hour; the lower error wins, the regression on
a tie. A load that follows the outdoor temperature favours the regression,
a sharp block at the same time every day (an appliance on a timer) favours
the profile, which the regression's smooth daily curve cannot draw. The
model_typeattribute of the plan status and consumption forecast sensors names the model in use; the plan status sensor and the diagnostics dump also carry both errors asmodel_holdout_mae. - Retrained automatically every 24 h, which repeats the comparison; the model is persisted across restarts.
- Pointing the integration at a different consumption sensor starts the history over, so the forecast falls back to the profile until the new sensor has ~14 days of its own — the regression is never carried over from the old one.
5. PV forecast (optional)¶
Daily production forecast sensors in kWh (e.g. Open-Meteo Solar Forecast's
sensor.energy_production_today / …_tomorrow). The daily total is
distributed over the daylight hours — read from sun.sun, so a December day
is modelled as the ~8 hours it really is — and subtracted from the
consumption forecast.
Optional current PV power is shown live on the Lovelace card. Without PV,
simply leave the fields empty.
Enter the forecast for the whole day, not "remaining today": the
integration already drops the hours that are over, so a remaining-today
sensor would be cut down a second time. Helios Forecast numbers its days from
today: use energy_day_1 for today and energy_day_2 for tomorrow.
Hourly forecast (Energy dashboard). The even daylight curve knows nothing about your roof: an east-facing array produces most of its energy before noon, a west-facing one after, and a cloudy morning moves the yield into the afternoon. If a forecast integration also feeds the Energy dashboard — Forecast.Solar, Open-Meteo Solar Forecast, Solcast, Helios and others do — pick its config entries in Hourly forecast, one per roof face. Their hourly curves are added up and replace the daylight curve slot by slot.
- A new setup pre-selects the entries your Energy dashboard already uses. Existing installations keep planning exactly as before until you pick entries yourself.
- The daily sensors stay the fallback. When one of the selected entries delivers no curve (integration not loaded, API down), the whole cycle plans with the daily sensors rather than silently leaving a roof out, and the log says so once. Slots beyond the end of the shortest curve use the daily sensors too, so keep them configured.
- Forecast.Solar stamps each value with the end of its hour; other integrations with the start. Both are read correctly. Forecast.Solar's curve ends at tomorrow's sunset, so the slot around that sunset and everything after it come from the daily sensors.
- The diagnostics download shows under
inputs.pv_hourlywhich entries were used, which failed, and how many slots came from the curve.
Options (Settings → Integrations → Configure)¶
The configure dialog opens a menu in which every input from the initial setup can be reviewed and changed later — each section shows the currently configured entities/values pre-filled:
- Optimizer tuning — spread, discharge mode, training days
- Price source & offsets — price entity, price offset, feed-in tariff
- Battery parameters — SOC entity, capacity, power limits, SOC window, efficiency
- Control scripts — the four action scripts
- Consumption & temperature — consumption sensor, temperature, heat pump
- PV forecast — hourly forecast entries, daily forecast entities and optional live PV power
- Cold-weather charging — only here, not in the initial setup; see below
- Backup reserve — only here; see below
- EV charging & other controllers — only here; see EV charging, evcc and other controllers
Sections return to the menu after submitting; changes are collected and only persisted via “💾 Save & close” (closing the dialog otherwise discards them). Saving reloads the integration and recomputes the plan.
| Option | Default | Description |
|---|---|---|
| Minimum price spread | 0.20 EUR/kWh | Grid charging only happens if discharge price > charge price / efficiency + spread. Raise it to be more conservative (fewer cycles), lower it to arbitrage more aggressively. |
| Discharge mode | Self-consumption | Self-consumption: the battery only ever covers the house load. Export: additionally force-discharge into the grid during extreme price peaks — requires the force-discharge script. Check your tariff/regulatory situation before enabling export. |
| Price offset / feed-in tariff | – | Same as in the config flow. Feed-in 0 uses the market price for export. A fixed tariff at or below the spread (default 0.20 EUR/kWh) could never schedule export, so the options flow refuses that combination instead of letting the setting look active while doing nothing. |
| Training days | 60 | History window for the consumption model. |
Cold-weather charging¶
Off by default. A cold LFP battery accepts far less than its nominal charge power — the BMS throttles below roughly 15 °C and nearly stops near 0 °C. With this option on, the planner knows that and books more (or longer) cheap charge slots instead of counting on energy the battery will not take.
| Setting | Default | Meaning |
|---|---|---|
| Plan with reduced charge power in the cold | off | The planner expects a cold battery to take less than the maximum charge power. A slot planned up to that cold limit still asks the charge script for full power — the battery's BMS does the throttling, never the pilot. A slot that only needs part of it asks for exactly what it needs, as it would without the option. |
| Battery temperature entity | – | Cell or module temperature of the battery (not the outdoor sensor). Required when the option is on. °F is converted. If it becomes unavailable, the planner falls back to full charge power and logs one warning. |
| Charge power at 0 / 5 / 10 / 15 / 20 °C | 10 / 20 / 50 / 80 / 100 % | Percent of the maximum charge power the battery accepts. Linear in between; below 0 °C the 0 °C value applies, above 20 °C the 20 °C value. The values must not fall as the temperature rises. The defaults are a conservative generic LFP curve — check your battery's datasheet. |
The five sliders sit in a collapsible block ("Sliders: charge power by temperature"). It is open while the option is still off and folded once it is on - the chart shows the sliders as the dashed line either way - and opens again right after "Adopt into the sliders".
Learning. With a battery charge energy meter configured (battery step), every forced charge slot the pilot really runs is measured: how much went into the battery compared with what was asked for. Slots near max SOC (the battery tapers because it is full), shorter than 10 minutes, with requests below 20 % of the max charge power, or with less than 0.2 kWh in under 30 minutes (too close to the resolution of typical energy meters) are ignored. Dry-run learns nothing, because no charge actually happens.
Once six slots have shown the battery taking clearly less than requested, a curve is fitted through the individual measurements, each at the temperature it was taken at. There is no fixed grid: the curve consists of temperature ranges that begin and end where the measurements do, each with one charge limit, for example "4..9 °C: 55 %". Ranges are split where the measurements differ and joined where they agree, so a battery whose BMS switches at 10 °C shows a step at 10 °C. The rules of the fit:
- The limit never falls with rising temperature.
- Each range takes the median of its slots, so one odd slot does not bend it.
- A slot in which the battery took everything it was asked for says only that the limit is at least that high, there and at every warmer temperature. It counts where it lies above the curve and is ignored otherwise.
- Between two ranges the limit rises in a straight line.
- Outside the measured temperatures the sliders stay in charge. The curve only bounds them: colder than the coldest measurement the planner never assumes more than the coldest learned limit, warmer than the warmest never less than the warmest learned limit. Where sliders and measurements disagree, that shows as a step at the edge of the measured temperatures.
A single slot can form a range of its own when it lies colder and lower, or warmer and higher, than all others - the number behind each range tells how much stands behind it.
When the temperature does not explain the limit. Some batteries limit
the charge power by something the configured sensor does not show - the
coldest cell rather than the average, for instance. The measurements then
contradict each other: the same limit at 8 and at 13 °C, and full power at
9 °C. A limited slot counts as contradicted when a slot at the same or a lower
temperature took at least 10 percentage points more. With three contradicted
slots that are at least half of the limited ones, the pilot stops planning
with the learned curve and uses the sliders. The dialog marks the count with a
warning sign and draws no learned curve; the configuration sensor shows
charge_rate_learning: temperature_mismatch. Measuring continues, and the
verdict lifts on its own once the contradicted slots are a minority. After
switching to a better temperature sensor, discard the measurements: the old
ones carry the old sensor's temperatures.
Seeing and steering what was learned. The section in the options dialog draws it: the learned curve as a blue line (bold where it was measured, thin where the sliders stand in), the sliders as a dashed orange line, a dot for every charge slot the battery limited, a small triangle for every slot in which it took all that was asked (the limit lies at least there) and a vertical line at the current battery temperature. Below the chart the learned ranges are listed with their limit and the number of slots behind each.
| Setting | Default | Meaning |
|---|---|---|
| Plan with the learned values | on | On: the learned curve replaces the slider curve wherever there are measurements. Off: only the sliders count. Measuring continues either way, and the values stay visible. |
| Learned values | Leave as they are | Adopt into the sliders writes the measured values into the five sliders and shows the form again, so you can still adjust them before saving. Each slider takes the learned curve's value at its own temperature. Five sliders cannot hold a step between two of them, so adopting is a coarser copy of the curve. Discard all measurements deletes every observation when you save the dialog — use it after a battery swap or when the temperature sensor was wrong. |
With learning on, adopting changes little about the plan: the learned curve applies either way. It is the way to freeze a measured curve — adopt, then switch "Plan with the learned values" off.
The configuration sensor shows charge_factor, its source (curve,
learned, no_temperature), whether learning is active, has
no_charge_meter or found a temperature_mismatch, whether the learned values are used, the number of learned
ranges (learned_bands) and the ranges themselves (charge_rate_bands, each
with from_c, to_c, the learned limit and its number of slots). The
diagnostics dump lists the same ranges.
Backup reserve¶
Off by default. Keeps enough energy in the battery for a grid outage.
| Setting | Default | Meaning |
|---|---|---|
| Backup reserve | 0 % | SOC that arbitrage never spends. At or below the minimum SOC the option is off. |
| Reserve entity | – | Optional input_number, number or sensor in percent. While it reads a number it replaces the fixed value — let an automation raise it on a storm warning, in winter, or before a holiday. If it becomes unavailable, the fixed value applies and one warning is logged. |
| Refill deadline | 12 h | If the SOC is below the reserve, it is brought back up within this many hours: forecast PV first, the rest in the cheapest grid slots, whether or not the price spread pays for it. Until the refill the battery spends nothing, so the plan shows no arbitrage and no idle before it. When the cheapest slot comes after a price peak, the pilot refills earlier at a slightly higher price if serving that peak pays more. |
| Block discharging at the reserve | off | Fallback for inverters without a reserve setting of their own: when the SOC reaches the reserve, the pilot runs the block discharge script instead of auto, and releases it 2 percentage points above the reserve. It reacts to SOC changes immediately, not only at the next re-plan. |
The effective reserve is the entity value (or the fixed value), clamped to the SOC window.
For a real outage, the reserve has to live in the inverter. The pilot can only enforce it while Home Assistant runs, and a grid outage may be exactly when it does not. Have your
autoandidlescripts writereserve_socinto the inverter's minimum reserve (for the Fronius GEN24 see the example). When the reserve changes, the pilot runs the current action's script again, so the new value reaches the inverter without waiting for the next action change.
Below the reserve the plan counts on the battery not discharging, but it still
shows those slots as auto: on an inverter that knows the reserve, auto can't
go below it. If neither the inverter nor the fallback holds the reserve, auto
keeps discharging. The pilot then says so under Settings → Repairs
(Battery discharges below the backup reserve, see
consistency checks).
Refilling costs money, and that cost is not counted against the estimated
savings — it is reported separately as reserve_refill_kwh and
reserve_refill_cost_eur on the plan sensor. Only the cheapest refill is
mandatory, though: when the pilot refills earlier to serve a peak, what that
costs on top does come off the estimated savings. The battery benefit
and pilot savings sensors count what the meters measured, so the energy bought
for a refill does show up there (a refill charge is a pilot lot like any grid
charge) — estimated and measured figures drift apart by the refill cost. A
deadline too short for the charge power shows the plan warning
reserve_refill_incomplete. The
configuration sensor shows reserve_soc, its source (fixed, entity,
off) and whether the discharge block is on.
EV charging & other controllers¶
Off by default. Takes wallbox energy out of the consumption history, can block discharging while an EV charges, and pauses the pilot while another controller such as evcc holds the battery. With evcc, the plan can also leave room for sessions evcc has planned. Settings, the evcc setup and other wallboxes: EV charging, evcc and other controllers.
Going live¶
- After setup the integration is disabled with dry-run on.
- Watch
sensor.…_charge_planand the log (DRY RUN: would apply …). - Turn on
switch.…_enabled— still dry-run, nothing is called yet. - When the plan looks sensible, turn off
switch.…_dry_run.
If anything goes wrong (price entity unavailable, no prices, SOC missing), the integration calls your auto mode script once, logs one warning naming the reason, and stops interfering until planning works again. Turning dry-run on after a live script was applied also restores auto.
Consistency checks¶
The plan rests on assumptions the pilot can't enforce. When one fails, nothing errors: the plan and the battery just drift apart. So the pilot compares the two and raises a message under Settings → Repairs when they disagree. The checks only report, they never change a plan or a script call. A message disappears by itself once the problem is gone. If it describes something you do on purpose, such as limiting charging, use Ignore on the message.
| Message | Raised when | Needs |
|---|---|---|
| A control script keeps failing | one script failed 3 times in a row | – |
| No valid plan | no plan for 12 hours | – |
| Another controller holds the battery | the external control entity is active for 24 hours | External control entity |
| SOC sensor frozen | the SOC stands still for 2 hours while 10 % of the capacity moves | Energy meters, or a forced slot |
| PV forecast is a "remaining today" sensor | the forecast for today falls under 15 % of its daily high by sunset, 3 days | Daily PV sensors without hourly forecast entries |
| Battery capacity looks wrong | charging implies a capacity more than 25 % off, 3 times | Both energy meters |
| Charge slots don't charge | under 50 % of the requested energy arrives, 3 days | SOC |
| Battery discharges in idle slots | over 5 % of the capacity net leaves during idle slots, 3 days | SOC |
| Export slots don't discharge | under 50 % of the planned energy leaves, 3 days | SOC |
| Battery doesn't cover the house | under 20 % of the measured deficit is delivered in auto slots, 3 days | Consumption, and live PV power or a slot with no forecast PV |
| PV surplus doesn't reach the battery | under 20 % of the measured surplus arrives in auto slots, 3 days | Live PV power and consumption |
| Battery discharges below the backup reserve | 3 % of the capacity leaves in auto slots at or below the reserve, at once | A backup reserve, with the discharge block fallback off |
Good to know:
- What counts: only slots the pilot really drives. Dry-run, external control and a charging car are left out, and so is a battery within 5 SOC points of its limit, since it may rightly stop there.
- Series: the 3 days need not be consecutive. A day with too little at stake (under 10 % of the capacity, 5 % in charge and export slots) doesn't count either way. A clearly good day resets the series and clears the message.
- A wrong forecast doesn't fire a check. Surplus and deficit come from the live PV sensor, not from the plan, read every minute together with the consumption. If the consumption sensor only counts energy, the forecast load stands in for the live load.
- Meters and accuracy: with charge and discharge meters the checks read those; without them they use the SOC. A live PV sensor that misses part of the PV underestimates the surplus, so the check stays quiet rather than raising a false message.
- The backup reserve check doesn't wait for 3 days: the reserve is there for an outage that may come tonight. It counts from the highest SOC the battery reached below the reserve, so a SOC sensor flickering by one point moves nothing, and PV lifting the SOC first hides nothing. The message clears at the end of a day on which auto slots held the reserve for at least an hour, or as soon as the discharge block fallback is on.
- Partial problems stay unreported. A battery that takes half of the surplus passes. The limits are set to catch what is clearly broken.
- The state of the checks is in the diagnostics download under
runtime.health.historylists the last 7 days: per check what was expected and what happened, the day's verdict (strike,pass,betweenortoo_little) and the strikes after it. A series that a good day reset shows up there too; a check missing from a day had nothing to judge on it.
Troubleshooting¶
- Download diagnostics (device page → ⋮) first. It holds the inputs of the last plan, the first day of the plan and the executor's last 50 decisions with their reasons — usually enough to see why a slot was charged, blocked or left alone.
- Debug logging (device page → Enable debug logging) adds every planning run's inputs, how the price adapter parsed the entity, each charge/discharge pairing the optimizer made or rejected (e.g. spread not reached, with the arithmetic), the reserve refill pass and every executor decision.
- "Why does it show savings although the pilot did nothing?" — Battery
benefit is what your battery is worth in self-consumption, pilot or not.
What the pilot added is Pilot savings, which stays at 0.00 while every
slot is
auto.
Dashboard¶
The bundled card (auto-registered as a lovelace resource in storage mode; with
resource_mode: yaml add it manually, see the README):
type: custom:smart-battery-pilot-card
entity: sensor.smart_battery_pilot_charge_plan # optional - auto-discovered
title: Smart Battery Pilot # optional
view: tracks # optional - tracks | balance | bars | flow | compact
height: auto # optional - auto | <pixels> | <percent>
entity may be omitted — the card auto-discovers the plan sensor (entity IDs
are localized, e.g. …_ladeplan on German installations).
Size¶
The card measures the box the dashboard gives it and draws at that size: one SVG unit is one CSS pixel, so axis labels stay 9 px and strokes stay 2 px whether the card is 300 px wide in a phone column or 1600 px wide in a panel view. Extra width buys a denser hour grid (down to one label per hour) and extra height buys taller panels with more y-axis ticks, rather than magnifying everything.
height |
Effect |
|---|---|
omitted, or auto (default) |
Grows gently with width and then stops: roughly 236–440 px in tracks, balance and flow, 250–460 px in bars, 158–300 px in compact. |
a number, e.g. height: 320 |
Exactly that many pixels for the chart. Accepts "320px" too. Panels shrink to a 30 px floor, below which the card grows back. |
a percentage, e.g. height: "100%" |
Fills the box the dashboard offers, minus the status line and legend. Only 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. |
In a sections view a card occupies 12 of the section's sub-columns by
default, and a section has 12 sub-columns per macro column it spans — so in a
column_span: 3 section the default fills one third of it. To use the whole
section:
type: custom:smart-battery-pilot-card
grid_options:
columns: full
Views¶
One scale per drawing surface. Price per kWh, energy in kWh and SOC in percent are three different units, and stacking them on a single plot means two of them have to be squeezed into whatever space the third leaves — which is how a correct consumption forecast ends up drawn along the baseline on a strong PV day.
view |
What it draws |
|---|---|
tracks (default) |
Three panels sharing one time axis: price, PV against consumption, SOC. |
balance |
Same three panels, but the middle one shows PV minus consumption as an area diverging around zero — the quantity the optimizer plans against. |
bars |
Price as one bar per slot in the colour of the action planned for it, which replaces the ribbon; PV against consumption; SOC. With EV charging on the plan, a lane under the price shows the car's planned kWh per slot on a scale of its own, and evcc's price limit runs across the bars. |
flow |
Price as a band of one orange that darkens with the price (cheapest and dearest slot labeled above it), the action ribbon directly under it, PV drawn upwards and consumption downwards around zero, and the SOC across 0–100 % with the reserve (min_soc) and ceiling (max_soc) as dashed lines. With EV charging, the same car lane as in bars, and a dot on every slot cheap enough to be under evcc's limit. |
compact |
Status line, three figures (SOC now → at horizon end, next change, planned discharge) and a smaller SOC + price chart. Roughly two thirds the height; below 420 px the three figures are set smaller so they stay on one line. With an EV charging session on the plan a fourth figure shows it (energy, time span, why evcc charges) in place of the status-line summary; below 420 px the four go two by two. |
An unrecognised value falls back to tracks. The button at the right of the
legend flips between tracks and balance without editing the dashboard;
it resets to the configured view whenever the card config is re-applied.
Common to every view: planned actions (charge/blocked/export/auto) — as a
labeled band across the top, as the colour of the price bars in bars, as a
thin band under the price in flow — local-midnight day separators with the date,
a "now" marker, the most expensive and the cheapest slot labeled on the
price, the SOC endpoint labeled, and a hover marker on every curve with a
tooltip showing time slot, action, price, SOC forecast, PV and net demand.
With planned EV sessions (evcc), every view adds the charging windows as a band through all panels, the slots where the battery is held for the car as a thick SOC segment, and, where it fits on the price axis, evcc's price limit as a dashed line. The tooltip names the EV energy, why evcc charges, and evcc's own limit for slots it picked for their price. A plan without EV charging looks exactly as before, apart from one line in the status bar that says how the charger stands (car SOC and "no charging planned", "not plugged in" or "evcc not reachable"). See EV charging.
Apart from flow, the SOC panel is drawn against the battery's configured window (min_soc to
max_soc, published on the plan sensor) instead of 0–100 %, so the discharge
depth uses the panel's full height rather than a sliver along the top edge.
The card's labels, tooltip and date/time formatting follow the Home Assistant
user's language (hass.locale.language, falling back to the browser language
and finally to English). The card ships the same ten languages as the
integration (English, German, Danish, Estonian, Finnish, Lithuanian, Latvian,
Norwegian Bokmål, Dutch, Swedish); to add another, extend the TRANSLATIONS
table at the top of
custom_components/smart_battery_pilot/frontend/smart-battery-pilot-card.js
with a new entry keyed by the language tag — any key missing from it falls back
to the English string.
Alternative with ApexCharts Card:
type: custom:apexcharts-card
header: { show: true, title: Strompreis & Ladeplan }
graph_span: 36h
span: { start: hour }
series:
- entity: sensor.smart_battery_pilot_charge_plan
name: Preis
data_generator: |
return entity.attributes.slots.map(s => [new Date(s.start), s.price]);
type: line
- entity: sensor.smart_battery_pilot_charge_plan
name: SOC-Prognose
data_generator: |
return entity.attributes.slots.map(s => [new Date(s.start), s.soc_forecast]);
type: line
yaxis_id: soc
yaxis:
- id: price
- id: soc
opposite: true
max: 100