Skip to content

Migrate EV/Wallbox to site-scoped v2 endpoints (fixes Enphase 30401) - #7

Merged
mrebbert merged 5 commits into
mainfrom
feature/site-scoped-ev-wallbox
Sep 5, 2026
Merged

mrebbert merged 5 commits into
mainfrom
feature/site-scoped-ev-wallbox

Conversation

@mrebbert

@mrebbert mrebbert commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Problem

get_wallboxes() (and by extension get_ev_chargers()) hits HTTP 422 error_code 30401 "DeviceGateway not found" on systems without a GridX device gateway. Confirmed live for an Enphase-based user (2026-09-04 diagnostics): the legacy system-scoped routes are coupled to the GridX DeviceGateway record, and non-GridX setups can't be served.

Solution

Migrate the three affected endpoints to the modern site-scoped equivalents that the 1KOMMA5° app itself uses (confirmed via HAR analysis of my own GridX system + the Enphase-user HAR):

  • GET /api/v1/systems/{id}/devices/evs → GET /api/v2/sites/{id}/assets/evs
  • GET /api/v1/systems/{id}/devices/ev-chargers → GET /api/v1/sites/{id}/assets/ev-chargers
  • PATCH /api/v1/systems/{id}/devices/evs/{eid} → PATCH /api/v2/sites/{id}/assets/evs/{eid}

The site-scoped path is universal — works on both GridX-native and non-GridX setups, and matches what the vendor app has moved to.

Public-API impact

  • Method signatures unchanged.
  • EVCharger.primary_schedule_departure_soc() now aliases target_soc(). The v2 API consolidated the two former separate SoC fields into a single value (user-confirmed via app UI showing one departure-SoC field). HA sensors keep working with correct semantics — no unavailable state.
  • Wallbox.gridx_hardware_id returns None for all users; the new canonical wallbox identifier is Wallbox.id. The HA integration only reads gridx_hardware_id as a presence boolean via getattr(..., None), so nothing breaks.
  • Reader methods for v1-only fields (safety_range_km, updated_at, charging_mode_updated_at, secondary_schedule_*, primary_schedule_days) return None/[] — v2 payload does not carry them.
  • capacity_wh() normalises kWh ↔ Wh (v2 payload uses either unit user-dependently).

Verification

  • 345 tests passing (was 343 + 2 new regression tests), ruff clean, Postman JSON valid.
  • Live-verified on GridX system (2026-09-05): all reads return correct values, primary_schedule_departure_soc() == target_soc() fallback holds, Wallbox.id populated, gridx_hardware_id None, setter round-trip OK.
  • Live-Enphase-user verification pending; expected: get_wallboxes() returns the Enphase-Wallbox instead of raising RequestError.

Test plan

  • Unit test suite green
  • Ruff clean
  • Postman JSON valid
  • Live smoke on GridX system
  • Live confirmation from Enphase-based user after release

Release plan

Bump to v0.2.0 (major) after merge. Signals the internal endpoint migration and new Wallbox.id field. HA integration (mrebbert/1komma5-ha) needs a follow-up release to bump its constraint from <0.2 to <0.3.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RgkYaQACLxezeceA1hrXQs

mrebbert and others added 5 commits September 5, 2026 08:43
Preparation for the endpoint migration to /api/v1/sites/{id}/assets/ev-chargers.
The site-scoped response returns the wallbox identifier under `id`; the legacy
system-scoped path returned it as `gridxHardwareId`. Both fields co-exist on
the dataclass so the from_dict() code path stays symmetric during the transition.

After the endpoint migration lands, `gridx_hardware_id` will be `None` for
every user (site-scoped payload does not surface it). HA integration only
reads it as a presence-boolean via getattr fallback, so no consumer breaks.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RgkYaQACLxezeceA1hrXQs
Enphase-based systems (no GridX device gateway) hit HTTP 422 error_code
30401 "DeviceGateway not found" on the legacy system-scoped routes:
- GET  /api/v1/systems/{id}/devices/evs
- GET  /api/v1/systems/{id}/devices/ev-chargers
- PATCH /api/v1/systems/{id}/devices/evs/{eid}

The 1KOMMA5° app itself uses the site-scoped equivalents on all
systems (verified live for the GridX author account + Enphase-user HAR
+ 2026-09-05 own-app HAR). Migrating the SDK to the same routes fixes
the Enphase HTTP 422 and stays on the trajectory the vendor is
maintaining.

Payload-shape changes handled inside EVCharger:

- Reader getters (name/manufacturer/model/capacity/chargingMode/
  targetSoc/defaultSoc/manualSoc/manualSocTimestamp/chargerId/
  departureTime/minChargingCurrent) now read from the flat top-level
  fields instead of the nested profile/chargeSettings blocks.
- capacity_wh() normalises kWh<->Wh (the site-scoped payload uses
  either unit user-dependently; still returns Wh consistently).
- assigned_charger_id() reads chargerId (site-scoped name).
- Fields the site-scoped payload no longer surfaces return None:
  safety_range_km, updated_at, charging_mode_updated_at,
  secondary_schedule_departure_time/soc. primary_schedule_days
  returns [] (no schedule-day field either).
- primary_schedule_departure_soc() now returns target_soc(); v2 API
  consolidated the two fields (user-confirmed via app UI showing a
  single departure-SoC value). HA consumers see the same value they
  saw before, semantically consistent with the vendor UI.

Setter bodies converted from nested chargeSettings.* to flat top-level
keys (chargingMode/manualSoc/targetSoc/departureTime). Verified live
that the site-scoped endpoint accepts both partial-flat and legacy
nested bodies; picking flat matches the app's own convention.

Wallbox model: `id` (new canonical identifier) is populated from the
site-scoped payload; `gridx_hardware_id` stays declared for
backwards-compatible presence-check callers but is now always None.

Live-verified round trip on GridX system:
- get_ev_chargers() → 1 EVCharger, correct manufacturer/model/capacity
- get_wallboxes() → 1 Wallbox with id set, gridx_hardware_id None
- set_target_soc round-trip (0.8→0.9→0.8) with PATCH bodies matching
  the app's minimal flat form

Tests updated in a follow-up commit.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RgkYaQACLxezeceA1hrXQs
- Fixtures (make_ev_data, make_wallboxes_data) now return the flat
  site-scoped v2 shape with the same values existing tests already
  expect. New capacity_unit parameter on make_ev_data lets tests
  exercise the kWh→Wh normalisation path.
- Test URLs bumped: v2/sites/assets/evs (EV chargers), v1/sites/assets/
  ev-chargers (wallboxes); _SITE_BASE_V1/V2 constants added.
- Setter body assertions now check the flat top-level keys the SDK
  sends (chargingMode, targetSoc, manualSoc, departureTime) instead
  of the deprecated nested chargeSettings.* form.
- New regression tests:
  - test_capacity_wh_normalizes_kwh_unit
  - test_primary_schedule_departure_soc_aliases_target_soc
  - test_gridx_hardware_id_is_none_on_site_scoped_response
  - test_safety_range_km/updated_at/charging_mode_updated_at/
    primary_schedule_days/secondary_schedule_* return None on v2
- CLI cmd_wallboxes prints Wallbox.id (canonical) instead of
  gridx_hardware_id (now always None post-migration).

Full suite: 345 passing, 0 failing, ruff clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RgkYaQACLxezeceA1hrXQs
- API.md URLs updated in place: /devices/evs → /assets/evs (v2),
  /devices/ev-chargers → /assets/ev-chargers (v1). Notes about
  capacity unit variance (Wh vs kWh) added to the unit table.
- Postman collection: all 6 EV/Wallbox request URLs and path arrays
  rewritten; wallbox description mentions the id/gridxHardwareId change
  and the Enphase-compatibility rationale.
- Memory endpoints_reference: matching row updates with the migration
  note (kept in the user's Claude memory dir, not the repo).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RgkYaQACLxezeceA1hrXQs
The v2 site-scoped API has a single departure-time slot per vehicle
(v1 had a primary/secondary distinction that never populated for
regular users). Cosmetic cleanup so nothing in the user's view still
says 'primary':

- CLI subparser help: 'Set EV departure time' (was 'Set EV primary
  departure time'); print output drops 'primary'; docstring updated.
- EVCharger.set_primary_departure_time / primary_schedule_departure_time
  docstrings now describe them as 'the scheduled departure time' and
  explain the historical name.
- Error label 'Failed to set primary departure time' → 'Failed to set
  departure time' (single-slot semantics).
- CLI 'ev' output now prints the departure line based on whether
  departure_time is set, not on the (now always-empty)
  primary_schedule_days.
- API.md List-EV response example rewritten to match the actual v2
  flat shape (top-level chargingMode/targetSoc/departureTime/
  manualSoc/defaultSoc/chargerId), with a note explaining that
  targetSoc and primaryScheduleDepartureSoc were consolidated.
- API.md Update-EV curl examples switched from nested chargeSettings.*
  bodies to the flat form the app and SDK send; departure-time example
  renamed and body is {"departureTime": "07:30"}.
- API.md wallbox response example uses `id` (was `gridxHardwareId`),
  with a note about the legacy field and the Enphase-fix motivation.

SDK method names (set_primary_departure_time,
primary_schedule_departure_time, primary_schedule_departure_soc) are
retained unchanged — HA integration and any other callers keep
working. Method-name cleanup can come in a later release if desired.

345 tests passing, ruff clean, Postman JSON valid.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RgkYaQACLxezeceA1hrXQs
@mrebbert
mrebbert merged commit d20c469 into main Sep 5, 2026
5 checks passed
@mrebbert
mrebbert deleted the feature/site-scoped-ev-wallbox branch September 7, 2026 08:15
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