diff --git a/README.md b/README.md index c0415ce..181bcf2 100644 --- a/README.md +++ b/README.md @@ -20,10 +20,13 @@ This software and documentation do not come from Tesla Motors Inc. Use these programs at your own risk. The authors do not guaranteed the proper functioning of these applications. This code attempts to use the same interfaces used by the official Tesla phone apps. However, it is possible that use of this code may cause unexpected damage for which nobody but you are responsible. Use of these functions can change the settings on your car and may have negative consequences such as (but not limited to) unlocking the doors, opening the sun roof, or reducing the available charge in the battery. #Contributors -[Joe Pasqua](https://github.com/jpasqua) +Joe Pasqua (https://github.com/jpasqua ): Author +Sune Jakobsson (https://github.com/sunejak ): Bug Fixes [Greyson Fischer](https://github.com/greyson) +[Sune Jakobsson](https://github.com/greyson) + # Preparing your build environment (gradle) The following command will resolve and download all dependencies, set @@ -43,14 +46,14 @@ This project assumes a directory structure that looks like this: src ThirdParty -- A repository for third party library dependencies apache - commons-codec-1.10 - commons-lang3-3.3.2 + commons-codec-1.9 + commons-lang3-3.3 commons-logging-1.1.3 geocoder google-gson-2.2.4 resty -The Tesla/TeslaClient directory corrsponds to this github project (TeslaClient.git). +The Tesla/TeslaClient directory corresponds to this github project (TeslaClient.git). The following commands will create and populate the hierarchy. It assumes that: @@ -70,16 +73,14 @@ Be sure to either set these variables or adapt the commands below: git clone https://github.com/jpasqua/TeslaClient.git # Download the apache libraries - cd $ROOT/ThirdParty/apache - curl -s -O http://www.eng.lsu.edu/mirrors/apache/commons/codec/binaries/commons-codec-1.10-bin.zip - unzip commons-codec-1.10-bin.zip - rm commons-codec-1.10-bin.zip + cd ThirdParty/apache + wget http://www.us.apache.org/dist//commons/codec/binaries/commons-codec-1.9-bin.zip + unzip commons-codec-1.9-bin.zip - curl -s -O http://mirror.cc.columbia.edu/pub/software/apache//commons/lang/binaries/commons-lang3-3.3.2-bin.zip - unzip commons-lang3-3.3.2-bin.zip - rm commons-lang3-3.3.2-bin.zip + wget http://www.us.apache.org/dist//commons/lang/binaries/commons-lang3-3.3-bin.zip + unzip commons-lang3-3.3-bin.zip - curl -s -O http://apache.mirrors.hoobly.com//commons/logging/binaries/commons-logging-1.1.3-bin.zip + wget http://www.us.apache.org/dist//commons/logging/binaries/commons-logging-1.1.3-bin.zip unzip commons-logging-1.1.3-bin.zip rm commons-logging-1.1.3-bin.zip @@ -97,8 +98,19 @@ Be sure to either set these variables or adapt the commands below: cd $ROOT/ThirdParty cd resty curl -s -O http://repo2.maven.org/maven2/us/monoid/web/resty/0.3.2/resty-0.3.2.jar + cd .. + + # Create the library and test your car + cd Tesla/TeslaClient + ant run -Darg0=username -Darg1=password + + # Consequent runs ( with the credential stored in cookies.txt ) + + ant run #Tests and Samples There are two test programs included in the project: BasicTest and Interactive. The former simply runs through a sequence of functions in the client library to demonstrate that it is connecting and working. The second presents an interactive shell that allows the user to issue the various commands that are available through the client library. To use either of these programs you must have active credentials for a Tesla vehicle that has remote access enabled. If you have more than one vehicle, you may select which vehicle to use in the Interactive program. BasicTest will always use the first vehicle returned by the Tesla portal. + + To compile and run "BasicTest" directly use: ant -Dapplication.args="userName passWord" run diff --git a/nbproject/build-impl.xml b/nbproject/build-impl.xml index 0250cb6..563a4bd 100644 --- a/nbproject/build-impl.xml +++ b/nbproject/build-impl.xml @@ -1166,6 +1166,8 @@ is divided into following sections: + + Must select one file in the IDE or set run.class @@ -1181,7 +1183,7 @@ is divided into following sections: Must select one file in the IDE or set run.class - + Must select one file in the IDE or set applet.url diff --git a/nbproject/project.properties b/nbproject/project.properties index 426c518..0dc4b4a 100644 --- a/nbproject/project.properties +++ b/nbproject/project.properties @@ -27,9 +27,14 @@ dist.jar=${dist.dir}/TeslaClient.jar dist.javadoc.dir=${dist.dir}/javadoc endorsed.classpath= excludes= -file.reference.commons-codec-1.8.jar=../../ThirdParty/apache/commons-codec-1.8/commons-codec-1.8.jar +<<<<<<< HEAD +file.reference.commons-codec-1.8.jar=../../ThirdParty/apache/commons-codec-1.9/commons-codec-1.9.jar +file.reference.commons-lang3-3.1.jar=../../ThirdParty/apache/commons-lang3-3.3/commons-lang3-3.3.jar +======= +file.reference.commons-codec-1.10.jar=../../ThirdParty/apache/commons-codec-1.10/commons-codec-1.10.jar file.reference.commons-io-2.4.jar=../../ThirdParty/apache/commons-io-2.4/commons-io-2.4.jar -file.reference.commons-lang3-3.1.jar=../../ThirdParty/apache/commons-lang3-3.1/commons-lang3-3.1.jar +file.reference.commons-lang3-3.3.2.jar=../../ThirdParty/apache/commons-lang3-3.3.2/commons-lang3-3.3.2.jar +>>>>>>> 60af0a012ad01539e8b0c8279e2c76ad39b63b8c file.reference.commons-logging-1.1.3.jar=../../ThirdParty/apache/commons-logging-1.1.3/commons-logging-1.1.3.jar file.reference.commons-logging-api-1.1.3.jar=../../ThirdParty/apache/commons-logging-1.1.3/commons-logging-api-1.1.3.jar file.reference.geocoder-java-0.15.jar=../../ThirdParty/geocoder-java/geocoder-java-0.15.jar @@ -38,8 +43,8 @@ file.reference.resty-0.3.2.jar=../../ThirdParty/resty/resty-0.3.2.jar includes=** jar.compress=false javac.classpath=\ - ${file.reference.commons-codec-1.8.jar}:\ - ${file.reference.commons-lang3-3.1.jar}:\ + ${file.reference.commons-codec-1.10.jar}:\ + ${file.reference.commons-lang3-3.3.2.jar}:\ ${file.reference.commons-logging-1.1.3.jar}:\ ${file.reference.commons-logging-api-1.1.3.jar}:\ ${file.reference.geocoder-java-0.15.jar}:\ diff --git a/src/org/noroomattheinn/tesla/SnapshotState.java b/src/org/noroomattheinn/tesla/SnapshotState.java new file mode 100644 index 0000000..f0bd111 --- /dev/null +++ b/src/org/noroomattheinn/tesla/SnapshotState.java @@ -0,0 +1,317 @@ +/* + * SnapshotState.java - Copyright(c) 2013 Joe Pasqua + * Provided under the MIT License. See the LICENSE file for details. + * Created: Jul 11, 2013 + */ + +package org.noroomattheinn.tesla; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.logging.Level; +import org.apache.commons.codec.binary.Base64; +import org.apache.commons.lang3.StringUtils; +import org.noroomattheinn.utils.RestyWrapper; +import org.noroomattheinn.utils.Utils; +import us.monoid.json.JSONException; +import us.monoid.json.JSONObject; +import us.monoid.web.TextResource; + +/** + * StreamingState: Provides access to streaming information about the current + * state of the vehicle. + * + * @author Joe Pasqua + */ + +public class SnapshotState extends APICall { + + public State state; + +/*------------------------------------------------------------------------------ + * + * Constants and Enums + * + *----------------------------------------------------------------------------*/ + private enum Keys { + timestamp, odometer, speed, soc, elevation, est_heading, + est_lat, est_lng, power, shift_state, range, est_range, heading}; + private final Keys[] keyList = Keys.values(); + private final String allKeys = + StringUtils.join(keyList, ',', 1, keyList.length); + + private static final String endpointFormat = + "https://streaming.vn.teslamotors.com/stream/%s/?values=%s"; + + private static final int WakeupRetries = 3; + private static final int ReadTimeoutInMillis = 25 * 1000; + +/*------------------------------------------------------------------------------ + * + * Internal State + * + *----------------------------------------------------------------------------*/ + + private Vehicle authenticatedVehicle = null; + private BufferedReader locationReader = null; + + +/*============================================================================== + * ------- ------- + * ------- Public Interface To This Class ------- + * ------- ------- + *============================================================================*/ + + // Accessors + + @Override public String getStateName() { return "Unstreamed State"; } + + @Override protected BaseState setState(boolean valid) { + return (state = valid ? new State(this) : null); + } + + // Constructors + public SnapshotState(Vehicle v) { + super(v); + } + + +/*------------------------------------------------------------------------------ + * + * Methods overridden from APICall + * + *----------------------------------------------------------------------------*/ + + public boolean opportunisticRefresh() { + // Just try reading. Maybe we're lucky enough to still be connected + if (getFromStream()) return true; + + // Well, that didn't work! Get a new reader and try again + locationReader = refreshReader(); + if (getFromStream()) return true; + + return false; + } + + public boolean refreshFromStream() { + return getFromStream(); + } + + @Override public boolean refresh() { + locationReader = refreshReader(); + return getFromStream(); + } + + +/*------------------------------------------------------------------------------ + * + * Methods overridden from Object + * + *----------------------------------------------------------------------------*/ + + + @Override public String toString() { + if (state == null) return "[ ]"; + return String.format( + "Time Stamp: %s (%s)\n" + + "Speed: %3.1f miles\n" + + "Location: [(Lat: %f, Lng: %3f), Heading: %d, Elevation: %d]\n" + + "Charge Info: [SoC: %d KW, Power: %d KW]\n" + + "Odometer: %7.1f miles\n" + + "Range: %d miles\n", + state.vehicleTimestamp, new Date(state.vehicleTimestamp), + state.speed, + state.estLat, state.estLng, state.estHeading, state.elevation, + state.soc, state.power, + state.odometer, state.range + // Don't know what shift_state is! Always seems to be null + ); + } + +/*------------------------------------------------------------------------------ + * + * Methods for setting up the Streaming connection and reading the data + * + *----------------------------------------------------------------------------*/ + + private boolean getFromStream () { + JSONObject val = produce(); + if (val == null) { + state = null; + return false; + } + setJSONState(val); + state = new State(this); + state.timestamp = System.currentTimeMillis(); + return true; + } + + private BufferedReader refreshReader() { + // Just try making a connection, maybe we're lucky enough to still be authenticated + return establishStreamingConnection(); + } + + private JSONObject produce() { + if (locationReader == null) { + return null; + } + + try { + String line = locationReader.readLine(); + if (line == null) { + locationReader = null; + return null; + } + + JSONObject jo = new JSONObject(); + String vals[] = line.split(","); + for (int i = 0; i < keyList.length; i++) { + try { + jo.put(keyList[i], vals[i]); + } catch (JSONException ex) { + Tesla.logger.log(Level.SEVERE, "Malformed data", ex); + return null; + } + } + return jo; + } catch (IOException ex) { + Tesla.logger.log(Level.FINEST, "Timeouts are expected here...", ex); + return null; + } + } + + private BufferedReader establishStreamingConnection() { + if (authenticatedVehicle == null) { + refreshAuthentication(); + if (authenticatedVehicle == null) { + Tesla.logger.warning("Can't authenticate for streaming!"); + return null; + } + } + + String endpoint = String.format( + endpointFormat, authenticatedVehicle.getStreamingVID(), allKeys); + + RestyWrapper rw = getAuthAPI(authenticatedVehicle); + + for (int i = 0; i < 5; i++) { + try { + TextResource r = rw.text(endpoint); + if (r.status(200)) { + return new BufferedReader(new InputStreamReader(r.stream())); + } + } catch (IOException e) { + String msg = e.toString(); + if (msg.contains("Error while reading from GET: [401] Unauthorized")) { + Tesla.logger.log(Level.INFO, "Authorization problem, getting new token"); + refreshAuthentication(); + if (authenticatedVehicle == null) break; + rw = getAuthAPI(authenticatedVehicle); + } else { + Tesla.logger.warning("Snapshot GET failed: " + e); + } + } + Utils.sleep(500); + } + + Tesla.logger.warning("Tried 5 times to establish a Snapshot stream - giving up"); + return null; + } + + +/*------------------------------------------------------------------------------ + * + * This section contains the methods and classes necessary to get auth + * tokens and create an authenticated connection to Tesla + * + *----------------------------------------------------------------------------*/ + + private void setAuthHeader(RestyWrapper api, String username, String authToken) { + byte[] authString = (username + ":" + authToken).getBytes(); + String encodedString = Base64.encodeBase64String(authString); + api.withHeader("Authorization", "Basic " + encodedString); + } + + + private void refreshAuthentication() { + String vid = v.getVID(); + // Remember this so we can find the right vehicle when we fetch + // the updated list of vehicles after doing the wakeup + + ActionController a = new ActionController(v); + for (int i = 0; i < WakeupRetries; i++) { + + List vList = new ArrayList<>(); + v.getContext().fetchVehiclesInto(vList); + for (Vehicle newV : vList) { + if (newV.getVID().equals(vid) && newV.getStreamingToken() != null) { + authenticatedVehicle = newV; + return; + } + } + a.wakeUp(); Utils.sleep(500); + } + + // For some reason we can't get Streaming tokens. We've tried enough - Give up + Tesla.logger.log(Level.WARNING, "Error: couldn't retreive auth tokens"); + authenticatedVehicle = null; + } + + private RestyWrapper getAuthAPI(Vehicle v) { + String authToken = v.getStreamingToken(); + + // This call requires BASIC authentication using the user name (this is + // the user's registered email address) and the authToken. + // We can't use the Resty authentication mechanism because the tesla + // site doesn't seem to request authentication - it just expects the + // Authorization header feld to be present. + // To accomplish that, create a new (temporary) Resty instance and + // add the auth header to it. + RestyWrapper api = new RestyWrapper(ReadTimeoutInMillis); + setAuthHeader(api, v.getContext().getUsername(), authToken); + return api; + } + +/*------------------------------------------------------------------------------ + * + * The State object + * + *----------------------------------------------------------------------------*/ + + public static class State extends BaseState { + public long vehicleTimestamp; + public double speed; + public double odometer; + public int soc; + public int elevation; + public int estHeading; + public int heading; + public double estLat; + public double estLng; + public int power; + public String shiftState; + public int range; + public int estRange; + + public State(SnapshotState ss) { + vehicleTimestamp = ss.getLong(Keys.timestamp); + speed = ss.getDouble(Keys.speed); + if (Double.isNaN(speed)) speed = 0.0; + odometer = ss.getDouble(Keys.odometer); + soc = ss.getInteger(Keys.soc); + elevation = ss.getInteger(Keys.elevation); + estHeading = ss.getInteger(Keys.est_heading); + heading = ss.getInteger(Keys.heading); + estLat = ss.getDouble(Keys.est_lat); + estLng = ss.getDouble(Keys.est_lng); + power = ss.getInteger(Keys.power); + shiftState = ss.getString(Keys.shift_state); + range = ss.getInteger(Keys.range); + estRange = ss.getInteger(Keys.est_range); + } + } +}