ewelink 2.0.0 → 2.2.2

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: 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