├── 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 |  [the-mace/rivian-python-api](https://github.com/the-mace/rivian-python-api)
32 |
33 | |11
Tools used|12/14/23
Report generated|
34 | |------|------|
35 |
36 |
37 | ##
Languages (1)
38 |
39 |
40 |
41 |
42 | Python
43 |
44 |
45 | |
46 |
47 |
48 |
49 |
50 | ##
Data (1)
51 |
52 |
53 |
54 |
55 | pgvector
56 |
57 |
58 | |
59 |
60 |
61 |
62 |
63 | ##
DevOps (2)
64 |
65 |
66 |
67 |
68 | Git
69 |
70 |
71 | |
72 |
73 |
74 |
75 |
76 | PyPI
77 |
78 |
79 | |
80 |
81 |
82 |
83 |
84 | ## Other (2)
85 |
86 |
87 |
88 |
89 | LangChain
90 |
91 |
92 | |
93 |
94 |
95 |
96 |
97 | Shell
98 |
99 |
100 | |
101 |
102 |
103 |
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 |
--------------------------------------------------------------------------------