Last active
October 8, 2019 15:58
-
-
Save aviflax/3b0495028d0dd7fc59225c836dee2416 to your computer and use it in GitHub Desktop.
BitBar plugin for Tesla cars
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/local/bin/python3 | |
# -*- coding: utf-8 -*- | |
# | |
# <bitbar.title>Tesla BitBar</bitbar.title> | |
# <bitbar.version>v1.0</bitbar.version> | |
# <bitbar.author>[email protected]</bitbar.author> | |
# <bitbar.author.github>therippa</bitbar.author.github> | |
# <bitbar.desc>Control your Tesla from the menubar</bitbar.desc> | |
# <bitbar.image>https://i.imgur.com/XbwiemY.png</bitbar.image> | |
# <bitbar.dependencies>python</bitbar.dependencies> | |
# <bitbar.abouturl>https://github.com/therippa/tesla-bitbar</bitbar.abouturl> | |
# | |
# by [email protected] | |
try: # Python 3 | |
from urllib.parse import urlencode | |
from urllib.request import Request, urlopen, build_opener | |
from urllib.request import ProxyHandler, HTTPBasicAuthHandler, HTTPHandler | |
from urllib.request import HTTPError # <- is this right??? | |
except: # Python 2 | |
from urllib import urlencode | |
from urllib2 import Request, urlopen, build_opener | |
from urllib2 import ProxyHandler, HTTPBasicAuthHandler, HTTPHandler | |
from urllib2 import HTTPError | |
import json | |
import sys, os | |
import datetime | |
import calendar | |
import base64 | |
import keyring | |
import time | |
from pprint import pprint | |
USE_EMOJI=True | |
TEMP_UNIT='F' # 'F' or 'C' | |
# ---------------------------------- | |
# Thank you to Greg Glockner for the code below - https://github.com/gglockner/teslajson | |
# ---------------------------------- | |
class Connection(object): | |
"""Connection to Tesla Motors API""" | |
def __init__(self, | |
email='', | |
password='', | |
access_token='', | |
proxy_url = '', | |
proxy_user = '', | |
proxy_password = ''): | |
"""Initialize connection object | |
Sets the vehicles field, a list of Vehicle objects | |
associated with your account | |
Required parameters: | |
email: your login for teslamotors.com | |
password: your password for teslamotors.com | |
Optional parameters: | |
access_token: API access token | |
proxy_url: URL for proxy server | |
proxy_user: username for proxy server | |
proxy_password: password for proxy server | |
""" | |
self.proxy_url = proxy_url | |
self.proxy_user = proxy_user | |
self.proxy_password = proxy_password | |
tesla_client = { | |
'v1': { | |
'id': 'e4a9949fcfa04068f59abb5a658f2bac0a3428e4652315490b659d5ab3f35a9e', | |
'secret': 'c75f14bbadc8bee3a7594412c31416f8300256d7668ea7e6e7f06727bfb9d220', | |
'baseurl': 'https://owner-api.teslamotors.com', | |
'api': '/api/1/' | |
} | |
} | |
current_client = tesla_client['v1'] | |
self.baseurl = current_client['baseurl'] | |
self.api = current_client['api'] | |
self.access_token = None | |
self.expiration = 0 | |
if access_token: | |
self.__sethead(access_token) | |
else: | |
self.oauth = { | |
"grant_type" : "password", | |
"client_id" : current_client['id'], | |
"client_secret" : current_client['secret'], | |
"email" : email, | |
"password" : password } | |
# This is now a vechicles() function | |
#self.vehicles = [Vehicle(v, self) for v in self.get('vehicles')['response']] | |
def vehicles(self): | |
return [Vehicle(v, self) for v in self.get('vehicles')['response']] | |
def get_token(self): | |
if self.access_token and self.expiration < time.time(): | |
return self.access_token | |
try: | |
auth = self.__open("/oauth/token", data=self.oauth) | |
except HTTPError as e: | |
# Typically, this means non 200 response code | |
return None | |
if 'access_token' in auth and auth['access_token']: | |
self.access_token = auth['access_token'] | |
self.expiration = int(time.time()) + auth['expires_in'] - 86400 | |
return self.access_token | |
return None | |
def get(self, command): | |
"""Utility command to get data from API""" | |
return self.post(command, None) | |
def post(self, command, data={}): | |
"""Utility command to post data to API""" | |
now = time.time() | |
if now > self.expiration: | |
auth = self.__open("/oauth/token", data=self.oauth) | |
self.__sethead(auth['access_token'], | |
auth['created_at'] + auth['expires_in'] - 86400) | |
return self.__open("%s%s" % (self.api, command), headers=self.head, data=data) | |
def __sethead(self, access_token, expiration=float('inf')): | |
"""Set HTTP header""" | |
self.access_token = access_token | |
self.expiration = expiration | |
self.head = {"Authorization": "Bearer %s" % access_token} | |
def __open(self, url, headers={}, data=None, baseurl=""): | |
"""Raw urlopen command""" | |
if not baseurl: | |
baseurl = self.baseurl | |
req = Request("%s%s" % (baseurl, url), headers=headers) | |
try: | |
req.data = urlencode(data).encode('utf-8') # Python 3 | |
except: | |
try: | |
req.add_data(urlencode(data)) # Python 2 | |
except: | |
pass | |
# Proxy support | |
if self.proxy_url: | |
if self.proxy_user: | |
proxy = ProxyHandler({'https': 'https://%s:%s@%s' % (self.proxy_user, | |
self.proxy_password, | |
self.proxy_url)}) | |
auth = HTTPBasicAuthHandler() | |
opener = build_opener(proxy, auth, HTTPHandler) | |
else: | |
handler = ProxyHandler({'https': self.proxy_url}) | |
opener = build_opener(handler) | |
else: | |
opener = build_opener() | |
try: | |
resp = opener.open(req) | |
except: | |
if check_token_age(): | |
abort_credentials() | |
else: | |
print('--Error contacting Tesla\'s servers') | |
else: | |
charset = resp.info().get('charset', 'utf-8') | |
return json.loads(resp.read().decode(charset)) | |
class Vehicle(dict): | |
"""Vehicle class, subclassed from dictionary. | |
There are 3 primary methods: wake_up, data_request and command. | |
data_request and command both require a name to specify the data | |
or command, respectively. These names can be found in the | |
Tesla JSON API.""" | |
def __init__(self, data, connection): | |
"""Initialize vehicle class | |
Called automatically by the Connection class | |
""" | |
super(Vehicle, self).__init__(data) | |
self.connection = connection | |
def data_request(self, name): | |
"""Get vehicle data""" | |
try: | |
result = self.get('data_request/%s' % name) | |
return result['response'] | |
except: | |
pass | |
def wake_up(self): | |
"""Wake the vehicle""" | |
return self.post('wake_up') | |
def command(self, name, data={}): | |
"""Run the command for the vehicle""" | |
return self.post('command/%s' % name, data) | |
def get(self, command): | |
"""Utility command to get data from API""" | |
return self.connection.get('vehicles/%i/%s' % (self['id'], command)) | |
def post(self, command, data={}): | |
"""Utility command to post data to API""" | |
return self.connection.post('vehicles/%i/%s' % (self['id'], command), data) | |
def convert_temp(temp): | |
if TEMP_UNIT == 'F': | |
return (temp * 1.8) + 32 | |
else: | |
return temp | |
def prompt_login(): | |
for attempt in range(3): | |
sys.stdout.write("\ntesla.com username (will not be saved): ") | |
username = sys.stdin.readline() | |
sys.stdout.write("tesla.com password (will not be saved): ") | |
os.system("stty -echo") # Don't echo typed characters to terminal | |
password = sys.stdin.readline() | |
os.system("stty echo") # Echo characters to terminal, as normal | |
sys.stdout.write("\nChecking...") | |
sys.stdout.flush() | |
c = Connection(username.rstrip(), password.rstrip()) | |
access_token = c.get_token() | |
if not access_token: | |
print ("Access denied") | |
time.sleep(0.5) | |
continue | |
tesla_access_token=keyring.set_password('tesla-bitbar', 'access-token', access_token) | |
keyring.set_password('tesla-bitbar', 'access-token-date', datetime.datetime.now().strftime('%Y-%m-%d')) | |
print ('Success!') | |
print ("\nType \"exit\" and hit enter to close this window") | |
return | |
print ("Sorry, double check your username and password then try again") | |
print ("\nType \"exit\" and hit enter to close this window") | |
def check_token_age(): | |
token_date = keyring.get_password('tesla-bitbar', 'access-token-date') | |
if token_date == None: | |
return False | |
elif datetime.datetime.strptime(token_date, '%Y-%m-%d') <= datetime.datetime.now() - datetime.timedelta(days=45): | |
return True | |
else: | |
return False | |
def abort_credentials(): | |
print ('Click to login | refresh=true terminal=true bash="%s" param1=login' % (sys.argv[0])) | |
def humanReadableDelta(delta): | |
deltaMinutes = delta.seconds // 60 | |
deltaHours = delta.seconds // 3600 | |
deltaMinutes -= deltaHours * 60 | |
deltaWeeks = delta.days // 7 | |
deltaDays = delta.days - deltaWeeks * 7 | |
valuesAndNames =[ (deltaWeeks ,"week" ), (deltaDays ,"day" ), | |
(deltaHours ,"hour" ), (deltaMinutes,"minute") ] | |
text = "" | |
for value, name in valuesAndNames: | |
if value > 0: | |
text += len(text) and ", " or "" | |
text += "%d %s" % (value, name) | |
text += (value > 1) and "s" or "" | |
if text.find(",") > 0: | |
text = " and ".join(text.rsplit(", ",1)) | |
return text | |
def main(): | |
if len(sys.argv) > 1 and sys.argv[1] == "login": | |
prompt_login() | |
return | |
bitBarDarkMode = os.getenv('BitBarDarkMode', 0) | |
if bitBarDarkMode: | |
color = 'white' | |
else: | |
color = 'black' | |
# print menu - below is icon.png encoded as base64 | |
print ('|templateImage=iVBORw0KGgoAAAANSUhEUgAAACwAAAAsCAYAAAAehFoBAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAACXBIWXMAABYlAAAWJQFJUiTwAAABWWlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS40LjAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyI+CiAgICAgICAgIDx0aWZmOk9yaWVudGF0aW9uPjE8L3RpZmY6T3JpZW50YXRpb24+CiAgICAgIDwvcmRmOkRlc2NyaXB0aW9uPgogICA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgpMwidZAAACQ0lEQVRYCe2XTUtVURSGrx+D/KJAMAxs4GcDnehAii4N/A9hREENFJzmpH+hkxo3UME/oKBNEowiBMtm6sSaFRWEEWn1vHA2bDb7WvfedThe2Ase9vda7153n33PKZWSpQykDKQMpAwUmYGmGoM3s24UJmEQBqAfuqEzg6L0PeMz5SEcwD68gj34DVVZNYL78HwbpuAGXARnp1SOQMKOMyhK7RnaiNa3gLNvVLbhOayC1v/T/kfwHF7uwnXQ/BN4Ay9gC5Q5mUS9BQnx7RKNMfgEWq9fogy3YAJa4Q+8hCV4AnXZMquVwQ24B8NwH56CBGoDCig0x8+i6pvZmMY1dxe01vlSqXWKsQJ12xU8DMEdWAdfoBPqlwvMcbZIxR8L6/K1BtOgGIplYrN4CYOprYBf4AMoQ27OA+oPvbbGPoLmVtrwDGOmNo+3XyBRr+EmXABneuqd4J/UhWvvuEmUbVAG+dC4fD6CXGwcrzpvOseh6fw5gWH5LJxMewTkSz4LscdEDYW6tlkGmw23phujkr2rNFBk/1WCu4yGZW+Rws6K/TUiWn8YZmZ5JCQqdixifTVvwFpw7KzG+s6N4Fg2G05wbBM1Z9h6YRcO9Y7rbgnVO6yDWPs79ATvWzu3fuikzz+z5schD8G+SF+8SbIbTrDJrgMn12i7hy72VhdML76pz6IfoI9R819QH4DWpq+L95lTXWumlodgCdTDpmNhbnkJ9m8KU9F5CTa/zkx3HXHWQ9/lSH/qShlIGUgZSBlo8Az8BUQapSzDgvJzAAAAAElFTkSuQmCC') | |
print ('---') | |
tesla_access_token=keyring.get_password('tesla-bitbar', 'access-token') | |
if not tesla_access_token: | |
abort_credentials() | |
return | |
# create connection to tesla account | |
c = Connection(access_token = tesla_access_token) | |
try: | |
vehicles = c.vehicles() | |
except HTTPError as e: | |
if e.code == 401: | |
abort_credentials() | |
return | |
raise | |
except TypeError as e: | |
abort_credentials() | |
return | |
raise | |
# see if args are passed, if so, pass commands and bail | |
if len(sys.argv) > 1: | |
v = vehicles[int(sys.argv[1])] | |
v.wake_up() | |
if sys.argv[2] != "wakeup": | |
v.command(sys.argv[2]) | |
return | |
# only do submenu if multiple vehicles | |
prefix = '' | |
if len(vehicles) > 1: | |
prefix = '--' | |
battery_str = "Battery:" | |
charging_str = "Charging:" | |
temperature_str = "Temp:" | |
if USE_EMOJI: | |
battery_str = ":battery:" | |
charging_str = ":electric_plug:" | |
temperature_str = ":partly_sunny:" | |
# loop through vehicles, print menu with relevant info | |
for i, vehicle in enumerate(vehicles): | |
if prefix: | |
print(vehicle['display_name']) | |
if vehicle['state'] != "online" and vehicle['state'] != "driving": | |
print ('%sState: %s| color=%s' % (prefix, vehicle['state'], color)) | |
print ('%sWakeup | refresh=true terminal=false bash="%s" param1=%s param2=wake_up color=%s' % (prefix, sys.argv[0], str(i), color)) | |
print ('%sStart HVAC | refresh=true terminal=false bash="%s" param1=%s param2=auto_conditioning_start color=%s' % (prefix, sys.argv[0], str(i), color)) | |
else: | |
charge_state = vehicle.data_request('charge_state') | |
climate_state = vehicle.data_request('climate_state') | |
vehicle_state = vehicle.data_request('vehicle_state') | |
drive_state = vehicle.data_request('drive_state') | |
gui_settings = vehicle.data_request('gui_settings') | |
print ('%s%s %s%%| color=%s' % (prefix, battery_str, str(charge_state['battery_level']), color)) | |
# The default charge state text | |
pretty_charge_state = charge_state['charging_state'] | |
if charge_state['charging_state'] == "Charging": | |
# Calculate the wattage ourself for more signifiant digits | |
v = charge_state['charger_voltage'] | |
a = charge_state['charger_actual_current'] | |
p = charge_state['charger_phases'] | |
if v and a and p: | |
rate = float(v * a * p) / 1000.0 | |
else: | |
rate = 0 | |
added = charge_state['charge_energy_added'] | |
pretty_charge_state = "+%0.2f kWh @ %0.2f kW" % (added, rate) | |
try: | |
ttf = float(charge_state['time_to_full_charge']) | |
pretty_charge_state += ", %s remaining" % humanReadableDelta(datetime.timedelta(hours=ttf)) | |
except Exception: | |
pass | |
elif charge_state['charge_port_latch'] != "Engaged": | |
pretty_charge_state = "Unplugged" | |
else: | |
if charge_state['scheduled_charging_pending']: | |
pretty_charge_state = "Scheduled" | |
print ('%s%s %s| color=%s' % (prefix, charging_str, pretty_charge_state, color)) | |
print ('%s%s Charge Limit: %s%%| color=%s' % (prefix, (':zap:' if USE_EMOJI else ''), charge_state['charge_limit_soc'], color)) | |
print ('%s--Set to Standard | refresh=true terminal=false bash="%s" param1=%s param2=charge_standard color=%s' % (prefix, sys.argv[0], str(i), color)) | |
print ('%s--Set to Max | refresh=true terminal=false bash="%s" param1=%s param2=charge_max_range color=%s' % (prefix, sys.argv[0], str(i), color)) | |
print ('%s---' % prefix) | |
inside_temp = '??' | |
outside_temp = '??' | |
try: | |
inside_temp = ('%.1f' % convert_temp(climate_state['inside_temp'])) | |
except: | |
pass | |
try: | |
outside_temp = ('%.1f' % convert_temp(climate_state['outside_temp'])) | |
except: | |
pass | |
print ('%s%s %s° inside / %s° outside|color=%s' % (prefix, temperature_str, inside_temp, outside_temp, color)) | |
if climate_state['is_climate_on']: | |
print ('%s--Stop HVAC | refresh=true terminal=false bash="%s" param1=%s param2=auto_conditioning_stop color=%s' % (prefix, sys.argv[0], str(i), color)) | |
else: | |
print ('%s--Start HVAC | refresh=true terminal=false bash="%s" param1=%s param2=auto_conditioning_start color=%s' % (prefix, sys.argv[0], str(i), color)) | |
if vehicle_state['locked']: | |
doors_locked = 'Locked' | |
door_emoji = ':lock:' if USE_EMOJI else '' | |
door_action = ('%s--Unlock Doors | refresh=true terminal=false bash="%s" param1=%s param2=door_unlock color=%s' % (prefix, sys.argv[0], str(i), color)) | |
else: | |
doors_locked = 'Unlocked' | |
door_emoji = ':unlock:' if USE_EMOJI else '' | |
door_action = ('%s--Lock Doors | refresh=true terminal=false bash="%s" param1=%s param2=door_lock color=%s' % (prefix, sys.argv[0], str(i), color)) | |
print ('%s---' % prefix) | |
print ('%s%s Doors are %s| color=%s' % (prefix, door_emoji, doors_locked, color)) | |
print (door_action) | |
print ('%s---' % prefix) | |
print ('%s%s View Location | href="https://maps.google.com?q=%s,%s" color=%s' % (prefix, (':earth_americas:' if USE_EMOJI else ''), drive_state['latitude'], drive_state['longitude'], color)) | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment