/carson

An asyncio package to interact with the Tesla JSON web service

Primary LanguagePythonMIT LicenseMIT

carson

Latest Version

Overview

carson is a simple Python interface for Tesla's unofficial JSON API and includes some utilities to work with data it generates. Lots of work to discover and document the API was done by Tim Dorr and dozens of contributors to his tesla-api project. So, thanks to them for the head start.

Among the goals for this project is to have an asyncio based library. As a result, Python 2 is not supported. In fact, it seems like it has been a decade since the provisional tag was removed from the asyncio library because it has evolved so much. There are many guides, articles, and posts based on early features of asyncio. The best way to stay up to date is by starting with Python's documentation at https://docs.python.org/library/asyncio.html. This project uses the async/await syntax introduced in PEP-492. As of this writing, the latest version of Python is 3.8.1.

Dependencies

There is one dependency for basic usage — aiohttp.

States And Commands

With its most basic usage, you can use carson to get the current state of car with the following code:

>>>import asyncio
>>>from carson import Session
>>>async def main():
...    name = 'Dark Nebula'
...    async with Session(email='nikola@tesla.com', password='electricity') as session:
...        car = await session.vehicles(name)
...        print(f'{name} is {car.state!r}')

>>>asyncio.run(main())
Dark Nebula is 'asleep'

Or you can run it from the command line in a similar fashion:

> python -m carson -v --email nikola@tesla.com --password electricity --display-name "Dark Nebula"

To get a sense of what is happening, you can add verbose and see the requests being made.

> python -m carson -v --email nikola@tesla.com --password electricity --display-name "Dark Nebula"
2020-01-01 10:45:59,418 D carson  Performing OAuth password grant for email='nikola@tesla.com'
2020-01-01 10:46:00,229 D carson  Req# 1:  Method=POST url='https://owner-api.teslamotors.com/oauth/token?grant_type=password' status=200 duration=0:00:00.810031
2020-01-01 10:46:00,943 D carson  Req# 2:  Method=GET url='https://owner-api.teslamotors.com/api/1/vehicles' status=200 duration=0:00:00.712868
2020-01-01 10:46:00,944 I carson  Vehicle('Dark Nebula' state='asleep')

You can see that two requests are made:

  1. First is to generate an oauth token which is required for all subsequent requests.
  2. Get a list of vehicles associated with the credentials provided.

OAuth

If you already have (or know how to generate) an auth_token or simply do not want to provide your Tesla account's email and password, you can instead provide your token. Simply replace the arguments email and password passed to the Session constructor with the argument access_token (or --auth-token if using the command line). Your auth token should resemble a long list of characters similar to the following:

0a1b2c3d4e5f0a1b2c3d4e5f0a1b2c3d4e5f0a1b2c3d4e5f0a1b2c3d4e5f0a1b

Example:

> python -m carson --auth-token 0a1b2c3d4e5f0a1b2c3d4e5f0a1b2c3d4e5f0a1b2c3d4e5f0a1b2c3d4e5f0a1b

Example:

...
async with Session(auth_token='0a1b2c3d4e5f0a1b2c3d4e5f0a1b2c3d4e5f0a1b2c3d4e5f0a1b2c3d4e5f0a1b') as session:
...

Configuration

Credentials can also be stored in configuration. carson looks for credentials in the following order:

  1. The arguments email and password passed to the Session constructor.
  2. The argument access_token passed to the Session constructor.
  3. The environment variables CARSON_EMAIL, CARSON_PASSWORD, CARSON_ACCESS_TOKEN.
  4. An .ini style config file named .carson or carson.ini in the user's home directory.

Regarding credentials: Always use care when storing credentials. Sometimes bad things can happen and often time will.

Credential Precedence

Credentials (password and auth_token) are used in the following order of precedence. When reading 'if' and 'if not', think Python boolean operations (e.g. '', None, 0 are all False).

  1. If password is given to the Session constructor, it will always be used to generate a new oauth token. Even if a valid auth_token is given to the Session constructor at the same time. This means a value for email must also be given (or implied from config).
  2. If password is not given to the Session constructor, the value used for access_token is used. Or, if not given, implied from config.
  3. If neither password nor access_token are given to the Session constructor, but both password and access_token are defined in config, the access_token from config will be used.

Streaming

Tesla provides a websocket endpoint from which telemetry data can be streamed and stored. To begin streaming this telemetry, issue the following command.

> python -m carson -v --display-name YOUR_CAR_NAME --stream

carson will attempt to wake-up the car and initiate the streaming telemetry. By default, the telemetry simply outputs the data to log. A sample of that output is below.

2020-01-01 14:09:30,129 D carson  Req# 1:  Method=GET url='https://owner-api.teslamotors.com/api/1/vehicles' status=200 duration=0:00:00.435516
2020-01-01 14:09:30,752 D carson  Req# 2:  Method=POST url='https://owner-api.teslamotors.com/api/1/vehicles/01234567890123456/wake_up' status=200 duration=0:00:00.614805
2020-01-01 14:09:30,752 D carson  Waiting for car to wake up.
2020-01-01 14:09:37,920 D carson  Req# 9:  Method=GET url='https://owner-api.teslamotors.com/api/1/vehicles/01234567890123456/vehicle_data' status=200 duration=0:00:00.301021
2020-01-01 14:09:37,920 I carson  Streaming iteration=1
2020-01-01 14:09:37,961 I carson  car=Vehicle('Dark Nebula' state='online' miles=18,421 software='2019.40.50.5' battery_level=81) iteration=1  client_errors=0 vehicle_disconnects=0
2020-01-01 14:09:38,412 D carson  {"msg_type":"data:subscribe","token":"bWlj********NmY1","value":"speed,odometer,soc,elevation,est_heading,est_lat,est_lng,power,shift_state,range,est_range,heading","tag":"0123456789"}
2020-01-01 14:09:38,412 D carson  msg_count=1 msg={'msg_type': 'control:hello', 'connection_timeout': 0}
2020-01-01 14:09:39,474 D carson  msg_count=2 msg={'msg_type': 'data:update', 'tag': '0123456789', 'value': '1577909378751,,18421.1,81,232,182,40.778955,-73.968583,0,,242,223,8'}
2020-01-01 14:09:49,479 D carson  Timeout waiting for next message.
2020-01-01 14:09:49,495 D carson  msg_count=3 msg={'msg_type': 'data:error', 'tag': '0123456789', 'value': 'disconnected', 'error_type': 'vehicle_disconnected'}
2020-01-01 14:09:49,566 I carson  Streamer task ending due to shift state=''.

Pythonic Features

Python is a fantastic language. One of my favorite features is its ability to customize attribute access. That ability allows a Vehcile class instance to basically act like a chameleon. As Tesla changes its data structure and command interface for its cars, it's pretty easy for a Python class to essentially keep itself up to date.

This section would normally be placed after the States And Commands section. But I wanted to put this above the fold to call out the Pythonic features of carson - both in programmability and general use on the command line.

Recursive Dot-Notation

Consider this JSON response from Tesla when getting making a call to vehicle_data:

{
  "response": {
    "id": 98765432109876543,
    "vehicle_id": 1234567890,
    "display_name": "Dark Nebula",
    "state": "online",
    ...
    "vehicle_state": {
      "api_version": 7,
      ...
      "sentry_mode": false,
      "sentry_mode_available": true,
      "smart_summon_available": true,
      "software_update": {
        "download_perc": 0.85279,
        "expected_duration_sec": 2700,
        "install_perc": 0,
        ...

With carson, after you make the call to get the vehicle data, you can access the JSON response that is returned, or simply reference its associated JSON path on the instance of the Vehicle using standard Python dot-notation like this:

car = await my_session.vehicles('Dark Nebula')
json_response = await car.vehicle_data()

# I have options here.  I can access the JSON data as a normal Python `dict`
perc = json_response['vehicle_state']['software_update']['download_perc']

# Or as a Python attribute
perc = car.vehicle_state.software_update.download_perc

if 0 < perc < 1:
    print(f'Downloading: {perc:.2%} complete.')
else:
    print('Download complete' if perc == 1 else 'N/A')

Endpoint Commands As awaitable Attributes

Similarly, commands can that are mapped to an endpoint accessed via Python's class instance attribute mechanism will return an awaitable coroutine. For example, the Vehicle class in carson does not have an attribute named start_charge. The endpoints mapping, however, does map START_CHARGE to a POST request to the URI api/1/vehicles/{vehicle_id}/command/charge_start. This makes it possible to start charging your Tesla with either this code:

car = await my_session.vehicles('Dark Nebula')
await car.start_charge()

or this command

> python -m carson -v --command start_charge
2020-01-01 11:51:44,349 D carson  Req# 1:  Method=GET url='https://owner-api.teslamotors.com/api/1/vehicles' status=200 duration=0:00:02.460019
2020-01-01 11:51:44,350 I carson  Vehicle('Dark Nebula' state='online')
2020-01-01 11:51:44,350 I carson  Performing 'start_charge'...
2020-01-01 11:51:44,753 D carson  Req# 2:  Method=POST url='https://owner-api.teslamotors.com/api/1/vehicles/01234567890123456/command/charge_start' status=200 duration=0:00:00.403062
2020-01-01 11:51:44,754 I carson  Result=
{'carsonRequest': {'method': 'POST',
                   'url': 'https://owner-api.teslamotors.com/api/1/vehicles/01234567890123456/command/charge_start'},
 'carsonTimestamp': '2020-01-01T17:51:44.350726',
 'error': None,
 'error_description': '',
 'response': {'reason': 'complete', 'result': True},
 'status': 200}