UNPKG

teslams

Version:

Utilities for the Tesla Model S

671 lines (633 loc) 24.7 kB
'use strict'; var request = require('request'); var util = require('util'); var JSONbig = require('json-bigint'); var portal = 'https://owner-api.teslamotors.com/api/1'; exports.portal = portal; var owner_api = 'https://owner-api.teslamotors.com'; exports.portal = owner_api; var token = ''; exports.token = token; // emulate the android mobile app var version = '2.1.79'; var model = 'SM-G900V'; var codename = 'REL'; var release = '4.4.4'; var locale = 'en_US'; var user_agent = 'Model S ' + version + ' (' + model + '; Android ' + codename + ' ' + release + '; ' + locale + ')'; var x_tesla_user_agent = 'TeslaApp/3.4.4-350/fad4a582e/android/9.0.0'; //Common HTTP header variable for all requests. Includes authentication credentials (token) and user agent string var http_header; var report = function(error, response, body, cb) { if (!!cb) cb(error || (new Error(response.statusCode + ': ' + body)), body); }; var report2 = function(call, body, cb) { if (typeof cb === 'function') cb(new Error('expecting JSON response to ' + call + ' request'), body); }; // backwards-compatible with previous API // all() gives the callback the raw response to the /vehicles call // vehicles gives the callback the first vehicle in the array returned // get_vid gives the callback the ID of the first vehicle in the array returned var all = exports.all = function(options, cb) { if (!cb) cb = function(error, response, body) {/* jshint unused: false */}; //add option to call without using email and password if (options.token) { exports.token = options.token; // set common HTTP Header used for all requests http_header = { 'Authorization': 'Bearer ' + options.token, 'Content-Type': 'application/json; charset=utf-8', 'User-Agent': user_agent, 'X-Tesla-User-Agent': x_tesla_user_agent, //'Accept-Encoding': 'gzip' // 'Accept-Encoding': 'gzip,deflate' }; request( { method : 'GET', url: portal + '/vehicles', gzip: true, headers: http_header }, cb); } else { http_header = { 'Authorization': 'Bearer ' + options.token, 'Content-Type': 'application/json; charset=utf-8', 'User-Agent': user_agent, 'X-Tesla-User-Agent': x_tesla_user_agent, }; request( { method: 'POST', url: owner_api + '/oauth/token', gzip: true, headers: http_header, form: { "grant_type" : "password", "client_id" : 'e4a9949fcfa04068f59abb5a658f2bac0a3428e4652315490b659d5ab3f35a9e', "client_secret" : 'c75f14bbadc8bee3a7594412c31416f8300256d7668ea7e6e7f06727bfb9d220', "email" : options.email, "password" : options.password } }, function (error, response, body) { try{ var authdata = JSON.parse( body ); token = authdata.access_token; exports.token = token; // set common HTTP Header used for all requests http_header = { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json; charset=utf-8', 'User-Agent': user_agent, //'Accept-Encoding': 'gzip' // 'Accept-Encoding': 'gzip,deflate' }; } catch (e) { console.log( 'Error parsing response to oauth token request'); } if ((!!error) || ((response.statusCode !== 200) && (response.statusCode !== 302))) return report(error, response, body, cb); request( { method : 'GET', url: portal + '/vehicles', gzip: true, headers: http_header }, cb); }); } }; // returns first vehicle in list var vehicles = exports.vehicles = function(options, cb) { if (!cb) cb = function(data) {/* jshint unused: false */}; all(options, function (error, response, body) { var data; try { data = JSONbig.parse(body); } catch(err) { return cb(new Error('login failed\nerr: ' + err + '\nbody: ' + body)); } if (!util.isArray(data.response)) return cb(new Error('expecting an array from Tesla Motors cloud service')); data = data.response[0]; data.id = JSONbig.stringify(data.id); cb((!!data.id) ? data : (new Error('expecting vehicle ID from Tesla Motors cloud service'))); }); }; // returns ID of first vehicle in list as a string to avoid bigint issues exports.get_vid = function(options, cb) { vehicles(options, function(data) { if (!!data.id) data = data.id; if (!!cb) cb(data); }); }; function set_token( token ) { exports.token = token; // set common HTTP Header used for all requests http_header = { 'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json; charset=utf-8', 'User-Agent': user_agent, //'Accept-Encoding': 'gzip' // 'Accept-Encoding': 'gzip,deflate' }; } exports.set_token = set_token; function mobile_enabled( vid, cb ) { request( { method: 'GET', url: portal + '/vehicles/' + vid + '/mobile_enabled', gzip: true, headers: http_header }, function (error, response, body) { if ((!!error) || (response.statusCode !== 200)) return report(error, response, body, cb); try { var data = JSON.parse(body); if (typeof cb == 'function') return cb( data.response ); else return true; } catch (err) { return report2('mobile_enabled', body, cb); } }); } exports.mobile_enabled = mobile_enabled; function get_charge_state( vid, cb ) { request( { method: 'GET', url: portal + '/vehicles/' + vid + '/data_request/charge_state', gzip: true, headers: http_header }, function (error, response, body) { if ((!!error) || (response.statusCode !== 200)) return report(error, response, body, cb); try { var data = JSON.parse(body); if (typeof cb == 'function') return cb( data.response ); else return true; } catch (err) { return report2('charge_state', body, cb); } }); } exports.get_charge_state = get_charge_state; function get_climate_state( vid, cb ) { request( { method: 'GET', url: portal + '/vehicles/' + vid + '/data_request/climate_state', gzip: true, headers: http_header }, function (error, response, body) { if ((!!error) || (response.statusCode !== 200)) return report(error, response, body, cb); try { var data = JSON.parse(body); if (typeof cb == 'function') return cb( data.response ); else return true; } catch (err) { return report2('climate_state', body, cb); } }); } exports.get_climate_state = get_climate_state; function get_drive_state( vid, cb ) { request( { method: 'GET', url: portal + '/vehicles/' + vid + '/data_request/drive_state', gzip: true, headers: http_header }, function (error, response, body) { if ((!!error) || (response.statusCode !== 200)) return report(error, response, body, cb); try { var data = JSON.parse(body); if (typeof cb == 'function') return cb( data.response ); else return true; } catch (err) { return report2('drive_state', body, cb); } }); } exports.get_drive_state = get_drive_state; function get_vehicle_state( vid, cb ) { request( { method: 'GET', url: portal + '/vehicles/' + vid + '/data_request/vehicle_state', gzip: true, headers: http_header }, function (error, response, body) { if ((!!error) || (response.statusCode !== 200)) return report(error, response, body, cb); try { var data = JSON.parse(body); if (typeof cb == 'function') return cb( data.response ); else return true; } catch (err) { return report2('vehicle_state', body, cb); } }); } exports.get_vehicle_state = get_vehicle_state; function get_gui_settings( vid, cb ) { request( { method: 'GET', url: portal + '/vehicles/' + vid + '/data_request/gui_settings', gzip: true, headers: http_header }, function (error, response, body) { if ((!!error) || (response.statusCode !== 200)) return report(error, response, body, cb); try { var data = JSON.parse(body); if (typeof cb == 'function') return cb( data.response ); else return true; } catch (err) { return report2('gui_settings', body, cb); } }); } exports.get_gui_settings = get_gui_settings; function wake_up( vid, cb ) { request( { method: 'POST', url: portal + '/vehicles/' + vid + '/command/wake_up', gzip: true, headers: http_header }, function (error, response, body) { if ((!!error) || (response.statusCode !== 200)) return report(error, response, body, cb); try { var data = JSON.parse(body); if (typeof cb == 'function') return cb( data.response ); else return true; } catch (err) { return report2('wake_up', body, cb); } }); } exports.wake_up = wake_up; function open_charge_port( vid, cb ) { request( { method: 'POST', url: portal + '/vehicles/' + vid + '/command/charge_port_door_open', gzip: true, headers: http_header }, function (error, response, body) { if ((!!error) || (response.statusCode !== 200)) return report(error, response, body, cb); try { var data = JSON.parse(body); if (typeof cb == 'function') return cb( data.response ); else return true; } catch (err) { return report2('charge_port_door_open', body, cb); } }); } exports.open_charge_port = open_charge_port; var CHARGE_OFF = 0; // changes charge state to ON without effecting range mode var CHARGE_ON = 1; // changes charge state to OFF without effecting range mode function charge_state( params, cb ) { var vid = params.id; var state = params.charge; // Change the range mode if necessary if (state == CHARGE_ON || state == "on" || state == "start" || state === true ) { state = "start"; } if (state == CHARGE_OFF || state == "off" || state == "stop" || state === false ) { state = "stop"; } if (state == "start" || state == "stop" ) { request( { method: 'POST', url: portal + '/vehicles/' + vid + '/command/charge_' + state, gzip: true, headers: http_header }, function (error, response, body) { if ((!!error) || (response.statusCode !== 200)) return report(error, response, body, cb); try { var data = JSON.parse(body); if (typeof cb == 'function') return cb( data.response ); else return true; } catch (err) { return report2('charge_' + state, body, cb); } }); } else { if (typeof cb == 'function') return cb( new Error("Invalid charge state = " + state)); else return false; } } exports.charge_state = charge_state; exports.CHARGE_OFF = CHARGE_OFF; exports.CHARGE_ON = CHARGE_ON; var RANGE_STD = 0; // changes range mode to STANDARD without effecting charge state var RANGE_MAX = 1; // changes range mode to MAX_RANGE without effecting charge state function charge_range( params, cb ) { var vid = params.id; var range = params.range; var percent = params.percent; if (range == RANGE_STD || range == "std" || range == "standard" ) { range = "standard"; } if (range == RANGE_MAX || range == "max" || range == "max_range") { range = "max_range"; } if (range == "standard" || range == "max_range" ) { request( { method: 'POST', url: portal + '/vehicles/' + vid + '/command/charge_' + range, gzip: true, headers: http_header }, function (error, response, body) { if ((!!error) || (response.statusCode !== 200)) return report(error, response, body, cb); try { var data = JSON.parse(body); if (typeof cb == 'function') return cb( data.response ); else return true; } catch (err) { return report2('charge_' + range, body, cb); } }); } else if ( range == "set" && (percent >= 50) && (percent <= 100) ) { request( { method: 'POST', url: portal + '/vehicles/' + vid + '/command/set_charge_limit', gzip: true, headers: http_header, form: { "percent" : percent.toString() } }, function (error, response, body) { if ((!!error) || (response.statusCode !== 200)) return report(error, response, body, cb); try { var data = JSON.parse(body); if (typeof cb == 'function') return cb( data.response ); else return true; } catch (err) { return report2('set_charge_limit', body, cb); } }); } else { if (typeof cb == 'function') return cb( new Error("Invalid charge range = " + range)); else return false; } } exports.charge_range = charge_range; exports.RANGE_STD = RANGE_STD; exports.RANGE_MAX = RANGE_MAX; function flash( vid, cb ) { request({ method: 'POST', url: portal + '/vehicles/' + vid + '/command/flash_lights', gzip: true, headers: http_header }, function (error, response, body) { if ((!!error) || (response.statusCode !== 200)) return report(error, response, body, cb); try { var data = JSON.parse(body); if (typeof cb == 'function') return cb( data.response ); else return true; } catch (err) { return report2('flash_lights', body, cb); } }); } exports.flash = flash; function honk( vid, cb ) { request( { method: 'POST', url: portal + '/vehicles/' + vid + '/command/honk_horn', gzip: true, headers: http_header }, function (error, response, body) { if ((!!error) || (response.statusCode !== 200)) return report(error, response, body, cb); try { var data = JSON.parse(body); if (typeof cb == 'function') return cb( data.response ); else return true; } catch (err) { return report2('honk_horn', body, cb); } }); } exports.honk = honk; var LOCK_OFF = 0; var LOCK_ON = 1; function door_lock( params, cb ) { var vid = params.id; var state = params.lock; if (state == "lock" || state === true || state == "on" || state == "close" ) { request( { method: 'POST', url: portal + '/vehicles/' + vid + '/command/door_lock', gzip: true, headers: http_header }, function (error, response, body) { if ((!!error) || (response.statusCode !== 200)) return report(error, response, body, cb); try { var data = JSON.parse(body); if (typeof cb == 'function') return cb( data.response ); else return true; } catch (err) { return report2('door_lock', body, cb); } }); } else if (state == "unlock" || state === false || state == "off" || state == "open" ) { request( { method: 'POST', url: portal + '/vehicles/' + vid + '/command/door_unlock', gzip: true, headers: http_header }, function (error, response, body) { if ((!!error) || (response.statusCode !== 200)) return report(error, response, body, cb); try { var data = JSON.parse(body); if (typeof cb == 'function') return cb( data.response ); else return true; } catch (err) { return report2('door_unlock', body, cb); } }); } else { if (typeof cb == 'function') return cb( new Error("Invalid door lock state = " + state)); else return false; } } exports.door_lock = door_lock; exports.LOCK_OFF = LOCK_OFF; exports.LOCK_ON = LOCK_ON; var TEMP_HI = 32; var TEMP_LO = 17; function set_temperature( params, cb ) { var dtemp = params.dtemp; var ptemp = params.ptemp; var vid = params.id; var error = false; //var temp_str = ""; if ( dtemp !== undefined && dtemp <= TEMP_HI && dtemp >= TEMP_LO) { //temp_str = 'driver_temp=' + dtemp; // change from string to JSON form data } else { error = true; } // if no passenger temp is passed, the driver temp is also used as the passenger temp if ( ptemp !== undefined && ptemp <= TEMP_HI && ptemp >= TEMP_LO) { //temp_str = temp_str +'&passenger_temp=' + ptemp; // change from string to JSON form data } else if ( ptemp === undefined ) { ptemp = dtemp; } else { error = true; } if (!error) { request( { method: 'POST', url: portal + '/vehicles/' + vid + '/command/set_temps', gzip: true, headers: http_header, form: { "driver_temp" : dtemp.toString(), "passenger_temp" : ptemp.toString(), } }, function (error, response, body) { if ((!!error) || (response.statusCode !== 200)) return report(error, response, body, cb); try { var data = JSON.parse(body); if (typeof cb == 'function') return cb( data.response ); else return true; } catch (err) { return report2('set_temps', body, cb); } }); } else { if (typeof cb == 'function') return cb( new Error('Invalid temperature setting (' + dtemp + 'C), Passenger (' + ptemp + 'C)')); else return false; } } exports.set_temperature = set_temperature; exports.TEMP_HI = TEMP_HI; exports.TEMP_LO = TEMP_LO; var CLIMATE_OFF = 0; var CLIMATE_ON = 1; function auto_conditioning( params, cb ) { var vid = params.id; var state = params.climate; if (state == CLIMATE_ON) { state = true; } if (state == CLIMATE_OFF) { state = false; } if (state == "start" || state === true || state == "on" ) { request( { method: 'POST', url: portal + '/vehicles/' + vid + '/command/auto_conditioning_start', gzip: true, headers: http_header }, function (error, response, body) { if ((!!error) || (response.statusCode !== 200)) return report(error, response, body, cb); try { var data = JSON.parse(body); if (typeof cb == 'function') return cb( data.response ); else return true; } catch (err) { return report2('auto_conditioning_start', body, cb); } }); } else if (state == "stop" || state === false || state == "off" ) { request( { method: 'POST', url: portal + '/vehicles/' + vid + '/command/auto_conditioning_stop', gzip: true, headers: http_header }, function (error, response, body) { if ((!!error) || (response.statusCode !== 200)) return report(error, response, body, cb); try { var data = JSON.parse(body); if (typeof cb == 'function') return cb( data.response ); else return true; } catch (err) { return report2('auto_conditioning_stop', body, cb); } }); } else { if (typeof cb == 'function') return cb( new Error("Invalid auto conditioning state = " + state)); else return false; } } exports.auto_conditioning = auto_conditioning; exports.CLIMATE_OFF = CLIMATE_OFF; exports.CLIMATE_ON = CLIMATE_ON; var ROOF_CLOSE = 0; var ROOF_VENT = 1; var ROOF_COMFORT = 2; var ROOF_OPEN = 3; function sun_roof( params, cb ) { var vid = params.id; var state = params.roof; var percent = params.percent; // add a check that their is a sunroof on the car?? if (state == ROOF_CLOSE) { state = "close"; } if (state == ROOF_VENT) { state = "vent"; } if (state == ROOF_COMFORT) { state = "comfort"; } if (state == ROOF_OPEN) { state = "open"; } if (state == "open" || state == "close" || state == "comfort" || state == "vent") { request( { method: 'POST', url: portal +'/vehicles/' + vid + '/command/sun_roof_control', gzip: true, headers: http_header, form: { 'state': state } }, function (error, response, body) { if ((!!error) || (response.statusCode !== 200)) return report(error, response, body, cb); try { var data = JSON.parse(body); if (typeof cb == 'function') return cb( data.response ); else return true; } catch (err) { return report2('sun_roof_control ' + state, body, cb); } }); } else if ( (state == "move") && (percent >= 0) && (percent <= 100) ) { request( { method: 'POST', url: portal +'/vehicles/' + vid + '/command/sun_roof_control', gzip: true, headers: http_header, form: { 'state': 'move', 'percent': percent.toString() } }, function (error, response, body) { if ((!!error) || (response.statusCode !== 200)) return report(error, response, body, cb); try { var data = JSON.parse(body); if (typeof cb == 'function') return cb( data.response ); else return true; } catch (err) { return report2('sun_roof_control move', body, cb); } }); } else { if (typeof cb == 'function') return cb( new Error("Invalid sun roof state " + util.inspect(params))); else return false; } } exports.sun_roof = sun_roof; exports.ROOF_CLOSE = ROOF_CLOSE; exports.ROOF_VENT = ROOF_VENT; exports.ROOF_COMFORT = ROOF_COMFORT; exports.ROOF_OPEN = ROOF_OPEN; //left off here// // Streaming API stuff is below. Everything above is the REST API // // Required options to teslams.stream() are { // email: 'your teslamotors.com login', // password: 'token returned from a prior call to teslams.vehicles()', // vehicle_id: 'Long form vehicle_id returned from a prior call to teslams.vehicles()' // a callback that expects ( error, response, body) for the HTTP response // } // See examples/examplestream.js for a simple one poll working example of how to use this function // See examples/streaming.js for a more complicated but useful continuous polling example of streaming exports.stream_columns = [ 'speed', 'odometer', 'soc', 'elevation', 'est_heading', 'est_lat', 'est_lng', 'power', 'shift_state', 'range', 'est_range', 'heading' ]; exports.stream = function(options, cb) { if (!cb) cb = function(error, response, body) {/* jshint unused: false */}; request({ method : 'GET', url : 'https://streaming.vn.teslamotors.com/stream/' + options.vehicle_id + '/?values=' + exports.stream_columns.join(','), gzip: true, auth : { user : options.email, pass : options.password } }, cb); };