ewelink 2.3.0 → 3.3.1

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: 1d4567a179d6caccebab59ae35e794c4826419b6147461a9f6d9aaf268dea875
4
+ data.tar.gz: d79d86055e3a9c2d5dbc9d2a89be4f4fc60b2a53cd863084a17b13740d7db02e
5
5
  SHA512:
6
- metadata.gz: 6e8a2717428e2b57c7a30a7d4e9a1658032b1155a2f9bd2ba9b8b0cb2309748d248f24d66babfbe2212af9d8096e6be18ebdc3c60d799654571526edff1204d5
7
- data.tar.gz: 9b9e517a3c8629d6a69b8195cab906d12f8f4d4231ab22311083deb75e5db97b02ec9c13a8509710017bb17cfcf1750c46919f52b393b73a0d453c423f120153
6
+ metadata.gz: b79ebc640d8452951fc967ba52a99f6c5d09ccd486b3a6fdf17e10e23abdec94b2933d54566324f214972b6d7c22484c70ab76f0dbc8266a2c806c207f65373b
7
+ data.tar.gz: 42a11ae61affa75e02924cfa8408851568e0a67c33ba9a21d7a7fe3a42d2b025e0909720b371d6cf9b21bc53ad75862ff91216df8a760dd391b1a1b1e5653da1
data/README.mdown CHANGED
@@ -75,6 +75,14 @@ api = Ewelink::Api.new(email: 'john@example.com', password: 'secr$t')
75
75
  api.press_rf_bridge_button!(button[:uuid])
76
76
  ```
77
77
 
78
+ ### Additional options
79
+
80
+ - `async_actions` (`true` | `false`): To perform actions (pressing an RF
81
+ bridge button or turning a switch on/off) in asynchronous mode. (default:
82
+ `false`).
83
+ - `update_devices_status_on_connect` (`true` | `false`): To update devices
84
+ status (on, off) when connecting to Ewelink API (default: `false`).
85
+
78
86
  ### Configuring logger
79
87
 
80
88
  In order to have some debug informations about what kagu does, you could
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.3.0
1
+ 3.3.1
data/ewelink.gemspec CHANGED
@@ -18,7 +18,8 @@ Gem::Specification.new do |s|
18
18
  s.add_dependency 'activesupport', '>= 6.0.0', '< 7.0.0'
19
19
  s.add_dependency 'faye-websocket', '>= 0.11.0', '< 0.12.0'
20
20
  s.add_dependency 'httparty', '>= 0.18.0', '< 0.19.0'
21
+ s.add_dependency 'thread', '>= 0.2.0', '< 0.3.0'
21
22
 
22
23
  s.add_development_dependency 'byebug', '>= 11.0.0', '< 12.0.0'
23
- s.add_development_dependency 'rake', '>= 12.0.0', '< 13.0.0'
24
+ s.add_development_dependency 'rake', '>= 13.0.0', '< 14.0.0'
24
25
  end
data/lib/ewelink.rb CHANGED
@@ -10,6 +10,7 @@ require 'logger'
10
10
  require 'openssl'
11
11
  require 'optparse'
12
12
  require 'set'
13
+ require 'thread/pool'
13
14
  require 'timeout'
14
15
 
15
16
  module Ewelink
data/lib/ewelink/api.rb CHANGED
@@ -18,12 +18,14 @@ module Ewelink
18
18
 
19
19
  attr_reader :email, :password, :phone_number
20
20
 
21
- def initialize(email: nil, password:, phone_number: nil)
21
+ def initialize(async_actions: false, email: nil, password:, phone_number: nil, update_devices_status_on_connect: false)
22
+ @async_actions = async_actions.present?
22
23
  @email = email.presence.try(:strip)
23
24
  @mutexs = {}
24
25
  @password = password.presence || raise(Error.new(":password must be specified"))
25
26
  @phone_number = phone_number.presence.try(:strip)
26
- @web_socket_authenticated_api_keys = Set.new
27
+ @update_devices_status_on_connect = update_devices_status_on_connect.present?
28
+ @web_socket_authenticated = false
27
29
  @web_socket_switches_statuses = {}
28
30
 
29
31
  raise(Error.new(':email or :phone_number must be specified')) if email.blank? && phone_number.blank?
@@ -31,33 +33,39 @@ module Ewelink
31
33
  start_web_socket_authentication_check_thread
32
34
  end
33
35
 
36
+ def async_actions?
37
+ @async_actions
38
+ end
39
+
34
40
  def press_rf_bridge_button!(uuid)
35
- synchronize(:press_rf_bridge_button) do
36
- 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
41
+ process_action do
42
+ synchronize(:press_rf_bridge_button) do
43
+ button = find_rf_bridge_button!(uuid)
44
+ web_socket_wait_for(-> { web_socket_authenticated? }, initialize_web_socket: true) do
45
+ params = {
46
+ 'action' => 'update',
47
+ 'apikey' => button[:api_key],
48
+ 'deviceid' => button[:device_id],
49
+ 'params' => {
50
+ 'cmd' => 'transmit',
51
+ 'rfChl' => button[:channel],
52
+ },
53
+ 'sequence' => web_socket_sequence,
54
+ 'ts' => 0,
55
+ 'userAgent' => 'app',
56
+ }
57
+ Ewelink.logger.debug(self.class.name) { "Pressing RF bridge button #{button[:uuid].inspect}" }
58
+ send_to_web_socket(JSON.generate(params))
59
+ true
60
+ end
53
61
  end
54
62
  end
55
63
  end
56
64
 
57
65
  def reload
58
- Ewelink.logger.debug(self.class.name) { 'Reloading API (authentication token, devices, region, connections,...)' }
66
+ Ewelink.logger.debug(self.class.name) { 'Reloading API (authentication token, api key, devices, region, connections,...)' }
59
67
 
60
- @web_socket_authenticated_api_keys.clear
68
+ @web_socket_authenticated = false
61
69
  @web_socket_switches_statuses.clear
62
70
 
63
71
  [@web_socket_ping_thread, @web_socket_thread].each do |thread|
@@ -78,8 +86,7 @@ module Ewelink
78
86
  end
79
87
 
80
88
  [
81
- :@api_keys,
82
- :@authentication_token,
89
+ :@authentication_infos,
83
90
  :@devices,
84
91
  :@last_web_socket_pong_at,
85
92
  :@region,
@@ -172,66 +179,68 @@ module Ewelink
172
179
  end
173
180
 
174
181
  def turn_switch!(uuid, on)
175
- if ['on', :on, 'true'].include?(on)
176
- on = true
177
- elsif ['off', :off, 'false'].include?(on)
178
- on = false
179
- end
180
- 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))
182
+ process_action do
183
+ if ['on', :on, 'true'].include?(on)
184
+ on = true
185
+ elsif ['off', :off, 'false'].include?(on)
186
+ on = false
187
+ end
188
+ switch = find_switch!(uuid)
189
+ @web_socket_switches_statuses[switch[:uuid]] = nil
190
+ web_socket_wait_for(-> { web_socket_authenticated? }, initialize_web_socket: true) do
191
+ params = {
192
+ 'action' => 'update',
193
+ 'apikey' => switch[:api_key],
194
+ 'deviceid' => switch[:device_id],
195
+ 'params' => {
196
+ 'switch' => on ? 'on' : 'off',
197
+ },
198
+ 'sequence' => web_socket_sequence,
199
+ 'ts' => 0,
200
+ 'userAgent' => 'app',
201
+ }
202
+ Ewelink.logger.debug(self.class.name) { "Turning switch #{switch[:uuid].inspect} #{on ? 'on' : 'off'}" }
203
+ send_to_web_socket(JSON.generate(params))
204
+ end
205
+ sleep(SWITCH_STATUS_CHANGE_CHECK_TIMEOUT)
206
+ switch_on?(switch[:uuid]) # Waiting for switch status update
207
+ true
196
208
  end
197
- sleep(SWITCH_STATUS_CHANGE_CHECK_TIMEOUT)
198
- switch_on?(switch[:uuid]) # Waiting for switch status update
199
- true
209
+ end
210
+
211
+ def update_devices_status_on_connect?
212
+ @update_devices_status_on_connect
200
213
  end
201
214
 
202
215
  private
203
216
 
204
- def api_keys
205
- synchronize(:api_keys) do
206
- @api_keys ||= Set.new(devices.map { |device| device['apikey'] })
207
- end
217
+ def api_key
218
+ authentication_infos[:api_key]
208
219
  end
209
220
 
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
221
+ def authenticate_web_socket_api_key
222
+ params = {
223
+ 'action' => 'userOnline',
224
+ 'apikey' => api_key,
225
+ 'appid' => APP_ID,
226
+ 'at' => authentication_token,
227
+ 'nonce' => nonce,
228
+ 'sequence' => web_socket_sequence,
229
+ 'ts' => Time.now.to_i,
230
+ 'userAgent' => 'app',
231
+ 'version' => VERSION,
232
+ }
233
+ Ewelink.logger.debug(self.class.name) { "Authenticating WebSocket API key: #{api_key.truncate(16).inspect}" }
234
+ send_to_web_socket(JSON.generate(params))
226
235
  end
227
236
 
228
237
  def authentication_headers
229
238
  { 'Authorization' => "Bearer #{authentication_token}" }
230
239
  end
231
240
 
232
- def authentication_token
233
- synchronize(:authentication_token) do
234
- @authentication_token ||= begin
241
+ def authentication_infos
242
+ synchronize(:authentication_infos) do
243
+ @authentication_infos ||= begin
235
244
  params = {
236
245
  'appid' => APP_ID,
237
246
  'imei' => SecureRandom.uuid.upcase,
@@ -248,11 +257,19 @@ module Ewelink
248
257
  body = JSON.generate(params)
249
258
  response = rest_request(:post, '/api/user/login', { body: body, headers: { 'Authorization' => "Sign #{Base64.encode64(OpenSSL::HMAC.digest('SHA256', APP_SECRET, body))}" } })
250
259
  raise(Error.new('Authentication token not found')) if response['at'].blank?
251
- response['at'].tap { Ewelink.logger.debug(self.class.name) { 'Authentication token found' } }
260
+ raise(Error.new('API key not found')) if response['user'].blank? || response['user']['apikey'].blank?
261
+ {
262
+ authentication_token: response['at'].tap { Ewelink.logger.debug(self.class.name) { 'Authentication token found' } },
263
+ api_key: response['user']['apikey'].tap { Ewelink.logger.debug(self.class.name) { 'API key found' } },
264
+ }
252
265
  end
253
266
  end
254
267
  end
255
268
 
269
+ def authentication_token
270
+ authentication_infos[:authentication_token]
271
+ end
272
+
256
273
  def devices
257
274
  synchronize(:devices) do
258
275
  @devices ||= begin
@@ -281,6 +298,13 @@ module Ewelink
281
298
  SecureRandom.hex[0, 8]
282
299
  end
283
300
 
301
+ def process_action(&block)
302
+ return yield unless async_actions?
303
+ @async_actions_thread_pool ||= Thread.pool(1)
304
+ @async_actions_thread_pool.process(&block)
305
+ nil
306
+ end
307
+
284
308
  def region
285
309
  @region ||= DEFAULT_REGION
286
310
  end
@@ -297,7 +321,7 @@ module Ewelink
297
321
  Ewelink.logger.debug(self.class.name) { "Switched to region #{region.inspect}" }
298
322
  return rest_request(method, path, options)
299
323
  end
300
- remove_instance_variable(:@authentication_token) if instance_variable_defined?(:@authentication_token) && [401, 403].include?(response['error'])
324
+ remove_instance_variable(:@authentication_infos) if instance_variable_defined?(:@authentication_infos) && [401, 403].include?(response['error'])
301
325
  raise(Error.new("#{method} #{url}: #{response['error']} #{response['msg']}".strip)) if response['error'].present? && response['error'] != 0
302
326
  response.to_h
303
327
  rescue Errno::ECONNREFUSED, OpenSSL::OpenSSLError, SocketError, Timeout::Error => e
@@ -358,7 +382,7 @@ module Ewelink
358
382
 
359
383
  # Initializes caches before opening WebSocket: important in order to
360
384
  # NOT cumulate requests Timeouts from #web_socket_wait_for.
361
- api_keys
385
+ api_key
362
386
  web_socket_url
363
387
 
364
388
  Ewelink.logger.debug(self.class.name) { "Opening WebSocket to #{web_socket_url.inspect}" }
@@ -375,7 +399,7 @@ module Ewelink
375
399
  @web_socket.on(:open) do |event|
376
400
  Ewelink.logger.debug(self.class.name) { 'WebSocket opened' }
377
401
  @last_web_socket_pong_at = Time.now
378
- authenticate_web_socket_api_keys
402
+ authenticate_web_socket_api_key
379
403
  end
380
404
 
381
405
  @web_socket.on(:message) do |event|
@@ -405,9 +429,10 @@ module Ewelink
405
429
  start_web_socket_ping_thread(json['config']['hbInterval'] + 7)
406
430
  end
407
431
 
408
- if json['apikey'].present? && !@web_socket_authenticated_api_keys.include?(json['apikey'])
409
- @web_socket_authenticated_api_keys << json['apikey']
432
+ if json['apikey'].present? && !@web_socket_authenticated && json['apikey'] == api_key
433
+ @web_socket_authenticated = true
410
434
  Ewelink.logger.debug(self.class.name) { "WebSocket successfully authenticated API key: #{json['apikey'].truncate(16).inspect}" }
435
+ Thread.new { switches.each { |switch| switch_on?(switch[:uuid]) } } if update_devices_status_on_connect?
411
436
  end
412
437
 
413
438
  if json['deviceid'].present? && json['params'].is_a?(Hash) && json['params']['switch'].present?
@@ -428,7 +453,7 @@ module Ewelink
428
453
  end
429
454
 
430
455
  def web_socket_authenticated?
431
- api_keys == @web_socket_authenticated_api_keys
456
+ @web_socket_authenticated.present?
432
457
  end
433
458
 
434
459
  def web_socket_outdated_ping?
@@ -3,7 +3,7 @@ module Ewelink
3
3
  class Runner
4
4
 
5
5
  def run
6
- api = Api.new(options.slice(:email, :password, :phone_number))
6
+ api = Api.new(**options.slice(:email, :password, :phone_number))
7
7
  puts(JSON.pretty_generate(api.switches)) if options[:list_switches]
8
8
  puts(JSON.pretty_generate(api.rf_bridge_buttons)) if options[:list_rf_bridge_buttons]
9
9
  options[:turn_switches_on_uuids].each { |uuid| api.turn_switch!(uuid, :on) }
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.3.1
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: 2021-06-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -70,6 +70,26 @@ dependencies:
70
70
  - - "<"
71
71
  - !ruby/object:Gem::Version
72
72
  version: 0.19.0
73
+ - !ruby/object:Gem::Dependency
74
+ name: thread
75
+ requirement: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: 0.2.0
80
+ - - "<"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.3.0
83
+ type: :runtime
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: 0.2.0
90
+ - - "<"
91
+ - !ruby/object:Gem::Version
92
+ version: 0.3.0
73
93
  - !ruby/object:Gem::Dependency
74
94
  name: byebug
75
95
  requirement: !ruby/object:Gem::Requirement
@@ -96,20 +116,20 @@ dependencies:
96
116
  requirements:
97
117
  - - ">="
98
118
  - !ruby/object:Gem::Version
99
- version: 12.0.0
119
+ version: 13.0.0
100
120
  - - "<"
101
121
  - !ruby/object:Gem::Version
102
- version: 13.0.0
122
+ version: 14.0.0
103
123
  type: :development
104
124
  prerelease: false
105
125
  version_requirements: !ruby/object:Gem::Requirement
106
126
  requirements:
107
127
  - - ">="
108
128
  - !ruby/object:Gem::Version
109
- version: 12.0.0
129
+ version: 13.0.0
110
130
  - - "<"
111
131
  - !ruby/object:Gem::Version
112
- version: 13.0.0
132
+ version: 14.0.0
113
133
  description: Manage eWeLink smart home devices
114
134
  email: al@alweb.org
115
135
  executables:
@@ -144,7 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
144
164
  - !ruby/object:Gem::Version
145
165
  version: '0'
146
166
  requirements: []
147
- rubygems_version: 3.0.3
167
+ rubygems_version: 3.2.15
148
168
  signing_key:
149
169
  specification_version: 4
150
170
  summary: Manage eWeLink devices