Conversation
| levels_post.append(level) | ||
| return levels_pre, levels_post | ||
|
|
||
| def plan_iboost_forecast(self): |
There was a problem hiding this comment.
iboost_smart_min_length silently stops applying once a forecast is configured: the legacy planner enforces a minimum window length (config min 30 / default 30 / max 120 minutes, plan.py:5225) but plan_iboost_forecast never reads it and emits slots as short as 5 minutes (e.g. the PR's own iboost_plan_reserve case books a 20-minute slot). With small forecast draws a user configured for 30-120 minute minimum boosts gets 5-25 minute diverter cycles instead, with no log line or doc noting that min_length stops applying. Either honour min_length by consolidating adjacent booked intervals, or document the changed behaviour in both doc pages.
There was a problem hiding this comment.
Taken the documentation option: both doc pages now state that
iboost_smart_min_length does not apply to forecast-driven slots — each slot's length
follows the energy the draw needs at the element power (minimum 5 minutes), with
adjacent-interval consolidation on price ties. I did not implement a hard minimum run
length because padding a run to min_length would book energy the tank model says it does
not need; if you'd rather have min_length enforced as a booking constraint (only book runs
that can reach min_length), I can do that as a follow-up.
|
|
||
| return self.plan_iboost_forecast_slots(interval_starts, boost, import_rates, stored_start, capacity, demand, uncovered_kwh) | ||
|
|
||
| def iboost_forecast_day_usage(self, interval_starts, boost, total_days): |
There was a problem hiding this comment.
Cleanup (reuse/simplification): iboost_forecast_day_usage() duplicates the legacy per-calendar-day cap bookkeeping (plan.py:5142-5146: same total_days expression, same day[0]=iboost_today seed) with drift already present: the legacy loop rounds accumulations with dp3(), this helper does not round. Two independent ledgers for the same iboost_max_energy cap mean a fix to one (midnight boundary, dp3 rounding) leaves the other stale, and the rounding drift makes the planners book different totals against an identical budget. Consider one shared day-usage helper for both planners.
There was a problem hiding this comment.
Partially unified: both planners now share iboost_plan_total_days() and the forecast
ledger applies the same dp3 accumulation as the legacy ledger, which removes both drifts
named here (total_days expression and rounding). The legacy planner keeps its incremental
ledger inside its own booking loop: recomputing it from the plan list would change the dp3
accumulation order and risk perturbing golden output for no behaviour gain. If you want one
literal helper for both, I'd rather do that in a follow-up with its own golden review.
| load_forecast_final[minute] = value | ||
| return load_forecast_final, load_forecast_array | ||
|
|
||
| def fetch_iboost_forecast(self): |
There was a problem hiding this comment.
Cleanup (reuse): fetch_iboost_forecast() copies the load_forecast ingestion block of fetch_extra_load_forecast() (fetch.py:2831) nearly line for line (~60 lines): entity_ids unwrap, 569Xlattribute split, get_state_wrapper try/except, dict->array conversion, identical minute_data() kwargs, per-entity warn-and-skip, cross-source summation. Its docstring says 'modelled on fetch_extra_load_forecast()'. A change to shared forecast-format handling must now be made twice; fixing only load_forecast leaves the iBoost path on old behaviour, and a format Predbat learns to read for load stays unsupported for hot-water demand (silently falling back to the legacy planner). Also, the per-minute positive delta below re-implements Fetch.get_from_incrementing(data, index, backwards=False) (fetch.py:752, 'max(data.get(index+1) - data.get(index), 0)'), which fetch.py already uses for exactly this reading on the load forecast - the comment's 'same reading get_from_incrementing gives the load forecast' guarantee is currently enforced only by copy.
There was a problem hiding this comment.
Fixed: fetch_cumulative_forecasts() now carries the shared block (entity unwrap,
$attribute split, state fetch with try/except, dict→list conversion and validation, the
minute_data call, per-source warn-and-skip) for both load_forecast and iboost_forecast, and
the per-minute delta is read through get_from_incrementing(..., backwards=False) as you
point out it should be. Two deliberate side effects on the load path: a non-list state is
now skipped with a warning rather than raising, and the fetch-exception log is unified on
"Warn:" (both paths degrade gracefully — load just skips the source). Say the word if you'd
rather keep "Error:" there.
| if demand[target] <= 0: | ||
| continue | ||
| while True: | ||
| levels_pre, levels_post = self.iboost_tank_trajectory(stored_start, capacity, demand, boost) |
There was a problem hiding this comment.
Efficiency (worst case untested): the earliest-deadline while-loop recomputes the full O(N) tank trajectory, a fresh O(target) running-max array, an O(N) day-usage pass and an O(target) candidate scan on every dose iteration, and each dose is capped at one interval's element energy. With plan_interval_minutes=5 and a long forecast (~540 intervals) plus a mis-scaled ibost_forecast_scaling that leaves demand nonzero everywhere, this is ~540 targets x many doses x O(540) work = tens of millions of Python steps per plan rebuild, every 5 minutes. All existing tests use 30-min intervals (36 intervals), so the worst case is untested. Cheaper: update levels incrementally per dose and keep the running max as a single rolling value.
There was a problem hiding this comment.
Reduced: each dose now does a single fused pass bounded at the target (trajectory and the
running max together) instead of four full-horizon passes, the day ledger is maintained
incrementally per dose, and the fill pass only recomputes state when a fill actually lands.
It is not yet fully incremental across doses — the clamping makes a correct suffix-only
update fiddly, and at the default 30-minute interval the loop is negligible. If 5-minute
interval profiling shows it matters I'll take the incremental-update step as a follow-up.
| offset_minute = minute + offset | ||
| if offset_minute < self.minutes_now: | ||
| continue | ||
| kwh += max(demand_cumulative.get(offset_minute + 1, 0) - demand_cumulative.get(offset_minute, 0), 0.0) |
There was a problem hiding this comment.
Automated comment from the triage bot.
The comment above claims the region minute_data back-fills beyond the data reads as zero demand, but minute_data tail-fills with newest_state — the value of the earliest sample (utils.py fill loop) — not the last one. So a cumulative series that ends lower than it started (e.g. an internal reset: 6.0 kWh at min 0 … 0.0 reset … 2.0 last) produces a positive delta at the minute right after the last sample: 6.0 − 2.0 = +4.0 kWh phantom demand, which the planner then pre-heats for.
Related, with multiple configured sensors of different horizons: a sensor whose series ends earlier contributes a step drop where its cumulative value vanishes, and the per-minute max(..., 0) clamp on the summed series can swallow a genuine draw from another sensor in that same minute (its delta is absorbed into the negative sum). Computing deltas per sensor before summing, or tail-filling with the last sample for this series, would close both.
There was a problem hiding this comment.
Right — the comment was wrong about the back-fill value, and the reset scenario did produce
phantom demand. Fixed by taking per-minute deltas per source and only within that source's
own raw extent: the tail back-fill and the first-sample step are both excluded, and since
deltas are now per source, one source's tail can no longer swallow another's draw in the
summed series. Tests: iboost_fetch_reset (internal reset books no phantom) and the
multi-sensor case.
| best = None | ||
| best_key = None | ||
| best_room = 0.0 | ||
| for slot_n in range(target): |
There was a problem hiding this comment.
iBoost review: hot-loop cost. Every dose iteration recomputes the full O(N) iboost_tank_trajectory, an O(target) running-max, an O(N) iboost_forecast_day_usage and an O(target) candidate scan; doses are capped at one candidate's element energy per iteration, so worst case is O(N^2 x doses). With plan_interval_minutes=5 and a 45h horizon (~540 intervals) and demand in every interval that is tens of millions of Python steps per plan rebuild, and the plan is rebuilt every 5 minutes - watchdog-restart territory on low-power hosts. All tests run at 30-min intervals (~36 intervals) so this is untested. A booking at index best only changes the trajectory suffix from best onward and one day bucket - keep levels/day_usage incrementally and update them after each boost[best] += amount; the fill pass (line 5367) has the same shape (trajectory + day_usage recomputed even when no fill is applied).
There was a problem hiding this comment.
See the inline perf thread: fused single pass per dose bounded at the target, incremental
day ledger, lazy fill-pass recompute; full incrementality deferred with reasoning.
The guard against booking the same slot start twice in plan_iboost_smart() tested the outer window-scan loop's leftover variable rather than the slot being booked, so overlapping iboost_smart_min_length windows could book the same slot start twice; in_iboost_slot() then drops the second slot's energy from the prediction. Key the guard on the slot start. Adds a non-flat-rate test case with overlapping 60-minute windows that double-book without the fix, plus plan invariant checks (sorted, no duplicate starts) applied to every iBoost smart test. (The window-averaging half of the original branch commit is already on main as f98554f / GH#4817 and is dropped here.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QiqN8TJpbXRsSkph6yNSx
Add the configuration surface for demand-forecast-driven iBoost planning: iboost_forecast (sensor list, same cumulative-kWh format as load_forecast), iboost_forecast_scaling and iboost_tank_soc in apps.yaml, plus input_number.predbat_iboost_tank_capacity and input_number.predbat_iboost_tank_reserve Home Assistant entities. fetch_iboost_forecast() reads the configured sensors, scales the series and converts it into demand per plan interval (per-minute positive deltas, the same reading the load forecast gets), judging staleness from the raw timestamps since minute_data back-fills beyond the last data point. When the forecast is missing, stale or empty it returns an empty dict and the legacy smart plan is used unchanged. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QiqN8TJpbXRsSkph6yNSx
When a hot water demand forecast is loaded, plan_iboost_smart() delegates to plan_iboost_forecast(): the tank is modelled as a charge-only store (iboost_tank_capacity kWh, initial level from iboost_tank_soc or empty) and the planner walks the plan intervals in time order, booking the cheapest eligible earlier interval whenever a draw would take the level below iboost_tank_reserve. Bookings respect the element power, the per-day iboost_max_energy cap, the rate and gas thresholds and the tank capacity; uncovered demand logs a warning and planning continues. Slots keep the legacy structure (start, end, kwh, average, cost), priced at each slot's own import rate, with a regression test proving the legacy path is unchanged when no forecast is configured. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QiqN8TJpbXRsSkph6yNSx
Document iboost_forecast, iboost_forecast_scaling and iboost_tank_soc in apps-yaml.md alongside the other iBoost items, cross-referencing the load_forecast data format, and add an 'iBoost demand forecast' section to customisation.md covering the charge-only store model, the tank capacity and reserve entities, the per-day iboost_max_energy cap interaction and the fallback to the legacy smart plan. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QiqN8TJpbXRsSkph6yNSx
Add input_number.predbat_iboost_fill_rate_threshold: any eligible slot with an import rate at or below the threshold tops the tank up to its capacity headroom regardless of the forecast, for example to heat fully on free or negative rates. After filling, the level trajectory is re-verified and later planned boosts the fill energy has made redundant are trimmed so the level stays at or below the capacity everywhere. The default of -99 p/kWh disables filling, leaving the planner unchanged. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QiqN8TJpbXRsSkph6yNSx
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QiqN8TJpbXRsSkph6yNSx
Address findings from an adversarial review of the forecast planner: - The current plan interval is usually partially elapsed (replans run every few minutes): cap its bookable energy to the remaining minutes and emit its slot starting at minutes_now, mirroring the legacy planner's clamp, so booked energy is always deliverable and the day cap is never consumed by phantom bookings. Demand already drawn earlier in the current interval is likewise excluded during ingestion since the tank SoC reading already reflects it. - The fill pass now trims displaced boosts immediately after each fill and derives day-cap usage from the surviving plan, so a fill no longer blocks later eligible fill slots with day-cap usage the trim then refunds. - Non-list forecast data (a scalar state from a sensor configured without an attribute suffix) is skipped with a warning instead of raising and aborting the fetch cycle, and data lying wholly beyond the planning horizon falls back to the legacy plan. - The candidate scan uses a running suffix maximum so the capacity headroom check is O(1) per candidate. - Docs: state the modes in which the forecast planner runs and fix the entity-count wording. Test hardening: unaligned minutes_now cases for the planner and the fetch, binding import/export/gas threshold cases, a capacity-headroom binding case, an adjacency tie-break case, fill day-cap accounting, multi-sensor summing, scalar-state and beyond-horizon fallbacks, a zero-power guard, slot length/overlap invariants, pinned thresholds in the helpers and shared-state restoration after the suite. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QiqN8TJpbXRsSkph6yNSx
The old guard tested a stale loop variable, so once the horizon's final window start had been booked every remaining window was rejected and the daily budget was silently truncated. With the guard keyed on the slot start, iboost_smart2 books the full 60 kWh/day cap (120 kWh, matching iboost_smart1 which runs without windowing) and iboost_smart3 books 110 kWh, both at the true 1.5x window-average rate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QiqN8TJpbXRsSkph6yNSx
…08#4756) - Boost within an interval now precedes its draw in the tank model, so a draw inside the current interval can be served by booking the current interval itself (the element runs while the water is drawn); the candidate scan includes the target interval. - The current, partially elapsed interval is gated and priced on the rate at minutes_now rather than at its start, so a rate step earlier in the interval cannot mis-gate or mis-price the bookable remainder. - The fill pass has its own import-rate rule (at or below the fill threshold) with the export and gas guards retained, so a tighter iboost_rate_threshold no longer silently blocks filling; fills stay within the fetched forecast horizon so the tariff compare's longer planning horizon cannot book fills the live plan would never see. - Residual bookings below one 5-minute step of element energy are dropped instead of floored up to a full-power 5-minute slot, so delivered and published energy agree. - Shared helpers replace the duplicated logic: iboost_rate_okay() (rate and gas eligibility for both planners and the fill pass), iboost_plan_total_days() and dp3-aligned day-usage bookkeeping, and quantise_slot_minutes() (also used by the legacy iBoost and car charging planners). - Hot-loop cost per dose reduced to one fused trajectory/running-max pass bounded at the target plus an incrementally maintained day ledger; the fill pass only recomputes state when a fill is actually applied. - A forecast that no longer aligns with the plan grid logs a warning instead of silently planning nothing. Tests: current-interval draws (aligned and mid-interval), independent fill threshold, fill resume after a draw under the boost-before-draw ordering, and updated expectations for the trimmed-fill scenario. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QiqN8TJpbXRsSkph6yNSx
…2008#4756) - The load_forecast and iboost_forecast ingestion now share one fetch_cumulative_forecasts() helper (entity unwrap, $attribute split, format conversion and validation, minute_data call, per-source raw extent), so a format Predbat learns to read for load is read for hot water demand too. The non-list guard now protects both paths and the fetch-exception severity is unified on Warn. - Demand deltas are taken per source with get_from_incrementing (the exact load-forecast reading) and only within each source's own raw extent: a series with an internal reset no longer books phantom demand where minute_data's tail back-fill rejoins the data, a series starting mid-horizon no longer books its first sample as a draw, and one source's tail can no longer mask another source's draw. - Staleness is judged only from sources that actually loaded, so a recent but unusable sensor cannot vouch for a stale one, and a fresh series carrying no future increments now falls back to the legacy plan with a warning instead of silently planning nothing. - A configured but unreadable iboost_tank_soc sensor falls back to the legacy plan for the cycle instead of assuming an empty tank, which could book the whole tank capacity at real import prices and flap the plan when the sensor recovers. Tests: meter-reset phantom demand, mixed stale/unusable sources, and a flat zero-increment series. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QiqN8TJpbXRsSkph6yNSx
When minutes_now is not a multiple of plan_interval_minutes (the normal case in production, where the plan is rebuilt every 5 minutes), the window containing minutes_now clipped its first sub-slot to minutes_now and then stepped off-grid, while other windows stepped on-grid - so overlapping iboost_smart_min_length windows could book overlapping slots, and in_iboost_slot() drops the second slot's energy from the prediction. Booking now iterates the plan-interval grid with the first sub-slot clipped to minutes_now and the duplicate guard keyed on the grid cell; behaviour with an aligned minutes_now is unchanged (the existing goldens prove it). Adds the off-grid overlapping-window test with minutes_now=735 and min_length=60. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QiqN8TJpbXRsSkph6yNSx
…ass) Both iBoost suites now snapshot the fixture's rate dicts and the scalars rate_scan/rate_scan_export derive from them before running and restore them in a finally block, so set_rate_profile's custom profiles (and a mid-suite abort) can no longer leak into later tests. The iBoost configuration is restored from its config defaults with the rate thresholds back at the reset() default of 9999 and iboost_max_power from the fixture's own args rather than a hard-coded wattage. The gas test restores the exact prior rate_gas/iboost_gas state instead of clearing it, and the per-test helper restores its state in a finally block. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QiqN8TJpbXRsSkph6yNSx
…uards State precisely when the forecast planner runs and takes over slot selection (including with iboost_smart On alongside the solar or battery modes) and that only iboost_smart Off in those modes leaves a configured forecast without effect; note in both pages that iboost_smart_min_length does not apply to forecast-driven slots; and document that the fill threshold replaces the import rate threshold for fill slots while the export and gas guards still apply. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QiqN8TJpbXRsSkph6yNSx
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QiqN8TJpbXRsSkph6yNSx
The tariff compare raises forecast_minutes to 48h after fetch_sensor_data has run, so a demand grid built over the live forecast_minutes read as zero for the extended horizon and compared tariffs saw different iBoost demand. The grid now covers the whole fetchable horizon (forecast_days + 1 days) and the raw data extent is recorded separately (iboost_forecast_extent) so the fill pass stays clamped to real data rather than the grid end. Test helpers reset the extent alongside the forecast they inject. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QiqN8TJpbXRsSkph6yNSx
570be80 to
4d2f7ea
Compare
A trailing entry with a parseable timestamp but an unusable value (e.g. a template sensor point still 'unavailable') extended a source's raw extent past its last usable sample, letting the back-fill junction book phantom demand, widening the fill horizon and letting a junk-valued source vouch for a stale one at the staleness gate. The extent loop now applies the same item validity rules as minute_data (numeric value required), with a regression test for the trailing-junk case. Also restores the correctly named rate_export_average (and the min/max minute markers) in the test rate-state snapshot - the previous list held a name that never matched - and logs a truthful message when forecast demand exists only beyond the planning horizon rather than diagnosing a grid misalignment. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QiqN8TJpbXRsSkph6yNSx
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QiqN8TJpbXRsSkph6yNSx
…d-forecast-i9lsss
…restore low_rates An instrumented replay of the pre-fix planner showed the old duplicate guard never rejected further windows in the smart2/3 scenarios: the plan was fully booked (120/110 kWh) with duplicate slot starts, and the 65/60 kWh shortfall came from in_iboost_slot() crediting only the first slot covering a minute - the scenario comment now states that mechanism. Adds a blocking-direction test for the fill pass's forecast-extent clamp (deleting the clamp previously left every suite green) and corrects the fill-trim comment that credited the clamp for a capacity-driven stop. The rate-state snapshot now also restores low_rates, which the smart suite derives from its own test rates. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016QiqN8TJpbXRsSkph6yNSx
Summary
Predbat can optionally plan iBoost energy from a 30-minute hot-water demand forecast and the tank's current state of charge, treating the tank as a charge-only store, so heating is scheduled before each draw and stops once the forecast is covered. The feature is off unless
iboost_forecastis configured inapps.yaml; existing behaviour is unchanged otherwise, and a regression test enforces that the legacy path produces identical output when no forecast is loaded.Problem
iboost_max_energyis a per-calendar-day scalar; the smart planner books the cheapest slots without knowing when hot water is drawn or how much energy the tank already holds, so intraday shortfalls are served reactively at uncontrolled rates and full budgets are booked on days the cylinder is already hot (#1424). Users with SoC-reporting tanks rebuild the missing logic as template sensors plus an automation rewriting the budget intraday, which races the replan cycle around midnight.Configuration
apps.yaml:Home Assistant entities (created under
iboost_enable):input_number.predbat_iboost_tank_capacity— usable stored energy, kWh (default 10)input_number.predbat_iboost_tank_reserve— minimum stored energy to hold before each draw, kWh (default 0)input_number.predbat_iboost_fill_rate_threshold— fill remaining headroom in any slot whose import rate is at or below this value, p/kWh (default −99, disabled). Implemented as a separate commit so it can be dropped in review without touching the rest.A check on the configuration shape and naming would be appreciated.
Behaviour
When
iboost_forecastis configured:fetch_iboost_forecast()(modelled onfetch_extra_load_forecast()) reads the configured sensors, scales the cumulative series byiboost_forecast_scalingand converts it into demand per plan interval as the per-minute positive delta (the same reading the load forecast gets), logging a one-line summary. If the sensor is missing, unavailable, stale or yields no data, a warning is logged and the unchanged legacy planner runs for that cycle.clamp(iboost_tank_soc / 100 × capacity, 0, capacity)when the SoC sensor is configured and readable, else 0 so the whole forecast is provisioned.iboost_rate_threshold,iboost_rate_threshold_export, the gas thresholds, the element power, the per-calendar-dayiboost_max_energy − iboost_todaycap and the capacity headroom), preferring intervals adjacent to already-booked ones on price ties so boosts consolidate into longer runs, and repeating until the draw is covered. If no eligible interval remains a warning names the uncovered interval and energy and planning continues.iboost_smarton); when configured it takes over slot selection in those modes, including the time-ordered plan used wheniboost_smartis off.iboost_planstructure (start,end,kwh,average,cost), sorted by time with no duplicate starts, soin_iboost_slot(), the plan card,binary_sensor.predbat_iboost_activeand the export-window clash checks are unchanged.averageandcostuse each slot's own import rate. The level trajectory is logged at debug level and a one-line summary at info level.The first commit is a separable two-line fix to
plan_iboost_smart()this feature depends on: the sliding-window average priced every window from its first interval only, and the duplicate-slot guard tested a stale loop variable so overlapping windows could double-book a slot start. There is no separate PR for it; it is included here as its own commit with its own test.Scope
Planner only. No change to
run_prediction()or the C++ kernel, no parity revision, no binary rebuild; both prediction engines consume the plan through the existingin_iboost_slot()path. Carrying tank state into the prediction loop (so the solar and charging modes respect headroom) is a possible follow-up, deliberately excluded, as is publishing the modelled per-slot tank level (the iBoost plan slots are not currently published as an entity, so there is no attribute to attach it to).Alternatives considered
External scaffolding: template sensors deriving a remaining-demand estimate, an automation rewriting
iboost_max_energyintraday, or setpoint automations on the diverter. A scalar budget cannot express when the energy is needed, so heating still lands in the cheapest slots of the day regardless of draw timing, cannot span a multi-day horizon, and the intraday rewrite races the replan cycle around midnight.Forecast source
load_forecastsets the precedent for ingesting an external forecast series (Predheat, PredAI). A producer exists: ML Forecast Lab (disclosure: the author is me), an open-source Home Assistant app that benchmarks state-of-the-art ML forecasting backends on the user's own sensor history and publishes calibrated forecasts as HA sensors in theload_forecastattribute format. For Mixergy tanks the demand series comes from the tank's own charge-drop meter, so it represents actual draw rather than heating energy.Testing
New cases in
apps/predbat/tests/test_iboost.py(registered asiboost_forecast, with the bug-fix case underiboost_smart), all on non-flat rate profiles:Commands run:
./run_pre_commit(all hooks pass) and./run_all --quick(all tests pass), plus./run_all --test iboost_smart --test iboost_forecast.Known limitation: the tariff compare feature raises
forecast_minutesto 48h after sensor data is fetched, so withforecast_hoursconfigured below 48 the comparison sees demand only over the shorter horizon; a follow-up could rebuild the forecast grid insiderecompute_iboost().Related
plan_iboost_smart()window-pricing/duplicate-slot fix is included as the first commit of this PR (separable).