From 87f2f858a05fc7eede681157f0b99b9c7be7e330 Mon Sep 17 00:00:00 2001 From: Michael Woods Date: Sun, 27 Dec 2020 21:21:13 -0500 Subject: [PATCH] retain messages, configure only at start --- README.md | 10 ++++++---- package.json | 7 +++++-- src/index.js | 49 ++++++++++++++++++++++++++++++++++------------- src/mqtt.js | 15 +++++++++++---- test/mqtt.spec.js | 29 ++++++++++++++++++---------- 5 files changed, 77 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 251c0fc..3ec0795 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,9 @@ # onstar2mqtt -A service that utilizes the [OnStarJS](https://github.com/samrum/OnStarJS) library to expose OnStar data to MQTT topics. Mostly focused around EVs, however happy to accept PRs for other vehicle types. +A service that utilizes the [OnStarJS](https://github.com/samrum/OnStarJS) library to expose OnStar data to MQTT topics. -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. +The functionality is mostly focused around EVs (specifically the Bolt EV), however PRs for other vehicle types are certainly welcome. + +There is no affiliation with this project and 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. ## Running Collect the following information: @@ -49,10 +51,10 @@ MQTT_PASSWORD= ``` ### Node.js It's a typical node.js application, define the same environment values as described in the docker sections and run with: -`npm run start`. Currently, only tested with Node.js 12.x. +`npm run start`. Currently, this is only tested with Node.js 12.x. ### Home Assistant configuration templates -MQTT auto discovery is enabled. For further integrations see [HA-MQTT.md](HA-MQTT.md). +MQTT auto discovery is enabled. For further integrations and screenshots see [HA-MQTT.md](HA-MQTT.md). ## Development ### Running diff --git a/package.json b/package.json index c857b62..15cbe36 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "onstar2mqtt", - "version": "1.0.2", + "version": "1.0.4", "description": "OnStarJS wrapper for MQTT", "main": "src/index.js", "scripts": { @@ -16,7 +16,10 @@ "onstar", "mqtt", "gm", - "chevrolet" + "chevrolet", + "homeassistant", + "home-assistant", + "home assistant" ], "author": "Michael Woods", "license": "MIT", diff --git a/src/index.js b/src/index.js index 145344d..f05dc28 100644 --- a/src/index.js +++ b/src/index.js @@ -29,10 +29,6 @@ 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 }); - console.log('Requesting vehicles.'); const vehiclesRes = await onStar.getAccountVehicles(); console.log(_.get(vehiclesRes, 'status')); @@ -44,9 +40,21 @@ let loop; for (const v of vehicles) { console.log(v.toString()); } - const mqttHA = new MQTT('homeassistant', vehicles[0].vin); + const mqttHA = new MQTT('homeassistant', vehicles[0].vin); + const availTopic = mqttHA.getAvailabilityTopic(); + const client = await mqtt.connectAsync(`${mqttConfig.tls + ? 'mqtts' : 'mqtt'}://${mqttConfig.host}:${mqttConfig.port}`, { + username: mqttConfig.username, + password: mqttConfig.password, + will: {topic: availTopic, payload: 'false', retain: true} + }); + + await client.publish(availTopic, 'true', {retain: true}); + + const configurations = new Map(); const run = async () => { + const states = new Map(); // 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:') @@ -63,24 +71,39 @@ let loop; if (!s.hasElements()) { continue; } - // configure, then set state + // configure once, then set or update states 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))); + const topic = mqttHA.getConfigTopic(d) + const payload = mqttHA.getConfigPayload(s, d); + configurations.set(topic, {configured: false, payload}); } - console.log(mqttHA.getStateTopic(s)); - console.log(JSON.stringify(mqttHA.getStatePayload(s))); - await client.publish(mqttHA.getStateTopic(s), JSON.stringify(mqttHA.getStatePayload(s))); + + const topic = mqttHA.getStateTopic(s); + const payload = mqttHA.getStatePayload(s); + states.set(topic, payload); } } + for (let [topic, config] of configurations) { + // configure once + if (!config.configured) { + config.configured = true; + const {payload} = config; + console.log(`${topic} ${JSON.stringify(payload)}`); + await client.publish(topic, JSON.stringify(payload), {retain: true}); + } + } + // update states + for (let [topic, state] of states) { + console.log(`${topic} ${JSON.stringify(state)}`); + await client.publish(topic, JSON.stringify(state), {retain: true}); + } }; const main = () => run() .then(() => console.log('Done, sleeping.')) .catch(e => console.error(e)) - main(); + await main(); loop = setInterval(main, onstarConfig.refreshInterval); } catch (e) { console.error(e); diff --git a/src/mqtt.js b/src/mqtt.js index 2963d95..8867ff9 100644 --- a/src/mqtt.js +++ b/src/mqtt.js @@ -69,6 +69,10 @@ class MQTT { return `${this.prefix}/${type}/${this.instance}`; } + getAvailabilityTopic() { + return `${this.prefix}/${this.instance}/available`; + } + /** * * @param {DiagnosticElement} diag @@ -135,6 +139,9 @@ class MQTT { return { device_class, name, + availability_topic: this.getAvailabilityTopic(), + payload_available: 'true', + payload_not_available: 'false', state_topic: this.getStateTopic(diag), unit_of_measurement: diagEl.unit, value_template: `{{ value_json.${MQTT.convertName(diagEl.name)} }}`, @@ -163,13 +170,13 @@ 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', `{{ {"recommendation": value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_FRONT')}} | tojson }}`); + return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Front', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_FRONT')}} | tojson }}`); case 'TIRE PRESSURE LR': - return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Rear', `{{ {"recommendation": value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_REAR')}} | tojson }}`); + return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Left Rear', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_REAR')}} | tojson }}`); case 'TIRE PRESSURE RF': - return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Front', `{{ {"recommendation": value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_FRONT')}} | tojson }}`); + return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Front', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_FRONT')}} | tojson }}`); case 'TIRE PRESSURE RR': - return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Rear', `{{ {"recommendation": value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_REAR')}} | tojson }}`); + return this.mapConfigPayload(diag, diagEl, 'pressure', 'Tire Pressure: Right Rear', `{{ {'recommendation': value_json.${MQTT.convertName('TIRE_PRESSURE_PLACARD_REAR')}} | tojson }}`); // binary sensor case 'EV PLUG STATE': // unplugged/plugged return this.mapConfigPayload(diag, diagEl, 'plug'); diff --git a/test/mqtt.spec.js b/test/mqtt.spec.js index 6469be5..f3ae6ae 100644 --- a/test/mqtt.spec.js +++ b/test/mqtt.spec.js @@ -56,9 +56,12 @@ describe('MQTT', () => { beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[0]'))); it('should generate config payloads', () => { assert.deepStrictEqual(mqtt.getConfigPayload(d, d.diagnosticElements[0]), { + availability_topic: 'homeassistant/XXX/available', device_class: 'temperature', json_attributes_template: undefined, name: 'Ambient Air Temperature', + payload_available: 'true', + payload_not_available: 'false', state_topic: 'homeassistant/sensor/XXX/ambient_air_temperature/state', unit_of_measurement: '°C', value_template: '{{ value_json.ambient_air_temperature }}' @@ -75,19 +78,22 @@ describe('MQTT', () => { beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[3]'))); it('should generate config payloads', () => { assert.deepStrictEqual(mqtt.getConfigPayload(d, d.diagnosticElements[1]), { - device_class: undefined, - json_attributes_template: undefined, - 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 }}' + availability_topic: 'homeassistant/XXX/available', + device_class: undefined, + json_attributes_template: undefined, + name: 'Priority Charge Indicator', + payload_available: 'true', + payload_not_available: 'false', + state_topic: 'homeassistant/binary_sensor/XXX/ev_charge_state/state', + unit_of_measurement: undefined, + value_template: '{{ value_json.priority_charge_indicator }}' }); }); it('should generate state payloads', () => { assert.deepStrictEqual(mqtt.getStatePayload(d), { - ev_charge_state: false, - priority_charge_indicator: false, - priority_charge_status: false + ev_charge_state: false, + priority_charge_indicator: false, + priority_charge_status: false }); }); }); @@ -96,9 +102,12 @@ describe('MQTT', () => { beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[8]'))); it('should generate payloads with an attribute', () => { assert.deepStrictEqual(mqtt.getConfigPayload(d, d.diagnosticElements[0]), { + availability_topic: 'homeassistant/XXX/available', device_class: 'pressure', - json_attributes_template: '{{ {"recommendation": value_json.tire_pressure_placard_front} | tojson }}', + json_attributes_template: "{{ {'recommendation': value_json.tire_pressure_placard_front} | tojson }}", name: 'Tire Pressure: Left Front', + payload_available: 'true', + payload_not_available: 'false', state_topic: 'homeassistant/sensor/XXX/tire_pressure/state', unit_of_measurement: 'kPa', value_template: '{{ value_json.tire_pressure_lf }}'