ewelink 1.2.0 → 2.2.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: 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: []