ewelink 2.1.1 → 3.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 +4 -4
- data/VERSION +1 -1
- data/ewelink.gemspec +0 -1
- data/lib/ewelink.rb +0 -3
- data/lib/ewelink/api.rb +38 -258
- metadata +2 -22
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 558b7f50525eabfc0e0ac3cd32fd5a0ff5c0d4b6cc7658024dba4c6c0336f96a
|
4
|
+
data.tar.gz: cda09a44af6247436a22af9443a1dd3c078e8b86ab228f3ce12761baca606a7b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9607d6170f6e3f0cb00d599fa7a8258b25367423e6a87b2238079d336d6ee2a58389f78ffdcbc3eaf9726c1685ff3cb8b95fb0387d655ae6b81614d50d990c01
|
7
|
+
data.tar.gz: 4ad312d700e40c50f20cd47f5d66dec54937633e1162e21b3d131556752dc937e41c70d128e210d344d19a8bd0cb0fb4de1606f081cba50ad1189c84655440b6
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
3.0.0
|
data/ewelink.gemspec
CHANGED
@@ -17,7 +17,6 @@ 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'
|
21
20
|
|
22
21
|
s.add_development_dependency 'byebug', '>= 11.0.0', '< 12.0.0'
|
23
22
|
s.add_development_dependency 'rake', '>= 12.0.0', '< 13.0.0'
|
data/lib/ewelink.rb
CHANGED
data/lib/ewelink/api.rb
CHANGED
@@ -5,14 +5,12 @@ module Ewelink
|
|
5
5
|
APP_ID = 'oeVkj2lYFGnJu5XUtWisfW4utiN4u9Mq'
|
6
6
|
APP_SECRET = '6Nz4n0xA8s8qdxQf2GqurZj2Fs55FUvM'
|
7
7
|
DEFAULT_REGION = 'us'
|
8
|
-
REQUEST_TIMEOUT = 10.seconds
|
9
8
|
RF_BRIDGE_DEVICE_UIID = 28
|
10
9
|
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
|
16
14
|
|
17
15
|
attr_reader :email, :password, :phone_number
|
18
16
|
|
@@ -21,45 +19,31 @@ module Ewelink
|
|
21
19
|
@mutexs = {}
|
22
20
|
@password = password.presence || raise(Error.new(":password must be specified"))
|
23
21
|
@phone_number = phone_number.presence.try(:strip)
|
24
|
-
@web_socket_authenticated_api_keys = Set.new
|
25
|
-
@web_socket_switches_statuses = {}
|
26
22
|
raise(Error.new(":email or :phone_number must be specified")) if email.blank? && phone_number.blank?
|
27
23
|
end
|
28
24
|
|
29
25
|
def press_rf_bridge_button!(uuid)
|
30
26
|
synchronize(:press_rf_bridge_button) do
|
31
27
|
button = find_rf_bridge_button!(uuid)
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
'
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
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
|
49
41
|
end
|
50
42
|
end
|
51
43
|
|
52
44
|
def reload
|
53
|
-
Ewelink.logger.debug(self.class.name) { 'Reloading API (authentication token, devices
|
54
|
-
|
55
|
-
[
|
56
|
-
:@api_keys,
|
57
|
-
:@authentication_token,
|
58
|
-
:@devices,
|
59
|
-
:@region,
|
60
|
-
:@rf_bridge_buttons,
|
61
|
-
:@switches,
|
62
|
-
].each do |variable|
|
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|
|
63
47
|
remove_instance_variable(variable) if instance_variable_defined?(variable)
|
64
48
|
end
|
65
49
|
self
|
@@ -72,12 +56,10 @@ module Ewelink
|
|
72
56
|
Ewelink.logger.debug(self.class.name) { "Found #{devices.size} RF 433MHz bridge device(s)" }
|
73
57
|
end
|
74
58
|
rf_bridge_devices.each do |device|
|
75
|
-
api_key = device['apikey'].presence || next
|
76
59
|
device_id = device['deviceid'].presence || next
|
77
60
|
device_name = device['name'].presence || next
|
78
61
|
buttons = device['params']['rfList'].each do |rf|
|
79
62
|
button = {
|
80
|
-
api_key: api_key,
|
81
63
|
channel: rf['rfChl'],
|
82
64
|
device_id: device_id,
|
83
65
|
device_name: device_name,
|
@@ -101,24 +83,15 @@ module Ewelink
|
|
101
83
|
|
102
84
|
def switch_on?(uuid)
|
103
85
|
switch = find_switch!(uuid)
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
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
|
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'
|
122
95
|
end
|
123
96
|
|
124
97
|
def switches
|
@@ -126,11 +99,9 @@ module Ewelink
|
|
126
99
|
@switches ||= [].tap do |switches|
|
127
100
|
switch_devices = devices.select { |device| SWITCH_DEVICES_UIIDS.include?(device['uiid']) }
|
128
101
|
switch_devices.each do |device|
|
129
|
-
api_key = device['apikey'].presence || next
|
130
102
|
device_id = device['deviceid'].presence || next
|
131
103
|
name = device['name'].presence || next
|
132
104
|
switch = {
|
133
|
-
api_key: api_key,
|
134
105
|
device_id: device_id,
|
135
106
|
name: name,
|
136
107
|
}
|
@@ -148,34 +119,22 @@ module Ewelink
|
|
148
119
|
on = false
|
149
120
|
end
|
150
121
|
switch = find_switch!(uuid)
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
'
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
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)
|
168
133
|
true
|
169
134
|
end
|
170
135
|
|
171
136
|
private
|
172
137
|
|
173
|
-
def api_keys
|
174
|
-
synchronize(:api_keys) do
|
175
|
-
@api_keys ||= Set.new(devices.map { |device| device['apikey'] })
|
176
|
-
end
|
177
|
-
end
|
178
|
-
|
179
138
|
def authentication_headers
|
180
139
|
{ 'Authorization' => "Bearer #{authentication_token}" }
|
181
140
|
end
|
@@ -220,37 +179,6 @@ module Ewelink
|
|
220
179
|
end
|
221
180
|
end
|
222
181
|
|
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
|
-
|
254
182
|
def find_rf_bridge_button!(uuid)
|
255
183
|
rf_bridge_buttons.find { |button| button[:uuid] == uuid } || raise(Error.new("No such RF bridge button with UUID: #{uuid.inspect}"))
|
256
184
|
end
|
@@ -272,7 +200,7 @@ module Ewelink
|
|
272
200
|
method = method.to_s.upcase
|
273
201
|
headers = (options[:headers] || {}).reverse_merge('Content-Type' => 'application/json')
|
274
202
|
Ewelink.logger.debug(self.class.name) { "#{method} #{url}" }
|
275
|
-
response = HTTParty.send(method.downcase, url, options.merge(headers: headers).reverse_merge(timeout:
|
203
|
+
response = HTTParty.send(method.downcase, url, options.merge(headers: headers).reverse_merge(timeout: TIMEOUT))
|
276
204
|
raise(Error.new("#{method} #{url}: #{response.code}")) unless response.success?
|
277
205
|
if response['error'] == 301 && response['region'].present?
|
278
206
|
@region = response['region']
|
@@ -281,163 +209,15 @@ module Ewelink
|
|
281
209
|
end
|
282
210
|
remove_instance_variable(:@authentication_token) if instance_variable_defined?(:@authentication_token) && [401, 403].include?(response['error'])
|
283
211
|
raise(Error.new("#{method} #{url}: #{response['error']} #{response['msg']}".strip)) if response['error'].present? && response['error'] != 0
|
284
|
-
response
|
212
|
+
response
|
285
213
|
rescue Errno::ECONNREFUSED, OpenSSL::OpenSSLError, SocketError, Timeout::Error => e
|
286
214
|
raise Error.new(e)
|
287
215
|
end
|
288
216
|
|
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
|
-
|
300
217
|
def synchronize(name, &block)
|
301
218
|
(@mutexs[name] ||= Mutex.new).synchronize(&block)
|
302
219
|
end
|
303
220
|
|
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
|
-
|
441
221
|
end
|
442
222
|
|
443
223
|
end
|
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:
|
4
|
+
version: 3.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-
|
11
|
+
date: 2020-10-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -50,26 +50,6 @@ 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
|
73
53
|
- !ruby/object:Gem::Dependency
|
74
54
|
name: byebug
|
75
55
|
requirement: !ruby/object:Gem::Requirement
|