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
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

View File

@ -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",

View File

@ -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);

View File

@ -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');

View File

@ -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 }}'