retain messages, configure only at start

This commit is contained in:
Michael Woods 2020-12-27 21:21:13 -05:00
parent 4dd146b6b1
commit 87f2f858a0
5 changed files with 77 additions and 33 deletions

View File

@ -1,7 +1,9 @@
# onstar2mqtt # 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 ## Running
Collect the following information: Collect the following information:
@ -49,10 +51,10 @@ MQTT_PASSWORD=
``` ```
### Node.js ### Node.js
It's a typical node.js application, define the same environment values as described in the docker sections and run with: 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 ### 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 ## Development
### Running ### Running

View File

@ -1,6 +1,6 @@
{ {
"name": "onstar2mqtt", "name": "onstar2mqtt",
"version": "1.0.2", "version": "1.0.4",
"description": "OnStarJS wrapper for MQTT", "description": "OnStarJS wrapper for MQTT",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {
@ -16,7 +16,10 @@
"onstar", "onstar",
"mqtt", "mqtt",
"gm", "gm",
"chevrolet" "chevrolet",
"homeassistant",
"home-assistant",
"home assistant"
], ],
"author": "Michael Woods", "author": "Michael Woods",
"license": "MIT", "license": "MIT",

View File

@ -29,10 +29,6 @@ let loop;
(async () => { (async () => {
try { try {
const onStar = OnStar.create(onstarConfig); 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.'); console.log('Requesting vehicles.');
const vehiclesRes = await onStar.getAccountVehicles(); const vehiclesRes = await onStar.getAccountVehicles();
console.log(_.get(vehiclesRes, 'status')); console.log(_.get(vehiclesRes, 'status'));
@ -44,9 +40,21 @@ let loop;
for (const v of vehicles) { for (const v of vehicles) {
console.log(v.toString()); 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 run = async () => {
const states = new Map();
// Note: the library is set to use only the configured VIN, but using multiple for future proofing. // Note: the library is set to use only the configured VIN, but using multiple for future proofing.
for (const v of vehicles) { for (const v of vehicles) {
console.log('Requesting diagnostics:') console.log('Requesting diagnostics:')
@ -63,24 +71,39 @@ let loop;
if (!s.hasElements()) { if (!s.hasElements()) {
continue; continue;
} }
// configure, then set state // configure once, then set or update states
for (const d of s.diagnosticElements) { for (const d of s.diagnosticElements) {
console.log(mqttHA.getConfigTopic(d)); const topic = mqttHA.getConfigTopic(d)
console.log(JSON.stringify(mqttHA.getConfigPayload(s, d))); const payload = mqttHA.getConfigPayload(s, d);
await client.publish(mqttHA.getConfigTopic(d), JSON.stringify(mqttHA.getConfigPayload(s, d))); configurations.set(topic, {configured: false, payload});
} }
console.log(mqttHA.getStateTopic(s));
console.log(JSON.stringify(mqttHA.getStatePayload(s))); const topic = mqttHA.getStateTopic(s);
await client.publish(mqttHA.getStateTopic(s), JSON.stringify(mqttHA.getStatePayload(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() const main = () => run()
.then(() => console.log('Done, sleeping.')) .then(() => console.log('Done, sleeping.'))
.catch(e => console.error(e)) .catch(e => console.error(e))
main(); await main();
loop = setInterval(main, onstarConfig.refreshInterval); loop = setInterval(main, onstarConfig.refreshInterval);
} catch (e) { } catch (e) {
console.error(e); console.error(e);

View File

@ -69,6 +69,10 @@ class MQTT {
return `${this.prefix}/${type}/${this.instance}`; return `${this.prefix}/${type}/${this.instance}`;
} }
getAvailabilityTopic() {
return `${this.prefix}/${this.instance}/available`;
}
/** /**
* *
* @param {DiagnosticElement} diag * @param {DiagnosticElement} diag
@ -135,6 +139,9 @@ class MQTT {
return { return {
device_class, device_class,
name, name,
availability_topic: this.getAvailabilityTopic(),
payload_available: 'true',
payload_not_available: 'false',
state_topic: this.getStateTopic(diag), state_topic: this.getStateTopic(diag),
unit_of_measurement: diagEl.unit, unit_of_measurement: diagEl.unit,
value_template: `{{ value_json.${MQTT.convertName(diagEl.name)} }}`, value_template: `{{ value_json.${MQTT.convertName(diagEl.name)} }}`,
@ -163,13 +170,13 @@ class MQTT {
case 'EV BATTERY LEVEL': case 'EV BATTERY LEVEL':
return this.mapConfigPayload(diag, diagEl, 'battery'); return this.mapConfigPayload(diag, diagEl, 'battery');
case 'TIRE PRESSURE LF': 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': 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': 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': 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 // binary sensor
case 'EV PLUG STATE': // unplugged/plugged case 'EV PLUG STATE': // unplugged/plugged
return this.mapConfigPayload(diag, diagEl, 'plug'); return this.mapConfigPayload(diag, diagEl, 'plug');

View File

@ -56,9 +56,12 @@ describe('MQTT', () => {
beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[0]'))); beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[0]')));
it('should generate config payloads', () => { it('should generate config payloads', () => {
assert.deepStrictEqual(mqtt.getConfigPayload(d, d.diagnosticElements[0]), { assert.deepStrictEqual(mqtt.getConfigPayload(d, d.diagnosticElements[0]), {
availability_topic: 'homeassistant/XXX/available',
device_class: 'temperature', device_class: 'temperature',
json_attributes_template: undefined, json_attributes_template: undefined,
name: 'Ambient Air Temperature', name: 'Ambient Air Temperature',
payload_available: 'true',
payload_not_available: 'false',
state_topic: 'homeassistant/sensor/XXX/ambient_air_temperature/state', state_topic: 'homeassistant/sensor/XXX/ambient_air_temperature/state',
unit_of_measurement: '°C', unit_of_measurement: '°C',
value_template: '{{ value_json.ambient_air_temperature }}' value_template: '{{ value_json.ambient_air_temperature }}'
@ -75,19 +78,22 @@ describe('MQTT', () => {
beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[3]'))); beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[3]')));
it('should generate config payloads', () => { it('should generate config payloads', () => {
assert.deepStrictEqual(mqtt.getConfigPayload(d, d.diagnosticElements[1]), { assert.deepStrictEqual(mqtt.getConfigPayload(d, d.diagnosticElements[1]), {
device_class: undefined, availability_topic: 'homeassistant/XXX/available',
json_attributes_template: undefined, device_class: undefined,
name: 'Priority Charge Indicator', json_attributes_template: undefined,
state_topic: 'homeassistant/binary_sensor/XXX/ev_charge_state/state', name: 'Priority Charge Indicator',
unit_of_measurement: undefined, payload_available: 'true',
value_template: '{{ value_json.priority_charge_indicator }}' 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', () => { it('should generate state payloads', () => {
assert.deepStrictEqual(mqtt.getStatePayload(d), { assert.deepStrictEqual(mqtt.getStatePayload(d), {
ev_charge_state: false, ev_charge_state: false,
priority_charge_indicator: false, priority_charge_indicator: false,
priority_charge_status: false priority_charge_status: false
}); });
}); });
}); });
@ -96,9 +102,12 @@ describe('MQTT', () => {
beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[8]'))); beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[8]')));
it('should generate payloads with an attribute', () => { it('should generate payloads with an attribute', () => {
assert.deepStrictEqual(mqtt.getConfigPayload(d, d.diagnosticElements[0]), { assert.deepStrictEqual(mqtt.getConfigPayload(d, d.diagnosticElements[0]), {
availability_topic: 'homeassistant/XXX/available',
device_class: 'pressure', 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', name: 'Tire Pressure: Left Front',
payload_available: 'true',
payload_not_available: 'false',
state_topic: 'homeassistant/sensor/XXX/tire_pressure/state', state_topic: 'homeassistant/sensor/XXX/tire_pressure/state',
unit_of_measurement: 'kPa', unit_of_measurement: 'kPa',
value_template: '{{ value_json.tire_pressure_lf }}' value_template: '{{ value_json.tire_pressure_lf }}'