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.
- checksums.yaml +7 -0
- data/.yardopts +5 -0
- data/CHANGELOG.md +31 -0
- data/CONTRIBUTING.md +45 -0
- data/{LICENSE.txt → LICENSE.md} +1 -1
- data/README.md +366 -0
- data/SECURITY.md +38 -0
- data/lib/tesla/api/alert_endpoints.rb +35 -0
- data/lib/tesla/api/charging_endpoints.rb +115 -0
- data/lib/tesla/api/climate_endpoints.rb +180 -0
- data/lib/tesla/api/closure_endpoints.rb +142 -0
- data/lib/tesla/api/command_endpoints.rb +63 -0
- data/lib/tesla/api/media_endpoints.rb +114 -0
- data/lib/tesla/api/navigation_endpoints.rb +51 -0
- data/lib/tesla/api/oauth_endpoints.rb +118 -0
- data/lib/tesla/api/partner_endpoints.rb +62 -0
- data/lib/tesla/api/security_endpoints.rb +123 -0
- data/lib/tesla/api/software_endpoints.rb +38 -0
- data/lib/tesla/api/user_endpoints.rb +40 -0
- data/lib/tesla/api/vehicle_endpoints.rb +174 -0
- data/lib/tesla/api.rb +39 -0
- data/lib/tesla/authenticator.rb +33 -0
- data/lib/tesla/bearer_token_authenticator.rb +38 -0
- data/lib/tesla/certificate_options.rb +51 -0
- data/lib/tesla/charge_state.rb +111 -0
- data/lib/tesla/charging_site.rb +67 -0
- data/lib/tesla/client.rb +336 -0
- data/lib/tesla/client_credentials.rb +230 -0
- data/lib/tesla/climate_state.rb +116 -0
- data/lib/tesla/configuration.rb +483 -0
- data/lib/tesla/connection.rb +361 -0
- data/lib/tesla/connection_pool.rb +186 -0
- data/lib/tesla/deep_copy.rb +41 -0
- data/lib/tesla/drive_state.rb +80 -0
- data/lib/tesla/errors/bad_gateway.rb +9 -0
- data/lib/tesla/errors/bad_request.rb +9 -0
- data/lib/tesla/errors/client_error.rb +9 -0
- data/lib/tesla/errors/command_failed.rb +42 -0
- data/lib/tesla/errors/device_unexpected_response.rb +12 -0
- data/lib/tesla/errors/error.rb +7 -0
- data/lib/tesla/errors/forbidden.rb +14 -0
- data/lib/tesla/errors/gateway_timeout.rb +9 -0
- data/lib/tesla/errors/http_error.rb +89 -0
- data/lib/tesla/errors/internal_server_error.rb +9 -0
- data/lib/tesla/errors/invalid_response.rb +33 -0
- data/lib/tesla/errors/locked.rb +12 -0
- data/lib/tesla/errors/method_not_allowed.rb +9 -0
- data/lib/tesla/errors/misdirected_request.rb +13 -0
- data/lib/tesla/errors/network_error.rb +30 -0
- data/lib/tesla/errors/not_acceptable.rb +9 -0
- data/lib/tesla/errors/not_found.rb +9 -0
- data/lib/tesla/errors/oauth_error.rb +53 -0
- data/lib/tesla/errors/payment_required.rb +12 -0
- data/lib/tesla/errors/precondition_failed.rb +13 -0
- data/lib/tesla/errors/request_timeout.rb +13 -0
- data/lib/tesla/errors/server_error.rb +9 -0
- data/lib/tesla/errors/service_unavailable.rb +9 -0
- data/lib/tesla/errors/too_many_redirects.rb +9 -0
- data/lib/tesla/errors/too_many_requests.rb +9 -0
- data/lib/tesla/errors/unauthorized.rb +9 -0
- data/lib/tesla/errors/unavailable_for_legal_reasons.rb +12 -0
- data/lib/tesla/errors/unprocessable_content.rb +12 -0
- data/lib/tesla/errors/vehicle_asleep.rb +32 -0
- data/lib/tesla/gui_settings.rb +60 -0
- data/lib/tesla/idempotence.rb +30 -0
- data/lib/tesla/identifiers.rb +27 -0
- data/lib/tesla/json_parsing.rb +38 -0
- data/lib/tesla/nearby_charging_sites.rb +35 -0
- data/lib/tesla/path_escaping.rb +30 -0
- data/lib/tesla/redacted_output.rb +161 -0
- data/lib/tesla/redirect_handler.rb +196 -0
- data/lib/tesla/region.rb +27 -0
- data/lib/tesla/request_builder.rb +138 -0
- data/lib/tesla/resource.rb +403 -0
- data/lib/tesla/response_parser.rb +92 -0
- data/lib/tesla/retry_after.rb +34 -0
- data/lib/tesla/retry_handler.rb +252 -0
- data/lib/tesla/settings.rb +115 -0
- data/lib/tesla/url_validation.rb +131 -0
- data/lib/tesla/user.rb +36 -0
- data/lib/tesla/vehicle.rb +90 -0
- data/lib/tesla/vehicle_config.rb +95 -0
- data/lib/tesla/vehicle_data.rb +67 -0
- data/lib/tesla/vehicle_state.rb +125 -0
- data/lib/tesla/version.rb +6 -0
- data/lib/tesla.rb +139 -0
- data/sig/manifest.yaml +10 -0
- data/sig/tesla.rbs +989 -0
- metadata +134 -118
- data/.document +0 -5
- data/.rvmrc +0 -1
- data/Gemfile +0 -13
- data/Gemfile.lock +0 -20
- data/README.rdoc +0 -19
- data/Rakefile +0 -47
- data/test/helper.rb +0 -19
- 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
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/
|
data/{LICENSE.txt → LICENSE.md}
RENAMED
data/README.md
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
# Tesla
|
|
2
|
+
|
|
3
|
+
[](https://github.com/sferik/tesla-ruby/actions/workflows/test.yml)
|
|
4
|
+
[](https://github.com/sferik/tesla-ruby/actions/workflows/mutant.yml)
|
|
5
|
+
[](https://github.com/sferik/tesla-ruby/actions/workflows/lint.yml)
|
|
6
|
+
[](https://github.com/sferik/tesla-ruby/actions/workflows/steep.yml)
|
|
7
|
+
[](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
|