ewelink 2.2.1 → 3.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 +4 -4
- data/README.mdown +5 -0
- data/VERSION +1 -1
- data/lib/ewelink/api.rb +127 -97
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c9b83156bba48622088845d22bd8fc3b559d6e8732acf4664ba35886c6c19d35
|
4
|
+
data.tar.gz: d69303e93b04b56fd713fc27220d15c4ab66ca767190a289d6440932dc468532
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fa3fd8b8a36ad7e4570db2941ebdffc1d134106ea40ce93866eaebe7b81a4cc0a35920f5366a48c0a9141476101cc03be42adf244b15bcf98eeec64b0e77a312
|
7
|
+
data.tar.gz: d98f59eaec9c582dd0627a52a392c45e98e351676b69d36f081df4ec819153bd32e340ced77440152c674d58013f413d8e42a5e86a50f65d57efce5782f26943
|
data/README.mdown
CHANGED
@@ -75,6 +75,11 @@ api = Ewelink::Api.new(email: 'john@example.com', password: 'secr$t')
|
|
75
75
|
api.press_rf_bridge_button!(button[:uuid])
|
76
76
|
```
|
77
77
|
|
78
|
+
### Additional options
|
79
|
+
|
80
|
+
- `update_devices_status_on_connect` (`true` | `false`): To update devices
|
81
|
+
status (on, off) when connecting to Ewelink API (default: `false`).
|
82
|
+
|
78
83
|
### Configuring logger
|
79
84
|
|
80
85
|
In order to have some debug informations about what kagu does, you could
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
3.2.0
|
data/lib/ewelink/api.rb
CHANGED
@@ -11,26 +11,31 @@ module Ewelink
|
|
11
11
|
URL = 'https://#{region}-api.coolkit.cc:8080'
|
12
12
|
UUID_NAMESPACE = 'e25750fb-3710-41af-b831-23224f4dd609';
|
13
13
|
VERSION = 8
|
14
|
+
WEB_SOCKET_CHECK_AUTHENTICATION_TIMEOUT = 30.seconds
|
14
15
|
WEB_SOCKET_PING_TOLERANCE_FACTOR = 1.5
|
15
16
|
SWITCH_STATUS_CHANGE_CHECK_TIMEOUT = 2.seconds
|
16
17
|
WEB_SOCKET_WAIT_INTERVAL = 0.2.seconds
|
17
18
|
|
18
19
|
attr_reader :email, :password, :phone_number
|
19
20
|
|
20
|
-
def initialize(email: nil, password:, phone_number: nil)
|
21
|
+
def initialize(email: nil, password:, phone_number: nil, update_devices_status_on_connect: false)
|
21
22
|
@email = email.presence.try(:strip)
|
22
23
|
@mutexs = {}
|
23
24
|
@password = password.presence || raise(Error.new(":password must be specified"))
|
24
25
|
@phone_number = phone_number.presence.try(:strip)
|
25
|
-
@
|
26
|
+
@update_devices_status_on_connect = update_devices_status_on_connect.present?
|
27
|
+
@web_socket_authenticated = false
|
26
28
|
@web_socket_switches_statuses = {}
|
27
|
-
|
29
|
+
|
30
|
+
raise(Error.new(':email or :phone_number must be specified')) if email.blank? && phone_number.blank?
|
31
|
+
|
32
|
+
start_web_socket_authentication_check_thread
|
28
33
|
end
|
29
34
|
|
30
35
|
def press_rf_bridge_button!(uuid)
|
31
36
|
synchronize(:press_rf_bridge_button) do
|
32
37
|
button = find_rf_bridge_button!(uuid)
|
33
|
-
web_socket_wait_for(-> { web_socket_authenticated? }) do
|
38
|
+
web_socket_wait_for(-> { web_socket_authenticated? }, initialize_web_socket: true) do
|
34
39
|
params = {
|
35
40
|
'action' => 'update',
|
36
41
|
'apikey' => button[:api_key],
|
@@ -51,15 +56,40 @@ module Ewelink
|
|
51
56
|
end
|
52
57
|
|
53
58
|
def reload
|
54
|
-
Ewelink.logger.debug(self.class.name) { 'Reloading API (authentication token, devices, region,...)' }
|
55
|
-
|
59
|
+
Ewelink.logger.debug(self.class.name) { 'Reloading API (authentication token, api key, devices, region, connections,...)' }
|
60
|
+
|
61
|
+
@web_socket_authenticated = false
|
62
|
+
@web_socket_switches_statuses.clear
|
63
|
+
|
64
|
+
[@web_socket_ping_thread, @web_socket_thread].each do |thread|
|
65
|
+
next unless thread
|
66
|
+
if Thread.current == thread
|
67
|
+
thread[:stop] = true
|
68
|
+
else
|
69
|
+
thread.kill
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
if @web_socket.present?
|
74
|
+
begin
|
75
|
+
@web_socket.close if @web_socket.open?
|
76
|
+
rescue
|
77
|
+
# Ignoring close errors
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
56
81
|
[
|
57
|
-
:@
|
58
|
-
:@authentication_token,
|
82
|
+
:@authentication_infos,
|
59
83
|
:@devices,
|
84
|
+
:@last_web_socket_pong_at,
|
60
85
|
:@region,
|
61
86
|
:@rf_bridge_buttons,
|
62
87
|
:@switches,
|
88
|
+
:@web_socket_ping_interval,
|
89
|
+
:@web_socket_ping_thread,
|
90
|
+
:@web_socket_thread,
|
91
|
+
:@web_socket_url,
|
92
|
+
:@web_socket,
|
63
93
|
].each do |variable|
|
64
94
|
remove_instance_variable(variable) if instance_variable_defined?(variable)
|
65
95
|
end
|
@@ -103,7 +133,7 @@ module Ewelink
|
|
103
133
|
def switch_on?(uuid)
|
104
134
|
switch = find_switch!(uuid)
|
105
135
|
if @web_socket_switches_statuses[switch[:uuid]].nil?
|
106
|
-
web_socket_wait_for(-> { web_socket_authenticated? }) do
|
136
|
+
web_socket_wait_for(-> { web_socket_authenticated? }, initialize_web_socket: true) do
|
107
137
|
Ewelink.logger.debug(self.class.name) { "Checking switch #{switch[:uuid].inspect} status" }
|
108
138
|
params = {
|
109
139
|
'action' => 'query',
|
@@ -116,7 +146,7 @@ module Ewelink
|
|
116
146
|
send_to_web_socket(JSON.generate(params))
|
117
147
|
end
|
118
148
|
end
|
119
|
-
web_socket_wait_for(-> { !@web_socket_switches_statuses[switch[:uuid]].nil? }) do
|
149
|
+
web_socket_wait_for(-> { !@web_socket_switches_statuses[switch[:uuid]].nil? }, initialize_web_socket: true) do
|
120
150
|
@web_socket_switches_statuses[switch[:uuid]] == 'on'
|
121
151
|
end
|
122
152
|
end
|
@@ -149,7 +179,7 @@ module Ewelink
|
|
149
179
|
end
|
150
180
|
switch = find_switch!(uuid)
|
151
181
|
@web_socket_switches_statuses[switch[:uuid]] = nil
|
152
|
-
web_socket_wait_for(-> { web_socket_authenticated? }) do
|
182
|
+
web_socket_wait_for(-> { web_socket_authenticated? }, initialize_web_socket: true) do
|
153
183
|
params = {
|
154
184
|
'action' => 'update',
|
155
185
|
'apikey' => switch[:api_key],
|
@@ -169,39 +199,39 @@ module Ewelink
|
|
169
199
|
true
|
170
200
|
end
|
171
201
|
|
202
|
+
def update_devices_status_on_connect?
|
203
|
+
@update_devices_status_on_connect
|
204
|
+
end
|
205
|
+
|
172
206
|
private
|
173
207
|
|
174
|
-
def
|
175
|
-
|
176
|
-
@api_keys ||= Set.new(devices.map { |device| device['apikey'] })
|
177
|
-
end
|
208
|
+
def api_key
|
209
|
+
authentication_infos[:api_key]
|
178
210
|
end
|
179
211
|
|
180
|
-
def
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
send_to_web_socket(JSON.generate(params))
|
195
|
-
end
|
212
|
+
def authenticate_web_socket_api_key
|
213
|
+
params = {
|
214
|
+
'action' => 'userOnline',
|
215
|
+
'apikey' => api_key,
|
216
|
+
'appid' => APP_ID,
|
217
|
+
'at' => authentication_token,
|
218
|
+
'nonce' => nonce,
|
219
|
+
'sequence' => web_socket_sequence,
|
220
|
+
'ts' => Time.now.to_i,
|
221
|
+
'userAgent' => 'app',
|
222
|
+
'version' => VERSION,
|
223
|
+
}
|
224
|
+
Ewelink.logger.debug(self.class.name) { "Authenticating WebSocket API key: #{api_key.truncate(16).inspect}" }
|
225
|
+
send_to_web_socket(JSON.generate(params))
|
196
226
|
end
|
197
227
|
|
198
228
|
def authentication_headers
|
199
229
|
{ 'Authorization' => "Bearer #{authentication_token}" }
|
200
230
|
end
|
201
231
|
|
202
|
-
def
|
203
|
-
synchronize(:
|
204
|
-
@
|
232
|
+
def authentication_infos
|
233
|
+
synchronize(:authentication_infos) do
|
234
|
+
@authentication_infos ||= begin
|
205
235
|
params = {
|
206
236
|
'appid' => APP_ID,
|
207
237
|
'imei' => SecureRandom.uuid.upcase,
|
@@ -218,11 +248,19 @@ module Ewelink
|
|
218
248
|
body = JSON.generate(params)
|
219
249
|
response = rest_request(:post, '/api/user/login', { body: body, headers: { 'Authorization' => "Sign #{Base64.encode64(OpenSSL::HMAC.digest('SHA256', APP_SECRET, body))}" } })
|
220
250
|
raise(Error.new('Authentication token not found')) if response['at'].blank?
|
221
|
-
|
251
|
+
raise(Error.new('API key not found')) if response['user'].blank? || response['user']['apikey'].blank?
|
252
|
+
{
|
253
|
+
authentication_token: response['at'].tap { Ewelink.logger.debug(self.class.name) { 'Authentication token found' } },
|
254
|
+
api_key: response['user']['apikey'].tap { Ewelink.logger.debug(self.class.name) { 'API key found' } },
|
255
|
+
}
|
222
256
|
end
|
223
257
|
end
|
224
258
|
end
|
225
259
|
|
260
|
+
def authentication_token
|
261
|
+
authentication_infos[:authentication_token]
|
262
|
+
end
|
263
|
+
|
226
264
|
def devices
|
227
265
|
synchronize(:devices) do
|
228
266
|
@devices ||= begin
|
@@ -239,40 +277,6 @@ module Ewelink
|
|
239
277
|
end
|
240
278
|
end
|
241
279
|
|
242
|
-
def dispose_web_socket
|
243
|
-
Ewelink.logger.debug(self.class.name) { 'Dispose WebSocket' }
|
244
|
-
@web_socket_authenticated_api_keys.clear
|
245
|
-
@web_socket_switches_statuses.clear
|
246
|
-
|
247
|
-
[@web_socket_ping_thread, @web_socket_thread].each do |thread|
|
248
|
-
next unless thread
|
249
|
-
if Thread.current == thread
|
250
|
-
thread[:stop] = true
|
251
|
-
else
|
252
|
-
thread.kill
|
253
|
-
end
|
254
|
-
end
|
255
|
-
|
256
|
-
if @web_socket.present?
|
257
|
-
begin
|
258
|
-
@web_socket.close if @web_socket.open?
|
259
|
-
rescue
|
260
|
-
# Ignoring close errors
|
261
|
-
end
|
262
|
-
end
|
263
|
-
|
264
|
-
[
|
265
|
-
:@last_web_socket_pong_at,
|
266
|
-
:@web_socket_ping_interval,
|
267
|
-
:@web_socket_ping_thread,
|
268
|
-
:@web_socket_thread,
|
269
|
-
:@web_socket_url,
|
270
|
-
:@web_socket,
|
271
|
-
].each do |variable|
|
272
|
-
remove_instance_variable(variable) if instance_variable_defined?(variable)
|
273
|
-
end
|
274
|
-
end
|
275
|
-
|
276
280
|
def find_rf_bridge_button!(uuid)
|
277
281
|
rf_bridge_buttons.find { |button| button[:uuid] == uuid } || raise(Error.new("No such RF bridge button with UUID: #{uuid.inspect}"))
|
278
282
|
end
|
@@ -301,7 +305,7 @@ module Ewelink
|
|
301
305
|
Ewelink.logger.debug(self.class.name) { "Switched to region #{region.inspect}" }
|
302
306
|
return rest_request(method, path, options)
|
303
307
|
end
|
304
|
-
remove_instance_variable(:@
|
308
|
+
remove_instance_variable(:@authentication_infos) if instance_variable_defined?(:@authentication_infos) && [401, 403].include?(response['error'])
|
305
309
|
raise(Error.new("#{method} #{url}: #{response['error']} #{response['msg']}".strip)) if response['error'].present? && response['error'] != 0
|
306
310
|
response.to_h
|
307
311
|
rescue Errno::ECONNREFUSED, OpenSSL::OpenSSLError, SocketError, Timeout::Error => e
|
@@ -309,16 +313,30 @@ module Ewelink
|
|
309
313
|
end
|
310
314
|
|
311
315
|
def send_to_web_socket(message)
|
312
|
-
if web_socket_outdated_ping?
|
313
|
-
Ewelink.logger.warn(self.class.name) { 'WebSocket ping is outdated' }
|
314
|
-
dispose_web_socket
|
315
|
-
end
|
316
316
|
web_socket.send(message)
|
317
317
|
rescue => e
|
318
|
-
|
318
|
+
reload
|
319
319
|
raise Error.new(e)
|
320
320
|
end
|
321
321
|
|
322
|
+
def start_web_socket_authentication_check_thread
|
323
|
+
raise Error.new('WebSocket authentication check must only be started once') if @web_socket_authentication_check_thread.present?
|
324
|
+
|
325
|
+
@web_socket_authentication_check_thread = Thread.new do
|
326
|
+
loop do
|
327
|
+
Ewelink.logger.debug(self.class.name) { 'Checking if WebSocket is authenticated' }
|
328
|
+
begin
|
329
|
+
web_socket_wait_for(-> { web_socket_authenticated? }, initialize_web_socket: true) do
|
330
|
+
Ewelink.logger.debug(self.class.name) { 'WebSocket is authenticated' }
|
331
|
+
end
|
332
|
+
rescue => e
|
333
|
+
Ewelink.logger.error(self.class.name) { e }
|
334
|
+
end
|
335
|
+
sleep(WEB_SOCKET_CHECK_AUTHENTICATION_TIMEOUT)
|
336
|
+
end
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
322
340
|
def start_web_socket_ping_thread(interval)
|
323
341
|
@last_web_socket_pong_at = Time.now
|
324
342
|
@web_socket_ping_interval = interval
|
@@ -338,24 +356,34 @@ module Ewelink
|
|
338
356
|
end
|
339
357
|
|
340
358
|
def web_socket
|
359
|
+
if web_socket_outdated_ping?
|
360
|
+
Ewelink.logger.warn(self.class.name) { 'WebSocket ping is outdated' }
|
361
|
+
reload
|
362
|
+
end
|
363
|
+
|
341
364
|
synchronize(:web_socket) do
|
342
365
|
next @web_socket if @web_socket
|
343
366
|
|
367
|
+
# Initializes caches before opening WebSocket: important in order to
|
368
|
+
# NOT cumulate requests Timeouts from #web_socket_wait_for.
|
369
|
+
api_key
|
370
|
+
web_socket_url
|
371
|
+
|
372
|
+
Ewelink.logger.debug(self.class.name) { "Opening WebSocket to #{web_socket_url.inspect}" }
|
373
|
+
|
344
374
|
@web_socket_thread = Thread.new do
|
345
375
|
EventMachine.run do
|
346
|
-
|
347
|
-
|
348
|
-
@web_socket = Faye::WebSocket::Client.new('wss://as-pconnect3.coolkit.cc:8080/api/ws')
|
376
|
+
@web_socket = Faye::WebSocket::Client.new(web_socket_url)
|
349
377
|
|
350
378
|
@web_socket.on(:close) do |event|
|
351
379
|
Ewelink.logger.debug(self.class.name) { 'WebSocket closed' }
|
352
|
-
|
380
|
+
reload
|
353
381
|
end
|
354
382
|
|
355
383
|
@web_socket.on(:open) do |event|
|
356
384
|
Ewelink.logger.debug(self.class.name) { 'WebSocket opened' }
|
357
385
|
@last_web_socket_pong_at = Time.now
|
358
|
-
|
386
|
+
authenticate_web_socket_api_key
|
359
387
|
end
|
360
388
|
|
361
389
|
@web_socket.on(:message) do |event|
|
@@ -371,11 +399,13 @@ module Ewelink
|
|
371
399
|
json = JSON.parse(message)
|
372
400
|
rescue => e
|
373
401
|
Ewelink.logger.error(self.class.name) { 'WebSocket JSON parse error' }
|
402
|
+
reload
|
374
403
|
next
|
375
404
|
end
|
376
405
|
|
377
406
|
if json.key?('error') && json['error'] != 0
|
378
407
|
Ewelink.logger.error(self.class.name) { "WebSocket message error: #{message.inspect}" }
|
408
|
+
reload
|
379
409
|
next
|
380
410
|
end
|
381
411
|
|
@@ -383,9 +413,10 @@ module Ewelink
|
|
383
413
|
start_web_socket_ping_thread(json['config']['hbInterval'] + 7)
|
384
414
|
end
|
385
415
|
|
386
|
-
if json['apikey'].present? && !@
|
387
|
-
@
|
416
|
+
if json['apikey'].present? && !@web_socket_authenticated && json['apikey'] == api_key
|
417
|
+
@web_socket_authenticated = true
|
388
418
|
Ewelink.logger.debug(self.class.name) { "WebSocket successfully authenticated API key: #{json['apikey'].truncate(16).inspect}" }
|
419
|
+
Thread.new { switches.each { |switch| switch_on?(switch[:uuid]) } } if update_devices_status_on_connect?
|
389
420
|
end
|
390
421
|
|
391
422
|
if json['deviceid'].present? && json['params'].is_a?(Hash) && json['params']['switch'].present?
|
@@ -399,18 +430,14 @@ module Ewelink
|
|
399
430
|
end
|
400
431
|
end
|
401
432
|
|
402
|
-
|
403
|
-
|
404
|
-
sleep(WEB_SOCKET_WAIT_INTERVAL)
|
405
|
-
end
|
433
|
+
web_socket_wait_for(-> { @web_socket.present? }) do
|
434
|
+
@web_socket
|
406
435
|
end
|
407
|
-
|
408
|
-
@web_socket
|
409
436
|
end
|
410
437
|
end
|
411
438
|
|
412
439
|
def web_socket_authenticated?
|
413
|
-
|
440
|
+
@web_socket_authenticated.present?
|
414
441
|
end
|
415
442
|
|
416
443
|
def web_socket_outdated_ping?
|
@@ -440,15 +467,18 @@ module Ewelink
|
|
440
467
|
end
|
441
468
|
end
|
442
469
|
|
443
|
-
def web_socket_wait_for(condition, &block)
|
444
|
-
web_socket
|
445
|
-
|
446
|
-
|
447
|
-
|
470
|
+
def web_socket_wait_for(condition, initialize_web_socket: false, &block)
|
471
|
+
web_socket if initialize_web_socket
|
472
|
+
begin
|
473
|
+
Timeout.timeout(REQUEST_TIMEOUT) do
|
474
|
+
while !condition.call
|
475
|
+
sleep(WEB_SOCKET_WAIT_INTERVAL)
|
476
|
+
end
|
477
|
+
block_given? ? yield : true
|
448
478
|
end
|
449
|
-
|
450
|
-
|
451
|
-
|
479
|
+
rescue => e
|
480
|
+
reload
|
481
|
+
raise Error.new(e)
|
452
482
|
end
|
453
483
|
end
|
454
484
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ewelink
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alexis Toulotte
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-12-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|