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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.mdown +5 -0
  3. data/VERSION +1 -1
  4. data/lib/ewelink/api.rb +127 -97
  5. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 957d00b0d8171519a48b1cf828cbd78c21dbde34b93f2c1930273ad460d5a770
4
- data.tar.gz: 0c636a47ac05f8428d9e099d952a0b89a512f8e199a3a85a72624c10a76590b3
3
+ metadata.gz: c9b83156bba48622088845d22bd8fc3b559d6e8732acf4664ba35886c6c19d35
4
+ data.tar.gz: d69303e93b04b56fd713fc27220d15c4ab66ca767190a289d6440932dc468532
5
5
  SHA512:
6
- metadata.gz: c20dfab19c133d80913716e7be2b20e47958e841e61d7b401f28b73e642358d80ce698d5fd22410ede0bb783114c0e91dfc7a1b4760ebab3738aaba3081275af
7
- data.tar.gz: 07664117dfacf9f32fa7e0965199bed938da619b75b313207d702d5ad83ebc2ceacbb76b266e40f3e995114542a110061ad271630a3b007fc2e4f1cbb2bccc58
6
+ metadata.gz: fa3fd8b8a36ad7e4570db2941ebdffc1d134106ea40ce93866eaebe7b81a4cc0a35920f5366a48c0a9141476101cc03be42adf244b15bcf98eeec64b0e77a312
7
+ data.tar.gz: d98f59eaec9c582dd0627a52a392c45e98e351676b69d36f081df4ec819153bd32e340ced77440152c674d58013f413d8e42a5e86a50f65d57efce5782f26943
@@ -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
- 2.2.1
1
+ 3.2.0
@@ -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
- @web_socket_authenticated_api_keys = Set.new
26
+ @update_devices_status_on_connect = update_devices_status_on_connect.present?
27
+ @web_socket_authenticated = false
26
28
  @web_socket_switches_statuses = {}
27
- raise(Error.new(":email or :phone_number must be specified")) if email.blank? && phone_number.blank?
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
- dispose_web_socket
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
- :@api_keys,
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 api_keys
175
- synchronize(:api_keys) do
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 authenticate_web_socket_api_keys
181
- api_keys.each do |api_key|
182
- params = {
183
- 'action' => 'userOnline',
184
- 'apikey' => api_key,
185
- 'appid' => APP_ID,
186
- 'at' => authentication_token,
187
- 'nonce' => nonce,
188
- 'sequence' => web_socket_sequence,
189
- 'ts' => Time.now.to_i,
190
- 'userAgent' => 'app',
191
- 'version' => VERSION,
192
- }
193
- Ewelink.logger.debug(self.class.name) { "Authenticating WebSocket API key: #{api_key.truncate(16).inspect}" }
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 authentication_token
203
- synchronize(:authentication_token) do
204
- @authentication_token ||= begin
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
- response['at'].tap { Ewelink.logger.debug(self.class.name) { 'Authentication token found' } }
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(:@authentication_token) if instance_variable_defined?(:@authentication_token) && [401, 403].include?(response['error'])
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
- dispose_web_socket
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
- Ewelink.logger.debug(self.class.name) { "Opening WebSocket to #{web_socket_url.inspect}" }
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
- dispose_web_socket
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
- authenticate_web_socket_api_keys
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? && !@web_socket_authenticated_api_keys.include?(json['apikey'])
387
- @web_socket_authenticated_api_keys << json['apikey']
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
- Timeout.timeout(REQUEST_TIMEOUT) do
403
- while @web_socket.blank?
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
- api_keys == @web_socket_authenticated_api_keys
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 # Initializes WebSocket
445
- Timeout.timeout(REQUEST_TIMEOUT) do
446
- while !condition.call
447
- sleep(WEB_SOCKET_WAIT_INTERVAL)
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
- block_given? ? yield : true
450
- rescue
451
- dispose_web_socket
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: 2.2.1
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-09-03 00:00:00.000000000 Z
11
+ date: 2020-12-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport