Added support for commands published to prefix/VIN/command topic.

This commit is contained in:
Michael Woods 2021-06-10 13:29:46 -04:00
parent 28698b7f59
commit f5dd435feb
4 changed files with 187 additions and 25 deletions

122
src/commands.js Normal file
View File

@ -0,0 +1,122 @@
class Commands {
static CONSTANTS = {
ALERT_ACTION: {
FLASH: 'Flash',
HONK: 'Honk',
},
ALERT_OVERRIDE: {
DOOR_OPEN: 'DoorOpen',
IGNITION_ON: 'IgnitionOn'
},
CHARGE_OVERRIDE: {
CHARGE_NOW: 'CHARGE_NOW',
CANCEL_OVERRIDE: 'CANCEL_OVERRIDE'
},
CHARGING_PROFILE_MODE: {
DEFAULT_IMMEDIATE: 'DEFAULT_IMMEDIATE',
IMMEDIATE: 'IMMEDIATE',
DEPARTURE_BASED: 'DEPARTURE_BASED',
RATE_BASED: 'RATE_BASED',
PHEV_AFTER_MIDNIGHT: 'PHEV_AFTER_MIDNIGHT'
},
CHARGING_PROFILE_RATE: {
OFFPEAK: 'OFFPEAK',
MIDPEAK: 'MIDPEAK',
PEAK: 'PEAK'
},
DIAGNOSTICS: {
ENGINE_COOLANT_TEMP: 'ENGINE COOLANT TEMP',
ENGINE_RPM: 'ENGINE RPM',
LAST_TRIP_FUEL_ECONOMY: 'LAST TRIP FUEL ECONOMY',
EV_ESTIMATED_CHARGE_END: 'EV ESTIMATED CHARGE END',
EV_BATTERY_LEVEL: 'EV BATTERY LEVEL',
OIL_LIFE: 'OIL LIFE',
EV_PLUG_VOLTAGE: 'EV PLUG VOLTAGE',
LIFETIME_FUEL_ECON: 'LIFETIME FUEL ECON',
HOTSPOT_CONFIG: 'HOTSPOT CONFIG',
LIFETIME_FUEL_USED: 'LIFETIME FUEL USED',
ODOMETER: 'ODOMETER',
HOTSPOT_STATUS: 'HOTSPOT STATUS',
LIFETIME_EV_ODOMETER: 'LIFETIME EV ODOMETER',
EV_PLUG_STATE: 'EV PLUG STATE',
EV_CHARGE_STATE: 'EV CHARGE STATE',
TIRE_PRESSURE: 'TIRE PRESSURE',
AMBIENT_AIR_TEMPERATURE: 'AMBIENT AIR TEMPERATURE',
LAST_TRIP_DISTANCE: 'LAST TRIP DISTANCE',
INTERM_VOLT_BATT_VOLT: 'INTERM VOLT BATT VOLT',
GET_COMMUTE_SCHEDULE: 'GET COMMUTE SCHEDULE',
GET_CHARGE_MODE: 'GET CHARGE MODE',
EV_SCHEDULED_CHARGE_START: 'EV SCHEDULED CHARGE START',
FUEL_TANK_INFO: 'FUEL TANK INFO',
HANDS_FREE_CALLING: 'HANDS FREE CALLING',
ENERGY_EFFICIENCY: 'ENERGY EFFICIENCY',
VEHICLE_RANGE: 'VEHICLE RANGE',
}
}
constructor(onstar) {
this.onstar = onstar;
}
async getAccountVehicles() {
return this.onstar.getAccountVehicles();
}
async startVehicle() {
return this.onstar.start();
}
async cancelStartVehicle() {
return this.onstar.cancelStart();
}
async alert({action = [Commands.CONSTANTS.ALERT_ACTION.FLASH],
delay = 0, duration = 1, override = []}) {
return this.onstar.alert({
action,
delay,
duration,
override
});
}
async cancelAlert() {
return this.onstar.cancelAlert();
}
async lockDoor({delay = 0}) {
return this.onstar.lockDoor({delay});
}
async unlockDoor({delay = 0}) {
return this.onstar.unlockDoor({delay});
}
async chargeOverride({mode = Commands.CONSTANTS.CHARGE_OVERRIDE.CHARGE_NOW}) {
return this.onstar.chargeOverride({mode});
}
async cancelChargeOverride({mode = Commands.CONSTANTS.CHARGE_OVERRIDE.CANCEL_OVERRIDE}) {
return this.onstar.chargeOverride({mode});
}
async getChargingProfile() {
return this.onstar.getChargingProfile();
}
async setChargingProfile() {
return this.onstar.setChargingProfile();
}
async diagnostics({diagnosticItem = [
Commands.CONSTANTS.DIAGNOSTICS.ODOMETER,
Commands.CONSTANTS.DIAGNOSTICS.TIRE_PRESSURE,
Commands.CONSTANTS.DIAGNOSTICS.AMBIENT_AIR_TEMPERATURE,
Commands.CONSTANTS.DIAGNOSTICS.LAST_TRIP_DISTANCE
]}) {
return this.onstar.diagnostics({diagnosticItem});
}
}
module.exports = Commands;

View File

@ -1,3 +1,4 @@
const OnStar = require('onstarjs');
const mqtt = require('async-mqtt');
const uuidv4 = require('uuid').v4;
@ -5,6 +6,7 @@ const _ = require('lodash');
const Vehicle = require('./vehicle');
const {Diagnostic} = require('./diagnostic');
const MQTT = require('./mqtt');
const Commands = require('./commands');
const onstarConfig = {
deviceId: process.env.ONSTAR_DEVICEID || uuidv4(),
@ -13,7 +15,8 @@ const onstarConfig = {
password: process.env.ONSTAR_PASSWORD,
onStarPin: process.env.ONSTAR_PIN,
checkRequestStatus: process.env.ONSTAR_SYNC === "true" || true,
refreshInterval: parseInt(process.env.ONSTAR_REFRESH) || (30 * 60 * 1000) // 30 min
refreshInterval: parseInt(process.env.ONSTAR_REFRESH) || (30 * 60 * 1000), // 30 min
allowCommands: _.toLower(_.get(process, 'env.ONSTAR_ALLOW_COMMANDS', 'true')) === 'true'
};
const mqttConfig = {
@ -25,39 +28,63 @@ const mqttConfig = {
prefix: process.env.MQTT_PREFIX || 'homeassistant',
};
let loop;
let loop, commands, vehicles;
const init = async () => {
commands = new Commands(OnStar.create(onstarConfig));
console.log('Requesting vehicles.');
const vehiclesRes = await commands.getAccountVehicles();
console.log(_.get(vehiclesRes, 'status'));
vehicles = _.map(
_.get(vehiclesRes, 'response.data.vehicles.vehicle'),
v => new Vehicle(v)
);
console.log('Vehicles returned:');
for (const v of vehicles) {
console.log(v.toString());
}
}
const connectMQTT = async () => {
const mqttHA = new MQTT(vehicles[0], 'homeassistant');
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}
});
if (onstarConfig.allowCommands) {
client.on('message', (topic, message) => {
console.log(`Subscription message: ${topic} ${message}`);
const {command, options} = JSON.parse(message);
const commandFn = commands[command].bind(commands);
commandFn(options || {})
.then(() => console.log(`Command completed: ${command}`))
.catch(err=> console.error(`Command error: ${command} ${err}`));
});
const topic = mqttHA.getCommandTopic();
console.log(`Subscribed to: ${topic}`);
await client.subscribe(topic);
}
await client.publish(availTopic, 'true', {retain: true});
return {mqttHA, client};
};
(async () => {
try {
const onStar = OnStar.create(onstarConfig);
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)
);
console.log('Vehicles returned:');
for (const v of vehicles) {
console.log(v.toString());
}
await init();
const mqttHA = new MQTT(vehicles[0], 'homeassistant');
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 {mqttHA, client} = await connectMQTT();
const configurations = new Map();
const run = async () => {
const states = new Map();
const v = vehicles[0];
console.log('Requesting diagnostics:')
const statsRes = await onStar.diagnostics({
const statsRes = await commands.diagnostics({
diagnosticItem: v.getSupported()
});
console.log(_.get(statsRes, 'status'));

View File

@ -74,6 +74,10 @@ class MQTT {
return `${this.prefix}/${this.instance}/available`;
}
getCommandTopic() {
return `${this.prefix}/${this.instance}/command`;
}
/**
*
* @param {DiagnosticElement} diag

View File

@ -30,6 +30,15 @@ describe('MQTT', () => {
describe('topics', () => {
let d;
it('should generate availability topic', () => {
assert.strictEqual(mqtt.getAvailabilityTopic(), 'homeassistant/XXX/available');
});
it('should generate command topic', () => {
assert.strictEqual(mqtt.getCommandTopic(), 'homeassistant/XXX/command');
});
describe('sensor', () => {
beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[0]')));