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 +4 -4
- data/README.mdown +11 -5
- data/VERSION +1 -1
- data/ewelink.gemspec +1 -0
- data/lib/ewelink.rb +3 -0
- data/lib/ewelink/api.rb +317 -97
- data/lib/ewelink/runner.rb +12 -8
- 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: 2c58323e838b9fbbdca7c7ee0fdf22d1e7950c627fb4fcf8851b3263e69ce883
|
4
|
+
data.tar.gz: 475d05f731b079f7734fde4dd93ec8f151077d6e5b716a86e611f11bdbb8d49b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 50ba5474e9012f219e380c47515d599da29a72b50cc26c36a19e253623c997e429a32c1b279619eef0e2d8b7f454fccde519500f998575c38d028ac86e6c1ba4
|
7
|
+
data.tar.gz: 122f2997897893b395cd04204e3e169d90bac15fc1a9eaabbf31def8da1c7f754bd502341411727364a79011379791a89031e9d42090d22bf96be233a24a5958
|
data/README.mdown
CHANGED
@@ -40,24 +40,30 @@ api.rf_bridge_buttons.each do |button|
|
|
40
40
|
end
|
41
41
|
```
|
42
42
|
|
43
|
-
###
|
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.
|
50
|
-
api.
|
49
|
+
api.turn_switch!(switch[:uuid], :on)
|
50
|
+
api.turn_switch!(switch[:uuid], :off)
|
51
51
|
```
|
52
52
|
|
53
|
-
|
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
|
+
2.1.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,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
|
-
|
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
|
+
@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
|
-
|
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
|
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
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
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 =
|
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 =
|
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
|
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 =
|
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
|
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
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
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
|
data/lib/ewelink/runner.rb
CHANGED
@@ -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[:
|
10
|
-
options[:
|
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: [],
|
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', '
|
41
|
-
options[:
|
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', '
|
44
|
-
options[:
|
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, :
|
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
|
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-
|
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: []
|