Skip to content

feat: add managed VPP mode to Axle Energy integration#3552

Open
mgazza wants to merge 4 commits intomainfrom
feat/axle-managed-vpp-mode
Open

feat: add managed VPP mode to Axle Energy integration#3552
mgazza wants to merge 4 commits intomainfrom
feat/axle-managed-vpp-mode

Conversation

@mgazza
Copy link
Collaborator

@mgazza mgazza commented Mar 11, 2026

Summary

  • Extends the Axle Energy component to support managed VPP mode alongside the existing BYOK mode
  • Managed mode uses partner OAuth credentials to fetch half-hourly wholesale price curves from /entities/site/{id}/price-curve, converting GBP/MWh to p/kWh and creating both export and import sessions per timeslot
  • Adds required_or: ["api_key", "managed_mode"] to component registration — the component only activates when at least one mode is configured, fixing a previous bug where setting api_key to non-required caused activation for all customers

Changes

  • axle.py: Add _get_partner_token(), _fetch_managed_price_curve(), _process_price_curve() methods; split fetch_axle_event() into dispatcher + _fetch_byok_event(); add allow_future param to add_event_to_history() for price curves; dedup by start_time AND import_export direction; add 5xx retry with backoff, fail immediately on 4xx
  • components.py: Change api_key from required: True to required: False; add managed mode args (managed_mode, site_id, partner_username, partner_password, api_base_url); add required_or
  • test_axle.py: Add 6 managed mode tests (initialization, price curve processing, event dedup, future events, publish attributes, disabled without credentials)

Test plan

  • All 26 axle tests pass (20 existing + 6 new managed mode)
  • Verify component doesn't activate without config (no api_key, managed_mode=False)
  • Verify BYOK mode still works (set api_key, no managed params)
  • Verify managed mode activates correctly (set managed_mode=True + site_id + creds)

🤖 Generated with Claude Code

mgazza and others added 4 commits March 11, 2026 12:21
Extend the Axle Energy component to support two modes:

- BYOK (existing): User's own API key with /vpp/home-assistant/event
- Managed VPP (new): Partner credentials with /entities/site/{id}/price-curve

Managed mode authenticates via partner API (OAuth token cached 50min),
fetches half-hourly wholesale prices (GBP/MWh), converts to p/kWh,
and creates both export and import sessions per timeslot.

Key changes:
- axle.py: Add managed mode init params, _get_partner_token(),
  _fetch_managed_price_curve(), _process_price_curve() methods.
  Split fetch_axle_event() into dispatcher with _fetch_byok_event().
  Add allow_future param to add_event_to_history() for price curves.
  Dedup events by start_time AND import_export direction.
  Add 5xx retry with backoff, fail immediately on 4xx.
- components.py: Change api_key to required=False, add managed mode
  args, add required_or=["api_key", "managed_mode"] to prevent
  activation without config.
- test_axle.py: Add 6 managed mode tests (init, price curve,
  dedup, future events, publish attrs, disabled without creds).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Validate access_token is truthy before caching in _get_partner_token()
  to prevent caching None with a 50-minute expiry when the auth response
  is 200 but missing the token field.

- Add 3 async HTTP tests for managed mode:
  - _test_axle_managed_get_partner_token: token fetch, caching, missing
    access_token, and auth failure
  - _test_axle_managed_fetch_end_to_end: full auth -> price curve ->
    session creation flow
  - _test_axle_managed_token_retry: token invalidation, re-auth, retry
    on price curve failure, and complete failure path

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@mgazza
Copy link
Collaborator Author

mgazza commented Mar 11, 2026

Code Review: feat: add managed VPP mode to Axle Energy integration

4 files changed | +708 / -19 | 4 commits (2 authored + 2 pre-commit CI)

Overview

Extends the Axle Energy VPP integration with a second operating mode ("managed VPP") alongside the existing BYOK mode. Managed mode authenticates via partner OAuth, fetches half-hourly wholesale price curves, and converts them into import/export sessions. Component registration updated with required_or gating to prevent spurious activation.


Correctness

  • Price conversion math is correct: GBP/MWh ÷ 10 = p/kWh. Export uses raw value, import negates it (because load_axle_slot subtracts import pence). Well-documented in comments.

  • Token validation is solid: Catches the case where auth returns 200 but access_token is missing/falsy — prevents caching None with a 50-minute TTL.

  • Token retry logic is correct: On price curve failure, invalidates cached token, re-auths, retries once. Double failure correctly bumps failures_total.

  • Dedup logic works: start_time + import_export correctly distinguishes the two sessions per timeslot that managed mode creates.

  • required_or gating: Prevents activation unless at least one of api_key or managed_mode is set. Solves the original bug where all alpha customers got the component.

Potential Issues

  1. _request_with_retry used for price curve — 4xx returns None, triggers re-auth path
    This is actually intentional and correct: a 401 from the price curve endpoint returns None, which triggers the token invalidation + retry in _fetch_managed_price_curve. But a 400 (bad request, e.g. invalid site_id) would also trigger unnecessary re-auth. Minor — unlikely in practice since site_id is static config.

  2. Event history linear scan (add_event_to_history):
    Each call scans all history to check for duplicates. With 48 half-hourly slots × 2 directions = 96 events per fetch, and history up to 7 days (potentially ~960 entries), worst case is ~92K comparisons per fetch cycle. Not a problem at this scale but worth noting if history ever grows.

  3. Current event selection (_process_price_curve):
    The "find active export slot" loop iterates self.event_history which isn't sorted. Works correctly since it breaks on first match, and price curve slots are added chronologically. But if history order ever changes (e.g. after dedup updates), this could pick the wrong slot.

  4. Hardcoded 30-minute slot duration (end_dt = start_dt + timedelta(minutes=30)):
    Assumes price curve always has half-hourly slots. If Axle ever changes granularity, this breaks. Fine for now — the endpoint is literally called half_hourly_traded_prices.

Test Coverage

29 tests total, 9 new managed mode tests — solid coverage:

  • Unit tests: init validation, price conversion, dedup, future events, publish attrs, creds fallback
  • Async HTTP tests: token fetch/cache/missing/failure, end-to-end flow, token retry with re-auth

One gap: No test for the _request_with_retry 5xx backoff path specifically (the new elif response.status >= 500 branch). The existing BYOK tests may cover it via the retry tests, but a dedicated test for the 5xx vs 4xx split would be ideal.

Verdict

Looks good to merge. The implementation is clean, well-tested, and correctly handles both operating modes. The required_or gating fixes the original activation bug. No security concerns — credentials flow through config params, not hardcoded. The token retry logic is robust with proper invalidation.

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.

1 participant