Skip to content

fix(axle): boost the import rate during an Axle export event - #4520

Merged
springfall2008 merged 1 commit into
mainfrom
fix/axle-export-event-import-rate-boost
Aug 14, 2026
Merged

fix(axle): boost the import rate during an Axle export event#4520
springfall2008 merged 1 commit into
mainfrom
fix/axle-export-event-import-rate-boost

Conversation

@springfall2008

Copy link
Copy Markdown
Owner

Summary

  • An Axle export event pays a premium to export power, so charging (importing) instead during the same window carries the same opportunity cost. load_axle_slot only ever boosted rate_export for these events, leaving rate_import completely 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_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 for the same event.
  • Managed-mode's price curve conversion (_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.
  • Added a doc note under Axle VPP in docs/energy-rates.md explaining the boost applies to both rates during an event.

Test plan

  • New unit test _test_axle_load_slot_export_boosts_import_rate — verifies load_axle_slot raises rate_import for an export-direction session.
  • New unit test _test_axle_byok_export_event_boosts_import_rate — end-to-end via fetch_axle_sessions + load_axle_slot, confirms a single BYOK export event boosts both rate_import and rate_export with no synthetic duplicate session needed.
  • Updated managed-mode tests (managed_price_curve, managed_fetch_e2e, managed_token_retry) for the new single-session-per-slot price curve output.
  • Full axle test 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

…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>
Copilot AI lite review requested due to automatic review settings August 14, 2026 12:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_kwh to both import and export rates in load_axle_slot.
  • Simplify managed-mode _process_price_curve to 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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_sessions can include up to 7 days of history (see cleanup_event_history), so minutes_to_time() can yield negative start_minutes/end_minutes for sessions that started before midnight_utc. Now that import_export == "export" sessions also mutate rate_import (export=False call), the current loop can create negative minute keys in rate_dict/rate_replicate (because it uses rate_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

@springfall2008
springfall2008 merged commit 21b7726 into main Aug 14, 2026
4 checks passed
@springfall2008
springfall2008 deleted the fix/axle-export-event-import-rate-boost branch August 14, 2026 13:42
dandwhelan added a commit to dandwhelan/batpred that referenced this pull request Aug 14, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants