tesla 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +5 -0
  3. data/CHANGELOG.md +31 -0
  4. data/CONTRIBUTING.md +45 -0
  5. data/{LICENSE.txt → LICENSE.md} +1 -1
  6. data/README.md +366 -0
  7. data/SECURITY.md +38 -0
  8. data/lib/tesla/api/alert_endpoints.rb +35 -0
  9. data/lib/tesla/api/charging_endpoints.rb +115 -0
  10. data/lib/tesla/api/climate_endpoints.rb +180 -0
  11. data/lib/tesla/api/closure_endpoints.rb +142 -0
  12. data/lib/tesla/api/command_endpoints.rb +63 -0
  13. data/lib/tesla/api/media_endpoints.rb +114 -0
  14. data/lib/tesla/api/navigation_endpoints.rb +51 -0
  15. data/lib/tesla/api/oauth_endpoints.rb +118 -0
  16. data/lib/tesla/api/partner_endpoints.rb +62 -0
  17. data/lib/tesla/api/security_endpoints.rb +123 -0
  18. data/lib/tesla/api/software_endpoints.rb +38 -0
  19. data/lib/tesla/api/user_endpoints.rb +40 -0
  20. data/lib/tesla/api/vehicle_endpoints.rb +174 -0
  21. data/lib/tesla/api.rb +39 -0
  22. data/lib/tesla/authenticator.rb +33 -0
  23. data/lib/tesla/bearer_token_authenticator.rb +38 -0
  24. data/lib/tesla/certificate_options.rb +51 -0
  25. data/lib/tesla/charge_state.rb +111 -0
  26. data/lib/tesla/charging_site.rb +67 -0
  27. data/lib/tesla/client.rb +336 -0
  28. data/lib/tesla/client_credentials.rb +230 -0
  29. data/lib/tesla/climate_state.rb +116 -0
  30. data/lib/tesla/configuration.rb +483 -0
  31. data/lib/tesla/connection.rb +361 -0
  32. data/lib/tesla/connection_pool.rb +186 -0
  33. data/lib/tesla/deep_copy.rb +41 -0
  34. data/lib/tesla/drive_state.rb +80 -0
  35. data/lib/tesla/errors/bad_gateway.rb +9 -0
  36. data/lib/tesla/errors/bad_request.rb +9 -0
  37. data/lib/tesla/errors/client_error.rb +9 -0
  38. data/lib/tesla/errors/command_failed.rb +42 -0
  39. data/lib/tesla/errors/device_unexpected_response.rb +12 -0
  40. data/lib/tesla/errors/error.rb +7 -0
  41. data/lib/tesla/errors/forbidden.rb +14 -0
  42. data/lib/tesla/errors/gateway_timeout.rb +9 -0
  43. data/lib/tesla/errors/http_error.rb +89 -0
  44. data/lib/tesla/errors/internal_server_error.rb +9 -0
  45. data/lib/tesla/errors/invalid_response.rb +33 -0
  46. data/lib/tesla/errors/locked.rb +12 -0
  47. data/lib/tesla/errors/method_not_allowed.rb +9 -0
  48. data/lib/tesla/errors/misdirected_request.rb +13 -0
  49. data/lib/tesla/errors/network_error.rb +30 -0
  50. data/lib/tesla/errors/not_acceptable.rb +9 -0
  51. data/lib/tesla/errors/not_found.rb +9 -0
  52. data/lib/tesla/errors/oauth_error.rb +53 -0
  53. data/lib/tesla/errors/payment_required.rb +12 -0
  54. data/lib/tesla/errors/precondition_failed.rb +13 -0
  55. data/lib/tesla/errors/request_timeout.rb +13 -0
  56. data/lib/tesla/errors/server_error.rb +9 -0
  57. data/lib/tesla/errors/service_unavailable.rb +9 -0
  58. data/lib/tesla/errors/too_many_redirects.rb +9 -0
  59. data/lib/tesla/errors/too_many_requests.rb +9 -0
  60. data/lib/tesla/errors/unauthorized.rb +9 -0
  61. data/lib/tesla/errors/unavailable_for_legal_reasons.rb +12 -0
  62. data/lib/tesla/errors/unprocessable_content.rb +12 -0
  63. data/lib/tesla/errors/vehicle_asleep.rb +32 -0
  64. data/lib/tesla/gui_settings.rb +60 -0
  65. data/lib/tesla/idempotence.rb +30 -0
  66. data/lib/tesla/identifiers.rb +27 -0
  67. data/lib/tesla/json_parsing.rb +38 -0
  68. data/lib/tesla/nearby_charging_sites.rb +35 -0
  69. data/lib/tesla/path_escaping.rb +30 -0
  70. data/lib/tesla/redacted_output.rb +161 -0
  71. data/lib/tesla/redirect_handler.rb +196 -0
  72. data/lib/tesla/region.rb +27 -0
  73. data/lib/tesla/request_builder.rb +138 -0
  74. data/lib/tesla/resource.rb +403 -0
  75. data/lib/tesla/response_parser.rb +92 -0
  76. data/lib/tesla/retry_after.rb +34 -0
  77. data/lib/tesla/retry_handler.rb +252 -0
  78. data/lib/tesla/settings.rb +115 -0
  79. data/lib/tesla/url_validation.rb +131 -0
  80. data/lib/tesla/user.rb +36 -0
  81. data/lib/tesla/vehicle.rb +90 -0
  82. data/lib/tesla/vehicle_config.rb +95 -0
  83. data/lib/tesla/vehicle_data.rb +67 -0
  84. data/lib/tesla/vehicle_state.rb +125 -0
  85. data/lib/tesla/version.rb +6 -0
  86. data/lib/tesla.rb +139 -0
  87. data/sig/manifest.yaml +10 -0
  88. data/sig/tesla.rbs +989 -0
  89. metadata +134 -118
  90. data/.document +0 -5
  91. data/.rvmrc +0 -1
  92. data/Gemfile +0 -13
  93. data/Gemfile.lock +0 -20
  94. data/README.rdoc +0 -19
  95. data/Rakefile +0 -47
  96. data/test/helper.rb +0 -19
  97. data/test/test_tesla.rb +0 -7
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5c27dc6f3458aabf11c477d29c0a76301e1c87f387507cf827fba13eafa29f7e
4
+ data.tar.gz: 5d4979456a47f1c0f54884976951ca8022b5eaeda6f090c7ae943a400be66756
5
+ SHA512:
6
+ metadata.gz: 9443e63fc24709dcc3dbadaf3e7875877481d60070f4f0e911bdef0084193a55afafff5a09900c3df4910deb55e5767d9b069f5941f943595862dca2b2a0a4a5
7
+ data.tar.gz: 34b2fc8770cbd81abaefde5a5a0a157253caa3c7b0d57f0775ebae58d1ed29379cd7e5c9a0a66cc08df818d99db895a5278c378b3a4a249c736c7473fc6749b8
data/.yardopts ADDED
@@ -0,0 +1,5 @@
1
+ --no-private
2
+ --hide-api private
3
+ --markup markdown
4
+ --tag authenticated:"Requires Authentication"
5
+ lib/**/*.rb
data/CHANGELOG.md ADDED
@@ -0,0 +1,31 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-10-04
11
+
12
+ ### Added
13
+ * Add `vehicles`, `vehicle`, `vehicle_data`, `wake_up`, `nearby_charging_sites`, and `mobile_enabled?` for the vehicle endpoints of the Tesla Fleet API; `wake_up` takes a `timeout` to wait for the vehicle to be online
14
+ * Add `command`, which sends any command to a vehicle, returns `true` once the vehicle has carried it out, and raises `Tesla::CommandFailed` with the reason the vehicle gave when it has not
15
+ * Add the closure commands `lock_doors`, `unlock_doors`, `actuate_trunk`, `open_frunk`, `vent_windows`, `close_windows`, `vent_sunroof`, `close_sunroof`, and `trigger_homelink`
16
+ * Add the climate commands `start_climate`, `stop_climate`, `set_temperature`, `set_max_defrost`, `set_seat_heater`, `set_seat_cooler`, `set_steering_wheel_heater`, `set_climate_keeper_mode`, `set_bioweapon_mode`, and `set_cabin_overheat_protection`
17
+ * Add the charging commands `open_charge_port`, `close_charge_port`, `start_charging`, `stop_charging`, `set_charge_limit`, `set_charging_amps`, `charge_to_standard_range`, and `charge_to_max_range`
18
+ * Add the alert commands `honk_horn` and `flash_lights`, the media commands `toggle_playback`, `next_track`, `previous_track`, `next_favorite`, `previous_favorite`, `volume_up`, `volume_down`, and `set_volume`, and the navigation commands `navigate_to` and `navigate_to_coordinates`
19
+ * Add the security commands `set_sentry_mode`, `set_valet_mode`, `reset_valet_pin`, `remote_start`, `set_speed_limit`, `activate_speed_limit`, `deactivate_speed_limit`, and `clear_speed_limit_pin`, and the software commands `schedule_software_update` and `cancel_software_update`
20
+ * Add `me` and `region` for the user endpoints, and `register_partner` and `partner_public_key` for the partner endpoints
21
+ * Add OAuth 2.0 with the simple_oauth gem: `authorization_url`, `exchange_code`, `refresh_access_token`, and `partner_token`
22
+ * Refresh the access token when the Fleet API answers that it is no longer good, for a client with a refresh token and a client ID, and call `on_token_refresh` with the new tokens
23
+ * Wrap responses in `Vehicle`, `VehicleData`, `ChargeState`, `ClimateState`, `DriveState`, `VehicleState`, `VehicleConfig`, `GUISettings`, `NearbyChargingSites`, `ChargingSite`, `User`, and `Region` objects, which are immutable, compare by identity, match `case`/`in` patterns, inspect as short summaries, and keep `[]` and `to_h` for the raw response
24
+ * Accept a `Vehicle` wherever a VIN or the ID of a vehicle is expected
25
+ * Add `host`, `audience`, `authorization_endpoint`, `token_endpoint`, `user_agent`, `open_timeout`, `read_timeout`, `write_timeout`, `keep_alive_timeout`, `debug_output`, `proxy_url`, `max_redirects`, and `ca_file` options, set globally or per client; the credentials fall back to the `TESLA_ACCESS_TOKEN`, `TESLA_REFRESH_TOKEN`, `TESLA_CLIENT_ID`, `TESLA_CLIENT_SECRET`, and `TESLA_REDIRECT_URI` environment variables, and the host to `TESLA_HOST`
26
+ * Add `max_retries` and `max_retry_delay` options: a 429 is sent again twice by default, honoring `Retry-After`, as is a 502, 503, or 504, or a request the network lost, for the requests that only ask for something; a command is never sent a second time
27
+ * Reuse connections across the requests to the same host that only ask for something; `Client#close` closes them, and a client built with a block closes them when the block returns
28
+ * Follow redirects, up to `max_redirects` times, without the access token when one leads to another host, and never with the body of a request to another host
29
+ * Redact the access token, the tokens and the client secret of a token request and its response, and the PIN or password of a command from `debug_output`
30
+ * Raise an error named for the HTTP status of an unsuccessful response, such as `Tesla::RequestTimeout` for a vehicle that is asleep, each a `Tesla::HTTPError` and a `Tesla::Error`, with the error the Fleet API describes as its message
31
+ * Ship RBS signatures
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,45 @@
1
+ ## Contributing
2
+ In the spirit of [free software][free-sw], **everyone** is encouraged to help improve this project.
3
+
4
+ Here are some ways *you* can contribute:
5
+
6
+ * by using alpha, beta, and prerelease versions
7
+ * by reporting bugs
8
+ * by suggesting new features
9
+ * by writing or editing documentation
10
+ * by writing specifications
11
+ * by writing code (**no patch is too small**: fix typos, add comments, clean up
12
+ inconsistent whitespace)
13
+ * by refactoring code
14
+ * by closing [issues][]
15
+ * by reviewing patches
16
+
17
+ ## Submitting an Issue
18
+ We use the [GitHub issue tracker][issues] to track bugs and features. Before
19
+ submitting a bug report or feature request, check to make sure it hasn't
20
+ already been submitted. When submitting a bug report, please include a [Gist][]
21
+ that includes a stack trace and any details that may be necessary to reproduce
22
+ the bug, including your gem version, Ruby version, and operating system.
23
+ Ideally, a bug report should include a pull request with failing specs.
24
+
25
+ ## Submitting a Pull Request
26
+ 1. [Fork the repository.][fork]
27
+ 2. [Create a topic branch.][branch]
28
+ 3. Add specs for your unimplemented feature or bug fix.
29
+ 4. Run `bundle exec rake spec`. If your specs pass, return to step 3.
30
+ 5. Implement your feature or bug fix.
31
+ 6. Run `bundle exec rake spec`. If your specs fail, return to step 5.
32
+ 7. Run `bundle exec rake mutant`. If any mutations survive, return to step 3.
33
+ 8. Add documentation and type signatures (in `sig/tesla.rbs`) for your feature
34
+ or bug fix.
35
+ 9. Run `bundle exec rake`. If the linters, type checker, or documentation
36
+ checks fail, go back to step 8.
37
+ 10. Add, commit, and push your changes.
38
+ 11. [Submit a pull request.][pr]
39
+
40
+ [free-sw]: http://www.fsf.org/licensing/essays/free-sw.html
41
+ [issues]: https://github.com/sferik/tesla-ruby/issues
42
+ [gist]: https://gist.github.com/
43
+ [fork]: http://help.github.com/fork-a-repo/
44
+ [branch]: http://learn.github.com/p/branching.html
45
+ [pr]: http://help.github.com/send-pull-requests/
@@ -1,4 +1,4 @@
1
- Copyright (c) 2011 Jesse Cooke
1
+ Copyright (c) 2026 Erik Berlin
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md ADDED
@@ -0,0 +1,366 @@
1
+ # Tesla
2
+
3
+ [![tests](https://github.com/sferik/tesla-ruby/actions/workflows/test.yml/badge.svg)](https://github.com/sferik/tesla-ruby/actions/workflows/test.yml)
4
+ [![mutation tests](https://github.com/sferik/tesla-ruby/actions/workflows/mutant.yml/badge.svg)](https://github.com/sferik/tesla-ruby/actions/workflows/mutant.yml)
5
+ [![linter](https://github.com/sferik/tesla-ruby/actions/workflows/lint.yml/badge.svg)](https://github.com/sferik/tesla-ruby/actions/workflows/lint.yml)
6
+ [![type checker](https://github.com/sferik/tesla-ruby/actions/workflows/steep.yml/badge.svg)](https://github.com/sferik/tesla-ruby/actions/workflows/steep.yml)
7
+ [![docs](https://github.com/sferik/tesla-ruby/actions/workflows/docs.yml/badge.svg)](https://github.com/sferik/tesla-ruby/actions/workflows/docs.yml)
8
+
9
+ Ruby wrapper for the [Tesla Fleet API](https://developer.tesla.com/docs/fleet-api).
10
+
11
+ ## Installation
12
+
13
+ Install the gem and add to the application's Gemfile:
14
+
15
+ bundle add tesla
16
+
17
+ Or, if Bundler is not being used to manage dependencies:
18
+
19
+ gem install tesla
20
+
21
+ ## Usage Examples
22
+
23
+ ```ruby
24
+ require 'tesla'
25
+
26
+ Tesla.access_token = 'eyJhbGciOiJSUzI1NiIs...'
27
+
28
+ # Return the vehicles of the account.
29
+ car = Tesla.vehicles.first
30
+ car.vin # => "5YJSA11111111111"
31
+ car.display_name # => "Nikola 2.0"
32
+ car.state # => "asleep"
33
+
34
+ # Wake the car, and wait up to a minute for it to be online.
35
+ car = Tesla.wake_up car, timeout: 60
36
+ car.online? # => true
37
+
38
+ # Pop the trunk. A powered liftgate that is open closes again.
39
+ Tesla.actuate_trunk car
40
+
41
+ # Open the frunk.
42
+ Tesla.open_frunk car
43
+
44
+ # Set the temperature, in degrees Celsius, and turn on the climate control.
45
+ Tesla.set_temperature car, 21.5
46
+ Tesla.start_climate car
47
+
48
+ # Heat the driver's seat, and the steering wheel.
49
+ Tesla.set_seat_heater car, :front_left, 3
50
+ Tesla.set_steering_wheel_heater car, on: true
51
+
52
+ # Keep the dog cool.
53
+ Tesla.set_climate_keeper_mode car, :dog
54
+
55
+ # Lock and unlock the doors.
56
+ Tesla.lock_doors car
57
+ Tesla.unlock_doors car
58
+
59
+ # Vent the windows, and close them again.
60
+ Tesla.vent_windows car
61
+ Tesla.close_windows car
62
+
63
+ # Find the car in a parking lot.
64
+ Tesla.flash_lights car
65
+ Tesla.honk_horn car
66
+
67
+ # Charge to 80%, at 32 amps.
68
+ Tesla.set_charge_limit car, 80
69
+ Tesla.set_charging_amps car, 32
70
+ Tesla.open_charge_port car
71
+ Tesla.start_charging car
72
+
73
+ # Send a destination to the navigation.
74
+ Tesla.navigate_to car, '3500 Deer Creek Road, Palo Alto, CA'
75
+
76
+ # Turn on Sentry Mode.
77
+ Tesla.set_sentry_mode car, on: true
78
+
79
+ # A VIN stands in for the car wherever one is expected.
80
+ Tesla.lock_doors '5YJSA11111111111'
81
+
82
+ # Send a command the library has no method for.
83
+ Tesla.command car, :set_vehicle_name, vehicle_name: 'Nikola 2.0'
84
+ ```
85
+
86
+ Every command returns `true` once the car has carried it out. Each is documented, with its parameters, in the
87
+ mixin of [`Tesla::API`](https://www.rubydoc.info/gems/tesla/Tesla/API) it belongs to:
88
+
89
+ | Mixin | Commands |
90
+ | ----- | -------- |
91
+ | `ClosureEndpoints` | `lock_doors`, `unlock_doors`, `actuate_trunk`, `open_frunk`, `vent_windows`, `close_windows`, `vent_sunroof`, `close_sunroof`, `trigger_homelink` |
92
+ | `ClimateEndpoints` | `start_climate`, `stop_climate`, `set_temperature`, `set_max_defrost`, `set_seat_heater`, `set_seat_cooler`, `set_steering_wheel_heater`, `set_climate_keeper_mode`, `set_bioweapon_mode`, `set_cabin_overheat_protection` |
93
+ | `ChargingEndpoints` | `open_charge_port`, `close_charge_port`, `start_charging`, `stop_charging`, `set_charge_limit`, `set_charging_amps`, `charge_to_standard_range`, `charge_to_max_range` |
94
+ | `AlertEndpoints` | `honk_horn`, `flash_lights` |
95
+ | `MediaEndpoints` | `toggle_playback`, `next_track`, `previous_track`, `next_favorite`, `previous_favorite`, `volume_up`, `volume_down`, `set_volume` |
96
+ | `NavigationEndpoints` | `navigate_to`, `navigate_to_coordinates` |
97
+ | `SecurityEndpoints` | `set_sentry_mode`, `set_valet_mode`, `reset_valet_pin`, `remote_start`, `set_speed_limit`, `activate_speed_limit`, `deactivate_speed_limit`, `clear_speed_limit_pin` |
98
+ | `SoftwareEndpoints` | `schedule_software_update`, `cancel_software_update` |
99
+
100
+ ### Reading the state of the car
101
+
102
+ ```ruby
103
+ data = Tesla.vehicle_data car
104
+
105
+ data.charge_state.battery_level # => 59
106
+ data.charge_state.battery_range # => 149.92, in miles
107
+ data.charge_state.charging_state # => "Charging"
108
+ data.climate_state.inside_temp # => 22.1, in degrees Celsius
109
+ data.climate_state.climate_on? # => false
110
+ data.vehicle_state.locked? # => true
111
+ data.vehicle_state.trunk_open? # => false
112
+ data.vehicle_state.odometer # => 57869.762487, in miles
113
+ data.vehicle_config.car_type # => "modely"
114
+ data.gui_settings.temperature_units # => "F"
115
+
116
+ # The Fleet API answers with the position of the car only when it is asked for,
117
+ # for an access token with the vehicle_location scope.
118
+ data = Tesla.vehicle_data car, endpoints: %i[location_data drive_state]
119
+ data.drive_state.latitude # => 33.111111
120
+
121
+ # The charging sites near the car.
122
+ Tesla.nearby_charging_sites(car).superchargers.map(&:name)
123
+
124
+ # The user the access token was issued for.
125
+ Tesla.me.email
126
+ ```
127
+
128
+ Responses are immutable objects with a reader for the fields used most. Every field of the response is kept, and
129
+ read with `[]` or `to_h`:
130
+
131
+ ```ruby
132
+ data.charge_state[:charge_energy_added] # => 2.42
133
+ data.to_h # => {"id" => 12345678901234567, ...}
134
+
135
+ case Tesla.vehicle(car)
136
+ in {state: "online"} then puts "ready"
137
+ in {state:} then puts "the car is #{state}"
138
+ end
139
+ ```
140
+
141
+ ## Using it with your own car
142
+
143
+ The Fleet API is not only for fleets: it is the API Tesla offers for any car, and an application that commands a
144
+ single car of your own is set up the same way as one that commands thousands. No approval to manage a fleet is
145
+ involved, but there is some setup, which Tesla describes in its
146
+ [getting started guide](https://developer.tesla.com/docs/fleet-api/getting-started/what-is-fleet-api):
147
+
148
+ 1. Create an application at [developer.tesla.com](https://developer.tesla.com) with the Tesla account that owns the
149
+ car, which issues a client ID and a client secret.
150
+ 2. Generate a key pair, and serve the public key from a domain of yours at
151
+ `/.well-known/appspecific/com.tesla.3p.public-key.pem`.
152
+ 3. Register the application in your region with `Tesla.register_partner`.
153
+ 4. Authorize the application for your own account (see [Authentication](#authentication)), and keep the refresh
154
+ token.
155
+ 5. Add the key of the application to the car, by opening `https://tesla.com/_ak/your-domain.example` on the phone
156
+ that has the Tesla app.
157
+ 6. Run the [vehicle command proxy](#the-vehicle-command-proxy) with the private key, and point the library at it.
158
+
159
+ ## Authentication
160
+
161
+ Requests are authorized with an OAuth 2.0 access token, which Tesla issues to an application registered at
162
+ [developer.tesla.com](https://developer.tesla.com). The OAuth 2.0 requests are built by the
163
+ [simple_oauth](https://github.com/laserlemon/simple_oauth) gem.
164
+
165
+ ```ruby
166
+ Tesla.configure do |config|
167
+ config.client_id = '81527cff06843c8634fdc09e8ac0abef'
168
+ config.client_secret = 'ta-secret.7vx6VkVvk2gBgSrN'
169
+ config.redirect_uri = 'https://example.com/auth/callback'
170
+ end
171
+ ```
172
+
173
+ Each of those falls back to an environment variable until it is assigned: `TESLA_CLIENT_ID`, `TESLA_CLIENT_SECRET`,
174
+ `TESLA_REDIRECT_URI`, `TESLA_ACCESS_TOKEN`, and `TESLA_REFRESH_TOKEN`.
175
+
176
+ ```ruby
177
+ # 1. Register the application in the region it is used in, once.
178
+ Tesla.register_partner 'example.com'
179
+
180
+ # 2. Send the owner of the car to authorize the application.
181
+ state = SecureRandom.hex
182
+ redirect_to Tesla.authorization_url(state: state)
183
+
184
+ # 3. Tesla sends them back to the redirect URI with a code. Check the state, and exchange the code for tokens.
185
+ response = SimpleOAuth::OAuth2::AuthorizationResponse.parse(request.query_string, state: state)
186
+ token = Tesla.exchange_code response.code
187
+
188
+ token.access_token # lasts eight hours
189
+ token.refresh_token # lasts three months, and is used once
190
+ token.expires_at # => 2026-10-04 20:00:00 UTC
191
+ ```
192
+
193
+ The module keeps the tokens, and authorizes its requests with them from then on. In another process, start from the
194
+ tokens that were stored:
195
+
196
+ ```ruby
197
+ Tesla.configure do |config|
198
+ config.access_token = stored.access_token
199
+ config.refresh_token = stored.refresh_token
200
+ end
201
+ ```
202
+
203
+ ### Refreshing the access token
204
+
205
+ A client with a refresh token and a client ID asks for a new access token when the Fleet API answers that the one it
206
+ sent is no longer good, and sends the request again. One with a refresh token and no access token asks for one before
207
+ its first request.
208
+
209
+ A refresh token is used once: each refresh answers with another, which is the one the next refresh needs. Store the
210
+ new tokens whenever they change:
211
+
212
+ ```ruby
213
+ Tesla.on_token_refresh = ->(token) { store(token.access_token, token.refresh_token) }
214
+
215
+ # Refresh ahead of time, rather than when a request is turned away.
216
+ Tesla.refresh_access_token
217
+ ```
218
+
219
+ ## The vehicle command proxy
220
+
221
+ Most cars built since 2021 only take commands signed with the
222
+ [Tesla Vehicle Command Protocol](https://github.com/teslamotors/vehicle-command), and the Fleet API answers an
223
+ unsigned one with a `Tesla::Forbidden`. Tesla's
224
+ [vehicle command proxy](https://github.com/teslamotors/vehicle-command#using-the-http-proxy) signs commands with the
225
+ private key of your application, and hands every other request on to the Fleet API. Point the library at it:
226
+
227
+ ```ruby
228
+ Tesla.configure do |config|
229
+ config.host = 'https://localhost:4443'
230
+ # Trust the certificate the proxy was started with. TLS verification is never turned off.
231
+ config.ca_file = 'config/tls-cert.pem'
232
+ # Tokens are issued for the Fleet API of your region rather than for the proxy.
233
+ config.audience = Tesla::Configuration::NORTH_AMERICA_HOST
234
+ end
235
+ ```
236
+
237
+ The proxy knows a car by its VIN, which is what the library sends for a `Tesla::Vehicle`.
238
+
239
+ ## Configuration
240
+
241
+ ```ruby
242
+ Tesla.configure do |config|
243
+ # The Fleet API of your region: NORTH_AMERICA_HOST (the default), EUROPE_HOST, or CHINA_HOST.
244
+ # Tesla.region.fleet_api_base_url names the one an account is served by.
245
+ config.host = Tesla::Configuration::EUROPE_HOST
246
+
247
+ config.open_timeout = 10 # seconds, 60 by default
248
+ config.read_timeout = 30 # seconds, 60 by default
249
+ config.write_timeout = 30 # seconds, 60 by default
250
+ config.keep_alive_timeout = 2 # seconds an idle connection is kept open, the default; 0 opens one per request
251
+ config.max_redirects = 10 # the default
252
+ config.proxy_url = 'http://proxy.example.com:8080'
253
+ config.user_agent = 'MyApp/1.0'
254
+ config.max_retries = 2 # the default; 0 turns retrying off
255
+ config.max_retry_delay = 60 # seconds, the default
256
+ end
257
+ ```
258
+
259
+ Every option can be given to a client of its own instead, which is how several accounts are used at once:
260
+
261
+ ```ruby
262
+ client = Tesla::Client.new(access_token: 'eyJhbGciOiJSUzI1NiIs...', host: Tesla::Configuration::EUROPE_HOST)
263
+ client.vehicles
264
+ client.actuate_trunk '5YJSA11111111111'
265
+
266
+ # A request of your own, for an endpoint the library has no method for.
267
+ client.get '/api/1/vehicles/5YJSA11111111111/release_notes'
268
+ ```
269
+
270
+ ### Connections
271
+
272
+ A request that only asks for something is sent on the connection the request before it left open, so a series of
273
+ them does not open a connection each. A command is sent on a connection of its own, which is closed afterwards, since
274
+ a command sent on a connection the server has closed could not be sent again. `Client#close` closes the connections
275
+ a client keeps open, and a client built with a block closes them when the block returns:
276
+
277
+ ```ruby
278
+ Tesla::Client.new(access_token: 'eyJhbGciOiJSUzI1NiIs...') do |client|
279
+ client.vehicles.each { |car| puts client.vehicle_data(car).charge_state.battery_level }
280
+ end
281
+ ```
282
+
283
+ ### Redirects
284
+
285
+ A redirect is followed, up to `max_redirects` times. One to another scheme, host, or port is followed without the
286
+ access token and without the headers of the caller, and one that would send the body of a request to another host
287
+ is not followed at all.
288
+
289
+ ### Debug output
290
+
291
+ ```ruby
292
+ Tesla.debug_output = $stderr
293
+ ```
294
+
295
+ Every request and response is written to the IO, with the credentials they carry redacted: the `Authorization` and
296
+ `Proxy-Authorization` headers, the tokens and the client secret of a request to the token endpoint and of its
297
+ response, and the PIN or password of a command.
298
+
299
+ ### Retries
300
+
301
+ A request the Fleet API rate limited (429) is sent again, up to `max_retries` times, after the wait its `Retry-After`
302
+ header asks for. A request that only asks for something is also sent again after a 502, 503, or 504, or when the
303
+ network loses it. A command is not: the car may have carried it out before the answer went missing, and a trunk asked
304
+ to open twice closes again.
305
+
306
+ ## Errors
307
+
308
+ Every error the library raises is a `Tesla::Error`.
309
+
310
+ ```ruby
311
+ begin
312
+ Tesla.start_charging car
313
+ rescue Tesla::CommandFailed => e
314
+ e.reason # => "disconnected": the car read the command and refused
315
+ rescue Tesla::RequestTimeout
316
+ Tesla.wake_up car, timeout: 60 # the car is asleep or offline
317
+ retry
318
+ rescue Tesla::TooManyRequests => e
319
+ sleep e.retry_after
320
+ retry
321
+ rescue Tesla::HTTPError => e
322
+ e.code # => 403
323
+ e.message # => the error the Fleet API describes
324
+ end
325
+ ```
326
+
327
+ | Error | Raised when |
328
+ | ----- | ----------- |
329
+ | `Tesla::CommandFailed` | the car answers that it did not carry out a command |
330
+ | `Tesla::VehicleAsleep` | the car is not online once the `timeout` of `wake_up` is over |
331
+ | `Tesla::OAuthError` | the token endpoint turns a request for a token away |
332
+ | `Tesla::NetworkError` | the request is lost to the network |
333
+ | `Tesla::TooManyRedirects` | a request is redirected more than `max_redirects` times |
334
+ | `Tesla::InvalidResponse` | a successful response cannot be read |
335
+ | `Tesla::HTTPError` | the response is not successful; its subclasses are named for the status, and are each a `Tesla::ClientError` or a `Tesla::ServerError` |
336
+
337
+ The statuses the Fleet API gives a meaning of its own:
338
+
339
+ | Status | Error | Meaning |
340
+ | ------ | ----- | ------- |
341
+ | 401 | `Tesla::Unauthorized` | the access token is missing, expired, or revoked |
342
+ | 403 | `Tesla::Forbidden` | the token lacks a scope, or the car takes only signed commands |
343
+ | 408 | `Tesla::RequestTimeout` | the car is asleep or offline |
344
+ | 412 | `Tesla::PreconditionFailed` | the application is not registered in the region |
345
+ | 421 | `Tesla::MisdirectedRequest` | the account belongs to another region |
346
+ | 429 | `Tesla::TooManyRequests` | the application is rate limited |
347
+ | 540 | `Tesla::DeviceUnexpectedResponse` | the car answered with an error |
348
+
349
+ ## Development
350
+
351
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `bundle exec rake` to run the specs,
352
+ the linters, the mutation tests, the type checker, and the documentation checks. `bin/console` opens an interactive
353
+ prompt.
354
+
355
+ The library is tested on Ruby 3.4, Ruby 4.0, and JRuby 10, on Linux, macOS, and Windows.
356
+
357
+ ## Contributing
358
+
359
+ Bug reports and pull requests are welcome on GitHub at https://github.com/sferik/tesla-ruby. See
360
+ [CONTRIBUTING.md](CONTRIBUTING.md).
361
+
362
+ ## License
363
+
364
+ The gem is available as open source under the terms of the [MIT License](LICENSE.md).
365
+
366
+ This project is not affiliated with or endorsed by Tesla, Inc.
data/SECURITY.md ADDED
@@ -0,0 +1,38 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ Security fixes are released for the most recent minor release of the current major series.
6
+
7
+ ## Reporting a vulnerability
8
+
9
+ Please do not report security vulnerabilities through public GitHub issues, pull requests, or discussions.
10
+
11
+ Report them privately through [GitHub security advisories](https://github.com/sferik/tesla-ruby/security/advisories/new),
12
+ or by email to [sferik@gmail.com](mailto:sferik@gmail.com). Include the version of the gem, the Ruby version, and the
13
+ steps to reproduce the problem.
14
+
15
+ ## What this library handles
16
+
17
+ This library sends credentials to the Tesla Fleet API and to Tesla's authorization server: OAuth 2.0 access tokens,
18
+ refresh tokens, and the client secret of an application. Those credentials can unlock and start a car, so reports
19
+ about the way they are stored, sent, logged, or resolved are particularly welcome. It already takes these
20
+ precautions, so a way around one of them is a vulnerability rather than a feature request:
21
+
22
+ * `inspect` output for clients and authenticators never includes credentials.
23
+ * The values interpolated into request paths are escaped, so a vehicle tag or a command name holding a slash cannot
24
+ walk out of the endpoint it was meant for and take the access token of the request with it.
25
+ * A request path that is a URL of another host, or that climbs out of the path prefix of the host, is refused rather
26
+ than sent the access token.
27
+ * Credentials are redacted from `debug_output`, which Net::HTTP would otherwise write in the clear: the
28
+ `Authorization` header of a request, the `Proxy-Authorization` header of a request sent through a proxy and of
29
+ the CONNECT that opens a TLS connection through one, the tokens and the client secret of a request to the token
30
+ endpoint and of its response, and the PIN or password of a command.
31
+ * A redirect to another scheme, host, or port is followed without the access token or the caller's headers, and one
32
+ that would send the body of the request again is not followed at all, so that a command cannot be replayed to a
33
+ host the client was not pointed at.
34
+ * A command is never sent a second time on its own: a request that asks a vehicle to do something is retried only
35
+ when a rate limiter turned it away before it reached the vehicle, and is sent on a connection of its own rather
36
+ than on one kept open, which the server may have closed.
37
+ * TLS certificates are verified, and no option turns that off: a host whose certificate OpenSSL does not already
38
+ trust, such as the vehicle command proxy, is reached by naming that certificate with `ca_file`.
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tesla
4
+ module API
5
+ # The alert commands, which draw attention to a vehicle
6
+ # @api public
7
+ module AlertEndpoints
8
+ # Honk the horn of a vehicle
9
+ #
10
+ # @api public
11
+ # @authenticated true
12
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
13
+ # @return [true] once the vehicle has carried out the command
14
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
15
+ # @example
16
+ # Tesla.honk_horn "5YJSA11111111111"
17
+ def honk_horn(vehicle)
18
+ command(vehicle, "honk_horn")
19
+ end
20
+
21
+ # Flash the headlights of a vehicle
22
+ #
23
+ # @api public
24
+ # @authenticated true
25
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
26
+ # @return [true] once the vehicle has carried out the command
27
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
28
+ # @example
29
+ # Tesla.flash_lights "5YJSA11111111111"
30
+ def flash_lights(vehicle)
31
+ command(vehicle, "flash_lights")
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tesla
4
+ module API
5
+ # The charging commands, which control how a vehicle charges
6
+ # @api public
7
+ module ChargingEndpoints
8
+ # Open the charge port of a vehicle, or unlock the cable in it
9
+ #
10
+ # @api public
11
+ # @authenticated true
12
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
13
+ # @return [true] once the vehicle has carried out the command
14
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
15
+ # @example
16
+ # Tesla.open_charge_port "5YJSA11111111111"
17
+ def open_charge_port(vehicle)
18
+ command(vehicle, "charge_port_door_open")
19
+ end
20
+
21
+ # Close the charge port of a vehicle
22
+ #
23
+ # @api public
24
+ # @authenticated true
25
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
26
+ # @return [true] once the vehicle has carried out the command
27
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
28
+ # @example
29
+ # Tesla.close_charge_port "5YJSA11111111111"
30
+ def close_charge_port(vehicle)
31
+ command(vehicle, "charge_port_door_close")
32
+ end
33
+
34
+ # Start charging a vehicle
35
+ #
36
+ # @api public
37
+ # @authenticated true
38
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
39
+ # @return [true] once the vehicle has carried out the command
40
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
41
+ # @example
42
+ # Tesla.start_charging "5YJSA11111111111"
43
+ def start_charging(vehicle)
44
+ command(vehicle, "charge_start")
45
+ end
46
+
47
+ # Stop charging a vehicle
48
+ #
49
+ # @api public
50
+ # @authenticated true
51
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
52
+ # @return [true] once the vehicle has carried out the command
53
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
54
+ # @example
55
+ # Tesla.stop_charging "5YJSA11111111111"
56
+ def stop_charging(vehicle)
57
+ command(vehicle, "charge_stop")
58
+ end
59
+
60
+ # Set the charge a vehicle stops charging at
61
+ #
62
+ # @api public
63
+ # @authenticated true
64
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
65
+ # @param percent [Integer] The charge to stop at, as a percentage.
66
+ # @return [true] once the vehicle has carried out the command
67
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
68
+ # @example
69
+ # Tesla.set_charge_limit "5YJSA11111111111", 80
70
+ def set_charge_limit(vehicle, percent)
71
+ command(vehicle, "set_charge_limit", percent:)
72
+ end
73
+
74
+ # Set the current a vehicle charges with
75
+ #
76
+ # @api public
77
+ # @authenticated true
78
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
79
+ # @param amps [Integer] The current in amps.
80
+ # @return [true] once the vehicle has carried out the command
81
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
82
+ # @example
83
+ # Tesla.set_charging_amps "5YJSA11111111111", 32
84
+ def set_charging_amps(vehicle, amps)
85
+ command(vehicle, "set_charging_amps", charging_amps: amps)
86
+ end
87
+
88
+ # Set the charge limit of a vehicle to the one Tesla recommends for daily driving
89
+ #
90
+ # @api public
91
+ # @authenticated true
92
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
93
+ # @return [true] once the vehicle has carried out the command
94
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
95
+ # @example
96
+ # Tesla.charge_to_standard_range "5YJSA11111111111"
97
+ def charge_to_standard_range(vehicle)
98
+ command(vehicle, "charge_standard")
99
+ end
100
+
101
+ # Set the charge limit of a vehicle to the whole of its battery
102
+ #
103
+ # @api public
104
+ # @authenticated true
105
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
106
+ # @return [true] once the vehicle has carried out the command
107
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
108
+ # @example
109
+ # Tesla.charge_to_max_range "5YJSA11111111111"
110
+ def charge_to_max_range(vehicle)
111
+ command(vehicle, "charge_max_range")
112
+ end
113
+ end
114
+ end
115
+ end