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 +4 -4
- data/VERSION +1 -1
- data/ewelink.gemspec +1 -0
- data/lib/ewelink.rb +3 -0
- data/lib/ewelink/api.rb +191 -38
- data/lib/ewelink/runner.rb +1 -1
- metadata +22 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 97046b44715a988f6ae92d986c0fad17ab4d5cae6420a125c10f23854cfaadc9
|
4
|
+
data.tar.gz: b95e52d88f8d57d5bf9f54b67d5a8c0779d26b66faadbc252e13eadb80854292
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f153a280ff50b245434b2039cd3bb88341da6bd7688556a8a7c3fe4558a2b13a6be5c1497f5b06dc381495cb63d8c26d4d3f2625ea5bd4148e4c55a2f28b2faf
|
7
|
+
data.tar.gz: e3b69fa1607599325ea0f2867fbd0d32fd3abd56a4e7bd8d07107e083d4ebbfaf919bc4af34b28948163d1fb5340a53489aec1839cc59ba238f2917a8db03cc5
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
|
1
|
+
2.0.0
|
data/ewelink.gemspec
CHANGED
@@ -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'
|
data/lib/ewelink.rb
CHANGED
data/lib/ewelink/api.rb
CHANGED
@@ -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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
'
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|
46
|
-
|
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
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
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
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
'
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
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
|
data/lib/ewelink/runner.rb
CHANGED
@@ -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:
|
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-
|
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
|