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 +4 -4
- data/VERSION +1 -1
- data/ewelink.gemspec +1 -0
- data/lib/ewelink.rb +3 -0
- data/lib/ewelink/api.rb +308 -82
- data/lib/ewelink/runner.rb +6 -2
- metadata +25 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bb352abf05d6bb6ef2f3b277c6f551d436bed969086478d3e121fd2e178fe4f9
|
4
|
+
data.tar.gz: b74266f2cbc92457a3bb0dc087752299a5a9f8348d3928a79d8c3c0719653e6a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2c5bb695c7d0094436454694008dd3c05c660fb1c7dfe2436ec1fa2fae3341559f901f3b9ae4ebfa99fcc276dcd4ceee02205ed1608d03e515b8da64b52f8a25
|
7
|
+
data.tar.gz: 4713a948c736a40496b3b33fff3b83253a4559693d692cfaa7251a4fc2d3f0e67699834f6513226edcbfe6cf7759ab54b1cb6c5a24cbe1559f54a75d6cbda6fd
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.1
|
1
|
+
2.1.1
|
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
@@ -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
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
44
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
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
|
-
|
77
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
'
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
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 =
|
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 =
|
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
|
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 =
|
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
|
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
|
data/lib/ewelink/runner.rb
CHANGED
@@ -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
|
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-
|
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: []
|