commit f39961eae384dbdbbc91aac2d78e0a0ab25a8461 Author: Michael Woods Date: Mon Nov 30 16:26:14 2020 -0500 Initial version that queries vehicles and its diagnostics then prints to console. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6cf89ba --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM node:12 + +RUN mkdir /app +WORKDIR /app + +COPY ["package.json", "/app/"] +RUN npm install + +COPY ["src", "/app/src"] + +ENTRYPOINT ["npm", "run", "start"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5c41c98 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020 Michael Woods + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..8d1c8b6 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ + diff --git a/package.json b/package.json new file mode 100644 index 0000000..ca9b15d --- /dev/null +++ b/package.json @@ -0,0 +1,36 @@ +{ + "name": "onstar2mqtt", + "version": "1.0.0", + "description": "OnStarJS wrapper for MQTT", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "test": "mocha" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/michaelwoods/onstar2mqtt.git" + }, + "keywords": [ + "onstar", + "mqtt", + "gm", + "chevrolet" + ], + "author": "Michael Woods", + "license": "MIT", + "bugs": { + "url": "https://github.com/michaelwoods/onstar2mqtt/issues" + }, + "homepage": "https://github.com/michaelwoods/onstar2mqtt#readme", + "dependencies": { + "async-mqtt": "^2.6.1", + "convert-units": "^2.3.4", + "lodash": "^4.17.20", + "onstarjs": "^2.0.10", + "uuid": "^8.3.1" + }, + "devDependencies": { + "mocha": "^8.2.1" + } +} diff --git a/src/diagnostic.js b/src/diagnostic.js new file mode 100644 index 0000000..26ad6b3 --- /dev/null +++ b/src/diagnostic.js @@ -0,0 +1,40 @@ +const _ = require('lodash'); + +const Measurement = require('./measurement'); + +class Diagnostic { + constructor(diagResponse) { + this.name = diagResponse.name; + const validEle = _.filter( + diagResponse.diagnosticElement, + d => _.has(d, 'value') && _.has(d, 'unit') + ); + this.diagnosticElements = _.map(validEle, e => new DiagnosticElement(e)); + } + + elementsToString(diag) { + const validEle = _.filter(diag, d => _.has(d, 'value') && _.has(d, 'unit')); + let output = ''; + _.forEach(validEle, e => output += ` ${e.name} ${e.value}${e.unit}\n`); + return output; + } + + toString() { + let elements = ''; + _.forEach(this.diagnosticElements, e => elements += ` ${e.toString()}\n`) + return `${this.name}:\n` + elements; + } +} + +class DiagnosticElement { + constructor(ele) { + this.name = ele.name; + this.measurement = new Measurement(ele.value, ele.unit); + } + + toString() { + return `${this.name}: ${this.measurement.toString()}`; + } +} + +module.exports = { Diagnostic, DiagnosticElement }; \ No newline at end of file diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..37046d3 --- /dev/null +++ b/src/index.js @@ -0,0 +1,70 @@ +const OnStar = require('onstarjs'); +const mqtt = require('async-mqtt'); +const uuidv4 = require('uuid').v4; +const _ = require('lodash'); +const Vehicle = require('./vehicle'); +const { Diagnostic } = require('./diagnostic'); + +const onstarConfig = { + deviceId: process.env.ONSTAR_DEVICEID || uuidv4(), + vin: process.env.ONSTAR_VIN, + 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 +}; + +const mqttConfig = { + host: process.env.MQTT_HOST, + 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, +}; + +const connectionHandler = async client => { + +}; + +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) + ); + _.forEach(stats, s => console.log(s.toString())); + } +}; + +run() + .then(() => console.log('Done, exiting.')) + .catch(e => console.error(e)); diff --git a/src/measurement.js b/src/measurement.js new file mode 100644 index 0000000..02bc380 --- /dev/null +++ b/src/measurement.js @@ -0,0 +1,40 @@ +const convert = require('convert-units'); + +class Measurement { + constructor(value, unit) { + this.value = value; + this.unit = Measurement.correctUnitName(unit); + } + + 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 'volts': return 'V'; + case 'Stat': + case 'N/A': + return ''; + default: return unit; + } + } + + // TODO this may not be required. Check consuming application. + static convertToImperial(value, unit) { + switch(unit) { + case 'Cel': + const val = convert(value).from('C').to('F'); + return new Measurement(val, 'F'); + default: + return new Measurement(value, unit); + } + } + + toString() { + return `${this.value}${this.unit}`; + } +} + +module.exports = Measurement; \ No newline at end of file diff --git a/src/mqtt.js b/src/mqtt.js new file mode 100644 index 0000000..2707baf --- /dev/null +++ b/src/mqtt.js @@ -0,0 +1,4 @@ + +class MQTT { + +} diff --git a/src/vehicle.js b/src/vehicle.js new file mode 100644 index 0000000..ebd7490 --- /dev/null +++ b/src/vehicle.js @@ -0,0 +1,34 @@ +const _ = require('lodash'); + +class Vehicle { + constructor(vehicle) { + this.make = vehicle.make; + this.model = vehicle.model; + this.vin = vehicle.vin; + this.year = vehicle.year; + + const diagCmd = _.find( + _.get(vehicle, 'commands.command'), + cmd => cmd.name === 'diagnostics' + ); + this.supportedDiagnostics = _.get(diagCmd, + 'commandData.supportedDiagnostics.supportedDiagnostic'); + } + + isSupported(diag) { + return _.includes(this.supportedDiagnostics, diag); + } + + getSupported(diags = []) { + if (diags.length === 0) { + return this.supportedDiagnostics; + } + return _.intersection(this.supportedDiagnostics, diags); + } + + toString() { + return `${this.year} ${this.make} ${this.model} ${this.vin}`; + } +} + +module.exports = Vehicle; \ No newline at end of file diff --git a/test/diagnostic.sample.json b/test/diagnostic.sample.json new file mode 100644 index 0000000..df56f15 --- /dev/null +++ b/test/diagnostic.sample.json @@ -0,0 +1,275 @@ +{ + "commandResponse": { + "requestTime": "2020-11-30T12:00:00.000Z", + "completionTime": "2020-11-30T00:00:00.000Z", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/requests/fizzbuzzREQ", + "status": "success", + "type": "diagnostics", + "body": { + "diagnosticResponse": [ + { + "name": "AMBIENT AIR TEMPERATURE", + "diagnosticElement": [ + { + "name": "AMBIENT AIR TEMPERATURE", + "status": "NA", + "message": "na", + "value": "15", + "unit": "Cel" + } + ] + }, + { + "name": "CHARGER POWER LEVEL", + "diagnosticElement": [ + { + "name": "CHARGER POWER LEVEL", + "status": "NA", + "message": "na", + "value": "NO_REDUCTION", + "unit": "N/A" + } + ] + }, + { + "name": "ENERGY EFFICIENCY", + "diagnosticElement": [ + { + "name": "CO2 AVOIDED", + "status": "NA", + "message": "na" + }, + { + "name": "ELECTRIC ECONOMY", + "status": "NA", + "message": "na", + "value": "21.85", + "unit": "kwh" + }, + { + "name": "FUEL AVOIDED", + "status": "NA", + "message": "na" + }, + { + "name": "GAS MILES", + "status": "NA", + "message": "na" + }, + { + "name": "LIFETIME EFFICIENCY", + "status": "NA", + "message": "na", + "value": "21.85", + "unit": "kwh" + }, + { + "name": "LIFETIME EV ODO", + "status": "NA", + "message": "na" + }, + { + "name": "LIFETIME FUEL ECON", + "status": "NA", + "message": "na" + }, + { + "name": "LIFETIME MPGE", + "status": "NA", + "message": "na", + "value": "40.73", + "unit": "kmple" + }, + { + "name": "ODOMETER", + "status": "NA", + "message": "na", + "value": "6013.8", + "unit": "KM" + } + ] + }, + { + "name": "EV CHARGE STATE", + "diagnosticElement": [ + { + "name": "EV CHARGE STATE", + "status": "NA", + "message": "charging_complete", + "value": "charging_complete", + "unit": "N/A" + }, + { + "name": "PRIORITY CHARGE INDICATOR", + "status": "NA", + "message": "na", + "value": "FALSE", + "unit": "N/A" + }, + { + "name": "PRIORITY CHARGE STATUS", + "status": "NA", + "message": "na", + "value": "NOT_ACTIVE", + "unit": "N/A" + }, + { + "name": "PRIORITY_CHARGE_STATUS", + "status": "NA", + "message": "na" + } + ] + }, + { + "name": "EV PLUG STATE", + "diagnosticElement": [ + { + "name": "EV PLUG STATE", + "status": "NA", + "message": "plugged", + "value": "plugged", + "unit": "Stat" + } + ] + }, + { + "name": "EV SCHEDULED CHARGE START", + "diagnosticElement": [ + { + "name": "EV SCHEDULED CHARGE START 120V DAY", + "status": "NA", + "message": "na", + "value": "Monday" + }, + { + "name": "EV SCHEDULED CHARGE START 240V DAY", + "status": "NA", + "message": "na", + "value": "Monday" + }, + { + "name": "SCHED CHG START 120V", + "status": "NA", + "message": "na", + "value": "11:30" + }, + { + "name": "SCHED CHG START 240V", + "status": "NA", + "message": "na", + "value": "11:30" + } + ] + }, + { + "name": "GET CHARGE MODE", + "diagnosticElement": [ + { + "name": "CHARGE MODE", + "status": "NA", + "message": "na", + "value": "DEPARTURE_BASED" + }, + { + "name": "RATE TYPE", + "status": "NA", + "message": "na", + "value": "PEAK" + } + ] + }, + { + "name": "ODOMETER", + "diagnosticElement": [ + { + "name": "ODOMETER", + "status": "NA", + "message": "na", + "value": "6013.8", + "unit": "KM" + } + ] + }, + { + "name": "TIRE PRESSURE", + "diagnosticElement": [ + { + "name": "TIRE PRESSURE LF", + "status": "NA", + "message": "YELLOW", + "value": "240.0", + "unit": "KPa" + }, + { + "name": "TIRE PRESSURE LR", + "status": "NA", + "message": "YELLOW", + "value": "236.0", + "unit": "KPa" + }, + { + "name": "TIRE PRESSURE PLACARD FRONT", + "status": "NA", + "message": "na", + "value": "262.0", + "unit": "KPa" + }, + { + "name": "TIRE PRESSURE PLACARD REAR", + "status": "NA", + "message": "na", + "value": "262.0", + "unit": "KPa" + }, + { + "name": "TIRE PRESSURE RF", + "status": "NA", + "message": "YELLOW", + "value": "236.0", + "unit": "KPa" + }, + { + "name": "TIRE PRESSURE RR", + "status": "NA", + "message": "YELLOW", + "value": "228.0", + "unit": "KPa" + } + ] + }, + { + "name": "VEHICLE RANGE", + "diagnosticElement": [ + { + "name": "EV MAX RANGE", + "status": "NA", + "message": "na" + }, + { + "name": "EV MIN RANGE", + "status": "NA", + "message": "na" + }, + { + "name": "EV RANGE", + "status": "NA", + "message": "na", + "value": "341.0", + "unit": "KM" + }, + { + "name": "GAS RANGE", + "status": "NA", + "message": "na" + }, + { + "name": "TOTAL RANGE", + "status": "NA", + "message": "na" + } + ] + } + ] + } + } +} diff --git a/test/diagnostic.spec.js b/test/diagnostic.spec.js new file mode 100644 index 0000000..edcabff --- /dev/null +++ b/test/diagnostic.spec.js @@ -0,0 +1,42 @@ +const assert = require('assert'); +const _ = require('lodash'); + +const { Diagnostic } = require('../src/diagnostic'); +const apiResponse = require('./diagnostic.sample.json'); + +describe('Diagnostics', () => { + let d; + + describe('Diagnostic', () => { + beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[0]'))); + + it('should parse a diagnostic response', () => { + assert.strictEqual(d.name, 'AMBIENT AIR TEMPERATURE'); + assert.strictEqual(d.diagnosticElements.length, 1); + }); + + it('should toString() correctly', () => { + const output = d.toString().trimEnd(); + const lines = output.split(/\r\n|\r|\n/); + assert.strictEqual(lines.length, 2); + assert.strictEqual(lines[0], 'AMBIENT AIR TEMPERATURE:'); + }); + }); + + describe('DiagnosticElement', () => { + beforeEach(() => d = new Diagnostic(_.get(apiResponse, 'commandResponse.body.diagnosticResponse[8]'))); + it('should parse a diagnostic element', () => { + assert.strictEqual(d.name, 'TIRE PRESSURE'); + assert.ok(_.isArray(d.diagnosticElements)); + assert.strictEqual(d.diagnosticElements.length, 6); + }); + + it('should toString() correctly', () => { + const output = d.toString().trimEnd(); + const lines = output.split(/\r\n|\r|\n/); + assert.strictEqual(lines.length, 7); + assert.strictEqual(lines[0], 'TIRE PRESSURE:'); + assert.strictEqual(lines[1], ' TIRE PRESSURE LF: 240.0kPa'); + }); + }); +}); diff --git a/test/vehicle.spec.js b/test/vehicle.spec.js new file mode 100644 index 0000000..9c66839 --- /dev/null +++ b/test/vehicle.spec.js @@ -0,0 +1,41 @@ +const assert = require('assert'); +const _ = require('lodash'); + +const Vehicle = require('../src/vehicle'); +const apiResponse = require('./vehicles.sample.json'); + +describe('Vehicle', () => { + let v; + beforeEach(() => v = new Vehicle(_.get(apiResponse, 'vehicles.vehicle[0]'))); + + it('should parse a vehicle response', () => { + assert.notStrictEqual(v.year, 2020); + assert.strictEqual(v.make, 'Chevrolet'); + assert.strictEqual(v.model, 'Bolt EV'); + assert.strictEqual(v.vin, 'foobarVIN'); + }); + + it('should return the list of supported diagnostics', () => { + const supported = v.getSupported(); + assert.ok(_.isArray(supported)); + assert.strictEqual(supported.length, 22); + }); + + it('should return common supported and requested diagnostics', () => { + let supported = v.getSupported(['ODOMETER']); + assert.ok(_.isArray(supported)); + assert.strictEqual(supported.length, 1); + + supported = v.getSupported(['ODOMETER', 'foo', 'bar']); + assert.ok(_.isArray(supported)); + assert.strictEqual(supported.length, 1); + + supported = v.getSupported(['foo', 'bar']); + assert.ok(_.isArray(supported)); + assert.strictEqual(supported.length, 0); + }); + + it('should toString() correctly', () => { + assert.strictEqual(v.toString(), '2020 Chevrolet Bolt EV foobarVIN') + }); +}); diff --git a/test/vehicles.sample.json b/test/vehicles.sample.json new file mode 100644 index 0000000..55d5244 --- /dev/null +++ b/test/vehicles.sample.json @@ -0,0 +1,237 @@ +{ + "vehicles": { + "size": "1", + "vehicle": [ + { + "vin": "foobarVIN", + "make": "Chevrolet", + "model": "Bolt EV", + "year": "2020", + "manufacturer": "General Motors", + "bodyStyle": "CAR", + "phone": "+5558675309", + "unitType": "EMBEDDED", + "onstarStatus": "ACTIVE", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN", + "isInPreActivation": "false", + "commands": { + "command": [ + { + "name": "cancelAlert", + "description": "Cancel a vehicle alert (honk horns/flash lights).", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/cancelAlert", + "isPrivSessionRequired": "false" + }, + { + "name": "getHotspotInfo", + "description": "Retrives the WiFi Hotspot info", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/hotspot/commands/getInfo", + "isPrivSessionRequired": "false" + }, + { + "name": "lockDoor", + "description": "Locks the doors.", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/lockDoor", + "isPrivSessionRequired": "false" + }, + { + "name": "unlockDoor", + "description": "Unlocks the doors.", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/unlockDoor", + "isPrivSessionRequired": "true" + }, + { + "name": "alert", + "description": "Triggers a vehicle alert (honk horns/flash lights).", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/alert", + "isPrivSessionRequired": "true" + }, + { + "name": "start", + "description": "Remotely starts the vehicle.", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/start", + "isPrivSessionRequired": "true" + }, + { + "name": "cancelStart", + "description": "Cancels previous remote start command.", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/cancelStart", + "isPrivSessionRequired": "false" + }, + { + "name": "diagnostics", + "description": "Retrieves diagnostic vehicle data.", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/diagnostics", + "isPrivSessionRequired": "false", + "commandData": { + "supportedDiagnostics": { + "supportedDiagnostic": [ + "LAST TRIP FUEL ECONOMY", + "ENERGY EFFICIENCY", + "HYBRID BATTERY MINIMUM TEMPERATURE", + "EV ESTIMATED CHARGE END", + "LIFETIME ENERGY USED", + "EV BATTERY LEVEL", + "EV PLUG VOLTAGE", + "HOTSPOT CONFIG", + "ODOMETER", + "HOTSPOT STATUS", + "CHARGER POWER LEVEL", + "LIFETIME EV ODOMETER", + "EV PLUG STATE", + "EV CHARGE STATE", + "TIRE PRESSURE", + "AMBIENT AIR TEMPERATURE", + "LAST TRIP DISTANCE", + "INTERM VOLT BATT VOLT", + "GET COMMUTE SCHEDULE", + "GET CHARGE MODE", + "EV SCHEDULED CHARGE START", + "VEHICLE RANGE" + ] + } + } + }, + { + "name": "location", + "description": "Retrieves the vehicle's current location.", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/location", + "isPrivSessionRequired": "true" + }, + { + "name": "chargeOverride", + "description": "Sends Charge Override", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/chargeOverride", + "isPrivSessionRequired": "false" + }, + { + "name": "getChargingProfile", + "description": "Gets the Charge Mode", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/getChargingProfile", + "isPrivSessionRequired": "false" + }, + { + "name": "getCommuteSchedule", + "description": "Gets the commuting schedule", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/getCommuteSchedule", + "isPrivSessionRequired": "false" + }, + { + "name": "connect", + "description": "Initiates a connection to the vehicle", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/connect", + "isPrivSessionRequired": "false" + }, + { + "name": "setChargingProfile", + "description": "Sets the charging profile", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/setChargingProfile", + "isPrivSessionRequired": "false" + }, + { + "name": "setCommuteSchedule", + "description": "Sets the commuting schedule", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/setCommuteSchedule", + "isPrivSessionRequired": "false" + }, + { + "name": "stopFastCharge", + "description": "Stops the charge", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/stopFastCharge", + "isPrivSessionRequired": "true" + }, + { + "name": "createTripPlan", + "description": "Create Trip Plan", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/createTripPlan", + "isPrivSessionRequired": "false" + }, + { + "name": "getTripPlan", + "description": "Provides the ability to retrieve an existing trip plan for an electric vehicle", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/getTripPlan", + "isPrivSessionRequired": "false" + }, + { + "name": "getHotspotStatus", + "description": "Retrive WiFi status", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/hotspot/commands/getStatus", + "isPrivSessionRequired": "false" + }, + { + "name": "setHotspotInfo", + "description": "update the WiFi SSID and passPhrase", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/hotspot/commands/setInfo", + "isPrivSessionRequired": "false" + }, + { + "name": "disableHotspot", + "description": "Disable WiFi Hotspot", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/hotspot/commands/disable", + "isPrivSessionRequired": "false" + }, + { + "name": "enableHotspot", + "description": "Enable WiFi Hotspot", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/hotspot/commands/enable", + "isPrivSessionRequired": "false" + }, + { + "name": "getRateSchedule", + "description": "Get EV Rate Schedule", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/getRateSchedule", + "isPrivSessionRequired": "true" + }, + { + "name": "setRateSchedule", + "description": "Set EV Rate Schedule.", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/setRateSchedule", + "isPrivSessionRequired": "true" + }, + { + "name": "getChargerPowerLevel", + "description": " Get the charger level", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/getChargerPowerLevel", + "isPrivSessionRequired": "false" + }, + { + "name": "setChargerPowerLevel", + "description": " Set the charger level", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/setChargerPowerLevel", + "isPrivSessionRequired": "false" + }, + { + "name": "setPriorityCharging", + "description": "Set priority charging", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/setPriorityCharging", + "isPrivSessionRequired": "false" + }, + { + "name": "getPriorityCharging", + "description": "Get priority charging", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/getPriorityCharging", + "isPrivSessionRequired": "false" + }, + { + "name": "stopCharge", + "description": "Sets the Stop Charge", + "url": "https://api.gm.com/api/v1/account/vehicles/foobarVIN/commands/stopCharge", + "isPrivSessionRequired": "true" + } + ] + }, + "modules": { + "module": [ + { + "moduleType": "BYOM2", + "moduleCapability": "SF3" + } + ] + }, + "propulsionType": "BEV", + "isSharedVehicle": "false", + "ownerAccount": "999999999" + } + ] + } +}