ewelink 2.1.1 → 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: 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