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,403 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "deep_copy"
|
|
4
|
+
require_relative "errors/invalid_response"
|
|
5
|
+
|
|
6
|
+
module Tesla
|
|
7
|
+
# Base class for objects that wrap Tesla Fleet API responses
|
|
8
|
+
#
|
|
9
|
+
# The endpoints answer with the resources the library defines, which declare their readers with {.attribute},
|
|
10
|
+
# {.predicate}, {.time_attribute}, {.resource_attribute}, and {.resource_list_attribute}, and declare what they
|
|
11
|
+
# inspect with and what identifies them with {.inspect_with} and {.identified_by}. Those declarations are private,
|
|
12
|
+
# since no endpoint answers with a resource of your own, so they can change within 0.x; the readers they declare,
|
|
13
|
+
# {.attribute_names}, and the rest of the instance interface are public.
|
|
14
|
+
#
|
|
15
|
+
# @api public
|
|
16
|
+
class Resource
|
|
17
|
+
include DeepCopy
|
|
18
|
+
|
|
19
|
+
# The raw attributes from the API response
|
|
20
|
+
# @api public
|
|
21
|
+
# @return [Hash{String => Object}] the raw attributes
|
|
22
|
+
# @example Get the raw attributes
|
|
23
|
+
# vehicle.attributes["vin"]
|
|
24
|
+
attr_reader :attributes
|
|
25
|
+
|
|
26
|
+
# Build a list of resources from a list of attribute hashes
|
|
27
|
+
#
|
|
28
|
+
# @api public
|
|
29
|
+
# @param list [Array<Hash{String, Symbol => Object}>] the attribute hashes
|
|
30
|
+
# @return [Array<Resource>] the resources
|
|
31
|
+
# @example Build a list of vehicles
|
|
32
|
+
# Tesla::Vehicle.list(JSON.parse(body).fetch("response"))
|
|
33
|
+
def self.list(list)
|
|
34
|
+
list.map { |attributes| new(attributes) } #: Array[instance]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# The macros that declare the readers of a resource, extended into {Resource}
|
|
38
|
+
# @api private
|
|
39
|
+
module Declaration
|
|
40
|
+
# Define a reader for an attribute
|
|
41
|
+
#
|
|
42
|
+
# Endpoints spell some fields differently, so a reader may declare several keys and reads the first
|
|
43
|
+
# one the response contains.
|
|
44
|
+
#
|
|
45
|
+
# @api private
|
|
46
|
+
# @param name [Symbol] the name of the reader
|
|
47
|
+
# @param keys [Array<String, Symbol>] the attribute keys, most preferred first (defaults to the name)
|
|
48
|
+
# @return [Symbol] the name of the reader
|
|
49
|
+
# @example Declare a reader
|
|
50
|
+
# attribute :name
|
|
51
|
+
def attribute(name, *keys)
|
|
52
|
+
keys = keys_for(name, keys)
|
|
53
|
+
define_method(name) do
|
|
54
|
+
# @type self: Resource
|
|
55
|
+
value_of(keys)
|
|
56
|
+
end
|
|
57
|
+
record_attribute(name)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Define a predicate for a boolean attribute
|
|
61
|
+
#
|
|
62
|
+
# @api private
|
|
63
|
+
# @param name [Symbol] the name of the attribute (the reader is suffixed with a question mark)
|
|
64
|
+
# @param keys [Array<String, Symbol>] the attribute keys, most preferred first (defaults to the name)
|
|
65
|
+
# @return [Symbol] the name of the reader
|
|
66
|
+
# @example Declare a predicate for a boolean field
|
|
67
|
+
# predicate :in_service
|
|
68
|
+
def predicate(name, *keys)
|
|
69
|
+
keys = keys_for(name, keys)
|
|
70
|
+
define_method(:"#{name}?") do
|
|
71
|
+
# @type self: Resource
|
|
72
|
+
!!value_of(keys)
|
|
73
|
+
end
|
|
74
|
+
record_attribute(:"#{name}?")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Define a reader that reads a timestamp attribute as a Time
|
|
78
|
+
#
|
|
79
|
+
# The Fleet API writes a timestamp as the milliseconds since the Unix epoch. The reader raises {InvalidResponse}
|
|
80
|
+
# when the attribute is not a number of them.
|
|
81
|
+
#
|
|
82
|
+
# @api private
|
|
83
|
+
# @param name [Symbol] the name of the reader
|
|
84
|
+
# @param keys [Array<String, Symbol>] the attribute keys, most preferred first (defaults to the name)
|
|
85
|
+
# @return [Symbol] the name of the reader
|
|
86
|
+
# @example Declare a reader that parses a timestamp field
|
|
87
|
+
# time_attribute :timestamp
|
|
88
|
+
def time_attribute(name, *keys)
|
|
89
|
+
keys = keys_for(name, keys)
|
|
90
|
+
define_method(name) do
|
|
91
|
+
# @type self: Resource
|
|
92
|
+
value = value_of(keys)
|
|
93
|
+
value && parse_time(value)
|
|
94
|
+
end
|
|
95
|
+
record_attribute(name)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Define a reader that wraps a nested object in a resource
|
|
99
|
+
#
|
|
100
|
+
# @api private
|
|
101
|
+
# @param name [Symbol] the name of the reader
|
|
102
|
+
# @param resource_class [Class<Resource>] the resource the nested object is wrapped in
|
|
103
|
+
# @param keys [Array<String, Symbol>] the attribute keys, most preferred first (defaults to the name)
|
|
104
|
+
# @return [Symbol] the name of the reader
|
|
105
|
+
# @example Declare a reader that wraps a nested object
|
|
106
|
+
# resource_attribute :charge_state, ChargeState
|
|
107
|
+
def resource_attribute(name, resource_class, *keys)
|
|
108
|
+
keys = keys_for(name, keys)
|
|
109
|
+
define_method(name) do
|
|
110
|
+
# @type self: Resource
|
|
111
|
+
value = value_of(keys)
|
|
112
|
+
value && resource_class.new(value)
|
|
113
|
+
end
|
|
114
|
+
record_attribute(name)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Define a reader that wraps each of a list of nested objects in a resource
|
|
118
|
+
#
|
|
119
|
+
# @api private
|
|
120
|
+
# @param name [Symbol] the name of the reader
|
|
121
|
+
# @param resource_class [Class<Resource>] the resource each nested object is wrapped in
|
|
122
|
+
# @param keys [Array<String, Symbol>] the attribute keys, most preferred first (defaults to the name)
|
|
123
|
+
# @return [Symbol] the name of the reader
|
|
124
|
+
# @example Declare a reader that wraps a list of nested objects
|
|
125
|
+
# resource_list_attribute :superchargers, ChargingSite
|
|
126
|
+
def resource_list_attribute(name, resource_class, *keys)
|
|
127
|
+
keys = keys_for(name, keys)
|
|
128
|
+
define_method(name) do
|
|
129
|
+
# @type self: Resource
|
|
130
|
+
resource_class.list(value_of(keys) || [])
|
|
131
|
+
end
|
|
132
|
+
record_attribute(name)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# The names of the readers the class declares
|
|
136
|
+
#
|
|
137
|
+
# These are the readers declared with attribute, predicate, time_attribute, resource_attribute, and
|
|
138
|
+
# resource_list_attribute, and a pattern matches a resource by these names (see {#deconstruct_keys}).
|
|
139
|
+
#
|
|
140
|
+
# A subclass of a resource inherits the readers its superclass declared, until it declares readers of its own.
|
|
141
|
+
#
|
|
142
|
+
# @api public
|
|
143
|
+
# @return [Array<Symbol>] the reader names, in the order they were declared
|
|
144
|
+
# @example
|
|
145
|
+
# Tesla::User.attribute_names # => [:email, :full_name, :profile_image_url]
|
|
146
|
+
def attribute_names
|
|
147
|
+
@attribute_names || (superclass.attribute_names if superclass.respond_to?(:attribute_names)) || [] # steep:ignore NoMethod
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private
|
|
151
|
+
|
|
152
|
+
# Record a declared reader for {.attribute_names}
|
|
153
|
+
#
|
|
154
|
+
# @api private
|
|
155
|
+
# @param name [Symbol] the name of the reader
|
|
156
|
+
# @return [Symbol] the name of the reader
|
|
157
|
+
def record_attribute(name)
|
|
158
|
+
@attribute_names = [*attribute_names, name]
|
|
159
|
+
name
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# The attribute keys a reader reads
|
|
163
|
+
#
|
|
164
|
+
# @api private
|
|
165
|
+
# @param name [Symbol] the name of the reader
|
|
166
|
+
# @param keys [Array<String, Symbol>] the declared keys
|
|
167
|
+
# @return [Array<String>] the keys, most preferred first
|
|
168
|
+
def keys_for(name, keys)
|
|
169
|
+
(keys.empty? ? [name] : keys).map(&:to_s)
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
extend Declaration
|
|
174
|
+
|
|
175
|
+
# Declare which readers appear in the inspect output
|
|
176
|
+
#
|
|
177
|
+
# @api private
|
|
178
|
+
# @param readers [Array<Symbol>] the readers to show
|
|
179
|
+
# @return [Array<Symbol>] the readers to show
|
|
180
|
+
# @example Declare the readers a resource inspects with
|
|
181
|
+
# inspect_with :vin, :display_name
|
|
182
|
+
def self.inspect_with(*readers)
|
|
183
|
+
@inspect_readers = readers
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# The readers shown in the inspect output
|
|
187
|
+
#
|
|
188
|
+
# @api private
|
|
189
|
+
# @return [Array<Symbol>] the readers to show, inherited from the superclass until the class declares its own
|
|
190
|
+
# @example
|
|
191
|
+
# Tesla::Vehicle.inspect_readers # => [:vin, :display_name, :state]
|
|
192
|
+
def self.inspect_readers
|
|
193
|
+
@inspect_readers || (superclass.inspect_readers if superclass.respond_to?(:inspect_readers)) || [] # steep:ignore NoMethod
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Declare which readers identify the resource
|
|
197
|
+
#
|
|
198
|
+
# Resources with an identity compare equal when those readers match, even if other attributes differ.
|
|
199
|
+
#
|
|
200
|
+
# @api private
|
|
201
|
+
# @param readers [Array<Symbol>] the identifying readers
|
|
202
|
+
# @return [Array<Symbol>] the identifying readers
|
|
203
|
+
# @example Declare what identifies a resource
|
|
204
|
+
# identified_by :vin
|
|
205
|
+
def self.identified_by(*readers)
|
|
206
|
+
@identity_readers = readers
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# The readers that identify the resource
|
|
210
|
+
#
|
|
211
|
+
# @api private
|
|
212
|
+
# @return [Array<Symbol>] the identifying readers, inherited from the superclass until the class declares its
|
|
213
|
+
# own, and empty when the resource is identified by all of its attributes
|
|
214
|
+
# @example
|
|
215
|
+
# Tesla::Vehicle.identity_readers # => [:vin]
|
|
216
|
+
def self.identity_readers
|
|
217
|
+
@identity_readers || (superclass.identity_readers if superclass.respond_to?(:identity_readers)) || [] # steep:ignore NoMethod
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Initialize a new resource
|
|
221
|
+
#
|
|
222
|
+
# The attributes are deeply copied and frozen, so resources are immutable values and the hash passed in
|
|
223
|
+
# stays mutable. Symbol keys are converted to strings, at every level.
|
|
224
|
+
#
|
|
225
|
+
# @api public
|
|
226
|
+
# @param attributes [Hash{String, Symbol => Object}] the raw attributes from the API response
|
|
227
|
+
# @return [Resource] a new instance
|
|
228
|
+
# @example Wrap a parsed response
|
|
229
|
+
# Tesla::Vehicle.new(JSON.parse(body).fetch("response"))
|
|
230
|
+
def initialize(attributes)
|
|
231
|
+
@attributes = deep_freeze(attributes)
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
# Read a raw attribute
|
|
235
|
+
#
|
|
236
|
+
# @api public
|
|
237
|
+
# @param key [String, Symbol] the attribute key
|
|
238
|
+
# @return [Object, nil] the attribute value
|
|
239
|
+
# @example Read an attribute that has no reader
|
|
240
|
+
# vehicle[:tokens]
|
|
241
|
+
def [](key)
|
|
242
|
+
attributes[key.to_s]
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Convert the resource to a hash
|
|
246
|
+
#
|
|
247
|
+
# The hash is a copy the caller owns and may change, as the hash `to_h` answers with elsewhere in Ruby is, and
|
|
248
|
+
# so is everything nested in it: the hashes, arrays, and strings a resource holds are frozen, so they are copied
|
|
249
|
+
# rather than handed to the caller to raise `FrozenError` on. {#attributes} answers with the frozen hash itself,
|
|
250
|
+
# for reading it without the copy.
|
|
251
|
+
#
|
|
252
|
+
# @api public
|
|
253
|
+
# @return [Hash{String => Object}] a copy of the raw attributes
|
|
254
|
+
# @example Convert a vehicle to a hash
|
|
255
|
+
# vehicle.to_h
|
|
256
|
+
# @example Add a field of your own to the copy
|
|
257
|
+
# vehicle.to_h.merge!("fetched_at" => Time.now)
|
|
258
|
+
# @example Change a field nested in the copy
|
|
259
|
+
# data.to_h["charge_state"]["fetched_at"] = Time.now
|
|
260
|
+
def to_h
|
|
261
|
+
deep_dup(attributes) #: Hash[String, untyped]
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# The attributes a pattern asks for, read by the declared readers
|
|
265
|
+
#
|
|
266
|
+
# A resource matches a `case`/`in` pattern by the names in {.attribute_names}, read as the readers read them, so
|
|
267
|
+
# a pattern sees a timestamp as a `Time` and a boolean as a predicate such as `in_service?`.
|
|
268
|
+
#
|
|
269
|
+
# A name whose reader raises {InvalidResponse}, such as a timestamp that is not a number, is left out rather than
|
|
270
|
+
# raised from: a pattern asking for it does not match, and one asking for the rest of the attributes, such as
|
|
271
|
+
# `in {vin:, **rest}`, matches without it. Reading that reader still raises, so a caller that asks for the
|
|
272
|
+
# attribute is told why it cannot be read.
|
|
273
|
+
#
|
|
274
|
+
# @api public
|
|
275
|
+
# @param keys [Array<Symbol>, nil] the names the pattern asks for, or nil for all of them
|
|
276
|
+
# @return [Hash{Symbol => Object}] the requested attributes, without the ones that cannot be read
|
|
277
|
+
# @example Match a vehicle by its state
|
|
278
|
+
# case Tesla.vehicle("5YJSA11111111111")
|
|
279
|
+
# in {state: "online", display_name:} then display_name
|
|
280
|
+
# end
|
|
281
|
+
def deconstruct_keys(keys)
|
|
282
|
+
names = self.class.attribute_names
|
|
283
|
+
names &= keys unless keys.nil?
|
|
284
|
+
requested = {} #: Hash[Symbol, untyped]
|
|
285
|
+
names.each do |name|
|
|
286
|
+
requested[name] = public_send(name)
|
|
287
|
+
rescue InvalidResponse
|
|
288
|
+
# The attribute cannot be read, so the pattern is answered without it
|
|
289
|
+
end
|
|
290
|
+
requested
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Compare with another resource
|
|
294
|
+
#
|
|
295
|
+
# @api public
|
|
296
|
+
# @param other [Object] the object to compare with
|
|
297
|
+
# @return [Boolean] true if the other object is the same kind of resource with the same identity
|
|
298
|
+
# @example Compare two vehicles
|
|
299
|
+
# Tesla.vehicle("5YJSA11111111111") == Tesla.vehicle("5YJSA11111111111") # => true
|
|
300
|
+
def ==(other)
|
|
301
|
+
other.instance_of?(self.class) && identity == other.identity
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Compare with another resource for use as a hash key
|
|
305
|
+
#
|
|
306
|
+
# Unlike {#==}, identity values are compared with `eql?`, so this agrees with {#hash}.
|
|
307
|
+
#
|
|
308
|
+
# @api public
|
|
309
|
+
# @param other [Object] the object to compare with
|
|
310
|
+
# @return [Boolean] true if the other object is the same kind of resource with an eql? identity
|
|
311
|
+
# @example Compare two vehicles strictly
|
|
312
|
+
# Tesla.vehicle("5YJSA11111111111").eql?(Tesla.vehicle("5YJSA11111111111")) # => true
|
|
313
|
+
def eql?(other)
|
|
314
|
+
other.instance_of?(self.class) && identity.eql?(other.identity)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Summarize the resource for the console
|
|
318
|
+
#
|
|
319
|
+
# Only the readers declared with {.inspect_with} are shown, so the output stays short and never includes secrets.
|
|
320
|
+
#
|
|
321
|
+
# @api public
|
|
322
|
+
# @return [String] the summary
|
|
323
|
+
# @example Inspect a vehicle
|
|
324
|
+
# vehicle.inspect # => #<Tesla::Vehicle vin="5YJSA11111111111" display_name="Nikola 2.0" state="online">
|
|
325
|
+
def inspect
|
|
326
|
+
fields = inspect_values.map { |reader, value| " #{reader}=#{value.inspect}" }
|
|
327
|
+
"#<#{self.class}#{fields.join}>"
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Summarize the resource as a String
|
|
331
|
+
#
|
|
332
|
+
# The summary {#inspect} answers with, so that a resource written into a message or a log reads as the one the
|
|
333
|
+
# console shows rather than as the address the object sits at.
|
|
334
|
+
#
|
|
335
|
+
# @api public
|
|
336
|
+
# @return [String] the summary
|
|
337
|
+
# @example Write a vehicle into a message
|
|
338
|
+
# "woke #{vehicle}" # => 'woke #<Tesla::Vehicle vin="5YJSA11111111111" display_name="Nikola 2.0" state="online">'
|
|
339
|
+
def to_s = inspect
|
|
340
|
+
|
|
341
|
+
# Generate a hash code for the resource
|
|
342
|
+
#
|
|
343
|
+
# @api public
|
|
344
|
+
# @return [Integer] the hash code
|
|
345
|
+
# @example Use resources as hash keys
|
|
346
|
+
# {vehicle => true}
|
|
347
|
+
def hash
|
|
348
|
+
[self.class, identity].hash
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
protected
|
|
352
|
+
|
|
353
|
+
# The values that identify the resource
|
|
354
|
+
#
|
|
355
|
+
# A resource answers with these to the resource it is compared with, rather than to a caller, which reads the
|
|
356
|
+
# readers {.identified_by} names or the attributes themselves: the values are what the comparison is made of,
|
|
357
|
+
# and are an Array of readers for one resource and the whole of the attributes for another.
|
|
358
|
+
#
|
|
359
|
+
# @api private
|
|
360
|
+
# @return [Array<Object>, Hash{String => Object}] the identifying values, or all attributes when no identity is
|
|
361
|
+
# declared
|
|
362
|
+
# @example Compare two vehicles by the VIN that identifies them
|
|
363
|
+
# Tesla.vehicle("5YJSA11111111111") == Tesla.vehicle("5YJSA11111111111") # => true
|
|
364
|
+
def identity
|
|
365
|
+
readers = self.class.identity_readers
|
|
366
|
+
readers.empty? ? attributes : readers.map { |reader| public_send(reader) }
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
private
|
|
370
|
+
|
|
371
|
+
# The readers {#inspect} shows, with their values
|
|
372
|
+
#
|
|
373
|
+
# These are the readers {.inspect_with} declares, which a resource that shows more when it has it adds to.
|
|
374
|
+
#
|
|
375
|
+
# @api private
|
|
376
|
+
# @return [Hash{Symbol => Object}] the value of each reader shown, in the order they are shown
|
|
377
|
+
def inspect_values = self.class.inspect_readers.to_h { |reader| [reader, public_send(reader)] }
|
|
378
|
+
|
|
379
|
+
# The value of the first of the given keys the response contains
|
|
380
|
+
# @api private
|
|
381
|
+
# @param keys [Array<String>] the attribute keys, most preferred first
|
|
382
|
+
# @return [Object, nil] the value, or nil when the response contains none of the keys
|
|
383
|
+
def value_of(keys)
|
|
384
|
+
keys.each { |key| return self[key] if attributes.key?(key) }
|
|
385
|
+
nil
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# Read a timestamp attribute as a Time in UTC
|
|
389
|
+
#
|
|
390
|
+
# The attribute is the milliseconds since the Unix epoch, which are read as a Rational number of seconds so that
|
|
391
|
+
# none of them are lost to a Float.
|
|
392
|
+
#
|
|
393
|
+
# @api private
|
|
394
|
+
# @param value [Object] the attribute value
|
|
395
|
+
# @return [Time] the time
|
|
396
|
+
# @raise [InvalidResponse] if the value is not a timestamp
|
|
397
|
+
def parse_time(value)
|
|
398
|
+
Time.at(Rational(value, 1000), in: "UTC")
|
|
399
|
+
rescue ArgumentError, TypeError
|
|
400
|
+
raise InvalidResponse.new(body: value.to_s, message: "#{value.inspect} is not a timestamp")
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require_relative "errors/bad_gateway"
|
|
5
|
+
require_relative "errors/bad_request"
|
|
6
|
+
require_relative "errors/client_error"
|
|
7
|
+
require_relative "errors/device_unexpected_response"
|
|
8
|
+
require_relative "errors/forbidden"
|
|
9
|
+
require_relative "errors/gateway_timeout"
|
|
10
|
+
require_relative "errors/http_error"
|
|
11
|
+
require_relative "errors/internal_server_error"
|
|
12
|
+
require_relative "errors/locked"
|
|
13
|
+
require_relative "errors/method_not_allowed"
|
|
14
|
+
require_relative "errors/misdirected_request"
|
|
15
|
+
require_relative "errors/not_acceptable"
|
|
16
|
+
require_relative "errors/not_found"
|
|
17
|
+
require_relative "errors/payment_required"
|
|
18
|
+
require_relative "errors/precondition_failed"
|
|
19
|
+
require_relative "errors/request_timeout"
|
|
20
|
+
require_relative "errors/server_error"
|
|
21
|
+
require_relative "errors/service_unavailable"
|
|
22
|
+
require_relative "errors/too_many_requests"
|
|
23
|
+
require_relative "errors/unauthorized"
|
|
24
|
+
require_relative "errors/unavailable_for_legal_reasons"
|
|
25
|
+
require_relative "errors/unprocessable_content"
|
|
26
|
+
|
|
27
|
+
module Tesla
|
|
28
|
+
# Parses HTTP responses from the Tesla Fleet API
|
|
29
|
+
# @api private
|
|
30
|
+
class ResponseParser
|
|
31
|
+
# Mapping of HTTP status codes to error classes
|
|
32
|
+
ERROR_MAP = {
|
|
33
|
+
400 => BadRequest,
|
|
34
|
+
401 => Unauthorized,
|
|
35
|
+
402 => PaymentRequired,
|
|
36
|
+
403 => Forbidden,
|
|
37
|
+
404 => NotFound,
|
|
38
|
+
405 => MethodNotAllowed,
|
|
39
|
+
406 => NotAcceptable,
|
|
40
|
+
408 => RequestTimeout,
|
|
41
|
+
412 => PreconditionFailed,
|
|
42
|
+
421 => MisdirectedRequest,
|
|
43
|
+
422 => UnprocessableContent,
|
|
44
|
+
423 => Locked,
|
|
45
|
+
429 => TooManyRequests,
|
|
46
|
+
451 => UnavailableForLegalReasons,
|
|
47
|
+
500 => InternalServerError,
|
|
48
|
+
502 => BadGateway,
|
|
49
|
+
503 => ServiceUnavailable,
|
|
50
|
+
504 => GatewayTimeout,
|
|
51
|
+
540 => DeviceUnexpectedResponse
|
|
52
|
+
}.freeze
|
|
53
|
+
|
|
54
|
+
# The error classes for the 4xx and 5xx statuses ERROR_MAP does not name
|
|
55
|
+
STATUS_CLASS_ERRORS = {4 => ClientError, 5 => ServerError}.freeze
|
|
56
|
+
private_constant :ERROR_MAP, :STATUS_CLASS_ERRORS
|
|
57
|
+
|
|
58
|
+
# Parse an HTTP response
|
|
59
|
+
#
|
|
60
|
+
# @api private
|
|
61
|
+
# @param response [Net::HTTPResponse] the HTTP response to parse
|
|
62
|
+
# @return [String] the response body
|
|
63
|
+
# @raise [HTTPError] if the response is not successful
|
|
64
|
+
# @example Parse a response
|
|
65
|
+
# parser.parse(response: response)
|
|
66
|
+
def parse(response:)
|
|
67
|
+
raise error(response) unless response.is_a?(Net::HTTPSuccess)
|
|
68
|
+
|
|
69
|
+
response.body.to_s
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Create an error from a response
|
|
75
|
+
# @api private
|
|
76
|
+
# @param response [Net::HTTPResponse] the HTTP response
|
|
77
|
+
# @return [HTTPError] the error
|
|
78
|
+
def error(response)
|
|
79
|
+
error_class(response).new(response:)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Get the error class for a response
|
|
83
|
+
# @api private
|
|
84
|
+
# @param response [Net::HTTPResponse] the HTTP response
|
|
85
|
+
# @return [Class] the error class: the one named for the status, else ClientError or ServerError for its class of
|
|
86
|
+
# status, else HTTPError
|
|
87
|
+
def error_class(response)
|
|
88
|
+
status = Integer(response.code)
|
|
89
|
+
ERROR_MAP.fetch(status) { STATUS_CLASS_ERRORS.fetch(status / 100, HTTPError) }
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
5
|
+
module Tesla
|
|
6
|
+
# Reads the `Retry-After` header of a response, for {HTTPError#retry_after} and for {RetryHandler}
|
|
7
|
+
#
|
|
8
|
+
# The Fleet API sends the header with a 429 Too Many Requests response, as a number of seconds or an HTTP date.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module RetryAfter
|
|
12
|
+
# A `Retry-After` header that is a number of seconds, which HTTP writes in decimal digits alone
|
|
13
|
+
DELTA_SECONDS = /\A-?\d+\z/
|
|
14
|
+
private_constant :DELTA_SECONDS
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# The seconds a response asks to wait before the request is sent again
|
|
19
|
+
#
|
|
20
|
+
# @api private
|
|
21
|
+
# @param response [Net::HTTPResponse] the HTTP response
|
|
22
|
+
# @return [Integer, nil] the seconds to wait, rounded up and never negative, or nil when the response has no
|
|
23
|
+
# `Retry-After` header or one that is neither a number of seconds nor an HTTP date
|
|
24
|
+
def retry_after_of(response)
|
|
25
|
+
value = response["Retry-After"]
|
|
26
|
+
return if value.nil?
|
|
27
|
+
|
|
28
|
+
seconds = DELTA_SECONDS.match?(value) ? value.to_i : (Time.httpdate(value) - Time.now).ceil
|
|
29
|
+
[seconds, 0].max
|
|
30
|
+
rescue ArgumentError
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|