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,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "resource"
4
+
5
+ module Tesla
6
+ # The state of the battery and the charging of a vehicle
7
+ # @api public
8
+ class ChargeState < Resource
9
+ inspect_with :battery_level, :charging_state
10
+
11
+ # @!method battery_level
12
+ # The charge of the battery, as a percentage
13
+ # @api public
14
+ # @return [Integer, nil] the charge of the battery
15
+ # @example
16
+ # charge_state.battery_level
17
+ attribute :battery_level
18
+
19
+ # @!method usable_battery_level
20
+ # The charge of the battery that can be used, as a percentage
21
+ #
22
+ # It is lower than the charge of the battery when the battery is cold.
23
+ #
24
+ # @api public
25
+ # @return [Integer, nil] the charge of the battery that can be used
26
+ # @example
27
+ # charge_state.usable_battery_level
28
+ attribute :usable_battery_level
29
+
30
+ # @!method battery_range
31
+ # The rated range of the battery in miles, whatever unit the vehicle displays
32
+ # @api public
33
+ # @return [Float, nil] the rated range of the battery
34
+ # @example
35
+ # charge_state.battery_range
36
+ attribute :battery_range
37
+
38
+ # @!method charge_limit
39
+ # The charge the vehicle stops charging at, as a percentage
40
+ # @api public
41
+ # @return [Integer, nil] the charge the vehicle stops charging at
42
+ # @example
43
+ # charge_state.charge_limit
44
+ attribute :charge_limit, :charge_limit_soc
45
+
46
+ # @!method charging_state
47
+ # The state of charging, such as "Charging" or "Disconnected"
48
+ # @api public
49
+ # @return [String, nil] the state of charging
50
+ # @example
51
+ # charge_state.charging_state
52
+ attribute :charging_state
53
+
54
+ # @!method charge_amps
55
+ # The current the vehicle asks the charger for, in amps
56
+ # @api public
57
+ # @return [Integer, nil] the current the vehicle asks the charger for
58
+ # @example
59
+ # charge_state.charge_amps
60
+ attribute :charge_amps
61
+
62
+ # @!method charge_rate
63
+ # The rate of charging in miles of range per hour
64
+ # @api public
65
+ # @return [Float, nil] the rate of charging
66
+ # @example
67
+ # charge_state.charge_rate
68
+ attribute :charge_rate
69
+
70
+ # @!method charger_power
71
+ # The power of the charger in kilowatts
72
+ # @api public
73
+ # @return [Integer, nil] the power of the charger
74
+ # @example
75
+ # charge_state.charger_power
76
+ attribute :charger_power
77
+
78
+ # @!method minutes_to_full_charge
79
+ # The minutes until the vehicle reaches its charge limit
80
+ # @api public
81
+ # @return [Integer, nil] the minutes until the vehicle reaches its charge limit
82
+ # @example
83
+ # charge_state.minutes_to_full_charge
84
+ attribute :minutes_to_full_charge
85
+
86
+ # @!method charge_port_latch
87
+ # The state of the latch of the charge port, such as "Engaged"
88
+ # @api public
89
+ # @return [String, nil] the state of the latch of the charge port
90
+ # @example
91
+ # charge_state.charge_port_latch
92
+ attribute :charge_port_latch
93
+
94
+ # @!method charge_port_door_open?
95
+ # Whether the door of the charge port is open
96
+ # @api public
97
+ # @return [Boolean] whether the door of the charge port is open
98
+ # @example
99
+ # charge_state.charge_port_door_open?
100
+ predicate :charge_port_door_open
101
+
102
+ # @!method timestamp
103
+ # The time the vehicle reported the state
104
+ # @api public
105
+ # @return [Time, nil] the time the vehicle reported the state
106
+ # @raise [InvalidResponse] if the timestamp is not a number of milliseconds
107
+ # @example
108
+ # charge_state.timestamp
109
+ time_attribute :timestamp
110
+ end
111
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "resource"
4
+
5
+ module Tesla
6
+ # A charging site near a vehicle
7
+ # @api public
8
+ class ChargingSite < Resource
9
+ inspect_with :name, :type
10
+
11
+ # @!method name
12
+ # The name of the site
13
+ # @api public
14
+ # @return [String, nil] the name of the site
15
+ # @example
16
+ # charging_site.name
17
+ attribute :name
18
+
19
+ # @!method type
20
+ # The type of the site, "supercharger" or "destination"
21
+ # @api public
22
+ # @return [String, nil] the type of the site
23
+ # @example
24
+ # charging_site.type
25
+ attribute :type
26
+
27
+ # @!method distance_miles
28
+ # The distance from the vehicle to the site in miles
29
+ # @api public
30
+ # @return [Float, nil] the distance from the vehicle to the site
31
+ # @example
32
+ # charging_site.distance_miles
33
+ attribute :distance_miles
34
+
35
+ # @!method available_stalls
36
+ # The number of stalls that are free, which a Supercharger reports
37
+ # @api public
38
+ # @return [Integer, nil] the number of stalls that are free
39
+ # @example
40
+ # charging_site.available_stalls
41
+ attribute :available_stalls
42
+
43
+ # @!method total_stalls
44
+ # The number of stalls the site has, which a Supercharger reports
45
+ # @api public
46
+ # @return [Integer, nil] the number of stalls the site has
47
+ # @example
48
+ # charging_site.total_stalls
49
+ attribute :total_stalls
50
+
51
+ # @!method location
52
+ # The position of the site, as its "lat" and "long"
53
+ # @api public
54
+ # @return [Hash{String => Float}, nil] the position of the site
55
+ # @example
56
+ # charging_site.location
57
+ attribute :location
58
+
59
+ # @!method site_closed?
60
+ # Whether the site is closed
61
+ # @api public
62
+ # @return [Boolean] whether the site is closed
63
+ # @example
64
+ # charging_site.site_closed?
65
+ predicate :site_closed
66
+ end
67
+ end
@@ -0,0 +1,336 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require "uri"
5
+ require_relative "api"
6
+ require_relative "client_credentials"
7
+ require_relative "configuration"
8
+ require_relative "connection"
9
+ require_relative "idempotence"
10
+ require_relative "redirect_handler"
11
+ require_relative "request_builder"
12
+ require_relative "response_parser"
13
+ require_relative "retry_handler"
14
+ require_relative "url_validation"
15
+
16
+ module Tesla
17
+ # A client for the Tesla Fleet API
18
+ # @api public
19
+ class Client
20
+ extend Forwardable
21
+ include API
22
+ include ClientCredentials
23
+ include Idempotence
24
+ include URLValidation
25
+
26
+ # The status the Fleet API answers a request with when its access token is missing, expired, or revoked
27
+ UNAUTHORIZED_STATUS = "401"
28
+ private_constant :UNAUTHORIZED_STATUS
29
+
30
+ # The host for API requests
31
+ # @api public
32
+ # @return [String] the host for API requests, including scheme
33
+ # @example Get the host
34
+ # client.host
35
+ attr_reader :host
36
+
37
+ def_delegators :@connection, :open_timeout, :read_timeout, :write_timeout, :keep_alive_timeout, :proxy_url, :debug_output
38
+ def_delegators :@connection, :open_timeout=, :read_timeout=, :write_timeout=, :keep_alive_timeout=, :proxy_url=, :debug_output=
39
+ def_delegators :@connection, :ca_file, :ca_file=
40
+ def_delegators :@redirect_handler, :max_redirects, :max_redirects=
41
+ def_delegators :@retry_handler, :max_retries, :max_retry_delay
42
+ def_delegators :@retry_handler, :max_retries=, :max_retry_delay=
43
+ def_delegators :@request_builder, :user_agent
44
+ def_delegators :@request_builder, :user_agent=
45
+
46
+ # The methods of Forwardable, which the class delegates with rather than offers, so that `Client.delegate` and
47
+ # the rest are not mistaken for methods of the API
48
+ private_class_method(*Forwardable.instance_methods)
49
+
50
+ # Build a client, and close it once a block is done with it
51
+ #
52
+ # Without a block the client is returned, as it is from any other constructor. With one, the client is given to
53
+ # the block and its connections are closed once the block returns or raises, as `Net::HTTP.start` closes the
54
+ # connection it opened, and what the block returns is returned. The client can still be used afterwards, since
55
+ # a closed client opens its connections again as it needs them.
56
+ #
57
+ # The options are handed to {#initialize}, which is what declares them, so they are collected here and named
58
+ # there.
59
+ #
60
+ # @api public
61
+ # @param options [Hash] the options of {#initialize}
62
+ # @yield [client] the client, which is closed once the block is done with it
63
+ # @return [Client, Object] the client, or what the block returned
64
+ # @example Close the connections of a client once a series of requests is done
65
+ # vehicles = Tesla::Client.new(access_token: "eyJhbGciOiJSUzI1NiIs") do |client|
66
+ # client.vehicles
67
+ # end
68
+ def self.new(**options) # steep:ignore DifferentMethodParameterKind
69
+ client = super
70
+
71
+ return client unless block_given?
72
+
73
+ begin
74
+ yield client
75
+ ensure
76
+ client.close
77
+ end
78
+ end
79
+
80
+ # Initialize a new Tesla Fleet API client
81
+ #
82
+ # Every option defaults to the global configuration (see {Tesla.configure}).
83
+ #
84
+ # A client with an access token alone sends it until it expires. One that also has a refresh token and a client
85
+ # ID asks for a new access token when the Fleet API answers that the one it sent is no longer good, and one
86
+ # with those and no access token asks for one before its first request.
87
+ #
88
+ # @api public
89
+ # @param host [String] the host for API requests, including scheme
90
+ # @param access_token [String, nil] the access token requests are authorized with
91
+ # @param refresh_token [String, nil] the refresh token a new access token is asked for with
92
+ # @param client_id [String, nil] the client ID of the application
93
+ # @param client_secret [String, nil] the client secret of the application
94
+ # @param redirect_uri [String, nil] the URL the authorization server sends a user back to
95
+ # @param audience [String, nil] the host of the Fleet API tokens are issued for, or nil for the host
96
+ # @param authorization_endpoint [String] the URL a user is sent to in order to authorize the application
97
+ # @param token_endpoint [String] the URL tokens are issued and refreshed at
98
+ # @param on_token_refresh [#call, nil] what is called with the token each time the access token is refreshed
99
+ # @param user_agent [String] the 'User-Agent' HTTP header sent with requests
100
+ # @param open_timeout [Numeric] the timeout for opening connections in seconds
101
+ # @param read_timeout [Numeric] the timeout for reading responses in seconds
102
+ # @param write_timeout [Numeric] the timeout for writing requests in seconds
103
+ # @param debug_output [IO, nil] the IO object for debug output
104
+ # @param proxy_url [String, nil] the proxy URL for requests
105
+ # @param keep_alive_timeout [Numeric] the seconds an idle connection is kept open for another request
106
+ # @param max_redirects [Integer] the maximum number of redirects to follow
107
+ # @param ca_file [String, nil] the path of a file of certificates TLS is verified with
108
+ # @param max_retries [Integer] the number of times a request that was turned away is sent again
109
+ # @param max_retry_delay [Numeric] the longest a request waits before it is sent again, in seconds
110
+ # @return [Client] a new client instance
111
+ # @example Create a client with an access token
112
+ # client = Tesla::Client.new(access_token: "eyJhbGciOiJSUzI1NiIs")
113
+ # @example Create a client that refreshes its access token
114
+ # client = Tesla::Client.new(client_id: "81527cff06843c8634fdc09e8ac0abef",
115
+ # refresh_token: "NA_9d0b4bfd0c4c3a6e")
116
+ # @example Create a client that sends requests through the vehicle command proxy
117
+ # client = Tesla::Client.new(host: "https://localhost:4443", ca_file: "config/tls-cert.pem",
118
+ # audience: Tesla::Configuration::NORTH_AMERICA_HOST)
119
+ # @raise [ArgumentError] if the host is not an HTTP or HTTPS URL, or the certificate path names nothing
120
+ def initialize(host: Tesla.host, access_token: Tesla.access_token, refresh_token: Tesla.refresh_token,
121
+ client_id: Tesla.client_id, client_secret: Tesla.client_secret, redirect_uri: Tesla.redirect_uri,
122
+ audience: Tesla.audience, authorization_endpoint: Tesla.authorization_endpoint, token_endpoint: Tesla.token_endpoint,
123
+ on_token_refresh: Tesla.on_token_refresh,
124
+ user_agent: Tesla.user_agent,
125
+ open_timeout: Tesla.open_timeout,
126
+ read_timeout: Tesla.read_timeout,
127
+ write_timeout: Tesla.write_timeout,
128
+ debug_output: Tesla.debug_output,
129
+ proxy_url: Tesla.proxy_url,
130
+ keep_alive_timeout: Tesla.keep_alive_timeout,
131
+ max_redirects: Tesla.max_redirects,
132
+ ca_file: Tesla.ca_file,
133
+ max_retries: Tesla.max_retries,
134
+ max_retry_delay: Tesla.max_retry_delay)
135
+ @host = validate_host(host)
136
+ @connection = Connection.new(open_timeout:, read_timeout:, write_timeout:, debug_output:, proxy_url:,
137
+ keep_alive_timeout:, ca_file:)
138
+ @request_builder = RequestBuilder.new(user_agent:)
139
+ @redirect_handler = RedirectHandler.new(connection: @connection, request_builder: @request_builder, max_redirects:)
140
+ @retry_handler = RetryHandler.new(max_retries:, max_retry_delay:)
141
+ @response_parser = ResponseParser.new
142
+ initialize_credentials(access_token:, refresh_token:, client_id:, client_secret:, redirect_uri:, audience:,
143
+ authorization_endpoint:, token_endpoint:, on_token_refresh:)
144
+ end
145
+
146
+ # Set the host for API requests
147
+ #
148
+ # @api public
149
+ # @param host [String] the host for API requests, including scheme
150
+ # @return [void]
151
+ # @raise [ArgumentError] if the host is not an HTTP or HTTPS URL, in which case the host is left as it was
152
+ # @example Set the host
153
+ # client.host = Tesla::Configuration::EUROPE_HOST
154
+ def host=(host)
155
+ @host = validate_host(host)
156
+ end
157
+
158
+ # Summarize the client for the console
159
+ #
160
+ # @api public
161
+ # @return [String] the summary, which includes the host but never credentials
162
+ # @example Inspect a client
163
+ # client.inspect # => #<Tesla::Client host="https://fleet-api.prd.na.vn.cloud.tesla.com">
164
+ def inspect
165
+ "#<#{self.class} host=#{host.inspect}>"
166
+ end
167
+
168
+ # Close the connections the client keeps open for the next request
169
+ #
170
+ # The connections are opened again as they are needed, so requests can still be made afterwards.
171
+ #
172
+ # @api public
173
+ # @return [Client] the client
174
+ # @example Close the connections a client keeps open
175
+ # client.close
176
+ def close
177
+ @connection.close
178
+ self
179
+ end
180
+
181
+ # Perform a GET request to the Tesla Fleet API
182
+ #
183
+ # @api public
184
+ # @param path [String] the request path
185
+ # @param params [Hash] the query parameters
186
+ # @param headers [Hash{String => String}] the headers to send with the request, which replace the ones the
187
+ # client would send, the 'Authorization' header of its access token among them
188
+ # @return [String] the response body
189
+ # @raise [HTTPError] if the response is not successful
190
+ # @raise [ArgumentError] if the path is a URL of an origin other than the host
191
+ # @example Get the vehicles of the account
192
+ # client.get("/api/1/vehicles")
193
+ # @example Send a header of your own
194
+ # client.get("/api/1/vehicles", headers: {"Accept" => "application/json"})
195
+ def get(path, params = {}, headers: {})
196
+ execute_request(:get, path, params:, headers:)
197
+ end
198
+
199
+ # Perform a DELETE request to the Tesla Fleet API
200
+ #
201
+ # @api public
202
+ # @param path [String] the request path
203
+ # @param params [Hash] the query parameters
204
+ # @param headers [Hash{String => String}] the headers to send with the request, which replace the ones the
205
+ # client would send, the 'Authorization' header of its access token among them
206
+ # @return [String] the response body
207
+ # @raise [HTTPError] if the response is not successful
208
+ # @raise [ArgumentError] if the path is a URL of an origin other than the host
209
+ # @example Remove a driver from a vehicle
210
+ # client.delete("/api/1/vehicles/5YJSA11111111111/drivers", {share_user_id: 800001})
211
+ def delete(path, params = {}, headers: {})
212
+ execute_request(:delete, path, params:, headers:)
213
+ end
214
+
215
+ # Perform a POST request to the Tesla Fleet API
216
+ #
217
+ # @api public
218
+ # @param path [String] the request path
219
+ # @param body [Hash, Array, String] the request body, which is sent as JSON unless it is a String
220
+ # @param headers [Hash{String => String}] the headers to send with the request, which replace the ones the
221
+ # client would send, the 'Authorization' header of its access token among them
222
+ # @return [String] the response body
223
+ # @raise [HTTPError] if the response is not successful
224
+ # @raise [ArgumentError] if the path is a URL of an origin other than the host
225
+ # @example Lock the doors of a vehicle
226
+ # client.post("/api/1/vehicles/5YJSA11111111111/command/door_lock")
227
+ def post(path, body = {}, headers: {})
228
+ execute_request(:post, path, body:, headers:)
229
+ end
230
+
231
+ # Perform a PUT request to the Tesla Fleet API
232
+ #
233
+ # @api public
234
+ # @param path [String] the request path
235
+ # @param body [Hash, Array, String] the request body, which is sent as JSON unless it is a String
236
+ # @param headers [Hash{String => String}] the headers to send with the request, which replace the ones the
237
+ # client would send, the 'Authorization' header of its access token among them
238
+ # @return [String] the response body
239
+ # @raise [HTTPError] if the response is not successful
240
+ # @raise [ArgumentError] if the path is a URL of an origin other than the host
241
+ # @example Replace the fleet telemetry configuration of a vehicle
242
+ # client.put("/api/1/vehicles/fleet_telemetry_config", {vins: ["5YJSA11111111111"], config: {}})
243
+ def put(path, body = {}, headers: {})
244
+ execute_request(:put, path, body:, headers:)
245
+ end
246
+
247
+ # Perform a PATCH request to the Tesla Fleet API
248
+ #
249
+ # @api public
250
+ # @param path [String] the request path
251
+ # @param body [Hash, Array, String] the request body, which is sent as JSON unless it is a String
252
+ # @param headers [Hash{String => String}] the headers to send with the request, which replace the ones the
253
+ # client would send, the 'Authorization' header of its access token among them
254
+ # @return [String] the response body
255
+ # @raise [HTTPError] if the response is not successful
256
+ # @raise [ArgumentError] if the path is a URL of an origin other than the host
257
+ # @example Update a resource
258
+ # client.patch("/api/1/vehicles/5YJSA11111111111", {display_name: "Nikola 2.0"})
259
+ def patch(path, body = {}, headers: {})
260
+ execute_request(:patch, path, body:, headers:)
261
+ end
262
+
263
+ private
264
+
265
+ # The connection used for API requests
266
+ # @api private
267
+ # @return [Connection] the connection
268
+ attr_reader :connection
269
+
270
+ # The request builder used for API requests
271
+ # @api private
272
+ # @return [RequestBuilder] the request builder
273
+ attr_reader :request_builder
274
+
275
+ # The redirect handler the responses of API requests are followed with
276
+ # @api private
277
+ # @return [RedirectHandler] the redirect handler
278
+ attr_reader :redirect_handler
279
+
280
+ # The retry handler the requests the server turns away are sent again with
281
+ # @api private
282
+ # @return [RetryHandler] the retry handler
283
+ attr_reader :retry_handler
284
+
285
+ # Execute an HTTP request to the Tesla Fleet API
286
+ #
287
+ # A request answered with a 401 Unauthorized is sent once more with a new access token when the client has what
288
+ # refreshing one takes, since the Fleet API never acted on a request it did not authorize.
289
+ #
290
+ # @api private
291
+ # @param http_method [Symbol] the HTTP method
292
+ # @param path [String] the request path
293
+ # @param params [Hash] the query parameters
294
+ # @param body [Hash, Array, String, nil] the request body
295
+ # @param headers [Hash{String => String}] the headers to send with the request
296
+ # @return [String] the response body
297
+ def execute_request(http_method, path, headers:, params: {}, body: nil)
298
+ uri = build_uri(host, path)
299
+ attempt = ->(access_token) { perform(access_token:, http_method:, uri:, params:, body:, headers:) }
300
+ access_token = renewed_access_token(nil)
301
+ response = attempt.call(access_token)
302
+ if response.code.eql?(UNAUTHORIZED_STATUS) && refreshable?
303
+ response = attempt.call(renewed_access_token(access_token))
304
+ end
305
+ @response_parser.parse(response:)
306
+ end
307
+
308
+ # Build a request, send it, and follow its redirects, again when it is turned away
309
+ #
310
+ # Each attempt builds a request of its own, so the retry handler is told whether the request is safe to send
311
+ # again rather than given one to read the method of. Only a safe request, which asks the server for something
312
+ # rather than asking a vehicle to do something, is sent again after a 502, 503, or 504, or after the network
313
+ # lost it, since the vehicle may have acted on it before the answer went missing. Any request is sent again for
314
+ # a 429, which a rate limiter answers before the request reaches the endpoint.
315
+ #
316
+ # A retry follows the redirects of the request again from the start, rather than sending it straight to where a
317
+ # redirect led the first time.
318
+ #
319
+ # @api private
320
+ # @param access_token [String, nil] the access token the request is authorized with
321
+ # @param http_method [Symbol] the HTTP method
322
+ # @param uri [URI::Generic] the request URI
323
+ # @param params [Hash] the query parameters
324
+ # @param body [Hash, Array, String, nil] the request body
325
+ # @param headers [Hash{String => String}] the headers to send with the request
326
+ # @return [Net::HTTPResponse] the response
327
+ def perform(access_token:, http_method:, uri:, params:, body:, headers:)
328
+ authenticator = authenticator_for(access_token)
329
+ @retry_handler.handle(retry_unanswered: safe?(http_method)) do
330
+ request = @request_builder.build(http_method:, uri:, params:, body:, headers:, authenticator:)
331
+ response = @connection.perform(request:)
332
+ @redirect_handler.handle(response:, request:, authenticator:, body:, headers:)
333
+ end
334
+ end
335
+ end
336
+ end