ewelink 1.2.0 → 2.2.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: 21968b7bb562eb3c32f0011a501ea3978fabd5ba3cc50164854e7a544c607f09
4
- data.tar.gz: cdedc7c05271d76f5f38317b3276bae9c24c7380877e97988c0aea9f048d88fa
3
+ metadata.gz: 2392627c54c851fb5fd14194b484b56b1013b7a26e8d5bafe2a79e37dfa80464
4
+ data.tar.gz: 2a5362bafa04f9e3cf212a5899b4e6183514fdff68b3146a4fa245c52e8fcadd
5
5
  SHA512:
6
- metadata.gz: ddd38e7e100f20ce2a96edcf3e2c3a3ac1c2945c0ad49e93aeeb3ecf943d08db03756afb4847c28c55a7601f952a13abe1034858f66ba46ec8e6359523a2b696
7
- data.tar.gz: fb913f0ae36bf7f72bb27c8af1e863deb55b8309f23a79dc799e677c2f5706671a7a7b2efdff1ad8a7bfeec46c6ffc23774564738a12ad4dc20ebf1f2f1fd2f8
6
+ metadata.gz: b2d3ecbf884461e11a51cf7e4efe7438f1c066cc7c076629371c96b3255b50f2a3f4e0314f6a40f9b7247adef06d69eb0b1f66b31ebcabff1c46b7def21d8291
7
+ data.tar.gz: a2d064d2af9e800ef0165bf5d614e4857abb0503b1faad7e280f3f2df422f08c5c49e57e10b3202bfcac700dc667861690c0a29a44faaf13a5ae00307201fe87
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.2.0
1
+ 2.2.0
@@ -16,6 +16,7 @@ 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'
19
20
  s.add_dependency 'httparty', '>= 0.18.0', '< 0.19.0'
20
21
 
21
22
  s.add_development_dependency 'byebug', '>= 11.0.0', '< 12.0.0'
@@ -1,12 +1,16 @@
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'
4
6
  require 'httparty'
5
7
  require 'io/console'
6
8
  require 'json'
7
9
  require 'logger'
8
10
  require 'openssl'
9
11
  require 'optparse'
12
+ require 'set'
13
+ require 'timeout'
10
14
 
11
15
  module Ewelink
12
16
 
@@ -5,12 +5,14 @@ module Ewelink
5
5
  APP_ID = 'oeVkj2lYFGnJu5XUtWisfW4utiN4u9Mq'
6
6
  APP_SECRET = '6Nz4n0xA8s8qdxQf2GqurZj2Fs55FUvM'
7
7
  DEFAULT_REGION = 'us'
8
+ REQUEST_TIMEOUT = 10.seconds
8
9
  RF_BRIDGE_DEVICE_UIID = 28
9
10
  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
14
16
 
15
17
  attr_reader :email, :password, :phone_number
16
18
 
@@ -19,31 +21,45 @@ module Ewelink
19
21
  @mutexs = {}
20
22
  @password = password.presence || raise(Error.new(":password must be specified"))
21
23
  @phone_number = phone_number.presence.try(:strip)
24
+ @web_socket_authenticated_api_keys = Set.new
25
+ @web_socket_switches_statuses = {}
22
26
  raise(Error.new(":email or :phone_number must be specified")) if email.blank? && phone_number.blank?
23
27
  end
24
28
 
25
29
  def press_rf_bridge_button!(uuid)
26
30
  synchronize(:press_rf_bridge_button) do
27
31
  button = find_rf_bridge_button!(uuid)
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
- http_request(:post, '/api/user/device/status', body: JSON.generate(params), headers: authentication_headers)
40
- true
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
41
49
  end
42
50
  end
43
51
 
44
52
  def reload
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|
53
+ Ewelink.logger.debug(self.class.name) { 'Reloading API (authentication token, devices, region,...)' }
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|
47
63
  remove_instance_variable(variable) if instance_variable_defined?(variable)
48
64
  end
49
65
  self
@@ -56,10 +72,12 @@ module Ewelink
56
72
  Ewelink.logger.debug(self.class.name) { "Found #{devices.size} RF 433MHz bridge device(s)" }
57
73
  end
58
74
  rf_bridge_devices.each do |device|
75
+ api_key = device['apikey'].presence || next
59
76
  device_id = device['deviceid'].presence || next
60
77
  device_name = device['name'].presence || next
61
78
  buttons = device['params']['rfList'].each do |rf|
62
79
  button = {
80
+ api_key: api_key,
63
81
  channel: rf['rfChl'],
64
82
  device_id: device_id,
65
83
  device_name: device_name,
@@ -83,15 +101,23 @@ module Ewelink
83
101
 
84
102
  def switch_on?(uuid)
85
103
  switch = find_switch!(uuid)
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 = http_request(:get, '/api/user/device/status', headers: authentication_headers, query: params)
94
- response['params']['switch'] == 'on'
104
+ if @web_socket_switches_statuses[switch[:uuid]].nil?
105
+ web_socket_wait_for(-> { web_socket_authenticated? }) do
106
+ Ewelink.logger.debug(self.class.name) { "Checking switch #{switch[:uuid].inspect} status" }
107
+ params = {
108
+ 'action' => 'query',
109
+ 'apikey' => switch[:api_key],
110
+ 'deviceid' => switch[:device_id],
111
+ 'sequence' => web_socket_sequence,
112
+ 'ts' => 0,
113
+ 'userAgent' => 'app',
114
+ }
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
+ @web_socket_switches_statuses[switch[:uuid]] == 'on'
120
+ end
95
121
  end
96
122
 
97
123
  def switches
@@ -99,9 +125,11 @@ module Ewelink
99
125
  @switches ||= [].tap do |switches|
100
126
  switch_devices = devices.select { |device| SWITCH_DEVICES_UIIDS.include?(device['uiid']) }
101
127
  switch_devices.each do |device|
128
+ api_key = device['apikey'].presence || next
102
129
  device_id = device['deviceid'].presence || next
103
130
  name = device['name'].presence || next
104
131
  switch = {
132
+ api_key: api_key,
105
133
  device_id: device_id,
106
134
  name: name,
107
135
  }
@@ -119,22 +147,52 @@ module Ewelink
119
147
  on = false
120
148
  end
121
149
  switch = find_switch!(uuid)
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
- http_request(:post, '/api/user/device/status', body: JSON.generate(params), headers: authentication_headers)
150
+ @web_socket_switches_statuses[switch[:uuid]] = nil
151
+ web_socket_wait_for(-> { web_socket_authenticated? }) do
152
+ params = {
153
+ 'action' => 'update',
154
+ 'apikey' => switch[:api_key],
155
+ 'deviceid' => switch[:device_id],
156
+ 'params' => {
157
+ 'switch' => on ? 'on' : 'off',
158
+ },
159
+ 'sequence' => web_socket_sequence,
160
+ 'ts' => 0,
161
+ 'userAgent' => 'app',
162
+ }
163
+ Ewelink.logger.debug(self.class.name) { "Turning switch #{switch[:uuid].inspect} #{on ? 'on' : 'off'}" }
164
+ send_to_web_socket(JSON.generate(params))
165
+ end
166
+ switch_on?(switch[:uuid]) # Waiting for switch status update
133
167
  true
134
168
  end
135
169
 
136
170
  private
137
171
 
172
+ def api_keys
173
+ synchronize(:api_keys) do
174
+ @api_keys ||= Set.new(devices.map { |device| device['apikey'] })
175
+ end
176
+ end
177
+
178
+ def authenticate_web_socket_api_keys
179
+ api_keys.each do |api_key|
180
+ params = {
181
+ 'action' => 'userOnline',
182
+ 'apikey' => api_key,
183
+ 'appid' => APP_ID,
184
+ 'at' => authentication_token,
185
+ 'nonce' => nonce,
186
+ 'sequence' => web_socket_sequence,
187
+ 'ts' => Time.now.to_i,
188
+ 'userAgent' => 'app',
189
+ 'version' => VERSION,
190
+ }
191
+ Ewelink.logger.debug(self.class.name) { "Authenticating WebSocket API key: #{api_key.truncate(16).inspect}" }
192
+ send_to_web_socket(JSON.generate(params))
193
+ end
194
+ end
195
+
138
196
  def authentication_headers
139
197
  { 'Authorization' => "Bearer #{authentication_token}" }
140
198
  end
@@ -156,7 +214,7 @@ module Ewelink
156
214
  params['phoneNumber'] = phone_number
157
215
  end
158
216
  body = JSON.generate(params)
159
- response = http_request(:post, '/api/user/login', { body: body, headers: { 'Authorization' => "Sign #{Base64.encode64(OpenSSL::HMAC.digest('SHA256', APP_SECRET, body))}" } })
217
+ response = rest_request(:post, '/api/user/login', { body: body, headers: { 'Authorization' => "Sign #{Base64.encode64(OpenSSL::HMAC.digest('SHA256', APP_SECRET, body))}" } })
160
218
  raise(Error.new('Authentication token not found')) if response['at'].blank?
161
219
  response['at'].tap { Ewelink.logger.debug(self.class.name) { 'Authentication token found' } }
162
220
  end
@@ -173,12 +231,46 @@ module Ewelink
173
231
  'ts' => Time.now.to_i,
174
232
  'version' => VERSION,
175
233
  }
176
- response = http_request(:get, '/api/user/device', headers: authentication_headers, query: params)
234
+ response = rest_request(:get, '/api/user/device', headers: authentication_headers, query: params)
177
235
  response['devicelist'].tap { |devices| Ewelink.logger.debug(self.class.name) { "Found #{devices.size} device(s)" } }
178
236
  end
179
237
  end
180
238
  end
181
239
 
240
+ def dispose_web_socket
241
+ Ewelink.logger.debug(self.class.name) { 'Dispose WebSocket' }
242
+ @web_socket_authenticated_api_keys.clear
243
+ @web_socket_switches_statuses.clear
244
+
245
+ [@web_socket_ping_thread, @web_socket_thread].each do |thread|
246
+ next unless thread
247
+ if Thread.current == thread
248
+ thread[:stop] = true
249
+ else
250
+ thread.kill
251
+ end
252
+ end
253
+
254
+ if @web_socket.present?
255
+ begin
256
+ @web_socket.close if @web_socket.open?
257
+ rescue
258
+ # Ignoring close errors
259
+ end
260
+ end
261
+
262
+ [
263
+ :@last_web_socket_pong_at,
264
+ :@web_socket_ping_interval,
265
+ :@web_socket_ping_thread,
266
+ :@web_socket_thread,
267
+ :@web_socket_url,
268
+ :@web_socket,
269
+ ].each do |variable|
270
+ remove_instance_variable(variable) if instance_variable_defined?(variable)
271
+ end
272
+ end
273
+
182
274
  def find_rf_bridge_button!(uuid)
183
275
  rf_bridge_buttons.find { |button| button[:uuid] == uuid } || raise(Error.new("No such RF bridge button with UUID: #{uuid.inspect}"))
184
276
  end
@@ -195,29 +287,167 @@ module Ewelink
195
287
  @region ||= DEFAULT_REGION
196
288
  end
197
289
 
198
- def http_request(method, path, options = {})
290
+ def rest_request(method, path, options = {})
199
291
  url = "#{URL.gsub('#{region}', region)}#{path}"
200
292
  method = method.to_s.upcase
201
293
  headers = (options[:headers] || {}).reverse_merge('Content-Type' => 'application/json')
202
294
  Ewelink.logger.debug(self.class.name) { "#{method} #{url}" }
203
- response = HTTParty.send(method.downcase, url, options.merge(headers: headers).reverse_merge(timeout: TIMEOUT))
295
+ response = HTTParty.send(method.downcase, url, options.merge(headers: headers).reverse_merge(timeout: REQUEST_TIMEOUT))
204
296
  raise(Error.new("#{method} #{url}: #{response.code}")) unless response.success?
205
297
  if response['error'] == 301 && response['region'].present?
206
298
  @region = response['region']
207
299
  Ewelink.logger.debug(self.class.name) { "Switched to region #{region.inspect}" }
208
- return http_request(method, path, options)
300
+ return rest_request(method, path, options)
209
301
  end
210
302
  remove_instance_variable(:@authentication_token) if instance_variable_defined?(:@authentication_token) && [401, 403].include?(response['error'])
211
303
  raise(Error.new("#{method} #{url}: #{response['error']} #{response['msg']}".strip)) if response['error'].present? && response['error'] != 0
212
- response
304
+ response.to_h
213
305
  rescue Errno::ECONNREFUSED, OpenSSL::OpenSSLError, SocketError, Timeout::Error => e
214
306
  raise Error.new(e)
215
307
  end
216
308
 
309
+ def send_to_web_socket(message)
310
+ if web_socket_outdated_ping?
311
+ Ewelink.logger.warn(self.class.name) { 'WebSocket ping is outdated' }
312
+ dispose_web_socket
313
+ end
314
+ web_socket.send(message)
315
+ rescue => e
316
+ dispose_web_socket
317
+ raise Error.new(e)
318
+ end
319
+
320
+ def start_web_socket_ping_thread(interval)
321
+ @last_web_socket_pong_at = Time.now
322
+ @web_socket_ping_interval = interval
323
+ Ewelink.logger.debug(self.class.name) { "Creating thread for WebSocket ping every #{@web_socket_ping_interval} seconds" }
324
+ @web_socket_ping_thread = Thread.new do
325
+ loop do
326
+ break if Thread.current[:stop]
327
+ sleep(@web_socket_ping_interval)
328
+ Ewelink.logger.debug(self.class.name) { 'Sending WebSocket ping' }
329
+ send_to_web_socket('ping')
330
+ end
331
+ end
332
+ end
333
+
217
334
  def synchronize(name, &block)
218
335
  (@mutexs[name] ||= Mutex.new).synchronize(&block)
219
336
  end
220
337
 
338
+ def web_socket
339
+ synchronize(:web_socket) do
340
+ next @web_socket if @web_socket
341
+
342
+ @web_socket_thread = Thread.new do
343
+ EventMachine.run do
344
+ Ewelink.logger.debug(self.class.name) { "Opening WebSocket to #{web_socket_url.inspect}" }
345
+
346
+ @web_socket = Faye::WebSocket::Client.new('wss://as-pconnect3.coolkit.cc:8080/api/ws')
347
+
348
+ @web_socket.on(:close) do |event|
349
+ Ewelink.logger.debug(self.class.name) { 'WebSocket closed' }
350
+ dispose_web_socket
351
+ end
352
+
353
+ @web_socket.on(:open) do |event|
354
+ Ewelink.logger.debug(self.class.name) { 'WebSocket opened' }
355
+ @last_web_socket_pong_at = Time.now
356
+ authenticate_web_socket_api_keys
357
+ end
358
+
359
+ @web_socket.on(:message) do |event|
360
+ message = event.data
361
+
362
+ if message == 'pong'
363
+ Ewelink.logger.debug(self.class.name) { "Received WebSocket #{message.inspect} message" }
364
+ @last_web_socket_pong_at = Time.now
365
+ next
366
+ end
367
+
368
+ begin
369
+ json = JSON.parse(message)
370
+ rescue => e
371
+ Ewelink.logger.error(self.class.name) { 'WebSocket JSON parse error' }
372
+ next
373
+ end
374
+
375
+ if json.key?('error') && json['error'] != 0
376
+ Ewelink.logger.error(self.class.name) { "WebSocket message error: #{message.inspect}" }
377
+ next
378
+ end
379
+
380
+ if !@web_socket_ping_thread && json.key?('config') && json['config']['hb'] == 1 && json['config']['hbInterval'].present?
381
+ start_web_socket_ping_thread(json['config']['hbInterval'] + 7)
382
+ end
383
+
384
+ if json['apikey'].present? && !@web_socket_authenticated_api_keys.include?(json['apikey'])
385
+ @web_socket_authenticated_api_keys << json['apikey']
386
+ Ewelink.logger.debug(self.class.name) { "WebSocket successfully authenticated API key: #{json['apikey'].truncate(16).inspect}" }
387
+ end
388
+
389
+ if json['deviceid'].present? && json['params'].is_a?(Hash) && json['params']['switch'].present?
390
+ switch = switches.find { |switch| switch[:device_id] == json['deviceid'] }
391
+ if switch.present?
392
+ @web_socket_switches_statuses[switch[:uuid]] = json['params']['switch']
393
+ Ewelink.logger.debug(self.class.name) { "Switch #{switch[:uuid].inspect} is #{@web_socket_switches_statuses[switch[:uuid]]}" }
394
+ end
395
+ end
396
+ end
397
+ end
398
+ end
399
+
400
+ Timeout.timeout(REQUEST_TIMEOUT) do
401
+ while @web_socket.blank?
402
+ sleep(WEB_SOCKET_WAIT_INTERVAL)
403
+ end
404
+ end
405
+
406
+ @web_socket
407
+ end
408
+ end
409
+
410
+ def web_socket_authenticated?
411
+ api_keys == @web_socket_authenticated_api_keys
412
+ end
413
+
414
+ def web_socket_outdated_ping?
415
+ @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
416
+ end
417
+
418
+ def web_socket_sequence
419
+ (Time.now.to_f * 1000).round.to_s
420
+ end
421
+
422
+ def web_socket_url
423
+ synchronize(:web_socket_url) do
424
+ @web_socket_url ||= begin
425
+ params = {
426
+ 'accept' => 'ws',
427
+ 'appid' => APP_ID,
428
+ 'nonce' => nonce,
429
+ 'ts' => Time.now.to_i,
430
+ 'version' => VERSION,
431
+ }
432
+ response = rest_request(:post, '/dispatch/app', body: JSON.generate(params), headers: authentication_headers)
433
+ raise('Error while getting WebSocket URL') unless response['error'] == 0
434
+ domain = response['domain'].presence || raise("Can't get WebSocket server domain")
435
+ port = response['port'].presence || raise("Can't get WebSocket server port")
436
+ "wss://#{domain}:#{port}/api/ws".tap { |url| Ewelink.logger.debug(self.class.name) { "WebSocket URL is: #{url.inspect}" } }
437
+ end
438
+ end
439
+ end
440
+
441
+ def web_socket_wait_for(condition, &block)
442
+ web_socket # Initializes WebSocket
443
+ Timeout.timeout(REQUEST_TIMEOUT) do
444
+ while !condition.call
445
+ sleep(WEB_SOCKET_WAIT_INTERVAL)
446
+ end
447
+ block_given? ? yield : true
448
+ end
449
+ end
450
+
221
451
  end
222
452
 
223
453
  end
@@ -9,13 +9,14 @@ module Ewelink
9
9
  options[:turn_switches_on_uuids].each { |uuid| api.turn_switch!(uuid, :on) }
10
10
  options[:turn_switches_off_uuids].each { |uuid| api.turn_switch!(uuid, :off) }
11
11
  options[:press_rf_bridge_buttons_uuids].each { |uuid| api.press_rf_bridge_button!(uuid) }
12
+ puts(JSON.pretty_generate(options[:switch_status_uuids].map { |uuid| [uuid, api.switch_on?(uuid) ? 'on' : 'off'] }.to_h)) if options[:switch_status_uuids].present?
12
13
  end
13
14
 
14
15
  private
15
16
 
16
17
  def options
17
18
  @options ||= begin
18
- options = { press_rf_bridge_buttons_uuids: [], turn_switches_off_uuids: [], turn_switches_on_uuids: [] }
19
+ options = { press_rf_bridge_buttons_uuids: [], turn_switches_off_uuids: [], turn_switches_on_uuids: [], switch_status_uuids: [] }
19
20
  parser = OptionParser.new do |opts|
20
21
  opts.banner = 'Manage eWeLink smart home devices'
21
22
  opts.version = File.read(File.expand_path('../../VERSION', __dir__)).strip
@@ -46,6 +47,9 @@ module Ewelink
46
47
  opts.on('--press-rf-bridge-button BUTTON_UUID', 'Press RF 433MHz bridge button with specified UUID') do |uuid|
47
48
  options[:press_rf_bridge_buttons_uuids] << uuid
48
49
  end
50
+ opts.on('--switch-status SWITCH_UUID', 'Displays switch status of specified UUID') do |uuid|
51
+ options[:switch_status_uuids] << uuid
52
+ end
49
53
  opts.on('-v', '--verbose', 'Verbose mode') do
50
54
  Ewelink.logger.level = :debug
51
55
  end
@@ -61,7 +65,7 @@ module Ewelink
61
65
  STDERR.puts(parser.summarize)
62
66
  exit(1)
63
67
  end
64
- if [:list_switches, :list_rf_bridge_buttons, :turn_switches_on_uuids, :turn_switches_off_uuids, :press_rf_bridge_buttons_uuids].map { |action| options[action] }.all?(&:blank?)
68
+ if [:list_switches, :list_rf_bridge_buttons, :turn_switches_on_uuids, :turn_switches_off_uuids, :press_rf_bridge_buttons_uuids, :switch_status_uuids].map { |action| options[action] }.all?(&:blank?)
65
69
  STDERR.puts('An action must be specified (listing switches, press RF bridge button, etc.)')
66
70
  STDERR.puts(parser.summarize)
67
71
  exit(1)
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: 1.2.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexis Toulotte
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-04-18 00:00:00.000000000 Z
11
+ date: 2020-09-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -30,6 +30,26 @@ 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
33
53
  - !ruby/object:Gem::Dependency
34
54
  name: httparty
35
55
  requirement: !ruby/object:Gem::Requirement
@@ -109,7 +129,7 @@ homepage: https://github.com/alexistoulotte/ewelink
109
129
  licenses:
110
130
  - MIT
111
131
  metadata: {}
112
- post_install_message:
132
+ post_install_message:
113
133
  rdoc_options: []
114
134
  require_paths:
115
135
  - lib
@@ -125,7 +145,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
125
145
  version: '0'
126
146
  requirements: []
127
147
  rubygems_version: 3.0.3
128
- signing_key:
148
+ signing_key:
129
149
  specification_version: 4
130
150
  summary: Manage eWeLink devices
131
151
  test_files: []