ewelink 1.1.0 → 2.1.1

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: aa2540d5e8dcf125b91c086731711b772516e28466e33b2bb668bd143abb5fe7
4
- data.tar.gz: caf1bfa5d91d0edefc143f49230b0592aafecbbb40e7e80a7d5279f5c3685e0d
3
+ metadata.gz: bb352abf05d6bb6ef2f3b277c6f551d436bed969086478d3e121fd2e178fe4f9
4
+ data.tar.gz: b74266f2cbc92457a3bb0dc087752299a5a9f8348d3928a79d8c3c0719653e6a
5
5
  SHA512:
6
- metadata.gz: c09430a4c2f0a31d62c20828f8e0591cff4f808e9660ae3d309c5fd67edde27aa750ba2d864b1c2d0ad99268b32cb8b05837c407afa268a74ae4d6254aac3859
7
- data.tar.gz: 2a5d636f6a571ece7e7cf83cac4eae90b9457212e6260b946c1172dd773e1a438e4ecdf31dee531ada59810440fa4e7a1bfa0af996c974c133e52f61a644638d
6
+ metadata.gz: 2c5bb695c7d0094436454694008dd3c05c660fb1c7dfe2436ec1fa2fae3341559f901f3b9ae4ebfa99fcc276dcd4ceee02205ed1608d03e515b8da64b52f8a25
7
+ data.tar.gz: 4713a948c736a40496b3b33fff3b83253a4559693d692cfaa7251a4fc2d3f0e67699834f6513226edcbfe6cf7759ab54b1cb6c5a24cbe1559f54a75d6cbda6fd
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.0
1
+ 2.1.1
@@ -17,6 +17,7 @@ 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'
20
21
 
21
22
  s.add_development_dependency 'byebug', '>= 11.0.0', '< 12.0.0'
22
23
  s.add_development_dependency 'rake', '>= 12.0.0', '< 13.0.0'
@@ -7,6 +7,9 @@ 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'
10
13
 
11
14
  module Ewelink
12
15
 
@@ -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,91 +21,124 @@ 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
- button = find_rf_bridge_button!(uuid)
27
- params = {
28
- 'appid' => APP_ID,
29
- 'deviceid' => button[:device_id],
30
- 'nonce' => nonce,
31
- 'params' => {
32
- 'cmd' => 'transmit',
33
- 'rfChl' => button[:channel],
34
- },
35
- 'ts' => Time.now.to_i,
36
- 'version' => VERSION,
37
- }
38
- http_request(:post, '/api/user/device/status', body: JSON.generate(params), headers: authentication_headers)
39
- true
30
+ synchronize(:press_rf_bridge_button) do
31
+ 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
49
+ end
40
50
  end
41
51
 
42
52
  def reload
43
- Ewelink.logger.debug(self.class.name) { 'Reloading API (authentication token, devices & region cache)' }
44
- [:@authentication_token, :@devices, :@rf_bridge_buttons, :@region, :@switches].each do |variable|
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
63
  remove_instance_variable(variable) if instance_variable_defined?(variable)
46
64
  end
47
65
  self
48
66
  end
49
67
 
50
68
  def rf_bridge_buttons
51
- @rf_bridge_buttons ||= [].tap do |buttons|
52
- rf_bridge_devices = devices.select { |device| device['uiid'] == RF_BRIDGE_DEVICE_UIID }.tap do |devices|
53
- Ewelink.logger.debug(self.class.name) { "Found #{devices.size} RF 433MHz Bridge device(s)" }
54
- end
55
- rf_bridge_devices.each do |device|
56
- device_id = device['deviceid'].presence || next
57
- device_name = device['name'].presence || next
58
- buttons = device['params']['rfList'].each do |rf|
59
- button = {
60
- channel: rf['rfChl'],
61
- device_id: device_id,
62
- device_name: device_name,
63
- }
64
- remote_info = device['tags']['zyx_info'].find { |info| info['buttonName'].find { |data| data.key?(button[:channel].to_s) } }.presence || next
65
- remote_name = remote_info['name'].try(:squish).presence || next
66
- button_info = remote_info['buttonName'].find { |info| info.key?(button[:channel].to_s) }.presence || next
67
- button_name = button_info.values.first.try(:squish).presence || next
68
- button.merge!({
69
- name: button_name,
70
- remote_name: remote_name,
71
- remote_type: remote_info['remote_type'],
72
- })
73
- button[:uuid] = Digest::UUID.uuid_v5(UUID_NAMESPACE, "#{button[:device_id]}/#{button[:channel]}")
74
- buttons << button
69
+ synchronize(:rf_bridge_buttons) do
70
+ @rf_bridge_buttons ||= [].tap do |buttons|
71
+ rf_bridge_devices = devices.select { |device| device['uiid'] == RF_BRIDGE_DEVICE_UIID }.tap do |devices|
72
+ Ewelink.logger.debug(self.class.name) { "Found #{devices.size} RF 433MHz bridge device(s)" }
75
73
  end
76
- end
77
- end.tap { |buttons| Ewelink.logger.debug(self.class.name) { "Found #{buttons.size} RF 433MHz bridge button(s)" } }
74
+ rf_bridge_devices.each do |device|
75
+ api_key = device['apikey'].presence || next
76
+ device_id = device['deviceid'].presence || next
77
+ device_name = device['name'].presence || next
78
+ buttons = device['params']['rfList'].each do |rf|
79
+ button = {
80
+ api_key: api_key,
81
+ channel: rf['rfChl'],
82
+ device_id: device_id,
83
+ device_name: device_name,
84
+ }
85
+ remote_info = device['tags']['zyx_info'].find { |info| info['buttonName'].find { |data| data.key?(button[:channel].to_s) } }.presence || next
86
+ remote_name = remote_info['name'].try(:squish).presence || next
87
+ button_info = remote_info['buttonName'].find { |info| info.key?(button[:channel].to_s) }.presence || next
88
+ button_name = button_info.values.first.try(:squish).presence || next
89
+ button.merge!({
90
+ name: button_name,
91
+ remote_name: remote_name,
92
+ remote_type: remote_info['remote_type'],
93
+ })
94
+ button[:uuid] = Digest::UUID.uuid_v5(UUID_NAMESPACE, "#{button[:device_id]}/#{button[:channel]}")
95
+ buttons << button
96
+ end
97
+ end
98
+ end.tap { |buttons| Ewelink.logger.debug(self.class.name) { "Found #{buttons.size} RF 433MHz bridge button(s)" } }
99
+ end
78
100
  end
79
101
 
80
102
  def switch_on?(uuid)
81
103
  switch = find_switch!(uuid)
82
- params = {
83
- 'appid' => APP_ID,
84
- 'deviceid' => switch[:device_id],
85
- 'nonce' => nonce,
86
- 'ts' => Time.now.to_i,
87
- 'version' => VERSION,
88
- }
89
- response = http_request(:get, '/api/user/device/status', headers: authentication_headers, query: params)
90
- response['params']['switch'] == 'on'
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
91
122
  end
92
123
 
93
124
  def switches
94
- @switches ||= [].tap do |switches|
95
- switch_devices = devices.select { |device| SWITCH_DEVICES_UIIDS.include?(device['uiid']) }
96
- switch_devices.each do |device|
97
- device_id = device['deviceid'].presence || next
98
- name = device['name'].presence || next
99
- switch = {
100
- device_id: device_id,
101
- name: name,
102
- }
103
- switch[:uuid] = Digest::UUID.uuid_v5(UUID_NAMESPACE, switch[:device_id])
104
- switches << switch
105
- end
106
- end.tap { |switches| Ewelink.logger.debug(self.class.name) { "Found #{switches.size} switch(es)" } }
125
+ synchronize(:switches) do
126
+ @switches ||= [].tap do |switches|
127
+ switch_devices = devices.select { |device| SWITCH_DEVICES_UIIDS.include?(device['uiid']) }
128
+ switch_devices.each do |device|
129
+ api_key = device['apikey'].presence || next
130
+ device_id = device['deviceid'].presence || next
131
+ name = device['name'].presence || next
132
+ switch = {
133
+ api_key: api_key,
134
+ device_id: device_id,
135
+ name: name,
136
+ }
137
+ switch[:uuid] = Digest::UUID.uuid_v5(UUID_NAMESPACE, switch[:device_id])
138
+ switches << switch
139
+ end
140
+ end.tap { |switches| Ewelink.logger.debug(self.class.name) { "Found #{switches.size} switch(es)" } }
141
+ end
107
142
  end
108
143
 
109
144
  def turn_switch!(uuid, on)
@@ -113,22 +148,34 @@ module Ewelink
113
148
  on = false
114
149
  end
115
150
  switch = find_switch!(uuid)
116
- params = {
117
- 'appid' => APP_ID,
118
- 'deviceid' => switch[:device_id],
119
- 'nonce' => nonce,
120
- 'params' => {
121
- 'switch' => on ? 'on' : 'off',
122
- },
123
- 'ts' => Time.now.to_i,
124
- 'version' => VERSION,
125
- }
126
- http_request(:post, '/api/user/device/status', body: JSON.generate(params), headers: authentication_headers)
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
127
168
  true
128
169
  end
129
170
 
130
171
  private
131
172
 
173
+ def api_keys
174
+ synchronize(:api_keys) do
175
+ @api_keys ||= Set.new(devices.map { |device| device['apikey'] })
176
+ end
177
+ end
178
+
132
179
  def authentication_headers
133
180
  { 'Authorization' => "Bearer #{authentication_token}" }
134
181
  end
@@ -150,7 +197,7 @@ module Ewelink
150
197
  params['phoneNumber'] = phone_number
151
198
  end
152
199
  body = JSON.generate(params)
153
- response = http_request(:post, '/api/user/login', { body: body, headers: { 'Authorization' => "Sign #{Base64.encode64(OpenSSL::HMAC.digest('SHA256', APP_SECRET, body))}" } })
200
+ response = rest_request(:post, '/api/user/login', { body: body, headers: { 'Authorization' => "Sign #{Base64.encode64(OpenSSL::HMAC.digest('SHA256', APP_SECRET, body))}" } })
154
201
  raise(Error.new('Authentication token not found')) if response['at'].blank?
155
202
  response['at'].tap { Ewelink.logger.debug(self.class.name) { 'Authentication token found' } }
156
203
  end
@@ -167,12 +214,43 @@ module Ewelink
167
214
  'ts' => Time.now.to_i,
168
215
  'version' => VERSION,
169
216
  }
170
- response = http_request(:get, '/api/user/device', headers: authentication_headers, query: params)
217
+ response = rest_request(:get, '/api/user/device', headers: authentication_headers, query: params)
171
218
  response['devicelist'].tap { |devices| Ewelink.logger.debug(self.class.name) { "Found #{devices.size} device(s)" } }
172
219
  end
173
220
  end
174
221
  end
175
222
 
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
+
176
254
  def find_rf_bridge_button!(uuid)
177
255
  rf_bridge_buttons.find { |button| button[:uuid] == uuid } || raise(Error.new("No such RF bridge button with UUID: #{uuid.inspect}"))
178
256
  end
@@ -189,29 +267,177 @@ module Ewelink
189
267
  @region ||= DEFAULT_REGION
190
268
  end
191
269
 
192
- def http_request(method, path, options = {})
270
+ def rest_request(method, path, options = {})
193
271
  url = "#{URL.gsub('#{region}', region)}#{path}"
194
272
  method = method.to_s.upcase
195
273
  headers = (options[:headers] || {}).reverse_merge('Content-Type' => 'application/json')
196
274
  Ewelink.logger.debug(self.class.name) { "#{method} #{url}" }
197
- response = synchronize(:http_request) { HTTParty.send(method.downcase, url, options.merge(headers: headers).reverse_merge(timeout: TIMEOUT)) }
275
+ response = HTTParty.send(method.downcase, url, options.merge(headers: headers).reverse_merge(timeout: REQUEST_TIMEOUT))
198
276
  raise(Error.new("#{method} #{url}: #{response.code}")) unless response.success?
199
277
  if response['error'] == 301 && response['region'].present?
200
278
  @region = response['region']
201
279
  Ewelink.logger.debug(self.class.name) { "Switched to region #{region.inspect}" }
202
- return http_request(method, path, options)
280
+ return rest_request(method, path, options)
203
281
  end
204
282
  remove_instance_variable(:@authentication_token) if instance_variable_defined?(:@authentication_token) && [401, 403].include?(response['error'])
205
283
  raise(Error.new("#{method} #{url}: #{response['error']} #{response['msg']}".strip)) if response['error'].present? && response['error'] != 0
206
- response
284
+ response.to_h
207
285
  rescue Errno::ECONNREFUSED, OpenSSL::OpenSSLError, SocketError, Timeout::Error => e
208
286
  raise Error.new(e)
209
287
  end
210
288
 
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
+
211
300
  def synchronize(name, &block)
212
301
  (@mutexs[name] ||= Mutex.new).synchronize(&block)
213
302
  end
214
303
 
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
+
215
441
  end
216
442
 
217
443
  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.1.0
4
+ version: 2.1.1
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-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -50,6 +50,26 @@ 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
53
73
  - !ruby/object:Gem::Dependency
54
74
  name: byebug
55
75
  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: []