diff --git a/src/commands.js b/src/commands.js new file mode 100644 index 0000000..2cf979b --- /dev/null +++ b/src/commands.js @@ -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; \ No newline at end of file diff --git a/src/index.js b/src/index.js index d6776ef..c7879fe 100644 --- a/src/index.js +++ b/src/index.js @@ -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')); diff --git a/src/mqtt.js b/src/mqtt.js index 395dc38..d31d479 100644 --- a/src/mqtt.js +++ b/src/mqtt.js @@ -74,6 +74,10 @@ class MQTT { return `${this.prefix}/${this.instance}/available`; } + getCommandTopic() { + return `${this.prefix}/${this.instance}/command`; + } + /** * * @param {DiagnosticElement} diag diff --git a/test/mqtt.spec.js b/test/mqtt.spec.js index ac08341..c61258d 100644 --- a/test/mqtt.spec.js +++ b/test/mqtt.spec.js @@ -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]')));