Yesterday my 2nd Gen Amazon Echo Dot arrived (check out my kids review of the Echo Dot here) and I thought it’d be fun to try and hook it up to my Renault ZOE so that I could check battery status and preheat it without having to load the clunky app.

Alexa Skills can be hosted on AWS Lambda and there’s an easy-to-follow tutorial in the dev docs. The Renault ZOE doesn’t have an official API but last year I’d used Fiddler to sniff the mobile app and been able to emulate that. Since then their app has just become a webview wrapper so I was a little worried I wouldn’t be able to do this - however, SPAs to the rescue… the app is written in Angular and calls the backend in a really simple JSON API (way better than the previous XML abomination!).

I had a quick Google and found that Terence Eden had already pasted all of the request/response info online which saved me a little effort trawling through. I whipped up a quick node script that calls the API to login and request battery status / preheating. It’s relatively simple, not much code at all (though could do with a little refactoring).

" use strict " ; let https = require ( " https " ); let token , vin , zoeUsername , zoePassword , loginFailureCallback ; function sendRequest ( action , requestData , successCallback , failureCallback ) { const options = { hostname : " www.services.renault-ze.com " , port : 443 , path : " /api " + action , method : requestData ? " POST " : " GET " , headers : { " Content-Type " : " application/json " , " Authorization " : ( token ? " Bearer " + token : "" ) } }; const req = https . request ( options , resp => { if ( resp . statusCode < 200 || resp . statusCode > 300 ) { console . log ( `Failed to send request ${ action } ( ${ resp . statusCode } : ${ resp . statusMessage } )` ); if ( failureCallback ) failureCallback (); return ; } console . log ( `Successful request ${ action } ( ${ resp . statusCode } : ${ resp . statusMessage } )` ); let respData = "" ; resp . on ( " data " , c => { //console.log("<== " + c.toString()); respData += c . toString (); }); resp . on ( " end " , () => { if ( successCallback ) successCallback ( respData && respData . length ? JSON . parse ( respData ) : null ); }); }); if ( requestData && JSON . stringify ( requestData ) !== ' {} ' ) req . write ( JSON . stringify ( requestData )); req . end (); } function login ( successCallback ) { sendRequest ( " /user/login " , { username : zoeUsername , password : zoePassword }, loginResponse => { token = loginResponse . token ; vin = loginResponse . user . vehicle_details . VIN ; successCallback (); }, loginFailureCallback ); } exports . setLogin = ( username , password , failureCallback ) => { zoeUsername = username ; zoePassword = password ; loginFailureCallback = failureCallback ; } exports . getBatteryStatus = ( successCallback , failureCallback ) => { login (() => sendRequest ( " /vehicle/ " + vin + " /battery " , null , successCallback , failureCallback )); } exports . sendPreheatCommand = ( successCallback , failureCallback ) => { login (() => sendRequest ( " /vehicle/ " + vin + " /air-conditioning " , {}, successCallback , failureCallback )); }

Next was wiring this up to work the Alexa API. I created a separate JS file that require'd the one above and just handled two simple intents ( preheat and battery status ). If you ask for help or launch without an intent it just tells you what the options are. Most requests return a card too, so you can see them inside the Alexa app.

Since we have two cars (two ZOEs!), I actually set this up to respond to two different app IDs and connect to the correct account (and rejected any other apps, so if you jokesters find the ID you can’t burn my battery ;)). The usernames/passwords come from environment variables I would set up in Lambda to make it easier to share code without removing them.

" use strict " ; const dannyAlexaApp = " amzn1.ask.skill.xxxxx " ; const heatherAlexaApp = " amzn1.ask.skill.yyyyy " ; let car = require ( " ./car " ); function buildResponse ( output , card , shouldEndSession ) { return { version : " 1.0 " , response : { outputSpeech : { type : " PlainText " , text : output , }, card , shouldEndSession } }; } // Helper to build the text response from battery information. function buildBatteryStatus ( battery ) { let response = `You have ${ battery . charge_level } % battery which will get you approximately ${ Math . round ( battery . remaining_range * 0.621371 )} miles. ` ; if ( battery . plugged ) response += " The car is plugged in " ; else response += " The car is not plugged in " ; if ( battery . charging ) response += " and charging " ; return response + " . " ; } exports . handler = ( event , context ) => { // Helper to return a response with a card. const sendResponse = ( title , text ) => { context . succeed ( buildResponse ( text , { " type " : " Simple " , " title " : title , " content " : text })); }; try { console . log ( `event.session.application.applicationId= ${ event . session . application . applicationId } ` ); // Shared callbacks. const exitCallback = () => context . succeed ( buildResponse ( " Goodbye! " )); const helpCallback = () => context . succeed ( buildResponse ( " What would you like to do? You can preheat the car or ask for battery status. " , null , false )); const loginFailureCallback = () => sendResponse ( " Authorisation Failure " , " Unable to login to Renault Z.E. Services, please check your login credentials. " ); // Set login based on the Alexa app ID. // We have two cars, so two activation names ("my car" and "Heather's car"). // If you're not either of these apps, you're not allowed to control our cars! if ( event . session . application . applicationId === dannyAlexaApp ) { car . setLogin ( process . env . ZOE_USER_DANNY , process . env . ZOE_PASS_DANNY , loginFailureCallback ); } else if ( event . session . application . applicationId === heatherAlexaApp ) { car . setLogin ( process . env . ZOE_USER_HEATHER , process . env . ZOE_PASS_HEATHER , loginFailureCallback ); } else { sendResponse ( " Invalid Application ID " , " You are not allowed to use this service. " ); return ; } // Handle launches without intents by just asking what to do. if ( event . request . type === " LaunchRequest " ) { helpCallback (); } else if ( event . request . type === " IntentRequest " ) { // Handle different intents by sending commands to the API and providing callbacks. switch ( event . request . intent . name ) { case " PreheatIntent " : car . sendPreheatCommand ( response => sendResponse ( " Car Preheat " , " The car will begin preheating shortly. " ), () => sendResponse ( " Car Preheat " , " Unable to begin preheating. Have you already done this recently? " ) ); break ; case " GetBatteryStatusIntent " : car . getBatteryStatus ( battery => sendResponse ( " Car Battery Status " , buildBatteryStatus ( battery )), () => sendResponse ( " Car Battery Status " , " Unable to get car battery status, please check your login details. " ) ); break ; case " AMAZON.HelpIntent " : helpCallback (); break ; case " AMAZON.StopIntent " : case " AMAZON.CancelIntent " : exitCallback (); break ; } } else if ( event . request . type === " SessionEndedRequest " ) { exitCallback (); } } catch ( err ) { console . error ( err . message ); sendResponse ( " Error Occurred " , " An error occurred. Fire the programmer! " + err . message ); } };

I created two apps in Alexa (both using the same Lambda function) and set up the intent schema:

{ " intents " : [ { " intent " : " PreheatIntent " }, { " intent " : " GetBatteryStatusIntent " }, { " intent " : " AMAZON.HelpIntent " }, { " intent " : " AMAZON.StopIntent " } ] }

… and some utterances:

PreheatIntent pre heat PreheatIntent preheat PreheatIntent heat PreheatIntent heater PreheatIntent turn the heater on PreheatIntent warm up PreheatIntent heat up PreheatIntent preheat for me PreheatIntent start the heaters PreheatIntent turn the heater on PreheatIntent turn on the heater PreheatIntent turn the heat on PreheatIntent turn on the heat PreheatIntent warm up PreheatIntent make it toasty for me PreheatIntent it's cold outside PreheatIntent heat my seat up GetBatteryStatusIntent battery status GetBatteryStatusIntent status GetBatteryStatusIntent battery GetBatteryStatusIntent level GetBatteryStatusIntent battery level GetBatteryStatusIntent how much range GetBatteryStatusIntent range GetBatteryStatusIntent what is your battery status GetBatteryStatusIntent tell me your status GetBatteryStatusIntent are you charged GetBatteryStatusIntent how do your batteries look GetBatteryStatusIntent what is your battery level GetBatteryStatusIntent how are your batteries GetBatteryStatusIntent is there any charge left GetBatteryStatusIntent how much charge is left GetBatteryStatusIntent how much range is left GetBatteryStatusIntent what is your range GetBatteryStatusIntent how many miles can we drive GetBatteryStatusIntent what is the range left in the battery GetBatteryStatusIntent how much range is left in the battery GetBatteryStatusIntent how far can we drive

Finally, I created a little dummy script that lets me test the functions without having to go through the Echo for a faster dev cycle. I didn’t go as far as mocking the HTTP requests but I did modify the code temporarily to ensure the error handling worked properly (detecting invalid username/password, or if you try to pre-heat too often).

" use strict " ; let zoe = require ( ' ./lambda/index ' ); let batteryStatusEvent = { " session " : { " application " : { " applicationId " : " amzn1.ask.skill.zzz " } }, " version " : " 1.0 " , " request " : { " type " : " IntentRequest " , " intent " : { " name " : " GetBatteryStatusIntent " } } }; let preheatEvent = { " session " : { " application " : { " applicationId " : " amzn1.ask.skill.zzz " } }, " version " : " 1.0 " , " request " : { " type " : " IntentRequest " , " intent " : { " name " : " PreheatIntent " } } }; let context = { succeed : function ( resp ) { console . log ( "



Result:

" + JSON . stringify ( resp , null , 4 )); } }; zoe . handler ( batteryStatusEvent , context ); zoe . handler ( preheatEvent , context );

After some successfully tests, I zipped the files up and uploaded them to Lambda to replace the favourite-color sample I’d been testing with. The final result is in the video at the top of this post!

You’re free to do as you please with this code. If you improve anything significantly, do leave a comment! If you’ve done anything similar with your Echo Dot, do leave a comment!