The Aiways U5 is a battery electric vehicle (BEV) produced in China and sold in several European countries.
This guide shows how to read the battery charge state (SOC, State of Charge) and other car status data of the U5 into Home Assistant entities. The data is fetched from the unofficial API that is used by the Aiways mobile app. The whole logic is created in Node-RED under Home Assistant.
Disclaimer
- This is all based on the API analysis and Python work of 0815eddi for the OpenWB Aiways SOC module. Thanks Eddi!
Preconditions
- You must be able to read your car’s data in the Aiways mobile app.
This requires the U5 software to be updated to version 1.7.0 or higher and the corresponding hardware update must be installed in the car. - Add-on Node-RED installed in Home Assistant.
Steps
-
The following Node-RED flows need the capability to do MD5 encoding. This can be done with the JS Crypto library, which is already part of Node.js (the server layer under Node-RED). But it must first be imported so you can really use it in function nodes:
Edit
/config/node-red/settings.js
,
locate the propertyfunctionGlobalContext
and
insert an additional line to load the crypto lib:functionGlobalContext: { crypto:require("crypto") },
You can use the HA file editor for this step.
When done restart Node-RED (HA → Settings → Add-Ons → Node-RED → Restart) -
Edit
/config/secrets.yaml
and add your Aiways app account credentials and vehicle identification number like this:aiways_user: YOUR_APP_USER_NAME aiways_pwd : YOUR_APP_PASSWORD aiways_vin : YOUR_CAR_VIN
-
Now import this flow into Node-RED:
[{"id":"24ebb2e2.2f6ffe","type":"file in","z":"9d7cd8ae.f2fe48","name":"Read secrets.yaml","filename":"/config/secrets.yaml","filenameType":"str","format":"utf8","chunk":false,"sendError":false,"encoding":"none","x":650,"y":120,"wires":[["2c59ff99.2affc"]]},{"id":"2c59ff99.2affc","type":"yaml","z":"9d7cd8ae.f2fe48","property":"payload","name":"Parse yaml","x":850,"y":120,"wires":[["9e7c7d2d.fdeb8","3fa027e1321af0da"]]},{"id":"9e7c7d2d.fdeb8","type":"function","z":"9d7cd8ae.f2fe48","name":"Set global config vars","func":"global.set(\"ha_secrets\", msg.payload);\n\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":1060,"y":120,"wires":[[]]},{"id":"563db3d3.d3e3ac","type":"server-events","z":"9d7cd8ae.f2fe48","name":"Run on HA startup","server":"bde4b68e.528848","version":1,"event_type":"home_assistant_client","exposeToHomeAssistant":false,"haConfig":[{"property":"name","value":""},{"property":"icon","value":""}],"waitForRunning":true,"outputProperties":[{"property":"payload","propertyType":"msg","value":"","valueType":"eventData"},{"property":"topic","propertyType":"msg","value":"$outputData(\"eventData\").event_type","valueType":"jsonata"},{"property":"event_type","propertyType":"msg","value":"$outputData(\"eventData\").event_type","valueType":"jsonata"}],"x":170,"y":120,"wires":[["6a8306b8e732392c"]]},{"id":"3a583032.2662b","type":"comment","z":"9d7cd8ae.f2fe48","name":"Set global config var with secrets.yaml entries...","info":"Usage example in functions:\n let fritzbox_pwd = global.get(\"ha_secrets\").fritzbox_pwd\n","x":220,"y":60,"wires":[]},{"id":"2cccdc63.dbb784","type":"inject","z":"9d7cd8ae.f2fe48","name":"Trigger now","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":450,"y":160,"wires":[["24ebb2e2.2f6ffe"]]},{"id":"3fa027e1321af0da","type":"debug","z":"9d7cd8ae.f2fe48","name":"debug","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1010,"y":160,"wires":[]},{"id":"6a8306b8e732392c","type":"switch","z":"9d7cd8ae.f2fe48","name":"payload == \"running\" ?","property":"payload","propertyType":"msg","rules":[{"t":"eq","v":"running","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":410,"y":120,"wires":[["24ebb2e2.2f6ffe"]]},{"id":"bde4b68e.528848","type":"server","name":"Home Assistant","version":2,"addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":false,"cacheJson":true,"heartbeat":false,"heartbeatInterval":30}]
This flow will read the HA
secrets.yaml
file into a global Node-RED variable, so you can use your secrets in Node-RED too.
Hit the “Trigger now” inject button once to make this happen. In the future this flow will automatically be run whenever HA is startet. So you only need to hit this button once. -
Import the second flow which does all the heavy lifting of polling the API to get your car’s status data into HA:
[{"id":"e5afc16c4577b23c","type":"inject","z":"c6ce30da03615610","name":"15 min","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"900","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":140,"y":4060,"wires":[["11644ec37221ea4b"]]},{"id":"213e47e34e291c5e","type":"comment","z":"c6ce30da03615610","name":"Lese Aiways-API","info":"","x":100,"y":4000,"wires":[]},{"id":"64c90bbe16dd829a","type":"function","z":"c6ce30da03615610","name":"Prep first request: POST Login","func":"// Based on: OpenWB-Module Aiways SOC:\n// https://github.com/snaptec/openWB/blob/9a955f00d4c84f038859b797002bc49cea1081b9/modules/soc_aiways/aiways_get_soc.py\n\n\n/*\n ############################################################# \n # Request1: aiways - passport - service / passport / login / password #\n # Login with accountname and password #\n # Also here we get the value for \"token\" #\n # This value is changing for each session #\n #############################################################\n*/\n\nlet PATH = \"aiways-passport-service/passport/login/password\"\n\nlet headers = {\n \"apptimezone\": msg.config.apptimezone,\n \"apptimezoneid\": msg.config.apptimezoneid,\n \"content-type\": \"application/json; charset=utf-8\",\n //\"accept-encoding\": \"gzip\",\n \"user-agent\": msg.config.user_agent\n}\n\n//encoded = password.encode()\n//passwordmd5 = hashlib.md5(encoded)\n\nlet LOGIN_PAYLOAD = { \n \"account\": msg.config.account, \n \"password\": msg.config.passwordmd5 \n}\n\n\n// Eingangsdaten\n//resp = requests.post(CS_URL + PATH, headers = headers, data = json.dumps(LOGIN_PAYLOAD))\nmsg.url = msg.config.CS_URL + PATH;\nmsg.method= \"POST\";\nmsg.headers= headers;\n//msg.cookies= \"\";\nmsg.payload = JSON.stringify(LOGIN_PAYLOAD)\n\nreturn msg;\n\n/*\ndata = resp.json()[\"data\"]\ntoken = data[\"token\"]\nuserId = data[\"userId\"]\n*/","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":570,"y":4060,"wires":[["80afdb92a51804d2","dbc0484b646aed93"]]},{"id":"80afdb92a51804d2","type":"http request","z":"c6ce30da03615610","name":"","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"7b79ba19.4d8194","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":850,"y":4060,"wires":[["7d5aa53b56dfbf3c","446a56759e541749"]]},{"id":"7d5aa53b56dfbf3c","type":"debug","z":"c6ce30da03615610","name":"debug 2","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1040,"y":4060,"wires":[]},{"id":"dbc0484b646aed93","type":"debug","z":"c6ce30da03615610","name":"debug 1","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":840,"y":4020,"wires":[]},{"id":"11644ec37221ea4b","type":"function","z":"c6ce30da03615610","name":"Prep msg.config","func":"\n/* \n* IMPORTANT:\n* The NPM package \"crypto\" is already part of Node.js \n* but it must be activated for Node RED in /config/node-red/settings.js:\n* functionGlobalContext: {\n* crypto:require(\"crypto\")\n* },\n*/\nlet crypto = global.get(\"crypto\");\n\n// Date().toString() = i.e. 'Mon Sep 26 2022 16:51:05 GMT+0200 (Mitteleuropäische Sommerzeit)'\n// timeZoneParts = i.e. [ \"GMT+0200, \"GMT+02\", \"00\" ]\nlet timeZoneParts = Date().toString().match(/(GMT[\\+\\-]\\d\\d)(\\d\\d)/);\n\nmsg.config = {\n account: global.get(\"ha_secrets\").aiways_user || \"\",\n passwordmd5: crypto.createHash('md5').update(global.get(\"ha_secrets\").aiways_pwd || \"\").digest('hex'),\n vin: global.get(\"ha_secrets\").aiways_vin || \"\",\n\n // apptimezone = i.e.: \"GMT+02:00\"\n apptimezone: `${timeZoneParts[1]}:${timeZoneParts[2]}`,\n // apptimezoneid = i.e.: \"Europe/Berlin\"\n apptimezoneid: Intl.DateTimeFormat().resolvedOptions().timeZone,\n\n user_agent: \"okhttp/4.3.1\",\n\n CS_URL: \"https://coiapp-api-eu.ai-ways.com:10443/\"\n}\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":320,"y":4060,"wires":[["64c90bbe16dd829a"]]},{"id":"9f5c32232cf662a0","type":"function","z":"c6ce30da03615610","name":"Prep second request: POST Get Data","func":"// Based on: OpenWB-Module Aiways SOC:\n// https://github.com/snaptec/openWB/blob/9a955f00d4c84f038859b797002bc49cea1081b9/modules/soc_aiways/aiways_get_soc.py\n\n\n// data = resp.json()[\"data\"]\n// token = data[\"token\"]\n// userId = data[\"userId\"]\nlet data = msg.payload.data;\nmsg.config.token = data.token;\nmsg.config.userId = data.userId;\nmsg.config.userEmail = data.email;\nmsg.config.userType = data.userType;\n\n\n/*\n #########################################################################\n # Request2 \"app/vc/getCondition\" #\n # Get the current Condition of the car #\n # The data are located in data / vc #\n # \"soc\" contains the value for the current State of Charge) #\n #########################################################################\n*/\n\nlet PATH = \"app/vc/getCondition\"\n\nlet headers = {\n \"token\": msg.config.token,\n \"apptimezone\": msg.config.apptimezone,\n \"apptimezoneid\": msg.config.apptimezoneid,\n \"content-type\": \"application/json; charset=utf-8\",\n //\"accept-encoding\": \"gzip\",\n \"user-agent\": msg.config.user_agent\n}\n\n//PAYLOAD = {'userId': userId,'vin': vin}\nlet PAYLOAD = { \n \"userId\": msg.config.userId, \n \"vin\": msg.config.vin\n}\n\n//resp = requests.post(CS_URL + PATH, headers = headers, data = json.dumps(PAYLOAD))\nmsg.url = msg.config.CS_URL + PATH;\nmsg.method= \"POST\";\nmsg.headers= headers;\n//msg.cookies= \"\";\nmsg.payload = JSON.stringify(PAYLOAD)\n\nreturn msg;\n\n/*\ndata = resp.json()[\"data\"]\nvc = data[\"vc\"]\nsoc = vc[\"soc\"]\nchargeSts = vc[\"chargeSts\"]\n*/\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":590,"y":4120,"wires":[["a21da7070dea1596"]]},{"id":"446a56759e541749","type":"switch","z":"c6ce30da03615610","name":"HTTP 200 ?","property":"statusCode","propertyType":"msg","rules":[{"t":"eq","v":"200","vt":"num"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":330,"y":4120,"wires":[["9f5c32232cf662a0"],[]]},{"id":"d512972e991404f7","type":"debug","z":"c6ce30da03615610","name":"debug 3","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1040,"y":4120,"wires":[]},{"id":"de66356dc0887ec6","type":"switch","z":"c6ce30da03615610","name":"HTTP 200 ?","property":"statusCode","propertyType":"msg","rules":[{"t":"eq","v":"200","vt":"num"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":330,"y":4180,"wires":[["093bdd3374adf214"],[]]},{"id":"a21da7070dea1596","type":"http request","z":"c6ce30da03615610","name":"","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"7b79ba19.4d8194","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":850,"y":4120,"wires":[["d512972e991404f7","de66356dc0887ec6"]]},{"id":"093bdd3374adf214","type":"function","z":"c6ce30da03615610","name":"Prep third request: GET Logout","func":"// Based on: OpenWB-Module Aiways SOC:\n// https://github.com/snaptec/openWB/blob/9a955f00d4c84f038859b797002bc49cea1081b9/modules/soc_aiways/aiways_get_soc.py\n\n\n// data = resp.json()[\"data\"]\n// vc = data[\"vc\"]\n// soc = vc[\"soc\"]\n// chargeSts = vc[\"chargeSts\"]\nlet data = msg.payload.data;\nmsg.cardata = data.vc;\n\n\n/*\n #########################################################################\n # Request3 \"aiways-passport-service/passport/logout\" #\n # Makes a logout #\n #########################################################################\n*/\n\nlet PATH = \"aiways-passport-service/passport/logout\"\n\nlet headers = {\n \"token\": msg.config.token,\n \"apptimezone\": msg.config.apptimezone,\n \"apptimezoneid\": msg.config.apptimezoneid,\n //\"accept-encoding\": \"gzip\",\n \"user-agent\": msg.config.user_agent\n}\n\n//resp = requests.get(CS_URL + PATH, headers = headers)\nmsg.url = msg.config.CS_URL + PATH;\nmsg.method= \"GET\";\nmsg.headers= headers;\n//msg.cookies= \"\";\nmsg.payload = \"\";\n\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":570,"y":4180,"wires":[["83567f6fd0f4c7ae"]]},{"id":"c57cbffde6889c06","type":"debug","z":"c6ce30da03615610","name":"debug 4","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1040,"y":4180,"wires":[]},{"id":"83567f6fd0f4c7ae","type":"http request","z":"c6ce30da03615610","name":"","method":"use","ret":"obj","paytoqs":"ignore","url":"","tls":"7b79ba19.4d8194","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":850,"y":4180,"wires":[["c57cbffde6889c06","7533790f9646c24f"]]},{"id":"7533790f9646c24f","type":"switch","z":"c6ce30da03615610","name":"HTTP 200 ?","property":"statusCode","propertyType":"msg","rules":[{"t":"eq","v":"200","vt":"num"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":330,"y":4240,"wires":[["f71413d712d057df"],[]]},{"id":"3f65a91212b7c412","type":"ha-entity","z":"c6ce30da03615610","name":"car_state_of_charge","server":"cb38d2a3.10198","version":2,"debugenabled":false,"outputs":1,"entityType":"sensor","config":[{"property":"name","value":"car_state_of_charge"},{"property":"device_class","value":"battery"},{"property":"icon","value":"mdi:ev-station"},{"property":"unit_of_measurement","value":"%"},{"property":"state_class","value":""},{"property":"last_reset","value":""}],"state":"cardata.soc","stateType":"msg","attributes":[],"resend":true,"outputLocation":"payload","outputLocationType":"none","inputOverride":"allow","outputOnStateChange":false,"outputPayload":"$entity().state ? \"on\": \"off\"","outputPayloadType":"jsonata","x":600,"y":4240,"wires":[[]]},{"id":"b28c9b9f47bc2085","type":"ha-entity","z":"c6ce30da03615610","name":"car_charge_status","server":"cb38d2a3.10198","version":2,"debugenabled":false,"outputs":1,"entityType":"sensor","config":[{"property":"name","value":"car_charge_status"},{"property":"device_class","value":""},{"property":"icon","value":"mdi:ev-station"},{"property":"unit_of_measurement","value":""},{"property":"state_class","value":""},{"property":"last_reset","value":""}],"state":"cardata.chargeSts","stateType":"msg","attributes":[],"resend":true,"outputLocation":"payload","outputLocationType":"none","inputOverride":"allow","outputOnStateChange":false,"outputPayload":"$entity().state ? \"on\": \"off\"","outputPayloadType":"jsonata","x":590,"y":4300,"wires":[[]]},{"id":"f71413d712d057df","type":"junction","z":"c6ce30da03615610","x":460,"y":4240,"wires":[["b28c9b9f47bc2085","3f65a91212b7c412"]]},{"id":"7b79ba19.4d8194","type":"tls-config","name":"Ignore Invalid Cert","cert":"","key":"","ca":"","certname":"","keyname":"","caname":"","servername":"","verifyservercert":false},{"id":"cb38d2a3.10198","type":"server","name":"Home Assistant","version":2,"addon":true,"rejectUnauthorizedCerts":true,"ha_boolean":"y|yes|true|on|home|open","connectionDelay":true,"cacheJson":true,"heartbeat":false,"heartbeatInterval":"30"}]
This flow will read the current status every 15 minutes.
You can hit the “15 min” inject button at any time to force an API poll for testing. Check the debug window to analyze any problems. Activate the debug nodes for more info.Currently the flow exposes only
sensor.state_of_charge
(= battery level) andsensor.car_charge_status
as HA entities. But there is much more to exploit. Just activate the last debug node, poll the API and check the debug output pane for all the data that the API returns.
Enjoy!