ewelink 2.0.0 → 2.2.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 97046b44715a988f6ae92d986c0fad17ab4d5cae6420a125c10f23854cfaadc9
4
- data.tar.gz: b95e52d88f8d57d5bf9f54b67d5a8c0779d26b66faadbc252e13eadb80854292
3
+ metadata.gz: b7b1ea698c178aeed55aa90970659bb230f84e611a1bff87d135e301a81bca0d
4
+ data.tar.gz: 73e86511d022f0481d0156b130ac08f679c5e06a1549ab1c903bb9b795187ee9
5
5
  SHA512:
6
- metadata.gz: f153a280ff50b245434b2039cd3bb88341da6bd7688556a8a7c3fe4558a2b13a6be5c1497f5b06dc381495cb63d8c26d4d3f2625ea5bd4148e4c55a2f28b2faf
7
- data.tar.gz: e3b69fa1607599325ea0f2867fbd0d32fd3abd56a4e7bd8d07107e083d4ebbfaf919bc4af34b28948163d1fb5340a53489aec1839cc59ba238f2917a8db03cc5
6
+ metadata.gz: 141e43b9082542fe3c6a598754b9d0e9e69f6b5612da8df2e56921e1b1df8c96517ac97059d782c7539177bf42889dbfd70b6f94186e99f8d22649071066ecf7
7
+ data.tar.gz: 445ce9af569e5b997eff1ff20fcafec709cc3edf0a60e278262ba66c56341cf389f6d133c4650f8916af995e42f61a132a56ba822455e8a6595003aca6e92a48
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.0.0
1
+ 2.2.2
@@ -16,8 +16,8 @@ Gem::Specification.new do |s|
16
16
  s.required_ruby_version = '>= 2.0.0'
17
17
 
18
18
  s.add_dependency 'activesupport', '>= 6.0.0', '< 7.0.0'
19
+ s.add_dependency 'faye-websocket', '>= 0.11.0', '< 0.12.0'
19
20
  s.add_dependency 'httparty', '>= 0.18.0', '< 0.19.0'
20
- s.add_dependency 'websocket-client-simple', '>= 0.3.0', '< 0.4.0'
21
21
 
22
22
  s.add_development_dependency 'byebug', '>= 11.0.0', '< 12.0.0'
23
23
  s.add_development_dependency 'rake', '>= 12.0.0', '< 13.0.0'
@@ -1,6 +1,8 @@
1
1
  require 'active_support'
2
2
  require 'active_support/core_ext'
3
3
  require 'byebug' if ENV['DEBUGGER']
4
+ require 'eventmachine'
5
+ require 'faye/websocket'
4
6
  require 'httparty'
5
7
  require 'io/console'
6
8
  require 'json'
@@ -9,7 +11,6 @@ require 'openssl'
9
11
  require 'optparse'
10
12
  require 'set'
11
13
  require 'timeout'
12
- require 'websocket-client-simple'
13
14
 
14
15
  module Ewelink
15
16
 
@@ -5,12 +5,14 @@ module Ewelink
5
5
  APP_ID = 'oeVkj2lYFGnJu5XUtWisfW4utiN4u9Mq'
6
6
  APP_SECRET = '6Nz4n0xA8s8qdxQf2GqurZj2Fs55FUvM'
7
7
  DEFAULT_REGION = 'us'
8
+ REQUEST_TIMEOUT = 10.seconds
8
9
  RF_BRIDGE_DEVICE_UIID = 28
9
10
  SWITCH_DEVICES_UIIDS = [1, 5, 6, 24]
10
- TIMEOUT = 10.seconds
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_PING_TOLERANCE_FACTOR = 1.5
15
+ SWITCH_STATUS_CHANGE_CHECK_TIMEOUT = 2.seconds
14
16
  WEB_SOCKET_WAIT_INTERVAL = 0.2.seconds
15
17
 
16
18
  attr_reader :email, :password, :phone_number
@@ -20,15 +22,15 @@ module Ewelink
20
22
  @mutexs = {}
21
23
  @password = password.presence || raise(Error.new(":password must be specified"))
22
24
  @phone_number = phone_number.presence.try(:strip)
23
- @switches_statuses = {}
24
25
  @web_socket_authenticated_api_keys = Set.new
26
+ @web_socket_switches_statuses = {}
25
27
  raise(Error.new(":email or :phone_number must be specified")) if email.blank? && phone_number.blank?
26
28
  end
27
29
 
28
30
  def press_rf_bridge_button!(uuid)
29
31
  synchronize(:press_rf_bridge_button) do
30
32
  button = find_rf_bridge_button!(uuid)
31
- web_socket_wait_for(-> { web_socket_authenticated? }) do
33
+ web_socket_wait_for(-> { web_socket_authenticated? }, initialize_web_socket: true) do
32
34
  params = {
33
35
  'action' => 'update',
34
36
  'apikey' => button[:api_key],
@@ -41,6 +43,7 @@ module Ewelink
41
43
  'ts' => 0,
42
44
  'userAgent' => 'app',
43
45
  }
46
+ Ewelink.logger.debug(self.class.name) { "Pressing RF bridge button #{button[:uuid].inspect}" }
44
47
  send_to_web_socket(JSON.generate(params))
45
48
  true
46
49
  end
@@ -48,10 +51,42 @@ module Ewelink
48
51
  end
49
52
 
50
53
  def reload
51
- Ewelink.logger.debug(self.class.name) { 'Reloading API (authentication token, devices, region,...)' }
52
- dispose_web_socket
53
- @switches_statuses.clear
54
- [:@api_keys, :@authentication_token, :@devices, :@rf_bridge_buttons, :@region, :@switches, :@web_socket, :@web_socket_url].each do |variable|
54
+ Ewelink.logger.debug(self.class.name) { 'Reloading API (authentication token, devices, region, connections,...)' }
55
+
56
+ @web_socket_authenticated_api_keys.clear
57
+ @web_socket_switches_statuses.clear
58
+
59
+ [@web_socket_ping_thread, @web_socket_thread].each do |thread|
60
+ next unless thread
61
+ if Thread.current == thread
62
+ thread[:stop] = true
63
+ else
64
+ thread.kill
65
+ end
66
+ end
67
+
68
+ if @web_socket.present?
69
+ begin
70
+ @web_socket.close if @web_socket.open?
71
+ rescue
72
+ # Ignoring close errors
73
+ end
74
+ end
75
+
76
+ [
77
+ :@api_keys,
78
+ :@authentication_token,
79
+ :@devices,
80
+ :@last_web_socket_pong_at,
81
+ :@region,
82
+ :@rf_bridge_buttons,
83
+ :@switches,
84
+ :@web_socket_ping_interval,
85
+ :@web_socket_ping_thread,
86
+ :@web_socket_thread,
87
+ :@web_socket_url,
88
+ :@web_socket,
89
+ ].each do |variable|
55
90
  remove_instance_variable(variable) if instance_variable_defined?(variable)
56
91
  end
57
92
  self
@@ -93,21 +128,22 @@ module Ewelink
93
128
 
94
129
  def switch_on?(uuid)
95
130
  switch = find_switch!(uuid)
96
- if @switches_statuses[switch[:uuid]].nil?
97
- params = {
98
- 'action' => 'query',
99
- 'apikey' => switch[:api_key],
100
- 'deviceid' => switch[:device_id],
101
- 'sequence' => web_socket_sequence,
102
- 'ts' => 0,
103
- 'userAgent' => 'app',
104
- }
105
- web_socket_wait_for(-> { web_socket_authenticated? }) do
131
+ if @web_socket_switches_statuses[switch[:uuid]].nil?
132
+ web_socket_wait_for(-> { web_socket_authenticated? }, initialize_web_socket: true) do
133
+ Ewelink.logger.debug(self.class.name) { "Checking switch #{switch[:uuid].inspect} status" }
134
+ params = {
135
+ 'action' => 'query',
136
+ 'apikey' => switch[:api_key],
137
+ 'deviceid' => switch[:device_id],
138
+ 'sequence' => web_socket_sequence,
139
+ 'ts' => 0,
140
+ 'userAgent' => 'app',
141
+ }
106
142
  send_to_web_socket(JSON.generate(params))
107
143
  end
108
144
  end
109
- web_socket_wait_for(-> { !@switches_statuses[switch[:uuid]].nil? }) do
110
- @switches_statuses[switch[:uuid]] == 'on'
145
+ web_socket_wait_for(-> { !@web_socket_switches_statuses[switch[:uuid]].nil? }, initialize_web_socket: true) do
146
+ @web_socket_switches_statuses[switch[:uuid]] == 'on'
111
147
  end
112
148
  end
113
149
 
@@ -138,8 +174,8 @@ module Ewelink
138
174
  on = false
139
175
  end
140
176
  switch = find_switch!(uuid)
141
- @switches_statuses[switch[:uuid]] = nil
142
- web_socket_wait_for(-> { web_socket_authenticated? }) do
177
+ @web_socket_switches_statuses[switch[:uuid]] = nil
178
+ web_socket_wait_for(-> { web_socket_authenticated? }, initialize_web_socket: true) do
143
179
  params = {
144
180
  'action' => 'update',
145
181
  'apikey' => switch[:api_key],
@@ -151,9 +187,12 @@ module Ewelink
151
187
  'ts' => 0,
152
188
  'userAgent' => 'app',
153
189
  }
190
+ Ewelink.logger.debug(self.class.name) { "Turning switch #{switch[:uuid].inspect} #{on ? 'on' : 'off'}" }
154
191
  send_to_web_socket(JSON.generate(params))
155
- true
156
192
  end
193
+ sleep(SWITCH_STATUS_CHANGE_CHECK_TIMEOUT)
194
+ switch_on?(switch[:uuid]) # Waiting for switch status update
195
+ true
157
196
  end
158
197
 
159
198
  private
@@ -164,6 +203,24 @@ module Ewelink
164
203
  end
165
204
  end
166
205
 
206
+ def authenticate_web_socket_api_keys
207
+ api_keys.each do |api_key|
208
+ params = {
209
+ 'action' => 'userOnline',
210
+ 'apikey' => api_key,
211
+ 'appid' => APP_ID,
212
+ 'at' => authentication_token,
213
+ 'nonce' => nonce,
214
+ 'sequence' => web_socket_sequence,
215
+ 'ts' => Time.now.to_i,
216
+ 'userAgent' => 'app',
217
+ 'version' => VERSION,
218
+ }
219
+ Ewelink.logger.debug(self.class.name) { "Authenticating WebSocket API key: #{api_key.truncate(16).inspect}" }
220
+ send_to_web_socket(JSON.generate(params))
221
+ end
222
+ end
223
+
167
224
  def authentication_headers
168
225
  { 'Authorization' => "Bearer #{authentication_token}" }
169
226
  end
@@ -208,18 +265,6 @@ module Ewelink
208
265
  end
209
266
  end
210
267
 
211
- def dispose_web_socket
212
- @web_socket_authenticated_api_keys = Set.new
213
- if instance_variable_defined?(:@web_socket)
214
- begin
215
- @web_socket.close if @web_socket.open?
216
- rescue
217
- # Ignoring close errors
218
- end
219
- remove_instance_variable(:@web_socket) if instance_variable_defined?(:@web_socket)
220
- end
221
- end
222
-
223
268
  def find_rf_bridge_button!(uuid)
224
269
  rf_bridge_buttons.find { |button| button[:uuid] == uuid } || raise(Error.new("No such RF bridge button with UUID: #{uuid.inspect}"))
225
270
  end
@@ -241,7 +286,7 @@ module Ewelink
241
286
  method = method.to_s.upcase
242
287
  headers = (options[:headers] || {}).reverse_merge('Content-Type' => 'application/json')
243
288
  Ewelink.logger.debug(self.class.name) { "#{method} #{url}" }
244
- response = HTTParty.send(method.downcase, url, options.merge(headers: headers).reverse_merge(timeout: TIMEOUT))
289
+ response = HTTParty.send(method.downcase, url, options.merge(headers: headers).reverse_merge(timeout: REQUEST_TIMEOUT))
245
290
  raise(Error.new("#{method} #{url}: #{response.code}")) unless response.success?
246
291
  if response['error'] == 301 && response['region'].present?
247
292
  @region = response['region']
@@ -255,11 +300,29 @@ module Ewelink
255
300
  raise Error.new(e)
256
301
  end
257
302
 
258
- def send_to_web_socket(data)
259
- web_socket.send(data)
260
- rescue
261
- dispose_web_socket
262
- raise
303
+ def send_to_web_socket(message)
304
+ if web_socket_outdated_ping?
305
+ Ewelink.logger.warn(self.class.name) { 'WebSocket ping is outdated' }
306
+ reload
307
+ end
308
+ web_socket.send(message)
309
+ rescue => e
310
+ reload
311
+ raise Error.new(e)
312
+ end
313
+
314
+ def start_web_socket_ping_thread(interval)
315
+ @last_web_socket_pong_at = Time.now
316
+ @web_socket_ping_interval = interval
317
+ Ewelink.logger.debug(self.class.name) { "Creating thread for WebSocket ping every #{@web_socket_ping_interval} seconds" }
318
+ @web_socket_ping_thread = Thread.new do
319
+ loop do
320
+ break if Thread.current[:stop]
321
+ sleep(@web_socket_ping_interval)
322
+ Ewelink.logger.debug(self.class.name) { 'Sending WebSocket ping' }
323
+ send_to_web_socket('ping')
324
+ end
325
+ end
263
326
  end
264
327
 
265
328
  def synchronize(name, &block)
@@ -268,69 +331,71 @@ module Ewelink
268
331
 
269
332
  def web_socket
270
333
  synchronize(:web_socket) do
271
- @web_socket ||= begin
272
- api = self
334
+ next @web_socket if @web_socket
273
335
 
274
- WebSocket::Client::Simple.connect(web_socket_url) do |web_socket|
275
- Ewelink.logger.debug(self.class.name) { "Opening WebSocket to #{web_socket_url}" }
336
+ @web_socket_thread = Thread.new do
337
+ EventMachine.run do
338
+ Ewelink.logger.debug(self.class.name) { "Opening WebSocket to #{web_socket_url.inspect}" }
276
339
 
277
- web_socket.on(:close) do
278
- api.instance_eval do
279
- Ewelink.logger.debug(self.class.name) { 'WebSocket closed' }
280
- dispose_web_socket
281
- end
340
+ @web_socket = Faye::WebSocket::Client.new('wss://as-pconnect3.coolkit.cc:8080/api/ws')
341
+
342
+ @web_socket.on(:close) do |event|
343
+ Ewelink.logger.debug(self.class.name) { 'WebSocket closed' }
344
+ reload
282
345
  end
283
346
 
284
- web_socket.on(:error) do |e|
285
- api.instance_eval do
286
- Ewelink.logger.warn(self.class.name) { "WebSocket error: #{e}" }
287
- dispose_web_socket
288
- end
347
+ @web_socket.on(:open) do |event|
348
+ Ewelink.logger.debug(self.class.name) { 'WebSocket opened' }
349
+ @last_web_socket_pong_at = Time.now
350
+ authenticate_web_socket_api_keys
289
351
  end
290
352
 
291
- web_socket.on(:message) do |message|
292
- api.instance_eval do
293
- response = JSON.parse(message.data)
353
+ @web_socket.on(:message) do |event|
354
+ message = event.data
294
355
 
295
- if response.key?('error') && response['error'] != 0
296
- Ewelink.logger.error(self.class.name) { "WebSocket message error: #{message.data}" }
297
- next
298
- end
356
+ if message == 'pong'
357
+ Ewelink.logger.debug(self.class.name) { "Received WebSocket #{message.inspect} message" }
358
+ @last_web_socket_pong_at = Time.now
359
+ next
360
+ end
299
361
 
300
- if response['apikey'].present? && !@web_socket_authenticated_api_keys.include?(response['apikey'])
301
- @web_socket_authenticated_api_keys << response['apikey']
302
- Ewelink.logger.debug(self.class.name) { "WebSocket successfully authenticated API key: #{response['apikey'].inspect}" }
303
- end
362
+ begin
363
+ json = JSON.parse(message)
364
+ rescue => e
365
+ Ewelink.logger.error(self.class.name) { 'WebSocket JSON parse error' }
366
+ reload
367
+ next
368
+ end
304
369
 
305
- if response['deviceid'].present? && response['params'].is_a?(Hash) && response['params']['switch'].present?
306
- switch = switches.find { |switch| switch[:device_id] == response['deviceid'] }
307
- @switches_statuses[switch[:uuid]] = response['params']['switch'] if switch.present?
308
- end
370
+ if json.key?('error') && json['error'] != 0
371
+ Ewelink.logger.error(self.class.name) { "WebSocket message error: #{message.inspect}" }
372
+ reload
373
+ next
374
+ end
375
+
376
+ if !@web_socket_ping_thread && json.key?('config') && json['config']['hb'] == 1 && json['config']['hbInterval'].present?
377
+ start_web_socket_ping_thread(json['config']['hbInterval'] + 7)
309
378
  end
310
- end
311
379
 
312
- web_socket.on(:open) do
313
- api.instance_eval do
314
- Ewelink.logger.debug(self.class.name) { 'WebSocket opened' }
315
- api_keys.each do |api_key|
316
- params = {
317
- 'action' => 'userOnline',
318
- 'apikey' => api_key,
319
- 'appid' => APP_ID,
320
- 'at' => authentication_token,
321
- 'nonce' => nonce,
322
- 'sequence' => web_socket_sequence,
323
- 'ts' => Time.now.to_i,
324
- 'userAgent' => 'app',
325
- 'version' => VERSION,
326
- }
327
- Ewelink.logger.debug(self.class.name) { "Authenticating WebSocket API key: #{api_key.inspect}" }
328
- send_to_web_socket(JSON.generate(params))
380
+ if json['apikey'].present? && !@web_socket_authenticated_api_keys.include?(json['apikey'])
381
+ @web_socket_authenticated_api_keys << json['apikey']
382
+ Ewelink.logger.debug(self.class.name) { "WebSocket successfully authenticated API key: #{json['apikey'].truncate(16).inspect}" }
383
+ end
384
+
385
+ if json['deviceid'].present? && json['params'].is_a?(Hash) && json['params']['switch'].present?
386
+ switch = switches.find { |switch| switch[:device_id] == json['deviceid'] }
387
+ if switch.present?
388
+ @web_socket_switches_statuses[switch[:uuid]] = json['params']['switch']
389
+ Ewelink.logger.debug(self.class.name) { "Switch #{switch[:uuid].inspect} is #{@web_socket_switches_statuses[switch[:uuid]]}" }
329
390
  end
330
391
  end
331
392
  end
332
393
  end
333
394
  end
395
+
396
+ web_socket_wait_for(-> { @web_socket.present? }) do
397
+ @web_socket
398
+ end
334
399
  end
335
400
  end
336
401
 
@@ -338,6 +403,10 @@ module Ewelink
338
403
  api_keys == @web_socket_authenticated_api_keys
339
404
  end
340
405
 
406
+ def web_socket_outdated_ping?
407
+ @last_web_socket_pong_at.present? && @web_socket_ping_interval.present? && @last_web_socket_pong_at < (@web_socket_ping_interval * WEB_SOCKET_PING_TOLERANCE_FACTOR).seconds.ago
408
+ end
409
+
341
410
  def web_socket_sequence
342
411
  (Time.now.to_f * 1000).round.to_s
343
412
  end
@@ -361,13 +430,18 @@ module Ewelink
361
430
  end
362
431
  end
363
432
 
364
- def web_socket_wait_for(condition, &block)
365
- web_socket # Initializes WebSocket
366
- Timeout.timeout(TIMEOUT) do
367
- loop do
368
- return yield if condition.call
369
- sleep(WEB_SOCKET_WAIT_INTERVAL)
433
+ def web_socket_wait_for(condition, initialize_web_socket: false, &block)
434
+ web_socket if initialize_web_socket
435
+ begin
436
+ Timeout.timeout(REQUEST_TIMEOUT) do
437
+ while !condition.call
438
+ sleep(WEB_SOCKET_WAIT_INTERVAL)
439
+ end
440
+ block_given? ? yield : true
370
441
  end
442
+ rescue => e
443
+ reload
444
+ raise Error.new(e)
371
445
  end
372
446
  end
373
447
 
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.0.0
4
+ version: 2.2.2
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-02 00:00:00.000000000 Z
11
+ date: 2020-09-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -31,45 +31,45 @@ dependencies:
31
31
  - !ruby/object:Gem::Version
32
32
  version: 7.0.0
33
33
  - !ruby/object:Gem::Dependency
34
- name: httparty
34
+ name: faye-websocket
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: 0.18.0
39
+ version: 0.11.0
40
40
  - - "<"
41
41
  - !ruby/object:Gem::Version
42
- version: 0.19.0
42
+ version: 0.12.0
43
43
  type: :runtime
44
44
  prerelease: false
45
45
  version_requirements: !ruby/object:Gem::Requirement
46
46
  requirements:
47
47
  - - ">="
48
48
  - !ruby/object:Gem::Version
49
- version: 0.18.0
49
+ version: 0.11.0
50
50
  - - "<"
51
51
  - !ruby/object:Gem::Version
52
- version: 0.19.0
52
+ version: 0.12.0
53
53
  - !ruby/object:Gem::Dependency
54
- name: websocket-client-simple
54
+ name: httparty
55
55
  requirement: !ruby/object:Gem::Requirement
56
56
  requirements:
57
57
  - - ">="
58
58
  - !ruby/object:Gem::Version
59
- version: 0.3.0
59
+ version: 0.18.0
60
60
  - - "<"
61
61
  - !ruby/object:Gem::Version
62
- version: 0.4.0
62
+ version: 0.19.0
63
63
  type: :runtime
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
66
66
  requirements:
67
67
  - - ">="
68
68
  - !ruby/object:Gem::Version
69
- version: 0.3.0
69
+ version: 0.18.0
70
70
  - - "<"
71
71
  - !ruby/object:Gem::Version
72
- version: 0.4.0
72
+ version: 0.19.0
73
73
  - !ruby/object:Gem::Dependency
74
74
  name: byebug
75
75
  requirement: !ruby/object:Gem::Requirement