Skip to content

Fix Hyundai Europe commands on legacy (non-CCS2) vehicles#32

Open
dz0ny wants to merge 1 commit into
schmidtwmark:mainfrom
dz0ny:fix/hyundai-eu-legacy-command-flow
Open

Fix Hyundai Europe commands on legacy (non-CCS2) vehicles#32
dz0ny wants to merge 1 commit into
schmidtwmark:mainfrom
dz0ny:fix/hyundai-eu-legacy-command-flow

Conversation

@dz0ny

@dz0ny dz0ny commented May 26, 2026

Copy link
Copy Markdown

Problem

Sending commands (lock/unlock/charge) to legacy, non-CCS2 Hyundai Europe vehicles (e.g. a gen2 Bayon) fails with "Failed to get command token" / Lock … failed.

Root cause

EU commands authenticate differently depending on vehicle generation, per the authoritative hyundai_kia_connect_api (ApiImplType1.lock_action / start_charge / …):

Legacy (non-CCS2) CCS2 (Gen5W)
URL POST /api/v1/spa/…/control/door POST /api/v2/spa/…/ccs2/control/door
Body {action, deviceId} {command}
Auth normal access token PIN-derived control token

sendCommand called setCommandToken() (the PUT /api/v1/user/pin step) unconditionally, but legacy cars have no PIN/control-token step — so that fetch is what threw "Failed to get command token". It also built a malformed path because commandPathAndBody() defaulted to ccs2: true while the URL was built as v1.

This is the "wrong token usage on legacy command path" anomaly noted in #8.

Changes

  • sendCommand: branch on ccs2 — control token + commandHeaders for CCS2; plain authorizedHeaders (no PIN) for legacy. Pass the real ccs2 flag into commandPathAndBody.
  • commandPathAndBody: drop the stray leading slash in the legacy door path ("/control/door""control/door") that produced a double slash.
  • setCommandToken: route through performJSONRequest so the PIN request is captured in HTTP logs and status-validated (previously a raw URLSession call, invisible to diagnostics), with a clearer PIN-failure message.

Testing

  • swift build passes.
  • Verified the legacy lock/unlock/charge paths, bodies, and header choice against hyundai_kia_connect_api ApiImplType1.
  • Live verification needs a real EU vehicle: lock should now POST to /api/v1/spa/…/control/door and return 200.

Known remaining gap (not addressed here)

startClimate still always returns the ccs2/control/temperature path regardless of the flag; legacy climate-start needs a dedicated control/temperature branch with the reference's hvac payload.

Refs #8

🤖 Generated with Claude Code

EU commands authenticate differently by generation. CCS2 (Gen5W) cars
use a PIN-derived control token and the /api/v2/.../ccs2/control/*
endpoints; legacy cars use the normal access token and the
/api/v1/.../control/* endpoints with no PIN step at all (per
hyundai_kia_connect_api's ApiImplType1).

sendCommand previously called setCommandToken() unconditionally, so
legacy vehicles failed with "Failed to get command token" — the PIN
endpoint isn't part of their flow. It also built a malformed path
because commandPathAndBody() defaulted to ccs2: true while the URL used
v1. This matches the "wrong token usage on legacy command path" issue
noted in schmidtwmark#8.

- sendCommand: branch on ccs2 — control token + commandHeaders for CCS2,
  plain authorizedHeaders (no PIN) for legacy; pass the real ccs2 flag
  into commandPathAndBody.
- commandPathAndBody: drop stray leading slash in the legacy door path
  ("/control/door" -> "control/door") that produced a double slash.
- setCommandToken: route through performJSONRequest so the request is
  logged and status-validated, and surface a clearer PIN-failure error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 26, 2026 09:13

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR updates the Hyundai Europe command flow to properly distinguish CCS2 (PIN/control-token) vehicles from legacy vehicles, and improves diagnostics by routing PIN verification through the shared request pipeline.

Changes:

  • Route the PIN/control-token request through performJSONRequest for consistent logging/status validation.
  • Only fetch a PIN-derived control token for CCS2 vehicles; use normal auth headers for legacy vehicles.
  • Fix legacy command paths to avoid double slashes in constructed URLs.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
Sources/BetterBlueKit/API/HyundaiEurope/HyundaiEuropeAPIClient.swift Reworks PIN/control-token fetching and command header selection based on CCS2 support; improves error messaging and logging consistency.
Sources/BetterBlueKit/API/HyundaiEurope/HyundaiEuropeAPIClient+Commands.swift Normalizes legacy command paths (removes leading /) to build correct request URLs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +202 to +203
let body: [String: Any] = [
"deviceId": configuration.deviceId ?? "",
Comment on lines +222 to +223
throw APIError(
message: "PIN verification failed — check that the account PIN is correct.",
@schmidtwmark

Copy link
Copy Markdown
Owner

This looks okay to me -- the AI comments are pretty sensible and should be addressed.

Have you confirmed this using bbcli on your own vehicle?

schmidtwmark pushed a commit that referenced this pull request May 28, 2026
Mirrors the three legacy-path bugs Nachtlatscher fixed for Hyundai EU
in #32 — flagged in the #29 thread after merge.

- Drop leading slash on lock/unlock door paths so the URL builder
  doesn't produce a double slash.
- Stop using the PIN-derived control token on legacy v1 endpoints.
  Legacy endpoints reject control-token auth with
  "400 Authorization field missing" and expect the regular access
  token. Gate the control-token flow on ccs2 and use
  authorizedHeaders for the legacy branch.
- Pass ccs2: ccs2 to commandPathAndBody. The call site relied on
  the default ccs2: true, so legacy vehicles got the CCS2 payload
  shape against a v1 URL.

Live-validated against a CCS2 EV9 by hard-coding ccs2 = false
locally: lock + unlock both return 200 / resCode 0000 and the
doors physically lock and unlock — confirming the Bluelink
backend routes legacy v1 and CCS2 endpoints to the same handler.

Co-Authored-By: Claude Opus 4.7 <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.

3 participants