ewelink 2.1.0 → 2.3.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: 2c58323e838b9fbbdca7c7ee0fdf22d1e7950c627fb4fcf8851b3263e69ce883
4
- data.tar.gz: 475d05f731b079f7734fde4dd93ec8f151077d6e5b716a86e611f11bdbb8d49b
3
+ metadata.gz: d17e40d48a819572e83369500edbe6295a991626c3333ce4ff0765876f25ed51
4
+ data.tar.gz: 9caca646a1355592a276e5e4595ab486e407ecaecb105a9e7134028ecab4cb42
5
5
  SHA512:
6
- metadata.gz: 50ba5474e9012f219e380c47515d599da29a72b50cc26c36a19e253623c997e429a32c1b279619eef0e2d8b7f454fccde519500f998575c38d028ac86e6c1ba4
7
- data.tar.gz: 122f2997897893b395cd04204e3e169d90bac15fc1a9eaabbf31def8da1c7f754bd502341411727364a79011379791a89031e9d42090d22bf96be233a24a5958
6
+ metadata.gz: 6e8a2717428e2b57c7a30a7d4e9a1658032b1155a2f9bd2ba9b8b0cb2309748d248f24d66babfbe2212af9d8096e6be18ebdc3c60d799654571526edff1204d5
7
+ data.tar.gz: 9b9e517a3c8629d6a69b8195cab906d12f8f4d4231ab22311083deb75e5db97b02ec9c13a8509710017bb17cfcf1750c46919f52b393b73a0d453c423f120153
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.1.0
1
+ 2.3.0
@@ -16,8 +16,8 @@ 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
- s.add_dependency 'websocket-client-simple', '>= 0.3.0', '< 0.4.0'
21
21
 
22
22
  s.add_development_dependency 'byebug', '>= 11.0.0', '< 12.0.0'
23
23
  s.add_development_dependency 'rake', '>= 12.0.0', '< 13.0.0'
@@ -1,6 +1,8 @@
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'
@@ -9,7 +11,6 @@ require 'openssl'
9
11
  require 'optparse'
10
12
  require 'set'
11
13
  require 'timeout'
12
- require 'websocket-client-simple'
13
14
 
14
15
  module Ewelink
15
16
 
@@ -5,13 +5,15 @@ 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.seconds
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_CHECK_AUTHENTICATION_TIMEOUT = 30.seconds
14
15
  WEB_SOCKET_PING_TOLERANCE_FACTOR = 1.5
16
+ SWITCH_STATUS_CHANGE_CHECK_TIMEOUT = 2.seconds
15
17
  WEB_SOCKET_WAIT_INTERVAL = 0.2.seconds
16
18
 
17
19
  attr_reader :email, :password, :phone_number
@@ -21,15 +23,18 @@ module Ewelink
21
23
  @mutexs = {}
22
24
  @password = password.presence || raise(Error.new(":password must be specified"))
23
25
  @phone_number = phone_number.presence.try(:strip)
24
- @switches_statuses = {}
25
26
  @web_socket_authenticated_api_keys = Set.new
26
- raise(Error.new(":email or :phone_number must be specified")) if email.blank? && phone_number.blank?
27
+ @web_socket_switches_statuses = {}
28
+
29
+ raise(Error.new(':email or :phone_number must be specified')) if email.blank? && phone_number.blank?
30
+
31
+ start_web_socket_authentication_check_thread
27
32
  end
28
33
 
29
34
  def press_rf_bridge_button!(uuid)
30
35
  synchronize(:press_rf_bridge_button) do
31
36
  button = find_rf_bridge_button!(uuid)
32
- web_socket_wait_for(-> { web_socket_authenticated? }) do
37
+ web_socket_wait_for(-> { web_socket_authenticated? }, initialize_web_socket: true) do
33
38
  params = {
34
39
  'action' => 'update',
35
40
  'apikey' => button[:api_key],
@@ -51,15 +56,40 @@ module Ewelink
51
56
 
52
57
  def reload
53
58
  Ewelink.logger.debug(self.class.name) { 'Reloading API (authentication token, devices, region, connections,...)' }
54
- dispose_web_socket
55
- @switches_statuses.clear
59
+
60
+ @web_socket_authenticated_api_keys.clear
61
+ @web_socket_switches_statuses.clear
62
+
63
+ [@web_socket_ping_thread, @web_socket_thread].each do |thread|
64
+ next unless thread
65
+ if Thread.current == thread
66
+ thread[:stop] = true
67
+ else
68
+ thread.kill
69
+ end
70
+ end
71
+
72
+ if @web_socket.present?
73
+ begin
74
+ @web_socket.close if @web_socket.open?
75
+ rescue
76
+ # Ignoring close errors
77
+ end
78
+ end
79
+
56
80
  [
57
81
  :@api_keys,
58
82
  :@authentication_token,
59
83
  :@devices,
84
+ :@last_web_socket_pong_at,
60
85
  :@region,
61
86
  :@rf_bridge_buttons,
62
87
  :@switches,
88
+ :@web_socket_ping_interval,
89
+ :@web_socket_ping_thread,
90
+ :@web_socket_thread,
91
+ :@web_socket_url,
92
+ :@web_socket,
63
93
  ].each do |variable|
64
94
  remove_instance_variable(variable) if instance_variable_defined?(variable)
65
95
  end
@@ -102,23 +132,22 @@ module Ewelink
102
132
 
103
133
  def switch_on?(uuid)
104
134
  switch = find_switch!(uuid)
105
- if @switches_statuses[switch[:uuid]].nil?
106
- params = {
107
- 'action' => 'query',
108
- 'apikey' => switch[:api_key],
109
- 'deviceid' => switch[:device_id],
110
- 'sequence' => web_socket_sequence,
111
- 'ts' => 0,
112
- 'userAgent' => 'app',
113
- }
114
- web_socket_wait_for(-> { web_socket_authenticated? }) do
135
+ if @web_socket_switches_statuses[switch[:uuid]].nil?
136
+ web_socket_wait_for(-> { web_socket_authenticated? }, initialize_web_socket: true) do
115
137
  Ewelink.logger.debug(self.class.name) { "Checking switch #{switch[:uuid].inspect} status" }
138
+ params = {
139
+ 'action' => 'query',
140
+ 'apikey' => switch[:api_key],
141
+ 'deviceid' => switch[:device_id],
142
+ 'sequence' => web_socket_sequence,
143
+ 'ts' => 0,
144
+ 'userAgent' => 'app',
145
+ }
116
146
  send_to_web_socket(JSON.generate(params))
117
147
  end
118
148
  end
119
- web_socket_wait_for(-> { !@switches_statuses[switch[:uuid]].nil? }) do
120
- Ewelink.logger.debug(self.class.name) { "Switch #{switch[:uuid].inspect} is #{@switches_statuses[switch[:uuid]]}" }
121
- @switches_statuses[switch[:uuid]] == 'on'
149
+ web_socket_wait_for(-> { !@web_socket_switches_statuses[switch[:uuid]].nil? }, initialize_web_socket: true) do
150
+ @web_socket_switches_statuses[switch[:uuid]] == 'on'
122
151
  end
123
152
  end
124
153
 
@@ -149,8 +178,8 @@ module Ewelink
149
178
  on = false
150
179
  end
151
180
  switch = find_switch!(uuid)
152
- @switches_statuses[switch[:uuid]] = nil
153
- web_socket_wait_for(-> { web_socket_authenticated? }) do
181
+ @web_socket_switches_statuses[switch[:uuid]] = nil
182
+ web_socket_wait_for(-> { web_socket_authenticated? }, initialize_web_socket: true) do
154
183
  params = {
155
184
  'action' => 'update',
156
185
  'apikey' => switch[:api_key],
@@ -165,6 +194,7 @@ module Ewelink
165
194
  Ewelink.logger.debug(self.class.name) { "Turning switch #{switch[:uuid].inspect} #{on ? 'on' : 'off'}" }
166
195
  send_to_web_socket(JSON.generate(params))
167
196
  end
197
+ sleep(SWITCH_STATUS_CHANGE_CHECK_TIMEOUT)
168
198
  switch_on?(switch[:uuid]) # Waiting for switch status update
169
199
  true
170
200
  end
@@ -177,6 +207,24 @@ module Ewelink
177
207
  end
178
208
  end
179
209
 
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
226
+ end
227
+
180
228
  def authentication_headers
181
229
  { 'Authorization' => "Bearer #{authentication_token}" }
182
230
  end
@@ -221,36 +269,6 @@ module Ewelink
221
269
  end
222
270
  end
223
271
 
224
- def dispose_web_socket
225
- @web_socket_authenticated_api_keys = Set.new
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
272
  def find_rf_bridge_button!(uuid)
255
273
  rf_bridge_buttons.find { |button| button[:uuid] == uuid } || raise(Error.new("No such RF bridge button with UUID: #{uuid.inspect}"))
256
274
  end
@@ -272,7 +290,7 @@ module Ewelink
272
290
  method = method.to_s.upcase
273
291
  headers = (options[:headers] || {}).reverse_merge('Content-Type' => 'application/json')
274
292
  Ewelink.logger.debug(self.class.name) { "#{method} #{url}" }
275
- response = HTTParty.send(method.downcase, url, options.merge(headers: headers).reverse_merge(timeout: TIMEOUT))
293
+ response = HTTParty.send(method.downcase, url, options.merge(headers: headers).reverse_merge(timeout: REQUEST_TIMEOUT))
276
294
  raise(Error.new("#{method} #{url}: #{response.code}")) unless response.success?
277
295
  if response['error'] == 301 && response['region'].present?
278
296
  @region = response['region']
@@ -286,15 +304,43 @@ module Ewelink
286
304
  raise Error.new(e)
287
305
  end
288
306
 
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
307
+ def send_to_web_socket(message)
308
+ web_socket.send(message)
309
+ rescue => e
310
+ reload
311
+ raise Error.new(e)
312
+ end
313
+
314
+ def start_web_socket_authentication_check_thread
315
+ raise Error.new('WebSocket authentication check must only be started once') if @web_socket_authentication_check_thread.present?
316
+
317
+ @web_socket_authentication_check_thread = Thread.new do
318
+ loop do
319
+ Ewelink.logger.debug(self.class.name) { 'Checking if WebSocket is authenticated' }
320
+ begin
321
+ web_socket_wait_for(-> { web_socket_authenticated? }, initialize_web_socket: true) do
322
+ Ewelink.logger.debug(self.class.name) { 'WebSocket is authenticated' }
323
+ end
324
+ rescue => e
325
+ Ewelink.logger.error(self.class.name) { e }
326
+ end
327
+ sleep(WEB_SOCKET_CHECK_AUTHENTICATION_TIMEOUT)
328
+ end
329
+ end
330
+ end
331
+
332
+ def start_web_socket_ping_thread(interval)
333
+ @last_web_socket_pong_at = Time.now
334
+ @web_socket_ping_interval = interval
335
+ Ewelink.logger.debug(self.class.name) { "Creating thread for WebSocket ping every #{@web_socket_ping_interval} seconds" }
336
+ @web_socket_ping_thread = Thread.new do
337
+ loop do
338
+ break if Thread.current[:stop]
339
+ sleep(@web_socket_ping_interval)
340
+ Ewelink.logger.debug(self.class.name) { 'Sending WebSocket ping' }
341
+ send_to_web_socket('ping')
342
+ end
293
343
  end
294
- web_socket.send(data)
295
- rescue
296
- dispose_web_socket
297
- raise
298
344
  end
299
345
 
300
346
  def synchronize(name, &block)
@@ -302,96 +348,82 @@ module Ewelink
302
348
  end
303
349
 
304
350
  def web_socket
351
+ if web_socket_outdated_ping?
352
+ Ewelink.logger.warn(self.class.name) { 'WebSocket ping is outdated' }
353
+ reload
354
+ end
355
+
305
356
  synchronize(:web_socket) do
306
- @web_socket ||= begin
307
- api = self
357
+ next @web_socket if @web_socket
308
358
 
309
- WebSocket::Client::Simple.connect(web_socket_url) do |web_socket|
310
- Ewelink.logger.debug(self.class.name) { "Opening WebSocket to #{web_socket_url}" }
359
+ # Initializes caches before opening WebSocket: important in order to
360
+ # NOT cumulate requests Timeouts from #web_socket_wait_for.
361
+ api_keys
362
+ web_socket_url
311
363
 
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
364
+ Ewelink.logger.debug(self.class.name) { "Opening WebSocket to #{web_socket_url.inspect}" }
365
+
366
+ @web_socket_thread = Thread.new do
367
+ EventMachine.run do
368
+ @web_socket = Faye::WebSocket::Client.new(web_socket_url)
369
+
370
+ @web_socket.on(:close) do |event|
371
+ Ewelink.logger.debug(self.class.name) { 'WebSocket closed' }
372
+ reload
317
373
  end
318
374
 
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
375
+ @web_socket.on(:open) do |event|
376
+ Ewelink.logger.debug(self.class.name) { 'WebSocket opened' }
377
+ @last_web_socket_pong_at = Time.now
378
+ authenticate_web_socket_api_keys
324
379
  end
325
380
 
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
381
+ @web_socket.on(:message) do |event|
382
+ message = event.data
333
383
 
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
384
+ if message == 'pong'
385
+ Ewelink.logger.debug(self.class.name) { "Received WebSocket #{message.inspect} message" }
386
+ @last_web_socket_pong_at = Time.now
387
+ next
388
+ end
340
389
 
341
- if response.key?('error') && response['error'] != 0
342
- Ewelink.logger.error(self.class.name) { "WebSocket message error: #{message.data}" }
343
- next
344
- end
390
+ begin
391
+ json = JSON.parse(message)
392
+ rescue => e
393
+ Ewelink.logger.error(self.class.name) { 'WebSocket JSON parse error' }
394
+ reload
395
+ next
396
+ end
345
397
 
346
- if !@web_socket_ping_thread && response.key?('config') && response['config'].key?('hbInterval')
347
- @last_web_socket_pong_at = Time.now
348
- # @web_socket_ping_interval = response['config']['hbInterval']
349
- @web_socket_ping_interval = 30.seconds
350
- Ewelink.logger.debug(self.class.name) { "Creating thread for WebSocket ping every #{@web_socket_ping_interval} seconds" }
351
- @web_socket_ping_thread = Thread.new do
352
- loop do
353
- break if Thread.current[:stop]
354
- sleep(@web_socket_ping_interval)
355
- Ewelink.logger.debug(self.class.name) { 'Sending WebSocket ping' }
356
- send_to_web_socket('ping')
357
- end
358
- end
359
- end
398
+ if json.key?('error') && json['error'] != 0
399
+ Ewelink.logger.error(self.class.name) { "WebSocket message error: #{message.inspect}" }
400
+ reload
401
+ next
402
+ end
360
403
 
361
- if response['apikey'].present? && !@web_socket_authenticated_api_keys.include?(response['apikey'])
362
- @web_socket_authenticated_api_keys << response['apikey']
363
- Ewelink.logger.debug(self.class.name) { "WebSocket successfully authenticated API key: #{response['apikey'].truncate(16).inspect}" }
364
- end
404
+ if !@web_socket_ping_thread && json.key?('config') && json['config']['hb'] == 1 && json['config']['hbInterval'].present?
405
+ start_web_socket_ping_thread(json['config']['hbInterval'] + 7)
406
+ end
365
407
 
366
- if response['deviceid'].present? && response['params'].is_a?(Hash) && response['params']['switch'].present?
367
- switch = switches.find { |switch| switch[:device_id] == response['deviceid'] }
368
- @switches_statuses[switch[:uuid]] = response['params']['switch'] if switch.present?
369
- end
408
+ if json['apikey'].present? && !@web_socket_authenticated_api_keys.include?(json['apikey'])
409
+ @web_socket_authenticated_api_keys << json['apikey']
410
+ Ewelink.logger.debug(self.class.name) { "WebSocket successfully authenticated API key: #{json['apikey'].truncate(16).inspect}" }
370
411
  end
371
- end
372
412
 
373
- web_socket.on(:open) do
374
- api.instance_eval do
375
- Ewelink.logger.debug(self.class.name) { 'WebSocket opened' }
376
- api_keys.each do |api_key|
377
- params = {
378
- 'action' => 'userOnline',
379
- 'apikey' => api_key,
380
- 'appid' => APP_ID,
381
- 'at' => authentication_token,
382
- 'nonce' => nonce,
383
- 'sequence' => web_socket_sequence,
384
- 'ts' => Time.now.to_i,
385
- 'userAgent' => 'app',
386
- 'version' => VERSION,
387
- }
388
- Ewelink.logger.debug(self.class.name) { "Authenticating WebSocket API key: #{api_key.truncate(16).inspect}" }
389
- send_to_web_socket(JSON.generate(params))
413
+ if json['deviceid'].present? && json['params'].is_a?(Hash) && json['params']['switch'].present?
414
+ switch = switches.find { |switch| switch[:device_id] == json['deviceid'] }
415
+ if switch.present?
416
+ @web_socket_switches_statuses[switch[:uuid]] = json['params']['switch']
417
+ Ewelink.logger.debug(self.class.name) { "Switch #{switch[:uuid].inspect} is #{@web_socket_switches_statuses[switch[:uuid]]}" }
390
418
  end
391
419
  end
392
420
  end
393
421
  end
394
422
  end
423
+
424
+ web_socket_wait_for(-> { @web_socket.present? }) do
425
+ @web_socket
426
+ end
395
427
  end
396
428
  end
397
429
 
@@ -426,16 +458,18 @@ module Ewelink
426
458
  end
427
459
  end
428
460
 
429
- def web_socket_wait_for(condition, &block)
430
- web_socket # Initializes WebSocket
431
- Timeout.timeout(TIMEOUT) do
432
- loop do
433
- if condition.call
434
- return yield if block_given?
435
- return true
461
+ def web_socket_wait_for(condition, initialize_web_socket: false, &block)
462
+ web_socket if initialize_web_socket
463
+ begin
464
+ Timeout.timeout(REQUEST_TIMEOUT) do
465
+ while !condition.call
466
+ sleep(WEB_SOCKET_WAIT_INTERVAL)
436
467
  end
437
- sleep(WEB_SOCKET_WAIT_INTERVAL)
468
+ block_given? ? yield : true
438
469
  end
470
+ rescue => e
471
+ reload
472
+ raise Error.new(e)
439
473
  end
440
474
  end
441
475
 
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.0
4
+ version: 2.3.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-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -31,45 +31,45 @@ dependencies:
31
31
  - !ruby/object:Gem::Version
32
32
  version: 7.0.0
33
33
  - !ruby/object:Gem::Dependency
34
- name: httparty
34
+ name: faye-websocket
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: 0.18.0
39
+ version: 0.11.0
40
40
  - - "<"
41
41
  - !ruby/object:Gem::Version
42
- version: 0.19.0
42
+ version: 0.12.0
43
43
  type: :runtime
44
44
  prerelease: false
45
45
  version_requirements: !ruby/object:Gem::Requirement
46
46
  requirements:
47
47
  - - ">="
48
48
  - !ruby/object:Gem::Version
49
- version: 0.18.0
49
+ version: 0.11.0
50
50
  - - "<"
51
51
  - !ruby/object:Gem::Version
52
- version: 0.19.0
52
+ version: 0.12.0
53
53
  - !ruby/object:Gem::Dependency
54
- name: websocket-client-simple
54
+ name: httparty
55
55
  requirement: !ruby/object:Gem::Requirement
56
56
  requirements:
57
57
  - - ">="
58
58
  - !ruby/object:Gem::Version
59
- version: 0.3.0
59
+ version: 0.18.0
60
60
  - - "<"
61
61
  - !ruby/object:Gem::Version
62
- version: 0.4.0
62
+ version: 0.19.0
63
63
  type: :runtime
64
64
  prerelease: false
65
65
  version_requirements: !ruby/object:Gem::Requirement
66
66
  requirements:
67
67
  - - ">="
68
68
  - !ruby/object:Gem::Version
69
- version: 0.3.0
69
+ version: 0.18.0
70
70
  - - "<"
71
71
  - !ruby/object:Gem::Version
72
- version: 0.4.0
72
+ version: 0.19.0
73
73
  - !ruby/object:Gem::Dependency
74
74
  name: byebug
75
75
  requirement: !ruby/object:Gem::Requirement