ruze 0.1.2 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c05176b082da3e8a263b95baf98b6982b0bf1563686fd9c357b575d33c860e2e
4
- data.tar.gz: a95ddfa50c47176f21bf7b36d5a6be61ec84f5ba8994abd99739f58fac2e292e
3
+ metadata.gz: 1fa802b8dceb6e72c4e9c04b3a0ee3fd0a1c70d2063b0ffae6d46398057b317d
4
+ data.tar.gz: 9976f664f420a5b351023f3d4bb8ef44947849c752aebbc0c0d8de402dd0a507
5
5
  SHA512:
6
- metadata.gz: c48c8ac959d98088fca9d2028e281264a37be188a2a94e5bc5233751bf3d9e4463da88b2a4e7eaf7381a16f15448b55e4e0ccfbbce23ebb94bbc6b068e655e2e
7
- data.tar.gz: 133d2f1b88778b4b01e56f03a1ba6dd629d7968a8030917ba39b14e71487ccb8e2bed733aaa60b7e402912457d75a9f24852f528e44127d93a838b455ec4e9c5
6
+ metadata.gz: 5b5e04039d1562360d0f237179555b5660e8f0d5369269723f343d689dd9f747c2ff184a6bd68331fa493bb9ae5f75e7801fdf55b8924928efe7fea1525f5691
7
+ data.tar.gz: 708613986c1f2f35bd215cc2a4099bc2487d7f456200643c8fba86ffef83b132e30a2cf2878337a15d0eb3236d8172a03bb97489f34abb152d2bbb55c7ecf08d
data/.env.test CHANGED
@@ -7,3 +7,6 @@ KAMEREON_API_KEY=bar
7
7
  RENAULT_PERSON_ID=1234567890
8
8
  RENAULT_VIN=VF1AG000X12345678
9
9
  RENAULT_ACCOUNT_ID=0987654321
10
+
11
+ RUZE_GMID=gmid.test
12
+ RUZE_UCID=ucid.test
@@ -9,7 +9,7 @@ jobs:
9
9
  strategy:
10
10
  fail-fast: false
11
11
  matrix:
12
- ruby: ['3.0', '3.1', '3.2', '3.3']
12
+ ruby: ['4.0']
13
13
 
14
14
  steps:
15
15
  - uses: actions/checkout@v3
data/.rubocop.yml CHANGED
@@ -1,8 +1,11 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.7
2
+ TargetRubyVersion: 4.0
3
3
  NewCops: enable
4
4
  SuggestExtensions: false
5
5
 
6
+ Metrics/ClassLength:
7
+ Max: 170
8
+
6
9
  Metrics/MethodLength:
7
10
  Max: 20
8
11
 
data/Gemfile CHANGED
@@ -20,3 +20,6 @@ gem 'dotenv'
20
20
 
21
21
  # Automatic Ruby code style checking tool. (https://github.com/rubocop/rubocop)
22
22
  gem 'rubocop'
23
+
24
+ # Interactive Ruby console for bin/console (no longer a default gem since Ruby 4.0)
25
+ gem 'irb'
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2021-2024 Georg Ledermann
3
+ Copyright (c) 2021-2026 Georg Ledermann
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -38,27 +38,25 @@ export KAMEREON_API_KEY=...
38
38
  ## Usage
39
39
 
40
40
  ```ruby
41
- car = Ruze::Car.new('me@example.com', 'my-password')
41
+ car = Ruze::Car.new('john@example.com', 'my-password')
42
42
 
43
43
  car.battery
44
44
  # {
45
- # "timestamp" => "2021-03-13T09:59:12Z",
46
- # "batteryLevel" => 66,
47
- # "batteryTemperature" => 20,
48
- # "batteryAutonomy" => 194,
49
- # "batteryCapacity" => 0,
50
- # "batteryAvailableEnergy" => 33,
51
- # "plugStatus" => 0,
52
- # "chargingStatus" => 0.0,
53
- # "chargingRemainingTime" => 55,
54
- # "chargingInstantaneousPower" => 0.0
45
+ # "timestamp" => "2026-03-13T09:59:12Z",
46
+ # "batteryLevel" => 66,
47
+ # "batteryAutonomy" => 194,
48
+ # "plugStatus" => 0,
49
+ # "chargingStatus" => 0.0,
50
+ # "chargingRemainingTime" => 55,
51
+ # "chargingRemainingTimeLastUpdateDateTime" => "2026-03-13T09:30:00Z"
55
52
  # }
56
53
 
57
54
  car.cockpit
58
55
  # {
59
56
  # "fuelAutonomy" => 0.0,
60
57
  # "fuelQuantity" => 0.0,
61
- # "totalMileage" => 12345.67
58
+ # "totalMileage" => 12345.67,
59
+ # "timestamp" => "2026-03-13T09:59:12Z"
62
60
  # }
63
61
 
64
62
  car.location
@@ -66,11 +64,51 @@ car.location
66
64
  # "gpsDirection" => nil,
67
65
  # "gpsLatitude" => 50.12345678,
68
66
  # "gpsLongitude" => 6.12345678,
69
- # "lastUpdateTime" => "2021-03-12T11:43:18Z"
67
+ # "lastUpdateTime" => "2026-03-12T11:43:18Z"
70
68
  # }
71
69
  ```
72
70
 
73
71
 
72
+ ## Two-factor authentication
73
+
74
+ Renault enforces two-factor authentication (2FA) on Gigya accounts, so a
75
+ password alone is no longer enough to log in. RuZE handles this by establishing
76
+ a *trusted device* once: after a single email verification, Renault remembers
77
+ the device for about 30 days, during which logins succeed without a prompt.
78
+
79
+ ### One-time setup
80
+
81
+ Trigger a verification code, confirm it with the 6-digit code Renault emails to
82
+ your account, then read the resulting trusted-device pair:
83
+
84
+ ```ruby
85
+ gigya = Ruze::Gigya.new('john@example.com', 'my-password')
86
+
87
+ gigya.request_verification_code # emails a 6-digit code (returns nil if none is needed)
88
+
89
+ gigya.verify_code('123456')
90
+
91
+ gmid = gigya.device.gmid
92
+ ucid = gigya.device.ucid
93
+ # Store this gmid/ucid pair somewhere durable (env vars, a file, a database).
94
+ ```
95
+
96
+ `Ruze::Device` keeps the pair in memory only, so persisting it is up to you. The
97
+ pair stays valid across renewals, so you store it once.
98
+
99
+ ### Normal usage
100
+
101
+ Seed a device with your stored pair and `Ruze::Car` logs in without prompting:
102
+
103
+ ```ruby
104
+ device = Ruze::Device.new(gmid: gmid, ucid: ucid)
105
+ car = Ruze::Car.new('john@example.com', 'my-password', device: device)
106
+ ```
107
+
108
+ When the device is missing or has expired, login raises
109
+ `Ruze::TwoFactorRequired` — repeat the one-time setup to renew it.
110
+
111
+
74
112
  ## Background
75
113
 
76
114
  Thanks to James Muscat (@jamesremuscat) for making PyZE, the Python client for Renault ZE API:
@@ -106,7 +144,7 @@ This project is not affiliated with, endorsed by, or connected to Renault. I acc
106
144
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
107
145
 
108
146
 
109
- Copyright (c) 2021-2024 Georg Ledermann, released under the AGPL-3.0 License
147
+ Copyright (c) 2021-2026 Georg Ledermann, released under the AGPL-3.0 License
110
148
 
111
149
  ## Code of Conduct
112
150
 
data/bin/console CHANGED
@@ -1,14 +1,24 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
3
  require 'bundler/setup'
4
+ require 'dotenv/load'
4
5
  require 'ruze'
5
6
 
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
7
+ # Convenience for experimenting against the real API, using the credentials and
8
+ # trusted device from your .env (RENAULT_EMAIL, RENAULT_PASSWORD, GIGYA_API_KEY,
9
+ # KAMEREON_API_KEY, RUZE_GMID, RUZE_UCID):
10
+ #
11
+ # car.battery
12
+ # car.cockpit
13
+ # car.location
14
+ #
15
+ def car
16
+ Ruze::Car.new(
17
+ ENV.fetch('RENAULT_EMAIL'),
18
+ ENV.fetch('RENAULT_PASSWORD'),
19
+ device: Ruze::Device.new(gmid: ENV.fetch('RUZE_GMID', nil), ucid: ENV.fetch('RUZE_UCID', nil))
20
+ )
21
+ end
12
22
 
13
23
  require 'irb'
14
24
  IRB.start(__FILE__)
data/lib/ruze/car.rb CHANGED
@@ -3,8 +3,8 @@ require_relative 'kamereon'
3
3
 
4
4
  module Ruze
5
5
  class Car
6
- def initialize(email, password, vin = nil)
7
- gigya = Ruze::Gigya.new(email, password)
6
+ def initialize(email, password, vin = nil, device: Ruze::Device.new)
7
+ gigya = Ruze::Gigya.new(email, password, device: device)
8
8
  @kamereon = Ruze::Kamereon.new(gigya.person_id, gigya.jwt, vin)
9
9
  end
10
10
 
@@ -0,0 +1,30 @@
1
+ module Ruze
2
+ # Holds the Gigya trusted-device identity (gmid/ucid) that lets us skip the
3
+ # two-factor prompt on subsequent logins.
4
+ #
5
+ # This default implementation keeps the pair in memory only. For headless
6
+ # operation across process restarts, seed it from durable storage and persist
7
+ # the pair (see #gmid/#ucid) after a successful bootstrap:
8
+ #
9
+ # device = Ruze::Device.new(gmid: stored_gmid, ucid: stored_ucid)
10
+ # Ruze::Car.new(email, password, device: device)
11
+ class Device
12
+ def initialize(gmid: nil, ucid: nil)
13
+ @gmid = gmid
14
+ @ucid = ucid
15
+ end
16
+ attr_reader :gmid, :ucid
17
+
18
+ # Returns { gmid:, ucid: } or nil when no (complete) device is available.
19
+ def load
20
+ return nil if gmid.to_s.empty? || ucid.to_s.empty?
21
+
22
+ { gmid: gmid, ucid: ucid }
23
+ end
24
+
25
+ def save(gmid:, ucid:)
26
+ @gmid = gmid
27
+ @ucid = ucid
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,8 @@
1
+ module Ruze
2
+ class Error < StandardError; end
3
+
4
+ # Raised when Renault/Gigya requires two-factor authentication and no trusted
5
+ # device is available. Run the bootstrap flow (request_verification_code +
6
+ # verify_code) to establish a trusted device.
7
+ class TwoFactorRequired < Error; end
8
+ end
data/lib/ruze/gigya.rb CHANGED
@@ -1,68 +1,227 @@
1
1
  require 'net/http'
2
2
  require 'json'
3
+ require_relative 'errors'
4
+ require_relative 'device'
3
5
 
4
6
  module Ruze
5
7
  class Gigya
6
- BASE_URL = 'https://accounts.eu1.gigya.com'.freeze
8
+ BASE_URL = 'https://accounts.eu1.gigya.com'.freeze
9
+ SOCIALIZE_URL = 'https://socialize.eu1.gigya.com'.freeze
7
10
 
8
- def initialize(email, password)
11
+ def initialize(email, password, device: Device.new)
9
12
  raise ArgumentError unless email.is_a?(String) && password.is_a?(String)
10
13
 
11
14
  @email = email
12
15
  @password = password
16
+ @device = device
13
17
  end
14
- attr_reader :email, :password
18
+ attr_reader :email, :password, :device
15
19
 
16
20
  def person_id
17
- @person_id ||= return_from Net::HTTP.post_form(
18
- uri('/accounts.getAccountInfo'),
19
- 'ApiKey' => api_key,
20
- 'login_token' => session_cookie_value
21
- ), keys: %w[data personId]
21
+ @person_id ||= dig_from post(
22
+ "#{BASE_URL}/accounts.getAccountInfo",
23
+ { 'apiKey' => api_key, 'login_token' => session_cookie_value }
24
+ ), label: 'person_id', keys: %w[data personId]
22
25
  end
23
26
 
24
27
  def jwt
25
- @jwt ||= return_from Net::HTTP.post_form(
26
- uri('/accounts.getJWT'),
27
- 'ApiKey' => api_key,
28
- 'login_token' => session_cookie_value,
29
- 'fields' => 'data.personId,data.gigyaDataCenter',
30
- 'expiration' => 900
31
- ), keys: %w[id_token]
28
+ @jwt ||= dig_from post(
29
+ "#{BASE_URL}/accounts.getJWT",
30
+ { 'apiKey' => api_key,
31
+ 'login_token' => session_cookie_value,
32
+ 'fields' => 'data.personId,data.gigyaDataCenter',
33
+ 'expiration' => 900 }
34
+ ), label: 'jwt', keys: %w[id_token]
32
35
  end
33
36
 
37
+ # Logs in using the trusted device and returns the session cookie value.
38
+ # Raises TwoFactorRequired when Renault demands a fresh 2FA verification.
34
39
  def session_cookie_value
35
- @session_cookie_value ||= return_from Net::HTTP.post_form(
36
- uri('/accounts.login'),
37
- 'ApiKey' => api_key,
38
- 'loginID' => email,
39
- 'password' => password
40
- ), keys: %w[sessionInfo cookieValue]
40
+ @session_cookie_value ||= login
41
+ end
42
+
43
+ # --- Two-factor bootstrap ------------------------------------------------
44
+ #
45
+ # The Gigya 2FA trusted-device flow below (initTFA -> getEmails ->
46
+ # sendVerificationCode -> completeVerification -> finalizeTFA -> re-login)
47
+ # was reverse-engineered by the renault-api community:
48
+ # https://github.com/hacf-fr/renault-api/issues/2132
49
+ #
50
+ # Run once interactively to establish a trusted device:
51
+ #
52
+ # gigya = Ruze::Gigya.new(email, password)
53
+ # puts "Code sent to #{gigya.request_verification_code}"
54
+ # gigya.verify_code(gets.strip)
55
+ #
56
+ # After that, session_cookie_value works headlessly for ~30 days.
57
+
58
+ # Triggers the email verification code and returns the obfuscated address it
59
+ # was sent to. Returns nil when no verification is needed -- either the
60
+ # account has no 2FA or this device is already trusted.
61
+ def request_verification_code
62
+ ids = device_ids
63
+ reg_token = login_reg_token(ids)
64
+ return nil unless reg_token
65
+
66
+ assertion = init_tfa(reg_token, ids)
67
+ mail = first_email(assertion, ids)
68
+ phv_token = send_verification_code(mail['id'], assertion, ids)
69
+
70
+ @bootstrap = { ids: ids, reg_token: reg_token, assertion: assertion, phv_token: phv_token }
71
+ mail['obfuscated']
72
+ end
73
+
74
+ # Completes the 2FA flow with the emailed code, finalizes the device as
75
+ # trusted (remembered for ~30 days) and persists it.
76
+ def verify_code(code)
77
+ raise Error, 'Call request_verification_code before verify_code' unless @bootstrap
78
+
79
+ ids = @bootstrap[:ids]
80
+ provider_assertion = complete_verification(code, ids)
81
+ finalize_tfa(provider_assertion, ids)
82
+
83
+ @session_cookie_value = login(ids)
84
+ device.save(gmid: ids[:gmid], ucid: ids[:ucid])
85
+ @session_cookie_value
41
86
  end
42
87
 
43
88
  private
44
89
 
90
+ def login(ids = device_ids)
91
+ json = parse post(
92
+ "#{BASE_URL}/accounts.login",
93
+ login_params(ids), cookie: cookie(ids)
94
+ )
95
+
96
+ case json['errorCode']
97
+ when 0
98
+ json.dig('sessionInfo', 'cookieValue')
99
+ when 403_101
100
+ raise TwoFactorRequired,
101
+ 'Two-factor authentication required. Run the bootstrap flow to trust this device.'
102
+ else
103
+ raise Error, "Error in session_cookie_value: #{error_detail(json)}"
104
+ end
105
+ end
106
+
107
+ # Like #login but returns the regToken from a pending-2FA response instead of
108
+ # raising. Returns nil when the account is already logged in without 2FA.
109
+ def login_reg_token(ids)
110
+ json = parse post(
111
+ "#{BASE_URL}/accounts.login",
112
+ login_params(ids), cookie: cookie(ids)
113
+ )
114
+ return json['regToken'] if json['errorCode'] == 403_101
115
+ return nil if json['errorCode']&.zero?
116
+
117
+ raise Error, "Error in session_cookie_value: #{error_detail(json)}"
118
+ end
119
+
120
+ def login_params(ids)
121
+ {
122
+ 'apiKey' => api_key,
123
+ 'loginID' => email,
124
+ 'password' => password,
125
+ 'gmid' => ids[:gmid],
126
+ 'ucid' => ids[:ucid]
127
+ }
128
+ end
129
+
130
+ def init_tfa(reg_token, ids)
131
+ dig_from post(
132
+ "#{BASE_URL}/accounts.tfa.initTFA",
133
+ { 'apiKey' => api_key, 'regToken' => reg_token, 'provider' => 'gigyaEmail',
134
+ 'mode' => 'verify', 'gmid' => ids[:gmid], 'ucid' => ids[:ucid] },
135
+ cookie: cookie(ids)
136
+ ), label: 'init_tfa', keys: %w[gigyaAssertion]
137
+ end
138
+
139
+ def first_email(assertion, ids)
140
+ emails = dig_from post(
141
+ "#{BASE_URL}/accounts.tfa.email.getEmails",
142
+ { 'apiKey' => api_key, 'gigyaAssertion' => assertion, 'gmid' => ids[:gmid], 'ucid' => ids[:ucid] },
143
+ cookie: cookie(ids)
144
+ ), label: 'get_emails', keys: %w[emails]
145
+ raise Error, 'No verified email address available for 2FA' if emails.nil? || emails.empty?
146
+
147
+ emails.first
148
+ end
149
+
150
+ def send_verification_code(email_id, assertion, ids)
151
+ dig_from post(
152
+ "#{BASE_URL}/accounts.tfa.email.sendVerificationCode",
153
+ { 'apiKey' => api_key, 'emailID' => email_id, 'gigyaAssertion' => assertion,
154
+ 'lang' => 'en', 'gmid' => ids[:gmid], 'ucid' => ids[:ucid] },
155
+ cookie: cookie(ids)
156
+ ), label: 'send_verification_code', keys: %w[phvToken]
157
+ end
158
+
159
+ def complete_verification(code, ids)
160
+ dig_from post(
161
+ "#{BASE_URL}/accounts.tfa.email.completeVerification",
162
+ { 'apiKey' => api_key, 'gigyaAssertion' => @bootstrap[:assertion],
163
+ 'phvToken' => @bootstrap[:phv_token], 'code' => code,
164
+ 'gmid' => ids[:gmid], 'ucid' => ids[:ucid] },
165
+ cookie: cookie(ids)
166
+ ), label: 'complete_verification', keys: %w[providerAssertion]
167
+ end
168
+
169
+ # tempDevice=false marks the device as remembered (no 2FA for ~30 days).
170
+ def finalize_tfa(provider_assertion, ids)
171
+ json = parse post(
172
+ "#{BASE_URL}/accounts.tfa.finalizeTFA",
173
+ { 'apiKey' => api_key, 'gigyaAssertion' => @bootstrap[:assertion],
174
+ 'providerAssertion' => provider_assertion, 'tempDevice' => 'false',
175
+ 'regToken' => @bootstrap[:reg_token], 'gmid' => ids[:gmid], 'ucid' => ids[:ucid] },
176
+ cookie: cookie(ids)
177
+ )
178
+ raise Error, "Error in finalize_tfa: #{error_detail(json)}" unless json['errorCode']&.zero?
179
+
180
+ json
181
+ end
182
+
183
+ def device_ids
184
+ @device_ids ||= device.load || fetch_ids
185
+ end
186
+
187
+ def fetch_ids
188
+ json = parse post("#{SOCIALIZE_URL}/socialize.getIDs", { 'apiKey' => api_key })
189
+ raise Error, "Error in fetch_ids: #{error_detail(json)}" unless json['errorCode']&.zero?
190
+
191
+ { gmid: json['gmid'], ucid: json['ucid'] }
192
+ end
193
+
194
+ def cookie(ids)
195
+ "gmid=#{ids[:gmid]}; ucid=#{ids[:ucid]}"
196
+ end
197
+
45
198
  def api_key
46
199
  ENV.fetch('GIGYA_API_KEY')
47
200
  end
48
201
 
49
- def uri(path)
50
- URI("#{BASE_URL}#{path}")
202
+ def post(url, params, cookie: nil)
203
+ uri = URI(url)
204
+ request = Net::HTTP::Post.new(uri)
205
+ request.set_form_data(params.merge('format' => 'json'))
206
+ request['Cookie'] = cookie if cookie
207
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
208
+ http.request(request)
209
+ end
51
210
  end
52
211
 
53
- def return_from(response, keys:)
54
- unless response.is_a?(Net::HTTPOK)
55
- caller = caller_locations(1, 1)[0].label
56
- raise Error, "Error in #{caller}: #{response.message} (#{response.code})"
57
- end
212
+ def parse(response)
213
+ JSON.parse(response.body)
214
+ end
58
215
 
59
- json = JSON.parse(response.body)
60
- unless json['errorCode']&.zero?
61
- caller = caller_locations(1, 1)[0].label
62
- raise Error, "Error in #{caller}: #{json['errorDetails']}"
63
- end
216
+ def dig_from(response, label:, keys:)
217
+ json = parse(response)
218
+ raise Error, "Error in #{label}: #{error_detail(json)}" unless json['errorCode']&.zero?
64
219
 
65
220
  json.dig(*keys)
66
221
  end
222
+
223
+ def error_detail(json)
224
+ json['errorDetails'] || json['errorMessage'] || "errorCode #{json['errorCode']}"
225
+ end
67
226
  end
68
227
  end
data/lib/ruze/kamereon.rb CHANGED
@@ -89,7 +89,7 @@ module Ruze
89
89
 
90
90
  def return_from(response, keys:)
91
91
  unless response.is_a?(Net::HTTPOK)
92
- caller = caller_locations(1, 1)[0].label
92
+ caller = caller_locations(1, 1)[0].base_label
93
93
  raise Error, "Error in #{caller}: #{response.message} (#{response.code})"
94
94
  end
95
95
 
data/lib/ruze/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Ruze
2
- VERSION = '0.1.2'.freeze
2
+ VERSION = '0.2.0'.freeze
3
3
  end
data/lib/ruze.rb CHANGED
@@ -1,6 +1,3 @@
1
1
  require_relative 'ruze/version'
2
+ require_relative 'ruze/errors'
2
3
  require_relative 'ruze/car'
3
-
4
- module Ruze
5
- class Error < StandardError; end
6
- end
data/ruze.gemspec CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.description = 'Queries vehicle data like mileage, charging state and GPS location'
11
11
  spec.homepage = 'https://github.com/solectrus/ruze'
12
12
  spec.license = 'MIT'
13
- spec.required_ruby_version = Gem::Requirement.new('>= 2.7.0')
13
+ spec.required_ruby_version = Gem::Requirement.new('>= 4.0.0')
14
14
 
15
15
  spec.metadata['homepage_uri'] = spec.homepage
16
16
  spec.metadata['source_code_uri'] = 'https://github.com/solectrus/ruze'
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruze
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Georg Ledermann
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-04-16 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies: []
13
12
  description: Queries vehicle data like mileage, charging state and GPS location
14
13
  email:
@@ -31,6 +30,8 @@ files:
31
30
  - bin/setup
32
31
  - lib/ruze.rb
33
32
  - lib/ruze/car.rb
33
+ - lib/ruze/device.rb
34
+ - lib/ruze/errors.rb
34
35
  - lib/ruze/gigya.rb
35
36
  - lib/ruze/kamereon.rb
36
37
  - lib/ruze/version.rb
@@ -43,7 +44,6 @@ metadata:
43
44
  source_code_uri: https://github.com/solectrus/ruze
44
45
  changelog_uri: https://github.com/solectrus/ruze/releases
45
46
  rubygems_mfa_required: 'true'
46
- post_install_message:
47
47
  rdoc_options: []
48
48
  require_paths:
49
49
  - lib
@@ -51,15 +51,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: 2.7.0
54
+ version: 4.0.0
55
55
  required_rubygems_version: !ruby/object:Gem::Requirement
56
56
  requirements:
57
57
  - - ">="
58
58
  - !ruby/object:Gem::Version
59
59
  version: '0'
60
60
  requirements: []
61
- rubygems_version: 3.5.9
62
- signing_key:
61
+ rubygems_version: 4.0.12
63
62
  specification_version: 4
64
63
  summary: Unofficial Ruby client for the Renault ZE API
65
64
  test_files: []