Wednesday, July 17, 2019

Delving into Renault's new API

(Geek level: High - you have been warned!)

I've been driving a Renault Zoe for over a year now. It's a great car, but the companion mobile application - allowing you to turn on the heater or air conditioning remotely, or to set a charge schedule to make the most of cheap overnight electricity - has been lacklustre at best.

At the tail end of last year, Renault decided to push an update to the ZE Services app that effectively removed the "app" part, instead redirecting users to their website (which works even more poorly on mobile devices). Renault promised a new "MY Renault application [with] an improved interface with new and easy-to-use services".

Nearly eight months later, still no sign of the new "MY Renault" app in the UK, but some countries on the continent have it in their hands. I decided to take a look and see how different the new API was to the previous one, and how much work I'd have to do to update my charge scheduling script (it takes half-hourly price data from Octopus, works out the cheapest window in which to charge overnight, and schedules the car to charge in that window.)

I'm not going to spend any time looking at registering for MY Renault; it's boring, and I went through the process in French, so all the following assumes you have a MY Renault account. I'm going to focus on the area of most interest to me: functions to interact with the electric-vehicle-specific parts of the API.

The first time I introduce a named piece of data, I'll make it bold so it's easier to skim back to. Where a parameter needs substituting in, it'll be in {italics, and probably monospace}. There are lots of scattered bits of information you'll need to pull together!

Update: This post has been updated with corrections from kind folk in the comments.

Logging in

Authentication has now been parcelled out to Israeli SAP subsidiary Gigya, who have extensive API documentation available online. The first thing to note is that you'll need the correct Gigya API key - this is embedded in the MY Renault app's configuration. Once you have that, you can log in by POSTing your MY Renault credentials to the appropriate endpoint. This will yield your Gigya session key (returned as sessionInfo.cookieValue). It's not clear when, or even if, this session key expires, so keep hold of it - you're going to need it a lot.

Once you've logged in, you'll need to extract a couple more pieces of information from the Gigya API before you can start to talk to Renault's servers. The first is your person ID, which ties your Gigya account to your MY Renault one (or specifically, ties you to a set of MY Renault accounts). You'll need use your Gigya session key as the oauth_token field value to pull the person ID from the Gigya accounts.getAccountInfo endpoint, but it's a fair bet the value won't change for a particular user.

You'll then need to request your Gigya JWT token from accounts.getJWT, again using your Gigya session key as the oauth_token, and you need to include data.personId,data.gigyaDataCenter as the value of fields - Renault's servers need that data to be in the token. It looks like you can pick the expiry of your choice here - Renault's app uses 900 seconds. When this token expires, you'll need to hit this endpoint again to get a new one.

OK, we're done talking to Gigya, now we can start the second part of the authentication process - this time with Renault's servers. Or, more precisely, the Nissan-Renault Alliance's shared platform Kamereon. Here, you'll need a Kamereon API key - again, this is embedded in the MY Renault app. The root URL for this API is https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1.

You don't yet have your Kamereon account ID, so you'll need to get it using your person ID from earlier. You'll need to pass your Gigya JWT token in the x-gigya-id_token header (note the funky mix of hyphens and underscore), and the Kamereon API key in the apikey header, in a GET request to the endpoint at persons/{person ID}?country=XX, inserting your person ID and two-letter country code. I'm not sure that the latter makes the slightest bit of difference, as I've tried exchanging FR for GB and not seen any effects, but the whole thing blows up if it's not there.

Looking at the data returned from that endpoint, you'll notice that it contains an array of accounts, not just a single account. I'm not sure in which scenarios one might have multiple accounts (multiple cars can be added to a single account), but it looks like it's possible. Not for me, though, since there was only a single account here; the value of accountId is what we'll need to go forward.

We're not done yet! The last thing you'll need to start pulling data is a Kamereon token. These are short-lived and are obtained from the endpoint at /accounts/{Kamereon account ID}/kamereon/token?country=XX, again with the apikey and x-gigya-id_token headers. The one you want is the accessToken. I've not looked into using the refreshToken - you might as well just repeat this request when the token expires - and the idToken returned is a copy of your Gigya JWT token (I think).

Phew! At the end of that process we should have:

  • A Gigya JWT token
  • A Kamereon account ID
  • A Kamereon token
and the means to regenerate those tokens when they expire.

Listing vehicles

I mentioned you can add more than one vehicle to an account. Before you can do much you'll need the VIN of the vehicle you're interested in. You might well have that to hand if it's your vehicle, but in the general case you'll need to get the list from /accounts/{Kamereon account ID}/vehicles?country=XX. You'll need three auth headers:
  • apikey: the Kamereon API key
  • x-gigya-id_token: your Gigya JWT token
  • x-kamereon-authorization: Bearer {kamereon token}
You'll note that the vehicle's registration plate is also included in the data now. That's a nice feature that ought to make it easier for end-users to identify vehicles in multi-vehicle accounts. As with the previous API, though, everything is keyed off the VIN (which makes sense, since the registration plate could change - though I've no idea if Renault's systems would reflect that sort of change).

Interacting with a vehicle

So, what can we do?

Each of these endpoints is under /accounts/kmr/remote-services/car-adapter/v1/cars/{vin}, and each requires the same three auth headers described above. The server expects a Content-type of application/vnd.api+json. For the most part, the returned data is self-explanatory.

Reading data

  • /battery-status (GET): plugged in or not, charging or not, percentage charge remaining, estimated range (given in km). Presumably, as with the previous API when charging, information about the charge rate etc - I've not tried this yet.
  • /hvac-status (GET): Air conditioning on or off, external temperature, and scheduled preconditioning start time. I've not seen it report anything other than AC off, even when preconditioning was running - possibly as a result of caching somewhere? External temperature seems accurate.
  • /charge-mode (GET): Always charging or on the scheduler? (There's possibly a third state for the in-car "charge after X" mode - I've not investigated this yet. The "charge after" in-car setting is represented here as always_charging.)
  • /charges?start=YYYMMDD&end=YYYYMMDD (GET): Detail for past charges. This isn't currently returning any useful data for me. end cannot be in the future.
  • /charge-history?type={type}&start={start}&end={end} (GET): aggregated charge statistics. type is the aggregation period, either month or day; for 'month' start and end should be of the form YYYYMM; for 'day' it should be YYYYMMDD. Again, this is not supplying any useful data for me right now.
  • /hvac-sessions?start=&end= (GET): Preconditioning history
  • /hvac-history?type=&start=&end= (GET): Same as charge history but for preconditioning stats.
  • /cockpit (GET): odometer reading (total mileage, though it's given in kilometers).
  • /charge-schedule (GET): the current charge schedule - see later.
  • /lock-status (GET): The server returns 501 Not Implemented.
  • /location (GET): The server returns 501 Not Implemented.
  • /notification-settings (GET): Settings for notifications (d'uh) - as well as email and SMS, there's also now an option for push notifications via the app, for each of "charge complete", "charge error", "charge on", "charge status" and "low battery alert" / "low battery reminder".

Writing data

Each of these expects a JSON object body of the form:

{
  "data": {
    "type": "SomeTypeName",
    "attributes": {
      (... the actual data ...)
    }
  }
}

In many cases, you'll be re-POSTing a similar object to that which you received for the corresponding GET method. A success response from the server tends to be the same object you just POSTed, with an ID added. I've listed the required attributes where I know them.

  • /actions/charge-mode (POST): sets the charge mode. (Type ChargeMode)
    • action: either schedule_mode or always_charging (possibly a third value - see above)
  • /actions/hvac-start (POST): Sets the preconditioning timer, or turns on preconditioning immediately. (Type HvacStart)
    • action: start
    • targetTemperature: in degrees Celsius. The app is hard-coded to use 21°C.
    • startDateTime: if supplied, should be of the form YYYY-MM-DDTHH:MM:SSZ - I'm not sure what would happen if you tried to use a different timezone offset. If not supplied, preconditioning will begin immediately.
  • /actions/charge-schedule (POST): Set a charge schedule. See later. (Type ChargeSchedule)
  • /actions/notification-settings (POST): Sets notification settings. (Type ZeNotifSettings)
  • /actions/send-navigation (POST): The much-vaunted "send a destination to your car and start navigating". I've not explored this one much but parameters include:
    • name
    • latitude
    • longitude
    • address
    • latitudeMode
    • longitudeMode
    • locationType
    • calculationCondition

The charge scheduler

Perhaps unsurprisingly, given the need to interoperate with the existing fleet of current Zoes, the charge scheduler functions in exactly the same way, and with the same limitations:
  • You must specify exactly one charging period per day, for every day of the week
  • Charge start and duration must be in intervals of 15 minutes
  • Charge duration must be at least 15 minutes
  • Charging periods must not overlap
  • Charge start time is specified as a four-character digit string e.g. "0115" (because that's how everyone represents time, right?)
  • Charge duration is specified as an integer, rather than a digit string as it was in the previous API
Interestingly, looking at the data structure, there's scope here to support multiple charging periods per day: each day has an array of periods against it. I wonder if the Zoe ZE50 might have different onboard software that isn't quite so arse-backwards as the Atos/Worldline system?

Conclusions

The new API is definitely different, and it's probably better than the old one. It certainly seems a lot faster to respond (it no longer takes several seconds to log in, for one). What it can't do is change the capabilities of the software deployed on vehicles already in the wild - hence the crappy charge scheduler remains, and no doubt people will still be annoyed at the infrequency of the battery state updates, especially on a rapid charger. (Aside: it's possible to tweak the parameters of your car's TCU to increase the frequency at which it reports its state to the server.)

I'm not sure why the login process is quite so convoluted, except that perhaps it needed to be given the constraints of interacting with a third-party authentication gateway (Gigya) and the Alliance Kamereon API which has its own set of requirements. I do feel that requiring three different tokens on each request is a little excessive!

We've lost a few odds and ends, none of which seemed particularly important:
  • It doesn't appear possible to cancel a preconditioning request
  • It doesn't appear possible to request a state update from the car (though it's possible this is now handled behind the scenes, as it was a bit cludgy anyway)
In each case, maybe it is possible, and I've simply not discovered that function yet.

What is clear, though, is that neither of these APIs were designed to be public - the requirement to have an API key for both Gigya and Kamereon makes that apparent, and that's the reason I've not included these keys in this post. If you've read this far, chances are you'll know exactly how and where in the MY Renault APK to find the configuration resource that lists them - you don't even need any specialist software to do so.

Update: Or you could grab the keys from Renault's own website where they've been uploaded in plain text for all to stumble upon.

Also clear is that the scope of these APIs extend far beyond the ZE-specific capabilities. I've not detailed them, as they're very much of secondary interest, but there's information available via the new app on all sorts, from the type of fuel your vehicle uses, to its service schedule and any optional extras you added when you bought it. I guess that's a natural part of moving to the "MY Renault" platform.

An implementation

The first cut of my CLI tool / thin Python API wrapper is now on GitHub.

17 comments:

-**- said...

Great work, thank you

Anonymous said...

That's cool ! Thanks a lot

Unknown said...

Super! Looping forward to a python client!

Anonymous said...

Gracias for this! πŸ‘ŒπŸΌπŸŽ‰

How do you work around the certificate pinning in apps, when wanting to eavesdrop, out of interest?

AP41 said...

1. In the section "Listing vehicles", instead of
x-kamereon-authorization: Bearer
you should explicit :
x-kamereon-authorization: Bearer [kamereon token]

2. In the "Writing data" section, the JSON object body is incomplete. Its expected form is :

{
"data": {
"type": "SomeTypeName",
"attributes": {
(... the actual data ...)
}
}
}

AP41 said...

(follow-up of my previous comment) Those little corrections are based on my own experiments (some bash scripts), themselves based on your in-depth description of the new Zoe API. The authentication procedure is indeed too convoluted.

Again, thanks for your excellent investigative work !!

yannickh said...

I can't seem to get the last authentication call (getting the Kamereon token) to work. All previous calls work fine, but I get a 404 on this last one. Anyone else getting this? Maybe any tips from someone who does get it to work? Thanks!

epenet said...

I too am stuck getting the Kamereon token.
The calls to https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/{accountid}/token?country=FR fail with the following error

{
'type': 'FUNCTIONAL',
'messages': [{
'code': 'err.func.wired.not-found',
'message': 'The specified url does not exist'
}],
'errors': [{
'errorCode': 'err.func.wired.not-found',
'errorMessage': 'The specified url does not exist'
}],
'error_reference': 'FUNCTIONAL'
}


Also, I should specify that I have two accounts associated with my personId. One is "accountType": "MYRENAULT", the other is "accountType": "SFDC".
I have tried with both accountId, but both fail for me.

-**- said...

1) Make sure use the GET method
2) Make sure you have the header "apikey" (value the kamereon key obtained from the APK) and "x-gigya-token", obtained in step 3 (getJWT, field id_token)
3) Make sure in the url where it states {accountid}, you substitute it for the id obtained in step 4 (persons call, field accounts[0].accountId)

epenet said...

1) I'm definitely using GET
2) "apikey" and "x-gigya-id_token" are definitely set
- if apikey is missing I get a 'Missing API Key header' message
- if token is missing I get a 'The access is unauthorized' message (at least on person query)
3) I'm definitely setting the accountid

I should also note that if I trim the "/token?country=FR" part of the query then it is working correctly
- https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/{accountid0} is working
- https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/{accountid1} is working
- https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/{accountid0}/token?country=FR is NOT working
- https://api-wired-prod-1-euw1.wrd-aws.com/commerce/v1/accounts/{accountid1}/token?country=FR is NOT working

epenet said...

Note: the failing code is available on GitHub: https://github.com/epenet/Renault-Zoe-API/tree/newapimockup/Test

-**- said...

Working Node-RED code available through https://canze.fisch.lu/access-renault-api-using-node-red-v2/

epenet said...

Thanks, the code helped to find the root cause.
The documentation states
/accounts/{Kamereon account ID}/token?country=XX
But the Node-RED code states
accounts/\" + msg.kamereon_id + \"/kamereon/token?country=NL

Replacing "/token" with "/kamereon/token" works.

-**- said...

Ah cool. Glad it's solved.

James Muscat said...

Thanks for the corrections all - I've updated the original post.

Now to work out why I didn't get any notifications about your comments (hence the slow response)!

epenet said...

Thanks James.

Do you have some news about the Python client? I'm happy to participate...
There are a few impatient people on GitHub...
- https://github.com/edent/Renault-Zoe-API/issues/18
- https://github.com/epenet/hassRenaultZE/issues/2

James Muscat said...

Just made a further update to point out that the API keys are in fact available in plain text via Renault's own website (thanks to oncleben31 on GitHub for discovering that), and that my implementation is now up on GitHub.