smartcar 3.8.0 → 3.9.5

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd4fcc86e7eb35c94b63f0546a0cac9795b8c9413e91da0631ddc04658e8d928
4
- data.tar.gz: 414d4999f1919ef259a9eaee0733e4e23bf60f29a53d667684c41142c88f3f91
3
+ metadata.gz: 516891931fbdcdde2425bfd191721725ac0461195f164be2f2dfe231412b2db1
4
+ data.tar.gz: 463981a1ae045345f3ed8aaeadaa894dea6bc2effe88ebed1500c90aeb94b22f
5
5
  SHA512:
6
- metadata.gz: 94512a886faac4382679adb6c6dc23464095e749907caab77c30914993cb12b084e1ea2377df9767b7bc855a14691204afb517764afa95242c4bd27575ef1695
7
- data.tar.gz: 4f1649632ff635bf6d6a806761b33c9fbd180f572ee31b30a5584971c3a3bcbe9407b3778a3d3acbcf9d22ff7ca3fd7aedad95975735ae3877bcb52f044613ee
6
+ metadata.gz: f5f916ff6589a296048636071fd0227ad0446078a8bdab9e34be8dbcef278f799b3c91369d5623be77a11efddf8781b4e07af77a36966fa46ebaa2c03310bd60
7
+ data.tar.gz: 7b8e0ebbf68942b14d29946039a10aa16b6de2f03d1d737b86a67245be4e53a656654857449716e0e69faf0a9289379257c3ff0f6b04fa21fc3c9c771cfa1e55
data/.buddy/cd.yml ADDED
@@ -0,0 +1,29 @@
1
+ - pipeline: cd
2
+ name: CD
3
+ fail_on_prepare_env_warning: true
4
+ events:
5
+ - type: PUSH
6
+ refs:
7
+ - refs/heads/master
8
+ actions:
9
+ - action: semantic release
10
+ type: BUILD
11
+ shell: BASH
12
+ docker_image_name: library/ruby
13
+ docker_image_tag: 2.7
14
+ execute_commands:
15
+ - "set -e # Exit immediately if a command exits with non-zero status"
16
+ - export GEM_NAME="smartcar"
17
+ - export GEMSPEC_FILE="ruby-sdk.gemspec"
18
+ - 'export GEM_HOST_API_KEY="${RUBYGEMSAPI_KEY}"'
19
+ - 'if [ "${BUDDY_EXECUTION_TAG}" != "" ] && [ "${BUDDY_EXECUTION_BRANCH}" == "master" ]; then'
20
+ - ' echo "Tagged commit on master branch detected, proceeding with deployment..."'
21
+ - " gem install bundler -v 2.3.1"
22
+ - " gem build ${GEMSPEC_FILE}"
23
+ - ' GEM_VERSION=$(ruby -e "require ''rubygems''; spec = Gem::Specification::load(''${GEMSPEC_FILE}''); puts spec.version")'
24
+ - ' echo "Publishing ${GEM_NAME} version ${GEM_VERSION} to RubyGems..."'
25
+ - ' gem push "${GEM_NAME}-${GEM_VERSION}.gem"'
26
+ - ' echo "Deployment to RubyGems completed successfully"'
27
+ - else
28
+ - ' echo "Skipping deployment: This is not a tagged commit on the master branch"'
29
+ - fi
data/.buddy/ci.yml ADDED
@@ -0,0 +1,69 @@
1
+ - pipeline: ci
2
+ name: CI
3
+ events:
4
+ - type: PUSH
5
+ refs:
6
+ - refs/heads/*
7
+ fail_on_prepare_env_warning: true
8
+ actions:
9
+ - action: Run tests
10
+ type: BUILD
11
+ docker_image_name: library/ruby
12
+ docker_image_tag: 3.3
13
+ execute_commands:
14
+ - "# Set Firefox environment variables to help with headless operation"
15
+ - Xvfb :99 -screen 0 1280x1024x24 &
16
+ - export DISPLAY=:99.0
17
+ - export MOZ_HEADLESS=1
18
+ - export MOZ_NO_REMOTE=1
19
+ - "export PATH=${HOME}/firefox-latest/firefox:$PATH"
20
+ - firefox --version
21
+ - firefox &
22
+ - ""
23
+ - "export PATH=${HOME}/geckodriver:$PATH"
24
+ - geckodriver --version
25
+ - ""
26
+ - export BUNDLE_GEMFILE=$PWD/Gemfile
27
+ - gem install bundler -v 2.3.1
28
+ - "bundle install --jobs=3 --retry=3 --deployment --path=${BUNDLE_PATH:-vendor/bundle}"
29
+ - ""
30
+ - export MODE=test
31
+ - bundle exec rake
32
+ setup_commands:
33
+ - apt-get update
34
+ - apt-get install -y \
35
+ - ' libgtk-3-0 \'
36
+ - ' libasound2 \'
37
+ - ' libdbus-glib-1-2 \'
38
+ - ' libx11-xcb1 \'
39
+ - ' libxt6 \'
40
+ - ' libnss3 \'
41
+ - ' libxtst6 \'
42
+ - ' libxss1 \'
43
+ - ' libpci3 \'
44
+ - ' libatk1.0-0 \'
45
+ - ' libatk-bridge2.0-0 \'
46
+ - ' libcups2 \'
47
+ - ' libdrm2 \'
48
+ - ' libxcomposite1 \'
49
+ - ' libxdamage1 \'
50
+ - ' libxfixes3 \'
51
+ - ' libxkbcommon0 \'
52
+ - ' libxrandr2 \'
53
+ - " xvfb"
54
+ - ""
55
+ - ""
56
+ - "# Download the latest Firefox"
57
+ - wget -O /tmp/firefox-latest.tar.xz 'https://download.mozilla.org/?product=firefox-latest&lang=en-US&os=linux64'
58
+ - "mkdir -p ${HOME}/firefox-latest"
59
+ - "tar -xJf /tmp/firefox-latest.tar.xz -C ${HOME}/firefox-latest"
60
+ - ""
61
+ - "# Download Geckodriver"
62
+ - wget -O /tmp/geckodriver.tar.xz 'https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-linux64.tar.gz'
63
+ - "mkdir -p ${HOME}/geckodriver"
64
+ - "tar -xzf /tmp/geckodriver.tar.xz -C ${HOME}/geckodriver"
65
+ services:
66
+ - type: SELENIUM_FIREFOX
67
+ connection:
68
+ host: selenium-ff
69
+ shell: BASH
data/.rubocop.yml CHANGED
@@ -48,3 +48,8 @@ Metrics/CyclomaticComplexity:
48
48
 
49
49
  Metrics/PerceivedComplexity:
50
50
  Max: 10
51
+
52
+ Layout/LineLength:
53
+ Max: 120
54
+ Exclude:
55
+ - 'lib/smartcar_error.rb'
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- smartcar (3.8.0)
4
+ smartcar (3.9.5)
5
5
  oauth2 (~> 1.4)
6
6
  recursive-open-struct (~> 1.1.3)
7
7
 
data/Rakefile CHANGED
@@ -3,7 +3,9 @@
3
3
  require 'bundler/gem_tasks'
4
4
  require 'rspec/core/rake_task'
5
5
 
6
- RSpec::Core::RakeTask.new(:spec)
6
+ RSpec::Core::RakeTask.new(:spec) do |t, args|
7
+ t.pattern = args.extras.join(' ') unless args.extras.empty?
8
+ end
7
9
 
8
10
  require 'rubocop/rake_task'
9
11
 
@@ -13,14 +13,16 @@ module Smartcar
13
13
  # @param [Hash] options
14
14
  # @option options[:client_id] [String] - Client ID, if not passed fallsback to ENV['SMARTCAR_CLIENT_ID']
15
15
  # @option options[:client_secret] [String] - Client Secret, if not passed fallsback to ENV['SMARTCAR_CLIENT_SECRET']
16
- # @option options[:redirect_uri] [String] - Redirect URI, if not passed fallsback to ENV['SMARTCAR_REDIRECT_URI']
16
+ # @option options[:redirect_uri] [String] - Redirect URI registered in the
17
+ # application settings. The given URL must exactly match one of the registered URLs.
18
+ # This parameter is optional and should normally be set within the Smartcar Dashboard.
17
19
  # @option options[:test_mode] [Boolean] - [DEPRECATED], please use `mode` instead.
18
20
  # Launch Smartcar Connect in [test mode](https://smartcar.com/docs/guides/testing/).
19
21
  # @option options[:mode] [String] - Determine what mode Smartcar Connect should be launched in.
20
22
  # Should be one of test, live or simulated.
21
23
  # @return [Smartcar::AuthClient] Returns a Smartcar::AuthClient Object that has other methods
22
24
  def initialize(options)
23
- options[:redirect_uri] ||= get_config('SMARTCAR_REDIRECT_URI')
25
+ options[:redirect_uri] ||= get_config('SMARTCAR_REDIRECT_URI', { nullable: true })
24
26
  options[:client_id] ||= get_config('SMARTCAR_CLIENT_ID')
25
27
  options[:client_secret] ||= get_config('SMARTCAR_CLIENT_SECRET')
26
28
  options[:auth_origin] = ENV['SMARTCAR_AUTH_ORIGIN'] || AUTH_ORIGIN
@@ -30,9 +32,11 @@ module Smartcar
30
32
  end
31
33
 
32
34
  # Generate the OAuth authorization URL.
33
- # @param scope [Array<String>] Array of permissions that specify what the user can access
35
+ # @param scope_or_options [Array<String>, Hash] Array of permissions that specify what the user can access
34
36
  # EXAMPLE : ['read_odometer', 'read_vehicle_info', 'required:read_location']
35
- # For further details refer to https://smartcar.com/docs/guides/scope/
37
+ # For further details refer to https://smartcar.com/docs/guides/scope/
38
+ # This parameter is optional and should normally be set within the Smartcar Dashboard.
39
+ # Can also be a Hash of options if scope is not needed.
36
40
  # @param [Hash] options
37
41
  # @option options[:force_prompt] [Boolean] - Setting `force_prompt` to
38
42
  # `true` will show the permissions approval screen on every authentication
@@ -53,9 +57,19 @@ module Smartcar
53
57
  # For a complete list of supported makes, please see our
54
58
  # [API Reference](https://smartcar.com/docs/api#authorization) documentation.
55
59
  # @option options[:flags] [Hash] - A hash of flag name string as key and a string or boolean value.
60
+ # @option options[:user] [String] - An optional unique identifier for a vehicle owner. This identifier
61
+ # is used to aggregate analytics across Connect sessions for each vehicle owner.
56
62
  #
57
63
  # @return [String] Authorization URL string
58
- def get_auth_url(scope, options = {})
64
+ def get_auth_url(scope_or_options = {}, options = {})
65
+ scope = nil
66
+ if scope_or_options.is_a?(Array)
67
+ scope = scope_or_options
68
+ options ||= {}
69
+ elsif scope_or_options.is_a?(Hash)
70
+ options = scope_or_options
71
+ end
72
+
59
73
  initialize_auth_parameters(scope, options)
60
74
  add_single_select_options(options[:single_select])
61
75
  connect_client.auth_code.authorize_url(@auth_parameters)
@@ -124,14 +138,15 @@ module Smartcar
124
138
  def initialize_auth_parameters(scope, options)
125
139
  @auth_parameters = {
126
140
  response_type: CODE,
127
- redirect_uri: redirect_uri,
128
- mode: mode,
129
- scope: scope.join(' ')
141
+ mode: mode
130
142
  }
143
+ @auth_parameters[:redirect_uri] = redirect_uri if redirect_uri
144
+ @auth_parameters[:scope] = scope.join(' ') if scope
131
145
  @auth_parameters[:approval_prompt] = options[:force_prompt] ? FORCE : AUTO unless options[:force_prompt].nil?
132
146
  @auth_parameters[:state] = options[:state] if options[:state]
133
147
  @auth_parameters[:make] = options[:make_bypass] if options[:make_bypass]
134
148
  @auth_parameters[:flags] = build_flags(options[:flags]) if options[:flags]
149
+ @auth_parameters[:user] = options[:user] if options[:user]
135
150
  end
136
151
 
137
152
  def add_single_select_options(single_select)
@@ -15,12 +15,19 @@ module Smartcar
15
15
 
16
16
  # gets a given env variable, checks for existence and throws exception if not present
17
17
  # @param config_name [String] key of the env variable
18
+ # @param options [Hash] options hash, supports :nullable key (default: false)
18
19
  #
19
20
  # @return [String] value of the env variable
20
- def get_config(config_name)
21
+ def get_config(config_name, options = {})
21
22
  # ENV.MODE is set to test by e2e tests.
22
23
  config_name = "E2E_#{config_name}" if ENV['MODE'] == 'test'
23
- raise Smartcar::ConfigNotFound, "Environment variable #{config_name} not found !" unless ENV[config_name]
24
+
25
+ unless ENV[config_name]
26
+ nullable = options.fetch(:nullable, false)
27
+ return nil if nullable
28
+
29
+ raise Smartcar::ConfigNotFound, "Environment variable #{config_name} not found !"
30
+ end
24
31
 
25
32
  ENV.fetch(config_name, nil)
26
33
  end
@@ -34,29 +41,101 @@ module Smartcar
34
41
  #
35
42
  # @return [RecursiveOpenStruct]
36
43
  def json_to_ostruct(hash)
37
- RecursiveOpenStruct.new(hash, recurse_over_arrays: true)
44
+ convert_to_ostruct_recursively(hash)
45
+ end
46
+
47
+ # Helper method to recursively convert hashes and arrays to RecursiveOpenStruct
48
+ def convert_to_ostruct_recursively(obj)
49
+ case obj
50
+ when Array
51
+ obj.map { |el| convert_to_ostruct_recursively(el) }
52
+ when Hash
53
+ RecursiveOpenStruct.new(
54
+ obj.transform_values { |value| convert_to_ostruct_recursively(value) },
55
+ recurse_over_arrays: true
56
+ )
57
+
58
+ else
59
+ obj
60
+ end
61
+ end
62
+
63
+ # Helper method to convert string from camelCase or kebab-case to snake_case
64
+ def to_snake_case(str)
65
+ str.to_s
66
+ .gsub('-', '_') # Convert kebab-case to snake_case
67
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
68
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
69
+ .downcase
70
+ end
71
+
72
+ # Helper method to recursively convert hash keys to snake_case
73
+ def deep_transform_keys_to_snake_case(obj)
74
+ case obj
75
+ when Array
76
+ obj.map { |el| deep_transform_keys_to_snake_case(el) }
77
+ when Hash
78
+ obj.each_with_object({}) do |(key, value), result|
79
+ new_key = to_snake_case(key)
80
+ result[new_key] = deep_transform_keys_to_snake_case(value)
81
+ end
82
+ else
83
+ obj
84
+ end
85
+ end
86
+
87
+ # Parse date string to DateTime or return nil on error
88
+ def parse_date_safely(date_string)
89
+ return nil unless date_string
90
+
91
+ begin
92
+ DateTime.parse(date_string)
93
+ rescue ArgumentError
94
+ nil
95
+ end
38
96
  end
39
97
 
40
98
  def build_meta(headers)
41
99
  meta_hash = {
42
100
  'sc-data-age' => :data_age,
43
101
  'sc-unit-system' => :unit_system,
44
- 'sc-request-id' => :request_id
102
+ 'sc-request-id' => :request_id,
103
+ 'sc-fetched-at' => :fetched_at
45
104
  }.each_with_object({}) do |(header_name, key), meta|
46
105
  meta[key] = headers[header_name] if headers[header_name]
47
106
  end
48
- meta = json_to_ostruct(meta_hash)
49
- meta.data_age &&= DateTime.parse(meta.data_age)
50
107
 
108
+ meta = json_to_ostruct(meta_hash)
109
+ meta.data_age = parse_date_safely(meta.data_age)
110
+ meta.fetched_at = parse_date_safely(meta.fetched_at)
51
111
  meta
52
112
  end
53
113
 
54
114
  def build_response(body, headers)
55
- response = json_to_ostruct(body)
56
- response.meta = build_meta(headers)
115
+ # Check if body is already parsed (i.e., a Hash) or if it needs parsing (i.e., a String)
116
+ body_data = body.is_a?(String) ? JSON.parse(body) : body
117
+ if body_data.is_a?(Array)
118
+ response = OpenStruct.new(items: json_to_ostruct(body), meta: build_meta(headers))
119
+ else
120
+ response = json_to_ostruct(body)
121
+ response.meta = build_meta(headers)
122
+ end
57
123
  response
58
124
  end
59
125
 
126
+ def build_v3_response(body, headers)
127
+ body_data = body.is_a?(String) ? JSON.parse(body) : body
128
+ headers_data = headers.is_a?(String) ? JSON.parse(headers) : headers
129
+
130
+ body_snake = deep_transform_keys_to_snake_case(body_data)
131
+ headers_snake = deep_transform_keys_to_snake_case(headers_data)
132
+
133
+ OpenStruct.new(
134
+ body: json_to_ostruct(body_snake),
135
+ headers: json_to_ostruct(headers_snake)
136
+ )
137
+ end
138
+
60
139
  def build_aliases(response, aliases)
61
140
  (aliases || []).each do |original_name, alias_name|
62
141
  # rubocop:disable Lint/SymbolConversion
@@ -129,6 +208,9 @@ module Smartcar
129
208
 
130
209
  return :lock_status if path == '/security'
131
210
 
211
+ return :diagnostic_system_status if path == '/diagnostics/system_status'
212
+ return :diagnostic_trouble_codes if path == '/diagnostics/dtcs'
213
+
132
214
  path.split('/').reject(&:empty?).join('_').to_sym
133
215
  end
134
216
 
@@ -25,10 +25,17 @@ module Smartcar
25
25
  aliases: { 'percentRemaining' => 'percentage_remaining' }
26
26
  },
27
27
  battery_capacity: { path: proc { |id| "/vehicles/#{id}/battery/capacity" } },
28
+ nominal_capacity: { path: proc { |id| "/vehicles/#{id}/battery/nominal_capacity" } },
28
29
  charge: {
29
30
  path: proc { |id| "/vehicles/#{id}/charge" },
30
31
  aliases: { 'isPluggedIn' => 'is_plugged_in?' }
31
32
  },
33
+ diagnostic_system_status: {
34
+ path: proc { |id| "/vehicles/#{id}/diagnostics/system_status" }
35
+ },
36
+ diagnostic_trouble_codes: {
37
+ path: proc { |id| "/vehicles/#{id}/diagnostics/dtcs" }
38
+ },
32
39
  engine_oil: {
33
40
  path: proc { |id| "/vehicles/#{id}/engine/oil" },
34
41
  aliases: { 'lifeRemaining' => 'life_remaining' }
@@ -42,6 +49,16 @@ module Smartcar
42
49
  },
43
50
  location: { path: proc { |id| "/vehicles/#{id}/location" } },
44
51
  odometer: { path: proc { |id| "/vehicles/#{id}/odometer" } },
52
+ service_history: {
53
+ path: proc { |id, start_date = nil, end_date = nil|
54
+ base_path = "/vehicles/#{id}/service/history"
55
+ query_params = []
56
+ query_params << "start_date=#{start_date}" unless start_date.nil?
57
+ query_params << "end_date=#{end_date}" unless end_date.nil?
58
+ "#{base_path}?#{query_params.join('&')}" unless query_params.empty?
59
+ },
60
+ skip: true
61
+ },
45
62
  tire_pressure: {
46
63
  path: proc { |id| "/vehicles/#{id}/tires/pressure" },
47
64
  aliases: {
@@ -129,6 +146,14 @@ module Smartcar
129
146
  # @return [OpenStruct] And object representing the JSON response mentioned in https://smartcar.com/docs/api#get-ev-battery-capacity
130
147
  # and a meta attribute with the relevant items from response headers.
131
148
 
149
+ # @!method nominal_capacity()
150
+ # Returns a list of nominal rated battery capacities for a vehicle.
151
+ #
152
+ # API Documentation https://smartcar.com/docs/api-reference/get-nominal-capacity
153
+ #
154
+ # @return [OpenStruct] And object representing the JSON response mentioned in https://smartcar.com/docs/api-reference/get-nominal-capacity
155
+ # and a meta attribute with the relevant items from response headers.
156
+
132
157
  # @!method charge()
133
158
  # Returns the current charge status of the vehicle.
134
159
  #
@@ -137,6 +162,20 @@ module Smartcar
137
162
  # @return [OpenStruct] And object representing the JSON response mentioned in https://smartcar.com/docs/api#get-ev-battery
138
163
  # and a meta attribute with the relevant items from response headers.
139
164
 
165
+ # @!method diagnostic_system_status()
166
+ # Returns the status of the vehicle's diagnostic systems.
167
+ #
168
+ # API Documentation: https://smartcar.com/docs/api#get-diagnostic-system-status
169
+ #
170
+ # @return [OpenStruct] An object representing the diagnostic system status and metadata.
171
+
172
+ # @!method diagnostic_trouble_codes()
173
+ # Retrieves diagnostic trouble codes (DTCs) from the vehicle.
174
+ #
175
+ # API Documentation: https://smartcar.com/docs/api#get-diagnostic-trouble-codes
176
+ #
177
+ # @return [OpenStruct] An object containing the DTCs and metadata.
178
+
140
179
  # @!method engine_oil()
141
180
  # Returns the remaining life span of a vehicle's engine oil
142
181
  #
@@ -232,6 +271,40 @@ module Smartcar
232
271
  build_response(response, headers)
233
272
  end
234
273
 
274
+ # Retrieves a list of service records for the vehicle, optionally filtered by a specified date range.
275
+ # If no dates are specified, the method defaults to returning records from the last year.
276
+ #
277
+ # This method calls the Smartcar API to fetch the vehicle's service history and processes the
278
+ # response to return structured data.
279
+ #
280
+ # @param start_date [String, nil] the start date of the period from which records are retrieved (inclusive).
281
+ # Expected in 'YYYY-MM-DD' format. If nil, defaults to one year ago from today.
282
+ # @param end_date [String, nil] the end date of the period until which records are retrieved (inclusive).
283
+ # Expected in 'YYYY-MM-DD' format. If nil, defaults to today's date.
284
+ #
285
+ # @return [OpenStruct] An object representing the parsed JSON response from the API, with service history
286
+ # data and metadata extracted from the response headers.
287
+ #
288
+ # Example usage:
289
+ # vehicle.service_history('2021-01-01', '2021-12-31')
290
+ #
291
+ # Note: This method assumes that the necessary error handling is embedded within the `get` method or handled
292
+ # externally when exceptions arise due to network issues, API limitations, or data encoding problems.
293
+ def service_history(start_date = nil, end_date = nil)
294
+ start_date, end_date = default_date_range if start_date.nil? || end_date.nil?
295
+
296
+ path = METHODS[:service_history][:path].call(id, start_date, end_date)
297
+ body, headers = get(path, @query_params)
298
+ build_response(body, headers)
299
+ end
300
+
301
+ # Utility method to provide default dates
302
+ def default_date_range
303
+ end_date = DateTime.now.new_offset(0).to_date
304
+ start_date = end_date - 365
305
+ [start_date.to_s, end_date.to_s]
306
+ end
307
+
235
308
  # Subscribe the vehicle to given webhook Id.
236
309
  #
237
310
  # @param webhook_id [String] Webhook id to subscribe to
@@ -298,6 +371,34 @@ module Smartcar
298
371
  process_batch_response(response, headers)
299
372
  end
300
373
 
374
+ # Retrieve a specific signal by signal code from the vehicle.
375
+ # Uses the Vehicle API (v3) endpoint.
376
+ #
377
+ # API Documentation - https://smartcar.com/docs/api-reference/get-signal
378
+ #
379
+ # @param signal_code [String] The code of the signal to retrieve.
380
+ #
381
+ # @return [OpenStruct] An object with a "body" attribute containing the signal data
382
+ # and a "headers" attribute containing the response headers.
383
+ def get_signal(signal_code)
384
+ raise InvalidParameterValue.new, 'signal_code is a required field' if signal_code.nil? || signal_code.empty?
385
+
386
+ path = "/vehicles/#{id}/signals/#{signal_code}"
387
+ request_v3(path)
388
+ end
389
+
390
+ # Retrieve all available signals from the vehicle.
391
+ # Uses the Vehicle API (v3) endpoint.
392
+ #
393
+ # API Documentation - https://smartcar.com/docs/api-reference/get-signals
394
+ #
395
+ # @return [OpenStruct] An object with a "body" attribute containing all signals data
396
+ # and a "headers" attribute containing the response headers.
397
+ def get_signals
398
+ path = "/vehicles/#{id}/signals"
399
+ request_v3(path)
400
+ end
401
+
301
402
  # General purpose method to make requests to the Smartcar API - can be
302
403
  # used to make requests to brand specific endpoints.
303
404
  #
@@ -314,5 +415,35 @@ module Smartcar
314
415
  meta = build_meta(headers)
315
416
  json_to_ostruct({ body: raw_response, meta: meta })
316
417
  end
418
+
419
+ private
420
+
421
+ # Makes a request to the v3 vehicles API using the vehicle API origin
422
+ #
423
+ # @param path [String] The API path to request.
424
+ #
425
+ # @return [OpenStruct] An object with body and headers attributes.
426
+ def request_v3(path)
427
+ vehicle_service = Faraday.new(
428
+ url: ENV['SMARTCAR_VEHICLE_API_ORIGIN'] || VEHICLE_API_ORIGIN,
429
+ request: { timeout: DEFAULT_REQUEST_TIMEOUT }
430
+ )
431
+
432
+ response = vehicle_service.get do |request|
433
+ request.headers['Authorization'] = "Bearer #{token}"
434
+ request.headers['Content-Type'] = 'application/json'
435
+ request.headers['User-Agent'] =
436
+ "Smartcar/#{VERSION} (#{RbConfig::CONFIG['host_os']}; #{RbConfig::CONFIG['arch']}) Ruby v#{RUBY_VERSION}"
437
+
438
+ complete_path = "/v3#{path}"
439
+ complete_path += "?#{URI.encode_www_form(@query_params.compact)}" unless @query_params.empty?
440
+ request.url complete_path
441
+ end
442
+
443
+ handle_error(response)
444
+
445
+ body = response.body.empty? ? '{}' : response.body
446
+ build_v3_response(body, response.headers.to_h)
447
+ end
317
448
  end
318
449
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Smartcar
4
4
  # Gem current version number
5
- VERSION = '3.8.0'
5
+ VERSION = '3.9.5'
6
6
  end
data/lib/smartcar.rb CHANGED
@@ -15,6 +15,7 @@ module Smartcar
15
15
 
16
16
  # Host to connect to smartcar
17
17
  API_ORIGIN = 'https://api.smartcar.com/'
18
+ VEHICLE_API_ORIGIN = 'https://vehicle.api.smartcar.com'
18
19
  MANAGEMENT_API_ORIGIN = 'https://management.smartcar.com'
19
20
  PATHS = {
20
21
  compatibility: '/compatibility',
@@ -101,6 +102,49 @@ module Smartcar
101
102
  ))
102
103
  end
103
104
 
105
+ # Module method to retrieve the Smartcar compatibility matrix for a given region.
106
+ # Provides the ability to filter by scope, make, and type.
107
+ #
108
+ # A compatible vehicle is a vehicle that:
109
+ # 1. has the hardware required for internet connectivity,
110
+ # 2. belongs to the makes and models Smartcar supports, and
111
+ # 3. supports the permissions.
112
+ #
113
+ # API Documentation - https://smartcar.com/docs/api-reference/compatibility/by-region-and-make
114
+ # @param region [String] One of the following regions: US, CA, or EUROPE
115
+ # @param options [Hash] Optional parameters
116
+ # @option options [Array<String>] :scope List of permissions to filter the matrix by
117
+ # @option options [String, Array<String>] :make List of makes to filter the matrix by
118
+ # (space-separated string or array)
119
+ # @option options [String] :type Engine type to filter the matrix by (e.g., "ICE", "HEV", "PHEV", "BEV")
120
+ # @option options [String] :client_id Client ID that overrides ENV
121
+ # @option options [String] :client_secret Client Secret that overrides ENV
122
+ # @option options [String] :version API version to use, defaults to what is globally set
123
+ # @option options [String] :mode Determine what mode Smartcar Connect should be launched in.
124
+ # Should be one of test, live or simulated.
125
+ # @option options [Faraday::Connection] :service Optional connection object to be used for requests
126
+ #
127
+ # @return [OpenStruct] An object representing the compatibility matrix organized by make,
128
+ # with each make containing an array of compatible models and a meta attribute with response headers.
129
+ def get_compatibility_matrix(region, options = {})
130
+ raise Base::InvalidParameterValue.new, 'region is a required field' if region.nil? || region.empty?
131
+
132
+ base_object = Base.new(
133
+ {
134
+ version: options[:version] || Smartcar.get_api_version,
135
+ auth_type: Base::BASIC,
136
+ service: options[:service]
137
+ }
138
+ )
139
+
140
+ base_object.token = generate_basic_auth(options, base_object)
141
+
142
+ base_object.build_response(*base_object.get(
143
+ "#{PATHS[:compatibility]}/matrix",
144
+ build_compatibility_matrix_params(region, options)
145
+ ))
146
+ end
147
+
104
148
  # Module method Used to get user id
105
149
  #
106
150
  # API Documentation - https://smartcar.com/docs/api#get-user
@@ -224,6 +268,45 @@ module Smartcar
224
268
  ))
225
269
  end
226
270
 
271
+ # Module method to retrieve vehicle information using the v3 API.
272
+ #
273
+ # API Documentation - https://smartcar.com/docs/api-reference/get-vehicle
274
+ # @param vehicle_id [String] The vehicle ID
275
+ # @param token [String] Access token
276
+ # @param options [Hash] Optional parameters
277
+ # @option options [Hash] :flags A hash of flag name string as key and a string or boolean value.
278
+ # @option options [Faraday::Connection] :service Optional connection object to be used for requests
279
+ #
280
+ # @return [OpenStruct] An object with a "body" attribute containing the vehicle data
281
+ # and a "headers" attribute containing the response headers.
282
+ def get_vehicle(vehicle_id:, token:, options: {})
283
+ raise Base::InvalidParameterValue.new, 'vehicle_id is a required field' if vehicle_id.nil? || vehicle_id.empty?
284
+ raise Base::InvalidParameterValue.new, 'token is a required field' if token.nil? || token.empty?
285
+
286
+ vehicle_service = Faraday.new(
287
+ url: ENV['SMARTCAR_VEHICLE_API_ORIGIN'] || VEHICLE_API_ORIGIN,
288
+ request: { timeout: DEFAULT_REQUEST_TIMEOUT }
289
+ )
290
+
291
+ query_params = { flags: stringify_params(options[:flags]) }
292
+
293
+ response = vehicle_service.get do |request|
294
+ request.headers['Authorization'] = "Bearer #{token}"
295
+ request.headers['Content-Type'] = 'application/json'
296
+ request.headers['User-Agent'] =
297
+ "Smartcar/#{VERSION} (#{RbConfig::CONFIG['host_os']}; #{RbConfig::CONFIG['arch']}) Ruby v#{RUBY_VERSION}"
298
+
299
+ complete_path = "/v3/vehicles/#{vehicle_id}"
300
+ complete_path += "?#{URI.encode_www_form(query_params.compact)}" unless query_params.empty?
301
+ request.url complete_path
302
+ end
303
+
304
+ raise build_error(response.status, response.body, response.headers) unless [200, 204].include?(response.status)
305
+
306
+ body = response.body.empty? ? '{}' : response.body
307
+ build_v3_response(body, response.headers.to_h)
308
+ end
309
+
227
310
  # returns auth token for Basic vehicle management auth
228
311
  #
229
312
  # @return [String] Base64 encoding of default:amt
@@ -252,6 +335,29 @@ module Smartcar
252
335
  query_params
253
336
  end
254
337
 
338
+ def build_compatibility_matrix_params(region, options)
339
+ query_params = { region: region }
340
+
341
+ # Handle scope - convert array to space-separated string
342
+ if options[:scope]
343
+ query_params[:scope] = options[:scope].is_a?(Array) ? options[:scope].join(' ') : options[:scope]
344
+ end
345
+
346
+ # Handle make - convert array to space-separated string
347
+ if options[:make]
348
+ query_params[:make] = options[:make].is_a?(Array) ? options[:make].join(' ') : options[:make]
349
+ end
350
+
351
+ # Handle type
352
+ query_params[:type] = options[:type] if options[:type]
353
+
354
+ # Handle mode
355
+ mode = determine_mode(options[:test_mode], options[:mode])
356
+ query_params[:mode] = mode unless mode.nil?
357
+
358
+ query_params
359
+ end
360
+
255
361
  # returns auth token for Basic auth
256
362
  #
257
363
  # @return [String] Base64 encoding of CLIENT:SECRET
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Custom SmartcarError class to represent errors from Smartcar APIs.
4
4
  class SmartcarError < StandardError
5
- attr_reader :code, :status_code, :request_id, :type, :description, :doc_url, :resolution, :detail, :retry_after
5
+ attr_reader :code, :status_code, :request_id, :type, :description, :doc_url, :resolution, :detail, :retry_after, :suggested_user_message
6
6
 
7
7
  def initialize(status, body, headers)
8
8
  @status_code = status
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: smartcar
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.8.0
4
+ version: 3.9.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ashwin Subramanian
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-12-19 00:00:00.000000000 Z
11
+ date: 2026-01-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -178,10 +178,11 @@ executables: []
178
178
  extensions: []
179
179
  extra_rdoc_files: []
180
180
  files:
181
+ - ".buddy/cd.yml"
182
+ - ".buddy/ci.yml"
181
183
  - ".gitignore"
182
184
  - ".rspec"
183
185
  - ".rubocop.yml"
184
- - ".travis.yml"
185
186
  - ".yardopts"
186
187
  - CODE_OF_CONDUCT.md
187
188
  - Gemfile
data/.travis.yml DELETED
@@ -1,28 +0,0 @@
1
- sudo: false
2
- language: ruby
3
- cache: bundler
4
- services:
5
- - xvfb
6
- addons:
7
- firefox: latest
8
- rvm:
9
- - 2.7
10
- - 3.0
11
- before_install:
12
- - gem install bundler -v 2.3.1
13
- - wget https://github.com/mozilla/geckodriver/releases/download/v0.26.0/geckodriver-v0.26.0-linux64.tar.gz
14
- - mkdir geckodriver
15
- - tar -xzf geckodriver-v0.26.0-linux64.tar.gz -C geckodriver
16
- - export PATH=$PATH:$PWD/geckodriver
17
- # install:
18
- # - firefox -headless &
19
- deploy:
20
- provider: rubygems
21
- api_key: $RUBYGEMSAPI_KEY
22
- gem: smartcar
23
- gemspec: ruby-sdk.gemspec
24
- on:
25
- tags: true
26
- branch: master
27
- rvm: 2.7
28
- skip_cleanup: 'true'