ewelink 1.0.1 → 2.1.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: 344143666abe75f11d9d3ccdb49116b6ded1579a609820dac2b26f97de27dcac
4
- data.tar.gz: 05fcfdaa151396125266d0125d3e28124eaae6da5a607a531a4729a937425902
3
+ metadata.gz: 2c58323e838b9fbbdca7c7ee0fdf22d1e7950c627fb4fcf8851b3263e69ce883
4
+ data.tar.gz: 475d05f731b079f7734fde4dd93ec8f151077d6e5b716a86e611f11bdbb8d49b
5
5
  SHA512:
6
- metadata.gz: b83fe71850b9c0b1595df52687082452e3c8e0da75c896027d472fbfd1df2dc0c3688181f242db506b76bb415aee6a8f60055e51c8af8963c7062ce2e2a27996
7
- data.tar.gz: 84577222fc03a3a20cb08eb5a8ad3243635b2694d33b4bee9bb52582bf9cc4ea8e2e404a7315206adf2bbce3e8ffdbb2b12792decf6ad24f17a844d4a24a6126
6
+ metadata.gz: 50ba5474e9012f219e380c47515d599da29a72b50cc26c36a19e253623c997e429a32c1b279619eef0e2d8b7f454fccde519500f998575c38d028ac86e6c1ba4
7
+ data.tar.gz: 122f2997897893b395cd04204e3e169d90bac15fc1a9eaabbf31def8da1c7f754bd502341411727364a79011379791a89031e9d42090d22bf96be233a24a5958
@@ -40,24 +40,30 @@ api.rf_bridge_buttons.each do |button|
40
40
  end
41
41
  ```
42
42
 
43
- ### Set switch on or off
43
+ ### Turn switch on or off
44
44
 
45
45
  ```ruby
46
46
  require 'ewelink'
47
47
 
48
48
  api = Ewelink::Api.new(phone_number: '+687 414243', password: 'secr$t')
49
- api.switch_on!(switch[:uuid])
50
- api.switch_off!(switch[:uuid])
49
+ api.turn_switch!(switch[:uuid], :on)
50
+ api.turn_switch!(switch[:uuid], :off)
51
51
  ```
52
52
 
53
- ### Check if switch is on or off
53
+ Or :
54
+
55
+ ```ruby
56
+ api.turn_switch!(switch[:uuid], true)
57
+ api.turn_switch!(switch[:uuid], false)
58
+ ```
59
+
60
+ ### Check if switch is on
54
61
 
55
62
  ```ruby
56
63
  require 'ewelink'
57
64
 
58
65
  api = Ewelink::Api.new(phone_number: '+687 414243', password: 'secr$t')
59
66
  puts api.switch_on?(switch[:uuid])
60
- puts api.switch_off?(switch[:uuid])
61
67
  ```
62
68
 
63
69
  ### Press RF bridge button
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.1
1
+ 2.1.0
@@ -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
 
@@ -7,10 +7,12 @@ module Ewelink
7
7
  DEFAULT_REGION = 'us'
8
8
  RF_BRIDGE_DEVICE_UIID = 28
9
9
  SWITCH_DEVICES_UIIDS = [1, 5, 6, 24]
10
- TIMEOUT = 10
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_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,107 +21,162 @@ 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
+ @switches_statuses = {}
25
+ @web_socket_authenticated_api_keys = Set.new
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
+ @switches_statuses.clear
56
+ [
57
+ :@api_keys,
58
+ :@authentication_token,
59
+ :@devices,
60
+ :@region,
61
+ :@rf_bridge_buttons,
62
+ :@switches,
63
+ ].each do |variable|
45
64
  remove_instance_variable(variable) if instance_variable_defined?(variable)
46
65
  end
47
66
  self
48
67
  end
49
68
 
50
69
  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
70
+ synchronize(:rf_bridge_buttons) do
71
+ @rf_bridge_buttons ||= [].tap do |buttons|
72
+ rf_bridge_devices = devices.select { |device| device['uiid'] == RF_BRIDGE_DEVICE_UIID }.tap do |devices|
73
+ Ewelink.logger.debug(self.class.name) { "Found #{devices.size} RF 433MHz bridge device(s)" }
75
74
  end
76
- end
77
- end.tap { |buttons| Ewelink.logger.debug(self.class.name) { "Found #{buttons.size} RF 433MHz bridge button(s)" } }
78
- end
79
-
80
- def switch_off!(uuid)
81
- update_switch_on!(uuid, false)
82
- end
83
-
84
- def switch_off?(uuid)
85
- !switch_on?(uuid)
86
- end
87
-
88
- def switch_on!(uuid)
89
- update_switch_on!(uuid, true)
75
+ rf_bridge_devices.each do |device|
76
+ api_key = device['apikey'].presence || next
77
+ device_id = device['deviceid'].presence || next
78
+ device_name = device['name'].presence || next
79
+ buttons = device['params']['rfList'].each do |rf|
80
+ button = {
81
+ api_key: api_key,
82
+ channel: rf['rfChl'],
83
+ device_id: device_id,
84
+ device_name: device_name,
85
+ }
86
+ remote_info = device['tags']['zyx_info'].find { |info| info['buttonName'].find { |data| data.key?(button[:channel].to_s) } }.presence || next
87
+ remote_name = remote_info['name'].try(:squish).presence || next
88
+ button_info = remote_info['buttonName'].find { |info| info.key?(button[:channel].to_s) }.presence || next
89
+ button_name = button_info.values.first.try(:squish).presence || next
90
+ button.merge!({
91
+ name: button_name,
92
+ remote_name: remote_name,
93
+ remote_type: remote_info['remote_type'],
94
+ })
95
+ button[:uuid] = Digest::UUID.uuid_v5(UUID_NAMESPACE, "#{button[:device_id]}/#{button[:channel]}")
96
+ buttons << button
97
+ end
98
+ end
99
+ end.tap { |buttons| Ewelink.logger.debug(self.class.name) { "Found #{buttons.size} RF 433MHz bridge button(s)" } }
100
+ end
90
101
  end
91
102
 
92
103
  def switch_on?(uuid)
93
104
  switch = find_switch!(uuid)
94
- params = {
95
- 'appid' => APP_ID,
96
- 'deviceid' => switch[:device_id],
97
- 'nonce' => nonce,
98
- 'ts' => Time.now.to_i,
99
- 'version' => VERSION,
100
- }
101
- response = http_request(:get, '/api/user/device/status', headers: authentication_headers, query: params)
102
- response['params']['switch'] == 'on'
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
115
+ Ewelink.logger.debug(self.class.name) { "Checking switch #{switch[:uuid].inspect} status" }
116
+ send_to_web_socket(JSON.generate(params))
117
+ end
118
+ 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'
122
+ end
103
123
  end
104
124
 
105
125
  def switches
106
- @switches ||= [].tap do |switches|
107
- switch_devices = devices.select { |device| SWITCH_DEVICES_UIIDS.include?(device['uiid']) }
108
- switch_devices.each do |device|
109
- device_id = device['deviceid'].presence || next
110
- name = device['name'].presence || next
111
- switch = {
112
- device_id: device_id,
113
- name: name,
114
- }
115
- switch[:uuid] = Digest::UUID.uuid_v5(UUID_NAMESPACE, switch[:device_id])
116
- switches << switch
117
- end
118
- end.tap { |switches| Ewelink.logger.debug(self.class.name) { "Found #{switches.size} switch(es)" } }
126
+ synchronize(:switches) do
127
+ @switches ||= [].tap do |switches|
128
+ switch_devices = devices.select { |device| SWITCH_DEVICES_UIIDS.include?(device['uiid']) }
129
+ switch_devices.each do |device|
130
+ api_key = device['apikey'].presence || next
131
+ device_id = device['deviceid'].presence || next
132
+ name = device['name'].presence || next
133
+ switch = {
134
+ api_key: api_key,
135
+ device_id: device_id,
136
+ name: name,
137
+ }
138
+ switch[:uuid] = Digest::UUID.uuid_v5(UUID_NAMESPACE, switch[:device_id])
139
+ switches << switch
140
+ end
141
+ end.tap { |switches| Ewelink.logger.debug(self.class.name) { "Found #{switches.size} switch(es)" } }
142
+ end
143
+ end
144
+
145
+ def turn_switch!(uuid, on)
146
+ if ['on', :on, 'true'].include?(on)
147
+ on = true
148
+ elsif ['off', :off, 'false'].include?(on)
149
+ on = false
150
+ end
151
+ switch = find_switch!(uuid)
152
+ @switches_statuses[switch[:uuid]] = nil
153
+ web_socket_wait_for(-> { web_socket_authenticated? }) do
154
+ params = {
155
+ 'action' => 'update',
156
+ 'apikey' => switch[:api_key],
157
+ 'deviceid' => switch[:device_id],
158
+ 'params' => {
159
+ 'switch' => on ? 'on' : 'off',
160
+ },
161
+ 'sequence' => web_socket_sequence,
162
+ 'ts' => 0,
163
+ 'userAgent' => 'app',
164
+ }
165
+ Ewelink.logger.debug(self.class.name) { "Turning switch #{switch[:uuid].inspect} #{on ? 'on' : 'off'}" }
166
+ send_to_web_socket(JSON.generate(params))
167
+ end
168
+ switch_on?(switch[:uuid]) # Waiting for switch status update
169
+ true
119
170
  end
120
171
 
121
172
  private
122
173
 
174
+ def api_keys
175
+ synchronize(:api_keys) do
176
+ @api_keys ||= Set.new(devices.map { |device| device['apikey'] })
177
+ end
178
+ end
179
+
123
180
  def authentication_headers
124
181
  { 'Authorization' => "Bearer #{authentication_token}" }
125
182
  end
@@ -141,7 +198,7 @@ module Ewelink
141
198
  params['phoneNumber'] = phone_number
142
199
  end
143
200
  body = JSON.generate(params)
144
- response = http_request(:post, '/api/user/login', { body: body, headers: { 'Authorization' => "Sign #{Base64.encode64(OpenSSL::HMAC.digest('SHA256', APP_SECRET, body))}" } })
201
+ response = rest_request(:post, '/api/user/login', { body: body, headers: { 'Authorization' => "Sign #{Base64.encode64(OpenSSL::HMAC.digest('SHA256', APP_SECRET, body))}" } })
145
202
  raise(Error.new('Authentication token not found')) if response['at'].blank?
146
203
  response['at'].tap { Ewelink.logger.debug(self.class.name) { 'Authentication token found' } }
147
204
  end
@@ -158,12 +215,42 @@ module Ewelink
158
215
  'ts' => Time.now.to_i,
159
216
  'version' => VERSION,
160
217
  }
161
- response = http_request(:get, '/api/user/device', headers: authentication_headers, query: params)
218
+ response = rest_request(:get, '/api/user/device', headers: authentication_headers, query: params)
162
219
  response['devicelist'].tap { |devices| Ewelink.logger.debug(self.class.name) { "Found #{devices.size} device(s)" } }
163
220
  end
164
221
  end
165
222
  end
166
223
 
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
+
167
254
  def find_rf_bridge_button!(uuid)
168
255
  rf_bridge_buttons.find { |button| button[:uuid] == uuid } || raise(Error.new("No such RF bridge button with UUID: #{uuid.inspect}"))
169
256
  end
@@ -180,43 +267,176 @@ module Ewelink
180
267
  @region ||= DEFAULT_REGION
181
268
  end
182
269
 
183
- def http_request(method, path, options = {})
270
+ def rest_request(method, path, options = {})
184
271
  url = "#{URL.gsub('#{region}', region)}#{path}"
185
272
  method = method.to_s.upcase
186
273
  headers = (options[:headers] || {}).reverse_merge('Content-Type' => 'application/json')
187
274
  Ewelink.logger.debug(self.class.name) { "#{method} #{url}" }
188
- 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: TIMEOUT))
189
276
  raise(Error.new("#{method} #{url}: #{response.code}")) unless response.success?
190
277
  if response['error'] == 301 && response['region'].present?
191
278
  @region = response['region']
192
279
  Ewelink.logger.debug(self.class.name) { "Switched to region #{region.inspect}" }
193
- return http_request(method, path, options)
280
+ return rest_request(method, path, options)
194
281
  end
195
282
  remove_instance_variable(:@authentication_token) if instance_variable_defined?(:@authentication_token) && [401, 403].include?(response['error'])
196
283
  raise(Error.new("#{method} #{url}: #{response['error']} #{response['msg']}".strip)) if response['error'].present? && response['error'] != 0
197
- response
284
+ response.to_h
198
285
  rescue Errno::ECONNREFUSED, OpenSSL::OpenSSLError, SocketError, Timeout::Error => e
199
286
  raise Error.new(e)
200
287
  end
201
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
+
202
300
  def synchronize(name, &block)
203
301
  (@mutexs[name] ||= Mutex.new).synchronize(&block)
204
302
  end
205
303
 
206
- def update_switch_on!(uuid, on)
207
- switch = find_switch!(uuid)
208
- params = {
209
- 'appid' => APP_ID,
210
- 'deviceid' => switch[:device_id],
211
- 'nonce' => nonce,
212
- 'params' => {
213
- 'switch' => on ? 'on' : 'off',
214
- },
215
- 'ts' => Time.now.to_i,
216
- 'version' => VERSION,
217
- }
218
- http_request(:post, '/api/user/device/status', body: JSON.generate(params), headers: authentication_headers)
219
- true
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}" }
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'].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
360
+
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
365
+
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
370
+ end
371
+ end
372
+
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))
390
+ end
391
+ end
392
+ end
393
+ end
394
+ end
395
+ end
396
+ end
397
+
398
+ def web_socket_authenticated?
399
+ api_keys == @web_socket_authenticated_api_keys
400
+ end
401
+
402
+ def web_socket_outdated_ping?
403
+ @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
404
+ end
405
+
406
+ def web_socket_sequence
407
+ (Time.now.to_f * 1000).round.to_s
408
+ end
409
+
410
+ def web_socket_url
411
+ synchronize(:web_socket_url) do
412
+ @web_socket_url ||= begin
413
+ params = {
414
+ 'accept' => 'ws',
415
+ 'appid' => APP_ID,
416
+ 'nonce' => nonce,
417
+ 'ts' => Time.now.to_i,
418
+ 'version' => VERSION,
419
+ }
420
+ response = rest_request(:post, '/dispatch/app', body: JSON.generate(params), headers: authentication_headers)
421
+ raise('Error while getting WebSocket URL') unless response['error'] == 0
422
+ domain = response['domain'].presence || raise("Can't get WebSocket server domain")
423
+ port = response['port'].presence || raise("Can't get WebSocket server port")
424
+ "wss://#{domain}:#{port}/api/ws".tap { |url| Ewelink.logger.debug(self.class.name) { "WebSocket URL is: #{url.inspect}" } }
425
+ end
426
+ end
427
+ end
428
+
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
436
+ end
437
+ sleep(WEB_SOCKET_WAIT_INTERVAL)
438
+ end
439
+ end
220
440
  end
221
441
 
222
442
  end
@@ -6,16 +6,17 @@ module Ewelink
6
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
- options[:switches_on_uuids].each { |uuid| api.switch_on!(uuid) }
10
- options[:switches_off_uuids].each { |uuid| api.switch_off!(uuid) }
9
+ options[:turn_switches_on_uuids].each { |uuid| api.turn_switch!(uuid, :on) }
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: [], switches_off_uuids: [], 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
@@ -37,15 +38,18 @@ module Ewelink
37
38
  opts.on('--list-rf-bridge-buttons', 'List all RF 433MHz bridge buttons in JSON format') do
38
39
  options[:list_rf_bridge_buttons] = true
39
40
  end
40
- opts.on('--switch-on SWITCH_UUID', 'Set the switch with specified UUID on') do |uuid|
41
- options[:switches_on_uuids] << uuid
41
+ opts.on('--turn-switch-on SWITCH_UUID', 'Turn the switch with specified UUID on') do |uuid|
42
+ options[:turn_switches_on_uuids] << uuid
42
43
  end
43
- opts.on('--switch-off SWITCH_UUID', 'Set the switch with specified UUID off') do |uuid|
44
- options[:switches_off_uuids] << uuid
44
+ opts.on('--turn-switch-off SWITCH_UUID', 'Turn the switch with specified UUID off') do |uuid|
45
+ options[:turn_switches_off_uuids] << uuid
45
46
  end
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, :switches_on_uuids, :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.0.1
4
+ version: 2.1.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-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: []