ewelink 2.3.0 → 3.0.0

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: d17e40d48a819572e83369500edbe6295a991626c3333ce4ff0765876f25ed51
4
- data.tar.gz: 9caca646a1355592a276e5e4595ab486e407ecaecb105a9e7134028ecab4cb42
3
+ metadata.gz: 558b7f50525eabfc0e0ac3cd32fd5a0ff5c0d4b6cc7658024dba4c6c0336f96a
4
+ data.tar.gz: cda09a44af6247436a22af9443a1dd3c078e8b86ab228f3ce12761baca606a7b
5
5
  SHA512:
6
- metadata.gz: 6e8a2717428e2b57c7a30a7d4e9a1658032b1155a2f9bd2ba9b8b0cb2309748d248f24d66babfbe2212af9d8096e6be18ebdc3c60d799654571526edff1204d5
7
- data.tar.gz: 9b9e517a3c8629d6a69b8195cab906d12f8f4d4231ab22311083deb75e5db97b02ec9c13a8509710017bb17cfcf1750c46919f52b393b73a0d453c423f120153
6
+ metadata.gz: 9607d6170f6e3f0cb00d599fa7a8258b25367423e6a87b2238079d336d6ee2a58389f78ffdcbc3eaf9726c1685ff3cb8b95fb0387d655ae6b81614d50d990c01
7
+ data.tar.gz: 4ad312d700e40c50f20cd47f5d66dec54937633e1162e21b3d131556752dc937e41c70d128e210d344d19a8bd0cb0fb4de1606f081cba50ad1189c84655440b6
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.3.0
1
+ 3.0.0
@@ -16,7 +16,6 @@ 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'
20
19
  s.add_dependency 'httparty', '>= 0.18.0', '< 0.19.0'
21
20
 
22
21
  s.add_development_dependency 'byebug', '>= 11.0.0', '< 12.0.0'
@@ -1,16 +1,12 @@
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'
6
4
  require 'httparty'
7
5
  require 'io/console'
8
6
  require 'json'
9
7
  require 'logger'
10
8
  require 'openssl'
11
9
  require 'optparse'
12
- require 'set'
13
- require 'timeout'
14
10
 
15
11
  module Ewelink
16
12
 
@@ -5,16 +5,12 @@ module Ewelink
5
5
  APP_ID = 'oeVkj2lYFGnJu5XUtWisfW4utiN4u9Mq'
6
6
  APP_SECRET = '6Nz4n0xA8s8qdxQf2GqurZj2Fs55FUvM'
7
7
  DEFAULT_REGION = 'us'
8
- REQUEST_TIMEOUT = 10.seconds
9
8
  RF_BRIDGE_DEVICE_UIID = 28
10
9
  SWITCH_DEVICES_UIIDS = [1, 5, 6, 24]
10
+ TIMEOUT = 10
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
15
- WEB_SOCKET_PING_TOLERANCE_FACTOR = 1.5
16
- SWITCH_STATUS_CHANGE_CHECK_TIMEOUT = 2.seconds
17
- WEB_SOCKET_WAIT_INTERVAL = 0.2.seconds
18
14
 
19
15
  attr_reader :email, :password, :phone_number
20
16
 
@@ -23,74 +19,31 @@ module Ewelink
23
19
  @mutexs = {}
24
20
  @password = password.presence || raise(Error.new(":password must be specified"))
25
21
  @phone_number = phone_number.presence.try(:strip)
26
- @web_socket_authenticated_api_keys = Set.new
27
- @web_socket_switches_statuses = {}
28
-
29
- raise(Error.new(':email or :phone_number must be specified')) if email.blank? && phone_number.blank?
30
-
31
- start_web_socket_authentication_check_thread
22
+ raise(Error.new(":email or :phone_number must be specified")) if email.blank? && phone_number.blank?
32
23
  end
33
24
 
34
25
  def press_rf_bridge_button!(uuid)
35
26
  synchronize(:press_rf_bridge_button) do
36
27
  button = find_rf_bridge_button!(uuid)
37
- web_socket_wait_for(-> { web_socket_authenticated? }, initialize_web_socket: true) do
38
- params = {
39
- 'action' => 'update',
40
- 'apikey' => button[:api_key],
41
- 'deviceid' => button[:device_id],
42
- 'params' => {
43
- 'cmd' => 'transmit',
44
- 'rfChl' => button[:channel],
45
- },
46
- 'sequence' => web_socket_sequence,
47
- 'ts' => 0,
48
- 'userAgent' => 'app',
49
- }
50
- Ewelink.logger.debug(self.class.name) { "Pressing RF bridge button #{button[:uuid].inspect}" }
51
- send_to_web_socket(JSON.generate(params))
52
- true
53
- end
28
+ params = {
29
+ 'appid' => APP_ID,
30
+ 'deviceid' => button[:device_id],
31
+ 'nonce' => nonce,
32
+ 'params' => {
33
+ 'cmd' => 'transmit',
34
+ 'rfChl' => button[:channel],
35
+ },
36
+ 'ts' => Time.now.to_i,
37
+ 'version' => VERSION,
38
+ }
39
+ rest_request(:post, '/api/user/device/status', body: JSON.generate(params), headers: authentication_headers)
40
+ true
54
41
  end
55
42
  end
56
43
 
57
44
  def reload
58
- Ewelink.logger.debug(self.class.name) { 'Reloading API (authentication token, devices, region, connections,...)' }
59
-
60
- @web_socket_authenticated_api_keys.clear
61
- @web_socket_switches_statuses.clear
62
-
63
- [@web_socket_ping_thread, @web_socket_thread].each do |thread|
64
- next unless thread
65
- if Thread.current == thread
66
- thread[:stop] = true
67
- else
68
- thread.kill
69
- end
70
- end
71
-
72
- if @web_socket.present?
73
- begin
74
- @web_socket.close if @web_socket.open?
75
- rescue
76
- # Ignoring close errors
77
- end
78
- end
79
-
80
- [
81
- :@api_keys,
82
- :@authentication_token,
83
- :@devices,
84
- :@last_web_socket_pong_at,
85
- :@region,
86
- :@rf_bridge_buttons,
87
- :@switches,
88
- :@web_socket_ping_interval,
89
- :@web_socket_ping_thread,
90
- :@web_socket_thread,
91
- :@web_socket_url,
92
- :@web_socket,
93
- ].each do |variable|
45
+ Ewelink.logger.debug(self.class.name) { 'Reloading API (authentication token, devices & region cache)' }
46
+ [:@authentication_token, :@devices, :@rf_bridge_buttons, :@region, :@switches].each do |variable|
94
47
  remove_instance_variable(variable) if instance_variable_defined?(variable)
95
48
  end
96
49
  self
@@ -103,12 +56,10 @@ module Ewelink
103
56
  Ewelink.logger.debug(self.class.name) { "Found #{devices.size} RF 433MHz bridge device(s)" }
104
57
  end
105
58
  rf_bridge_devices.each do |device|
106
- api_key = device['apikey'].presence || next
107
59
  device_id = device['deviceid'].presence || next
108
60
  device_name = device['name'].presence || next
109
61
  buttons = device['params']['rfList'].each do |rf|
110
62
  button = {
111
- api_key: api_key,
112
63
  channel: rf['rfChl'],
113
64
  device_id: device_id,
114
65
  device_name: device_name,
@@ -132,23 +83,15 @@ module Ewelink
132
83
 
133
84
  def switch_on?(uuid)
134
85
  switch = find_switch!(uuid)
135
- if @web_socket_switches_statuses[switch[:uuid]].nil?
136
- web_socket_wait_for(-> { web_socket_authenticated? }, initialize_web_socket: true) do
137
- Ewelink.logger.debug(self.class.name) { "Checking switch #{switch[:uuid].inspect} status" }
138
- params = {
139
- 'action' => 'query',
140
- 'apikey' => switch[:api_key],
141
- 'deviceid' => switch[:device_id],
142
- 'sequence' => web_socket_sequence,
143
- 'ts' => 0,
144
- 'userAgent' => 'app',
145
- }
146
- send_to_web_socket(JSON.generate(params))
147
- end
148
- end
149
- web_socket_wait_for(-> { !@web_socket_switches_statuses[switch[:uuid]].nil? }, initialize_web_socket: true) do
150
- @web_socket_switches_statuses[switch[:uuid]] == 'on'
151
- end
86
+ params = {
87
+ 'appid' => APP_ID,
88
+ 'deviceid' => switch[:device_id],
89
+ 'nonce' => nonce,
90
+ 'ts' => Time.now.to_i,
91
+ 'version' => VERSION,
92
+ }
93
+ response = rest_request(:get, '/api/user/device/status', headers: authentication_headers, query: params)
94
+ response['params']['switch'] == 'on'
152
95
  end
153
96
 
154
97
  def switches
@@ -156,11 +99,9 @@ module Ewelink
156
99
  @switches ||= [].tap do |switches|
157
100
  switch_devices = devices.select { |device| SWITCH_DEVICES_UIIDS.include?(device['uiid']) }
158
101
  switch_devices.each do |device|
159
- api_key = device['apikey'].presence || next
160
102
  device_id = device['deviceid'].presence || next
161
103
  name = device['name'].presence || next
162
104
  switch = {
163
- api_key: api_key,
164
105
  device_id: device_id,
165
106
  name: name,
166
107
  }
@@ -178,53 +119,22 @@ module Ewelink
178
119
  on = false
179
120
  end
180
121
  switch = find_switch!(uuid)
181
- @web_socket_switches_statuses[switch[:uuid]] = nil
182
- web_socket_wait_for(-> { web_socket_authenticated? }, initialize_web_socket: true) do
183
- params = {
184
- 'action' => 'update',
185
- 'apikey' => switch[:api_key],
186
- 'deviceid' => switch[:device_id],
187
- 'params' => {
188
- 'switch' => on ? 'on' : 'off',
189
- },
190
- 'sequence' => web_socket_sequence,
191
- 'ts' => 0,
192
- 'userAgent' => 'app',
193
- }
194
- Ewelink.logger.debug(self.class.name) { "Turning switch #{switch[:uuid].inspect} #{on ? 'on' : 'off'}" }
195
- send_to_web_socket(JSON.generate(params))
196
- end
197
- sleep(SWITCH_STATUS_CHANGE_CHECK_TIMEOUT)
198
- switch_on?(switch[:uuid]) # Waiting for switch status update
122
+ params = {
123
+ 'appid' => APP_ID,
124
+ 'deviceid' => switch[:device_id],
125
+ 'nonce' => nonce,
126
+ 'params' => {
127
+ 'switch' => on ? 'on' : 'off',
128
+ },
129
+ 'ts' => Time.now.to_i,
130
+ 'version' => VERSION,
131
+ }
132
+ rest_request(:post, '/api/user/device/status', body: JSON.generate(params), headers: authentication_headers)
199
133
  true
200
134
  end
201
135
 
202
136
  private
203
137
 
204
- def api_keys
205
- synchronize(:api_keys) do
206
- @api_keys ||= Set.new(devices.map { |device| device['apikey'] })
207
- end
208
- end
209
-
210
- def authenticate_web_socket_api_keys
211
- api_keys.each do |api_key|
212
- params = {
213
- 'action' => 'userOnline',
214
- 'apikey' => api_key,
215
- 'appid' => APP_ID,
216
- 'at' => authentication_token,
217
- 'nonce' => nonce,
218
- 'sequence' => web_socket_sequence,
219
- 'ts' => Time.now.to_i,
220
- 'userAgent' => 'app',
221
- 'version' => VERSION,
222
- }
223
- Ewelink.logger.debug(self.class.name) { "Authenticating WebSocket API key: #{api_key.truncate(16).inspect}" }
224
- send_to_web_socket(JSON.generate(params))
225
- end
226
- end
227
-
228
138
  def authentication_headers
229
139
  { 'Authorization' => "Bearer #{authentication_token}" }
230
140
  end
@@ -290,7 +200,7 @@ module Ewelink
290
200
  method = method.to_s.upcase
291
201
  headers = (options[:headers] || {}).reverse_merge('Content-Type' => 'application/json')
292
202
  Ewelink.logger.debug(self.class.name) { "#{method} #{url}" }
293
- response = HTTParty.send(method.downcase, url, options.merge(headers: headers).reverse_merge(timeout: REQUEST_TIMEOUT))
203
+ response = HTTParty.send(method.downcase, url, options.merge(headers: headers).reverse_merge(timeout: TIMEOUT))
294
204
  raise(Error.new("#{method} #{url}: #{response.code}")) unless response.success?
295
205
  if response['error'] == 301 && response['region'].present?
296
206
  @region = response['region']
@@ -299,180 +209,15 @@ module Ewelink
299
209
  end
300
210
  remove_instance_variable(:@authentication_token) if instance_variable_defined?(:@authentication_token) && [401, 403].include?(response['error'])
301
211
  raise(Error.new("#{method} #{url}: #{response['error']} #{response['msg']}".strip)) if response['error'].present? && response['error'] != 0
302
- response.to_h
212
+ response
303
213
  rescue Errno::ECONNREFUSED, OpenSSL::OpenSSLError, SocketError, Timeout::Error => e
304
214
  raise Error.new(e)
305
215
  end
306
216
 
307
- def send_to_web_socket(message)
308
- web_socket.send(message)
309
- rescue => e
310
- reload
311
- raise Error.new(e)
312
- end
313
-
314
- def start_web_socket_authentication_check_thread
315
- raise Error.new('WebSocket authentication check must only be started once') if @web_socket_authentication_check_thread.present?
316
-
317
- @web_socket_authentication_check_thread = Thread.new do
318
- loop do
319
- Ewelink.logger.debug(self.class.name) { 'Checking if WebSocket is authenticated' }
320
- begin
321
- web_socket_wait_for(-> { web_socket_authenticated? }, initialize_web_socket: true) do
322
- Ewelink.logger.debug(self.class.name) { 'WebSocket is authenticated' }
323
- end
324
- rescue => e
325
- Ewelink.logger.error(self.class.name) { e }
326
- end
327
- sleep(WEB_SOCKET_CHECK_AUTHENTICATION_TIMEOUT)
328
- end
329
- end
330
- end
331
-
332
- def start_web_socket_ping_thread(interval)
333
- @last_web_socket_pong_at = Time.now
334
- @web_socket_ping_interval = interval
335
- Ewelink.logger.debug(self.class.name) { "Creating thread for WebSocket ping every #{@web_socket_ping_interval} seconds" }
336
- @web_socket_ping_thread = Thread.new do
337
- loop do
338
- break if Thread.current[:stop]
339
- sleep(@web_socket_ping_interval)
340
- Ewelink.logger.debug(self.class.name) { 'Sending WebSocket ping' }
341
- send_to_web_socket('ping')
342
- end
343
- end
344
- end
345
-
346
217
  def synchronize(name, &block)
347
218
  (@mutexs[name] ||= Mutex.new).synchronize(&block)
348
219
  end
349
220
 
350
- def web_socket
351
- if web_socket_outdated_ping?
352
- Ewelink.logger.warn(self.class.name) { 'WebSocket ping is outdated' }
353
- reload
354
- end
355
-
356
- synchronize(:web_socket) do
357
- next @web_socket if @web_socket
358
-
359
- # Initializes caches before opening WebSocket: important in order to
360
- # NOT cumulate requests Timeouts from #web_socket_wait_for.
361
- api_keys
362
- web_socket_url
363
-
364
- Ewelink.logger.debug(self.class.name) { "Opening WebSocket to #{web_socket_url.inspect}" }
365
-
366
- @web_socket_thread = Thread.new do
367
- EventMachine.run do
368
- @web_socket = Faye::WebSocket::Client.new(web_socket_url)
369
-
370
- @web_socket.on(:close) do |event|
371
- Ewelink.logger.debug(self.class.name) { 'WebSocket closed' }
372
- reload
373
- end
374
-
375
- @web_socket.on(:open) do |event|
376
- Ewelink.logger.debug(self.class.name) { 'WebSocket opened' }
377
- @last_web_socket_pong_at = Time.now
378
- authenticate_web_socket_api_keys
379
- end
380
-
381
- @web_socket.on(:message) do |event|
382
- message = event.data
383
-
384
- if message == 'pong'
385
- Ewelink.logger.debug(self.class.name) { "Received WebSocket #{message.inspect} message" }
386
- @last_web_socket_pong_at = Time.now
387
- next
388
- end
389
-
390
- begin
391
- json = JSON.parse(message)
392
- rescue => e
393
- Ewelink.logger.error(self.class.name) { 'WebSocket JSON parse error' }
394
- reload
395
- next
396
- end
397
-
398
- if json.key?('error') && json['error'] != 0
399
- Ewelink.logger.error(self.class.name) { "WebSocket message error: #{message.inspect}" }
400
- reload
401
- next
402
- end
403
-
404
- if !@web_socket_ping_thread && json.key?('config') && json['config']['hb'] == 1 && json['config']['hbInterval'].present?
405
- start_web_socket_ping_thread(json['config']['hbInterval'] + 7)
406
- end
407
-
408
- if json['apikey'].present? && !@web_socket_authenticated_api_keys.include?(json['apikey'])
409
- @web_socket_authenticated_api_keys << json['apikey']
410
- Ewelink.logger.debug(self.class.name) { "WebSocket successfully authenticated API key: #{json['apikey'].truncate(16).inspect}" }
411
- end
412
-
413
- if json['deviceid'].present? && json['params'].is_a?(Hash) && json['params']['switch'].present?
414
- switch = switches.find { |switch| switch[:device_id] == json['deviceid'] }
415
- if switch.present?
416
- @web_socket_switches_statuses[switch[:uuid]] = json['params']['switch']
417
- Ewelink.logger.debug(self.class.name) { "Switch #{switch[:uuid].inspect} is #{@web_socket_switches_statuses[switch[:uuid]]}" }
418
- end
419
- end
420
- end
421
- end
422
- end
423
-
424
- web_socket_wait_for(-> { @web_socket.present? }) do
425
- @web_socket
426
- end
427
- end
428
- end
429
-
430
- def web_socket_authenticated?
431
- api_keys == @web_socket_authenticated_api_keys
432
- end
433
-
434
- def web_socket_outdated_ping?
435
- @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
436
- end
437
-
438
- def web_socket_sequence
439
- (Time.now.to_f * 1000).round.to_s
440
- end
441
-
442
- def web_socket_url
443
- synchronize(:web_socket_url) do
444
- @web_socket_url ||= begin
445
- params = {
446
- 'accept' => 'ws',
447
- 'appid' => APP_ID,
448
- 'nonce' => nonce,
449
- 'ts' => Time.now.to_i,
450
- 'version' => VERSION,
451
- }
452
- response = rest_request(:post, '/dispatch/app', body: JSON.generate(params), headers: authentication_headers)
453
- raise('Error while getting WebSocket URL') unless response['error'] == 0
454
- domain = response['domain'].presence || raise("Can't get WebSocket server domain")
455
- port = response['port'].presence || raise("Can't get WebSocket server port")
456
- "wss://#{domain}:#{port}/api/ws".tap { |url| Ewelink.logger.debug(self.class.name) { "WebSocket URL is: #{url.inspect}" } }
457
- end
458
- end
459
- end
460
-
461
- def web_socket_wait_for(condition, initialize_web_socket: false, &block)
462
- web_socket if initialize_web_socket
463
- begin
464
- Timeout.timeout(REQUEST_TIMEOUT) do
465
- while !condition.call
466
- sleep(WEB_SOCKET_WAIT_INTERVAL)
467
- end
468
- block_given? ? yield : true
469
- end
470
- rescue => e
471
- reload
472
- raise Error.new(e)
473
- end
474
- end
475
-
476
221
  end
477
222
 
478
223
  end
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.3.0
4
+ version: 3.0.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-09 00:00:00.000000000 Z
11
+ date: 2020-10-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -30,26 +30,6 @@ dependencies:
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
32
  version: 7.0.0
33
- - !ruby/object:Gem::Dependency
34
- name: faye-websocket
35
- requirement: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - ">="
38
- - !ruby/object:Gem::Version
39
- version: 0.11.0
40
- - - "<"
41
- - !ruby/object:Gem::Version
42
- version: 0.12.0
43
- type: :runtime
44
- prerelease: false
45
- version_requirements: !ruby/object:Gem::Requirement
46
- requirements:
47
- - - ">="
48
- - !ruby/object:Gem::Version
49
- version: 0.11.0
50
- - - "<"
51
- - !ruby/object:Gem::Version
52
- version: 0.12.0
53
33
  - !ruby/object:Gem::Dependency
54
34
  name: httparty
55
35
  requirement: !ruby/object:Gem::Requirement