├── tests └── __init__.py ├── .python-version ├── bin └── rivian_cli ├── src └── rivian_python_api │ ├── __init__.py │ ├── rivian_map.py │ ├── rivian_api.py │ └── rivian_cli.py ├── requirements.txt ├── pyproject.toml ├── LICENSE ├── .gitignore ├── techstack.md ├── techstack.yml └── README.md /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.12.7 2 | -------------------------------------------------------------------------------- /bin/rivian_cli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | python src/rivian_python_api/rivian_cli.py "$@" -------------------------------------------------------------------------------- /src/rivian_python_api/__init__.py: -------------------------------------------------------------------------------- 1 | from .rivian_api import Rivian 2 | 3 | __all__ = ["Rivian"] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | plotly 2 | polyline 3 | python-dateutil 4 | python-dotenv 5 | requests 6 | geopy -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "rivian-python-api" 7 | version = "0.1.3" 8 | authors = [ 9 | { name="Robert Mason", email="rivian@crickers.com" }, 10 | ] 11 | description = "Python API for Rivian" 12 | readme = "README.md" 13 | requires-python = ">=3.7" 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | ] 19 | 20 | [project.urls] 21 | "Homepage" = "https://github.com/the-mace/rivian-python-api" 22 | "Bug Tracker" = "https://github.com/the-mace/rivian-python-api/issues" 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Rob Mason 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | staticfiles 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Use pipenv 39 | requirements.txt 40 | dev-requirements.txt 41 | 42 | # OSX Trash 43 | .DS_Store 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # celery beat schedule file 83 | celerybeat-schedule 84 | 85 | # SageMath parsed files 86 | *.sage.py 87 | 88 | # Environments 89 | .env 90 | .venv 91 | env/ 92 | venv/ 93 | ENV/ 94 | env.bak/ 95 | venv.bak/ 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | # pickle files 104 | *.pickle 105 | 106 | # IDE 107 | .idea/ 108 | .vscode/ 109 | -------------------------------------------------------------------------------- /src/rivian_python_api/rivian_map.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import math 4 | import polyline 5 | import plotly.graph_objects as go 6 | from geopy.geocoders import Nominatim 7 | 8 | # Initialize Nominatim geocoder 9 | geolocator = Nominatim(user_agent="rivian_cli") 10 | 11 | def decode_and_map(planned_trip): 12 | # route response is a json object embedded as a string so parse it out 13 | route_response = json.loads(planned_trip['data']['planTrip']['routes'][0]['routeResponse']) 14 | 15 | # decode polyline from geometry 16 | route_path = polyline.decode(route_response['geometry'], 6) 17 | 18 | show_map(route_path, planned_trip['data']['planTrip']['routes'][0]['waypoints']) 19 | 20 | def show_map(route, waypoints=[]): 21 | MAPBOX_API_KEY = os.getenv('MAPBOX_API_KEY') 22 | 23 | if MAPBOX_API_KEY is None: 24 | print("Missing MAPBOX_API_KEY, please set .env") 25 | return 26 | 27 | fig = go.Figure() 28 | filtered_waypoints = [] 29 | 30 | # Filter waypoints to only include objects with waypointType equal to 'DC_CHARGE_STATION' 31 | if waypoints is not None: 32 | filtered_waypoints = list(filter(lambda waypoint: waypoint['waypointType'] == 'DC_CHARGE_STATION', waypoints)) 33 | 34 | fig.add_trace(go.Scattermapbox( 35 | mode = "lines", 36 | hoverinfo = "none", 37 | lon = [wp[1] for wp in route], 38 | lat = [wp[0] for wp in route], 39 | marker = {'size': 10}, 40 | name='Route' 41 | )) 42 | 43 | fig.add_trace(go.Scattermapbox( 44 | mode = "markers", 45 | lat = [wp['latitude'] for wp in filtered_waypoints], 46 | lon = [wp['longitude'] for wp in filtered_waypoints], 47 | customdata=[charger_hover_info(wp) for wp in filtered_waypoints], 48 | hovertemplate='%{customdata}', 49 | marker = {'size': 20, 'color': 'green'}, 50 | name='Charge Stops' 51 | )) 52 | 53 | if waypoints is not None: 54 | fig.add_trace(go.Scattermapbox( 55 | mode = "markers", 56 | lat = [waypoints[-1]['latitude']], 57 | lon = [waypoints[-1]['longitude']], 58 | customdata=[destination_hover_info(waypoints[-1])], 59 | hovertemplate='%{customdata}', 60 | marker = {'size': 20}, 61 | name='Destination' 62 | )) 63 | 64 | # Find the bounding box of the polyline 65 | min_lat, max_lat = min(lat for lat, lng in route), max(lat for lat, lng in route) 66 | min_lng, max_lng = min(lng for lat, lng in route), max(lng for lat, lng in route) 67 | 68 | # # Calculate the center of the bounding box 69 | center_lat = (min_lat + max_lat) / 2 70 | center_lng = (min_lng + max_lng) / 2 71 | 72 | fig.update_layout( 73 | mapbox = { 74 | 'accesstoken': MAPBOX_API_KEY, 75 | 'center': {'lon': center_lng, 'lat': center_lat}, 76 | # this is pretty arbitrary 77 | 'zoom': 6 - 0.3 * ((max_lng - min_lng) / (max_lat - min_lat)) 78 | }) 79 | 80 | # Show the map 81 | fig.show() 82 | 83 | def charger_hover_info(charger): 84 | info = (f" {charger['name']}

" 85 | f"Charge for {math.ceil(charger['chargeDuration']/60)} minutes:
" 86 | f"{str(math.floor(charger['arrivalSOC']))}% → {str(math.floor(charger['departureSOC']))}%
" 87 | ) 88 | return info 89 | 90 | def destination_hover_info(dest): 91 | info = (f" Destination

" 92 | f"Arrival SOC: {str(math.floor(dest['arrivalSOC']))}%" 93 | ) 94 | return info 95 | 96 | # Define function to extract latitude and longitude from input field 97 | def extract_lat_long(input_field): 98 | location = geolocator.geocode(input_field) 99 | lat = location.latitude 100 | long = location.longitude 101 | return lat, long -------------------------------------------------------------------------------- /techstack.md: -------------------------------------------------------------------------------- 1 | 28 |
29 | 30 | # Tech Stack File 31 | ![](https://img.stackshare.io/repo.svg "repo") [the-mace/rivian-python-api](https://github.com/the-mace/rivian-python-api)![](https://img.stackshare.io/public_badge.svg "public") 32 |

33 | |11
Tools used|12/14/23
Report generated| 34 | |------|------| 35 |
36 | 37 | ## Languages (1) 38 | 39 | 46 | 47 | 48 |
40 | Python 41 |
42 | Python 43 |
44 | 45 |
49 | 50 | ## Data (1) 51 | 52 | 59 | 60 | 61 |
53 | pgvector 54 |
55 | pgvector 56 |
57 | 58 |
62 | 63 | ## DevOps (2) 64 | 65 | 72 | 73 | 80 | 81 | 82 |
66 | Git 67 |
68 | Git 69 |
70 | 71 |
74 | PyPI 75 |
76 | PyPI 77 |
78 | 79 |
83 | 84 | ## Other (2) 85 | 86 | 93 | 94 | 101 | 102 | 103 |
87 | LangChain 88 |
89 | LangChain 90 |
91 | 92 |
95 | Shell 96 |
97 | Shell 98 |
99 | 100 |
104 | 105 | 106 | ## Open source packages (5) 107 | 108 | ## PyPI (5) 109 | 110 | |NAME|VERSION|LAST UPDATED|LAST UPDATED BY|LICENSE|VULNERABILITIES| 111 | |:------|:------|:------|:------|:------|:------| 112 | |[geopy](https://pypi.org/project/geopy)|N/A|04/18/23|Rob |MIT|N/A| 113 | |[plotly](https://pypi.org/project/plotly)|N/A|04/12/23|Geoffrey Kruse |MIT|N/A| 114 | |[python-dateutil](https://pypi.org/project/python-dateutil)|N/A|04/12/23|Geoffrey Kruse |NRL|N/A| 115 | |[python-dotenv](https://pypi.org/project/python-dotenv)|N/A|04/12/23|Geoffrey Kruse |BSD-3-Clause|N/A| 116 | |[requests](https://pypi.org/project/requests)|N/A|04/18/23|Rob |Apache-2.0|N/A| 117 | 118 |
119 |
120 | 121 | Generated via [Stack File](https://github.com/marketplace/stack-file) 122 | -------------------------------------------------------------------------------- /techstack.yml: -------------------------------------------------------------------------------- 1 | repo_name: the-mace/rivian-python-api 2 | report_id: ab368ab65a6b000fd4f352bc32c8251f 3 | repo_type: Public 4 | timestamp: '2023-12-14T09:30:24+00:00' 5 | requested_by: the-mace 6 | provider: github 7 | branch: main 8 | detected_tools_count: 11 9 | tools: 10 | - name: Python 11 | description: A clear and powerful object-oriented programming language, comparable 12 | to Perl, Ruby, Scheme, or Java. 13 | website_url: https://www.python.org 14 | open_source: true 15 | hosted_saas: false 16 | category: Languages & Frameworks 17 | sub_category: Languages 18 | image_url: https://img.stackshare.io/service/993/pUBY5pVj.png 19 | detection_source: Repo Metadata 20 | - name: pgvector 21 | description: Open-source vector similarity search for Postgres 22 | website_url: https://github.com/pgvector/pgvector/ 23 | open_source: false 24 | hosted_saas: false 25 | category: Data Stores 26 | sub_category: Database Tools 27 | image_url: https://img.stackshare.io/service/109221/default_b888cdf5617d936aa6aacf130911906955508639.png 28 | detection_source: pyproject.toml 29 | last_updated_by: Rob 30 | last_updated_on: 2023-02-25 21:03:19.000000000 Z 31 | - name: Git 32 | description: Fast, scalable, distributed revision control system 33 | website_url: http://git-scm.com/ 34 | open_source: true 35 | hosted_saas: false 36 | category: Build, Test, Deploy 37 | sub_category: Version Control System 38 | image_url: https://img.stackshare.io/service/1046/git.png 39 | detection_source: Repo Metadata 40 | - name: PyPI 41 | description: A repository of software for the Python programming language 42 | website_url: https://pypi.org/ 43 | open_source: false 44 | hosted_saas: false 45 | category: Build, Test, Deploy 46 | sub_category: Hosted Package Repository 47 | image_url: https://img.stackshare.io/service/12572/-RIWgodF_400x400.jpg 48 | detection_source: requirements.txt 49 | last_updated_by: Geoffrey Kruse 50 | last_updated_on: 2023-04-12 18:46:19.000000000 Z 51 | - name: LangChain 52 | description: Build AI apps with LLMs through composability 53 | website_url: https://github.com/hwchase17/langchain 54 | open_source: true 55 | hosted_saas: false 56 | category: Communications 57 | sub_category: Large Language Model Tools 58 | image_url: https://img.stackshare.io/service/48790/default_5b6c6b73f1ff3775c85d2a1ba954cb87e30cbf13.jpg 59 | detection_source: pyproject.toml 60 | last_updated_by: Rob 61 | last_updated_on: 2023-02-25 21:03:19.000000000 Z 62 | - name: Shell 63 | description: A shell is a text-based terminal, used for manipulating programs and 64 | files. Shell scripts typically manage program execution. 65 | website_url: https://en.wikipedia.org/wiki/Shell_script 66 | open_source: false 67 | hosted_saas: false 68 | category: Languages & Frameworks 69 | sub_category: Languages 70 | image_url: https://img.stackshare.io/service/4631/default_c2062d40130562bdc836c13dbca02d318205a962.png 71 | detection_source: Repo Metadata 72 | - name: geopy 73 | description: Python Geocoding Toolbox 74 | package_url: https://pypi.org/project/geopy 75 | license: MIT 76 | open_source: true 77 | hosted_saas: false 78 | category: Libraries 79 | sub_category: PyPI Packages 80 | image_url: https://img.stackshare.io/package/20597/default_765f8741ddac0d2e816e17438b35e7fda5019b4a.png 81 | detection_source: requirements.txt 82 | last_updated_by: Rob 83 | last_updated_on: 2023-04-18 13:55:06.000000000 Z 84 | - name: plotly 85 | description: An open-source, interactive graphing library for Python 86 | package_url: https://pypi.org/project/plotly 87 | license: MIT 88 | open_source: true 89 | hosted_saas: false 90 | category: Libraries 91 | sub_category: PyPI Packages 92 | image_url: https://img.stackshare.io/package/20062/default_7d86b2789b7e98a881e37db483c09c6a1aa3e995.png 93 | detection_source: requirements.txt 94 | last_updated_by: Geoffrey Kruse 95 | last_updated_on: 2023-04-12 18:46:19.000000000 Z 96 | - name: python-dateutil 97 | description: Extensions to the standard Python datetime module 98 | package_url: https://pypi.org/project/python-dateutil 99 | license: NRL 100 | open_source: true 101 | hosted_saas: false 102 | category: Libraries 103 | sub_category: PyPI Packages 104 | image_url: https://img.stackshare.io/package/19833/default_58dbe7b4d7ec447b62773209af0f9a31bbabf5bd.png 105 | detection_source: requirements.txt 106 | last_updated_by: Geoffrey Kruse 107 | last_updated_on: 2023-04-12 18:46:19.000000000 Z 108 | - name: python-dotenv 109 | description: Add .env support to your django/flask apps in development and deployments 110 | package_url: https://pypi.org/project/python-dotenv 111 | license: BSD-3-Clause 112 | open_source: true 113 | hosted_saas: false 114 | category: Libraries 115 | sub_category: PyPI Packages 116 | image_url: https://img.stackshare.io/package/20095/default_3141eabecdd8efa55de73a33c43f2ac0d5bbf954.png 117 | detection_source: requirements.txt 118 | last_updated_by: Geoffrey Kruse 119 | last_updated_on: 2023-04-12 18:46:19.000000000 Z 120 | - name: requests 121 | description: Python HTTP for Humans 122 | package_url: https://pypi.org/project/requests 123 | license: Apache-2.0 124 | open_source: true 125 | hosted_saas: false 126 | category: Libraries 127 | sub_category: PyPI Packages 128 | image_url: https://img.stackshare.io/package/19826/default_d7c684bf2673f008a9f02ac93901229297a22d7e.png 129 | detection_source: requirements.txt 130 | last_updated_by: Rob 131 | last_updated_on: 2023-04-18 13:55:06.000000000 Z 132 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rivian-python-api - A python API for Rivian 2 | 3 | ## Sources 4 | 5 | Based on information from https://github.com/kaedenbrinkman/rivian-api. 6 | 7 | See https://github.com/the-mace/rivian-ruby-api for a Ruby version 8 | 9 | ## State of development 10 | 11 | ### Polling 12 | After a number of tests, polling appears not to impact the sleep state of the vehicle unlike 13 | the behavior of other vendors. 14 | 15 | The CLI has a polling option (--poll) which you can experiment with yourself, but leaving the 16 | vehicle alone from a polling perspective or constantly hitting it via the API appears to have no impact 17 | on when it goes to sleep. 18 | 19 | You can modify polling frequency etc with the CLI options. Be careful as polling too frequently can cause your account to get locked out. 20 | 21 | **It's strongly recommended that you use a non-primary driver account for API access** to avoid locking yourself out of your account and being unable to reset it on your own (by deleting/readding the secondary). 22 | 23 | Polling was also possible during a software update with no disruption to the update and it's possible 24 | to monitor software update progress that way. 25 | 26 | ### Actions 27 | I have not yet tested/completed actions like "Open Frunk" (see `--command` option in CLI) 28 | Rivian has greatly limited the utility of this for third parties because: 29 | 30 | 1. They limit you to 2 phones per vehicle 31 | 2. Actions are cryptographically signed by the registered phones and validated 32 | 3. You can't move/use your signature from your phone, so you need to register a new "phone" (the API) to do actions, thus giving up one of the 2 precious phone slots. 33 | 4. With more than one driver this is very limiting 34 | 35 | So while technically possible to remotely control the vehicle via API, 36 | the utility is greatly limited due to Rivian's approach. 37 | 38 | Note you can definitely argue that their approach is more secure than that of other vendors, but 39 | it also limits the ability to extend the owners experience through third party products. 40 | 41 | ### Missing & Unknown 42 | 1. There does not appear to be an API call that returns `speed` for the vehicle. With odometer and polling you can calculate it. Example in the CLI 43 | 2. If you lock yourself out of your account by asking for too much data too often (note that this isnt that easy to do) you'll get a response like: 44 | 45 | `{'errors': [{'extensions': {'code': 'RATE_LIMIT'}, 'message': 'See server logs for error details', 'path': ['vehicleState']}], 'data': {'vehicleState': None}}` 46 | 47 | If thats on your primary account you'll need to involve Rivian support to get it unlocked and that will take time etc. Best to use a secondary account for API access. 48 | 49 | ## Dependencies 50 | 51 | Python 3 52 | pip 53 | 54 | ## Security 55 | 56 | Without additional authentication the API and CLI can only monitor your 57 | Rivian (when you use the API or issue CLI commands). 58 | 59 | They have no ability to do the `actions` (see above) to unlock, enable drive, etc. 60 | 61 | Some information returned by the API from Rivian and to the screen by the CLI is personally 62 | identifiable information (PII) such as addresses, email addresses, GPS coordinates, etc. 63 | 64 | There are some options in the CLI to hide some of that but consider 65 | your data before sharing in public places. 66 | 67 | ### API 68 | The API does nothing in terms of storage of credentials etc. 69 | 70 | ### CLI 71 | The CLI supports the login flow including multi-factor authentication communicating directly with Rivian. 72 | 73 | It does not preserve your email or password. 74 | It does save your authentication tokens (locally on your machine in `rivian_auth.pickle`) 75 | to make it possible to run subsequent commands without logging in again. 76 | 77 | To remove your authentication information (again only on your machine) delete the `rivian_auth.pickle` file. 78 | 79 | No data is sent or stored anywhere other than your machine or directly at Rivian according 80 | to their understood API behavior. 81 | 82 | Feel free to review the code to verify the above. 83 | 84 | ## Setup 85 | 86 | ### For API 87 | None 88 | 89 | ### For CLI 90 | `pip install -r requirements.txt` 91 | 92 | *Note: For any actions with the CLI you'll need to login, see login information below.* 93 | 94 | ## CLI Commands 95 | 96 | The CLI is meant to be an example of API usage as well as to provide some 97 | useful outputs to see what your vehicle is reporting. The CLI is not meant to be 98 | a full-blown application. 99 | 100 | For simplicity, the CLI will "guess" at which vehicle it should be talking to for responses. 101 | You can specify a specific vehicle (and avoid some extra API calls) using `--vehicle_id` 102 | 103 | There's intentionally no multi-vehicle support other than the above, the CLI is a limited 104 | test bed / example of API use. 105 | 106 | In most cases CLI output shows a subset of overall response data. Use `--verbose` to see 107 | all the infor returned by the API for the given call. 108 | 109 | ### Login 110 | ``` 111 | bin/rivian_cli --login 112 | ``` 113 | 114 | Login, will interactively prompt for MFA if needed. 115 | Expects `RIVIAN_USERNAME` and `RIVIAN_PASSWORD` in shell environment. 116 | 117 | ### Vehicle Orders 118 | ``` 119 | bin/rivian_cli --vehicle_orders 120 | ``` 121 | 122 | ### Vehicle Orders hiding PII 123 | ``` 124 | bin/rivian_cli --vehicle_orders --privacy 125 | ``` 126 | 127 | ### Vehicle Orders with raw dumps 128 | ``` 129 | bin/rivian_cli --vehicle_orders --verbose 130 | ``` 131 | 132 | ### Vehicle State 133 | ``` 134 | bin/rivian_cli --state 135 | ``` 136 | 137 | ### Vehicle State Polling 138 | ``` 139 | bin/rivian_cli --poll 140 | ``` 141 | 142 | ### Trip planning 143 | Plan trip will create a basic visualization of the route and charge stops. MAPBOX_API_KEY needs to be set in `.env` 144 | ``` 145 | bin/rivian_cli --plan_trip 85,225,40.5112,-89.0559,39.7706,-104.9530 146 | ``` 147 | 148 | ### Other commands 149 | ``` 150 | bin/rivian_cli --help 151 | ``` 152 | 153 | ## CLI Notes 154 | * Supports authentication with and without OTP (interactive terminal) 155 | * Saves login information in a .pickle file to avoid login each time (login once, then run other commands) 156 | -------------------------------------------------------------------------------- /src/rivian_python_api/rivian_api.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import requests 3 | import uuid 4 | import time 5 | 6 | RIVIAN_BASE_PATH = "https://rivian.com/api/gql" 7 | RIVIAN_GATEWAY_PATH = RIVIAN_BASE_PATH + "/gateway/graphql" 8 | RIVIAN_CHARGING_PATH = RIVIAN_BASE_PATH + "/chrg/user/graphql" 9 | RIVIAN_ORDERS_PATH = RIVIAN_BASE_PATH + '/orders/graphql' 10 | RIVIAN_CONTENT_PATH = RIVIAN_BASE_PATH + '/content/graphql' 11 | RIVIAN_TRANSACTIONS_PATH = RIVIAN_BASE_PATH + '/t2d/graphql' 12 | 13 | log = logging.getLogger(__name__) 14 | 15 | HEADERS = { 16 | "User-Agent": "RivianApp/1304 CFNetwork/1404.0.5 Darwin/22.3.0", 17 | "Accept": "application/json", 18 | "Content-Type": "application/json", 19 | "Apollographql-Client-Name": "com.rivian.ios.consumer-apollo-ios", 20 | } 21 | 22 | 23 | class Rivian: 24 | def __init__(self): 25 | self._close_session = False 26 | self._session_token = "" 27 | self._access_token = "" 28 | self._refresh_token = "" 29 | self._app_session_token = "" 30 | self._user_session_token = "" 31 | self.client_id = "" 32 | self.client_secret = "" 33 | self.request_timeout = "" 34 | self._csrf_token = "" 35 | 36 | self.otp_needed = False 37 | self._otp_token = "" 38 | 39 | def login(self, username, password): 40 | self.create_csrf_token() 41 | url = RIVIAN_GATEWAY_PATH 42 | headers = HEADERS 43 | headers.update( 44 | { 45 | "Csrf-Token": self._csrf_token, 46 | "A-Sess": self._app_session_token, 47 | "Apollographql-Client-Name": "com.rivian.ios.consumer-apollo-ios", 48 | "Dc-Cid": f"m-ios-{uuid.uuid4()}", 49 | } 50 | ) 51 | 52 | query = { 53 | "operationName": "Login", 54 | "query": "mutation Login($email: String!, $password: String!) {\n login(email: $email, password: $password) {\n __typename\n ... on MobileLoginResponse {\n __typename\n accessToken\n refreshToken\n userSessionToken\n }\n ... on MobileMFALoginResponse {\n __typename\n otpToken\n }\n }\n}", 55 | "variables": {"email": username, "password": password}, 56 | } 57 | 58 | response = self.raw_graphql_query(url=url, query=query, headers=headers) 59 | response_json = response.json() 60 | if response.status_code == 200 and response_json["data"] and "login" in response_json["data"]: 61 | login_data = response_json["data"]["login"] 62 | if "otpToken" in login_data: 63 | self.otp_needed = True 64 | self._otp_token = login_data["otpToken"] 65 | else: 66 | self._access_token = login_data["accessToken"] 67 | self._refresh_token = login_data["refreshToken"] 68 | self._user_session_token = login_data["userSessionToken"] 69 | else: 70 | message = f"Status: {response.status_code}: Details: {response_json}" 71 | print(f"Login failed: {message}") 72 | raise Exception(message) 73 | return response 74 | 75 | def login_with_otp(self, username, otpCode, otpToken=None): 76 | if self._csrf_token == "": 77 | self.create_csrf_token() 78 | url = RIVIAN_GATEWAY_PATH 79 | headers = HEADERS 80 | headers.update( 81 | { 82 | "Csrf-Token": self._csrf_token, 83 | "A-Sess": self._app_session_token, 84 | "Apollographql-Client-Name": "com.rivian.ios.consumer-apollo-ios", 85 | } 86 | ) 87 | 88 | query = { 89 | "operationName": "LoginWithOTP", 90 | "query": "mutation LoginWithOTP($email: String!, $otpCode: String!, $otpToken: String!) {\n loginWithOTP(email: $email, otpCode: $otpCode, otpToken: $otpToken) {\n __typename\n ... on MobileLoginResponse {\n __typename\n accessToken\n refreshToken\n userSessionToken\n }\n }\n}", 91 | "variables": { 92 | "email": username, 93 | "otpCode": otpCode, 94 | "otpToken": otpToken or self._otp_token, 95 | }, 96 | } 97 | 98 | response = self.raw_graphql_query(url=url, query=query, headers=headers) 99 | response_json = response.json() 100 | if response.status_code == 200 and response_json["data"] and "loginWithOTP" in response_json["data"]: 101 | login_data = response_json["data"]["loginWithOTP"] 102 | self._access_token = login_data["accessToken"] 103 | self._refresh_token = login_data["refreshToken"] 104 | self._user_session_token = login_data["userSessionToken"] 105 | else: 106 | message = f"Status: {response.status_code}: Details: {response_json}" 107 | print(f"Login with otp failed: {message}") 108 | raise Exception(message) 109 | return response 110 | 111 | def create_csrf_token(self): 112 | url = RIVIAN_GATEWAY_PATH 113 | headers = HEADERS 114 | 115 | query = { 116 | "operationName": "CreateCSRFToken", 117 | "query": "mutation CreateCSRFToken {createCsrfToken {__typename csrfToken appSessionToken}}", 118 | "variables": None, 119 | } 120 | 121 | response = self.raw_graphql_query(url=url, query=query, headers=headers) 122 | response_json = response.json() 123 | csrf_data = response_json["data"]["createCsrfToken"] 124 | self._csrf_token = csrf_data["csrfToken"] 125 | self._app_session_token = csrf_data["appSessionToken"] 126 | return response 127 | 128 | def raw_graphql_query(self, url, query, headers): 129 | response = requests.post(url, json=query, headers=headers) 130 | if response.status_code != 200: 131 | log.warning(f"Graphql error: Response status: {response.status_code} Reason: {response.reason}") 132 | return response 133 | 134 | def gateway_headers(self): 135 | headers = HEADERS 136 | headers.update( 137 | { 138 | "Csrf-Token": self._csrf_token, 139 | "A-Sess": self._app_session_token, 140 | "U-Sess": self._user_session_token, 141 | "Dc-Cid": f"m-ios-{uuid.uuid4()}", 142 | } 143 | ) 144 | return headers 145 | 146 | def transaction_headers(self): 147 | headers = self.gateway_headers() 148 | headers.update( 149 | { 150 | "dc-cid": f"t2d--{uuid.uuid4()}--{uuid.uuid4()}", 151 | "csrf-token": self._csrf_token, 152 | "app-id": "t2d" 153 | } 154 | ) 155 | return headers 156 | 157 | def vehicle_orders(self): 158 | headers = self.gateway_headers() 159 | query = { 160 | "operationName": "vehicleOrders", 161 | "query": "query vehicleOrders { orders(input: {orderTypes: [PRE_ORDER, VEHICLE], pageInfo: {from: 0, size: 10000}}) { __typename data { __typename id orderDate state configurationStatus fulfillmentSummaryStatus items { __typename sku } consumerStatuses { __typename isConsumerFlowComplete } } } }", 162 | "variables": {}, 163 | } 164 | response = self.raw_graphql_query(url=RIVIAN_GATEWAY_PATH, query=query, headers=headers) 165 | return response.json() 166 | 167 | def delivery(self, order_id): 168 | headers = self.gateway_headers() 169 | query = { 170 | "operationName": "delivery", 171 | "query": "query delivery($orderId: ID!) { delivery(orderId: $orderId) { __typename status carrier deliveryAddress { __typename addressLine1 addressLine2 city state country zipcode } appointmentDetails { __typename appointmentId startDateTime endDateTime timeZone } vehicleVIN } }", 172 | "variables": { 173 | "orderId": order_id, 174 | }, 175 | } 176 | response = self.raw_graphql_query(url=RIVIAN_GATEWAY_PATH, query=query, headers=headers) 177 | return response.json() 178 | 179 | def transaction_status(self, order_id): 180 | headers = self.transaction_headers() 181 | query = { 182 | "operationName": "transactionStatus", 183 | "query": "query transactionStatus($orderId: ID!) { transactionStatus(orderId: $orderId) { titleAndReg { sourceStatus { status details } consumerStatus { displayOrder current complete locked inProgress notStarted error } } tradeIn { sourceStatus { status details } consumerStatus { displayOrder current complete locked inProgress notStarted error } } finance { sourceStatus { status details } consumerStatus { displayOrder current complete locked inProgress notStarted error } } delivery { sourceStatus { status details } consumerStatus { displayOrder current complete locked inProgress notStarted error } } insurance { sourceStatus { status details } consumerStatus { displayOrder current complete locked inProgress notStarted error } } documentUpload { sourceStatus { status details } consumerStatus { displayOrder current complete locked inProgress notStarted error } } contracts { sourceStatus { status details } consumerStatus { displayOrder current complete locked inProgress notStarted error } } payment { sourceStatus { status details } consumerStatus { displayOrder current complete locked inProgress notStarted error } } } }", 184 | "variables": { 185 | "orderId": order_id 186 | }, 187 | } 188 | response = self.raw_graphql_query(url=RIVIAN_TRANSACTIONS_PATH, query=query, headers=headers) 189 | return response.json() 190 | 191 | def finance_summary(self, order_id): 192 | headers = self.transaction_headers() 193 | query = { 194 | "operationName": "financeSummary", 195 | "query": "query financeSummary($orderId: ID!) { ...FinanceSummaryFragment } fragment FinanceSummaryFragment on Query { financeSummary(orderId: $orderId) { orderId status financeChoice { financeChoice institutionName paymentMethod trackingNumber preApprovedAmount loanOfficerName loanOfficerContact downPayment rate term rateAndTermSkipped } } }", 196 | "variables": {"orderId": order_id}, 197 | } 198 | response = self.raw_graphql_query(url=RIVIAN_TRANSACTIONS_PATH, query=query, headers=headers) 199 | return response.json() 200 | 201 | def order(self, order_id): 202 | headers = self.transaction_headers() 203 | query = { 204 | "operationName": "order", 205 | "query": "query order($id: String!) { order(id: $id) { vin state billingAddress { firstName lastName line1 line2 city state country postalCode } shippingAddress { firstName lastName line1 line2 city state country postalCode } orderCancelDate orderEmail currency locale storeId type subtotal discountTotal taxTotal feesTotal paidTotal remainingTotal outstandingBalance costAfterCredits total payments { id intent date method amount referenceNumber status card { last4 expiryDate brand } bank { bankName country last4 } transactionNotes } tradeIns { tradeInReferenceId amount } vehicle { vehicleId vin modelYear model make } items { id discounts { total items { amount title code } } subtotal quantity title productId type unitPrice fees { items { description amount code type } total } taxes { items { description amount code rate type } total } sku shippingAddress { firstName lastName line1 line2 city state country postalCode } configuration { ruleset { meta { rulesetId storeId country vehicle version effectiveDate currency locale availableLocales } defaults { basePrice initialSelection } groups options specs rules } basePrice version options { optionId optionName optionDetails { name attrs price visualExterior visualInterior hidden disabled required } groupId groupName groupDetails { name attrs multiselect required options } price } } } }}", 206 | "variables": {"id": order_id}, 207 | } 208 | response = self.raw_graphql_query(url=RIVIAN_ORDERS_PATH, query=query, headers=headers) 209 | return response.json() 210 | 211 | def retail_orders(self): 212 | headers = self.transaction_headers() 213 | query = { 214 | "operationName": "searchOrders", 215 | "query": "query searchOrders($input: UserOrderSearchInput!) { searchOrders(input: $input) { total data { id type orderDate state fulfillmentSummaryStatus items { id title type sku __typename } __typename } __typename }}", 216 | "variables": { 217 | "input": { 218 | "orderTypes": ["RETAIL"], 219 | "orderStates": None, 220 | "pageInfo": { 221 | "from": 0, 222 | "size": 5 223 | }, 224 | "dateRange": None, 225 | "sortFields": { 226 | "orderDate": "DESC" 227 | } 228 | } 229 | }, 230 | } 231 | response = self.raw_graphql_query(url=RIVIAN_ORDERS_PATH, query=query, headers=headers) 232 | return response.json() 233 | 234 | def get_order(self, order_id): 235 | headers = self.transaction_headers() 236 | query = { 237 | "operationName": "getOrder", 238 | "query": "query getOrder($orderId: String!) { order(id: $orderId) { id storeId userId orderDate orderCancelDate type state currency locale subtotal discountTotal taxTotal total shippingAddress { firstName lastName line1 line2 city state country postalCode __typename } payments { method currency status type card { last4 expiryDate brand __typename } __typename } items { id title type sku unitPrice quantity state productDetails { ... on ChildProduct { dimensionValues { name valueName localizedStrings __typename } __typename } __typename } __typename } fulfillmentSummaryStatus fulfillmentInfo { fulfillments { fulfillmentId fulfillmentStatus fulfillmentMethod fulfillmentVendor tracking { status carrier number url shipDate deliveredDate serviceType __typename } estimatedDeliveryWindow { startDate endDate __typename } items { orderItemId quantityFulfilled isPartialFulfillment __typename } __typename } pendingFulfillmentItems { orderItemId quantity __typename } __typename } __typename }}", 239 | "variables": { 240 | "orderId": order_id 241 | }, 242 | } 243 | response = self.raw_graphql_query(url=RIVIAN_ORDERS_PATH, query=query, headers=headers) 244 | return response.json() 245 | 246 | def payment_methods(self): 247 | headers = self.transaction_headers() 248 | query = { 249 | "operationName": "paymentMethods", 250 | "query": "query paymentMethods { paymentMethods { id type default card { lastFour brand expiration postalCode } } }", 251 | "variables": {}, 252 | } 253 | response = self.raw_graphql_query(url=RIVIAN_ORDERS_PATH, query=query, headers=headers) 254 | return response.json() 255 | 256 | def get_user_information(self): 257 | headers = self.gateway_headers() 258 | query = { 259 | "operationName": "getUserInfo", 260 | "query": "query getUserInfo { currentUser { __typename id firstName lastName email address { __typename country } vehicles { __typename id name owner roles vin vas { __typename vasVehicleId vehiclePublicKey } state createdAt updatedAt vehicle { __typename id vin modelYear make model expectedBuildDate plannedBuildDate expectedGeneralAssemblyStartDate actualGeneralAssemblyDate mobileConfiguration { __typename trimOption { __typename optionId optionName } exteriorColorOption { __typename optionId optionName } interiorColorOption { __typename optionId optionName } } vehicleState { __typename supportedFeatures { __typename name status } } otaEarlyAccessStatus } settings { __typename name { __typename value } } } enrolledPhones { __typename vas { __typename vasPhoneId publicKey } enrolled { __typename deviceType deviceName vehicleId identityId shortName } } pendingInvites { __typename id invitedByFirstName role status vehicleId vehicleModel email } } }", 261 | # "query": "query getUserInfo {currentUser {__typename id firstName lastName email address { __typename country } vehicles {id name owner roles vin vas {__typename vasVehicleId vehiclePublicKey } state createdAt updatedAt vehicle { __typename id vin modelYear make model expectedBuildDate plannedBuildDate expectedGeneralAssemblyStartDate actualGeneralAssemblyDate } } }}", 262 | "variables": None, 263 | } 264 | response = self.raw_graphql_query(url=RIVIAN_GATEWAY_PATH, query=query, headers=headers) 265 | return response.json() 266 | 267 | def get_vehicle_state(self, vehicle_id, minimal=False): 268 | headers = self.gateway_headers() 269 | if minimal: 270 | query = "query GetVehicleState($vehicleID: String!) { vehicleState(id: $vehicleID) { " \ 271 | "cloudConnection { lastSync } " \ 272 | "powerState { value } " \ 273 | "driveMode { value } " \ 274 | "gearStatus { value } " \ 275 | "vehicleMileage { value } " \ 276 | "batteryLevel { value } " \ 277 | "distanceToEmpty { value } " \ 278 | "gnssLocation { latitude longitude } " \ 279 | "gnssSpeed { value } " \ 280 | "chargerStatus { value } " \ 281 | "chargerState { value } " \ 282 | "batteryLimit { value } " \ 283 | "timeToEndOfCharge { value } " \ 284 | "} }" 285 | else: 286 | query = "query GetVehicleState($vehicleID: String!) { " \ 287 | "vehicleState(id: $vehicleID) { __typename " \ 288 | "cloudConnection { __typename lastSync } " \ 289 | "gnssLocation { __typename latitude longitude timeStamp } " \ 290 | "gnssSpeed { __typename timeStamp value } " \ 291 | "gnssBearing { __typename timeStamp value } " \ 292 | "gnssAltitude { __typename timeStamp value } " \ 293 | "gnssError { __typename timeStamp positionVertical positionHorizontal speed bearing } " \ 294 | "alarmSoundStatus { __typename timeStamp value } " \ 295 | "timeToEndOfCharge { __typename timeStamp value } " \ 296 | "doorFrontLeftLocked { __typename timeStamp value } " \ 297 | "doorFrontLeftClosed { __typename timeStamp value } " \ 298 | "doorFrontRightLocked { __typename timeStamp value } " \ 299 | "doorFrontRightClosed { __typename timeStamp value } " \ 300 | "doorRearLeftLocked { __typename timeStamp value } " \ 301 | "doorRearLeftClosed { __typename timeStamp value } " \ 302 | "doorRearRightLocked { __typename timeStamp value } " \ 303 | "doorRearRightClosed { __typename timeStamp value } " \ 304 | "windowFrontLeftClosed { __typename timeStamp value } " \ 305 | "windowFrontRightClosed { __typename timeStamp value } " \ 306 | "windowRearLeftClosed { __typename timeStamp value } " \ 307 | "windowRearRightClosed { __typename timeStamp value } " \ 308 | "windowFrontLeftCalibrated { __typename timeStamp value } " \ 309 | "windowFrontRightCalibrated { __typename timeStamp value } " \ 310 | "windowRearLeftCalibrated { __typename timeStamp value } " \ 311 | "windowRearRightCalibrated { __typename timeStamp value } " \ 312 | "windowsNextAction { __typename timeStamp value } " \ 313 | "closureFrunkLocked { __typename timeStamp value } " \ 314 | "closureFrunkClosed { __typename timeStamp value } " \ 315 | "closureFrunkNextAction { __typename timeStamp value } " \ 316 | "gearGuardLocked { __typename timeStamp value } " \ 317 | "closureLiftgateLocked { __typename timeStamp value } " \ 318 | "closureLiftgateClosed { __typename timeStamp value } " \ 319 | "closureLiftgateNextAction { __typename timeStamp value } " \ 320 | "windowRearLeftClosed { __typename timeStamp value } " \ 321 | "windowRearRightClosed { __typename timeStamp value } " \ 322 | "closureSideBinLeftLocked { __typename timeStamp value } " \ 323 | "closureSideBinLeftClosed { __typename timeStamp value } " \ 324 | "closureSideBinRightLocked { __typename timeStamp value } " \ 325 | "closureSideBinRightClosed { __typename timeStamp value } " \ 326 | "closureTailgateLocked { __typename timeStamp value } " \ 327 | "closureTailgateClosed { __typename timeStamp value } " \ 328 | "closureTonneauLocked { __typename timeStamp value } " \ 329 | "closureTonneauClosed { __typename timeStamp value } " \ 330 | "wiperFluidState { __typename timeStamp value } " \ 331 | "powerState { __typename timeStamp value } " \ 332 | "batteryHvThermalEventPropagation { __typename timeStamp value } " \ 333 | "vehicleMileage { __typename timeStamp value } " \ 334 | "brakeFluidLow { __typename timeStamp value } " \ 335 | "gearStatus { __typename timeStamp value } " \ 336 | "tirePressureStatusFrontLeft { __typename timeStamp value } " \ 337 | "tirePressureStatusValidFrontLeft { __typename timeStamp value } " \ 338 | "tirePressureStatusFrontRight { __typename timeStamp value } " \ 339 | "tirePressureStatusValidFrontRight { __typename timeStamp value } " \ 340 | "tirePressureStatusRearLeft { __typename timeStamp value } " \ 341 | "tirePressureStatusValidRearLeft { __typename timeStamp value } " \ 342 | "tirePressureStatusRearRight { __typename timeStamp value } " \ 343 | "tirePressureStatusValidRearRight { __typename timeStamp value } " \ 344 | "batteryLevel { __typename timeStamp value } " \ 345 | "chargerState { __typename timeStamp value } " \ 346 | "batteryLimit { __typename timeStamp value } " \ 347 | "batteryCapacity { __typename timeStamp value } " \ 348 | "remoteChargingAvailable { __typename timeStamp value } " \ 349 | "batteryHvThermalEvent { __typename timeStamp value } " \ 350 | "rangeThreshold { __typename timeStamp value } " \ 351 | "distanceToEmpty { __typename timeStamp value } " \ 352 | "otaAvailableVersion { __typename timeStamp value } " \ 353 | "otaAvailableVersionWeek { __typename timeStamp value } " \ 354 | "otaAvailableVersionYear { __typename timeStamp value } " \ 355 | "otaCurrentVersion { __typename timeStamp value } " \ 356 | "otaCurrentVersionNumber { __typename timeStamp value } " \ 357 | "otaCurrentVersionWeek { __typename timeStamp value } " \ 358 | "otaCurrentVersionYear { __typename timeStamp value } " \ 359 | "otaDownloadProgress { __typename timeStamp value } " \ 360 | "otaInstallDuration { __typename timeStamp value } " \ 361 | "otaInstallProgress { __typename timeStamp value } " \ 362 | "otaInstallReady { __typename timeStamp value } " \ 363 | "otaInstallTime { __typename timeStamp value } " \ 364 | "otaInstallType { __typename timeStamp value } " \ 365 | "otaStatus { __typename timeStamp value } " \ 366 | "otaCurrentStatus { __typename timeStamp value } " \ 367 | "cabinClimateInteriorTemperature { __typename timeStamp value } " \ 368 | "cabinPreconditioningStatus { __typename timeStamp value } " \ 369 | "cabinPreconditioningType { __typename timeStamp value } " \ 370 | "petModeStatus { __typename timeStamp value } " \ 371 | "petModeTemperatureStatus { __typename timeStamp value } " \ 372 | "cabinClimateDriverTemperature { __typename timeStamp value } " \ 373 | "gearGuardVideoStatus { __typename timeStamp value } " \ 374 | "gearGuardVideoMode { __typename timeStamp value } " \ 375 | "gearGuardVideoTermsAccepted { __typename timeStamp value } " \ 376 | "defrostDefogStatus { __typename timeStamp value } " \ 377 | "steeringWheelHeat { __typename timeStamp value } " \ 378 | "seatFrontLeftHeat { __typename timeStamp value } " \ 379 | "seatFrontRightHeat { __typename timeStamp value } " \ 380 | "seatRearLeftHeat { __typename timeStamp value } " \ 381 | "seatRearRightHeat { __typename timeStamp value } " \ 382 | "chargerStatus { __typename timeStamp value } " \ 383 | "seatFrontLeftVent { __typename timeStamp value } " \ 384 | "seatFrontRightVent { __typename timeStamp value } " \ 385 | "chargerDerateStatus { __typename timeStamp value } " \ 386 | "driveMode { __typename timeStamp value } " \ 387 | "limitedAccelCold { __typename timeStamp value } " \ 388 | "limitedRegenCold { __typename timeStamp value } " \ 389 | "twelveVoltBatteryHealth { __typename timeStamp value } " \ 390 | "serviceMode { __typename timeStamp value } " \ 391 | "trailerStatus { __typename timeStamp value } " \ 392 | "btmFfHardwareFailureStatus { __typename timeStamp value } " \ 393 | "btmIcHardwareFailureStatus { __typename timeStamp value } " \ 394 | "btmLfdHardwareFailureStatus { __typename timeStamp value } " \ 395 | "btmRfHardwareFailureStatus { __typename timeStamp value } " \ 396 | "btmRfdHardwareFailureStatus { __typename timeStamp value } " \ 397 | "carWashMode { __typename timeStamp value } " \ 398 | "chargePortState { __typename timeStamp value } " \ 399 | "chargingTimeEstimationValidity { __typename timeStamp value } " \ 400 | "rearHitchStatus { __typename timeStamp value } " \ 401 | "} }" 402 | 403 | query = { 404 | "operationName": "GetVehicleState", 405 | "query": query, 406 | "variables": { 407 | 'vehicleID': vehicle_id, 408 | }, 409 | } 410 | response = self.raw_graphql_query(url=RIVIAN_GATEWAY_PATH, query=query, headers=headers) 411 | return response.json() 412 | 413 | def get_vehicle_last_connection(self, vehicle_id): 414 | headers = self.gateway_headers() 415 | query = { 416 | "operationName": "GetVehicleLastConnection", 417 | "query": "query GetVehicleLastConnection($vehicleID: String!) { vehicleState(id: $vehicleID) { __typename cloudConnection { __typename lastSync } } }", 418 | "variables": { 419 | 'vehicleID': vehicle_id, 420 | }, 421 | } 422 | response = self.raw_graphql_query(url=RIVIAN_GATEWAY_PATH, query=query, headers=headers) 423 | return response.json() 424 | 425 | def plan_trip(self, vehicle_id, starting_soc, starting_range_meters, origin_lat, origin_long, dest_lat, dest_long): 426 | headers = self.gateway_headers() 427 | query = { 428 | "operationName": "planTrip", 429 | "query": "query planTrip($origin: CoordinatesInput!, $destination: CoordinatesInput!, $bearing: Float!, $vehicleId: String!, $startingSoc: Float!, $startingRangeMeters: Float!) { planTrip(bearing: $bearing, vehicleId: $vehicleId, startingSoc: $startingSoc, origin: $origin, destination: $destination, startingRangeMeters: $startingRangeMeters) { routes { routeResponse destinationReached totalChargingDuration arrivalSOC arrivalReachableDistance waypoints { waypointType entityId name latitude longitude maxPower chargeDuration arrivalSOC arrivalReachableDistance departureSOC departureReachableDistance } energyConsumptionOnLeg batteryEmptyToDestinationDistance batteryEmptyLocationLatitude batteryEmptyLocationLongitude } tripPlanStatus chargeStationsAvailable socBelowLimit } }", 430 | "variables": { 431 | 'origin': { 432 | 'latitude': origin_lat, 433 | 'longitude': origin_long, 434 | }, 435 | 'destination': { 436 | 'latitude': dest_lat, 437 | 'longitude': dest_long, 438 | }, 439 | 'bearing': 0, 440 | 'vehicleId': vehicle_id, 441 | 'startingRangeMeters': starting_range_meters, 442 | 'startingSoc': starting_soc, 443 | }, 444 | } 445 | response = self.raw_graphql_query(url=RIVIAN_GATEWAY_PATH, query=query, headers=headers) 446 | return response.json() 447 | 448 | def get_ota_details(self, vehicle_id): 449 | headers = self.gateway_headers() 450 | query = { 451 | "operationName": "GetVehicle", 452 | "query": "query GetVehicle($vehicleId: String!) { getVehicle(id: $vehicleId) { availableOTAUpdateDetails { url version locale } currentOTAUpdateDetails { url version locale } } }", 453 | "variables": { 454 | 'vehicleId': vehicle_id, 455 | }, 456 | } 457 | response = self.raw_graphql_query(url=RIVIAN_GATEWAY_PATH, query=query, headers=headers) 458 | return response.json() 459 | 460 | def check_by_rivian_id(self): 461 | headers = self.transaction_headers() 462 | query = { 463 | "operationName": "CheckByRivianId", 464 | "query": "query CheckByRivianId { chargepoint { checkByRivianId } }", 465 | "variables": {}, 466 | } 467 | response = self.raw_graphql_query(url=RIVIAN_CHARGING_PATH, query=query, headers=headers) 468 | return response.json() 469 | 470 | def get_linked_email_for_rivian_id(self): 471 | headers = self.transaction_headers() 472 | query = { 473 | "operationName": "getLinkedEmailForRivianId", 474 | "query": "query getLinkedEmailForRivianId { chargepoint { getLinkedEmailForRivianId { email } } }", 475 | "variables": {}, 476 | } 477 | response = self.raw_graphql_query(url=RIVIAN_CHARGING_PATH, query=query, headers=headers) 478 | return response.json() 479 | 480 | def get_parameter_store_values(self): 481 | headers = self.transaction_headers() 482 | query = { 483 | "operationName": "getParameterStoreValues", 484 | "query": "query getParameterStoreValues($keys: [String!]!) { getParameterStoreValues(keys: $keys) { key value } }", 485 | "variables": { 486 | "keys": ["FF_ACCOUNT_ESTIMATED_DELIVERY_WINDOW_STATIC_MSG"] 487 | }, 488 | } 489 | response = self.raw_graphql_query(url=RIVIAN_ORDERS_PATH, query=query, headers=headers) 490 | return response.json() 491 | 492 | def get_vehicle(self, vehicle_id): 493 | headers = self.gateway_headers() 494 | query = { 495 | "operationName": "GetVehicle", 496 | "query": "query GetVehicle($getVehicleId: String) { getVehicle(id: $getVehicleId) { invitedUsers { __typename ... on ProvisionedUser { devices { type mappedIdentityId id hrid deviceName isPaired isEnabled } firstName lastName email roles userId } ... on UnprovisionedUser { email inviteId status } } } }", 497 | "variables": { 498 | "getVehicleId": vehicle_id 499 | }, 500 | } 501 | response = self.raw_graphql_query(url=RIVIAN_GATEWAY_PATH, query=query, headers=headers) 502 | return response.json() 503 | 504 | def get_registered_wallboxes(self): 505 | headers = self.gateway_headers() 506 | query = { 507 | "operationName": "getRegisteredWallboxes", 508 | "variables": {}, 509 | "query": "query getRegisteredWallboxes { getRegisteredWallboxes { __typename wallboxId userId wifiId name linked latitude longitude chargingStatus power currentVoltage currentAmps softwareVersion model serialNumber maxPower maxVoltage maxAmps } }" 510 | } 511 | response = self.raw_graphql_query(url=RIVIAN_CHARGING_PATH, query=query, headers=headers) 512 | return response.json() 513 | 514 | def get_provisioned_camp_speakers(self): 515 | headers = self.gateway_headers() 516 | query = { 517 | "operationName": "GetProvisionedCampSpeakers", 518 | "query": "query GetProvisionedCampSpeakers { currentUser { __typename vehicles { __typename id connectedProducts { __typename ... on CampSpeaker { serialNumber id } } } } }", 519 | "variables": {}, 520 | } 521 | response = self.raw_graphql_query(url=RIVIAN_GATEWAY_PATH, query=query, headers=headers) 522 | return response.json() 523 | 524 | def get_vehicle_images(self): 525 | headers = self.gateway_headers() 526 | query = { 527 | "operationName": "getVehicleImages", 528 | "query": "query getVehicleImages($extension: String!, $resolution: String!) { getVehicleOrderMobileImages(resolution: $resolution, extension: $extension) { __typename orderId url resolution size design placement } getVehicleMobileImages(resolution: $resolution, extension: $extension) { __typename vehicleId url resolution size design placement } }", 529 | "variables": { 530 | "extension": "webp", 531 | "resolution": "hdpi" 532 | }, 533 | } 534 | response = self.raw_graphql_query(url=RIVIAN_GATEWAY_PATH, query=query, headers=headers) 535 | return response.json() 536 | 537 | def user(self): 538 | headers = self.gateway_headers() 539 | query = { 540 | "operationName": "user", 541 | # "query": "query user { user { email { email } phone { formatted } firstName lastName addresses { id type line1 line2 city state country postalCode } newsletterSubscription smsSubscription registrationChannels2FA userId vehicles {id highestPriorityRole __typename } orderSnapshots(filterTypes: [PRE_ORDER, VEHICLE]) { ...OrderSnapshotsFragment __typename } __typename }} fragment OrderSnapshotsFragment on OrderSnapshot { id total paidTotal subtotal state configurationStatus currency orderDate type fulfillmentSummaryStatus items { id total unitPrice quantity productDetails { ... on VehicleProduct { sku store { country __typename } __typename } ... on StandaloneProduct { sku store { country __typename } __typename } ... on ChildProduct { sku store { country __typename } __typename } __typename } configuration { basePrice ruleset { meta { locale currency country vehicle version rulesetId effectiveDate __typename } groups rules specs options defaults { basePrice initialSelection __typename } __typename } options { optionId groupId price optionDetails { name attrs price visualExterior visualInterior __typename } __typename } __typename } __typename } __typename } } } ", 542 | "query": "query user { user { email { email } phone { formatted } firstName lastName addresses { id type line1 line2 city state country postalCode } newsletterSubscription smsSubscription registrationChannels2FA userId vehicles {id highestPriorityRole __typename } invites (filterStates: [PENDING]) {id inviteState vehicleModel vehicleId creatorFirstName} orderSnapshots(filterTypes: [PRE_ORDER, VEHICLE, RETAIL]) { ...OrderSnapshotsFragment } }} fragment OrderSnapshotsFragment on OrderSnapshot { id total paidTotal subtotal state configurationStatus currency orderDate type fulfillmentSummaryStatus }", 543 | "variables": {}, 544 | } 545 | response = self.raw_graphql_query(url=RIVIAN_ORDERS_PATH, query=query, headers=headers) 546 | return response.json() 547 | 548 | def get_charging_schedule(self, vehicle_id): 549 | headers = self.gateway_headers() 550 | query = { 551 | "operationName": "GetChargingSchedule", 552 | "query": "query GetChargingSchedule($vehicleId: String!) { getVehicle(id: $vehicleId) { chargingSchedules { startTime duration location { latitude longitude } amperage enabled weekDays } } }", 553 | "variables": { 554 | "vehicleId": vehicle_id 555 | }, 556 | } 557 | response = self.raw_graphql_query(url=RIVIAN_GATEWAY_PATH, query=query, headers=headers) 558 | return response.json() 559 | 560 | 561 | 562 | def get_completed_session_summaries(self): 563 | headers = self.gateway_headers() 564 | query = { 565 | "operationName": "getCompletedSessionSummaries", 566 | "query": "query getCompletedSessionSummaries { getCompletedSessionSummaries { chargerType currencyCode paidTotal startInstant endInstant totalEnergyKwh rangeAddedKm city transactionId vehicleId vehicleName vendor isRoamingNetwork isPublic isHomeCharger meta { transactionIdGroupingKey dataSources } }}", 567 | "variables": {}, 568 | } 569 | response = self.raw_graphql_query(url=RIVIAN_CHARGING_PATH, query=query, headers=headers) 570 | return response.json() 571 | 572 | 573 | def get_charging_session_status(self, job_id, user_id): 574 | headers = self.gateway_headers() 575 | query = { 576 | "operationName": "GetChargingSessionStatus", 577 | "query": "query GetChargingSessionStatus($jobId: ID!, $userId: ID!) { getSessionStatus(jobId: $jobId, userId: $userId) { status errorMessage errorId sessionId } }", 578 | "variables": { 579 | "jobId": "123", 580 | "userId": "123" 581 | }, 582 | } 583 | response = self.raw_graphql_query(url=RIVIAN_CHARGING_PATH, query=query, headers=headers) 584 | return response.json() 585 | 586 | 587 | def get_non_rivian_user_session(self): 588 | headers = self.gateway_headers() 589 | query = { 590 | "operationName": "getNonRivianUserSession", 591 | "query": "query getNonRivianUserSession { getNonRivianUserSession { chargerId transactionId isRivianCharger vehicleChargerState { value updatedAt } } }", 592 | "variables": {}, 593 | } 594 | response = self.raw_graphql_query(url=RIVIAN_CHARGING_PATH, query=query, headers=headers) 595 | return response.json() 596 | 597 | def get_live_session_data(self, vehicle_id): 598 | headers = self.gateway_headers() 599 | query = { 600 | "operationName": "getLiveSessionData", 601 | "query": "query getLiveSessionData($vehicleId: ID) " 602 | "{ getLiveSessionData(vehicleId: $vehicleId) " 603 | "{ isRivianCharger isFreeSession vehicleChargerState { value updatedAt } " 604 | "chargerId startTime timeElapsed timeRemaining { value updatedAt } kilometersChargedPerHour " 605 | "{ value updatedAt } power { value updatedAt } rangeAddedThisSession { value updatedAt } " 606 | "totalChargedEnergy { value updatedAt } timeRemaining { value updatedAt } vehicleChargerState " 607 | "{ value updatedAt } kilometersChargedPerHour { value updatedAt } " 608 | "currentPrice soc { value } currentMiles { value } current { value } } }", 609 | "variables": { 610 | "vehicleId": vehicle_id 611 | }, 612 | } 613 | response = self.raw_graphql_query(url=RIVIAN_CHARGING_PATH, query=query, headers=headers) 614 | return response.json() 615 | 616 | 617 | def get_live_session_history(self, vehicle_id): 618 | headers = self.gateway_headers() 619 | query = { 620 | "operationName": "getLiveSessionHistory", 621 | "query": "query getLiveSessionHistory($vehicleId: ID) { getLiveSessionHistory(vehicleId: $vehicleId) { chartData { kw time } } }", 622 | "variables": { 623 | "vehicleId": vehicle_id 624 | }, 625 | } 626 | response = self.raw_graphql_query(url=RIVIAN_CHARGING_PATH, query=query, headers=headers) 627 | return response.json() 628 | 629 | 630 | # Vehicle commands require an HMAC signature to be sent with the request. 631 | # The HMAC is generated using the command name and the current timestamp, 632 | # using a shared key generated from the phone’s private key and the vehicle’s 633 | # public key. The vehicle’s public key is available in the vehiclePublicKey 634 | # field of the getUserInfo endpoint. 635 | def send_vehicle_command(self, vehicle_id, command, vasPhoneId, deviceId, vehiclePublicKey): 636 | headers = self.gateway_headers() 637 | 638 | query = { 639 | "operationName": "sendVehicleCommand", 640 | "query": "mutation sendVehicleCommand($attrs: VehicleCommandAttributes!) { sendVehicleCommand(attrs: $attrs) { __typename id command state } }", 641 | "variables": { 642 | "attrs": { 643 | "command": command, 644 | "hmac": 0, #your-hmac 645 | "timestamp": time.time(), 646 | "vasPhoneId": vasPhoneId, 647 | "deviceId": deviceId, 648 | "vehicleId": vehicle_id, 649 | } 650 | }, 651 | } 652 | response = self.raw_graphql_query(url=RIVIAN_GATEWAY_PATH, query=query, headers=headers) 653 | return response.json() 654 | -------------------------------------------------------------------------------- /src/rivian_python_api/rivian_cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | import sys 4 | import argparse 5 | from rivian_api import * 6 | from rivian_map import * 7 | import pickle 8 | from dateutil.parser import parse 9 | from dateutil import tz 10 | import time 11 | from datetime import datetime, timedelta 12 | 13 | from dotenv import load_dotenv 14 | load_dotenv() 15 | 16 | PICKLE_FILE = 'rivian_auth.pickle' 17 | 18 | 19 | def save_state(rivian): 20 | state = { 21 | "_access_token": rivian._access_token, 22 | "_refresh_token": rivian._refresh_token, 23 | "_user_session_token": rivian._user_session_token, 24 | } 25 | with open(PICKLE_FILE, 'wb') as f: 26 | pickle.dump(state, f) 27 | 28 | 29 | def restore_state(rivian): 30 | while True: 31 | try: 32 | rivian.create_csrf_token() 33 | break 34 | except Exception as e: 35 | time.sleep(5) 36 | 37 | RIVIAN_AUTHORIZATION = os.getenv('RIVIAN_AUTHORIZATION') 38 | if RIVIAN_AUTHORIZATION: 39 | rivian._access_token, rivian._refresh_token, rivian._user_session_token = RIVIAN_AUTHORIZATION.split(';') 40 | elif os.path.exists(PICKLE_FILE): 41 | with open(PICKLE_FILE, 'rb') as f: 42 | obj = pickle.load(f) 43 | rivian._access_token = obj['_access_token'] 44 | rivian._refresh_token = obj['_refresh_token'] 45 | rivian._user_session_token = obj['_user_session_token'] 46 | else: 47 | raise Exception("Please log in first") 48 | 49 | 50 | def get_rivian_object(): 51 | rivian = Rivian() 52 | restore_state(rivian) 53 | return rivian 54 | 55 | 56 | def login_with_password(verbose): 57 | rivian = Rivian() 58 | try: 59 | rivian.login(os.getenv('RIVIAN_USERNAME'), os.getenv('RIVIAN_PASSWORD')) 60 | except Exception as e: 61 | if verbose: 62 | print(f"Authentication failed, check RIVIAN_USERNAME and RIVIAN_PASSWORD: {str(e)}") 63 | return None 64 | return rivian 65 | 66 | 67 | def login_with_otp(verbose, otp_token): 68 | otpCode = input('Enter OTP: ') 69 | rivian = Rivian() 70 | try: 71 | rivian.login_with_otp( 72 | username=os.getenv('RIVIAN_USERNAME'), 73 | otpCode=otpCode, 74 | otpToken=otp_token) 75 | except Exception as e: 76 | if verbose: 77 | print(f"Authentication failed, OTP mismatch: {str(e)}") 78 | return None 79 | return rivian 80 | 81 | 82 | def login(verbose): 83 | # Intentionally don't use same Rivian object for login and subsequent calls 84 | rivian = login_with_password(verbose) 85 | if not rivian: 86 | return 87 | if rivian.otp_needed: 88 | rivian = login_with_otp(verbose, otp_token=rivian._otp_token) 89 | if rivian: 90 | print("Login successful") 91 | save_state(rivian) 92 | return 93 | 94 | 95 | def user_information(verbose): 96 | rivian = get_rivian_object() 97 | response_json = rivian.get_user_information() 98 | if verbose: 99 | print(f"user_information:\n{response_json}") 100 | return response_json['data']['currentUser'] 101 | 102 | 103 | def vehicle_orders(verbose): 104 | rivian = get_rivian_object() 105 | try: 106 | response_json = rivian.vehicle_orders() 107 | except: 108 | return [] 109 | if verbose: 110 | print(f"orders:\n{response_json}") 111 | orders = [] 112 | for order in response_json['data']['orders']['data']: 113 | orders.append({ 114 | 'id': order['id'], 115 | 'orderDate': order['orderDate'], 116 | 'state': order['state'], 117 | 'configurationStatus': order['configurationStatus'], 118 | 'fulfillmentSummaryStatus': order['fulfillmentSummaryStatus'], 119 | 'items': [i['sku'] for i in order['items']], 120 | 'isConsumerFlowComplete': order['consumerStatuses']['isConsumerFlowComplete'], 121 | }) 122 | return orders 123 | 124 | 125 | def order_details(order_id, verbose): 126 | rivian = get_rivian_object() 127 | response_json = rivian.order(order_id=order_id) 128 | if verbose: 129 | print(f"order_details:\n{response_json}") 130 | data = {} 131 | if 'data' in response_json and 'order' in response_json['data']: 132 | if 'vehicle' in response_json['data']['order']: 133 | try: 134 | data = { 135 | 'vehicleId': response_json['data']['order']['vehicle']['vehicleId'], 136 | 'vin': response_json['data']['order']['vehicle']['vin'], 137 | 'modelYear': response_json['data']['order']['vehicle']['modelYear'], 138 | 'make': response_json['data']['order']['vehicle']['make'], 139 | 'model': response_json['data']['order']['vehicle']['model'], 140 | } 141 | except: 142 | log.warning(f"Order details missing key items, " 143 | f"found: {response_json['data']['order']['vehicle']}") 144 | pass 145 | if 'items' in response_json['data']['order']: 146 | for i in response_json['data']['order']['items']: 147 | if i['configuration'] is not None: 148 | for c in i['configuration']['options']: 149 | data[c['groupName']] = c['optionName'] 150 | return data 151 | 152 | 153 | def retail_orders(verbose): 154 | rivian = get_rivian_object() 155 | response_json = rivian.retail_orders() 156 | if verbose: 157 | print(f"retail_orders:\n{response_json}") 158 | orders = [] 159 | for order in response_json['data']['searchOrders']['data']: 160 | orders.append({ 161 | 'id': order['id'], 162 | 'orderDate': order['orderDate'], 163 | 'state': order['state'], 164 | 'fulfillmentSummaryStatus': order['fulfillmentSummaryStatus'], 165 | 'items': [item['title'] for item in order['items']] 166 | }) 167 | return orders 168 | 169 | 170 | def get_order(order_id, verbose): 171 | rivian = get_rivian_object() 172 | response_json = rivian.get_order(order_id=order_id) 173 | if verbose: 174 | print(f"get_order:\n{response_json}") 175 | order = {} 176 | order['orderDate'] = response_json['data']['order']['orderDate'] 177 | return order 178 | 179 | 180 | def payment_methods(verbose): 181 | rivian = get_rivian_object() 182 | response_json = rivian.payment_methods() 183 | if verbose: 184 | print(f"payment_methods:\n{response_json}") 185 | pmt = [] 186 | for p in response_json['data']['paymentMethods']: 187 | pmt.append({ 188 | 'type': p['type'], 189 | 'default': p['default'], 190 | 'card': p['card'] if 'card' in p else None, 191 | }) 192 | return pmt 193 | 194 | 195 | def check_by_rivian_id(verbose): 196 | rivian = get_rivian_object() 197 | response_json = rivian.check_by_rivian_id() 198 | if verbose: 199 | print(f"check_by_rivian_id:\n{response_json}") 200 | data = {'Chargepoint checkByRivianId': response_json['data']['chargepoint']['checkByRivianId']} 201 | return data 202 | 203 | 204 | def get_linked_email_for_rivian_id(verbose): 205 | rivian = get_rivian_object() 206 | response_json = rivian.get_linked_email_for_rivian_id() 207 | if verbose: 208 | print(f"get_linked_email_for_rivian_id:\n{response_json}") 209 | data = { 210 | 'Chargepoint linked email': 211 | response_json['data']['chargepoint']['getLinkedEmailForRivianId']['email'] 212 | } 213 | return data 214 | 215 | 216 | def get_parameter_store_values(verbose): 217 | rivian = get_rivian_object() 218 | response_json = rivian.get_parameter_store_values() 219 | if verbose: 220 | print(f"get_parameter_store_values:\n{response_json}") 221 | 222 | 223 | def get_vehicle(vehicle_id, verbose): 224 | rivian = get_rivian_object() 225 | response_json = rivian.get_vehicle(vehicle_id=vehicle_id) 226 | if verbose: 227 | print(f"get_vehicle:\n{response_json}") 228 | data = [] 229 | if 'data' not in response_json: 230 | return data 231 | for u in response_json['data']['getVehicle']['invitedUsers']: 232 | if u['__typename'] != 'ProvisionedUser': 233 | continue 234 | ud = { 235 | 'firstName': u['firstName'], 236 | 'lastName': u['lastName'], 237 | 'email': u['email'], 238 | 'roles': ', '.join(u['roles']), 239 | 'devices': [], 240 | } 241 | for d in u['devices']: 242 | ud['devices'].append({ 243 | "id": d["id"], 244 | "deviceName": d["deviceName"], 245 | "isPaired": d["isPaired"], 246 | "isEnabled": d["isEnabled"], 247 | }) 248 | data.append(ud) 249 | return data 250 | 251 | 252 | def get_vehicle_state(vehicle_id, verbose, minimal=False): 253 | rivian = get_rivian_object() 254 | try: 255 | response_json = rivian.get_vehicle_state(vehicle_id=vehicle_id, minimal=minimal) 256 | except Exception as e: 257 | print(f"Error: {str(e)}") 258 | return None 259 | if verbose: 260 | print(f"get_vehicle_state:\n{response_json}") 261 | if 'data' in response_json and 'vehicleState' in response_json['data']: 262 | return response_json['data']['vehicleState'] 263 | else: 264 | return None 265 | 266 | 267 | def get_vehicle_last_seen(vehicle_id, verbose): 268 | rivian = get_rivian_object() 269 | try: 270 | response_json = rivian.get_vehicle_last_connection(vehicle_id=vehicle_id) 271 | except Exception as e: 272 | print(f"{str(e)}") 273 | return None 274 | if verbose: 275 | print(f"get_vehicle_last_seen:\n{response_json}") 276 | last_seen = parse(response_json['data']['vehicleState']['cloudConnection']['lastSync']) 277 | return last_seen 278 | 279 | 280 | def plan_trip(vehicle_id, starting_soc, starting_range_meters, origin_lat, origin_long, dest_lat, dest_long, verbose): 281 | rivian = get_rivian_object() 282 | try: 283 | response_json = rivian.plan_trip( 284 | vehicle_id=vehicle_id, 285 | starting_soc=float(starting_soc), 286 | starting_range_meters=float(starting_range_meters), 287 | origin_lat=float(origin_lat), 288 | origin_long=float(origin_long), 289 | dest_lat=float(dest_lat), 290 | dest_long=float(dest_long), 291 | ) 292 | except Exception as e: 293 | print(f"{str(e)}") 294 | return None 295 | if verbose: 296 | print(f"plan_trip:\n{response_json}") 297 | return response_json 298 | 299 | 300 | def get_ota_info(vehicle_id, verbose): 301 | rivian = get_rivian_object() 302 | try: 303 | response_json = rivian.get_ota_details(vehicle_id=vehicle_id) 304 | except Exception as e: 305 | print(f"{str(e)}") 306 | return None 307 | if verbose: 308 | print(f"get_ota_info:\n{response_json}") 309 | return response_json['data']['getVehicle'] 310 | 311 | 312 | def transaction_status(order_id, verbose): 313 | rivian = get_rivian_object() 314 | try: 315 | response_json = rivian.transaction_status(order_id) 316 | except Exception as e: 317 | if verbose: 318 | print(f"Error getting transaction status for {order_id}") 319 | return None 320 | if verbose: 321 | print(f"transaction_status:\n{response_json}") 322 | status = {} 323 | if response_json and \ 324 | 'data' in response_json and \ 325 | response_json['data'] and \ 326 | "transactionStatus" in response_json['data']: 327 | transaction_status = response_json['data']['transactionStatus'] 328 | for s in ( 329 | 'titleAndReg', 330 | 'tradeIn', 331 | 'finance', 332 | 'delivery', 333 | 'insurance', 334 | 'documentUpload', 335 | 'contracts', 336 | 'payment', 337 | ): 338 | status[transaction_status[s]['consumerStatus']['displayOrder']] = { 339 | 'item': s, 340 | 'status': transaction_status[s]['sourceStatus']['status'], 341 | 'complete': transaction_status[s]['consumerStatus']['complete'] 342 | } 343 | return status 344 | 345 | 346 | def finance_summary(order_id, verbose): 347 | rivian = get_rivian_object() 348 | response_json = rivian.finance_summary(order_id=order_id) 349 | if verbose: 350 | print(f"finance_summary:\n{response_json}") 351 | 352 | 353 | def chargers(verbose): 354 | rivian = get_rivian_object() 355 | response_json = rivian.get_registered_wallboxes() 356 | if verbose: 357 | print(f"chargers:\n{response_json}") 358 | return response_json['data']['getRegisteredWallboxes'] 359 | 360 | 361 | def delivery(order_id, verbose): 362 | rivian = get_rivian_object() 363 | response_json = rivian.delivery(order_id=order_id) 364 | if verbose: 365 | print(f"delivery:\n{response_json}") 366 | vehicle = {} 367 | if 'delivery' in response_json['data'] and response_json['data']['delivery']: 368 | vehicle['vin'] = response_json['data']['delivery']['vehicleVIN'] 369 | vehicle['carrier'] = response_json['data']['delivery']['carrier'] 370 | vehicle['status'] = response_json['data']['delivery']['status'] 371 | vehicle['appointmentDetails'] = response_json['data']['delivery']['appointmentDetails'] 372 | return vehicle 373 | 374 | 375 | def speakers(verbose): 376 | rivian = get_rivian_object() 377 | response_json = rivian.get_provisioned_camp_speakers() 378 | if verbose: 379 | print(f"speakers:\n{response_json}") 380 | return response_json['data']['currentUser']['vehicles'] 381 | 382 | 383 | def images(verbose): 384 | rivian = get_rivian_object() 385 | response_json = rivian.get_vehicle_images() 386 | if verbose: 387 | print(f"images:\n{response_json}") 388 | images = [] 389 | for i in response_json['data']['getVehicleOrderMobileImages']: 390 | images.append({ 391 | 'size': i['size'], 392 | 'design': i['design'], 393 | 'placement': i['placement'], 394 | 'url': i['url'] 395 | }) 396 | print(images) 397 | return images 398 | 399 | 400 | def get_user(verbose): 401 | rivian = get_rivian_object() 402 | response_json = rivian.user() 403 | if verbose: 404 | print(f"get_user:\n{response_json}") 405 | try: 406 | phone = response_json['data']['user']['phone']['formatted'] 407 | except Exception as e: 408 | phone = None 409 | user = { 410 | 'userId': response_json['data']['user']['userId'], 411 | 'email': response_json['data']['user']['email']['email'], 412 | 'phone': phone, 413 | 'firstName': response_json['data']['user']['firstName'], 414 | 'lastName': response_json['data']['user']['lastName'], 415 | 'newsletterSubscription': response_json['data']['user']['newsletterSubscription'], 416 | 'smsSubscription': response_json['data']['user']['smsSubscription'], 417 | 'registrationChannels2FA': response_json['data']['user']['registrationChannels2FA'], 418 | 'addresses': [], 419 | } 420 | for a in response_json['data']['user']['addresses']: 421 | user['addresses'].append({ 422 | 'type': a['type'], 423 | 'line1': a['line1'], 424 | 'line2': a['line2'], 425 | 'city': a['city'], 426 | 'state': a['state'], 427 | 'country': a['country'], 428 | 'postalCode': a['postalCode'], 429 | }) 430 | return user 431 | 432 | 433 | def charging_schedule(vehicle_id, verbose): 434 | rivian = get_rivian_object() 435 | response_json = rivian.get_charging_schedule(vehicle_id) 436 | if verbose: 437 | print(f"get_charging_schedule:\n{response_json}") 438 | schedule = response_json['data']['getVehicle']['chargingSchedules'] 439 | return schedule 440 | 441 | 442 | def charging_sessions(verbose): 443 | rivian = get_rivian_object() 444 | response_json = rivian.get_completed_session_summaries() 445 | if verbose: 446 | print(f"get_completed_session_summaries:\n{response_json}") 447 | sessions = [] 448 | for s in response_json['data']['getCompletedSessionSummaries']: 449 | sessions.append({ 450 | 'charge_start': s['startInstant'], 451 | 'charge_end': s['endInstant'], 452 | 'energy': s['totalEnergyKwh'], 453 | 'vendor': s['vendor'], 454 | 'range_added': s['rangeAddedKm'], 455 | 'transaction_id': s['transactionId'], 456 | }) 457 | # sort sessions by charge_start 458 | sessions.sort(key=lambda x: x['charge_start']) 459 | return sessions 460 | 461 | 462 | def charging_session(verbose): 463 | rivian = get_rivian_object() 464 | response_json = rivian.get_non_rivian_user_session() 465 | if verbose: 466 | print(f"get_non_rivian_user_session:\n{response_json}") 467 | session = response_json['data']['getNonRivianUserSession'] 468 | return session 469 | 470 | 471 | def live_charging_session(vehicle_id, verbose=False): 472 | rivian = get_rivian_object() 473 | response_json = rivian.get_live_session_data(vehicle_id) 474 | if verbose: 475 | print(f"get_live_session_data:\n{response_json}") 476 | session = response_json['data']['getLiveSessionData'] 477 | return session 478 | 479 | 480 | def live_charging_history(vehicle_id, verbose=False): 481 | rivian = get_rivian_object() 482 | response_json = rivian.get_live_session_history(vehicle_id) 483 | if verbose: 484 | print(f"get_live_session_history:\n{response_json}") 485 | history = response_json['data']['getLiveSessionHistory']['chartData'] 486 | # sort history by 'time' 487 | history.sort(key=lambda x: x['time']) 488 | return history 489 | 490 | 491 | def vehicle_command(command, vehicle_id=None, verbose=False): 492 | vehiclePublicKey = None 493 | user_info = user_information(verbose) 494 | for v in user_info['vehicles']: 495 | if vehicle_id and v['id'] == vehicle_id: 496 | found = True 497 | else: 498 | vehicle_id = v['id'] 499 | found = True 500 | if found: 501 | vehiclePublicKey = v['vas']['vehiclePublicKey'] 502 | break 503 | # Only need first 504 | vasPhoneId = user_info['enrolledPhones'][0]['vas']['vasPhoneId'] 505 | deviceName = user_info['enrolledPhones'][0]['enrolled'][0]['deviceName'] 506 | 507 | vehicle = get_vehicle(vehicle_id=vehicle_id, verbose=verbose) 508 | deviceId = None 509 | for u in vehicle: 510 | for d in u['devices']: 511 | if d['deviceName'] == deviceName: 512 | deviceId = d['id'] 513 | break 514 | if deviceId: 515 | break 516 | 517 | print(f"Vehicle ID: {vehicle_id} vasPhoneID: {vasPhoneId} vehiclePublicKey: {vehiclePublicKey} deviceId: {deviceId}") 518 | 519 | 520 | def test_graphql(verbose): 521 | rivian = get_rivian_object() 522 | query = { 523 | "operationName": "GetAdventureFeed", 524 | "query": 'query GetAdventureFeed($locale: String!, $slug: String!) { egAdventureFeedCollection(locale: $locale, limit: 1, where: { slug: $slug } ) { items { slug entryTitle cardsCollection(limit: 15) { items { __typename ... on EgAdventureFeedStoryCard { slug entryTitle title subtitle cover { entryTitle sourcesCollection(limit: 1) { items { entryTitle media auxiliaryData { __typename ... on EgImageAuxiliaryData { altText } } } } } slidesCollection { items { entryTitle duration theme gradient mediaCollection(limit: 2) { items { __typename ... on EgCloudinaryMedia { entryTitle sourcesCollection(limit: 1) { items { entryTitle media auxiliaryData { __typename ... on EgImageAuxiliaryData { altText } } } } } ... on EgLottieAnimation { entryTitle altText media mode } } } } } } ... on EgAdventureFeedEditorialCard { slug entryTitle title subtitle cover { entryTitle sourcesCollection(limit: 1) { items { entryTitle media auxiliaryData { __typename ... on EgImageAuxiliaryData { altText } } } } } sectionsCollection { items { entryTitle theme mediaCollection(limit: 2) { items { __typename ... on EgCloudinaryMedia { entryTitle sourcesCollection(limit: 1) { items { entryTitle media auxiliaryData { __typename ... on EgImageAuxiliaryData { altText } } } } } ... on EgLottieAnimation { entryTitle altText media mode } } } } } } } } } } }', 525 | "variables": { 526 | "locale": "en_US", 527 | }, 528 | } 529 | response = rivian.raw_graphql_query(url=RIVIAN_CONTENT_PATH, query=query, headers=rivian.gateway_headers()) 530 | response_json = response.json() 531 | if verbose: 532 | print(f"test_graphql:\n{response_json}") 533 | 534 | 535 | def get_local_time(ts): 536 | if type(ts) is str: 537 | try: 538 | t = parse(ts) 539 | except: 540 | return 541 | else: 542 | t = ts 543 | to_zone = tz.tzlocal() 544 | if t: 545 | t = t.astimezone(to_zone) 546 | return t 547 | 548 | 549 | def show_local_time(ts): 550 | t = get_local_time(ts) 551 | return t.strftime("%m/%d/%Y, %H:%M%p %Z") if t else None 552 | 553 | 554 | def celsius_to_temp_units(c, metric=False): 555 | if metric: 556 | return c 557 | else: 558 | return (c * 9/5) + 32 559 | 560 | 561 | def meters_to_distance_units(m, metric=False): 562 | if metric: 563 | return m / 1000 564 | else: 565 | return m / 1609.0 566 | 567 | 568 | def miles_to_meters(m, metric=False): 569 | if metric: 570 | return m 571 | else: 572 | return m * 1609.0 573 | 574 | 575 | def kilometers_to_distance_units(m, metric=False): 576 | if metric: 577 | return m 578 | else: 579 | return (m * 1000) / 1609.0 580 | 581 | 582 | def get_elapsed_time_string(elapsed_time_in_seconds): 583 | elapsed_time = timedelta(seconds=elapsed_time_in_seconds) 584 | total_seconds = int(elapsed_time.total_seconds()) 585 | hours = total_seconds // 3600 586 | minutes = (total_seconds % 3600) // 60 587 | seconds = total_seconds % 60 588 | return f"{hours} hours, {minutes} minutes, {seconds} seconds" 589 | 590 | 591 | def main(): 592 | parser = argparse.ArgumentParser(description='Rivian CLI') 593 | parser.add_argument('--login', help='Login to account', required=False, action='store_true') 594 | parser.add_argument('--user', help='Display user info', required=False, action='store_true') 595 | parser.add_argument('--vehicles', help='Display vehicles', required=False, action='store_true') 596 | parser.add_argument('--chargers', help='Display chargers', required=False, action='store_true') 597 | parser.add_argument('--speakers', help='Display Speakers', required=False, action='store_true') 598 | parser.add_argument('--images', help='Display Image URLs', required=False, action='store_true') 599 | parser.add_argument('--vehicle_orders', help='Display vehicle orders', required=False, action='store_true') 600 | parser.add_argument('--retail_orders', help='Display retail orders', required=False, action='store_true') 601 | parser.add_argument('--payment_methods', help='Show payment methods', required=False, action='store_true') 602 | parser.add_argument('--test', help='For testing graphql queries', required=False, action='store_true') 603 | parser.add_argument('--charge_ids', help='Show charge_ids', required=False, action='store_true') 604 | parser.add_argument('--verbose', help='Verbose output', required=False, action='store_true') 605 | parser.add_argument('--privacy', help='Fuzz order/vin info', required=False, action='store_true') 606 | parser.add_argument('--state', help='Get vehicle state', required=False, action='store_true') 607 | parser.add_argument('--vehicle', help='Get vehicle access info', required=False, action='store_true') 608 | parser.add_argument('--vehicle_id', help='Vehicle to query (defaults to first one found)', required=False) 609 | parser.add_argument('--last_seen', help='Timestamp vehicle was last seen', required=False, action='store_true') 610 | parser.add_argument('--user_info', help='Show user information', required=False, action='store_true') 611 | parser.add_argument('--ota', help='Show user information', required=False, action='store_true') 612 | parser.add_argument('--poll', help='Poll vehicle state', required=False, action='store_true') 613 | parser.add_argument('--poll_frequency', help='Poll frequency', required=False, default=30, type=int) 614 | parser.add_argument('--poll_show_all', help='Show all poll results even if no changes occurred', required=False, action='store_true') 615 | parser.add_argument('--poll_inactivity_wait', 616 | help='If not sleeping and nothing changes for this period of time ' 617 | 'then do a poll_sleep_wait. Defaults to 0 for continual polling ' 618 | 'at poll_frequency', 619 | required=False, default=0, type=int) 620 | parser.add_argument('--poll_sleep_wait', 621 | help='# How long to stop polling to let car go to sleep (depends on poll_inactivity_wait)', 622 | required=False, default=40*60, type=int) 623 | parser.add_argument('--query', help='Single poll instance (quick poll)', required=False, action='store_true') 624 | parser.add_argument('--metric', help='Use metric vs imperial units', required=False, action='store_true') 625 | parser.add_argument('--plan_trip', help='Plan a trip - starting soc, starting range in meters, origin lat,origin long,dest lat,dest long', required=False) 626 | 627 | parser.add_argument('--charging_schedule', help='Get charging schedule', required=False, action='store_true') 628 | parser.add_argument('--charge_sessions', help='Get charging sessions', required=False, action='store_true') 629 | parser.add_argument('--last_charge', help='Get last charge session', required=False, action='store_true') 630 | parser.add_argument('--charge_session', help='Get current charging session', required=False, action='store_true') 631 | parser.add_argument('--live_charging_session', help='Get live charging session', required=False, action='store_true') 632 | parser.add_argument('--live_charging_history', help='Get live charging session history', required=False, action='store_true') 633 | 634 | parser.add_argument('--all', help='Run all commands silently as a sort of test of all commands', required=False, action='store_true') 635 | parser.add_argument('--command', help='Send vehicle a command', required=False, 636 | choices=['WAKE_VEHICLE', 637 | 'OPEN_FRUNK', 638 | 'CLOSE_FRUNK', 639 | 'OPEN_ALL_WINDOWS', 640 | 'CLOSE_ALL_WINDOWS', 641 | 'UNLOCK_ALL_CLOSURES', 642 | 'LOCK_ALL_CLOSURES', 643 | 'ENABLE_GEAR_GUARD_VIDEO', 644 | 'DISABLE_GEAR_GUARD_VIDEO', 645 | 'HONK_AND_FLASH_LIGHTS', 646 | 'OPEN_TONNEAU_COVER', 647 | 'CLOSE_TONNEAU_COVER', 648 | ] 649 | ) 650 | args = parser.parse_args() 651 | original_stdout = sys.stdout 652 | 653 | if args.all: 654 | print("Running all commands silently") 655 | f = open(os.devnull, 'w') 656 | sys.stdout = f 657 | 658 | if args.login: 659 | login(args.verbose) 660 | 661 | rivian_info = { 662 | 'vehicle_orders': [], 663 | 'retail_orders': [], 664 | 'vehicles': [], 665 | } 666 | 667 | if args.metric: 668 | distance_units = "km" 669 | distance_units_string = "kph" 670 | temp_units_string = "C" 671 | else: 672 | distance_units = "mi" 673 | distance_units_string = "mph" 674 | temp_units_string = "F" 675 | 676 | vehicle_id = None 677 | if args.vehicle_id: 678 | vehicle_id = args.vehicle_id 679 | 680 | needs_vehicle = args.vehicles or \ 681 | args.vehicle or \ 682 | args.state or \ 683 | args.last_seen or \ 684 | args.ota or \ 685 | args.poll or \ 686 | args.query or \ 687 | args.plan_trip or \ 688 | args.user_info or \ 689 | args.charge_session or \ 690 | args.live_charging_session or \ 691 | args.live_charging_history or \ 692 | args.charging_schedule or \ 693 | args.all 694 | 695 | if args.vehicle_orders or (needs_vehicle and not args.vehicle_id): 696 | verbose = args.vehicle_orders and args.verbose 697 | rivian_info['vehicle_orders'] = vehicle_orders(verbose) 698 | 699 | if args.vehicle_orders or args.all: 700 | if len(rivian_info['vehicle_orders']): 701 | print("Vehicle Orders:") 702 | for order in rivian_info['vehicle_orders']: 703 | order_id = 'xxxx' + order['id'][-4:] if args.privacy else order['id'] 704 | print(f"Order ID: {order_id}") 705 | print(f"Order Date: {order['orderDate'][:10] if args.privacy else order['orderDate']}") 706 | print(f"Config State: {order['configurationStatus']}") 707 | print(f"Order State: {order['state']}") 708 | print(f"Status: {order['fulfillmentSummaryStatus']}") 709 | print(f"Item: {order['items'][0]}") 710 | print(f"Customer flow complete: {'Yes' if order['isConsumerFlowComplete'] else 'No'}") 711 | 712 | # No extra useful info to display 713 | # order_info = get_order(order['id'], args.verbose) 714 | 715 | # Get delivery info 716 | delivery_status = delivery(order['id'], args.verbose) 717 | if 'carrier' in delivery_status: 718 | print(f"Delivery carrier: {delivery_status['carrier']}") 719 | if 'status' in delivery_status: 720 | print(f"Delivery status: {delivery_status['status']}") 721 | if 'appointmentDetails' in delivery_status and delivery_status['appointmentDetails']: 722 | print("Delivery appointment details:") 723 | start = parse(delivery_status['appointmentDetails']['startDateTime']) 724 | end = parse(delivery_status['appointmentDetails']['endDateTime']) 725 | print(f' Start: {start.strftime("%m/%d/%Y, %H:%M %p")}') 726 | print(f' End : {end.strftime("%m/%d/%Y, %H:%M %p")}') 727 | else: 728 | print("Delivery appointment details: Not available yet") 729 | 730 | # Get transaction steps 731 | if order['id']: 732 | transaction_steps = None 733 | try: 734 | transaction_steps = transaction_status(order['id'], args.verbose) 735 | except Exception as e: 736 | if verbose: 737 | print(f"Error getting transaction status for {order_id}") 738 | i = 1 739 | completed = 0 740 | if transaction_steps: 741 | for s in transaction_steps: 742 | if transaction_steps[s]['complete']: 743 | completed += 1 744 | print(f"{completed}/{len(transaction_steps)} Steps Complete:") 745 | for s in sorted(transaction_steps): 746 | print(f" Step: {s}: {transaction_steps[s]['item']}: {transaction_steps[s]['status']}, Complete: {transaction_steps[s]['complete']}") 747 | i += 1 748 | 749 | # Don't need to show this for now 750 | # finance_summary(order['id'], args.verbose) 751 | print("\n") 752 | else: 753 | print("No Vehicle Orders found") 754 | 755 | if args.retail_orders or args.all: 756 | rivian_info['retail_orders'] = retail_orders(args.verbose) 757 | if len(rivian_info['retail_orders']): 758 | print("Retail Orders:") 759 | for order in rivian_info['retail_orders']: 760 | order_id = 'xxxx' + order['id'][-4:] if args.privacy else order['id'] 761 | print(f"Order ID: {order_id}") 762 | print(f"Order Date: {order['orderDate'][:10] if args.privacy else order['orderDate']}") 763 | print(f"Order State: {order['state']}") 764 | print(f"Status: {order['fulfillmentSummaryStatus']}") 765 | print(f"Items: {', '.join(order['items'])}") 766 | print("\n") 767 | else: 768 | print("No Retail Orders found") 769 | 770 | if args.vehicles or args.all or (needs_vehicle and not args.vehicle_id): 771 | found_vehicle = False 772 | verbose = args.vehicles and args.verbose 773 | for order in rivian_info['vehicle_orders']: 774 | details = order_details(order['id'], verbose) 775 | vehicle = {} 776 | for i in details: 777 | value = details[i] 778 | vehicle[i] = value 779 | rivian_info['vehicles'].append(vehicle) 780 | if not found_vehicle: 781 | if args.vehicle_id: 782 | if vehicle['vehicleId'] == args.vehicle_id: 783 | found_vehicle = True 784 | elif 'vehicleId' in vehicle: 785 | vehicle_id = vehicle['vehicleId'] 786 | found_vehicle = True 787 | if not found_vehicle: 788 | user_info = user_information(verbose) 789 | for v in user_info['vehicles']: 790 | if 'id' in v: 791 | vehicle_id = v['id'] 792 | found_vehicle = True 793 | break 794 | if not found_vehicle: 795 | print(f"Didn't find vehicle ID {args.vehicle_id}") 796 | return -1 797 | 798 | if args.vehicles or args.all: 799 | if len(rivian_info['vehicles']): 800 | print("Vehicles:") 801 | for v in rivian_info['vehicles']: 802 | for i in v: 803 | print(f"{i}: {v[i]}") 804 | print("\n") 805 | else: 806 | print("No Vehicles found") 807 | 808 | if args.payment_methods or args.all: 809 | pmt = payment_methods(args.verbose) 810 | print("Payment Methods:") 811 | if len(pmt): 812 | for p in pmt: 813 | print(f"Type: {p['type']}") 814 | print(f"Default: {p['default']}") 815 | if p['card']: 816 | for i in p['card']: 817 | print(f"Card {i}: {p['card'][i]}") 818 | print("\n") 819 | else: 820 | print("No Payment Methods found") 821 | 822 | if args.charge_ids or args.all: 823 | print("Charge IDs:") 824 | data = check_by_rivian_id(args.verbose) 825 | for i in data: 826 | print(f"{i}: {data[i]}") 827 | data = get_linked_email_for_rivian_id(args.verbose) 828 | for i in data: 829 | print(f"{i}: {data[i]}") 830 | print("\n") 831 | 832 | # No value? 833 | # get_parameter_store_values(args.verbose) 834 | 835 | # For testing new graphql queries 836 | if args.test: 837 | test_graphql(args.verbose) 838 | 839 | if args.chargers or args.all: 840 | rivian_info['chargers'] = chargers(args.verbose) 841 | if len(rivian_info['chargers']): 842 | print("Chargers:") 843 | for c in rivian_info['chargers']: 844 | for i in c: 845 | print(f"{i}: {c[i]}") 846 | print("\n") 847 | else: 848 | print("No Chargers found") 849 | 850 | if args.speakers or args.all: 851 | rivian_info['speakers'] = speakers(args.verbose) 852 | if len(rivian_info['speakers']): 853 | print("Speakers:") 854 | for v in rivian_info['speakers']: 855 | print(f"Vehicle ID: {v['id']}") 856 | for c in v['connectedProducts']: 857 | print(f" {c['__typename']}: Serial # {c['serialNumber']}") 858 | else: 859 | print("No Speakers found") 860 | 861 | if args.ota or args.all: 862 | ota = get_ota_info(vehicle_id, args.verbose) 863 | if len(ota): 864 | if ota['availableOTAUpdateDetails']: 865 | print(f"Available OTA Version: {ota['availableOTAUpdateDetails']['version']}") 866 | print(f"Available OTA Release notes: {ota['availableOTAUpdateDetails']['url']}") 867 | if ota['currentOTAUpdateDetails']: 868 | print(f"Current Version: {ota['currentOTAUpdateDetails']['version']}") 869 | print(f"Current Version Release notes: {ota['currentOTAUpdateDetails']['url']}") 870 | else: 871 | print("No OTA info available") 872 | 873 | # Basic images for vehicle 874 | if args.images or args.all: 875 | rivian_info['images'] = images(args.verbose) 876 | if len(rivian_info['images']): 877 | print("Images:") 878 | for c in rivian_info['images']: 879 | for i in c: 880 | print(f"{i}: {c[i]}") 881 | print("\n") 882 | else: 883 | print("No Images found") 884 | 885 | if args.user_info or args.all: 886 | print("User Vehicles:") 887 | user_info = user_information(args.verbose) 888 | for v in user_info['vehicles']: 889 | print(f"Vehicle ID: {v['id']}") 890 | if args.privacy: 891 | vin = v['vin'][-8:-3] + 'xxx' 892 | else: 893 | vin = v['vin'] 894 | print(f" Vin: {vin}") 895 | print(f" State: {v['state']}") 896 | print(f" Kind: {v['vehicle']['modelYear']} {v['vehicle']['make']} {v['vehicle']['model']}") 897 | print(f" General assembly date: {v['vehicle']['actualGeneralAssemblyDate']}") 898 | print(f" OTA early access: {v['vehicle']['otaEarlyAccessStatus']}") 899 | if ('vehicleState' in v['vehicle'] and v['vehicle']['vehicleState'] and 900 | 'supportedFeatures' in v['vehicle']['vehicleState']): 901 | print(" Features:") 902 | for f in v['vehicle']['vehicleState']['supportedFeatures']: 903 | print(f" {f['name']}: {f['status']}") 904 | for p in user_info['enrolledPhones']: 905 | print("Enrolled phones:") 906 | for d in p['enrolled']: 907 | if d['vehicleId'] == vehicle_id: 908 | print(f" Device Name: {d['deviceName']}") 909 | print(f" Device identityId: {d['identityId']}") 910 | print(f" vasPhoneId: {p['vas']['vasPhoneId']}") 911 | print(f" publicKey: {p['vas']['publicKey']}") 912 | 913 | if (args.user or args.all) and not args.privacy: 914 | user = get_user(args.verbose) 915 | print("User details:") 916 | for i in user: 917 | if i == 'registrationChannels2FA': 918 | for j in user[i]: 919 | print(f"registrationChannels2FA {j}: {user[i][j]}") 920 | elif i == 'addresses': 921 | address_num = 1 922 | for a in user[i]: 923 | print(f"Address {address_num}:") 924 | for j in a: 925 | data = a[j] 926 | if type(data) == list: 927 | data = ", ".join(data) 928 | print(f" {j}: {data}") 929 | address_num += 1 930 | else: 931 | print(f"{i}: {user[i]}") 932 | print("\n") 933 | 934 | if args.state or args.all: 935 | state = get_vehicle_state(vehicle_id, args.verbose) 936 | if not state: 937 | print("Unable to retrieve vehicle state, try with --verbose") 938 | else: 939 | print("Vehicle State:") 940 | print(f"Power State: {state['powerState']['value']}") 941 | print(f"Drive Mode: {state['driveMode']['value']}") 942 | print(f"Gear Status: {state['gearStatus']['value']}") 943 | print(f"Odometer: {meters_to_distance_units(state['vehicleMileage']['value'], args.metric):.1f} {distance_units}") 944 | if not args.privacy: 945 | print(f"Location: {state['gnssLocation']['latitude']},{state['gnssLocation']['longitude']}") 946 | print(f"Speed: {meters_to_distance_units(state['gnssSpeed']['value'], args.metric):.1f} {distance_units}/h") 947 | print(f"Bearing: {state['gnssBearing']['value']:.1f} degrees") 948 | print(f"Altitude: {state['gnssAltitude']['value']}") 949 | print(f"Location Error:") 950 | print(f" Vertical {state['gnssError']['positionVertical']} m") 951 | print(f" Horizontal {state['gnssError']['positionHorizontal']} m") 952 | print(f" Speed {meters_to_distance_units(state['gnssError']['speed'], args.metric):.1f} {distance_units}/h") 953 | print(f" Bearing {state['gnssError']['bearing']} degrees") 954 | 955 | print("Battery:") 956 | print(f" Battery Level: {state['batteryLevel']['value']:.1f}%") 957 | print(f" Range: {kilometers_to_distance_units(state['distanceToEmpty']['value'], args.metric):.1f} {distance_units}") 958 | print(f" Battery Limit: {state['batteryLimit']['value']:.1f}%") 959 | print(f" Battery Capacity: {state['batteryCapacity']['value']} kW") 960 | print(f" Charging state: {state['chargerState']['value']}") 961 | if state['chargerStatus']: 962 | print(f" Charger status: {state['chargerStatus']['value']}") 963 | print(f" Time to end of charge: {state['timeToEndOfCharge']['value']}") 964 | print(f" Charging Time Estimation Validity: {state['chargingTimeEstimationValidity']['value']}") 965 | print(f" Limited Accel Cold: {state['limitedAccelCold']['value']}") 966 | print(f" Limited Regen Cold: {state['limitedRegenCold']['value']}") 967 | 968 | 969 | print("OTA:") 970 | print(f" Current Version: {state['otaCurrentVersion']['value']}") 971 | print(f" Available version: {state['otaAvailableVersion']['value']}") 972 | if state['otaStatus']: 973 | print(f" Status: {state['otaStatus']['value']}") 974 | if state['otaInstallType']: 975 | print(f" Install type: {state['otaInstallType']['value']}") 976 | if state['otaInstallDuration']: 977 | print(f" Duration: {state['otaInstallDuration']['value']}") 978 | if state['otaDownloadProgress']: 979 | print(f" Download progress: {state['otaDownloadProgress']['value']}") 980 | print(f" Install ready: {state['otaInstallReady']['value']}") 981 | if state['otaInstallProgress']: 982 | print(f" Install progress: {state['otaInstallProgress']['value']}") 983 | if state['otaInstallTime']: 984 | print(f" Install time: {state['otaInstallTime']['value']}") 985 | if state['otaCurrentStatus']: 986 | print(f" Current Status: {state['otaCurrentStatus']['value']}") 987 | 988 | print("Climate:") 989 | print(f" Climate Interior Temp: {celsius_to_temp_units(state['cabinClimateInteriorTemperature']['value'], args.metric)}º{temp_units_string}") 990 | print(f" Climate Driver Temp: {celsius_to_temp_units(state['cabinClimateDriverTemperature']['value'], args.metric)}º{temp_units_string}") 991 | print(f" Cabin Preconditioning Status: {state['cabinPreconditioningStatus']['value']}") 992 | print(f" Cabin Preconditioning Type: {state['cabinPreconditioningType']['value']}") 993 | print(f" Defrost: {state['defrostDefogStatus']['value']}") 994 | print(f" Steering Wheel Heat: {state['steeringWheelHeat']['value']}") 995 | print(f" Pet Mode: {state['petModeStatus']['value']}") 996 | 997 | print("Security:") 998 | if state['alarmSoundStatus']: 999 | print(f" Alarm active: {state['alarmSoundStatus']['value']}") 1000 | if state['gearGuardVideoStatus']: 1001 | print(f" Gear Guard Video: {state['gearGuardVideoStatus']['value']}") 1002 | if state['gearGuardVideoMode']: 1003 | print(f" Gear Guard Mode: {state['gearGuardVideoMode']['value']}") 1004 | if state['alarmSoundStatus']: 1005 | print(f" Last Alarm: {show_local_time(state['alarmSoundStatus']['timeStamp'])}") 1006 | print(f" Gear Guard Locked: {state['gearGuardLocked']['value'] == 'locked'}") 1007 | 1008 | print(f"Charge Port: {state['chargePortState']['value']}") 1009 | print("Doors:") 1010 | print(f" Front left locked: {state['doorFrontLeftLocked']['value'] == 'locked'}") 1011 | print(f" Front left closed: {state['doorFrontLeftClosed']['value'] == 'closed'}") 1012 | print(f" Front right locked: {state['doorFrontRightLocked']['value'] == 'locked'}") 1013 | print(f" Front right closed: {state['doorFrontRightClosed']['value'] == 'closed'}") 1014 | print(f" Rear left locked: {state['doorRearLeftLocked']['value'] == 'locked'}") 1015 | print(f" Rear left closed: {state['doorRearLeftClosed']['value'] == 'closed'}") 1016 | print(f" Rear right locked: {state['doorRearRightLocked']['value'] == 'locked'}") 1017 | print(f" Rear right closed: {state['doorRearRightClosed']['value'] == 'closed'}") 1018 | 1019 | print("Windows:") 1020 | print(f" Front left closed: {state['windowFrontLeftClosed']['value'] == 'closed'}") 1021 | print(f" Front right closed: {state['windowFrontRightClosed']['value'] == 'closed'}") 1022 | print(f" Rear left closed: {state['windowRearLeftClosed']['value'] == 'closed'}") 1023 | print(f" Rear right closed: {state['windowRearRightClosed']['value'] == 'closed'}") 1024 | print(f" Next Action: {state['windowsNextAction']['value']}") 1025 | 1026 | print("Seats:") 1027 | print(f" Front left Heat: {state['seatFrontLeftHeat']['value'] == 'On'}") 1028 | print(f" Front right Heat: {state['seatFrontRightHeat']['value'] == 'On'}") 1029 | print(f" Rear left Heat: {state['seatRearLeftHeat']['value'] == 'On'}") 1030 | print(f" Rear right Heat: {state['seatRearRightHeat']['value'] == 'On'}") 1031 | 1032 | print("Storage:") 1033 | print(" Frunk:") 1034 | print(f" Frunk locked: {state['closureFrunkLocked']['value'] == 'locked'}") 1035 | print(f" Frunk closed: {state['closureFrunkClosed']['value'] == 'closed'}") 1036 | print(f" Frunk Next Action: {state['closureFrunkNextAction']['value']}") 1037 | 1038 | print(" Lift Gate:") 1039 | print(f" Lift Gate Locked: {state['closureLiftgateLocked']['value'] == 'locked'}") 1040 | print(f" Lift Gate Closed: {state['closureLiftgateClosed']['value']}") 1041 | print(f" Lift Next Action: {state['closureLiftgateNextAction']['value']}") 1042 | 1043 | print(" Tonneau:") 1044 | print(f" Tonneau Locked: {state['closureTonneauLocked']['value']}") 1045 | print(f" Tonneau Closed: {state['closureTonneauClosed']['value']}") 1046 | 1047 | print("Trailer:") 1048 | print(f" Trailer Status: {state['trailerStatus']['value']}") 1049 | if state['rearHitchStatus']: 1050 | print(f" Rear Hitch Status: {state['rearHitchStatus']['value']}") 1051 | 1052 | print("Maintenance:") 1053 | print(f" Service Mode: {state['serviceMode']['value']}") 1054 | print(f" Car Wash Mode: {state['carWashMode']['value']}") 1055 | print(f" Wiper Fluid: {state['wiperFluidState']['value']}") 1056 | print(" Tire pressures:") 1057 | print(f" Front Left: {state['tirePressureStatusFrontLeft']['value']}") 1058 | print(f" Front Right: {state['tirePressureStatusFrontRight']['value']}") 1059 | print(f" Rear Left: {state['tirePressureStatusRearLeft']['value']}") 1060 | print(f" Rear Right: {state['tirePressureStatusRearRight']['value']}") 1061 | print(f" 12V Battery: {state['twelveVoltBatteryHealth']['value']}") 1062 | if state['btmFfHardwareFailureStatus']: 1063 | print(f" btmFf Hardware Failure Status {state['btmFfHardwareFailureStatus']['value']}") 1064 | if state['btmIcHardwareFailureStatus']: 1065 | print(f" btmIc Hardware Failure Status {state['btmIcHardwareFailureStatus']['value']}") 1066 | if state['btmLfdHardwareFailureStatus']: 1067 | print(f" btmLfd Hardware Failure Status {state['btmLfdHardwareFailureStatus']['value']}") 1068 | if state['btmRfdHardwareFailureStatus']: 1069 | print(f" btmRfd Hardware Failure Status {state['btmRfdHardwareFailureStatus']['value']}") 1070 | 1071 | if args.poll or args.query or args.all: 1072 | single_poll = args.query or args.all 1073 | # Power state = ready, go, sleep, standby, 1074 | # Charge State = charging_ready or charging_active 1075 | # Charger Status = chrgr_sts_not_connected, chrgr_sts_connected_charging, chrgr_sts_connected_no_chrg 1076 | if not single_poll: 1077 | print(f"Polling car every {args.poll_frequency} seconds, only showing changes in data.") 1078 | if args.poll_inactivity_wait: 1079 | print(f"If 'ready' and inactive for {args.poll_inactivity_wait / 60:.0f} minutes will pause polling once for " 1080 | f"every ready state cycle for {args.poll_sleep_wait / 60:.0f} minutes to allow car to go to sleep.") 1081 | print("") 1082 | 1083 | if args.privacy: 1084 | lat_long_title = '' 1085 | else: 1086 | lat_long_title = 'Latitude,Longitude,' 1087 | print(f"timestamp,Power,Drive Mode,Gear,Mileage,Battery,Range,Speed,{lat_long_title}Charger Status,Charge State,Battery Limit,Charge End") 1088 | last_state_change = time.time() 1089 | last_state = None 1090 | last_power_state = None 1091 | long_sleep_completed = False 1092 | last_mileage = None 1093 | distance_time = None 1094 | elapsed_time = None 1095 | speed = 0 1096 | found_bad_response = False 1097 | while True: 1098 | state = get_vehicle_state(vehicle_id, args.verbose, minimal=True) 1099 | if not state: 1100 | if not found_bad_response: 1101 | print(f"{datetime.now().strftime('%m/%d/%Y, %H:%M:%S %p %Z').strip()} Rivian API appears offline") 1102 | found_bad_response = True 1103 | last_state = None 1104 | time.sleep(args.poll_frequency) 1105 | continue 1106 | found_bad_response = False 1107 | if last_power_state != 'ready' and state['powerState']['value'] == 'ready': 1108 | # Allow one long sleep per ready state cycle to allow car to sleep 1109 | long_sleep_completed = False 1110 | last_power_state = state['powerState']['value'] 1111 | if distance_time: 1112 | elapsed_time = (datetime.now() - distance_time).total_seconds() 1113 | if last_mileage and elapsed_time: 1114 | distance_meters = state['vehicleMileage']['value'] - last_mileage 1115 | distance = meters_to_distance_units(distance_meters, args.metric) 1116 | speed = distance * (60 * 60 / elapsed_time) 1117 | last_mileage = state['vehicleMileage']['value'] 1118 | distance_time = datetime.now() 1119 | current_state = \ 1120 | f"{state['powerState']['value']}," \ 1121 | f"{state['driveMode']['value']}," \ 1122 | f"{state['gearStatus']['value']}," \ 1123 | f"{meters_to_distance_units(state['vehicleMileage']['value'], args.metric):.1f}," \ 1124 | f"{state['batteryLevel']['value']:.1f}%," \ 1125 | f"{kilometers_to_distance_units(state['distanceToEmpty']['value'], args.metric):.1f}," \ 1126 | f"{speed:.1f} {distance_units_string}," 1127 | if not args.privacy: 1128 | current_state += \ 1129 | f"{state['gnssLocation']['latitude']}," \ 1130 | f"{state['gnssLocation']['longitude']}," 1131 | if state['chargerStatus']: 1132 | current_state += \ 1133 | f"{state['chargerStatus']['value']}," \ 1134 | f"{state['chargerState']['value']}," \ 1135 | f"{state['batteryLimit']['value']:.1f}%," \ 1136 | f"{state['timeToEndOfCharge']['value'] // 60}h{state['timeToEndOfCharge']['value'] % 60}m" 1137 | if args.poll_show_all or single_poll or current_state != last_state: 1138 | print(f"{datetime.now().strftime('%m/%d/%Y, %H:%M:%S %p %Z').strip()}," + current_state) 1139 | last_state_change = datetime.now() 1140 | last_state = current_state 1141 | if single_poll: 1142 | break 1143 | if state['powerState']['value'] == 'sleep': 1144 | time.sleep(args.poll_frequency) 1145 | else: 1146 | delta = (datetime.now() - last_state_change).total_seconds() 1147 | if args.poll_inactivity_wait and not long_sleep_completed and delta >= args.poll_inactivity_wait: 1148 | print(f"{datetime.now().strftime('%m/%d/%Y, %H:%M:%S %p %Z').strip()} " 1149 | f"Sleeping for {args.poll_sleep_wait / 60:.0f} minutes") 1150 | time.sleep(args.poll_sleep_wait) 1151 | print(f"{datetime.now().strftime('%m/%d/%Y, %H:%M:%S %p %Z').strip()} " 1152 | f"Back to polling every {args.poll_frequency} seconds, showing changes only") 1153 | long_sleep_completed = True 1154 | else: 1155 | time.sleep(args.poll_frequency) 1156 | 1157 | if args.vehicle or args.all: 1158 | vehicle = get_vehicle(vehicle_id, args.verbose) 1159 | print("Vehicle Users:") 1160 | for u in vehicle: 1161 | print(f"{u['firstName']} {u['lastName']}") 1162 | print(f" Email: {u['email']}") 1163 | print(f" Roles: {u['roles']}") 1164 | print(" Devices:") 1165 | for d in u['devices']: 1166 | print(f" {d['deviceName']}, Paired: {d['isPaired']}, Enabled: {d['isEnabled']}, ID: {d['id']}") 1167 | 1168 | if args.last_seen or args.all: 1169 | last_seen = get_vehicle_last_seen(vehicle_id, args.verbose) 1170 | print(f"Vehicle last seen: {show_local_time(last_seen)}") 1171 | 1172 | if args.plan_trip or args.all: 1173 | if args.all: 1174 | starting_soc, starting_range, origin_lat, origin_long, dest_lat, dest_long = \ 1175 | ["85.0", "360", "42.0772", "-71.6303", "42.1399", "-71.5163"] 1176 | else: 1177 | if len(args.plan_trip.split(',')) == 4: 1178 | starting_soc, starting_range, origin_place, dest_place = args.plan_trip.split(',') 1179 | origin_lat, origin_long = extract_lat_long(origin_place) 1180 | dest_lat, dest_long = extract_lat_long(dest_place) 1181 | else: 1182 | starting_soc, starting_range, origin_lat, origin_long, dest_lat, dest_long = args.plan_trip.split(',') 1183 | 1184 | starting_range_meters = miles_to_meters(float(starting_range), args.metric) 1185 | planned_trip = plan_trip( 1186 | vehicle_id, 1187 | starting_soc, 1188 | starting_range_meters, 1189 | origin_lat, 1190 | origin_long, 1191 | dest_lat, 1192 | dest_long, 1193 | args.verbose 1194 | ) 1195 | decode_and_map(planned_trip) 1196 | 1197 | if args.charging_schedule or args.all: 1198 | schedules = charging_schedule(vehicle_id, args.verbose) 1199 | for s in schedules: 1200 | print(f"Start Time: {s['startTime']}") 1201 | print(f"Duration: {s['duration']}") 1202 | if not args.privacy: 1203 | print(f"Location: {s['location']['latitude']},{s['location']['longitude']}") 1204 | print(f"Amperage: {s['amperage']}") 1205 | print(f"Enabled: {s['enabled']}") 1206 | print(f"Weekdays: {s['weekDays']}") 1207 | 1208 | 1209 | if args.charge_sessions or args.last_charge or args.all: 1210 | sessions = charging_sessions(args.verbose) 1211 | if args.last_charge: 1212 | sessions = [sessions[-1]] 1213 | for s in sessions: 1214 | if s['energy'] == 0: 1215 | continue 1216 | print(f"Transaction Id: {s['transaction_id']}") 1217 | print(f"Charge Start: {show_local_time(s['charge_start'])}") 1218 | print(f"Charge End: {show_local_time(s['charge_end'])}") 1219 | print(f"Energy added: {s['energy']} kWh") 1220 | eph = s['energy'] / \ 1221 | ((get_local_time(s['charge_end']) - get_local_time(s['charge_start'])).total_seconds() / 3600) 1222 | print(f"Charge rate: {eph:.1f} kW/h") 1223 | print(f"Vendor: {s['vendor']}") if s['vendor'] else None 1224 | if s['range_added']: 1225 | print(f"Range added: {kilometers_to_distance_units(s['range_added'], args.metric):.1f} {distance_units}") 1226 | rph = kilometers_to_distance_units(s['range_added'], args.metric) / \ 1227 | ((get_local_time(s['charge_end']) - get_local_time(s['charge_start'])).total_seconds() / 3600) 1228 | print(f"Range added rate: {rph:.1f} {distance_units}/h") 1229 | print() 1230 | 1231 | if args.charge_session or args.all: 1232 | session = charging_session(args.verbose) 1233 | print(f"Charger ID: {session['chargerId']}") 1234 | print(f"Transaction ID: {session['transactionId']}") 1235 | print(f"Rivian Charger: {session['isRivianCharger']}") 1236 | print(f"Charging Active: {session['vehicleChargerState']['value'] == 'charging_active'}") 1237 | print(f"Charging Updated: {show_local_time(session['vehicleChargerState']['updatedAt'])}") 1238 | 1239 | if args.live_charging_session or args.all: 1240 | state = get_vehicle_state(vehicle_id, args.verbose) 1241 | s = live_charging_session(vehicle_id=vehicle_id, 1242 | verbose=args.verbose) 1243 | 1244 | print(f"Battery Level: {state['batteryLevel']['value']:.1f}%") 1245 | print(f"Range: {kilometers_to_distance_units(state['distanceToEmpty']['value'], args.metric):.1f} {distance_units}") 1246 | print(f"Battery Limit: {state['batteryLimit']['value']:.1f}%") 1247 | print(f"Charging state: {state['chargerState']['value']}") 1248 | print(f"Charger status: {state['chargerStatus']['value']}") 1249 | 1250 | print(f"Charging Active: {s['vehicleChargerState']['value'] == 'charging_active'}") 1251 | print(f"Charging Updated: {show_local_time(s['vehicleChargerState']['updatedAt'])}") 1252 | print(f"Charge Start: {show_local_time(s['startTime'])}") 1253 | if s['timeElapsed']: 1254 | elapsed_seconds = int(s['timeElapsed']) 1255 | elapsed = get_elapsed_time_string(elapsed_seconds) 1256 | print(f"Elapsed Time: {elapsed}") 1257 | if s['timeRemaining'] and s['timeRemaining']['value']: 1258 | remaining_seconds = int(s['timeRemaining']['value']) 1259 | remaining = get_elapsed_time_string(remaining_seconds) 1260 | print(f"Remaining Time: {remaining}") 1261 | print(f"Charge power: {s['power']['value']} kW") 1262 | print(f"Charge rate: {meters_to_distance_units(s['kilometersChargedPerHour']['value']*1000, args.metric):.1f} {distance_units_string}") 1263 | print(f"Range added: {meters_to_distance_units(s['rangeAddedThisSession']['value']*1000, args.metric):.1f} {distance_units}") 1264 | print(f"Total charged energy: {s['totalChargedEnergy']['value']} kW") 1265 | print(f"State of Charge: {s['soc']['value']:.1f}%") 1266 | print(f"currentMiles: {kilometers_to_distance_units(s['currentMiles']['value'], args.metric):.1f} {distance_units}") 1267 | print(f"current: {s['current']['value']}") 1268 | 1269 | if args.live_charging_history or args.all: 1270 | s = live_charging_history(vehicle_id=vehicle_id, 1271 | verbose=args.verbose) 1272 | start_time = None 1273 | end_time = None 1274 | for d in s: 1275 | print(f"{show_local_time(d['time'])}: {d['kw']} kW") 1276 | if not start_time: 1277 | start_time = get_local_time(d['time']) 1278 | end_time = get_local_time(d['time']) 1279 | if start_time and end_time: 1280 | elapsed = get_elapsed_time_string((end_time - start_time).total_seconds()) 1281 | print(f"Elapsed Time: {elapsed}") 1282 | 1283 | # Work in progress - TODO 1284 | if args.command: 1285 | vehicle_command(args.command, args.vehicle_id, args.verbose) 1286 | 1287 | if args.all: 1288 | sys.stdout = original_stdout 1289 | print("All commands ran and no exceptions encountered") 1290 | 1291 | 1292 | if __name__ == '__main__': 1293 | main() 1294 | 1295 | --------------------------------------------------------------------------------