The OVMS has Wifi and also exposes a websocket server from which you can stream metrics from. I've glued together some horrible code that allows displaying some information to an M5Stack Core.
The buttons also allow to trigger hand crafted events that end up being managed by some javascript code in NodeRed (that subscribes to the MQTT broker used by the Protocol V3 stack in OVMS)
The ABRP plugin and explanations for OVMS can be found here
Beyond setting your car model with config set usr abrp.car_model <value>
and personnal API token with config set usr abrp.user_token <value>
you will need to change the default url with config set usr abrp.url <value>
The URL will point to code that looks a bit like this:
<?php $CAR_MODEL = "kia:soul:19:64:other"; $OVMS_API_KEY = "32b2162f-9599-4647-8139-66e9f9528370"; $MY_TOKEN = "xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx"; $URL = "http://api.iternio.com/1/tlm/send"; /* include_once('libTorque.php'); processAndSend($_SERVER['QUERY_STRING']); */ $ctx = stream_context_create(array( 'http' => array( 'timeout' => 1 ) ) ); file_get_contents($URL.'?'.$_SERVER['QUERY_STRING'], 0, $ctx); // <-- simply copy in the end 🤷🏻♂️ //echo "OK!";
the libTorque.php used to be old code to parse the crazy stuff that the android Torque app used (with a Bluetooth OBD dongle)… because it started by sending data to inform on how to parse subsequent url calls… (where the ABRP plugin directly embedded in the OVMS has much easier to parse data) :
37.164.143.zz - - [28/Jul/2020:10:20:21 +0200] "GET /?api_key=32b2162f-9599-4647-8139-66e9f9528370&token=xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx&tlm=%7B%22utc%22%3A1595924417%2C%22soc%22%3A61%2C%22soh%22%3A100%2C%22speed%22%3A0%2C%22car_model%22%3A%22kia%3Asoul%3A19%3A64%3Aother%22%2C%22lat%22%3A%22xyz.327%22%2C%22lon%22%3A%225.048%22%2C%22alt%22%3A%220%22%2C%22ext_temp%22%3A23.5%2C%22is_charging%22%3A0%2C%22batt_temp%22%3A28%2C%22voltage%22%3A371.2%2C%22current%22%3A1.2%2C%22aux12v%22%3A13.99%2C%22power%22%3A%220.4%22%7D HTTP/1.1" 200 5 "-" "ovms/v3.1 (joul 3.2.013-304-g599b0975/ota_1/edge (build idf v3.3.2-879-g0137aef47 Jul 18 2020 15:14:00))"
The tlm
parameter decodes to a clean json string:
tlm={"utc":1595924417,"soc":61,"soh":100,"speed":0,"car_model":"kia:soul:19:64:other","lat":"xyz.327","lon":"5.048","alt":"0","ext_temp":23.5,"is_charging":0,"batt_temp":28,"voltage":371.2,"current":1.2,"aux12v":13.99,"power":"0.4"}
but here's the old stuff:
<?php global $paramMap; include_once('vendor/autoload.php'); function processQueryString($qs){ // userUnitXXXXXX // userShortNameXXXXXX // userFullNameXXXXXX // defaultUnitXXXXXX // kXXXXXX // eml / v / session / id / time $result = array(); $m = new Memcached(); $m->addServer('localhost', 11211); parse_str($qs, $queryParts); foreach($queryParts as $qKey => $qVal){ if (preg_match("/^(eml|v|session|id)/", $qKey, $matches)) { //echo "elem ".$matches[1]." val: $qVal \n"; } else if (preg_match("/^(userUnit|userShortName|userFullName|defaultUnit)([0-9A-Fa-f]{2,6})/", $qKey, $matches)) { //echo "elem ".$matches[1]." id (".$matches[2].") val: $qVal \n"; //echo "memcache set : ".$matches[1]."_".$matches[2].", $qVal\n"; $m->set($matches[1]."_".$matches[2], $qVal, 24*3600*21); } else if (preg_match("/^(k)([0-9A-Fa-f]{2,6})/", $qKey, $matches)) { //echo $matches[0]." : $qVal (".$matches[2].")\n"; $id=$matches[2]; if($m->get("userShortName_".$id)){ $keyName=$m->get("userShortName_".$id); if (in_array($keyName,array(""))){ $result[$keyName]=$qVal; } else if (in_array($keyName,array("Min Cell V No.","Charging"))){ $result[$keyName]=intval($qVal); } else // Default to float $result[$keyName]=floatval($qVal); } //echo $m->get($tag.$id)." : ".$qVal." ($id)\n"; } else if (preg_match("/^(plop|profileName|profileFuelType|profileWeight|profileVe|profileFuelCost|\/)/", $qKey)) { //Discard } else if (preg_match("/^(time)/", $qKey)) { //echo "elem ".$matches[1]." val: $qVal \n"; $result[$qKey]=intval($qVal); } else { //echo "unhandled : $qKey => $qVal\n"; } } //var_dump($result); return $result; } function sendToInfluxDB($data){ // directly get the database object $database = InfluxDB\Client::fromDSN(sprintf('influxdb://kiasoul:soulsoul@%s:%s/%s', "10.33.57.XX", 8086, "kiasoul64")); // get the client to retrieve other databases $client = $database->getClient(); // Extract ["time"] if(isset($data["time"])){ $timestamp=$data["time"]*1000; unset($data["time"]); }else return false; // Extract ["SOC Display"] if(isset($data["SOC Display"])){ $SOCdisplay=$data["SOC Display"]; //unset($data["SOC Display"]); }else return false; $points = [ new InfluxDB\Point( 'SOCdisplay', $SOCdisplay, ['car' => 'joul'], $data, $timestamp ) ]; $newPoints = $database->writePoints($points, InfluxDB\Database::PRECISION_MICROSECONDS); return $newPoints; } function processAndSend($qs){ sendToInfluxDB( processQueryString($qs) ); } function loadParamMap(){ }
Since, I have simply configured a NodeRed server, that consumes and parses ovms metrics over MQTT.
The nodered flow will look like this:
[ { "id": "6b91f982.e7a02", "type": "tab", "label": "Kia Joul", "disabled": false, "info": "" }, { "id": "cc30af3f.2a19b", "type": "mqtt in", "z": "6b91f982.e7a02", "name": "joul@mqtt", "topic": "joul/#", "qos": "2", "datatype": "auto", "broker": "a83ee8f6.376a98", "x": 290, "y": 340, "wires": [ [ "f1bcd001.877828", "e75ed110.0438e" ] ] }, { "id": "f1bcd001.877828", "type": "debug", "z": "6b91f982.e7a02", "name": "", "active": false, "tosidebar": true, "console": false, "tostatus": false, "complete": "true", "targetType": "full", "x": 870, "y": 400, "wires": [] }, { "id": "e75ed110.0438e", "type": "function", "z": "6b91f982.e7a02", "name": "mqtt2influx", "func": "msg.topic = msg.topic.replace(/\\//g,\"_\").replace(\"joul_\",\"\").replace(\"darthmaul_\",\"\");\n\n\nif(msg.topic.startsWith(\"metric_\")){\n msg.topic = msg.topic.replace(\"metric_\",\"\");\n var returnmsg = {};\n \n returnmsg.payload = [\n {\n measurement: msg.topic,\n fields: {},\n timestamp: new Date()\n }\n ];\n \n if(msg.payload.includes(\",\")){\n var a = msg.payload.split(\",\"), i;\n for (i = 0; i < a.length; i++) {\n returnmsg.payload[0].fields[i]=parseFloat(a[i]);\n }\n }else{\n returnmsg.payload[0].fields[msg.topic] = parseFloat(msg.payload);\n }\n \n return returnmsg;\n}", "outputs": 1, "noerr": 0, "x": 530, "y": 520, "wires": [ [ "f1bcd001.877828", "d072b101.e61ea" ] ] }, { "id": "d072b101.e61ea", "type": "influxdb batch", "z": "6b91f982.e7a02", "influxdb": "b0eafaa2.4c476", "precision": "s", "retentionPolicy": "", "name": "kiasoul@influx", "x": 900, "y": 680, "wires": [] }, { "id": "9eb93ce5.41b728", "type": "mqtt in", "z": "6b91f982.e7a02", "name": "darthmaul@mqtt", "topic": "darthmaul/#", "qos": "2", "datatype": "auto", "broker": "a83ee8f6.376a98", "x": 220, "y": 500, "wires": [ [ "e75ed110.0438e", "f1bcd001.877828" ] ] }, { "id": "a83ee8f6.376a98", "type": "mqtt-broker", "z": "", "name": "joul", "broker": "10.33.57.YY", "port": "1883", "clientid": "", "usetls": false, "compatmode": false, "keepalive": "15", "cleansession": true, "birthTopic": "", "birthQos": "0", "birthPayload": "", "closeTopic": "", "closeQos": "0", "closePayload": "", "willTopic": "", "willQos": "0", "willPayload": "" }, { "id": "b0eafaa2.4c476", "type": "influxdb", "z": "", "hostname": "10.33.57.XX", "port": "8086", "protocol": "http", "database": "kiasoul64", "name": "kiasoul@influx", "usetls": false, "tls": "" } ]
It listens to joul/#
and darthmaul/#
because I ended up renaming the car after a while.
Easier to understand, here's what's inside mqtt2influx:
msg.topic = msg.topic.replace(/\//g,"_").replace("joul_","").replace("darthmaul_",""); if(msg.topic.startsWith("metric_")){ msg.topic = msg.topic.replace("metric_",""); var returnmsg = {}; returnmsg.payload = [ { measurement: msg.topic, fields: {}, timestamp: new Date() } ]; if(msg.payload.includes(",")){ var a = msg.payload.split(","), i; for (i = 0; i < a.length; i++) { returnmsg.payload[0].fields[i]=parseFloat(a[i]); } }else{ returnmsg.payload[0].fields[msg.topic] = parseFloat(msg.payload); } return returnmsg; }
Once you're all set, you can get precise metric data and plot it over in your Chronograf dashboard:
Or even nicer with Grafana