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,230 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "simple_oauth"
|
|
4
|
+
require "uri"
|
|
5
|
+
require_relative "authenticator"
|
|
6
|
+
require_relative "bearer_token_authenticator"
|
|
7
|
+
require_relative "errors/oauth_error"
|
|
8
|
+
|
|
9
|
+
module Tesla
|
|
10
|
+
# The credentials of a client, and the tokens it asks Tesla's authorization server for, mixed into {Client}
|
|
11
|
+
#
|
|
12
|
+
# The requests to the token endpoint are built by the simple_oauth gem and sent on the connection of the client,
|
|
13
|
+
# so that the timeouts, the proxy, and the certificates of the client apply to them.
|
|
14
|
+
#
|
|
15
|
+
# A refresh token of the Fleet API is used once: refreshing the access token answers with another refresh token,
|
|
16
|
+
# and the one it was asked for with stops working a day later. Two threads refreshing with the same refresh token
|
|
17
|
+
# would each be answered with tokens of their own, so a client refreshes one thread at a time, and a thread that
|
|
18
|
+
# waited for another to refresh uses the token that thread was answered with rather than refreshing again.
|
|
19
|
+
#
|
|
20
|
+
# @api private
|
|
21
|
+
module ClientCredentials
|
|
22
|
+
# The access token requests are authorized with
|
|
23
|
+
# @api public
|
|
24
|
+
# @return [String, nil] the access token
|
|
25
|
+
# @example Get or set the access token
|
|
26
|
+
# client.access_token = "eyJhbGciOiJSUzI1NiIs"
|
|
27
|
+
attr_accessor :access_token
|
|
28
|
+
|
|
29
|
+
# The refresh token a new access token is asked for with
|
|
30
|
+
#
|
|
31
|
+
# This is the refresh token the last refresh answered with, once the client has refreshed its access token.
|
|
32
|
+
#
|
|
33
|
+
# @api public
|
|
34
|
+
# @return [String, nil] the refresh token
|
|
35
|
+
# @example Get or set the refresh token
|
|
36
|
+
# client.refresh_token = "NA_9d0b4bfd0c4c3a6e"
|
|
37
|
+
attr_accessor :refresh_token
|
|
38
|
+
|
|
39
|
+
# The client ID of the application, as developer.tesla.com issued it
|
|
40
|
+
# @api public
|
|
41
|
+
# @return [String, nil] the client ID
|
|
42
|
+
# @example Get or set the client ID
|
|
43
|
+
# client.client_id = "81527cff06843c8634fdc09e8ac0abef"
|
|
44
|
+
attr_accessor :client_id
|
|
45
|
+
|
|
46
|
+
# The client secret of the application, as developer.tesla.com issued it
|
|
47
|
+
# @api public
|
|
48
|
+
# @return [String, nil] the client secret
|
|
49
|
+
# @example Get or set the client secret
|
|
50
|
+
# client.client_secret = "ta-secret.7vx6VkVvk2gBgSrN"
|
|
51
|
+
attr_accessor :client_secret
|
|
52
|
+
|
|
53
|
+
# The URL the authorization server sends a user back to
|
|
54
|
+
# @api public
|
|
55
|
+
# @return [String, nil] the redirect URI
|
|
56
|
+
# @example Get or set the redirect URI
|
|
57
|
+
# client.redirect_uri = "https://example.com/auth/callback"
|
|
58
|
+
attr_accessor :redirect_uri
|
|
59
|
+
|
|
60
|
+
# Set the audience tokens are issued for
|
|
61
|
+
# @api public
|
|
62
|
+
# @param value [String, nil] the host of the Fleet API the tokens are used with, or nil for the host of the client
|
|
63
|
+
# @return [void]
|
|
64
|
+
# @example Issue tokens for the Fleet API for Europe while requests are sent through a proxy
|
|
65
|
+
# client.audience = Tesla::Configuration::EUROPE_HOST
|
|
66
|
+
attr_writer :audience
|
|
67
|
+
|
|
68
|
+
# The URL a user is sent to in order to authorize the application
|
|
69
|
+
# @api public
|
|
70
|
+
# @return [String] the authorization endpoint
|
|
71
|
+
# @example Get or set the authorization endpoint
|
|
72
|
+
# client.authorization_endpoint = "https://auth.tesla.cn/oauth2/v3/authorize"
|
|
73
|
+
attr_accessor :authorization_endpoint
|
|
74
|
+
|
|
75
|
+
# The URL tokens are issued and refreshed at
|
|
76
|
+
# @api public
|
|
77
|
+
# @return [String] the token endpoint
|
|
78
|
+
# @example Get or set the token endpoint
|
|
79
|
+
# client.token_endpoint = "https://auth.tesla.cn/oauth2/v3/token"
|
|
80
|
+
attr_accessor :token_endpoint
|
|
81
|
+
|
|
82
|
+
# What is called with the token each time the access token is refreshed
|
|
83
|
+
# @api public
|
|
84
|
+
# @return [#call, nil] what is called with the `SimpleOAuth::OAuth2::Token`, or nil to call nothing
|
|
85
|
+
# @example Store the tokens each time they are refreshed
|
|
86
|
+
# client.on_token_refresh = ->(token) { File.write("refresh_token", token.refresh_token) }
|
|
87
|
+
attr_accessor :on_token_refresh
|
|
88
|
+
|
|
89
|
+
# The audience tokens are issued for
|
|
90
|
+
#
|
|
91
|
+
# @api public
|
|
92
|
+
# @return [String] the host of the Fleet API the tokens are used with, which is the host of the client unless
|
|
93
|
+
# another has been assigned
|
|
94
|
+
# @example Get the audience
|
|
95
|
+
# client.audience
|
|
96
|
+
def audience
|
|
97
|
+
@audience || host
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
# Assign the credentials the client is built with
|
|
103
|
+
#
|
|
104
|
+
# @api private
|
|
105
|
+
# @param credentials [Hash{Symbol => Object}] the credentials, each named as the setting it is assigned to
|
|
106
|
+
# @return [void]
|
|
107
|
+
def initialize_credentials(**credentials)
|
|
108
|
+
@token_mutex = Mutex.new
|
|
109
|
+
credentials.each { |setting, value| public_send(:"#{setting}=", value) }
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# The authenticator for a request authorized with an access token
|
|
113
|
+
#
|
|
114
|
+
# @api private
|
|
115
|
+
# @param access_token [String, nil] the access token, or nil to send the request without one
|
|
116
|
+
# @return [Authenticator] the authenticator
|
|
117
|
+
def authenticator_for(access_token)
|
|
118
|
+
access_token ? BearerTokenAuthenticator.new(access_token:) : Authenticator.new
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Whether the client has what a refresh of its access token takes
|
|
122
|
+
#
|
|
123
|
+
# @api private
|
|
124
|
+
# @return [Boolean] whether the client has a refresh token and a client ID
|
|
125
|
+
def refreshable?
|
|
126
|
+
[refresh_token, client_id].none?(&:nil?)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Run a block while no other thread refreshes the tokens of the client
|
|
130
|
+
#
|
|
131
|
+
# @api private
|
|
132
|
+
# @yield while the tokens are held
|
|
133
|
+
# @return [Object] what the block returned
|
|
134
|
+
def exclusively(&)
|
|
135
|
+
@token_mutex.synchronize(&)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# The access token to authorize a request with, refreshed when it is stale
|
|
139
|
+
#
|
|
140
|
+
# The token is refreshed only while it is still the stale one, so that a thread that waited for another to
|
|
141
|
+
# refresh it is given the token that thread was answered with rather than using a refresh token a second time.
|
|
142
|
+
# A client without an access token is given one this way before its first request, since nil is the token it
|
|
143
|
+
# has until then.
|
|
144
|
+
#
|
|
145
|
+
# @api private
|
|
146
|
+
# @param stale [String, nil] the access token that is no longer good, or nil for none
|
|
147
|
+
# @return [String, nil] the access token, or nil when the client has none and cannot refresh
|
|
148
|
+
def renewed_access_token(stale)
|
|
149
|
+
exclusively do
|
|
150
|
+
refresh if refreshable? && access_token.eql?(stale)
|
|
151
|
+
access_token
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Ask the token endpoint for a new access token with the refresh token
|
|
156
|
+
#
|
|
157
|
+
# @api private
|
|
158
|
+
# @return [SimpleOAuth::OAuth2::Token] the token
|
|
159
|
+
# @raise [ArgumentError] if the client has no refresh token or no client ID
|
|
160
|
+
# @raise [OAuthError] if the token endpoint turns the request away
|
|
161
|
+
def refresh
|
|
162
|
+
current = refresh_token
|
|
163
|
+
raise ArgumentError, "A refresh_token is required to refresh the access token" if current.nil?
|
|
164
|
+
|
|
165
|
+
token = store_token(request_token(oauth_client.refresh_token_request(refresh_token: current)))
|
|
166
|
+
on_token_refresh&.call(token)
|
|
167
|
+
token
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Keep the tokens the token endpoint answered with
|
|
171
|
+
#
|
|
172
|
+
# The refresh token is kept as it was when the response carries none.
|
|
173
|
+
#
|
|
174
|
+
# @api private
|
|
175
|
+
# @param token [SimpleOAuth::OAuth2::Token] the token
|
|
176
|
+
# @return [SimpleOAuth::OAuth2::Token] the token
|
|
177
|
+
def store_token(token)
|
|
178
|
+
@access_token = token.access_token
|
|
179
|
+
@refresh_token = token.refresh_token || refresh_token
|
|
180
|
+
token
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# The OAuth 2.0 client that builds the requests for the authorization server
|
|
184
|
+
#
|
|
185
|
+
# The client secret is sent in the body of a request, as the token endpoint reads it.
|
|
186
|
+
#
|
|
187
|
+
# @api private
|
|
188
|
+
# @return [SimpleOAuth::OAuth2::Client] the OAuth 2.0 client
|
|
189
|
+
# @raise [ArgumentError] if the client has no client ID
|
|
190
|
+
def oauth_client
|
|
191
|
+
id = client_id
|
|
192
|
+
raise ArgumentError, "A client_id is required to ask the authorization server for a token" if id.nil?
|
|
193
|
+
|
|
194
|
+
SimpleOAuth::OAuth2::Client.new(client_id: id, client_secret:, authorization_endpoint:, token_endpoint:,
|
|
195
|
+
auth_method: :client_secret_post)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Send a request to the token endpoint, and read the token it answers with
|
|
199
|
+
#
|
|
200
|
+
# @api private
|
|
201
|
+
# @param oauth_request [SimpleOAuth::OAuth2::Request] the request, as simple_oauth built it
|
|
202
|
+
# @return [SimpleOAuth::OAuth2::Token] the token
|
|
203
|
+
# @raise [OAuthError] if the token endpoint turns the request away, or answers without a token
|
|
204
|
+
# @raise [NetworkError] if the request is lost to the network
|
|
205
|
+
def request_token(oauth_request)
|
|
206
|
+
response = perform_token_request(oauth_request)
|
|
207
|
+
SimpleOAuth::OAuth2::Token.from_response(status: response.code, body: response.body)
|
|
208
|
+
rescue SimpleOAuth::OAuth2::Error => e
|
|
209
|
+
raise OAuthError.new(code: e.code, description: e.description, status: e.status)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# Send a request to the token endpoint
|
|
213
|
+
#
|
|
214
|
+
# A request a rate limiter turned away is sent again, as any request is, but one that went unanswered is not,
|
|
215
|
+
# since the token endpoint may have used the refresh token it carried.
|
|
216
|
+
#
|
|
217
|
+
# @api private
|
|
218
|
+
# @param oauth_request [SimpleOAuth::OAuth2::Request] the request, as simple_oauth built it
|
|
219
|
+
# @return [Net::HTTPResponse] the response
|
|
220
|
+
# @raise [NetworkError] if the request is lost to the network
|
|
221
|
+
def perform_token_request(oauth_request)
|
|
222
|
+
uri = URI(oauth_request.url)
|
|
223
|
+
body = oauth_request.body
|
|
224
|
+
headers = oauth_request.headers
|
|
225
|
+
retry_handler.handle(retry_unanswered: false) do
|
|
226
|
+
connection.perform(request: request_builder.build(http_method: :post, uri:, body:, headers:))
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "resource"
|
|
4
|
+
|
|
5
|
+
module Tesla
|
|
6
|
+
# The state of the climate control of a vehicle
|
|
7
|
+
# @api public
|
|
8
|
+
class ClimateState < Resource
|
|
9
|
+
inspect_with :inside_temp, :driver_temp_setting
|
|
10
|
+
|
|
11
|
+
# @!method inside_temp
|
|
12
|
+
# The temperature inside the vehicle in degrees Celsius
|
|
13
|
+
# @api public
|
|
14
|
+
# @return [Float, nil] the temperature inside the vehicle
|
|
15
|
+
# @example
|
|
16
|
+
# climate_state.inside_temp
|
|
17
|
+
attribute :inside_temp
|
|
18
|
+
|
|
19
|
+
# @!method outside_temp
|
|
20
|
+
# The temperature outside the vehicle in degrees Celsius
|
|
21
|
+
# @api public
|
|
22
|
+
# @return [Float, nil] the temperature outside the vehicle
|
|
23
|
+
# @example
|
|
24
|
+
# climate_state.outside_temp
|
|
25
|
+
attribute :outside_temp
|
|
26
|
+
|
|
27
|
+
# @!method driver_temp_setting
|
|
28
|
+
# The temperature the driver's side is set to in degrees Celsius
|
|
29
|
+
# @api public
|
|
30
|
+
# @return [Float, nil] the temperature the driver's side is set to
|
|
31
|
+
# @example
|
|
32
|
+
# climate_state.driver_temp_setting
|
|
33
|
+
attribute :driver_temp_setting
|
|
34
|
+
|
|
35
|
+
# @!method passenger_temp_setting
|
|
36
|
+
# The temperature the passenger's side is set to in degrees Celsius
|
|
37
|
+
# @api public
|
|
38
|
+
# @return [Float, nil] the temperature the passenger's side is set to
|
|
39
|
+
# @example
|
|
40
|
+
# climate_state.passenger_temp_setting
|
|
41
|
+
attribute :passenger_temp_setting
|
|
42
|
+
|
|
43
|
+
# @!method fan_status
|
|
44
|
+
# The speed of the fan
|
|
45
|
+
# @api public
|
|
46
|
+
# @return [Integer, nil] the speed of the fan
|
|
47
|
+
# @example
|
|
48
|
+
# climate_state.fan_status
|
|
49
|
+
attribute :fan_status
|
|
50
|
+
|
|
51
|
+
# @!method climate_keeper_mode
|
|
52
|
+
# The Climate Keeper mode, such as "off", "dog", or "camp"
|
|
53
|
+
# @api public
|
|
54
|
+
# @return [String, nil] the Climate Keeper mode
|
|
55
|
+
# @example
|
|
56
|
+
# climate_state.climate_keeper_mode
|
|
57
|
+
attribute :climate_keeper_mode
|
|
58
|
+
|
|
59
|
+
# @!method seat_heater_left
|
|
60
|
+
# The level of the heater of the front left seat, from 0 to 3
|
|
61
|
+
# @api public
|
|
62
|
+
# @return [Integer, nil] the level of the heater of the front left seat
|
|
63
|
+
# @example
|
|
64
|
+
# climate_state.seat_heater_left
|
|
65
|
+
attribute :seat_heater_left
|
|
66
|
+
|
|
67
|
+
# @!method seat_heater_right
|
|
68
|
+
# The level of the heater of the front right seat, from 0 to 3
|
|
69
|
+
# @api public
|
|
70
|
+
# @return [Integer, nil] the level of the heater of the front right seat
|
|
71
|
+
# @example
|
|
72
|
+
# climate_state.seat_heater_right
|
|
73
|
+
attribute :seat_heater_right
|
|
74
|
+
|
|
75
|
+
# @!method climate_on?
|
|
76
|
+
# Whether the climate control is on
|
|
77
|
+
# @api public
|
|
78
|
+
# @return [Boolean] whether the climate control is on
|
|
79
|
+
# @example
|
|
80
|
+
# climate_state.climate_on?
|
|
81
|
+
predicate :climate_on, :is_climate_on
|
|
82
|
+
|
|
83
|
+
# @!method preconditioning?
|
|
84
|
+
# Whether the vehicle is preconditioning
|
|
85
|
+
# @api public
|
|
86
|
+
# @return [Boolean] whether the vehicle is preconditioning
|
|
87
|
+
# @example
|
|
88
|
+
# climate_state.preconditioning?
|
|
89
|
+
predicate :preconditioning, :is_preconditioning
|
|
90
|
+
|
|
91
|
+
# @!method front_defroster_on?
|
|
92
|
+
# Whether the front defroster is on
|
|
93
|
+
# @api public
|
|
94
|
+
# @return [Boolean] whether the front defroster is on
|
|
95
|
+
# @example
|
|
96
|
+
# climate_state.front_defroster_on?
|
|
97
|
+
predicate :front_defroster_on, :is_front_defroster_on
|
|
98
|
+
|
|
99
|
+
# @!method rear_defroster_on?
|
|
100
|
+
# Whether the rear defroster is on
|
|
101
|
+
# @api public
|
|
102
|
+
# @return [Boolean] whether the rear defroster is on
|
|
103
|
+
# @example
|
|
104
|
+
# climate_state.rear_defroster_on?
|
|
105
|
+
predicate :rear_defroster_on, :is_rear_defroster_on
|
|
106
|
+
|
|
107
|
+
# @!method timestamp
|
|
108
|
+
# The time the vehicle reported the state
|
|
109
|
+
# @api public
|
|
110
|
+
# @return [Time, nil] the time the vehicle reported the state
|
|
111
|
+
# @raise [InvalidResponse] if the timestamp is not a number of milliseconds
|
|
112
|
+
# @example
|
|
113
|
+
# climate_state.timestamp
|
|
114
|
+
time_attribute :timestamp
|
|
115
|
+
end
|
|
116
|
+
end
|