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

@ -3,4 +3,8 @@ 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.
### 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');
/**
*
*/
class Diagnostic {
constructor(diagResponse) {
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 _ = require('lodash');
const Vehicle = require('./vehicle');
const { Diagnostic } = require('./diagnostic');
const {Diagnostic} = require('./diagnostic');
const MQTT = require('./mqtt');
const onstarConfig = {
deviceId: process.env.ONSTAR_DEVICEID || uuidv4(),
@ -11,60 +12,77 @@ const onstarConfig = {
username: process.env.ONSTAR_USERNAME,
password: process.env.ONSTAR_PASSWORD,
onStarPin: process.env.ONSTAR_PIN,
checkRequestStatus: process.env.ONSTAR_SYNC == "true",
refreshInterval: 30*60*1000 // 30min TODO: configurable
checkRequestStatus: process.env.ONSTAR_SYNC === "true",
refreshInterval: parseInt(process.env.ONSTAR_REFRESH) || (30 * 60 * 1000) // 30 min
};
const mqttConfig = {
host: process.env.MQTT_HOST,
host: process.env.MQTT_HOST || 'localhost',
username: process.env.MQTT_USERNAME,
password: process.env.MQTT_PASSWORD,
port: process.env.MQTT_PORT,
tls: process.env.MQTT_TLS,
prefix: process.env.MQTT_PREFIX,
port: parseInt(process.env.MQTT_PORT) || 1883,
tls: process.env.MQTT_TLS || false,
prefix: process.env.MQTT_PREFIX || 'homeassistant',
};
const connectionHandler = async client => {
};
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 });
const messageHandler = async client => {
};
const run = async () => {
const onStar = OnStar.create(onstarConfig);
// const client = await mqtt.connectAsync(`${mqttConfig ? 'mqtt' : 'mqtts'}://${mqttConfig.host}:${mqttConfig.port}`, {
// username, password
// } = mqttConfig);
// connectionHandler();
console.log('Requesting vehicles.');
const vehiclesRes = await onStar.getAccountVehicles().catch(err => console.error(err));
console.log(_.get(vehiclesRes, 'status'));
const vehicles = _.map(
_.get(vehiclesRes, 'response.data.vehicles.vehicle'),
v => new Vehicle(v)
);
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) {
console.log(v.toString());
console.log('Requesting diagnostics:')
const statsRes = await onStar.diagnostics({
diagnosticItem: v.getSupported()
}).catch(err => console.error(err));
console.log(_.get(statsRes, 'status'));
const stats = _.map(
_.get(statsRes, 'response.data.commandResponse.body.diagnosticResponse'),
d => new Diagnostic(d)
console.log('Requesting vehicles.');
const vehiclesRes = await onStar.getAccountVehicles();
console.log(_.get(vehiclesRes, 'status'));
const vehicles = _.map(
_.get(vehiclesRes, 'response.data.vehicles.vehicle'),
v => new Vehicle(v)
);
_.forEach(stats, s => console.log(s.toString()));
}
};
console.log('Vehicles returned:');
for (const v of vehicles) {
console.log(v.toString());
}
const mqttHA = new MQTT('homeassistant', vehicles[0].vin);
run()
.then(() => console.log('Done, exiting.'))
.catch(e => console.error(e));
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:')
const statsRes = await onStar.diagnostics({
diagnosticItem: v.getSupported()
});
console.log(_.get(statsRes, 'status'));
const stats = _.map(
_.get(statsRes, 'response.data.commandResponse.body.diagnosticResponse'),
d => new Diagnostic(d)
);
for (const s of stats) {
if (!s.hasElements()) {
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) {
switch (unit) {
case 'Cel': return '°C';
case 'kwh': return 'kWh';
case 'KM': return 'km';
case 'KPa': return 'kPa';
case 'kmple': return 'km/l(e)'; // TODO check on this
case 'Cel':
return '°C';
case 'kwh':
return 'kWh';
case 'KM':
return 'km';
case 'KPa':
return 'kPa';
case 'kmple':
return 'km/l(e)'; // TODO check on this
case 'volts':
case 'Volts':
return 'V';
@ -26,7 +31,8 @@ class Measurement {
case 'N/A':
return undefined;
default: return unit;
default:
return unit;
}
}

View File

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

View File

@ -35,17 +35,17 @@ describe('MQTT', () => {
assert.strictEqual(mqtt.getConfigTopic(d), 'homeassistant/sensor/XXX/ambient_air_temperature/config');
});
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', () => {
beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[3]')));
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', () => {
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',
state_topic: 'homeassistant/sensor/XXX/ambient_air_temperature/state',
unit_of_measurement: '°C',
value_template: '{{ value_json.ambient_air_temperature }'
value_template: '{{ value_json.ambient_air_temperature }}'
});
});
it('should generate state payloads', () => {
@ -80,7 +80,7 @@ describe('MQTT', () => {
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 }'
value_template: '{{ value_json.priority_charge_indicator }}'
});
});
it('should generate state payloads', () => {