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
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tesla
4
+ module API
5
+ # The climate commands, which control the temperature of the cabin and the seats of a vehicle
6
+ # @api public
7
+ module ClimateEndpoints
8
+ # The seats of a vehicle, by the number the Fleet API knows each by
9
+ #
10
+ # @api public
11
+ SEATS = {front_left: 0, front_right: 1, rear_left: 2, rear_left_back: 3, rear_center: 4, rear_right: 5,
12
+ rear_right_back: 6, third_row_left: 7, third_row_right: 8}.freeze
13
+
14
+ # The Climate Keeper modes, by the number the Fleet API knows each by
15
+ #
16
+ # Dog Mode and Camp Mode keep the climate control on after the driver has left the vehicle.
17
+ #
18
+ # @api public
19
+ CLIMATE_KEEPER_MODES = {off: 0, on: 1, dog: 2, camp: 3}.freeze
20
+
21
+ # Turn on the climate control of a vehicle
22
+ #
23
+ # The cabin is heated or cooled to the temperature it is set to (see {#set_temperature}).
24
+ #
25
+ # @api public
26
+ # @authenticated true
27
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
28
+ # @return [true] once the vehicle has carried out the command
29
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
30
+ # @example
31
+ # Tesla.start_climate "5YJSA11111111111"
32
+ def start_climate(vehicle)
33
+ command(vehicle, "auto_conditioning_start")
34
+ end
35
+
36
+ # Turn off the climate control of a vehicle
37
+ #
38
+ # @api public
39
+ # @authenticated true
40
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
41
+ # @return [true] once the vehicle has carried out the command
42
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
43
+ # @example
44
+ # Tesla.stop_climate "5YJSA11111111111"
45
+ def stop_climate(vehicle)
46
+ command(vehicle, "auto_conditioning_stop")
47
+ end
48
+
49
+ # Set the temperature the climate control of a vehicle heats or cools the cabin to
50
+ #
51
+ # The temperatures are in degrees Celsius, whatever unit the vehicle displays. The climate control is not turned
52
+ # on (see {#start_climate}).
53
+ #
54
+ # @api public
55
+ # @authenticated true
56
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
57
+ # @param driver [Numeric] The temperature of the driver's side in degrees Celsius.
58
+ # @param passenger [Numeric] The temperature of the passenger's side in degrees Celsius, which defaults to
59
+ # the driver's.
60
+ # @return [true] once the vehicle has carried out the command
61
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
62
+ # @example
63
+ # Tesla.set_temperature "5YJSA11111111111", 21.5
64
+ # @example
65
+ # Tesla.set_temperature "5YJSA11111111111", 21.5, 19
66
+ def set_temperature(vehicle, driver, passenger = driver)
67
+ command(vehicle, "set_temps", driver_temp: driver, passenger_temp: passenger)
68
+ end
69
+
70
+ # Turn the maximum defrost of a vehicle on or off
71
+ #
72
+ # @api public
73
+ # @authenticated true
74
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
75
+ # @param on [Boolean] Whether to turn it on.
76
+ # @return [true] once the vehicle has carried out the command
77
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
78
+ # @example
79
+ # Tesla.set_max_defrost "5YJSA11111111111", on: true
80
+ def set_max_defrost(vehicle, on:)
81
+ command(vehicle, "set_preconditioning_max", on:)
82
+ end
83
+
84
+ # Set the heater of a seat of a vehicle
85
+ #
86
+ # @api public
87
+ # @authenticated true
88
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
89
+ # @param seat [Symbol, String] The seat, which is one of the seats {SEATS} names.
90
+ # @param level [Integer] The level of the heater, from 0, which is off, to 3.
91
+ # @return [true] once the vehicle has carried out the command
92
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
93
+ # @raise [ArgumentError] if the seat is not one the API defines
94
+ # @example
95
+ # Tesla.set_seat_heater "5YJSA11111111111", :front_left, 3
96
+ def set_seat_heater(vehicle, seat, level)
97
+ command(vehicle, "remote_seat_heater_request", seat_position: number_of(SEATS, seat, "seat"), level:)
98
+ end
99
+
100
+ # Set the cooler of a seat of a vehicle
101
+ #
102
+ # @api public
103
+ # @authenticated true
104
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
105
+ # @param seat [Symbol, String] The seat, which is one of the seats {SEATS} names.
106
+ # @param level [Integer] The level of the cooler, from 0, which is off, to 3.
107
+ # @return [true] once the vehicle has carried out the command
108
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
109
+ # @raise [ArgumentError] if the seat is not one the API defines
110
+ # @example
111
+ # Tesla.set_seat_cooler "5YJSA11111111111", :front_left, 3
112
+ def set_seat_cooler(vehicle, seat, level)
113
+ command(vehicle, "remote_seat_cooler_request", seat_position: number_of(SEATS, seat, "seat"), seat_cooler_level: level)
114
+ end
115
+
116
+ # Turn the heater of the steering wheel of a vehicle on or off
117
+ #
118
+ # @api public
119
+ # @authenticated true
120
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
121
+ # @param on [Boolean] Whether to turn it on.
122
+ # @return [true] once the vehicle has carried out the command
123
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
124
+ # @example
125
+ # Tesla.set_steering_wheel_heater "5YJSA11111111111", on: true
126
+ def set_steering_wheel_heater(vehicle, on:)
127
+ command(vehicle, "remote_steering_wheel_heater_request", on:)
128
+ end
129
+
130
+ # Set the Climate Keeper mode of a vehicle
131
+ #
132
+ # @api public
133
+ # @authenticated true
134
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
135
+ # @param mode [Symbol, String] The mode, which is one of the modes {CLIMATE_KEEPER_MODES} names.
136
+ # @return [true] once the vehicle has carried out the command
137
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
138
+ # @raise [ArgumentError] if the mode is not one the API defines
139
+ # @example
140
+ # Tesla.set_climate_keeper_mode "5YJSA11111111111", :dog
141
+ def set_climate_keeper_mode(vehicle, mode)
142
+ mode = number_of(CLIMATE_KEEPER_MODES, mode, "climate keeper mode")
143
+ command(vehicle, "set_climate_keeper_mode", climate_keeper_mode: mode)
144
+ end
145
+
146
+ # Turn the Bioweapon Defense Mode of a vehicle on or off
147
+ #
148
+ # @api public
149
+ # @authenticated true
150
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
151
+ # @param on [Boolean] Whether to turn it on.
152
+ # @param manual_override [Boolean] Whether to turn it on although the vehicle would not, such as with a low
153
+ # battery.
154
+ # @return [true] once the vehicle has carried out the command
155
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
156
+ # @example
157
+ # Tesla.set_bioweapon_mode "5YJSA11111111111", on: true
158
+ def set_bioweapon_mode(vehicle, on:, manual_override: false)
159
+ command(vehicle, "set_bioweapon_mode", on:, manual_override:)
160
+ end
161
+
162
+ # Turn the Cabin Overheat Protection of a vehicle on or off
163
+ #
164
+ # @api public
165
+ # @authenticated true
166
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
167
+ # @param on [Boolean] Whether to turn it on.
168
+ # @param fan_only [Boolean] Whether to cool the cabin with the fan alone, without the air conditioning.
169
+ # @return [true] once the vehicle has carried out the command
170
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
171
+ # @example
172
+ # Tesla.set_cabin_overheat_protection "5YJSA11111111111", on: true
173
+ # @example
174
+ # Tesla.set_cabin_overheat_protection "5YJSA11111111111", on: true, fan_only: true
175
+ def set_cabin_overheat_protection(vehicle, on:, fan_only: false)
176
+ command(vehicle, "set_cabin_overheat_protection", on:, fan_only:)
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tesla
4
+ module API
5
+ # The closure commands, which lock, open, and close the doors, trunks, windows, and sunroof of a vehicle
6
+ # @api public
7
+ module ClosureEndpoints
8
+ # Lock the doors 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.lock_doors "5YJSA11111111111"
17
+ def lock_doors(vehicle)
18
+ command(vehicle, "door_lock")
19
+ end
20
+
21
+ # Unlock the doors 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.unlock_doors "5YJSA11111111111"
30
+ def unlock_doors(vehicle)
31
+ command(vehicle, "door_unlock")
32
+ end
33
+
34
+ # Open the rear trunk of a vehicle, or close it
35
+ #
36
+ # A vehicle with a powered liftgate closes a trunk that is open, so this is the command that pops the trunk and
37
+ # the one that closes it again. A trunk without one is opened and stays open.
38
+ #
39
+ # @api public
40
+ # @authenticated true
41
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
42
+ # @return [true] once the vehicle has carried out the command
43
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
44
+ # @example
45
+ # Tesla.actuate_trunk "5YJSA11111111111"
46
+ def actuate_trunk(vehicle)
47
+ command(vehicle, "actuate_trunk", which_trunk: "rear")
48
+ end
49
+
50
+ # Open the front trunk of a vehicle
51
+ #
52
+ # The front trunk is closed by hand on the vehicles that do not close it themselves.
53
+ #
54
+ # @api public
55
+ # @authenticated true
56
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
57
+ # @return [true] once the vehicle has carried out the command
58
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
59
+ # @example
60
+ # Tesla.open_frunk "5YJSA11111111111"
61
+ def open_frunk(vehicle)
62
+ command(vehicle, "actuate_trunk", which_trunk: "front")
63
+ end
64
+
65
+ # Vent the windows of a vehicle
66
+ #
67
+ # @api public
68
+ # @authenticated true
69
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
70
+ # @return [true] once the vehicle has carried out the command
71
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
72
+ # @example
73
+ # Tesla.vent_windows "5YJSA11111111111"
74
+ def vent_windows(vehicle)
75
+ command(vehicle, "window_control", command: "vent", lat: 0, lon: 0)
76
+ end
77
+
78
+ # Close the windows of a vehicle
79
+ #
80
+ # A vehicle that takes the command over the Fleet API rather than through the vehicle command proxy closes its
81
+ # windows only for a user who is near it, so the position of the user is sent along.
82
+ #
83
+ # @api public
84
+ # @authenticated true
85
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
86
+ # @param latitude [Numeric] The latitude of the user.
87
+ # @param longitude [Numeric] The longitude of the user.
88
+ # @return [true] once the vehicle has carried out the command
89
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
90
+ # @example
91
+ # Tesla.close_windows "5YJSA11111111111"
92
+ # @example
93
+ # Tesla.close_windows "5YJSA11111111111", latitude: 37.4929, longitude: -121.9453
94
+ def close_windows(vehicle, latitude: 0, longitude: 0)
95
+ command(vehicle, "window_control", command: "close", lat: latitude, lon: longitude)
96
+ end
97
+
98
+ # Vent the sunroof of a vehicle
99
+ #
100
+ # @api public
101
+ # @authenticated true
102
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
103
+ # @return [true] once the vehicle has carried out the command
104
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
105
+ # @example
106
+ # Tesla.vent_sunroof "5YJSA11111111111"
107
+ def vent_sunroof(vehicle)
108
+ command(vehicle, "sun_roof_control", state: "vent")
109
+ end
110
+
111
+ # Close the sunroof of a vehicle
112
+ #
113
+ # @api public
114
+ # @authenticated true
115
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
116
+ # @return [true] once the vehicle has carried out the command
117
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
118
+ # @example
119
+ # Tesla.close_sunroof "5YJSA11111111111"
120
+ def close_sunroof(vehicle)
121
+ command(vehicle, "sun_roof_control", state: "close")
122
+ end
123
+
124
+ # Open or close the garage door a vehicle is paired with over HomeLink
125
+ #
126
+ # The vehicle triggers HomeLink only for a user who is near it, so the position of the user is sent along.
127
+ #
128
+ # @api public
129
+ # @authenticated true
130
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
131
+ # @param latitude [Numeric] The latitude of the user.
132
+ # @param longitude [Numeric] The longitude of the user.
133
+ # @return [true] once the vehicle has carried out the command
134
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
135
+ # @example
136
+ # Tesla.trigger_homelink "5YJSA11111111111", latitude: 37.4929, longitude: -121.9453
137
+ def trigger_homelink(vehicle, latitude:, longitude:)
138
+ command(vehicle, "trigger_homelink", lat: latitude, lon: longitude)
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../errors/command_failed"
4
+ require_relative "../identifiers"
5
+ require_relative "../json_parsing"
6
+ require_relative "../path_escaping"
7
+
8
+ module Tesla
9
+ module API
10
+ # The command endpoint, which every command of the other mixins is sent through
11
+ #
12
+ # A vehicle has to be online to take a command (see {VehicleEndpoints#wake_up}), and most vehicles take only
13
+ # commands signed with the Tesla Vehicle Command Protocol, which the vehicle command proxy does: a client whose
14
+ # `host` is the proxy sends its commands through it.
15
+ #
16
+ # @api public
17
+ module CommandEndpoints
18
+ include Identifiers
19
+ include JSONParsing
20
+ include PathEscaping
21
+
22
+ # Send a command to a vehicle
23
+ #
24
+ # The commands of the other mixins are sent with this, which also sends the commands the library has no
25
+ # method for.
26
+ #
27
+ # @api public
28
+ # @authenticated true
29
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
30
+ # @param name [String, Symbol] The name of the command, as the Fleet API names it.
31
+ # @param params [Hash] The parameters of the command.
32
+ # @return [true] once the vehicle has carried out the command
33
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
34
+ # @raise [RequestTimeout] if the vehicle is asleep or offline
35
+ # @raise [InvalidResponse] if the response does not say whether the vehicle carried out the command
36
+ # @example
37
+ # Tesla.command "5YJSA11111111111", :set_vehicle_name, vehicle_name: "Nikola 2.0"
38
+ def command(vehicle, name, **params)
39
+ body = post("/api/1/vehicles/#{escape(tag_of(vehicle))}/command/#{escape(name)}", params)
40
+ result, reason = parse_response(body) { |response| [response.fetch("result"), response["reason"]] }
41
+ raise CommandFailed.new(command: name.to_s, reason:) unless result
42
+
43
+ true
44
+ end
45
+
46
+ private
47
+
48
+ # Look up the number the Fleet API knows a named option by
49
+ #
50
+ # @api private
51
+ # @param options [Hash{Symbol => Integer}] the numbers of the options, by name
52
+ # @param name [Symbol, String] the name of the option
53
+ # @param kind [String] what the option is, which the message names
54
+ # @return [Integer] the number
55
+ # @raise [ArgumentError] if the name is not one of the options
56
+ def number_of(options, name, kind)
57
+ options.fetch(name.to_sym) do
58
+ raise ArgumentError, "Unknown #{kind}: #{name}. The #{kind}s the API defines are: #{options.keys.join(", ")}"
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tesla
4
+ module API
5
+ # The media commands, which control what a vehicle plays
6
+ # @api public
7
+ module MediaEndpoints
8
+ # Play or pause the media 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.toggle_playback "5YJSA11111111111"
17
+ def toggle_playback(vehicle)
18
+ command(vehicle, "media_toggle_playback")
19
+ end
20
+
21
+ # Skip to the next track of the media 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.next_track "5YJSA11111111111"
30
+ def next_track(vehicle)
31
+ command(vehicle, "media_next_track")
32
+ end
33
+
34
+ # Skip to the previous track of the media of 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.previous_track "5YJSA11111111111"
43
+ def previous_track(vehicle)
44
+ command(vehicle, "media_prev_track")
45
+ end
46
+
47
+ # Skip to the next favorite of the media of 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.next_favorite "5YJSA11111111111"
56
+ def next_favorite(vehicle)
57
+ command(vehicle, "media_next_fav")
58
+ end
59
+
60
+ # Skip to the previous favorite of the media of a vehicle
61
+ #
62
+ # @api public
63
+ # @authenticated true
64
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
65
+ # @return [true] once the vehicle has carried out the command
66
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
67
+ # @example
68
+ # Tesla.previous_favorite "5YJSA11111111111"
69
+ def previous_favorite(vehicle)
70
+ command(vehicle, "media_prev_fav")
71
+ end
72
+
73
+ # Turn up the volume of the media of a vehicle
74
+ #
75
+ # @api public
76
+ # @authenticated true
77
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
78
+ # @return [true] once the vehicle has carried out the command
79
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
80
+ # @example
81
+ # Tesla.volume_up "5YJSA11111111111"
82
+ def volume_up(vehicle)
83
+ command(vehicle, "media_volume_up")
84
+ end
85
+
86
+ # Turn down the volume of the media of a vehicle
87
+ #
88
+ # @api public
89
+ # @authenticated true
90
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
91
+ # @return [true] once the vehicle has carried out the command
92
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
93
+ # @example
94
+ # Tesla.volume_down "5YJSA11111111111"
95
+ def volume_down(vehicle)
96
+ command(vehicle, "media_volume_down")
97
+ end
98
+
99
+ # Set the volume of the media of a vehicle
100
+ #
101
+ # @api public
102
+ # @authenticated true
103
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
104
+ # @param volume [Numeric] The volume, from 0 to 11.
105
+ # @return [true] once the vehicle has carried out the command
106
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
107
+ # @example
108
+ # Tesla.set_volume "5YJSA11111111111", 4.5
109
+ def set_volume(vehicle, volume)
110
+ command(vehicle, "adjust_volume", volume:)
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tesla
4
+ module API
5
+ # The navigation commands, which send a destination to a vehicle
6
+ # @api public
7
+ module NavigationEndpoints
8
+ # The type of a navigation request, which is the one the Tesla app shares a destination with
9
+ NAVIGATION_REQUEST_TYPE = "share_ext_content_raw"
10
+ # The key of the destination of a navigation request
11
+ NAVIGATION_REQUEST_TEXT = "android.intent.extra.TEXT"
12
+ private_constant :NAVIGATION_REQUEST_TYPE, :NAVIGATION_REQUEST_TEXT
13
+
14
+ # Send a destination to the navigation of a vehicle
15
+ #
16
+ # The vehicle searches for the destination as it would for one typed into its navigation, so an address and
17
+ # the name of a place both work.
18
+ #
19
+ # @api public
20
+ # @authenticated true
21
+ # @param vehicle [String, Integer, Vehicle] The VIN or ID of a vehicle, or a vehicle.
22
+ # @param destination [String] The address or the name of the place to navigate to.
23
+ # @param locale [String] The locale the destination is written in.
24
+ # @return [true] once the vehicle has carried out the command
25
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
26
+ # @example
27
+ # Tesla.navigate_to "5YJSA11111111111", "3500 Deer Creek Road, Palo Alto, CA"
28
+ def navigate_to(vehicle, destination, locale: "en-US")
29
+ command(vehicle, "navigation_request", type: NAVIGATION_REQUEST_TYPE, locale:,
30
+ timestamp_ms: Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond),
31
+ value: {NAVIGATION_REQUEST_TEXT => destination})
32
+ end
33
+
34
+ # Send a position to the navigation of 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
+ # @param latitude [Numeric] The latitude of the destination.
40
+ # @param longitude [Numeric] The longitude of the destination.
41
+ # @param order [Integer] The place of the destination among the ones the vehicle is navigating to.
42
+ # @return [true] once the vehicle has carried out the command
43
+ # @raise [CommandFailed] if the vehicle answers that it did not carry out the command
44
+ # @example
45
+ # Tesla.navigate_to_coordinates "5YJSA11111111111", 37.3947, -122.1503
46
+ def navigate_to_coordinates(vehicle, latitude, longitude, order: 1)
47
+ command(vehicle, "navigation_gps_request", lat: latitude, lon: longitude, order:)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Tesla
4
+ module API
5
+ # The OAuth 2.0 endpoints of Tesla's authorization server, which issue the tokens requests are authorized with
6
+ #
7
+ # An application is created at developer.tesla.com, which issues its client ID and client secret, and is
8
+ # registered in a region once (see {PartnerEndpoints#register_partner}). A user is then sent to
9
+ # {#authorization_url}, and comes back to the redirect URI with a code that {#exchange_code} exchanges for an
10
+ # access token and a refresh token. The access token lasts eight hours, and the client asks for another with
11
+ # the refresh token when it runs out (see {#refresh_access_token}).
12
+ #
13
+ # The requests are built by the simple_oauth gem, and the tokens are its `SimpleOAuth::OAuth2::Token`.
14
+ #
15
+ # @api public
16
+ module OAuthEndpoints
17
+ # The scopes a user is asked for by default, which are the ones the endpoints of the library need
18
+ #
19
+ # @api public
20
+ SCOPES = %w[openid offline_access user_data vehicle_device_data vehicle_location vehicle_cmds
21
+ vehicle_charging_cmds].freeze
22
+
23
+ # The scopes a partner token is asked for by default
24
+ #
25
+ # @api public
26
+ PARTNER_SCOPES = %w[openid vehicle_device_data vehicle_cmds vehicle_charging_cmds].freeze
27
+
28
+ # The URL a user is sent to in order to authorize the application
29
+ #
30
+ # The state is a value of your own that the authorization server hands back with the code, to be compared
31
+ # with the one sent (see `SimpleOAuth::OAuth2::AuthorizationResponse.parse`).
32
+ #
33
+ # @api public
34
+ # @authenticated false
35
+ # @param state [String] A random value that ties the response of the authorization server to this request.
36
+ # @param pkce [SimpleOAuth::OAuth2::PKCE, nil] A PKCE challenge, whose verifier is given to {#exchange_code}.
37
+ # @param scope [Array<String>, String] The scopes to ask the user for.
38
+ # @param params [Hash] Other parameters of the authorization request, such as `prompt_missing_scopes`.
39
+ # @return [String] the URL
40
+ # @raise [ArgumentError] if the client has no client ID, or the state is empty
41
+ # @example
42
+ # Tesla.authorization_url(state: SecureRandom.hex)
43
+ def authorization_url(state:, pkce: nil, scope: SCOPES, **params)
44
+ oauth_client.authorization_url(redirect_uri: required_redirect_uri, pkce:, state:, scope:, params:)
45
+ end
46
+
47
+ # Exchange an authorization code for an access token and a refresh token
48
+ #
49
+ # The code is the one the authorization server sent the user back to the redirect URI with.
50
+ #
51
+ # The client keeps the tokens, and authorizes its requests with them from then on.
52
+ #
53
+ # @api public
54
+ # @authenticated false
55
+ # @param code [String] The code the authorization server sent the user back to the redirect URI with.
56
+ # @param code_verifier [String, nil] The verifier of the PKCE challenge the authorization URL was built with.
57
+ # @return [SimpleOAuth::OAuth2::Token] the token
58
+ # @raise [OAuthError] if the token endpoint turns the request away
59
+ # @raise [ArgumentError] if the client has no client ID or no redirect URI
60
+ # @example
61
+ # token = Tesla.exchange_code(params[:code])
62
+ # token.refresh_token
63
+ def exchange_code(code, code_verifier: nil)
64
+ request = oauth_client.authorization_code_request(code:, redirect_uri: required_redirect_uri, code_verifier:,
65
+ params: {audience:})
66
+ token = request_token(request)
67
+ exclusively { store_token(token) }
68
+ end
69
+
70
+ # Ask for a new access token with the refresh token
71
+ #
72
+ # The client does this itself when the Fleet API answers that its access token is no longer good, so this is
73
+ # for refreshing ahead of time. The client keeps the tokens it is answered with, the refresh token among
74
+ # them, since a refresh token is used once.
75
+ #
76
+ # @api public
77
+ # @authenticated false
78
+ # @return [SimpleOAuth::OAuth2::Token] the token
79
+ # @raise [OAuthError] if the token endpoint turns the request away
80
+ # @raise [ArgumentError] if the client has no client ID or no refresh token
81
+ # @example
82
+ # token = Tesla.refresh_access_token
83
+ # token.expires_at
84
+ def refresh_access_token
85
+ exclusively { refresh }
86
+ end
87
+
88
+ # Ask for a partner token
89
+ #
90
+ # A partner token authorizes the endpoints that act for the application rather than for a user.
91
+ #
92
+ # The client does not keep the token, since its own requests are authorized by a user.
93
+ #
94
+ # @api public
95
+ # @authenticated false
96
+ # @param scope [Array<String>, String] The scopes to ask for.
97
+ # @return [SimpleOAuth::OAuth2::Token] the token
98
+ # @raise [OAuthError] if the token endpoint turns the request away
99
+ # @raise [ArgumentError] if the client has no client ID or no client secret
100
+ # @example
101
+ # Tesla.partner_token.access_token
102
+ def partner_token(scope: PARTNER_SCOPES)
103
+ request_token(oauth_client.client_credentials_request(scope:, params: {audience:}))
104
+ end
105
+
106
+ private
107
+
108
+ # The redirect URI of the client, which has to have one
109
+ #
110
+ # @api private
111
+ # @return [String] the redirect URI
112
+ # @raise [ArgumentError] if the client has no redirect URI
113
+ def required_redirect_uri
114
+ redirect_uri || raise(ArgumentError, "A redirect_uri is required to authorize a user")
115
+ end
116
+ end
117
+ end
118
+ end