retain messages, configure only at start
This commit is contained in:
parent
4dd146b6b1
commit
87f2f858a0
10
README.md
10
README.md
@ -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
|
||||||
|
@ -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",
|
||||||
|
49
src/index.js
49
src/index.js
@ -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);
|
||||||
|
15
src/mqtt.js
15
src/mqtt.js
@ -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');
|
||||||
|
@ -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,9 +78,12 @@ 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]), {
|
||||||
|
availability_topic: 'homeassistant/XXX/available',
|
||||||
device_class: undefined,
|
device_class: undefined,
|
||||||
json_attributes_template: undefined,
|
json_attributes_template: undefined,
|
||||||
name: 'Priority Charge Indicator',
|
name: 'Priority Charge Indicator',
|
||||||
|
payload_available: 'true',
|
||||||
|
payload_not_available: 'false',
|
||||||
state_topic: 'homeassistant/binary_sensor/XXX/ev_charge_state/state',
|
state_topic: 'homeassistant/binary_sensor/XXX/ev_charge_state/state',
|
||||||
unit_of_measurement: undefined,
|
unit_of_measurement: undefined,
|
||||||
value_template: '{{ value_json.priority_charge_indicator }}'
|
value_template: '{{ value_json.priority_charge_indicator }}'
|
||||||
@ -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 }}'
|
||||||
|
Loading…
Reference in New Issue
Block a user