Plan Charging Action
The plan_charging action turns battery parameters into a complete cost-minimized charging schedule. Instead of manually computing energy, duration, and power, you describe the battery (capacity, current SoC, target SoC, max power) and the action returns a per-interval plan with SoC progression, cost totals, and segment grouping.
The plan_charging action is experimental and still undergoing testing. Its parameters, response format, and behavior may change in future releases. Use it in automations with care, and please report any issues.
If you already know the duration in minutes and just need the cheapest time window, use find_cheapest_hours or find_cheapest_block. Use plan_charging when you know your battery/EV parameters and want the integration to compute the duration, account for charging losses, and produce a SoC progression.
At a Glance
| Situation | Example |
|---|---|
| Home battery: "Charge from 20% to 80%, efficiency 0.92" | current_soc_percent: 20, target_soc_percent: 80, battery_capacity_kwh: 10 |
| EV with 3-phase charger: "Use 1/2/3 phases as needed" | charge_power_steps_w: [1380, 4140, 11000] |
| Battery with modulation: "30 W – 1200 W continuous" | min_charge_power_w: 30, max_charge_power_w: 1200 |
| Deadline-aware: "At least 50% before next peak" | must_reach_soc_percent: 50, must_reach_by_event: next_peak_period |
| Arbitrage: "Only charge if later discharge is profitable" | expected_discharge_price: 0.28, reserve_for_discharge: true |
Required Inputs
| Field | Description |
|---|---|
max_charge_power_w | Maximum charging power in watts (upper bound for every interval). |
current_soc_percent or current_soc_kwh | Current battery state of charge. |
target_soc_percent or target_soc_kwh | Desired battery state of charge. |
battery_capacity_kwh | Required when you use percent values. |
All other inputs (deadline, power steps, grid limit, economics, search range) are optional.
Choosing Between Fixed / Continuous / Stepped Power
| Mode | Trigger | Behavior |
|---|---|---|
| Fixed | Only max_charge_power_w set | Every selected interval charges at full power. Last interval may over-shoot the target slightly (rounding up). |
| Continuous | Add min_charge_power_w | Planner can reduce the final partial interval down to the minimum power — no over-shoot. |
| Stepped | Add charge_power_steps_w: [a, b, c] | Planner picks the smallest allowed step that covers the remaining energy. Mutually exclusive with min_charge_power_w. |
Deadlines
Combine a minimum SoC with a deadline:
must_reach_soc_percent/must_reach_soc_kwh— the minimum you need by the deadline.- Then pick one of:
must_reach_by— absolute datetime.must_reach_by_event— one ofmidnight,next_peak_period,next_best_period_end.
The planner runs a two-pass schedule: first guarantee the minimum SoC before the deadline using the cheapest pre-deadline intervals, then fill the remaining target with the cheapest intervals from the full search range.
Economics (Arbitrage)
For "charge cheap now, discharge expensive later" use cases:
discharging_efficiency— fraction still usable when discharged (default1.0).expected_discharge_price— expected price per kWh at discharge time (in your configured display unit).reserve_for_discharge— whentrue, discards intervals that are unprofitable given the round-trip efficiency.max_cost_per_kwh— a hard ceiling; any interval above this price is discarded before scheduling.
The response's economics.break_even_price tells you the maximum charging price at which the round-trip still breaks even.
Examples
Home battery — charge from 20% to 80% overnight
Show YAML
service: tibber_prices.plan_charging
data:
battery_capacity_kwh: 10
current_soc_percent: 20
target_soc_percent: 80
charging_efficiency: 0.92
max_charge_power_w: 2500
search_scope: remaining_today
response_variable: plan
EV — 3-phase, at least 50% before next peak period
Show YAML
service: tibber_prices.plan_charging
data:
battery_capacity_kwh: 60
current_soc_percent: 30
target_soc_percent: 80
must_reach_soc_percent: 50
must_reach_by_event: next_peak_period
max_charge_power_w: 11000
charge_power_steps_w: [1380, 4140, 11000]
grid_import_limit_w: 16000
response_variable: plan
Battery arbitrage — only if profitable
Show YAML
service: tibber_prices.plan_charging
data:
battery_capacity_kwh: 10
current_soc_percent: 10
target_soc_percent: 100
charging_efficiency: 0.92
discharging_efficiency: 0.92
expected_discharge_price: 0.28 # ct/kWh value expected when discharging
reserve_for_discharge: true
max_charge_power_w: 3000
search_scope: next_48h
response_variable: plan
Response Structure
The response contains the following top-level keys:
| Key | Description |
|---|---|
success | true when the request itself worked (even if no schedule was produced); false only on a Tibber API outage (reason: "price_data_unavailable"). |
intervals_found | true when a schedule was produced. |
battery | Normalized SoC / capacity / efficiency / achieved_soc_kwh (what you actually reach with the returned schedule). |
charging | Mode, total duration, total energy, total cost, and the schedule block. |
charging.schedule | segments[], intervals[], segment_count, seconds_until_start, seconds_until_end, and price statistics. |
deadline | Present when a deadline was set — includes must_reach_by, must_reach_soc_kwh, achieved_soc_kwh, deadline_met. |
economics | Present when any economic parameter was set — includes break_even_price, expected_net_savings, round_trip_efficiency. |
price_comparison | Difference between the selected schedule and the most expensive equivalent window. |
relaxation_applied / relaxation_steps | Whether the schedule was relaxed to fit available data. |
reason | Stable reason code when no schedule was found (see below). |
Per-Interval Fields
Each entry in charging.schedule.intervals[] includes:
starts_at,ends_at,price,level,rating_levelpower_w— power assigned to this interval (watts)grid_energy_kwh— energy drawn from the gridstored_energy_kwh— energy actually stored after lossessoc_after_kwh,soc_after_percent— cumulative SoC after this interval
Reason Codes
When no schedule is found, reason contains one of:
| Code | Meaning |
|---|---|
price_data_unavailable | The Tibber API was temporarily unavailable for an uncached range. This is the only case where success is false — treat it as a transient error and retry later. Your sensors keep running from cache. |
already_at_target | Current SoC is already at or above target — no charging needed. |
no_data_in_range | The search range has no price data. Often not an error: tomorrow's prices are usually published by Tibber around 13:00 local time, but the day-ahead auction can be delayed — sometimes the data only arrives later in the afternoon or evening. A range covering tomorrow returns this until then (with success: true). Retry later. |
no_intervals_matching_level_filter | min_price_level / max_price_level filtered everything out. |
no_intervals_after_economic_filter | max_cost_per_kwh or reserve_for_discharge filtered everything out. |
energy_unreachable | The energy needed cannot be charged within the available intervals + power limits. |
energy_unreachable_by_deadline | The minimum SoC cannot be reached before the deadline with the available intervals. |
selection_above_distance_threshold | min_distance_from_avg is not satisfied by the cheapest selection. |
Related
find_cheapest_hours— when you already know the duration in minutes.find_cheapest_block— for appliances that must run uninterrupted.- Scheduling Actions — shared parameters (search range, price filters, relaxation).
💬 Comments are page-specific. For a new question or idea, open a dedicated Discussion on GitHub so it gets its own thread and proper visibility.