weconnect 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/.gitignore +45 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile +6 -0
- data/Rakefile +19 -0
- data/lib/weconnect/api.rb +36 -0
- data/lib/weconnect/authorization.rb +232 -0
- data/lib/weconnect/client.rb +60 -0
- data/lib/weconnect/connection.rb +55 -0
- data/lib/weconnect/const.rb +116 -0
- data/lib/weconnect/control_operation.rb +26 -0
- data/lib/weconnect/error.rb +15 -0
- data/lib/weconnect/pagination.rb +22 -0
- data/lib/weconnect/request.rb +8 -0
- data/lib/weconnect/version.rb +5 -0
- data/lib/weconnect.rb +24 -0
- data/poekoerifab.doc +1001 -0
- data/snippets.rb +218 -0
- data/weconnect.gemspec +42 -0
- metadata +188 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b46aa0f0215101e02ed972ad36c7a1a9a67fb08d67944d8c080b3eb10b85c2c4
|
4
|
+
data.tar.gz: 48e88d187dea1793ba0fe3293acb83c02ad614ac32c890da085fa8b9948f3ef0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 257b77464932e392d531d71f1786a63e612fe92b056d999d814b81f06389abc7c4c9c0bbb9f0a6426c8a95d6df87f8df42732748ccffe683c39a91c4c10bc734
|
7
|
+
data.tar.gz: 75d62c8d770ce7f0655e36e996bdbdbac2c204d04a2a412694fc987921c45ce470ae1951e64376340636c2987ce64e535d8951ec0f9d76a42677cbf5e5f84a72
|
data/.gitignore
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/spec/examples.txt
|
9
|
+
/test/tmp/
|
10
|
+
/test/version_tmp/
|
11
|
+
/tmp/
|
12
|
+
/data/
|
13
|
+
*.log
|
14
|
+
*.txt
|
15
|
+
*.json
|
16
|
+
*.yml
|
17
|
+
.DS_Store
|
18
|
+
|
19
|
+
# Used by dotenv library to load environment variables.
|
20
|
+
.env
|
21
|
+
|
22
|
+
|
23
|
+
## Documentation cache and generated files:
|
24
|
+
/.yardoc/
|
25
|
+
/_yardoc/
|
26
|
+
/doc/
|
27
|
+
/rdoc/
|
28
|
+
|
29
|
+
## Environment normalization:
|
30
|
+
/.bundle/
|
31
|
+
/vendor/bundle
|
32
|
+
/lib/bundler/man/
|
33
|
+
|
34
|
+
# for a library or gem, you might want to ignore these files since the code is
|
35
|
+
# intended to run in multiple environments; otherwise, check them in:
|
36
|
+
# Gemfile.lock
|
37
|
+
# .ruby-version
|
38
|
+
# .ruby-gemset
|
39
|
+
|
40
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
41
|
+
.rvmrc
|
42
|
+
|
43
|
+
# Used by RuboCop. Remote config files pulled in from inherit_from directive.
|
44
|
+
# .rubocop-https?--*
|
45
|
+
Gemfile.lock
|
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'dotenv'
|
5
|
+
require 'rake/testtask'
|
6
|
+
|
7
|
+
Dotenv.load
|
8
|
+
|
9
|
+
#system './bin/cc-test-reporter before-build'
|
10
|
+
Rake::TestTask.new(:test) do |t|
|
11
|
+
t.libs << 'test'
|
12
|
+
t.libs << 'lib'
|
13
|
+
t.test_files = FileList['test/**/*_test.rb']
|
14
|
+
end
|
15
|
+
|
16
|
+
require 'rubocop/rake_task'
|
17
|
+
RuboCop::RakeTask.new
|
18
|
+
task default: %i[test rubocop]
|
19
|
+
#system './bin/cc-test-reporter after-build'
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require "wrapi"
|
2
|
+
require File.expand_path('connection', __dir__)
|
3
|
+
require File.expand_path('request', __dir__)
|
4
|
+
require File.expand_path('authorization', __dir__)
|
5
|
+
|
6
|
+
module WeConnect
|
7
|
+
# @private
|
8
|
+
class API
|
9
|
+
|
10
|
+
# @private
|
11
|
+
attr_accessor *WrAPI::Configuration::VALID_OPTIONS_KEYS
|
12
|
+
|
13
|
+
# Creates a new API and copies settings from singleton
|
14
|
+
def initialize(options = {})
|
15
|
+
options = WeConnect.options.merge(options)
|
16
|
+
WrAPI::Configuration::VALID_OPTIONS_KEYS.each do |key|
|
17
|
+
send("#{key}=", options[key])
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def config
|
22
|
+
conf = {}
|
23
|
+
WrAPI::Configuration::VALID_OPTIONS_KEYS.each do |key|
|
24
|
+
conf[key] = send key
|
25
|
+
end
|
26
|
+
conf
|
27
|
+
end
|
28
|
+
|
29
|
+
include WrAPI::Connection
|
30
|
+
include Connection
|
31
|
+
include WrAPI::Request
|
32
|
+
include WrAPI::Authentication
|
33
|
+
include Authentication
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,232 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'yaml'
|
3
|
+
require 'nokogiri'
|
4
|
+
require File.expand_path('error', __dir__)
|
5
|
+
|
6
|
+
module WeConnect
|
7
|
+
# Deals with authentication flow and stores it within global configuration
|
8
|
+
module Authentication
|
9
|
+
TOKENS = %w(state id_token access_token code)
|
10
|
+
TOKEN_URL = 'https://emea.bff.cariad.digital/user-login/login/v1'.freeze
|
11
|
+
REFRESH_URL = 'https://emea.bff.cariad.digital/user-login/refresh/v1'.freeze
|
12
|
+
|
13
|
+
# Authorize to the WeConnect portal
|
14
|
+
def login(options = {})
|
15
|
+
raise ConfigurationError, "username/password not set" unless username && password
|
16
|
+
# only bearer token needed
|
17
|
+
car = CarConnectInfo.new
|
18
|
+
|
19
|
+
@tokens = WebLogin.new(self,car).login
|
20
|
+
api_process_token(@tokens)
|
21
|
+
reauth_connection(self.access_token)
|
22
|
+
self.access_token
|
23
|
+
end
|
24
|
+
def auth_tokens
|
25
|
+
@tokens
|
26
|
+
end
|
27
|
+
def api_process_token(tokens)
|
28
|
+
self.access_token = tokens['access_token']
|
29
|
+
self.token_type = tokens['token_type']
|
30
|
+
self.refresh_token = tokens['refresh_token']
|
31
|
+
self.token_expires = tokens['expires_at'] if tokens['expires_at']
|
32
|
+
end
|
33
|
+
|
34
|
+
def refresh_token
|
35
|
+
raise Error.new 'not implemented'
|
36
|
+
end
|
37
|
+
|
38
|
+
# private
|
39
|
+
class CarConnectInfo
|
40
|
+
attr_reader :type, :country, :xrequest, :xclient_id, :client_id, :scope, :response_type, :redirect, :refresh_url
|
41
|
+
def initialize
|
42
|
+
@type = "Id";
|
43
|
+
@country = "DE";
|
44
|
+
@xrequest = "com.volkswagen.weconnect";
|
45
|
+
@xclientId = "";
|
46
|
+
@client_id = "a24fba63-34b3-4d43-b181-942111e6bda8@apps_vw-dilab_com";
|
47
|
+
|
48
|
+
@scope = "openid profile badge cars dealers vin";
|
49
|
+
@response_type = "code id_token token";
|
50
|
+
@redirect = "weconnect://authenticated";
|
51
|
+
@refresh_url='https://identity.vwgroup.io/oidc/v1/token'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
class WebLogin
|
55
|
+
def initialize(connection,car_info)
|
56
|
+
@connection = connection
|
57
|
+
@car_info = car_info
|
58
|
+
end
|
59
|
+
def login
|
60
|
+
@connection.format = 'x-www-form-urlencoded'
|
61
|
+
page = login_page_step
|
62
|
+
form = PasswordFormParser.new(page.body)
|
63
|
+
page = email_page_step(form)
|
64
|
+
idk = IDKParser.new(page.body)
|
65
|
+
page = password_page_step(idk)
|
66
|
+
|
67
|
+
raise IncompatibleAPIError.new( "#{@car_info.redirect} redirect not found" )
|
68
|
+
rescue WeconnectAuthenticated => authenticated
|
69
|
+
# weconnect://authenticatied#... extpected
|
70
|
+
|
71
|
+
@tokens = query_parameters(URI.parse(authenticated.redirect).fragment)
|
72
|
+
# fetch final tokens from login
|
73
|
+
@tokens = fetch_tokens(@tokens)
|
74
|
+
end
|
75
|
+
private
|
76
|
+
def login_page_step
|
77
|
+
params = {
|
78
|
+
nonce: nonce(),
|
79
|
+
redirect_uri: @car_info.redirect
|
80
|
+
}
|
81
|
+
r = @connection.get('https://emea.bff.cariad.digital/user-login/v1/authorize',params,true)
|
82
|
+
@login_url = r.env.url
|
83
|
+
r
|
84
|
+
end
|
85
|
+
|
86
|
+
def email_page_step(form)
|
87
|
+
fields = form.fields
|
88
|
+
fields['email'] = @connection.username
|
89
|
+
# update to login form
|
90
|
+
@login_url = URI.join(@login_url, form.action)
|
91
|
+
r = @connection.post(@login_url.to_s,fields,true) do |request|
|
92
|
+
request.headers=request.headers.merge({
|
93
|
+
'x-requested-with': @car_info.xrequest,
|
94
|
+
'upgrade-insecure-requests': "1"
|
95
|
+
})
|
96
|
+
end
|
97
|
+
end
|
98
|
+
def password_page_step(idk)
|
99
|
+
params = {
|
100
|
+
:email => @connection.username,
|
101
|
+
:password => @connection.password,
|
102
|
+
idk.idk['csrf_parameterName'] => idk.idk['csrf_token'],
|
103
|
+
:hmac => idk.template_model['hmac'],
|
104
|
+
'relayState' => idk.template_model['relayState']
|
105
|
+
}
|
106
|
+
|
107
|
+
rpw = @connection.post(idk.post_action_uri(@login_url),params,true) do |request|
|
108
|
+
request.headers=request.headers.merge({
|
109
|
+
'x-requested-with': @car_info.xrequest,
|
110
|
+
'upgrade-insecure-requests': "1"
|
111
|
+
})
|
112
|
+
end
|
113
|
+
# should not come here, exception raised by auth redirect
|
114
|
+
if rpw.env.url.query['login.error']
|
115
|
+
params = query_parameters(rpw.env.url.query)
|
116
|
+
description = {
|
117
|
+
'login.errors.password_invalid': 'Password is invalid',
|
118
|
+
'login.error.throttled': 'Login throttled, probably too many wrong logins. You have to wait some minutes until a new login attempt is possible'
|
119
|
+
}
|
120
|
+
error = params['error']
|
121
|
+
error = description[error] if description[error]
|
122
|
+
raise AuthenticationError.new( "Login error #{error}" )
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def fetch_tokens(tokens)
|
127
|
+
# check if all keys exist
|
128
|
+
if TOKENS.all? { |s| tokens.key? s }
|
129
|
+
params = {
|
130
|
+
'state': tokens['state'],
|
131
|
+
'id_token': tokens['id_token'],
|
132
|
+
'redirect_uri': @car_info.redirect,
|
133
|
+
'region': 'emea',
|
134
|
+
'access_token': tokens['access_token'],
|
135
|
+
'authorizationCode': tokens['code'],
|
136
|
+
}
|
137
|
+
|
138
|
+
@connection.format = :json
|
139
|
+
@connection.reauth_connection(tokens['id_token'])
|
140
|
+
# complete set tokens
|
141
|
+
token_response = @connection.post(TOKEN_URL, params)
|
142
|
+
# translate token names to _token suffix
|
143
|
+
token_response = translate_tokens(token_response.body, %w(accessToken idToken refreshToken))
|
144
|
+
token_response = parse_token_response(token_response)
|
145
|
+
token_response
|
146
|
+
else
|
147
|
+
raise IncompatibleAPIError.new( 'Expected tokens: #{TOKENS}, but found: #{tokens}' )
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def translate_tokens(tokens, keys)
|
152
|
+
keys.each do |name|
|
153
|
+
if tokens[name]
|
154
|
+
tokens[name.gsub('Token', '_token')] = tokens.delete(name)
|
155
|
+
end
|
156
|
+
end
|
157
|
+
tokens
|
158
|
+
end
|
159
|
+
# oauthlib/oauth2/rfc6749/parameters.py
|
160
|
+
def parse_token_response(tokens)
|
161
|
+
puts "\n\nEXPIRE #{tokens['expires_in']}\n\n"
|
162
|
+
tokens['expires_at'] = Time.new() + tokens['expires_in'] if tokens['expires_in']
|
163
|
+
# validate
|
164
|
+
raise AuthenticationError.new(tokens['error']) if tokens['error']
|
165
|
+
#raise AuthenticationError.new('Missing access token error') if tokens['access_token']
|
166
|
+
|
167
|
+
tokens
|
168
|
+
end
|
169
|
+
|
170
|
+
def query_parameters(query_fragment)
|
171
|
+
parameters = query_fragment.split('&').inject({}) do |result,param|
|
172
|
+
k,v = param.split('=');
|
173
|
+
result.merge({k => v })
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
def nonce
|
178
|
+
rand(10 ** 30).to_s.rjust(30,'0')
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
|
183
|
+
class PasswordFormParser
|
184
|
+
attr_reader :action, :method, :fields
|
185
|
+
def initialize(html)
|
186
|
+
doc = Nokogiri::HTML( html )
|
187
|
+
# 1 loginform username
|
188
|
+
form = doc/'form[name="emailPasswordForm"]'
|
189
|
+
if form
|
190
|
+
@action = form.attribute('action').to_s
|
191
|
+
@method = form.attribute('method').to_s
|
192
|
+
# get hidden fields
|
193
|
+
|
194
|
+
@fields = {}
|
195
|
+
(form/'input').each do |input|
|
196
|
+
@fields[input.attribute('name').to_s] = input.attribute('value').to_s
|
197
|
+
end
|
198
|
+
else
|
199
|
+
raise IncompatibleAPIError.new( 'emailPasswordForm not found' )
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
class IDKParser
|
205
|
+
attr_reader :idk, :template_model, :post_action, :identifier, :error
|
206
|
+
def initialize(html)
|
207
|
+
doc = Nokogiri::HTML( html )
|
208
|
+
# get script with IDK
|
209
|
+
scripts = doc./'script:contains("_IDK")'
|
210
|
+
# extract json part by greedy match till last '}'
|
211
|
+
m = scripts.text.gsub("\n",'').gsub(/([\w]+):/, '"\1":').match('({.*})')
|
212
|
+
@idk = {}
|
213
|
+
if m.size > 1
|
214
|
+
# load with yam due to single quotes
|
215
|
+
@idk = YAML.load(m[1])
|
216
|
+
raise IncompatibleAPIError.new( "_IDK.templateModel not found #{@idk}" ) unless @idk['templateModel']
|
217
|
+
else
|
218
|
+
raise IncompatibleAPIError.new( "_IDK not found" )
|
219
|
+
end
|
220
|
+
# r = self.__get_url(upr.scheme+'://'+upr.netloc+form_url.replace(idk['templateModel']['identifierUrl'],idk['templateModel']['postAction']), post=post)
|
221
|
+
@template_model = @idk['templateModel']
|
222
|
+
@post_action = @template_model['postAction']
|
223
|
+
@identifier = @template_model['identifierUrl']
|
224
|
+
@error = @template_model['error']
|
225
|
+
end
|
226
|
+
|
227
|
+
def post_action_uri(base_uri)
|
228
|
+
base_uri.to_s.gsub(@identifier,@post_action)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require File.expand_path('api', __dir__)
|
2
|
+
require File.expand_path('const', __dir__)
|
3
|
+
require File.expand_path('error', __dir__)
|
4
|
+
|
5
|
+
module WeConnect
|
6
|
+
# Wrapper for the WeConnect REST API
|
7
|
+
#
|
8
|
+
# @see no docs, reversed engineered
|
9
|
+
class Client < API
|
10
|
+
attr_accessor :openid_config
|
11
|
+
|
12
|
+
def initialize(options = {})
|
13
|
+
super(options)
|
14
|
+
|
15
|
+
@openid_config = openid_configuration
|
16
|
+
end
|
17
|
+
|
18
|
+
def vehicles(params={})
|
19
|
+
self.get(vehicle_api,params)
|
20
|
+
end
|
21
|
+
def vehicle_status(vin, jobs=['all'])
|
22
|
+
jobs = jobs.join(',') if jobs.is_a? Array
|
23
|
+
self.get(vehicle_api(vin,"/selectivestatus?jobs=#{jobs}"))
|
24
|
+
end
|
25
|
+
|
26
|
+
def parking(vin)
|
27
|
+
self.get(vehicle_api(vin,'/parkingposition'))
|
28
|
+
end
|
29
|
+
|
30
|
+
def trips(vin,trip_type=TripType::SHORT_TERM,period)
|
31
|
+
sef.get('/vehicle/v1/trips/#{vin}/#{trip_type.downcase}/last',params)
|
32
|
+
end
|
33
|
+
|
34
|
+
def images(vin)
|
35
|
+
self.get("/media/v2/vehicle-images/{self.vin.value}?resolution=2x")
|
36
|
+
end
|
37
|
+
|
38
|
+
def control(vin, operation, value)
|
39
|
+
self.post(vehicle_api(vin,"/#{operation}/#{value}"))
|
40
|
+
end
|
41
|
+
def control_charging(vin, value)
|
42
|
+
if ControlOperation.allowed_values.includes? value
|
43
|
+
control(vin,'charging',value)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
private
|
47
|
+
def openid_configuration
|
48
|
+
get(self.endpoint)
|
49
|
+
end
|
50
|
+
|
51
|
+
PATH = "/vehicle/v1/vehicles".freeze
|
52
|
+
def vehicle_api(vin=nil,path=nil)
|
53
|
+
if vin
|
54
|
+
File.join PATH, vin, path
|
55
|
+
else
|
56
|
+
PATH
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'faraday'
|
2
|
+
require 'faraday/follow_redirects'
|
3
|
+
require 'faraday-cookie_jar'
|
4
|
+
require File.expand_path('error', __dir__)
|
5
|
+
|
6
|
+
module WeConnect
|
7
|
+
class WeconnectAuthenticated < WeConnectError
|
8
|
+
attr_reader :redirect
|
9
|
+
def initialize(location)
|
10
|
+
@redirect = location
|
11
|
+
end
|
12
|
+
end
|
13
|
+
# Create connection including authorization parameters with default Accept format and User-Agent
|
14
|
+
# By default
|
15
|
+
# - Bearer authorization is access_token is not nil override with @setup_authorization
|
16
|
+
# - Headers setup for client-id and client-secret when client_id and client_secret are not nil @setup_headers
|
17
|
+
# @private
|
18
|
+
module Connection
|
19
|
+
class WeConnectMiddleware < Faraday::Middleware
|
20
|
+
def call(env)
|
21
|
+
response = @app.call(env)
|
22
|
+
if location = response['location']
|
23
|
+
raise WeconnectAuthenticated.new(location) if location['weconnect:']
|
24
|
+
end
|
25
|
+
response
|
26
|
+
end
|
27
|
+
end
|
28
|
+
def reauth_connection(token)
|
29
|
+
self.access_token = token
|
30
|
+
setup_authorization(@connection)
|
31
|
+
end
|
32
|
+
private
|
33
|
+
def connection
|
34
|
+
raise ConfigurationError, "Option for endpoint is not defined" unless endpoint
|
35
|
+
|
36
|
+
options = setup_options
|
37
|
+
@connection ||= Faraday::Connection.new(options) do |connection|
|
38
|
+
connection.use Faraday::FollowRedirects::Middleware, limit: 10
|
39
|
+
|
40
|
+
connection.use WeConnectMiddleware
|
41
|
+
connection.use :cookie_jar
|
42
|
+
|
43
|
+
connection.use Faraday::Response::RaiseError
|
44
|
+
connection.adapter Faraday.default_adapter
|
45
|
+
setup_authorization(connection)
|
46
|
+
setup_headers(connection)
|
47
|
+
connection.response :json, content_type: /\bjson$/
|
48
|
+
connection.use Faraday::Request::UrlEncoded
|
49
|
+
|
50
|
+
setup_logger_filtering(connection, logger) if logger
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
class String
|
2
|
+
def underscore
|
3
|
+
gsub(/::/, '/').
|
4
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
5
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
6
|
+
tr("-", "_").
|
7
|
+
downcase
|
8
|
+
end
|
9
|
+
end
|
10
|
+
module WeConnect
|
11
|
+
class Enum
|
12
|
+
def self.enum(array, proc=:to_s)
|
13
|
+
array.each do |c|
|
14
|
+
const_set c.underscore.upcase,c.send(proc)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
class Role < Enum
|
19
|
+
enum %w[PRIMARY_USER SECONDARY_USER GUEST_USER CDIS_UNKNOWN_USER UNKNOWN]
|
20
|
+
end
|
21
|
+
|
22
|
+
class EnrollmentStatus < Enum
|
23
|
+
enum %w[STARTED NOT_STARTED COMPLETED GDC_MISSING INACTIVE UNKNOWN]
|
24
|
+
end
|
25
|
+
class UserRoleStatus < Enum
|
26
|
+
enum %w[ENABLED DISABLED_HMI DISABLED_SPIN DISABLED_PU_SPIN_RESET CDIS_UNKNOWN_USER UNKNOWN]
|
27
|
+
end
|
28
|
+
class Status
|
29
|
+
UNKNOWN = 0
|
30
|
+
|
31
|
+
DEACTIVATED = 1001
|
32
|
+
INITIALLY_DISABLED = 1003
|
33
|
+
DISABLED_BY_USER = 1004
|
34
|
+
OFFLINE_MODE = 1005
|
35
|
+
WORKSHOP_MODE = 1006
|
36
|
+
MISSING_OPERATION = 1007
|
37
|
+
MISSING_SERVICE = 1008
|
38
|
+
PLAY_PROTECTION = 1009
|
39
|
+
POWER_BUDGET_REACHED = 1010
|
40
|
+
DEEP_SLEEP = 1011
|
41
|
+
LOCATION_DATA_DISABLED = 1013
|
42
|
+
|
43
|
+
LICENSE_INACTIVE = 2001
|
44
|
+
LICENSE_EXPIRED = 2002
|
45
|
+
MISSING_LICENSE = 2003
|
46
|
+
|
47
|
+
USER_NOT_VERIFIED = 3001
|
48
|
+
TERMS_AND_CONDITIONS_NOT_ACCEPTED = 3002
|
49
|
+
INSUFFICIENT_RIGHTS = 3003
|
50
|
+
CONSENT_MISSING = 3004
|
51
|
+
LIMITED_FEATURE = 3005
|
52
|
+
AUTH_APP_CERT_ERROR = 3006
|
53
|
+
|
54
|
+
STATUS_UNSUPPORTED = 4001
|
55
|
+
|
56
|
+
KNOWN_STATUS = self.constants.inject([]){|result,const| result << self.const_get(const)}
|
57
|
+
def self.known_status? status
|
58
|
+
KNOWN_STATUS.include? status
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class Badge < Enum
|
63
|
+
enum %w[charging connected cooling heating locked parking unlocked ventilating warning]
|
64
|
+
end
|
65
|
+
|
66
|
+
class DevicePlatform < Enum
|
67
|
+
enum %w[MBB MBB_ODP MBB_OFFLINE WCAR UNKNOWN]
|
68
|
+
end
|
69
|
+
|
70
|
+
class BrandCode
|
71
|
+
N = 'N'
|
72
|
+
V = 'V'
|
73
|
+
UNKNOWN = 'unknown brand code'
|
74
|
+
end
|
75
|
+
class JobDomain < Enum
|
76
|
+
enum %w[
|
77
|
+
access activeVentilation automation auxiliaryHeating
|
78
|
+
userCapabilities charging chargingProfiles batteryChargingCare
|
79
|
+
climatisation climatisationTimers departureTimers
|
80
|
+
fuelStatus vehicleLights lvBattery readiness
|
81
|
+
vehicleHealthInspection vehicleHealthWarnings oilLevel
|
82
|
+
measurements batterySupport
|
83
|
+
]
|
84
|
+
JOB_DOMAINS = self.constants.inject([]){|result,const| result << self.const_get(const)}
|
85
|
+
end
|
86
|
+
class AllDomains < JobDomain
|
87
|
+
enum %w[
|
88
|
+
all allCapable parking trips
|
89
|
+
]
|
90
|
+
end
|
91
|
+
|
92
|
+
class TripType < Enum
|
93
|
+
enum %w[shortTerm longTerm cyclic]
|
94
|
+
UNKNOWN = 'unkown trip type'
|
95
|
+
end
|
96
|
+
|
97
|
+
class PlugConnectionState < Enum
|
98
|
+
enum %w[connected disconnected invalid unsupported]
|
99
|
+
UNKNOWN = 'unknown unlock plug state'
|
100
|
+
end
|
101
|
+
|
102
|
+
class PlugLockState < Enum
|
103
|
+
enum %w[locked unlocked invalid unsupported]
|
104
|
+
UNKNOWN = 'unknown unlock plug state'
|
105
|
+
end
|
106
|
+
|
107
|
+
class ExternalPower < Enum
|
108
|
+
enum %w[ready active unavailable invalid unsupported]
|
109
|
+
UNKNOWN = 'unknown external power'
|
110
|
+
end
|
111
|
+
|
112
|
+
class LedColor < Enum
|
113
|
+
enum %w[nune green red]
|
114
|
+
UNKNOWN = 'unknown plug led color'
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require File.expand_path('const', __dir__)
|
2
|
+
|
3
|
+
module WeConnect
|
4
|
+
module Control
|
5
|
+
# all possible operations
|
6
|
+
class Operation < Enum
|
7
|
+
enum %w[start stop settings lock unlock flash, honkandflash, timers mode profiles unknown]
|
8
|
+
end
|
9
|
+
|
10
|
+
class ControlOperation < Enum
|
11
|
+
enum %w[start stop none settings unknown]
|
12
|
+
|
13
|
+
def self.allowed_values
|
14
|
+
[START, STOP]
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class AccessControlOperation < Enum
|
19
|
+
enum %w[lock unlock none unknown]
|
20
|
+
end
|
21
|
+
|
22
|
+
class HonkAndFlashControlOperation < Enum
|
23
|
+
enum %w[flash honkandflash none unknown]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module WeConnect
|
2
|
+
|
3
|
+
# Generic error to be able to rescue all WeConnect errors
|
4
|
+
class WeConnectError < StandardError; end
|
5
|
+
|
6
|
+
# configuration returns error
|
7
|
+
class IncompatibleAPIError < WeConnectError; end
|
8
|
+
|
9
|
+
# configuration returns error
|
10
|
+
class ConfigurationError < WeConnectError; end
|
11
|
+
|
12
|
+
# Issue authenticting
|
13
|
+
class AuthenticationError < WeConnectError; end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module WeConnect
|
5
|
+
# Defines HTTP request methods
|
6
|
+
# required attributes format
|
7
|
+
module RequestPagination
|
8
|
+
|
9
|
+
# Defaut pages assumes all data retrieved in a single go.
|
10
|
+
class DefaultPager < WrAPI::RequestPagination::DefaultPager
|
11
|
+
|
12
|
+
def self.data(body)
|
13
|
+
if body['data']
|
14
|
+
body['data']
|
15
|
+
else
|
16
|
+
body
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|