ewelink 1.3.0 → 2.0.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: 9f2548dd14bcfce1ce4cacb1a2768687d88e15bb4dae14223cadccd61176ab3d
4
- data.tar.gz: 995f4281dd22f168ffe90dc2f072209bd25b13487bb171d5dbcc4efc94380811
3
+ metadata.gz: 97046b44715a988f6ae92d986c0fad17ab4d5cae6420a125c10f23854cfaadc9
4
+ data.tar.gz: b95e52d88f8d57d5bf9f54b67d5a8c0779d26b66faadbc252e13eadb80854292
5
5
  SHA512:
6
- metadata.gz: 74821997ef9c625b2506ebda9229ddac0605f7e4dda4ac836c60974fa6d17a2ebcb9cfd306947c60d958409e5277b6d2ebe1189effef32f91730bfa0be2a9883
7
- data.tar.gz: 0d1999216c2006a5f70102190196a9c890ef9ab0c9aa6464402f8c843a51c8211852c560e62778d901eb91bf19f331f1008dd500b998cb59daf6ebe72477e5b1
6
+ metadata.gz: f153a280ff50b245434b2039cd3bb88341da6bd7688556a8a7c3fe4558a2b13a6be5c1497f5b06dc381495cb63d8c26d4d3f2625ea5bd4148e4c55a2f28b2faf
7
+ data.tar.gz: e3b69fa1607599325ea0f2867fbd0d32fd3abd56a4e7bd8d07107e083d4ebbfaf919bc4af34b28948163d1fb5340a53489aec1839cc59ba238f2917a8db03cc5
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.3.0
1
+ 2.0.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,11 @@ 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_WAIT_INTERVAL = 0.2.seconds
14
15
 
15
16
  attr_reader :email, :password, :phone_number
16
17
 
@@ -19,31 +20,38 @@ module Ewelink
19
20
  @mutexs = {}
20
21
  @password = password.presence || raise(Error.new(":password must be specified"))
21
22
  @phone_number = phone_number.presence.try(:strip)
23
+ @switches_statuses = {}
24
+ @web_socket_authenticated_api_keys = Set.new
22
25
  raise(Error.new(":email or :phone_number must be specified")) if email.blank? && phone_number.blank?
23
26
  end
24
27
 
25
28
  def press_rf_bridge_button!(uuid)
26
29
  synchronize(:press_rf_bridge_button) do
27
30
  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
- rest_request(:post, '/api/user/device/status', body: JSON.generate(params), headers: authentication_headers)
40
- true
31
+ web_socket_wait_for(-> { web_socket_authenticated? }) do
32
+ params = {
33
+ 'action' => 'update',
34
+ 'apikey' => button[:api_key],
35
+ 'deviceid' => button[:device_id],
36
+ 'params' => {
37
+ 'cmd' => 'transmit',
38
+ 'rfChl' => button[:channel],
39
+ },
40
+ 'sequence' => web_socket_sequence,
41
+ 'ts' => 0,
42
+ 'userAgent' => 'app',
43
+ }
44
+ send_to_web_socket(JSON.generate(params))
45
+ true
46
+ end
41
47
  end
42
48
  end
43
49
 
44
50
  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|
51
+ Ewelink.logger.debug(self.class.name) { 'Reloading API (authentication token, devices, region,...)' }
52
+ dispose_web_socket
53
+ @switches_statuses.clear
54
+ [:@api_keys, :@authentication_token, :@devices, :@rf_bridge_buttons, :@region, :@switches, :@web_socket, :@web_socket_url].each do |variable|
47
55
  remove_instance_variable(variable) if instance_variable_defined?(variable)
48
56
  end
49
57
  self
@@ -56,10 +64,12 @@ module Ewelink
56
64
  Ewelink.logger.debug(self.class.name) { "Found #{devices.size} RF 433MHz bridge device(s)" }
57
65
  end
58
66
  rf_bridge_devices.each do |device|
67
+ api_key = device['apikey'].presence || next
59
68
  device_id = device['deviceid'].presence || next
60
69
  device_name = device['name'].presence || next
61
70
  buttons = device['params']['rfList'].each do |rf|
62
71
  button = {
72
+ api_key: api_key,
63
73
  channel: rf['rfChl'],
64
74
  device_id: device_id,
65
75
  device_name: device_name,
@@ -83,15 +93,22 @@ module Ewelink
83
93
 
84
94
  def switch_on?(uuid)
85
95
  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 = rest_request(:get, '/api/user/device/status', headers: authentication_headers, query: params)
94
- response['params']['switch'] == 'on'
96
+ if @switches_statuses[switch[:uuid]].nil?
97
+ params = {
98
+ 'action' => 'query',
99
+ 'apikey' => switch[:api_key],
100
+ 'deviceid' => switch[:device_id],
101
+ 'sequence' => web_socket_sequence,
102
+ 'ts' => 0,
103
+ 'userAgent' => 'app',
104
+ }
105
+ web_socket_wait_for(-> { web_socket_authenticated? }) do
106
+ send_to_web_socket(JSON.generate(params))
107
+ end
108
+ end
109
+ web_socket_wait_for(-> { !@switches_statuses[switch[:uuid]].nil? }) do
110
+ @switches_statuses[switch[:uuid]] == 'on'
111
+ end
95
112
  end
96
113
 
97
114
  def switches
@@ -99,9 +116,11 @@ module Ewelink
99
116
  @switches ||= [].tap do |switches|
100
117
  switch_devices = devices.select { |device| SWITCH_DEVICES_UIIDS.include?(device['uiid']) }
101
118
  switch_devices.each do |device|
119
+ api_key = device['apikey'].presence || next
102
120
  device_id = device['deviceid'].presence || next
103
121
  name = device['name'].presence || next
104
122
  switch = {
123
+ api_key: api_key,
105
124
  device_id: device_id,
106
125
  name: name,
107
126
  }
@@ -119,22 +138,32 @@ module Ewelink
119
138
  on = false
120
139
  end
121
140
  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
- rest_request(:post, '/api/user/device/status', body: JSON.generate(params), headers: authentication_headers)
133
- true
141
+ @switches_statuses[switch[:uuid]] = nil
142
+ web_socket_wait_for(-> { web_socket_authenticated? }) do
143
+ params = {
144
+ 'action' => 'update',
145
+ 'apikey' => switch[:api_key],
146
+ 'deviceid' => switch[:device_id],
147
+ 'params' => {
148
+ 'switch' => on ? 'on' : 'off',
149
+ },
150
+ 'sequence' => web_socket_sequence,
151
+ 'ts' => 0,
152
+ 'userAgent' => 'app',
153
+ }
154
+ send_to_web_socket(JSON.generate(params))
155
+ true
156
+ end
134
157
  end
135
158
 
136
159
  private
137
160
 
161
+ def api_keys
162
+ synchronize(:api_keys) do
163
+ @api_keys ||= Set.new(devices.map { |device| device['apikey'] })
164
+ end
165
+ end
166
+
138
167
  def authentication_headers
139
168
  { 'Authorization' => "Bearer #{authentication_token}" }
140
169
  end
@@ -179,6 +208,18 @@ module Ewelink
179
208
  end
180
209
  end
181
210
 
211
+ def dispose_web_socket
212
+ @web_socket_authenticated_api_keys = Set.new
213
+ if instance_variable_defined?(:@web_socket)
214
+ begin
215
+ @web_socket.close if @web_socket.open?
216
+ rescue
217
+ # Ignoring close errors
218
+ end
219
+ remove_instance_variable(:@web_socket) if instance_variable_defined?(:@web_socket)
220
+ end
221
+ end
222
+
182
223
  def find_rf_bridge_button!(uuid)
183
224
  rf_bridge_buttons.find { |button| button[:uuid] == uuid } || raise(Error.new("No such RF bridge button with UUID: #{uuid.inspect}"))
184
225
  end
@@ -209,15 +250,127 @@ module Ewelink
209
250
  end
210
251
  remove_instance_variable(:@authentication_token) if instance_variable_defined?(:@authentication_token) && [401, 403].include?(response['error'])
211
252
  raise(Error.new("#{method} #{url}: #{response['error']} #{response['msg']}".strip)) if response['error'].present? && response['error'] != 0
212
- response
253
+ response.to_h
213
254
  rescue Errno::ECONNREFUSED, OpenSSL::OpenSSLError, SocketError, Timeout::Error => e
214
255
  raise Error.new(e)
215
256
  end
216
257
 
258
+ def send_to_web_socket(data)
259
+ web_socket.send(data)
260
+ rescue
261
+ dispose_web_socket
262
+ raise
263
+ end
264
+
217
265
  def synchronize(name, &block)
218
266
  (@mutexs[name] ||= Mutex.new).synchronize(&block)
219
267
  end
220
268
 
269
+ def web_socket
270
+ synchronize(:web_socket) do
271
+ @web_socket ||= begin
272
+ api = self
273
+
274
+ WebSocket::Client::Simple.connect(web_socket_url) do |web_socket|
275
+ Ewelink.logger.debug(self.class.name) { "Opening WebSocket to #{web_socket_url}" }
276
+
277
+ web_socket.on(:close) do
278
+ api.instance_eval do
279
+ Ewelink.logger.debug(self.class.name) { 'WebSocket closed' }
280
+ dispose_web_socket
281
+ end
282
+ end
283
+
284
+ web_socket.on(:error) do |e|
285
+ api.instance_eval do
286
+ Ewelink.logger.warn(self.class.name) { "WebSocket error: #{e}" }
287
+ dispose_web_socket
288
+ end
289
+ end
290
+
291
+ web_socket.on(:message) do |message|
292
+ api.instance_eval do
293
+ response = JSON.parse(message.data)
294
+
295
+ if response.key?('error') && response['error'] != 0
296
+ Ewelink.logger.error(self.class.name) { "WebSocket message error: #{message.data}" }
297
+ next
298
+ end
299
+
300
+ if response['apikey'].present? && !@web_socket_authenticated_api_keys.include?(response['apikey'])
301
+ @web_socket_authenticated_api_keys << response['apikey']
302
+ Ewelink.logger.debug(self.class.name) { "WebSocket successfully authenticated API key: #{response['apikey'].inspect}" }
303
+ end
304
+
305
+ if response['deviceid'].present? && response['params'].is_a?(Hash) && response['params']['switch'].present?
306
+ switch = switches.find { |switch| switch[:device_id] == response['deviceid'] }
307
+ @switches_statuses[switch[:uuid]] = response['params']['switch'] if switch.present?
308
+ end
309
+ end
310
+ end
311
+
312
+ web_socket.on(:open) do
313
+ api.instance_eval do
314
+ Ewelink.logger.debug(self.class.name) { 'WebSocket opened' }
315
+ api_keys.each do |api_key|
316
+ params = {
317
+ 'action' => 'userOnline',
318
+ 'apikey' => api_key,
319
+ 'appid' => APP_ID,
320
+ 'at' => authentication_token,
321
+ 'nonce' => nonce,
322
+ 'sequence' => web_socket_sequence,
323
+ 'ts' => Time.now.to_i,
324
+ 'userAgent' => 'app',
325
+ 'version' => VERSION,
326
+ }
327
+ Ewelink.logger.debug(self.class.name) { "Authenticating WebSocket API key: #{api_key.inspect}" }
328
+ send_to_web_socket(JSON.generate(params))
329
+ end
330
+ end
331
+ end
332
+ end
333
+ end
334
+ end
335
+ end
336
+
337
+ def web_socket_authenticated?
338
+ api_keys == @web_socket_authenticated_api_keys
339
+ end
340
+
341
+ def web_socket_sequence
342
+ (Time.now.to_f * 1000).round.to_s
343
+ end
344
+
345
+ def web_socket_url
346
+ synchronize(:web_socket_url) do
347
+ @web_socket_url ||= begin
348
+ params = {
349
+ 'accept' => 'ws',
350
+ 'appid' => APP_ID,
351
+ 'nonce' => nonce,
352
+ 'ts' => Time.now.to_i,
353
+ 'version' => VERSION,
354
+ }
355
+ response = rest_request(:post, '/dispatch/app', body: JSON.generate(params), headers: authentication_headers)
356
+ raise('Error while getting WebSocket URL') unless response['error'] == 0
357
+ domain = response['domain'].presence || raise("Can't get WebSocket server domain")
358
+ port = response['port'].presence || raise("Can't get WebSocket server port")
359
+ "wss://#{domain}:#{port}/api/ws".tap { |url| Ewelink.logger.debug(self.class.name) { "WebSocket URL is: #{url.inspect}" } }
360
+ end
361
+ end
362
+ end
363
+
364
+ def web_socket_wait_for(condition, &block)
365
+ web_socket # Initializes WebSocket
366
+ Timeout.timeout(TIMEOUT) do
367
+ loop do
368
+ return yield if condition.call
369
+ sleep(WEB_SOCKET_WAIT_INTERVAL)
370
+ end
371
+ end
372
+ end
373
+
221
374
  end
222
375
 
223
376
  end
@@ -9,7 +9,7 @@ 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))
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?
13
13
  end
14
14
 
15
15
  private
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.3.0
4
+ version: 2.0.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-08-31 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