diff --git a/README.md b/README.md index 85525a8..8b3121c 100644 --- a/README.md +++ b/README.md @@ -3,4 +3,8 @@ A service that utilizes the [OnStarJS](https://github.com/samrum/OnStarJS) libra There is no official relationship with GM, Chevrolet nor OnStar. In fact, it would be nice if they'd even respond to development requests, so we wouldn't have to reverse engineer their API. ### Home Assistant configuration templates -Auto discovery is enabled, for further integrations see [HA-MQTT.md](HA-MQTT.md). \ No newline at end of file +Auto discovery is enabled, for further integrations see [HA-MQTT.md](HA-MQTT.md). + +### TODO +1. Add travis docker build w/ push to docker hub +1. Logging library \ No newline at end of file diff --git a/src/diagnostic.js b/src/diagnostic.js index 90c79a3..9a8bbd3 100644 --- a/src/diagnostic.js +++ b/src/diagnostic.js @@ -2,9 +2,6 @@ const _ = require('lodash'); const Measurement = require('./measurement'); -/** - * - */ class Diagnostic { constructor(diagResponse) { this.name = diagResponse.name; @@ -49,4 +46,4 @@ class DiagnosticElement { } } -module.exports = { Diagnostic, DiagnosticElement }; \ No newline at end of file +module.exports = {Diagnostic, DiagnosticElement}; \ No newline at end of file diff --git a/src/index.js b/src/index.js index 37046d3..003ddc7 100644 --- a/src/index.js +++ b/src/index.js @@ -3,7 +3,8 @@ const mqtt = require('async-mqtt'); const uuidv4 = require('uuid').v4; const _ = require('lodash'); const Vehicle = require('./vehicle'); -const { Diagnostic } = require('./diagnostic'); +const {Diagnostic} = require('./diagnostic'); +const MQTT = require('./mqtt'); const onstarConfig = { deviceId: process.env.ONSTAR_DEVICEID || uuidv4(), @@ -11,60 +12,77 @@ const onstarConfig = { username: process.env.ONSTAR_USERNAME, password: process.env.ONSTAR_PASSWORD, onStarPin: process.env.ONSTAR_PIN, - checkRequestStatus: process.env.ONSTAR_SYNC == "true", - refreshInterval: 30*60*1000 // 30min TODO: configurable + checkRequestStatus: process.env.ONSTAR_SYNC === "true", + refreshInterval: parseInt(process.env.ONSTAR_REFRESH) || (30 * 60 * 1000) // 30 min }; const mqttConfig = { - host: process.env.MQTT_HOST, + host: process.env.MQTT_HOST || 'localhost', username: process.env.MQTT_USERNAME, password: process.env.MQTT_PASSWORD, - port: process.env.MQTT_PORT, - tls: process.env.MQTT_TLS, - prefix: process.env.MQTT_PREFIX, + port: parseInt(process.env.MQTT_PORT) || 1883, + tls: process.env.MQTT_TLS || false, + prefix: process.env.MQTT_PREFIX || 'homeassistant', }; -const connectionHandler = async client => { - -}; +let loop; +(async () => { + try { + const onStar = OnStar.create(onstarConfig); + const client = await mqtt.connectAsync(`${mqttConfig.tls + ? 'mqtts' : 'mqtt'}://${mqttConfig.host}:${mqttConfig.port}`, + { username: mqttConfig.username, password: mqttConfig.password }); -const messageHandler = async client => { - -}; - -const run = async () => { - const onStar = OnStar.create(onstarConfig); - // const client = await mqtt.connectAsync(`${mqttConfig ? 'mqtt' : 'mqtts'}://${mqttConfig.host}:${mqttConfig.port}`, { - // username, password - // } = mqttConfig); - - // connectionHandler(); - console.log('Requesting vehicles.'); - const vehiclesRes = await onStar.getAccountVehicles().catch(err => console.error(err)); - console.log(_.get(vehiclesRes, 'status')); - const vehicles = _.map( - _.get(vehiclesRes, 'response.data.vehicles.vehicle'), - v => new Vehicle(v) - ); - console.log('Vehicles returned:'); - - // Note: the library is set to use only the configured VIN, but using multiple for future proofing. - for (const v of vehicles) { - console.log(v.toString()); - - console.log('Requesting diagnostics:') - const statsRes = await onStar.diagnostics({ - diagnosticItem: v.getSupported() - }).catch(err => console.error(err)); - console.log(_.get(statsRes, 'status')); - const stats = _.map( - _.get(statsRes, 'response.data.commandResponse.body.diagnosticResponse'), - d => new Diagnostic(d) + console.log('Requesting vehicles.'); + const vehiclesRes = await onStar.getAccountVehicles(); + console.log(_.get(vehiclesRes, 'status')); + const vehicles = _.map( + _.get(vehiclesRes, 'response.data.vehicles.vehicle'), + v => new Vehicle(v) ); - _.forEach(stats, s => console.log(s.toString())); - } -}; + console.log('Vehicles returned:'); + for (const v of vehicles) { + console.log(v.toString()); + } + const mqttHA = new MQTT('homeassistant', vehicles[0].vin); -run() - .then(() => console.log('Done, exiting.')) - .catch(e => console.error(e)); + const run = async () => { + // Note: the library is set to use only the configured VIN, but using multiple for future proofing. + for (const v of vehicles) { + console.log('Requesting diagnostics:') + const statsRes = await onStar.diagnostics({ + diagnosticItem: v.getSupported() + }); + console.log(_.get(statsRes, 'status')); + const stats = _.map( + _.get(statsRes, 'response.data.commandResponse.body.diagnosticResponse'), + d => new Diagnostic(d) + ); + + for (const s of stats) { + if (!s.hasElements()) { + continue; + } + // configure, then set state + for (const d of s.diagnosticElements) { + console.log(mqttHA.getConfigTopic(d)); + console.log(JSON.stringify(mqttHA.getConfigPayload(s, d))); + await client.publish(mqttHA.getConfigTopic(d), JSON.stringify(mqttHA.getConfigPayload(s, d))); + } + console.log(mqttHA.getStateTopic(s)); + console.log(JSON.stringify(mqttHA.getStatePayload(s))); + await client.publish(mqttHA.getStateTopic(s), JSON.stringify(mqttHA.getStatePayload(s))); + } + } + }; + + const main = () => run() + .then(() => console.log('Done, sleeping.')) + .catch(e => console.error(e)) + + main(); + loop = setInterval(main, onstarConfig.refreshInterval); + } catch (e) { + console.error(e); + } +})(); \ No newline at end of file diff --git a/src/measurement.js b/src/measurement.js index 0eec7f5..5aecc05 100644 --- a/src/measurement.js +++ b/src/measurement.js @@ -13,11 +13,16 @@ class Measurement { */ static correctUnitName(unit) { switch (unit) { - case 'Cel': return '°C'; - case 'kwh': return 'kWh'; - case 'KM': return 'km'; - case 'KPa': return 'kPa'; - case 'kmple': return 'km/l(e)'; // TODO check on this + case 'Cel': + return '°C'; + case 'kwh': + return 'kWh'; + case 'KM': + return 'km'; + case 'KPa': + return 'kPa'; + case 'kmple': + return 'km/l(e)'; // TODO check on this case 'volts': case 'Volts': return 'V'; @@ -26,7 +31,8 @@ class Measurement { case 'N/A': return undefined; - default: return unit; + default: + return unit; } } diff --git a/src/mqtt.js b/src/mqtt.js index 885c541..3cd9a37 100644 --- a/src/mqtt.js +++ b/src/mqtt.js @@ -49,6 +49,18 @@ class MQTT { return _.startCase(_.lowerCase(name)); } + static determineSensorType(name) { + switch (name) { + case 'EV CHARGE STATE': + case 'EV PLUG STATE': + case 'PRIORITY CHARGE INDICATOR': + case 'PRIORITY CHARGE STATUS': + return 'binary_sensor'; + default: + return 'sensor'; + } + } + /** * @param {'sensor'|'binary_sensor'} type * @returns {string} @@ -59,40 +71,41 @@ class MQTT { /** * - * @param {DiagnosticElement} diagnostic + * @param {DiagnosticElement} diag */ - getConfigTopic(diagnostic) { - return `${this.getBaseTopic()}/${MQTT.convertName(diagnostic.name)}/config`; + getConfigTopic(diag) { + let sensorType = MQTT.determineSensorType(diag.name); + return `${this.getBaseTopic(sensorType)}/${MQTT.convertName(diag.name)}/config`; } /** * + * @param {Diagnostic} diag + */ + getStateTopic(diag) { + let sensorType = MQTT.determineSensorType(diag.name); + return `${this.getBaseTopic(sensorType)}/${MQTT.convertName(diag.name)}/state`; + } + + /** + * + * @param {Diagnostic} diag * @param {DiagnosticElement} diagEl - * @param {'sensor'|'binary_sensor'} sensorType */ - getStateTopic(diagEl, sensorType = 'sensor') { - return `${this.getBaseTopic(sensorType)}/${MQTT.convertName(diagEl.name)}/state`; - } - - /** - * - * @param {Diagnostic} diagnostic - * @param {DiagnosticElement} diagnosticElement - */ - getConfigPayload(diagnostic, diagnosticElement) { - return this.getConfigMapping(diagnostic, diagnosticElement); + getConfigPayload(diag, diagEl) { + return this.getConfigMapping(diag, diagEl); } /** * Return the state payload for this diagnostic - * @param {Diagnostic} diagnostic + * @param {Diagnostic} diag */ - getStatePayload(diagnostic) { + getStatePayload(diag) { const state = {}; - _.forEach(diagnostic.diagnosticElements, e => { + _.forEach(diag.diagnosticElements, e => { // massage the binary_sensor values let value; - switch(e.name) { + switch (e.name) { case 'EV PLUG STATE': // unplugged/plugged value = e.value === 'plugged'; break; @@ -116,15 +129,15 @@ class MQTT { return state; } - mapConfigPayload(diag, diagEl, device_class, name, sensorType = 'sensor', attr) { + mapConfigPayload(diag, diagEl, device_class, name, attr) { name = name || MQTT.convertFriendlyName(diagEl.name); // TODO availability return { device_class, name, - state_topic: this.getStateTopic(diag, sensorType), + state_topic: this.getStateTopic(diag), unit_of_measurement: diagEl.unit, - value_template: `{{ value_json.${MQTT.convertName(diagEl.name)} }`, + value_template: `{{ value_json.${MQTT.convertName(diagEl.name)} }}`, json_attributes_template: attr }; } @@ -150,21 +163,21 @@ class MQTT { case 'EV BATTERY LEVEL': return this.mapConfigPayload(diag, diagEl, 'battery'); case 'TIRE PRESSURE LF': - return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Front', 'sensor', "{{ {'recommendation': value_json.TIRE_PRESSURE_PLACARD_FRONT} | tojson }}"); + return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Front', "{{ {'recommendation': value_json.TIRE_PRESSURE_PLACARD_FRONT} | tojson }}"); case 'TIRE PRESSURE LR': - return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Rear', 'sensor', "{{ {'recommendation': value_json.TIRE_PRESSURE_PLACARD_FRONT} | tojson }}"); + return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Rear', "{{ {'recommendation': value_json.TIRE_PRESSURE_PLACARD_FRONT} | tojson }}"); case 'TIRE PRESSURE RF': - return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Front', 'sensor', "{{ {'recommendation': value_json.TIRE_PRESSURE_PLACARD_REAR} | tojson }}"); + return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Front', "{{ {'recommendation': value_json.TIRE_PRESSURE_PLACARD_REAR} | tojson }}"); case 'TIRE PRESSURE RR': - return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Rear', 'sensor', "{{ {'recommendation': value_json.TIRE_PRESSURE_PLACARD_REAR} | tojson }}"); + return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Rear', "{{ {'recommendation': value_json.TIRE_PRESSURE_PLACARD_REAR} | tojson }}"); // binary sensor case 'EV PLUG STATE': // unplugged/plugged - return this.mapConfigPayload(diag, diagEl, 'plug', undefined, 'binary_sensor'); + return this.mapConfigPayload(diag, diagEl, 'plug'); case 'EV CHARGE STATE': // not_charging/charging - return this.mapConfigPayload(diag, diagEl, 'battery_charging', undefined, 'binary_sensor'); + return this.mapConfigPayload(diag, diagEl, 'battery_charging'); + // binary_sensor, but no applicable device_class case 'PRIORITY CHARGE INDICATOR': // FALSE/TRUE case 'PRIORITY CHARGE STATUS': // NOT_ACTIVE/ACTIVE - return this.mapConfigPayload(diag, diagEl, undefined, undefined, 'binary_sensor'); // no device class, camel case name case 'EV RANGE': case 'ODOMETER': diff --git a/test/mqtt.spec.js b/test/mqtt.spec.js index acc2f1e..2fb546b 100644 --- a/test/mqtt.spec.js +++ b/test/mqtt.spec.js @@ -35,17 +35,17 @@ describe('MQTT', () => { assert.strictEqual(mqtt.getConfigTopic(d), 'homeassistant/sensor/XXX/ambient_air_temperature/config'); }); it('should generate state topics', () => { - assert.strictEqual(mqtt.getStateTopic(d), 'homeassistant/sensor/XXX/ambient_air_temperature/state'); + assert.strictEqual(mqtt.getStateTopic(d, d.diagnosticElements[0]), 'homeassistant/sensor/XXX/ambient_air_temperature/state'); }); }); describe('binary_sensor', () => { beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[3]'))); it('should generate config topics', () => { - assert.strictEqual(mqtt.getConfigTopic(d), 'homeassistant/sensor/XXX/ev_charge_state/config'); + assert.strictEqual(mqtt.getConfigTopic(d), 'homeassistant/binary_sensor/XXX/ev_charge_state/config'); }); it('should generate state topics', () => { - assert.strictEqual(mqtt.getStateTopic(d.diagnosticElements[1]), 'homeassistant/sensor/XXX/priority_charge_indicator/state'); + assert.strictEqual(mqtt.getStateTopic(d.diagnosticElements[1]), 'homeassistant/binary_sensor/XXX/priority_charge_indicator/state'); }); }); }); @@ -61,7 +61,7 @@ describe('MQTT', () => { name: 'Ambient Air Temperature', state_topic: 'homeassistant/sensor/XXX/ambient_air_temperature/state', unit_of_measurement: '°C', - value_template: '{{ value_json.ambient_air_temperature }' + value_template: '{{ value_json.ambient_air_temperature }}' }); }); it('should generate state payloads', () => { @@ -80,7 +80,7 @@ describe('MQTT', () => { name: 'Priority Charge Indicator', state_topic: 'homeassistant/binary_sensor/XXX/ev_charge_state/state', unit_of_measurement: undefined, - value_template: '{{ value_json.priority_charge_indicator }' + value_template: '{{ value_json.priority_charge_indicator }}' }); }); it('should generate state payloads', () => {