Small fixes and looping.

This commit is contained in:
Michael Woods 2020-12-02 14:11:50 -05:00
parent ed2c38f53f
commit d095287158
6 changed files with 131 additions and 93 deletions

View File

@ -4,3 +4,7 @@ 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. 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 ### Home Assistant configuration templates
Auto discovery is enabled, for further integrations see [HA-MQTT.md](HA-MQTT.md). 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

View File

@ -2,9 +2,6 @@ const _ = require('lodash');
const Measurement = require('./measurement'); const Measurement = require('./measurement');
/**
*
*/
class Diagnostic { class Diagnostic {
constructor(diagResponse) { constructor(diagResponse) {
this.name = diagResponse.name; this.name = diagResponse.name;
@ -49,4 +46,4 @@ class DiagnosticElement {
} }
} }
module.exports = { Diagnostic, DiagnosticElement }; module.exports = {Diagnostic, DiagnosticElement};

View File

@ -3,7 +3,8 @@ const mqtt = require('async-mqtt');
const uuidv4 = require('uuid').v4; const uuidv4 = require('uuid').v4;
const _ = require('lodash'); const _ = require('lodash');
const Vehicle = require('./vehicle'); const Vehicle = require('./vehicle');
const { Diagnostic } = require('./diagnostic'); const {Diagnostic} = require('./diagnostic');
const MQTT = require('./mqtt');
const onstarConfig = { const onstarConfig = {
deviceId: process.env.ONSTAR_DEVICEID || uuidv4(), deviceId: process.env.ONSTAR_DEVICEID || uuidv4(),
@ -11,60 +12,77 @@ const onstarConfig = {
username: process.env.ONSTAR_USERNAME, username: process.env.ONSTAR_USERNAME,
password: process.env.ONSTAR_PASSWORD, password: process.env.ONSTAR_PASSWORD,
onStarPin: process.env.ONSTAR_PIN, onStarPin: process.env.ONSTAR_PIN,
checkRequestStatus: process.env.ONSTAR_SYNC == "true", checkRequestStatus: process.env.ONSTAR_SYNC === "true",
refreshInterval: 30*60*1000 // 30min TODO: configurable refreshInterval: parseInt(process.env.ONSTAR_REFRESH) || (30 * 60 * 1000) // 30 min
}; };
const mqttConfig = { const mqttConfig = {
host: process.env.MQTT_HOST, host: process.env.MQTT_HOST || 'localhost',
username: process.env.MQTT_USERNAME, username: process.env.MQTT_USERNAME,
password: process.env.MQTT_PASSWORD, password: process.env.MQTT_PASSWORD,
port: process.env.MQTT_PORT, port: parseInt(process.env.MQTT_PORT) || 1883,
tls: process.env.MQTT_TLS, tls: process.env.MQTT_TLS || false,
prefix: process.env.MQTT_PREFIX, prefix: process.env.MQTT_PREFIX || 'homeassistant',
}; };
const connectionHandler = async client => { let loop;
(async () => {
}; try {
const messageHandler = async client => {
};
const run = async () => {
const onStar = OnStar.create(onstarConfig); const onStar = OnStar.create(onstarConfig);
// const client = await mqtt.connectAsync(`${mqttConfig ? 'mqtt' : 'mqtts'}://${mqttConfig.host}:${mqttConfig.port}`, { const client = await mqtt.connectAsync(`${mqttConfig.tls
// username, password ? 'mqtts' : 'mqtt'}://${mqttConfig.host}:${mqttConfig.port}`,
// } = mqttConfig); { username: mqttConfig.username, password: mqttConfig.password });
// connectionHandler();
console.log('Requesting vehicles.'); console.log('Requesting vehicles.');
const vehiclesRes = await onStar.getAccountVehicles().catch(err => console.error(err)); const vehiclesRes = await onStar.getAccountVehicles();
console.log(_.get(vehiclesRes, 'status')); console.log(_.get(vehiclesRes, 'status'));
const vehicles = _.map( const vehicles = _.map(
_.get(vehiclesRes, 'response.data.vehicles.vehicle'), _.get(vehiclesRes, 'response.data.vehicles.vehicle'),
v => new Vehicle(v) v => new Vehicle(v)
); );
console.log('Vehicles returned:'); 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) { for (const v of vehicles) {
console.log(v.toString()); console.log(v.toString());
}
const mqttHA = new MQTT('homeassistant', vehicles[0].vin);
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:') console.log('Requesting diagnostics:')
const statsRes = await onStar.diagnostics({ const statsRes = await onStar.diagnostics({
diagnosticItem: v.getSupported() diagnosticItem: v.getSupported()
}).catch(err => console.error(err)); });
console.log(_.get(statsRes, 'status')); console.log(_.get(statsRes, 'status'));
const stats = _.map( const stats = _.map(
_.get(statsRes, 'response.data.commandResponse.body.diagnosticResponse'), _.get(statsRes, 'response.data.commandResponse.body.diagnosticResponse'),
d => new Diagnostic(d) d => new Diagnostic(d)
); );
_.forEach(stats, s => console.log(s.toString()));
}
};
run() for (const s of stats) {
.then(() => console.log('Done, exiting.')) if (!s.hasElements()) {
.catch(e => console.error(e)); 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);
}
})();

View File

@ -13,11 +13,16 @@ class Measurement {
*/ */
static correctUnitName(unit) { static correctUnitName(unit) {
switch (unit) { switch (unit) {
case 'Cel': return '°C'; case 'Cel':
case 'kwh': return 'kWh'; return '°C';
case 'KM': return 'km'; case 'kwh':
case 'KPa': return 'kPa'; return 'kWh';
case 'kmple': return 'km/l(e)'; // TODO check on this case 'KM':
return 'km';
case 'KPa':
return 'kPa';
case 'kmple':
return 'km/l(e)'; // TODO check on this
case 'volts': case 'volts':
case 'Volts': case 'Volts':
return 'V'; return 'V';
@ -26,7 +31,8 @@ class Measurement {
case 'N/A': case 'N/A':
return undefined; return undefined;
default: return unit; default:
return unit;
} }
} }

View File

@ -49,6 +49,18 @@ class MQTT {
return _.startCase(_.lowerCase(name)); 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 * @param {'sensor'|'binary_sensor'} type
* @returns {string} * @returns {string}
@ -59,40 +71,41 @@ class MQTT {
/** /**
* *
* @param {DiagnosticElement} diagnostic * @param {DiagnosticElement} diag
*/ */
getConfigTopic(diagnostic) { getConfigTopic(diag) {
return `${this.getBaseTopic()}/${MQTT.convertName(diagnostic.name)}/config`; 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 {DiagnosticElement} diagEl
* @param {'sensor'|'binary_sensor'} sensorType
*/ */
getStateTopic(diagEl, sensorType = 'sensor') { getConfigPayload(diag, diagEl) {
return `${this.getBaseTopic(sensorType)}/${MQTT.convertName(diagEl.name)}/state`; return this.getConfigMapping(diag, diagEl);
}
/**
*
* @param {Diagnostic} diagnostic
* @param {DiagnosticElement} diagnosticElement
*/
getConfigPayload(diagnostic, diagnosticElement) {
return this.getConfigMapping(diagnostic, diagnosticElement);
} }
/** /**
* Return the state payload for this diagnostic * Return the state payload for this diagnostic
* @param {Diagnostic} diagnostic * @param {Diagnostic} diag
*/ */
getStatePayload(diagnostic) { getStatePayload(diag) {
const state = {}; const state = {};
_.forEach(diagnostic.diagnosticElements, e => { _.forEach(diag.diagnosticElements, e => {
// massage the binary_sensor values // massage the binary_sensor values
let value; let value;
switch(e.name) { switch (e.name) {
case 'EV PLUG STATE': // unplugged/plugged case 'EV PLUG STATE': // unplugged/plugged
value = e.value === 'plugged'; value = e.value === 'plugged';
break; break;
@ -116,15 +129,15 @@ class MQTT {
return state; return state;
} }
mapConfigPayload(diag, diagEl, device_class, name, sensorType = 'sensor', attr) { mapConfigPayload(diag, diagEl, device_class, name, attr) {
name = name || MQTT.convertFriendlyName(diagEl.name); name = name || MQTT.convertFriendlyName(diagEl.name);
// TODO availability // TODO availability
return { return {
device_class, device_class,
name, name,
state_topic: this.getStateTopic(diag, sensorType), 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)} }}`,
json_attributes_template: attr json_attributes_template: attr
}; };
} }
@ -150,21 +163,21 @@ 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', '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': 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': 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': 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 // binary sensor
case 'EV PLUG STATE': // unplugged/plugged 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 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 INDICATOR': // FALSE/TRUE
case 'PRIORITY CHARGE STATUS': // NOT_ACTIVE/ACTIVE case 'PRIORITY CHARGE STATUS': // NOT_ACTIVE/ACTIVE
return this.mapConfigPayload(diag, diagEl, undefined, undefined, 'binary_sensor');
// no device class, camel case name // no device class, camel case name
case 'EV RANGE': case 'EV RANGE':
case 'ODOMETER': case 'ODOMETER':

View File

@ -35,17 +35,17 @@ describe('MQTT', () => {
assert.strictEqual(mqtt.getConfigTopic(d), 'homeassistant/sensor/XXX/ambient_air_temperature/config'); assert.strictEqual(mqtt.getConfigTopic(d), 'homeassistant/sensor/XXX/ambient_air_temperature/config');
}); });
it('should generate state topics', () => { 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', () => { describe('binary_sensor', () => {
beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[3]'))); beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[3]')));
it('should generate config topics', () => { 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', () => { 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', name: 'Ambient Air Temperature',
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 }}'
}); });
}); });
it('should generate state payloads', () => { it('should generate state payloads', () => {
@ -80,7 +80,7 @@ describe('MQTT', () => {
name: 'Priority Charge Indicator', name: 'Priority Charge Indicator',
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 }}'
}); });
}); });
it('should generate state payloads', () => { it('should generate state payloads', () => {