ewelink 2.1.1 → 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: bb352abf05d6bb6ef2f3b277c6f551d436bed969086478d3e121fd2e178fe4f9
4
- data.tar.gz: b74266f2cbc92457a3bb0dc087752299a5a9f8348d3928a79d8c3c0719653e6a
3
+ metadata.gz: 558b7f50525eabfc0e0ac3cd32fd5a0ff5c0d4b6cc7658024dba4c6c0336f96a
4
+ data.tar.gz: cda09a44af6247436a22af9443a1dd3c078e8b86ab228f3ce12761baca606a7b
5
5
  SHA512:
6
- metadata.gz: 2c5bb695c7d0094436454694008dd3c05c660fb1c7dfe2436ec1fa2fae3341559f901f3b9ae4ebfa99fcc276dcd4ceee02205ed1608d03e515b8da64b52f8a25
7
- data.tar.gz: 4713a948c736a40496b3b33fff3b83253a4559693d692cfaa7251a4fc2d3f0e67699834f6513226edcbfe6cf7759ab54b1cb6c5a24cbe1559f54a75d6cbda6fd
6
+ metadata.gz: 9607d6170f6e3f0cb00d599fa7a8258b25367423e6a87b2238079d336d6ee2a58389f78ffdcbc3eaf9726c1685ff3cb8b95fb0387d655ae6b81614d50d990c01
7
+ data.tar.gz: 4ad312d700e40c50f20cd47f5d66dec54937633e1162e21b3d131556752dc937e41c70d128e210d344d19a8bd0cb0fb4de1606f081cba50ad1189c84655440b6
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.1.1
1
+ 3.0.0
@@ -17,7 +17,6 @@ Gem::Specification.new do |s|
17
17
 
18
18
  s.add_dependency 'activesupport', '>= 6.0.0', '< 7.0.0'
19
19
  s.add_dependency 'httparty', '>= 0.18.0', '< 0.19.0'
20
- s.add_dependency 'websocket-client-simple', '>= 0.3.0', '< 0.4.0'
21
20
 
22
21
  s.add_development_dependency 'byebug', '>= 11.0.0', '< 12.0.0'
23
22
  s.add_development_dependency 'rake', '>= 12.0.0', '< 13.0.0'
@@ -7,9 +7,6 @@ require 'json'
7
7
  require 'logger'
8
8
  require 'openssl'
9
9
  require 'optparse'
10
- require 'set'
11
- require 'timeout'
12
- require 'websocket-client-simple'
13
10
 
14
11
  module Ewelink
15
12
 
@@ -5,14 +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_PING_TOLERANCE_FACTOR = 1.5
15
- WEB_SOCKET_WAIT_INTERVAL = 0.2.seconds
16
14
 
17
15
  attr_reader :email, :password, :phone_number
18
16
 
@@ -21,45 +19,31 @@ module Ewelink
21
19
  @mutexs = {}
22
20
  @password = password.presence || raise(Error.new(":password must be specified"))
23
21
  @phone_number = phone_number.presence.try(:strip)
24
- @web_socket_authenticated_api_keys = Set.new
25
- @web_socket_switches_statuses = {}
26
22
  raise(Error.new(":email or :phone_number must be specified")) if email.blank? && phone_number.blank?
27
23
  end
28
24
 
29
25
  def press_rf_bridge_button!(uuid)
30
26
  synchronize(:press_rf_bridge_button) do
31
27
  button = find_rf_bridge_button!(uuid)
32
- web_socket_wait_for(-> { web_socket_authenticated? }) do
33
- params = {
34
- 'action' => 'update',
35
- 'apikey' => button[:api_key],
36
- 'deviceid' => button[:device_id],
37
- 'params' => {
38
- 'cmd' => 'transmit',
39
- 'rfChl' => button[:channel],
40
- },
41
- 'sequence' => web_socket_sequence,
42
- 'ts' => 0,
43
- 'userAgent' => 'app',
44
- }
45
- Ewelink.logger.debug(self.class.name) { "Pressing RF bridge button #{button[:uuid].inspect}" }
46
- send_to_web_socket(JSON.generate(params))
47
- true
48
- 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
49
41
  end
50
42
  end
51
43
 
52
44
  def reload
53
- Ewelink.logger.debug(self.class.name) { 'Reloading API (authentication token, devices, region, connections,...)' }
54
- dispose_web_socket
55
- [
56
- :@api_keys,
57
- :@authentication_token,
58
- :@devices,
59
- :@region,
60
- :@rf_bridge_buttons,
61
- :@switches,
62
- ].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|
63
47
  remove_instance_variable(variable) if instance_variable_defined?(variable)
64
48
  end
65
49
  self
@@ -72,12 +56,10 @@ module Ewelink
72
56
  Ewelink.logger.debug(self.class.name) { "Found #{devices.size} RF 433MHz bridge device(s)" }
73
57
  end
74
58
  rf_bridge_devices.each do |device|
75
- api_key = device['apikey'].presence || next
76
59
  device_id = device['deviceid'].presence || next
77
60
  device_name = device['name'].presence || next
78
61
  buttons = device['params']['rfList'].each do |rf|
79
62
  button = {
80
- api_key: api_key,
81
63
  channel: rf['rfChl'],
82
64
  device_id: device_id,
83
65
  device_name: device_name,
@@ -101,24 +83,15 @@ module Ewelink
101
83
 
102
84
  def switch_on?(uuid)
103
85
  switch = find_switch!(uuid)
104
- if @web_socket_switches_statuses[switch[:uuid]].nil?
105
- params = {
106
- 'action' => 'query',
107
- 'apikey' => switch[:api_key],
108
- 'deviceid' => switch[:device_id],
109
- 'sequence' => web_socket_sequence,
110
- 'ts' => 0,
111
- 'userAgent' => 'app',
112
- }
113
- web_socket_wait_for(-> { web_socket_authenticated? }) do
114
- Ewelink.logger.debug(self.class.name) { "Checking switch #{switch[:uuid].inspect} status" }
115
- send_to_web_socket(JSON.generate(params))
116
- end
117
- end
118
- web_socket_wait_for(-> { !@web_socket_switches_statuses[switch[:uuid]].nil? }) do
119
- Ewelink.logger.debug(self.class.name) { "Switch #{switch[:uuid].inspect} is #{@web_socket_switches_statuses[switch[:uuid]]}" }
120
- @web_socket_switches_statuses[switch[:uuid]] == 'on'
121
- 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'
122
95
  end
123
96
 
124
97
  def switches
@@ -126,11 +99,9 @@ module Ewelink
126
99
  @switches ||= [].tap do |switches|
127
100
  switch_devices = devices.select { |device| SWITCH_DEVICES_UIIDS.include?(device['uiid']) }
128
101
  switch_devices.each do |device|
129
- api_key = device['apikey'].presence || next
130
102
  device_id = device['deviceid'].presence || next
131
103
  name = device['name'].presence || next
132
104
  switch = {
133
- api_key: api_key,
134
105
  device_id: device_id,
135
106
  name: name,
136
107
  }
@@ -148,34 +119,22 @@ module Ewelink
148
119
  on = false
149
120
  end
150
121
  switch = find_switch!(uuid)
151
- @web_socket_switches_statuses[switch[:uuid]] = nil
152
- web_socket_wait_for(-> { web_socket_authenticated? }) do
153
- params = {
154
- 'action' => 'update',
155
- 'apikey' => switch[:api_key],
156
- 'deviceid' => switch[:device_id],
157
- 'params' => {
158
- 'switch' => on ? 'on' : 'off',
159
- },
160
- 'sequence' => web_socket_sequence,
161
- 'ts' => 0,
162
- 'userAgent' => 'app',
163
- }
164
- Ewelink.logger.debug(self.class.name) { "Turning switch #{switch[:uuid].inspect} #{on ? 'on' : 'off'}" }
165
- send_to_web_socket(JSON.generate(params))
166
- end
167
- 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)
168
133
  true
169
134
  end
170
135
 
171
136
  private
172
137
 
173
- def api_keys
174
- synchronize(:api_keys) do
175
- @api_keys ||= Set.new(devices.map { |device| device['apikey'] })
176
- end
177
- end
178
-
179
138
  def authentication_headers
180
139
  { 'Authorization' => "Bearer #{authentication_token}" }
181
140
  end
@@ -220,37 +179,6 @@ module Ewelink
220
179
  end
221
180
  end
222
181
 
223
- def dispose_web_socket
224
- @web_socket_authenticated_api_keys.clear
225
- @web_socket_switches_statuses.clear
226
-
227
- if @web_socket_ping_thread
228
- if Thread.current == @web_socket_ping_thread
229
- Thread.current[:stop] = true
230
- else
231
- @web_socket_ping_thread.kill
232
- end
233
- end
234
-
235
- if @web_socket.present?
236
- begin
237
- @web_socket.close if @web_socket.open?
238
- rescue
239
- # Ignoring close errors
240
- end
241
- end
242
-
243
- [
244
- :@last_web_socket_pong_at,
245
- :@web_socket_ping_interval,
246
- :@web_socket_ping_thread,
247
- :@web_socket_url,
248
- :@web_socket,
249
- ].each do |variable|
250
- remove_instance_variable(variable) if instance_variable_defined?(variable)
251
- end
252
- end
253
-
254
182
  def find_rf_bridge_button!(uuid)
255
183
  rf_bridge_buttons.find { |button| button[:uuid] == uuid } || raise(Error.new("No such RF bridge button with UUID: #{uuid.inspect}"))
256
184
  end
@@ -272,7 +200,7 @@ module Ewelink
272
200
  method = method.to_s.upcase
273
201
  headers = (options[:headers] || {}).reverse_merge('Content-Type' => 'application/json')
274
202
  Ewelink.logger.debug(self.class.name) { "#{method} #{url}" }
275
- 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))
276
204
  raise(Error.new("#{method} #{url}: #{response.code}")) unless response.success?
277
205
  if response['error'] == 301 && response['region'].present?
278
206
  @region = response['region']
@@ -281,163 +209,15 @@ module Ewelink
281
209
  end
282
210
  remove_instance_variable(:@authentication_token) if instance_variable_defined?(:@authentication_token) && [401, 403].include?(response['error'])
283
211
  raise(Error.new("#{method} #{url}: #{response['error']} #{response['msg']}".strip)) if response['error'].present? && response['error'] != 0
284
- response.to_h
212
+ response
285
213
  rescue Errno::ECONNREFUSED, OpenSSL::OpenSSLError, SocketError, Timeout::Error => e
286
214
  raise Error.new(e)
287
215
  end
288
216
 
289
- def send_to_web_socket(data)
290
- if web_socket_outdated_ping?
291
- Ewelink.logger.warn(self.class.name) { 'WebSocket ping is outdated' }
292
- dispose_web_socket
293
- end
294
- web_socket.send(data)
295
- rescue
296
- dispose_web_socket
297
- raise
298
- end
299
-
300
217
  def synchronize(name, &block)
301
218
  (@mutexs[name] ||= Mutex.new).synchronize(&block)
302
219
  end
303
220
 
304
- def web_socket
305
- synchronize(:web_socket) do
306
- @web_socket ||= begin
307
- api = self
308
-
309
- WebSocket::Client::Simple.connect(web_socket_url) do |web_socket|
310
- Ewelink.logger.debug(self.class.name) { "Opening WebSocket to #{web_socket_url.inspect}" }
311
-
312
- web_socket.on(:close) do
313
- api.instance_eval do
314
- Ewelink.logger.debug(self.class.name) { 'WebSocket closed' }
315
- dispose_web_socket
316
- end
317
- end
318
-
319
- web_socket.on(:error) do |e|
320
- api.instance_eval do
321
- Ewelink.logger.warn(self.class.name) { "WebSocket error: #{e}" }
322
- dispose_web_socket
323
- end
324
- end
325
-
326
- web_socket.on(:message) do |message|
327
- api.instance_eval do
328
- if message.data == 'pong'
329
- Ewelink.logger.debug(self.class.name) { "Received WebSocket #{message.data.inspect} message" }
330
- @last_web_socket_pong_at = Time.now
331
- next
332
- end
333
-
334
- begin
335
- response = JSON.parse(message.data)
336
- rescue => e
337
- Ewelink.logger.error(self.class.name) { "WebSocket JSON parse error" }
338
- next
339
- end
340
-
341
- if response.key?('error') && response['error'] != 0
342
- Ewelink.logger.error(self.class.name) { "WebSocket message error: #{message.data}" }
343
- next
344
- end
345
-
346
- if !@web_socket_ping_thread && response.key?('config') && response['config']['hb'] == 1 && response['config']['hbInterval'].present?
347
- @last_web_socket_pong_at = Time.now
348
- @web_socket_ping_interval = response['config']['hbInterval'] + 7
349
- Ewelink.logger.debug(self.class.name) { "Creating thread for WebSocket ping every #{@web_socket_ping_interval} seconds" }
350
- @web_socket_ping_thread = Thread.new do
351
- loop do
352
- break if Thread.current[:stop]
353
- sleep(@web_socket_ping_interval)
354
- Ewelink.logger.debug(self.class.name) { 'Sending WebSocket ping' }
355
- send_to_web_socket('ping')
356
- end
357
- end
358
- end
359
-
360
- if response['apikey'].present? && !@web_socket_authenticated_api_keys.include?(response['apikey'])
361
- @web_socket_authenticated_api_keys << response['apikey']
362
- Ewelink.logger.debug(self.class.name) { "WebSocket successfully authenticated API key: #{response['apikey'].truncate(16).inspect}" }
363
- end
364
-
365
- if response['deviceid'].present? && response['params'].is_a?(Hash) && response['params']['switch'].present?
366
- switch = switches.find { |switch| switch[:device_id] == response['deviceid'] }
367
- @web_socket_switches_statuses[switch[:uuid]] = response['params']['switch'] if switch.present?
368
- end
369
- end
370
- end
371
-
372
- web_socket.on(:open) do
373
- api.instance_eval do
374
- Ewelink.logger.debug(self.class.name) { 'WebSocket opened' }
375
- api_keys.each do |api_key|
376
- params = {
377
- 'action' => 'userOnline',
378
- 'apikey' => api_key,
379
- 'appid' => APP_ID,
380
- 'at' => authentication_token,
381
- 'nonce' => nonce,
382
- 'sequence' => web_socket_sequence,
383
- 'ts' => Time.now.to_i,
384
- 'userAgent' => 'app',
385
- 'version' => VERSION,
386
- }
387
- Ewelink.logger.debug(self.class.name) { "Authenticating WebSocket API key: #{api_key.truncate(16).inspect}" }
388
- send_to_web_socket(JSON.generate(params))
389
- end
390
- end
391
- end
392
- end
393
- end
394
- end
395
- end
396
-
397
- def web_socket_authenticated?
398
- api_keys == @web_socket_authenticated_api_keys
399
- end
400
-
401
- def web_socket_outdated_ping?
402
- @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
403
- end
404
-
405
- def web_socket_sequence
406
- (Time.now.to_f * 1000).round.to_s
407
- end
408
-
409
- def web_socket_url
410
- synchronize(:web_socket_url) do
411
- @web_socket_url ||= begin
412
- params = {
413
- 'accept' => 'ws',
414
- 'appid' => APP_ID,
415
- 'nonce' => nonce,
416
- 'ts' => Time.now.to_i,
417
- 'version' => VERSION,
418
- }
419
- response = rest_request(:post, '/dispatch/app', body: JSON.generate(params), headers: authentication_headers)
420
- raise('Error while getting WebSocket URL') unless response['error'] == 0
421
- domain = response['domain'].presence || raise("Can't get WebSocket server domain")
422
- port = response['port'].presence || raise("Can't get WebSocket server port")
423
- "wss://#{domain}:#{port}/api/ws".tap { |url| Ewelink.logger.debug(self.class.name) { "WebSocket URL is: #{url.inspect}" } }
424
- end
425
- end
426
- end
427
-
428
- def web_socket_wait_for(condition, &block)
429
- web_socket # Initializes WebSocket
430
- Timeout.timeout(REQUEST_TIMEOUT) do
431
- loop do
432
- if condition.call
433
- return yield if block_given?
434
- return true
435
- end
436
- sleep(WEB_SOCKET_WAIT_INTERVAL)
437
- end
438
- end
439
- end
440
-
441
221
  end
442
222
 
443
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.1.1
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-02 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
@@ -50,26 +50,6 @@ dependencies:
50
50
  - - "<"
51
51
  - !ruby/object:Gem::Version
52
52
  version: 0.19.0
53
- - !ruby/object:Gem::Dependency
54
- name: websocket-client-simple
55
- requirement: !ruby/object:Gem::Requirement
56
- requirements:
57
- - - ">="
58
- - !ruby/object:Gem::Version
59
- version: 0.3.0
60
- - - "<"
61
- - !ruby/object:Gem::Version
62
- version: 0.4.0
63
- type: :runtime
64
- prerelease: false
65
- version_requirements: !ruby/object:Gem::Requirement
66
- requirements:
67
- - - ">="
68
- - !ruby/object:Gem::Version
69
- version: 0.3.0
70
- - - "<"
71
- - !ruby/object:Gem::Version
72
- version: 0.4.0
73
53
  - !ruby/object:Gem::Dependency
74
54
  name: byebug
75
55
  requirement: !ruby/object:Gem::Requirement