ewelink 2.3.0 → 3.0.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 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