Tesla Integration to Query Status and Send Commands to Your Car

i have it working!

Tesla Auth Token on Raspberry PI

  1. Install raspbian (first option in noobs)

  2. Run this command:
    sudo apt install python3 chromium-chromedriver

  3. Run this command:
    git clone https://github.com/enode-engineering/tesla-oauth2.git

  4. Run this command:
    cd tesla-auth2/

  5. Run this command:
    pip install -r requirements.txt

  6. Run this command:
    pip3 install selenium

  7. You are ready to run the command!
    python3 tesla.py -e USERMAIL -p PASSWORD -f FILENAME

afterwords you could "nano FILENAME" and see the acces_token, push it to webserver or whatever...

question though... what does the APP expect from the URL, is it enough just to display the raw contents of the file as JSON?

1 Like

yep i push the token as is.. it is a json file. and it will get a json post and response and pull the token out.. Wish i could get it working on qnap.. for now i am generating on windows and pushin to qnap once a month on schedule.. I believe the token expires every 45 days.. You can test by installing a new version of the testla connect driver.. and filling in your url to your web server.. and put anything in for username pwd.. and on initial commit if it works you will see in logs, and see the car available to be selected.. As i said that works fine, not sure if it will work when token expires and it goes to try again.. that may need some massaging.. but hoping.

Allright guys, i have spend a couple of hours creating this little step by step guide, to setup a raspberry pi (out-of-the-box) with this new Tesla Token system for use with the Hubitat App to refresh the token automatically

Tesla Auth Token on Raspberry PI
Note:
step 10 is only for manual test, you can skip that if you like.
This guide is only intended for an “internal server”, DO NOT expose this online, this should only be accessible by your own LAN or else your access to the vehicles can be compromised.

  1. Install raspbian (first option in noobs)

  2. Run this command (to install necessary drivers/extensions):
    sudo apt install python 3 chromium-chromedriver

  3. Run this command (this fetches the script):
    git clone GitHub - enode/tesla-oauth2: General description about how Tesla does 2FA and how to detect if a user has 2FA enabled

  4. Run this command (this just puts us in the folder of the downloaded script):
    cd tesla-oauth2/

  5. Run this command (to install necessary drivers/extensions):
    pip install -r requirements.txt

  6. Run this command (to install necessary drivers/extensions):
    pip3 install selenium

  7. You are ready to run the command which fetches our Token!
    python3 tesla.py -e USEREMAIL -p PASSWORD -f token_file
    if you want, you can open the file by using the command: “nano token_file”

  8. Install webserver (This is to present the file automatically to the hubitat App):
    sudo apt install apache2 -y

  9. Run this command
    sudo chmod 777 -R /var/www/html/

  10. Run this command (this copies the file to our new webserver)
    cp /home/pi/tesla-oauth2/token_file /var/www/html/index.html
    congrats: you can now fetch the token file by the IP of the PI like this:
    HTTP://ipaddress_of_the_raspberry

  11. Run this command (this makes a new file):
    touch getToken.sh

  12. Run this command (to edit the file):
    nano getToken.sh

  13. Insert the below contents to the file (right click If using SSH) save by pressing ctrl+x:
    #!/bin/bash
    > rm /home/pi/tesla-oauth2/token_file
    > sleep 1
    > python3 /home/pi/tesla-oauth2/tesla.py -e USER -p PASS -f /home/pi/tesla-oauth2/token_file
    > sleep 40
    > cp /home/pi/tesla-oauth2/token_file /var/www/html/index.html

  14. Run this command (this makes the script executable):
    sudo chmod +x getToken.sh

  15. Run this command (scheduled tasks):
    crontab -e

  16. Insert this (this makes the script run on the 1st of every month) save by pressing ctrl+x:
    0 0 1 * * /home/pi/tesla-oauth2/getToken.sh

@kahn-hubitat feel free to use the above guides in your github description/readme

Thank you for the tool and assistance!
Oh and a little request for us Europeans, can you make a selector to choose Kilometers instead of Miles (as you have converted the Fahrenheit to Celcius)

Best Regards
Dion - Denmark

2 Likes

one comment on my windows it was pip3 not pip to install requierments..

i have gotten farther on the qnap.. i have a newer version of python 3.9 i found that works fine.. no errors, but I cannot find a version of chromedriver to install.. i tried the linux one .. just one executable and it is missing a bunch of library dependecies like libnss3 .. sigh

I installed this over the weekend and it seems to be working great, I can read my battery charge level and send myself reminders to plug in. Thanks OP.

I'd like to be able to programmatically change the charge limit to 50% during peak electrical rates, then to my normal max of 80% during off-peak electrical rates.

I can't figure out how to do this, is this app able to change the max charge limit? If not, please consider this a feature request.

Thanks.

not implemented.. Did not touch max or min charge limit, just set current charge limit.
new version on git hub with setchargelimit command

Summary

1 Like

Wow, what quick service!

I just did a quick test and looks like it works wonderfully.

Thanks so much.

1 Like

It seems it doesnt fetch the token from my PI ?

this morning nothing worked, so i manually copy-pasted the token into the field "new-access" token...

but i have already setup the APP to "URL (on your server) that holds new access token as generated from python script?" to my script...

is there anything else needed ?

i have set the Pi to create a new token every 30 days...

note the URL i have set just returns the RAW json output, this is correct right?
(dont worry i have garbage data in the strings below to show the example)

http://192.168.1.230

{"access_token":"qts-902953be28b71e1awega701ewehwhewehaa342b149064e755fbd07b13eeb6f8da968","token_type":"bearer","expires_in":3888000,"refresh_token":"f9146db52352345235sdgaf8703bcab10b9da175f198a235822dccd7","created_at":1618987438}

After a fair amount of work (breaking a couple other packages in the process), I got this running on my Ubuntu machine without errors, but the script to download the token file hangs. When I hit CTRL-C I have the following:

Traceback (most recent call last):
File "tesla.py", line 259, in
login(args)
File "tesla.py", line 65, in login
resp = session.get("https://auth.tesla.com/oauth2/v3/authorize", headers=headers, params=params)
File "/home/mike/.local/lib/python3.7/site-packages/requests/sessions.py", line 543, in get
return self.request('GET', url, **kwargs)
File "/home/mike/.local/lib/python3.7/site-packages/requests/sessions.py", line 530, in request
resp = self.send(prep, **send_kwargs)
File "/home/mike/.local/lib/python3.7/site-packages/requests/sessions.py", line 643, in send
r = adapter.send(request, **kwargs)
File "/home/mike/.local/lib/python3.7/site-packages/requests/adapters.py", line 449, in send
timeout=timeout
File "/home/mike/.local/lib/python3.7/site-packages/urllib3/connectionpool.py", line 677, in urlopen
chunked=chunked,
File "/home/mike/.local/lib/python3.7/site-packages/urllib3/connectionpool.py", line 426, in _make_request
six.raise_from(e, None)
File "", line 3, in raise_from
File "/home/mike/.local/lib/python3.7/site-packages/urllib3/connectionpool.py", line 421, in _make_request
httplib_response = conn.getresponse()
File "/usr/lib/python3.7/http/client.py", line 1369, in getresponse
response.begin()
File "/usr/lib/python3.7/http/client.py", line 310, in begin
version, status, reason = self._read_status()
File "/usr/lib/python3.7/http/client.py", line 271, in _read_status
line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
File "/usr/lib/python3.7/socket.py", line 589, in readinto
return self._sock.recv_into(b)
File "/usr/lib/python3.7/ssl.py", line 1071, in recv_into
return self.read(nbytes, buffer)
File "/usr/lib/python3.7/ssl.py", line 929, in read
return self._sslobj.read(len, buffer)
KeyboardInterrupt

I am running Python 3.7.10, chromium-chromedriver version 90.0.4430.72-0ubuntu0.16.04.1, pip 21.1, selenium 3.141.0, chardet 3.0.4, requests 2.24.0.

Any ideas on how to troubleshoot?

For what it's worth, I've been getting the same issue with the script just hanging on a fresh Ubuntu install in a virtual machine, as well as on Windows (though I am not sure I have everything installed properly on Windows).

Having the same issue as @mgchan. Unable to get token when running the tesla.py script. Any idea what the issue could be?

looks like the tesla website is hanging and not answering.. my last run also did not return .. was auto killed after 2 days,

i put in a bug report on the original developer and he fixed it.. basically change the ua etc to be blank

see here

2 Likes

Great. Got this working now.

1 Like

Sexy

1 Like

Kinda related to this thread, I should be getting my Model 3 in the next couple weeks. Does anyone happen to know if the USB ports stay powered for an extended period of time or will they power on under any circumstances while parked?

I have a Xbee in my current car, powered via USB that I use for car presence detection to open my garage door. I’m wondering if I’m going to continue to be able to do this or if I’ll find the Xbee getting powered on while the car is parked in my garage.

You can this integration itself as the presence . Ie.

I believe it turns off except for the port in.use for sentry mode if you have.that enabled.

I recommend you pay the extra foe the.home link..it works well for.me.

How quick is it to detect presence changes?

depends how often you run it. but probably not a good long term solution if you want it immediate.

that is why i recommend getting the homelink.

When will it try to fetch new token from the URL setup ?
i have not once recieved or experienced this with succes, but when i manually put in the token from the script, it correctly sends a message over Pushover...

if anyone is interested, i have added the option to choose between Miles and Kilometers (please note that i have changed the driver to default to Celcius and Kilometers "for us not in the US ;-)", which can be changed in settings)

/**
 *  Tesla
 *
 *  Copyright 2018 Trent Foley
 *
 *  Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
 *  in compliance with the License. You may obtain a copy of the License at:
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
 *  on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
 *  for the specific language governing permissions and limitations under the License.
 *
 * lgk kahn@lgk.com 10/13/20 Added user selectable refresh update schedule instead of default 15 minutes.
 * also add custom lastupdate time attribute and refreshTime so that info is displayable on dashboards.
 * also round mileage off to whole number so again it appears better on dashboard.
 * same for temp, round off so that we can do custom colors on dashbaord based on temp.
 * Same for temp setpoint. showing non integer makes no sens.
 * 10/18/20 added unlock/open charge port command
 * 12/25/20 new attributes and functions for seat heater,windows etc thanks to gomce62f, Also add input to set temp scale to either F or C.
 
 * lgk new versino, not letting the car sleep, add option to disable and schedule it between certain times.
 * lgk add misssing parameter to sethermostatsetpoint.. note it is in farenheit so be aware of that.. in the future i can look at supporting both
 * with option to convert.
 * lgk add code to turn off debugging after 30 minutes.
 * DPE add code to select measure from Miles to KM, changed default temp to celcius and measure to kilometers
*/
metadata {
	definition (name: "Tesla", namespace: "trentfoley", author: "Trent Foley, Larry Kahn") {
		capability "Actuator"
		capability "Battery"
// not supported	capability "Geolocation"
		capability "Lock"
		capability "Motion Sensor"
		capability "Presence Sensor"
		capability "Refresh"
		capability "Temperature Measurement"
		capability "Thermostat Mode"
        capability "Thermostat Setpoint"
     
		attribute "state", "string"
        attribute "vin", "string"
        attribute "odometer", "number"
        attribute "batteryRange", "number"
        attribute "chargingState", "string"
        attribute "refreshTime", "string"
        attribute "lastUpdate", "string"
        attribute "minutes_to_full_charge", "number"
        attribute "seat_heater_left", "number"
        attribute "seat_heater_right", "number"        
        attribute "seat_heater_rear_left", "number"
        attribute "seat_heater_rear_right", "number"
        attribute "seat_heater_rear_center", "number"    
        attribute "sentry_mode", "string"
        attribute "front_drivers_window" , "number"
        attribute "front_pass_window" , "number"
        attribute "rear_drivers_window" , "number"
        attribute "rear_pass_window" , "number"

		command "wake"
        command "setThermostatSetpoint", ["Number"]
        command "startCharge"
        command "stopCharge"
        command "openFrontTrunk"
        command "openRearTrunk"
        command "unlockandOpenChargePort"
        command "setSeatHeaters", ["number","number"]  /** first attribute is seat number 0-5 and second attribute is heat level 0-3 e.g. 0,3 is drivers seat heat to max *  Future plan is to have this be  drop down list */ 
        command "sentryModeOn"
        command "sentryModeOff"
        command "ventWindows"
        command "closeWindows"

	}


	simulator {
		// TODO: define status and reply messages here
	}


    preferences
    {
       input "refreshTime", "enum", title: "How often to refresh?",options: ["Disabled","1-Hour", "30-Minutes", "15-Minutes", "10-Minutes", "5-Minutes"],  required: true, defaultValue: "15-Minutes"
       input "AllowSleep", "bool", title: "Schedule a time to disable/reenable to allow the car to sleep?", required: true, defaultValue: false
       input "fromTime", "time", title: "From", required:false, width: 6, submitOnChange:true
       input "toTime", "time", title: "To", required:false, width: 6 
       input "tempScale", "enum", title: "Display temperature in F or C ?", options: ["F", "C"], required: true, defaultValue: "C" 
       input "MeasureScale", "enum", title: "Display Range/Speed in Miles or Kilometers ?", options: ["Miles", "KM"], required: true, defaultValue: "KM" 
       input "debug", "bool", title: "Turn on Debug Logging?", required:true, defaultValue: false   
    }
}

def logsOff()
{
    log.debug "Turning off Logging!"
    device.updateSetting("debug",[value:"false",type:"bool"])
}

def initialize() {
	log.debug "Executing 'initialize'"
     def now = new Date().format('MM/dd/yyyy h:mm a',location.timeZone)
    
    sendEvent(name: "supportedThermostatModes", value: ["auto", "off"])
    log.debug "Refresh time currently set to: $refreshTime"
    unschedule()  
   
    sendEvent(name: "lastUpdate", value: now, descriptionText: "Last Update: $now")
    sendEvent(name: "refreshTime", value: refreshTime)
    
    if (refreshTime == "1-Hour")
      runEvery1Hour(refresh)
      else if (refreshTime == "30-Minutes")
       runEvery30Minutes(refresh)
     else if (refreshTime == "15-Minutes")
       runEvery15Minutes(refresh)
     else if (refreshTime == "10-Minutes")
       runEvery10Minutes(refresh)
     else if (refreshTime == "5-Minutes")
       runEvery5Minutes(refresh)
    else if (refreshTime == "Disabled")
    {
        log.debug "Disabling..."
    }
      else 
      { 
          log.debug "Unknown refresh time specified.. defaulting to 15 Minutes"
          runEvery15Minutes(refresh)
      }
    // now handle scheduling to turn on and off to allow sleep
    if ((AllowSleep == true) && (fromTime != null) && (toTime != null))
    {
       log.debug "Scheduling disable and re-enable times to allow sleep!" 
        schedule(fromTime, disable)
        schedule(toTime, reenable)       
    }
    
     if (debug)
    {
        log.debug "Turning off logging in 1/2 hour!"
        runIn(1800,logsOff)
    } 
   
}


def disable()
{
    log.debug "Disabling to allow sleep!"
    unschedule()
    // schedule reenable time
    if (toTime != null)
     schedule(toTime, reenable)
}

def reenable()
{
    log.debug "Waking up app in re-enable!"
    // now schedule the sleep again
    initialize() 
    wake()
}

// parse events into attributes
def parse(String description) {
	log.debug "Parsing '${description}'"
}
    
private processData(data) {
	if(data) {
    	log.debug "processData: ${data}"
        
    	sendEvent(name: "state", value: data.state)
        sendEvent(name: "motion", value: data.motion)
        
        
       if (MeasureScale == "KM")
        {
        sendEvent(name: "speed", value: milesToKm(data.speed).toInteger(), unit: "KM/H")
        }
        else
        {
        sendEvent(name: "speed", value: data.speed, unit: "MPH") 
        }
        
        sendEvent(name: "vin", value: data.vin)
        sendEvent(name: "thermostatMode", value: data.thermostatMode)
        
        if (data.chargeState) {
            if (debug) log.debug "chargeStte = $data.chargeState"
            
        	sendEvent(name: "battery", value: data.chargeState.battery)
            
            if (MeasureScale == "KM")
            {
            sendEvent(name: "batteryRange", value: milesToKm(data.chargeState.batteryRange).toInteger())
            }
            else
            {
            sendEvent(name: "batteryRange", value: data.chargeState.batteryRange.toInteger())    
            }
            
            sendEvent(name: "chargingState", value: data.chargeState.chargingState)
            sendEvent(name: "minutes_to_full_charge", value: data.chargeState.minutes_to_full_charge)

        }
        
        if (data.driveState) {
        	sendEvent(name: "latitude", value: data.driveState.latitude)
			sendEvent(name: "longitude", value: data.driveState.longitude)
            sendEvent(name: "method", value: data.driveState.method)
            sendEvent(name: "heading", value: data.driveState.heading)
            sendEvent(name: "lastUpdateTime", value: data.driveState.lastUpdateTime)
        }
        
        if (data.vehicleState) {
           if (debug) log.debug "vehicle state = $data.vehicleState"
            
        	sendEvent(name: "presence", value: data.vehicleState.presence)
            sendEvent(name: "lock", value: data.vehicleState.lock)
            
            if (MeasureScale == "KM")
            {
            sendEvent(name: "odometer", value: milesToKm(data.vehicleState.odometer).toInteger())
            }
            else
            {
            sendEvent(name: "odometer", value: data.vehicleState.odometer.toInteger())    
            }
            
            sendEvent(name: "sentry_mode", value: data.vehicleState.sentry_mode)
            sendEvent(name: "front_drivers_window" , value: data.vehicleState.front_drivers_window)
            sendEvent(name: "front_pass_window" , value: data.vehicleState.front_pass_window)
            sendEvent(name: "rear_drivers_window" , value: data.vehicleState.rear_drivers_window)
            sendEvent(name: "rear_pass_window" , value: data.vehicleState.rear_pass_window)

        }
        
        if (data.climateState) {
            if (tempScale == "F")
            {
        	  sendEvent(name: "temperature", value: data.climateState.temperature.toInteger(), unit: "F")
              sendEvent(name: "thermostatSetpoint", value: data.climateState.thermostatSetpoint.toInteger(), unit: "F")
            }
            else
            {
              sendEvent(name: "temperature", value: farenhietToCelcius(data.climateState.temperature).toInteger(), unit: "C")
              sendEvent(name: "thermostatSetpoint", value: farenhietToCelcius(data.climateState.thermostatSetpoint).toInteger(), unit: "C")
            }
            
            sendEvent(name: "seat_heater_left", value: data.climateState.seat_heater_left)
            sendEvent(name: "seat_heater_right", value: data.climateState.seat_heater_right)            
            sendEvent(name: "seat_heater_rear_left", value: data.climateState.seat_heater_rear_left) 
            sendEvent(name: "seat_heater_rear_right", value: data.climateState.seat_heater_rear_right)
            sendEvent(name: "seat_heater_rear_center", value: data.climateState.seat_heater_rear_center)

        }
	} else {
    	log.error "No data found for ${device.deviceNetworkId}"
    }
}

def refresh() {
	log.debug "Executing 'refresh'"
     def now = new Date().format('MM/dd/yyyy h:mm a',location.timeZone)
     sendEvent(name: "lastUpdate", value: now, descriptionText: "Last Update: $now")
   
    def data = parent.refresh(this)
	processData(data)
}

def wake() {
	log.debug "Executing 'wake'"
	def data = parent.wake(this)
    processData(data)
    runIn(30, refresh)
}

def lock() {
	log.debug "Executing 'lock'"
	def result = parent.lock(this)
    if (result) { refresh() }
}

def unlock() {
	log.debug "Executing 'unlock'"
	def result = parent.unlock(this)
    if (result) { refresh() }
}

def auto() {
	log.debug "Executing 'auto'"
	def result = parent.climateAuto(this)
    if (result) { refresh() }
}

def off() {
	log.debug "Executing 'off'"
	def result = parent.climateOff(this)
    if (result) { refresh() }
}

def heat() {
	log.debug "Executing 'heat'"
	// Not supported
}

def emergencyHeat() {
	log.debug "Executing 'emergencyHeat'"
	// Not supported
}

def cool() {
	log.debug "Executing 'cool'"
	// Not supported
}

def setThermostatMode(mode) {
	log.debug "Executing 'setThermostatMode'"
	switch (mode) {
    	case "auto":
        	auto()
            break
        case "off":
        	off()
            break
        default:
        	log.error "setThermostatMode: Only thermostat modes Auto and Off are supported"
    }
}

def setThermostatSetpoint(Number setpoint) {
	log.debug "Executing 'setThermostatSetpoint with temp scale $tempScale'"
    if (tempScale == "F")
      {
	    def result = parent.setThermostatSetpointF(this, setpoint)
        if (result) { refresh() }
      }
    else
    {
        def result = parent.setThermostatSetpointC(this, setpoint)
        if (result) { refresh() }
    }
}

def startCharge() {
	log.debug "Executing 'startCharge'"
    def result = parent.startCharge(this)
    if (result) { refresh() }
}

def stopCharge() {
	log.debug "Executing 'stopCharge'"
    def result = parent.stopCharge(this)
    if (result) { refresh() }
}

def openFrontTrunk() {
	log.debug "Executing 'openFrontTrunk'"
    def result = parent.openTrunk(this, "front")
    // if (result) { refresh() }
}

def openRearTrunk() {
	log.debug "Executing 'openRearTrunk'"
    def result = parent.openTrunk(this, "rear")
    // if (result) { refresh() }
}

def unlockandOpenChargePort() {
	log.debug "Executing 'unock and open charge port'"
    def result = parent.unlockandOpenChargePort(this)
    // if (result) { refresh() }   
}  

def updated()
{
   
   initialize()
    
}

def setSeatHeaters(seat,level) {
	log.debug "Executing 'setSeatheater'"
	def result = parent.setSeatHeaters(this, seat,level)
    if (result) { refresh() }
}

def sentryModeOn() {
	log.debug "Executing 'Turn Sentry Mode On'"
	def result = parent.sentryModeOn(this)
    if (result) { refresh() }
}

def sentryModeOff() {
	log.debug "Executing 'Turn Sentry Mode Off'"
	def result = parent.sentryModeOff(this)
    if (result) { refresh() }
}

def ventWindows() {
	log.debug "Executing 'Venting Windows'"
	def result = parent.ventWindows(this)
    if (result) { refresh() }
}

def closeWindows() {
	log.debug "Executing 'Close Windows'"
	def result = parent.closeWindows(this)
    if (result) { refresh() }
}


private farenhietToCelcius(dF) {
	return (dF - 32) * 5/9
}

private milesToKm(dM) {
	return (dM * 1.609344)
}
1 Like