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
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require_relative "certificate_options"
|
|
6
|
+
require_relative "connection_pool"
|
|
7
|
+
require_relative "errors/network_error"
|
|
8
|
+
require_relative "redacted_output"
|
|
9
|
+
require_relative "settings"
|
|
10
|
+
require_relative "url_validation"
|
|
11
|
+
|
|
12
|
+
module Tesla
|
|
13
|
+
# Manages HTTP connections to the Tesla Fleet API
|
|
14
|
+
#
|
|
15
|
+
# The connection of a client is its own, and cannot be given to it or read from it, so this class can change
|
|
16
|
+
# within 0.x as the rest of the internals can. Its settings are options of {Client} and of the {Tesla} module,
|
|
17
|
+
# such as `open_timeout` and `proxy_url`, which is where they are configured.
|
|
18
|
+
#
|
|
19
|
+
# TLS certificates are verified, and no option turns that off: a host whose certificate OpenSSL does not already
|
|
20
|
+
# trust, such as the vehicle command proxy with a certificate of its own, is reached by naming that certificate
|
|
21
|
+
# with `ca_file`.
|
|
22
|
+
#
|
|
23
|
+
# @api private
|
|
24
|
+
class Connection
|
|
25
|
+
include CertificateOptions
|
|
26
|
+
include Settings
|
|
27
|
+
include URLValidation
|
|
28
|
+
|
|
29
|
+
# Default timeout for opening connections in seconds
|
|
30
|
+
DEFAULT_OPEN_TIMEOUT = 60 # seconds
|
|
31
|
+
# Default timeout for reading responses in seconds
|
|
32
|
+
DEFAULT_READ_TIMEOUT = 60 # seconds
|
|
33
|
+
# Default timeout for writing requests in seconds
|
|
34
|
+
DEFAULT_WRITE_TIMEOUT = 60 # seconds
|
|
35
|
+
# Default seconds an idle connection is kept open for another request
|
|
36
|
+
DEFAULT_KEEP_ALIVE_TIMEOUT = 2 # seconds
|
|
37
|
+
|
|
38
|
+
# @!method open_timeout
|
|
39
|
+
# The timeout for opening connections in seconds
|
|
40
|
+
# @api private
|
|
41
|
+
# @return [Numeric] the timeout for opening connections in seconds
|
|
42
|
+
# @example Get the open timeout
|
|
43
|
+
# connection.open_timeout
|
|
44
|
+
# @!method open_timeout=(open_timeout)
|
|
45
|
+
# Set the timeout for opening connections in seconds
|
|
46
|
+
# @api private
|
|
47
|
+
# @param open_timeout [Numeric] the timeout for opening connections in seconds
|
|
48
|
+
# @return [void]
|
|
49
|
+
# @raise [ArgumentError] if it is not a number of seconds, in which case the timeout is left as it was
|
|
50
|
+
# @example Set the open timeout
|
|
51
|
+
# connection.open_timeout = 30
|
|
52
|
+
seconds_setting :open_timeout
|
|
53
|
+
|
|
54
|
+
# @!method read_timeout
|
|
55
|
+
# The timeout for reading responses in seconds
|
|
56
|
+
# @api private
|
|
57
|
+
# @return [Numeric] the timeout for reading responses in seconds
|
|
58
|
+
# @example Get the read timeout
|
|
59
|
+
# connection.read_timeout
|
|
60
|
+
# @!method read_timeout=(read_timeout)
|
|
61
|
+
# Set the timeout for reading responses in seconds
|
|
62
|
+
# @api private
|
|
63
|
+
# @param read_timeout [Numeric] the timeout for reading responses in seconds
|
|
64
|
+
# @return [void]
|
|
65
|
+
# @raise [ArgumentError] if it is not a number of seconds, in which case the timeout is left as it was
|
|
66
|
+
# @example Set the read timeout
|
|
67
|
+
# connection.read_timeout = 30
|
|
68
|
+
seconds_setting :read_timeout
|
|
69
|
+
|
|
70
|
+
# @!method write_timeout
|
|
71
|
+
# The timeout for writing requests in seconds
|
|
72
|
+
# @api private
|
|
73
|
+
# @return [Numeric] the timeout for writing requests in seconds
|
|
74
|
+
# @example Get the write timeout
|
|
75
|
+
# connection.write_timeout
|
|
76
|
+
# @!method write_timeout=(write_timeout)
|
|
77
|
+
# Set the timeout for writing requests in seconds
|
|
78
|
+
# @api private
|
|
79
|
+
# @param write_timeout [Numeric] the timeout for writing requests in seconds
|
|
80
|
+
# @return [void]
|
|
81
|
+
# @raise [ArgumentError] if it is not a number of seconds, in which case the timeout is left as it was
|
|
82
|
+
# @example Set the write timeout
|
|
83
|
+
# connection.write_timeout = 30
|
|
84
|
+
seconds_setting :write_timeout
|
|
85
|
+
|
|
86
|
+
# @!method keep_alive_timeout
|
|
87
|
+
# The seconds an idle connection is kept open for another request
|
|
88
|
+
#
|
|
89
|
+
# A request is sent on the connection kept open for its host when one was used within this many seconds, so
|
|
90
|
+
# that a series of requests does not open a connection each. Zero closes every connection when its request is
|
|
91
|
+
# done.
|
|
92
|
+
#
|
|
93
|
+
# @api private
|
|
94
|
+
# @return [Numeric] the seconds an idle connection is kept open
|
|
95
|
+
# @example Get the keep-alive timeout
|
|
96
|
+
# connection.keep_alive_timeout
|
|
97
|
+
# @!method keep_alive_timeout=(keep_alive_timeout)
|
|
98
|
+
# Set the seconds an idle connection is kept open for another request
|
|
99
|
+
# @api private
|
|
100
|
+
# @param keep_alive_timeout [Numeric] the seconds an idle connection is kept open
|
|
101
|
+
# @return [void]
|
|
102
|
+
# @raise [ArgumentError] if it is not a number of seconds, in which case the timeout is left as it was
|
|
103
|
+
# @example Set the keep-alive timeout
|
|
104
|
+
# connection.keep_alive_timeout = 0
|
|
105
|
+
seconds_setting :keep_alive_timeout
|
|
106
|
+
|
|
107
|
+
# The IO object for debug output
|
|
108
|
+
#
|
|
109
|
+
# The credentials requests and responses carry are redacted from what is written to it (see {RedactedOutput}).
|
|
110
|
+
#
|
|
111
|
+
# @api private
|
|
112
|
+
# @return [IO, nil] the IO object for debug output
|
|
113
|
+
# @example Get or set the debug output
|
|
114
|
+
# connection.debug_output = $stderr
|
|
115
|
+
attr_accessor :debug_output
|
|
116
|
+
|
|
117
|
+
# The proxy URL for requests
|
|
118
|
+
# @api private
|
|
119
|
+
# @return [String, nil] the proxy URL for requests
|
|
120
|
+
# @example Get the proxy URL
|
|
121
|
+
# connection.proxy_url
|
|
122
|
+
attr_reader :proxy_url
|
|
123
|
+
|
|
124
|
+
# The parsed proxy URI
|
|
125
|
+
# @api private
|
|
126
|
+
# @return [URI::Generic, nil] the parsed proxy URI
|
|
127
|
+
# @example Get the proxy URI
|
|
128
|
+
# connection.proxy_uri
|
|
129
|
+
attr_reader :proxy_uri
|
|
130
|
+
|
|
131
|
+
# Summarize the connection for the console
|
|
132
|
+
#
|
|
133
|
+
# @api private
|
|
134
|
+
# @return [String] the summary, which includes the proxy URL without its user and password
|
|
135
|
+
# @example Inspect a connection
|
|
136
|
+
# connection.inspect # => #<Tesla::Connection proxy_url=nil open_timeout=60 read_timeout=60 write_timeout=60 keep_alive_timeout=2>
|
|
137
|
+
def inspect
|
|
138
|
+
"#<#{self.class} proxy_url=#{redact(proxy_url).inspect} open_timeout=#{open_timeout} " \
|
|
139
|
+
"read_timeout=#{read_timeout} write_timeout=#{write_timeout} keep_alive_timeout=#{keep_alive_timeout}>"
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Initialize a new connection
|
|
143
|
+
#
|
|
144
|
+
# @api private
|
|
145
|
+
# @param open_timeout [Numeric] the timeout for opening connections in seconds
|
|
146
|
+
# @param read_timeout [Numeric] the timeout for reading responses in seconds
|
|
147
|
+
# @param write_timeout [Numeric] the timeout for writing requests in seconds
|
|
148
|
+
# @param debug_output [IO, nil] the IO object for debug output
|
|
149
|
+
# @param proxy_url [String, nil] the proxy URL for requests
|
|
150
|
+
# @param keep_alive_timeout [Numeric] the seconds an idle connection is kept open for another request
|
|
151
|
+
# @param ca_file [String, nil] the path of a file of certificates TLS is verified with
|
|
152
|
+
# @return [Connection] a new connection instance
|
|
153
|
+
# @raise [ArgumentError] if the proxy URL is invalid, or the certificate path names nothing
|
|
154
|
+
# @example Create a connection with default settings
|
|
155
|
+
# connection = Tesla::Connection.new
|
|
156
|
+
# @example Create a connection with custom timeouts
|
|
157
|
+
# connection = Tesla::Connection.new(open_timeout: 30, read_timeout: 30)
|
|
158
|
+
# @example Create a connection that trusts the certificate of the vehicle command proxy
|
|
159
|
+
# connection = Tesla::Connection.new(ca_file: "config/tls-cert.pem")
|
|
160
|
+
def initialize(open_timeout: DEFAULT_OPEN_TIMEOUT, read_timeout: DEFAULT_READ_TIMEOUT,
|
|
161
|
+
write_timeout: DEFAULT_WRITE_TIMEOUT, debug_output: nil, proxy_url: nil,
|
|
162
|
+
keep_alive_timeout: DEFAULT_KEEP_ALIVE_TIMEOUT, ca_file: nil)
|
|
163
|
+
self.open_timeout = open_timeout
|
|
164
|
+
self.read_timeout = read_timeout
|
|
165
|
+
self.write_timeout = write_timeout
|
|
166
|
+
@debug_output = debug_output
|
|
167
|
+
self.keep_alive_timeout = keep_alive_timeout
|
|
168
|
+
@pool = ConnectionPool.new
|
|
169
|
+
self.proxy_url = proxy_url
|
|
170
|
+
self.ca_file = ca_file
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Perform an HTTP request
|
|
174
|
+
#
|
|
175
|
+
# The request is sent on the connection kept open for its host, when there is one it can be sent on, and that
|
|
176
|
+
# connection is kept open for the next request (see {#keep_alive_timeout}).
|
|
177
|
+
#
|
|
178
|
+
# The settings are read once, before the connection is taken, and a connection that has to be opened is opened
|
|
179
|
+
# with them and kept under them, so that a setting changed while the request is being sent, from another thread,
|
|
180
|
+
# closes the connection when the next request is sent rather than keeping it as though it had been opened with
|
|
181
|
+
# the setting as it is now, and a connection is never opened with one setting and kept under another.
|
|
182
|
+
#
|
|
183
|
+
# @api private
|
|
184
|
+
# @param request [Net::HTTPRequest] the HTTP request to perform
|
|
185
|
+
# @return [Net::HTTPResponse] the HTTP response
|
|
186
|
+
# @raise [NetworkError] if a network error occurs
|
|
187
|
+
# @example Perform a request
|
|
188
|
+
# response = connection.perform(request: request)
|
|
189
|
+
def perform(request:)
|
|
190
|
+
send_request(request, settings_for(request.uri))
|
|
191
|
+
rescue *NetworkError::WRAPPED => e
|
|
192
|
+
raise NetworkError, "Network error: #{e}"
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Close the connections kept open for the next request
|
|
196
|
+
#
|
|
197
|
+
# The connections are opened again as they are needed, so a connection that is closed while it is being used is
|
|
198
|
+
# not interrupted, and requests can still be made afterwards.
|
|
199
|
+
#
|
|
200
|
+
# @api private
|
|
201
|
+
# @return [Connection] the connection
|
|
202
|
+
# @example Close the connections a client keeps open
|
|
203
|
+
# connection.close
|
|
204
|
+
def close
|
|
205
|
+
pool.close
|
|
206
|
+
self
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Set the proxy URL for requests
|
|
210
|
+
#
|
|
211
|
+
# @api private
|
|
212
|
+
# @param proxy_url [String, nil] the proxy URL, or nil to read proxies from the environment again
|
|
213
|
+
# @return [void]
|
|
214
|
+
# @raise [ArgumentError] if the proxy URL is invalid, in which case the proxy is left as it was
|
|
215
|
+
# @example Set the proxy URL
|
|
216
|
+
# connection.proxy_url = "http://proxy.example.com:8080"
|
|
217
|
+
# @example Clear the proxy URL
|
|
218
|
+
# connection.proxy_url = nil
|
|
219
|
+
def proxy_url=(proxy_url)
|
|
220
|
+
proxy_uri = proxy_url && parse_proxy_uri(proxy_url)
|
|
221
|
+
@proxy_url = proxy_url
|
|
222
|
+
@proxy_uri = proxy_uri
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
private
|
|
226
|
+
|
|
227
|
+
# The connections kept open for the next request
|
|
228
|
+
# @api private
|
|
229
|
+
# @return [ConnectionPool] the pool
|
|
230
|
+
attr_reader :pool
|
|
231
|
+
|
|
232
|
+
# The settings a connection to a URI is opened with
|
|
233
|
+
#
|
|
234
|
+
# A connection kept open must still have them to be reused. The proxy is the one a request to the URI is sent
|
|
235
|
+
# through, which is read from the http_proxy, https_proxy, and no_proxy environment variables when no proxy URL
|
|
236
|
+
# is configured, so that a connection kept open is not reused past a change to the environment either.
|
|
237
|
+
#
|
|
238
|
+
# @api private
|
|
239
|
+
# @param uri [URI::Generic] the URI a request is sent to
|
|
240
|
+
# @return [Hash{Symbol => Object}] the settings
|
|
241
|
+
def settings_for(uri)
|
|
242
|
+
{open_timeout:, read_timeout:, write_timeout:, keep_alive_timeout:, debug_output:,
|
|
243
|
+
proxy: proxy_uri || uri.find_proxy, ca_file:}
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Send a request on a connection opened with the settings given
|
|
247
|
+
#
|
|
248
|
+
# A connection that has to be opened is opened with those settings, and the connection is kept under them
|
|
249
|
+
# afterwards, rather than under the settings as they are then.
|
|
250
|
+
#
|
|
251
|
+
# A request sent on a connection with a debug output asks for its response uncompressed, since the debug output
|
|
252
|
+
# is written the body as it was sent, and the credentials of a compressed body could not be redacted from it
|
|
253
|
+
# (see {RedactedOutput}).
|
|
254
|
+
#
|
|
255
|
+
# @api private
|
|
256
|
+
# @param request [Net::HTTPRequest] the HTTP request to send
|
|
257
|
+
# @param opened_with [Hash{Symbol => Object}] the settings a connection must have been opened with to send it on
|
|
258
|
+
# @return [Net::HTTPResponse] the HTTP response
|
|
259
|
+
def send_request(request, opened_with)
|
|
260
|
+
kept_for = opened_with.fetch(:keep_alive_timeout)
|
|
261
|
+
request["Accept-Encoding"] = "identity" if opened_with.fetch(:debug_output)
|
|
262
|
+
http_client = pool.checkout(request:, settings: opened_with, keep_alive_timeout: kept_for) do
|
|
263
|
+
build_http_client(request.uri, opened_with)
|
|
264
|
+
end
|
|
265
|
+
response = request_on(http_client, request)
|
|
266
|
+
pool.checkin(request:, http_client:, settings: opened_with, keep_alive_timeout: kept_for)
|
|
267
|
+
response
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
# Send a request on a connection, and close the connection if the request raises
|
|
271
|
+
#
|
|
272
|
+
# The connection is closed whatever the request raises, rather than left open with a response half read: a
|
|
273
|
+
# timeout of the caller's own, a Thread#raise, or an interrupt stops a request as surely as the network does.
|
|
274
|
+
#
|
|
275
|
+
# @api private
|
|
276
|
+
# @param http_client [Net::HTTP] the connection
|
|
277
|
+
# @param request [Net::HTTPRequest] the HTTP request to send
|
|
278
|
+
# @return [Net::HTTPResponse] the HTTP response
|
|
279
|
+
def request_on(http_client, request)
|
|
280
|
+
http_client.request(request)
|
|
281
|
+
rescue Exception # rubocop:disable Lint/RescueException
|
|
282
|
+
pool.discard(http_client)
|
|
283
|
+
raise
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Decode a percent-encoded component of a URL
|
|
287
|
+
# @api private
|
|
288
|
+
# @param component [String, nil] the component
|
|
289
|
+
# @return [String, nil] the decoded component, or nil for nil
|
|
290
|
+
def decode(component)
|
|
291
|
+
component && URI.decode_uri_component(component)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Build an HTTP client for the given URI, with the settings given
|
|
295
|
+
#
|
|
296
|
+
# An https:// proxy is connected to over TLS.
|
|
297
|
+
#
|
|
298
|
+
# @api private
|
|
299
|
+
# @param uri [URI::Generic] the URI to connect to
|
|
300
|
+
# @param opened_with [Hash{Symbol => Object}] the settings to open the connection with
|
|
301
|
+
# @return [Net::HTTP] the HTTP client
|
|
302
|
+
# @raise [ArgumentError] if the URI has no host
|
|
303
|
+
def build_http_client(uri, opened_with)
|
|
304
|
+
host = uri.host
|
|
305
|
+
raise ArgumentError, "URI has no host: #{uri}" if host.nil?
|
|
306
|
+
|
|
307
|
+
proxy_host, proxy_port, proxy_user, proxy_pass, proxy_use_ssl = proxy_arguments_for(opened_with.fetch(:proxy))
|
|
308
|
+
http_client = Net::HTTP.new(host, uri.port, proxy_host, proxy_port, proxy_user, proxy_pass, nil, proxy_use_ssl)
|
|
309
|
+
http_client.use_ssl = uri.scheme.eql?("https")
|
|
310
|
+
configure_http_client(http_client, opened_with)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# The proxy host, port, user, password, and whether to use TLS for a proxy
|
|
314
|
+
#
|
|
315
|
+
# The user and password are decoded from the proxy URL, and an https:// proxy is connected to over TLS.
|
|
316
|
+
#
|
|
317
|
+
# @api private
|
|
318
|
+
# @param proxy [URI::Generic, nil] the proxy, or nil to connect directly
|
|
319
|
+
# @return [Array] the proxy host, port, user, password, and whether to use TLS, each nil without a proxy
|
|
320
|
+
def proxy_arguments_for(proxy)
|
|
321
|
+
[proxy&.host, proxy&.port, decode(proxy&.user), decode(proxy&.password), proxy&.instance_of?(URI::HTTPS)]
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Configure an HTTP client with the timeouts given
|
|
325
|
+
#
|
|
326
|
+
# @api private
|
|
327
|
+
# @param http_client [Net::HTTP] the HTTP client to configure
|
|
328
|
+
# @param opened_with [Hash{Symbol => Object}] the settings to configure it with
|
|
329
|
+
# @return [Net::HTTP] the configured HTTP client
|
|
330
|
+
def configure_timeouts(http_client, opened_with)
|
|
331
|
+
http_client.tap do |c|
|
|
332
|
+
c.open_timeout = opened_with.fetch(:open_timeout)
|
|
333
|
+
c.read_timeout = opened_with.fetch(:read_timeout)
|
|
334
|
+
c.write_timeout = opened_with.fetch(:write_timeout)
|
|
335
|
+
c.keep_alive_timeout = opened_with.fetch(:keep_alive_timeout)
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Configure an HTTP client with the timeouts, debug output, and CA file given
|
|
340
|
+
#
|
|
341
|
+
# The debug output is wrapped, so that the credentials Net::HTTP writes with the headers of a request do not
|
|
342
|
+
# reach the IO.
|
|
343
|
+
#
|
|
344
|
+
# Net::HTTP sends an idempotent request again once on its own when the connection it was sent on fails, which
|
|
345
|
+
# the retries of {RetryHandler} would otherwise come on top of, so a request that timed out reading its response
|
|
346
|
+
# would wait twice as many read timeouts as `max_retries` allows for. It is left to {RetryHandler} alone.
|
|
347
|
+
#
|
|
348
|
+
# @api private
|
|
349
|
+
# @param http_client [Net::HTTP] the HTTP client to configure
|
|
350
|
+
# @param opened_with [Hash{Symbol => Object}] the settings to configure it with
|
|
351
|
+
# @return [Net::HTTP] the configured HTTP client
|
|
352
|
+
def configure_http_client(http_client, opened_with)
|
|
353
|
+
debug_output = opened_with.fetch(:debug_output)
|
|
354
|
+
configure_timeouts(http_client, opened_with).tap do |c|
|
|
355
|
+
c.max_retries = 0
|
|
356
|
+
c.set_debug_output(debug_output && RedactedOutput.new(debug_output))
|
|
357
|
+
c.ca_file = opened_with.fetch(:ca_file)
|
|
358
|
+
end
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require_relative "idempotence"
|
|
5
|
+
|
|
6
|
+
module Tesla
|
|
7
|
+
# Keeps the connections a {Connection} is not using, so that a request can be sent on one that is already open
|
|
8
|
+
#
|
|
9
|
+
# One connection is kept for each host, so that requests made at the same time are sent on connections of their
|
|
10
|
+
# own rather than waiting for one another, and the connections they opened are closed when they are given back.
|
|
11
|
+
# A connection opened with settings that have since changed is closed rather than reused.
|
|
12
|
+
#
|
|
13
|
+
# A process forked from one that kept a connection open shares the socket of that connection with it, so the
|
|
14
|
+
# connection is kept for the process that opened it alone: a forked process opens one of its own, rather than
|
|
15
|
+
# reading the responses to the requests of the other process, and leaves the one it inherited alone, rather than
|
|
16
|
+
# closing it under the process still using it.
|
|
17
|
+
#
|
|
18
|
+
# @api private
|
|
19
|
+
class ConnectionPool
|
|
20
|
+
include Idempotence
|
|
21
|
+
|
|
22
|
+
# Initialize a new ConnectionPool
|
|
23
|
+
#
|
|
24
|
+
# @api private
|
|
25
|
+
# @return [ConnectionPool] a new instance
|
|
26
|
+
# @example Create a connection pool
|
|
27
|
+
# pool = Tesla::ConnectionPool.new
|
|
28
|
+
def initialize
|
|
29
|
+
@connections = {}
|
|
30
|
+
@mutex = Mutex.new
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# The connection to send a request on
|
|
34
|
+
#
|
|
35
|
+
# A connection kept open for the host of the request is used when the request can be sent on one, and one is
|
|
36
|
+
# opened with the block otherwise.
|
|
37
|
+
#
|
|
38
|
+
# @api private
|
|
39
|
+
# @param request [Net::HTTPRequest] the request
|
|
40
|
+
# @param settings [Object] the settings a connection must have been opened with to be reused
|
|
41
|
+
# @param keep_alive_timeout [Numeric] the seconds an idle connection is kept open, or zero to open one each time
|
|
42
|
+
# @yield the connection to open when none is kept for the host of the request
|
|
43
|
+
# @return [Net::HTTP] the connection, which is open
|
|
44
|
+
# @example Take the connection to send a request on
|
|
45
|
+
# pool.checkout(request:, settings:, keep_alive_timeout: 2) { Net::HTTP.new(uri.host, uri.port) }
|
|
46
|
+
def checkout(request:, settings:, keep_alive_timeout:)
|
|
47
|
+
http_client = take(pool_key(request), settings) if keep_alive?(request, keep_alive_timeout)
|
|
48
|
+
http_client || yield.tap(&:start)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Keep the connection a request was sent on, or close it
|
|
52
|
+
#
|
|
53
|
+
# @api private
|
|
54
|
+
# @param request [Net::HTTPRequest] the request
|
|
55
|
+
# @param http_client [Net::HTTP] the connection it was sent on
|
|
56
|
+
# @param settings [Object] the settings the connection was opened with
|
|
57
|
+
# @param keep_alive_timeout [Numeric] the seconds an idle connection is kept open, or zero to close it
|
|
58
|
+
# @return [void]
|
|
59
|
+
# @example Give back the connection a request was sent on
|
|
60
|
+
# pool.checkin(request:, http_client:, settings:, keep_alive_timeout: 2)
|
|
61
|
+
def checkin(request:, http_client:, settings:, keep_alive_timeout:)
|
|
62
|
+
return discard(http_client) unless keep_alive?(request, keep_alive_timeout)
|
|
63
|
+
|
|
64
|
+
store(pool_key(request), http_client, settings)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Close a connection
|
|
68
|
+
#
|
|
69
|
+
# @api private
|
|
70
|
+
# @param http_client [Net::HTTP, nil] the connection, or nil when none was opened
|
|
71
|
+
# @return [void]
|
|
72
|
+
# @example Close the connection a request failed on
|
|
73
|
+
# pool.discard(http_client)
|
|
74
|
+
def discard(http_client)
|
|
75
|
+
http_client.finish if http_client&.started?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Take the connection kept for a key
|
|
79
|
+
#
|
|
80
|
+
# The connection is removed from the pool, so that it is used by one request at a time, and is given back with
|
|
81
|
+
# {#store}. A connection opened with other settings is closed instead of returned, and one opened by the process
|
|
82
|
+
# this one was forked from is left to that process.
|
|
83
|
+
#
|
|
84
|
+
# @api private
|
|
85
|
+
# @param key [Object] the key the connection is kept under
|
|
86
|
+
# @param settings [Object] the settings a connection must have been opened with to be reused
|
|
87
|
+
# @return [Net::HTTP, nil] the connection, or nil when none is kept for the key
|
|
88
|
+
# @example Take the connection kept for a host
|
|
89
|
+
# pool.take(["https", "fleet-api.prd.na.vn.cloud.tesla.com", 443], settings)
|
|
90
|
+
def take(key, settings)
|
|
91
|
+
http_client, kept_settings, pid = @mutex.synchronize { @connections.delete(key) }
|
|
92
|
+
return unless opened_here?(pid)
|
|
93
|
+
return http_client if settings.eql?(kept_settings)
|
|
94
|
+
|
|
95
|
+
discard(http_client)
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Keep a connection for a key
|
|
100
|
+
#
|
|
101
|
+
# A connection is kept only when none is kept for the key already, so that the pool holds one connection per
|
|
102
|
+
# host; a connection it cannot keep is closed. One kept by the process this one was forked from does not count,
|
|
103
|
+
# and is replaced.
|
|
104
|
+
#
|
|
105
|
+
# @api private
|
|
106
|
+
# @param key [Object] the key to keep the connection under
|
|
107
|
+
# @param http_client [Net::HTTP] the connection
|
|
108
|
+
# @param settings [Object] the settings the connection was opened with
|
|
109
|
+
# @return [Boolean] whether the connection was kept
|
|
110
|
+
# @example Keep a connection for a host
|
|
111
|
+
# pool.store(["https", "fleet-api.prd.na.vn.cloud.tesla.com", 443], http_client, settings)
|
|
112
|
+
def store(key, http_client, settings)
|
|
113
|
+
stored = @mutex.synchronize do
|
|
114
|
+
@connections[key] = [http_client, settings, Process.pid] unless opened_here?(@connections.dig(key, 2))
|
|
115
|
+
end
|
|
116
|
+
discard(http_client) if stored.nil?
|
|
117
|
+
!stored.nil?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Close every connection the pool keeps
|
|
121
|
+
#
|
|
122
|
+
# The keys are read under the lock, as every other read and write of the connections is, since a request giving
|
|
123
|
+
# its connection back at that moment writes to them from another thread. The connections are then taken one at a
|
|
124
|
+
# time, so that a connection given back while the pool is closing is either closed with the rest or kept for the
|
|
125
|
+
# next request, rather than dropped without being closed. The connections opened by the process this one was
|
|
126
|
+
# forked from are forgotten rather than closed, since that process may still be using them.
|
|
127
|
+
#
|
|
128
|
+
# @api private
|
|
129
|
+
# @return [ConnectionPool] the pool
|
|
130
|
+
# @example Close the connections of a pool
|
|
131
|
+
# pool.close
|
|
132
|
+
def close
|
|
133
|
+
keys = @mutex.synchronize { @connections.keys }
|
|
134
|
+
keys.each { |key| discard(delete(key)) }
|
|
135
|
+
self
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
private
|
|
139
|
+
|
|
140
|
+
# Take the connection kept for a key, whatever settings it was opened with
|
|
141
|
+
# @api private
|
|
142
|
+
# @param key [Object] the key the connection is kept under
|
|
143
|
+
# @return [Net::HTTP, nil] the connection, or nil when none is kept for the key or the one kept was opened by the
|
|
144
|
+
# process this one was forked from
|
|
145
|
+
def delete(key)
|
|
146
|
+
http_client, _settings, pid = @mutex.synchronize { @connections.delete(key) }
|
|
147
|
+
http_client if opened_here?(pid)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Whether a connection was opened by this process
|
|
151
|
+
#
|
|
152
|
+
# A connection opened by the process this one was forked from was not, and shares its socket with that process.
|
|
153
|
+
#
|
|
154
|
+
# @api private
|
|
155
|
+
# @param pid [Integer, nil] the ID of the process that opened the connection, or nil when none is kept
|
|
156
|
+
# @return [Boolean] whether it is the ID of this process
|
|
157
|
+
def opened_here?(pid)
|
|
158
|
+
Process.pid.eql?(pid)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Whether a request can be sent on a connection that is kept open
|
|
162
|
+
#
|
|
163
|
+
# The connection it is sent on is kept open afterwards too. A request that asks the server to do something is
|
|
164
|
+
# sent on a connection of its own and that connection is closed afterwards: Net::HTTP reconnects before it reuses
|
|
165
|
+
# a connection the server has closed, and {RetryHandler} sends a safe request whose connection breaks again, but
|
|
166
|
+
# a request that asks the server to do something cannot be sent again to find out whether the server acted on
|
|
167
|
+
# the first one.
|
|
168
|
+
#
|
|
169
|
+
# @api private
|
|
170
|
+
# @param request [Net::HTTPRequest] the request
|
|
171
|
+
# @param keep_alive_timeout [Numeric] the seconds an idle connection is kept open
|
|
172
|
+
# @return [Boolean] whether the connection is kept open
|
|
173
|
+
def keep_alive?(request, keep_alive_timeout)
|
|
174
|
+
keep_alive_timeout.positive? && safe?(request.method)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# The key a connection is kept under, which is the host it is open to
|
|
178
|
+
# @api private
|
|
179
|
+
# @param request [Net::HTTPRequest] the request
|
|
180
|
+
# @return [Array] the scheme, host, and port
|
|
181
|
+
def pool_key(request)
|
|
182
|
+
uri = request.uri
|
|
183
|
+
[uri.scheme, uri.host, uri.port]
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Tesla
|
|
4
|
+
# The copies a {Resource} is made of and answers with: frozen for the resource, and unfrozen for the caller
|
|
5
|
+
# @api private
|
|
6
|
+
module DeepCopy
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
# Copy a value, freezing the copy and everything nested inside it
|
|
10
|
+
#
|
|
11
|
+
# The keys of hashes are converted to strings. Everything a parsed JSON response holds other than hashes, arrays, and strings is already immutable.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
# @param value [Object] the value to copy
|
|
15
|
+
# @return [Object] the frozen copy
|
|
16
|
+
def deep_freeze(value)
|
|
17
|
+
case value
|
|
18
|
+
when Hash then value.to_h { |key, nested| [key.to_s, deep_freeze(nested)] }.freeze
|
|
19
|
+
when Array then value.map { |nested| deep_freeze(nested) }.freeze
|
|
20
|
+
when String then value.dup.freeze
|
|
21
|
+
else value
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Copy a value and everything nested inside it, for the caller to change
|
|
26
|
+
#
|
|
27
|
+
# The keys of hashes are left as they are, since a hash freezes the strings it is keyed by itself. Everything a
|
|
28
|
+
# parsed JSON response holds other than hashes, arrays, and strings answers `dup` with itself.
|
|
29
|
+
#
|
|
30
|
+
# @api private
|
|
31
|
+
# @param value [Object] the value to copy
|
|
32
|
+
# @return [Object] the copy, which is not frozen
|
|
33
|
+
def deep_dup(value)
|
|
34
|
+
case value
|
|
35
|
+
when Hash then value.transform_values { |nested| deep_dup(nested) }
|
|
36
|
+
when Array then value.map { |nested| deep_dup(nested) }
|
|
37
|
+
else value.dup
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "resource"
|
|
4
|
+
|
|
5
|
+
module Tesla
|
|
6
|
+
# The position and the driving state of a vehicle
|
|
7
|
+
# @api public
|
|
8
|
+
class DriveState < Resource
|
|
9
|
+
inspect_with :latitude, :longitude, :shift_state
|
|
10
|
+
|
|
11
|
+
# @!method latitude
|
|
12
|
+
# The latitude of the vehicle
|
|
13
|
+
#
|
|
14
|
+
# The Fleet API answers with it when it is asked for "location_data", for a token with the vehicle_location scope.
|
|
15
|
+
#
|
|
16
|
+
# @api public
|
|
17
|
+
# @return [Float, nil] the latitude of the vehicle
|
|
18
|
+
# @example
|
|
19
|
+
# drive_state.latitude
|
|
20
|
+
attribute :latitude
|
|
21
|
+
|
|
22
|
+
# @!method longitude
|
|
23
|
+
# The longitude of the vehicle
|
|
24
|
+
#
|
|
25
|
+
# The Fleet API answers with it when it is asked for "location_data", for a token with the vehicle_location scope.
|
|
26
|
+
#
|
|
27
|
+
# @api public
|
|
28
|
+
# @return [Float, nil] the longitude of the vehicle
|
|
29
|
+
# @example
|
|
30
|
+
# drive_state.longitude
|
|
31
|
+
attribute :longitude
|
|
32
|
+
|
|
33
|
+
# @!method heading
|
|
34
|
+
# The compass heading of the vehicle in degrees
|
|
35
|
+
# @api public
|
|
36
|
+
# @return [Integer, nil] the compass heading of the vehicle
|
|
37
|
+
# @example
|
|
38
|
+
# drive_state.heading
|
|
39
|
+
attribute :heading
|
|
40
|
+
|
|
41
|
+
# @!method speed
|
|
42
|
+
# The speed of the vehicle in miles per hour, or nil when it is parked
|
|
43
|
+
# @api public
|
|
44
|
+
# @return [Integer, nil] the speed of the vehicle
|
|
45
|
+
# @example
|
|
46
|
+
# drive_state.speed
|
|
47
|
+
attribute :speed
|
|
48
|
+
|
|
49
|
+
# @!method power
|
|
50
|
+
# The power the vehicle is drawing in kilowatts
|
|
51
|
+
#
|
|
52
|
+
# It is negative while the vehicle charges or regenerates.
|
|
53
|
+
#
|
|
54
|
+
# @api public
|
|
55
|
+
# @return [Integer, nil] the power the vehicle is drawing
|
|
56
|
+
# @example
|
|
57
|
+
# drive_state.power
|
|
58
|
+
attribute :power
|
|
59
|
+
|
|
60
|
+
# @!method shift_state
|
|
61
|
+
# The gear the vehicle is in, "P", "R", "N", or "D"
|
|
62
|
+
#
|
|
63
|
+
# It is nil when the vehicle is parked and idle.
|
|
64
|
+
#
|
|
65
|
+
# @api public
|
|
66
|
+
# @return [String, nil] the gear the vehicle is in
|
|
67
|
+
# @example
|
|
68
|
+
# drive_state.shift_state
|
|
69
|
+
attribute :shift_state
|
|
70
|
+
|
|
71
|
+
# @!method timestamp
|
|
72
|
+
# The time the vehicle reported the state
|
|
73
|
+
# @api public
|
|
74
|
+
# @return [Time, nil] the time the vehicle reported the state
|
|
75
|
+
# @raise [InvalidResponse] if the timestamp is not a number of milliseconds
|
|
76
|
+
# @example
|
|
77
|
+
# drive_state.timestamp
|
|
78
|
+
time_attribute :timestamp
|
|
79
|
+
end
|
|
80
|
+
end
|