fix(axle): boost the import rate during an Axle export event - #4520
Conversation
…t export An Axle export event pays a premium to export, so charging instead during the same window carries the same opportunity cost - but load_axle_slot only ever boosted rate_export, leaving rate_import untouched. Predbat could therefore plan to charge cheaply through an event just because the normal import rate happened to be low at that time, with no cost reflected in the final plan. load_axle_slot now applies an export-direction session's pence_per_kwh to both rate_export and rate_import, mirroring how Octopus saving sessions already boost both directions. Managed mode's price curve conversion no longer needs to fabricate a second, negated "import" session per slot to get the same effect, so that trick is removed too. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR updates Axle VPP event handling so that export-direction events increase both rate_export and rate_import, ensuring Predbat models the opportunity cost of charging during premium export windows. It also simplifies managed-mode price curve processing by removing the need to generate synthetic “import” sessions per slot, since a single export-direction session now impacts both directions.
Changes:
- Apply export-session
pence_per_kwhto both import and export rates inload_axle_slot. - Simplify managed-mode
_process_price_curveto emit one export-direction session per slot (no negated import companion). - Add/adjust Axle unit tests and document the new “boost both rates” behavior.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| docs/energy-rates.md | Documents that Axle export events boost both import and export rates to reflect opportunity cost. |
| apps/predbat/axle.py | Updates load_axle_slot export-session handling to raise both rates; simplifies managed price-curve session generation. |
| apps/predbat/tests/test_axle.py | Adds regression tests for import-rate boosting during export events and updates managed-mode expectations for single-session-per-slot. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (1)
apps/predbat/axle.py:668
axle_sessionscan include up to 7 days of history (seecleanup_event_history), sominutes_to_time()can yield negativestart_minutes/end_minutesfor sessions that started beforemidnight_utc. Now thatimport_export == "export"sessions also mutaterate_import(export=False call), the current loop can create negative minute keys inrate_dict/rate_replicate(because it usesrate_dict.get(minute, 0)), which can bloat these dicts and make debugging/replication data confusing.
Clamp the window to non-negative minutes (and skip empty windows) before iterating.
if start_minutes is not None and end_minutes is not None and start_minutes < (base.forecast_minutes + base.minutes_now):
if import_export == "export":
# An export event pays a premium to export, so charging instead during the same
# window carries the same opportunity cost - apply the same boost to both the
# export and the import rate (mirrors how Octopus saving sessions boost both
Brings in 65 upstream commits on top of the Pi's local work. 43 non-test source files merged with no conflict at all; the six that did are resolved so both sides survive rather than one overwriting the other: - axle.py: upstream's springfall2008#4520 fix (an export event boosts the import rate too, since charging during the window carries the same opportunity cost) kept, wrapped in this fork's guard that only applies minutes the rate data still covers, so a session that has scrolled out is not re-applied every run. - inverter.py: upstream's two-source readback (Control.Discharge_Target_SOC_1 first, raw.invertor as fallback) and its "unreadable register is left alone" guard, plus this fork's rest_write_acknowledged() fallback for hardware where the register is absent from GivTCP's read map entirely and no readback can ever confirm a write that did land. The two cases are distinct and both are now handled. - predbat.py: fork version string and watchdog constants kept. - annual.py: upstream's baseline_tariff threading applied to BOTH the gas and the heat pump leg - the two legs' no_pvbat scenarios are differenced against each other, so pricing one on the baseline tariff and the other on the main tariff would bias the heating answer. - web_annual.py: upstream wins on the Octopus load-source prefill. Its reasoning applies directly here: the import meter already has PV and battery self-consumption subtracted, so modelling a system on top double-counts. The predheat heating prefill, which upstream has no equivalent of, is kept. - prediction_kernel: ABI/parity bumped to 103/104, past this fork's 102/103 and upstream's 3/5, because both sides changed the context struct and the hot loop. Any binary built before this merge is now rejected at load time and falls back to the Python engine rather than being silently trusted. annual_heat.py is added because the Pi's annual.py and web_annual.py import it and it existed only on feat/annual-heat-pump - without it the tree does not import at all. Verified: merged kernel compiles under -Wall -Werror and PASSES parity across 450 configurations (nominal/pv10/pv90); unit_test.py --quick is 224 passed, with the one pre-existing db_manager corruption failure that reproduces identically on main without this merge. prediction_kernel_lib_aarch64.so is rebuilt from the merged source (needs GLIBC 2.32, container has 2.43). The other five architectures need zig, so they stay stale until CI's kernel-binaries job rebuilds them - note that workflow is on: pull_request, so it does not run on a push to main. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Summary
load_axle_slotonly ever boostedrate_exportfor these events, leavingrate_importcompletely untouched — Predbat could plan to charge cheaply through an export event just because the normal import rate happened to be low at that time, with no cost reflected in the final plan/figure.load_axle_slotnow applies an export-direction session'spence_per_kwhto bothrate_exportandrate_import, mirroring how Octopus saving sessions already boost both directions for the same event._process_price_curve) no longer needs to fabricate a second, sign-negated "import" session per half-hourly slot to get the same net effect — that trick is removed, since a single export-direction session now covers both sides of the meter. Net rate impact for managed mode is unchanged.docs/energy-rates.mdexplaining the boost applies to both rates during an event.Test plan
_test_axle_load_slot_export_boosts_import_rate— verifiesload_axle_slotraisesrate_importfor an export-direction session._test_axle_byok_export_event_boosts_import_rate— end-to-end viafetch_axle_sessions+load_axle_slot, confirms a single BYOK export event boosts bothrate_importandrate_exportwith no synthetic duplicate session needed.managed_price_curve,managed_fetch_e2e,managed_token_retry) for the new single-session-per-slot price curve output.axletest suite: 32/32 passed../run_all --quick: all tests passed, no regressions../run_pre_commit: all hooks passed (ruff, black, cspell, etc).🤖 Generated with Claude Code