Skip to content

Hyundai EU - Add support for EVs#25

Merged
schmidtwmark merged 3 commits into
schmidtwmark:mainfrom
Nachtlatscher:hyundai_eu
May 18, 2026
Merged

Hyundai EU - Add support for EVs#25
schmidtwmark merged 3 commits into
schmidtwmark:mainfrom
Nachtlatscher:hyundai_eu

Conversation

@Nachtlatscher
Copy link
Copy Markdown
Contributor

Changes Login to use refresh token.
Implementation to fetch data for EV Cars
Fix: #8

@gitguardian
Copy link
Copy Markdown

gitguardian Bot commented Apr 28, 2026

⚠️ GitGuardian has uncovered 1 secret following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

Since your pull request originates from a forked repository, GitGuardian is not able to associate the secrets uncovered with secret incidents on your GitGuardian dashboard.
Skipping this check run and merging your pull request will create secret incidents on your GitGuardian dashboard.

🔎 Detected hardcoded secret in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
- - Generic High Entropy Secret 3e3ceca Sources/BetterBlueKit/API/HyundaiEurope/HyundaiEuropeAPIClient.swift View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secret safely. Learn here the best practices.
  3. Revoke and rotate this secret.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@schmidtwmark
Copy link
Copy Markdown
Owner

Hey, thanks for sending this over! I'll review it later today.

@jeroenvisser101
Copy link
Copy Markdown

@schmidtwmark if there's any way to test a build with this in it, I can give it a try and give feedback.

@schmidtwmark
Copy link
Copy Markdown
Owner

You can use bbcli to test the API directly. swift run bbcli should show help information to get started

There may be some changes needed to bbcli to support these fixes

@Nachtlatscher
Copy link
Copy Markdown
Contributor Author

It is only a first draft where mainly the information fetch is working at least for my mj25 ioniq5.
I pushed it to my iphone via xcode and all i did till now seem to work.

Thats the current status tested with CCS2 endpoints:

  • lock/unlock -> working
  • car status including location -> working
  • Climate Control -> stop = should work but not tested; start = work in progress
  • Widget is showing miles instead of kilometers on iphone but in simulator on mac it is working.
  • Client secret is hardcoded in ApiClient and maybe should be moved out from there (nothing wild as python api is sharing it anyways)

Did i forget something?
My plan is also to show some more informations like „remaining energy“ converted to kWh.
But thats when it comes to real swift stuff and i am not really familar with it.
My Business is java development ;-)

Feel free to merge it already to provide the first draft also to others that may own an older model car.

@jeroenvisser101
Copy link
Copy Markdown

@Nachtlatscher so to get the refresh token, do I just inspect the network traffic of the stock Bluelink app and intercept the refresh token?

@Nachtlatscher
Copy link
Copy Markdown
Contributor Author

@Nachtlatscher so to get the refresh token, do I just inspect the network traffic of the stock Bluelink app and intercept the refresh token?

You need to do the login workaround of homeassitant project / phyton api to get the token.
In one of the issues here there is a link to
https://github.com/Puma7/KiaHyundaiToken

Maybe there is also some information about the process.

Copy link
Copy Markdown
Owner

@schmidtwmark schmidtwmark left a comment

Choose a reason for hiding this comment

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

Looks like a good start! Some general feedback:

I like the idea of having ResponseKey / ResponseKeyPath, but I think it could be much cleaner. Instead of having getKeyForPath(string, bool), you should get the correct ResponseKey -> String map once at the top of the response based on whether it needs CCS2 or not. Then, you can just do keyPathMap[.chargeTime] or whatever to get the full path.

The response keys should be moved into the API/HyundaiEurope directory at minimum, and named appropriately. They are not relevant to other brands/regions

My understanding here is that this requires the user to use a separate Python script to fetch their refresh token, which is then fed into the password field. Is there a reason that cannot be integrated into bbcli / the BetterBlue app directly? As the person who will have to respond to emails about this, I'm going to get a lot of confused and angry people if they have to run a Python script.

public func convert(_ temperature: Double, to targetUnits: Units) -> Double {
let convertedTemperature: Double

convertedTemperature = switch (self, targetUnits) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Let's reuse code here -- if you're going to add a convert function, then the format function should call it directly

public var regId: String, vin: String, model: String
public var accountId: UUID, fuelType: FuelType
public var generation: Int, odometer: Distance, vehicleKey: String?
public var ccs2Supported: Bool
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I think we need a better approach for handling region / brand specific options. Can we add an enum like:

 case hyundaiEurope(ccs2Supported: bool)
}```

Then I could make a separate PR for moving vehicleKey into a `case kiaUS` enum value

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

i currently rework that to a version you hopefully more happy with

public let accessToken: String
public let refreshToken: String
public let expiresAt: Date
public var deviceId: String
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

There's already a notion of DeviceID in the APIClientConfiguration. Why do we need it here as well?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes it is but Hyundai/Kia Europe requires to register the device against the API.
And the configuration value cant be updated.
I dont know if this registration has some kind of lifetime.

Currently i see two szenarios:

  1. give the possibility to update the deviceId in configuration
  2. on Account init i see the configuration and the client is created.
    Maybe it would be possible to call a "registerDevice()" with the created Api that saves the deviceId in "Account"
    -> each time a client is created the config would use this stable id then.
    -> config parameter still has to be editable because you first need to create the API to call the endpoint or return nil if its not implemented for that Api?

I tryed solution 2 but its not working
If i put in the function to ApiProtocolClient and write a default to prevent implementation for each Brand
it allways calls the default instead of EU Impl.
I don´t know why...

If you can provide the information how it should work or an other idea i would be happy.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Update i got it working now.
Maybe look again at current commit and tell me what you think about it

func commandPathAndBody(for command: VehicleCommand) -> (String, [String: Any]) {
func commandPathAndBody(for command: VehicleCommand, ccs2: Bool = true, deviceId: String)
-> (String, [String: Any]) {
switch command {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This would be easier to read if you split this into two functions -- one for ccs2, one for legacy. Or, switch on (command, ccs2)

Copy link
Copy Markdown
Owner

@schmidtwmark schmidtwmark left a comment

Choose a reason for hiding this comment

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

Thanks for the improvements, looking better!

self.expiresAt = expiresAt
}

public mutating func setDeviceID(_ deviceId: String) -> Self {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Do we still need this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

no it is not i forgot to delete it.

}
}

public enum VehicleMarketOptions: Codable, Equatable, Sendable {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Something I hadn't thought about when I suggested this: this is going to end up stored in SwiftData, which is notoriously bad at handling data structures more complicated than arrays. I think this is the right approach, and having Codable should make it work, but in my experience this could lead to to some weird crashes on startup. Just make sure to test the full app thoroughly by fully killing and and reopening it -- if you see crashes when it tries to read this value, then there's a problem.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

no crashes so far but i will do some more testing for sure.

}

// MARK: - default so set deviceId in configuration
public func registerDevice() async throws -> String? {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

It looks like this is is still creating a new deviceID, which should be handled by the Account in BetterBlue?

Copy link
Copy Markdown
Contributor Author

@Nachtlatscher Nachtlatscher Apr 30, 2026

Choose a reason for hiding this comment

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

I did not find a better solution.
EU Hyundai and Kia need that deviceId requested by api call -> done corresponding method in HyundaiEuropeApi

Account does following:

  1. Create an APIClient with an configuration (no deviceId)
  2. this Client is used then to call "registerDevice() -> String?"
    ->if an Api does not define this method then the UUID is returned and set in currend used configuration
    (thats more or less what you did in Account Line 122 for commented for kia but done for all)

If you look into ApiClient.swift there is a default implementation too (Line:144).
But i cant do the configuration overwrite there.

And the FakeAPIClient is the reason why there is still a default in Line 144 of ApiClient.
The FakeAPIClient is not a SubClass of APIBaseClient

What do you think?
Is there a better way?

try await fetchVehicleStatus(for: vehicle, authToken: authToken, cached: true)
}

public func registerDevice() async throws -> String? {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

It's unclear what this is meant to be doing

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

see on the other comment.
Its just needed because FakeAPIClient

+ login as with username and password to generate access and refresh token
+ refresh token is used for login with priority after first login
+ parsing of CCNC and non CCNC car data
+ get a command token for command execution
+ add functions to get values directly from deeper json path
@Nachtlatscher
Copy link
Copy Markdown
Contributor Author

Nachtlatscher commented May 4, 2026

@schmidtwmark sry for the force pushes i had to resolve conflicts with your lates changes and changed commit informations to my private Information instead of business one.

With this state of the branch i got rid of only using a refresh token.
Workflow is as follows:

  1. add Account
  2. fill in Username, Password, PIN, (Optional) Refresh Token
  3. Login will be done by provided Refresh Token or by User and Password
  4. If User and Password was used we get an Refresh Token from API that is saved into Account for further use

Each additional Login (Access Token expired) will use the Refresh Token to get a new Access Token.
User is able to delete the current Refresh Token from Account (next Login will get a new one).
<-- Thats needed because they expire after about 6-8 month

I think thats a good start for an initial merge.
I will then create a new branches for the different enhancements that are still to do.

Copy link
Copy Markdown
Owner

@schmidtwmark schmidtwmark left a comment

Choose a reason for hiding this comment

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

This is looking pretty good!

One other thing to add -- BetterBlueKit includes bbcli, a CLI swift program that lets you login and fetch status. It's really useful for testing without having to run the entire BetterBlue app. It can also be used to test a captured JSON response without pinging the server again.

Can you add an (optional) refreshToken parameter to bbcli and the plumbing to read it correctly?

return current
}

private func getAnyFromJson(from data: [String: Any], key keyString: String?) -> Any? {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

These functions could probably be moved into a generic API parsing swift file -- would be nice to reuse this keystring setup for other APIs!

+ if refresh token runs into error and password is given try to get a new refresh token
+ error if login fails because of credentials
Copy link
Copy Markdown
Owner

@schmidtwmark schmidtwmark left a comment

Choose a reason for hiding this comment

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

This is looking good! I’m going to merge this (and the BetterBlue changes) as is later tonight. I’ll need to test and make sure it doesn’t break existing stuff, but then I’ll roll it out later this week.

@Nachtlatscher
Copy link
Copy Markdown
Contributor Author

This is looking good! I’m going to merge this (and the BetterBlue changes) as is later tonight. I’ll need to test and make sure it doesn’t break existing stuff, but then I’ll roll it out later this week.

Nice i will look at the encrypted password flow commented to the issue. So that will maybe be the next step then to switch from unencrypted to encrypted one for next pull request.

@schmidtwmark schmidtwmark merged commit 3260694 into schmidtwmark:main May 18, 2026
1 check failed
@lordz-ei
Copy link
Copy Markdown

This is looking good! I’m going to merge this (and the BetterBlue changes) as is later tonight. I’ll need to test and make sure it doesn’t break existing stuff, but then I’ll roll it out later this week.

Let us know so we can test

@Nachtlatscher Nachtlatscher deleted the hyundai_eu branch May 19, 2026 07:19
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.

Support for EU Hyundai

4 participants