ewelink 4.1.0 → 6.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/README.mdown +0 -2
- data/VERSION +1 -1
- data/ewelink.gemspec +0 -1
- data/lib/ewelink/api.rb +106 -309
- data/lib/ewelink.rb +0 -2
- 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: '03889642d2851312650d6419544fd7423b2b8ca0a64a272386de46646640b191'
|
|
4
|
+
data.tar.gz: b36f496794dce02953b423d601f4e71905afca7c0aebe4fe5bfc65d08f65daff
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c3a3349b5c5f5384eb4f02abcf3749b0674ef839c2354f38d319db4ff15e272f7ec551058b52601a1971307f225eac8d0c5c7cfee00d6537a527bb09f044aceb
|
|
7
|
+
data.tar.gz: 63035965d8732f4fda004b16331ee06a4ad6f55da7a4338b041585823397b14bb6dfecacf83123354b024f2a9fb8e1b724d32011862d9343babec527fff12d2e
|
data/README.mdown
CHANGED
|
@@ -80,8 +80,6 @@ api.press_rf_bridge_button!(button[:uuid])
|
|
|
80
80
|
- `async_actions` (`true` | `false`): To perform actions (pressing an RF
|
|
81
81
|
bridge button or turning a switch on/off) in asynchronous mode. (default:
|
|
82
82
|
`false`).
|
|
83
|
-
- `update_devices_status_on_connect` (`true` | `false`): To update devices
|
|
84
|
-
status (on, off) when connecting to Ewelink API (default: `false`).
|
|
85
83
|
|
|
86
84
|
### Configuring logger
|
|
87
85
|
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
6.0.0
|
data/ewelink.gemspec
CHANGED
|
@@ -16,7 +16,6 @@ Gem::Specification.new do |s|
|
|
|
16
16
|
s.required_ruby_version = '>= 3.1.0'
|
|
17
17
|
|
|
18
18
|
s.add_dependency 'activesupport', '>= 7.0.0', '< 8.0.0'
|
|
19
|
-
s.add_dependency 'faye-websocket', '>= 0.11.0', '< 0.12.0'
|
|
20
19
|
s.add_dependency 'httparty', '>= 0.20.0', '< 0.21.0'
|
|
21
20
|
s.add_dependency 'thread', '>= 0.2.0', '< 0.3.0'
|
|
22
21
|
|
data/lib/ewelink/api.rb
CHANGED
|
@@ -2,34 +2,26 @@ module Ewelink
|
|
|
2
2
|
|
|
3
3
|
class Api
|
|
4
4
|
|
|
5
|
-
APP_ID = '
|
|
6
|
-
APP_SECRET = '
|
|
5
|
+
APP_ID = 'Uw83EKZFxdif7XFXEsrpduz5YyjP7nTl'.freeze
|
|
6
|
+
APP_SECRET = 'mXLOjea0woSMvK9gw7Fjsy7YlFO4iSu6'.freeze
|
|
7
|
+
DEFAULT_COUNTRY_CODE = '+44'.freeze
|
|
7
8
|
DEFAULT_REGION = 'us'.freeze
|
|
8
9
|
REQUEST_TIMEOUT = 10.seconds
|
|
9
10
|
RF_BRIDGE_DEVICE_UIID = 28
|
|
10
11
|
SWITCH_DEVICES_UIIDS = [1, 5, 6, 24].freeze
|
|
11
|
-
URL = 'https://#{region}-
|
|
12
|
+
URL = 'https://#{region}-apia.coolkit.cc'.freeze
|
|
12
13
|
VERSION = 8
|
|
13
|
-
WEB_SOCKET_CHECK_AUTHENTICATION_TIMEOUT = 30.seconds
|
|
14
|
-
WEB_SOCKET_PING_TOLERANCE_FACTOR = 1.5
|
|
15
|
-
SWITCH_STATUS_CHANGE_CHECK_TIMEOUT = 2.seconds
|
|
16
|
-
WEB_SOCKET_WAIT_INTERVAL = 0.2.seconds
|
|
17
14
|
|
|
18
15
|
attr_reader :email, :password, :phone_number
|
|
19
16
|
|
|
20
|
-
def initialize(password:, async_actions: false, email: nil, phone_number: nil
|
|
17
|
+
def initialize(password:, async_actions: false, email: nil, phone_number: nil)
|
|
21
18
|
@async_actions = async_actions.present?
|
|
22
19
|
@email = email.presence.try(:strip)
|
|
23
20
|
@mutexs = {}
|
|
24
21
|
@password = password.presence || raise(Error.new(':password must be specified'))
|
|
25
22
|
@phone_number = phone_number.presence.try(:strip)
|
|
26
|
-
@update_devices_status_on_connect = update_devices_status_on_connect.present?
|
|
27
|
-
@web_socket_authenticated = false
|
|
28
|
-
@web_socket_switches_statuses = {}
|
|
29
23
|
|
|
30
24
|
raise(Error.new(':email or :phone_number must be specified')) if email.blank? && phone_number.blank?
|
|
31
|
-
|
|
32
|
-
start_web_socket_authentication_check_thread
|
|
33
25
|
end
|
|
34
26
|
|
|
35
27
|
def async_actions?
|
|
@@ -40,23 +32,21 @@ module Ewelink
|
|
|
40
32
|
process_action do
|
|
41
33
|
synchronize(:press_rf_bridge_button) do
|
|
42
34
|
button = find_rf_bridge_button!(uuid)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
'
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
true
|
|
59
|
-
end
|
|
35
|
+
headers = authentication_headers.merge(
|
|
36
|
+
'X-CK-Appid' => APP_ID,
|
|
37
|
+
'X-CK-Nonce' => nonce,
|
|
38
|
+
)
|
|
39
|
+
params = {
|
|
40
|
+
'type' => 1,
|
|
41
|
+
'id' => button[:device_id],
|
|
42
|
+
'params' => {
|
|
43
|
+
'cmd' => 'transmit',
|
|
44
|
+
'rfChl' => button[:channel],
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
body = JSON.generate(params)
|
|
48
|
+
response = rest_request(:post, '/v2/device/thing/status', headers:, body:)
|
|
49
|
+
response['error'] == 0
|
|
60
50
|
end
|
|
61
51
|
end
|
|
62
52
|
end
|
|
@@ -64,38 +54,13 @@ module Ewelink
|
|
|
64
54
|
def reload
|
|
65
55
|
Ewelink.logger.debug(self.class.name) { 'Reloading API (authentication token, api key, devices, region, connections,...)' }
|
|
66
56
|
|
|
67
|
-
@web_socket_authenticated = false
|
|
68
|
-
@web_socket_switches_statuses.clear
|
|
69
|
-
|
|
70
|
-
[@web_socket_ping_thread, @web_socket_thread].each do |thread|
|
|
71
|
-
next unless thread
|
|
72
|
-
if Thread.current == thread
|
|
73
|
-
thread[:stop] = true
|
|
74
|
-
else
|
|
75
|
-
thread.kill
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
if @web_socket.present?
|
|
80
|
-
begin
|
|
81
|
-
@web_socket.close if @web_socket.open?
|
|
82
|
-
rescue
|
|
83
|
-
# Ignoring close errors
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
|
|
87
57
|
%i(
|
|
88
58
|
@authentication_infos
|
|
89
59
|
@devices
|
|
90
|
-
@
|
|
60
|
+
@homes_ids
|
|
91
61
|
@region
|
|
92
62
|
@rf_bridge_buttons
|
|
93
63
|
@switches
|
|
94
|
-
@web_socket_ping_interval
|
|
95
|
-
@web_socket_ping_thread
|
|
96
|
-
@web_socket_thread
|
|
97
|
-
@web_socket_url
|
|
98
|
-
@web_socket
|
|
99
64
|
).each do |variable|
|
|
100
65
|
remove_instance_variable(variable) if instance_variable_defined?(variable)
|
|
101
66
|
end
|
|
@@ -105,21 +70,21 @@ module Ewelink
|
|
|
105
70
|
def rf_bridge_buttons
|
|
106
71
|
synchronize(:rf_bridge_buttons) do
|
|
107
72
|
@rf_bridge_buttons ||= [].tap do |buttons|
|
|
108
|
-
rf_bridge_devices = devices.select { |device| device['uiid'] == RF_BRIDGE_DEVICE_UIID }.tap do |devices|
|
|
73
|
+
rf_bridge_devices = devices.select { |device| device['itemData']['extra']['uiid'] == RF_BRIDGE_DEVICE_UIID }.tap do |devices|
|
|
109
74
|
Ewelink.logger.debug(self.class.name) { "Found #{devices.size} RF 433MHz bridge device(s)" }
|
|
110
75
|
end
|
|
111
76
|
rf_bridge_devices.each do |device|
|
|
112
|
-
api_key = device['apikey'].presence || next
|
|
113
|
-
device_id = device['deviceid'].presence || next
|
|
114
|
-
device_name = device['name'].presence || next
|
|
115
|
-
buttons = device['params']['rfList'].each do |rf|
|
|
77
|
+
api_key = device['itemData']['apikey'].presence || next
|
|
78
|
+
device_id = device['itemData']['deviceid'].presence || next
|
|
79
|
+
device_name = device['itemData']['name'].presence || next
|
|
80
|
+
buttons = device['itemData']['params']['rfList'].each do |rf|
|
|
116
81
|
button = {
|
|
117
82
|
api_key:,
|
|
118
83
|
channel: rf['rfChl'],
|
|
119
84
|
device_id:,
|
|
120
85
|
device_name:,
|
|
121
86
|
}
|
|
122
|
-
remote_info = device['tags']['zyx_info'].find { |info| info['buttonName'].find { |data| data.key?(button[:channel].to_s) } }.presence || next
|
|
87
|
+
remote_info = device['itemData']['tags']['zyx_info'].find { |info| info['buttonName'].find { |data| data.key?(button[:channel].to_s) } }.presence || next
|
|
123
88
|
remote_name = remote_info['name'].try(:squish).presence || next
|
|
124
89
|
button_info = remote_info['buttonName'].find { |info| info.key?(button[:channel].to_s) }.presence || next
|
|
125
90
|
button_name = button_info.values.first.try(:squish).presence || next
|
|
@@ -138,37 +103,32 @@ module Ewelink
|
|
|
138
103
|
|
|
139
104
|
def switch_on?(uuid)
|
|
140
105
|
switch = find_switch!(uuid)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
web_socket_wait_for(-> { !@web_socket_switches_statuses[switch[:uuid]].nil? }, initialize_web_socket: true) do
|
|
156
|
-
@web_socket_switches_statuses[switch[:uuid]] == 'on'
|
|
157
|
-
end
|
|
106
|
+
headers = authentication_headers.merge(
|
|
107
|
+
'X-CK-Appid' => APP_ID,
|
|
108
|
+
'X-CK-Nonce' => nonce,
|
|
109
|
+
)
|
|
110
|
+
params = {
|
|
111
|
+
'type' => 1,
|
|
112
|
+
'id' => switch[:device_id],
|
|
113
|
+
'params' => 'switch',
|
|
114
|
+
}
|
|
115
|
+
Ewelink.logger.debug(self.class.name) { "Checking switch #{switch[:uuid].inspect} status" }
|
|
116
|
+
response = rest_request(:get, '/v2/device/thing/status', headers:, query: params)
|
|
117
|
+
response['data']['params']['switch'] == 'on'
|
|
158
118
|
end
|
|
159
119
|
|
|
160
120
|
def switches
|
|
161
121
|
synchronize(:switches) do
|
|
162
122
|
@switches ||= [].tap do |switches|
|
|
163
|
-
switch_devices = devices.select { |device| SWITCH_DEVICES_UIIDS.include?(device['uiid']) }
|
|
123
|
+
switch_devices = devices.select { |device| SWITCH_DEVICES_UIIDS.include?(device['itemData']['extra']['uiid']) }
|
|
164
124
|
switch_devices.each do |device|
|
|
165
|
-
api_key = device['apikey'].presence || next
|
|
166
|
-
device_id = device['deviceid'].presence || next
|
|
167
|
-
name = device['name'].presence || next
|
|
125
|
+
api_key = device['itemData']['apikey'].presence || next
|
|
126
|
+
device_id = device['itemData']['deviceid'].presence || next
|
|
127
|
+
name = device['itemData']['name'].presence || next
|
|
168
128
|
switch = {
|
|
169
129
|
api_key:,
|
|
170
130
|
device_id:,
|
|
171
|
-
model: device['productModel'],
|
|
131
|
+
model: device['itemData']['productModel'],
|
|
172
132
|
name:,
|
|
173
133
|
}
|
|
174
134
|
switch[:uuid] = Digest::UUID.uuid_v5(Digest::UUID::DNS_NAMESPACE, switch[:device_id])
|
|
@@ -186,54 +146,30 @@ module Ewelink
|
|
|
186
146
|
on = false
|
|
187
147
|
end
|
|
188
148
|
switch = find_switch!(uuid)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
end
|
|
205
|
-
sleep(SWITCH_STATUS_CHANGE_CHECK_TIMEOUT)
|
|
206
|
-
switch_on?(switch[:uuid]) # Waiting for switch status update
|
|
207
|
-
true
|
|
149
|
+
headers = authentication_headers.merge(
|
|
150
|
+
'X-CK-Appid' => APP_ID,
|
|
151
|
+
'X-CK-Nonce' => nonce,
|
|
152
|
+
)
|
|
153
|
+
params = {
|
|
154
|
+
'type' => 1,
|
|
155
|
+
'id' => switch[:device_id],
|
|
156
|
+
'params' => {
|
|
157
|
+
'switch' => on ? 'on' : 'off',
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
body = JSON.generate(params)
|
|
161
|
+
Ewelink.logger.debug(self.class.name) { "Turning switch #{switch[:uuid].inspect} #{on ? 'on' : 'off'}" }
|
|
162
|
+
response = rest_request(:post, '/v2/device/thing/status', headers:, body:)
|
|
163
|
+
response['error'] == 0
|
|
208
164
|
end
|
|
209
165
|
end
|
|
210
166
|
|
|
211
|
-
def update_devices_status_on_connect?
|
|
212
|
-
@update_devices_status_on_connect
|
|
213
|
-
end
|
|
214
|
-
|
|
215
167
|
private
|
|
216
168
|
|
|
217
169
|
def api_key
|
|
218
170
|
authentication_infos[:api_key]
|
|
219
171
|
end
|
|
220
172
|
|
|
221
|
-
def authenticate_web_socket_api_key
|
|
222
|
-
params = {
|
|
223
|
-
'action' => 'userOnline',
|
|
224
|
-
'apikey' => api_key,
|
|
225
|
-
'appid' => APP_ID,
|
|
226
|
-
'at' => authentication_token,
|
|
227
|
-
'nonce' => nonce,
|
|
228
|
-
'sequence' => web_socket_sequence,
|
|
229
|
-
'ts' => Time.now.to_i,
|
|
230
|
-
'userAgent' => 'app',
|
|
231
|
-
'version' => VERSION,
|
|
232
|
-
}
|
|
233
|
-
Ewelink.logger.debug(self.class.name) { "Authenticating WebSocket API key: #{api_key.truncate(16).inspect}" }
|
|
234
|
-
send_to_web_socket(JSON.generate(params))
|
|
235
|
-
end
|
|
236
|
-
|
|
237
173
|
def authentication_headers
|
|
238
174
|
{ 'Authorization' => "Bearer #{authentication_token}" }
|
|
239
175
|
end
|
|
@@ -242,12 +178,8 @@ module Ewelink
|
|
|
242
178
|
synchronize(:authentication_infos) do
|
|
243
179
|
@authentication_infos ||= begin
|
|
244
180
|
params = {
|
|
245
|
-
'
|
|
246
|
-
'imei' => SecureRandom.uuid.upcase,
|
|
247
|
-
'nonce' => nonce,
|
|
181
|
+
'countryCode' => DEFAULT_COUNTRY_CODE,
|
|
248
182
|
'password' => password,
|
|
249
|
-
'ts' => Time.now.to_i,
|
|
250
|
-
'version' => VERSION,
|
|
251
183
|
}
|
|
252
184
|
if email.present?
|
|
253
185
|
params['email'] = email
|
|
@@ -255,12 +187,20 @@ module Ewelink
|
|
|
255
187
|
params['phoneNumber'] = phone_number
|
|
256
188
|
end
|
|
257
189
|
body = JSON.generate(params)
|
|
258
|
-
response = rest_request(:post, '/
|
|
259
|
-
|
|
260
|
-
|
|
190
|
+
response = rest_request(:post, '/v2/user/login', {
|
|
191
|
+
body:,
|
|
192
|
+
headers: {
|
|
193
|
+
'Authorization' => "Sign #{Base64.encode64(OpenSSL::HMAC.digest('SHA256', APP_SECRET, body))}",
|
|
194
|
+
'X-CK-Appid': APP_ID,
|
|
195
|
+
'X-CK-Nonce': nonce,
|
|
196
|
+
},
|
|
197
|
+
})
|
|
198
|
+
raise(Error.new('Invalid authentication response')) unless response['data'].is_a?(Hash) && response['data']['user'].is_a?(Hash)
|
|
199
|
+
raise(Error.new('Authentication token not found')) unless response['data']['at'].present?
|
|
200
|
+
raise(Error.new('API key not found')) unless response['data']['user']['apikey'].present?
|
|
261
201
|
{
|
|
262
|
-
|
|
263
|
-
|
|
202
|
+
api_key: response['data']['user']['apikey'].tap { Ewelink.logger.debug(self.class.name) { 'API key found' } },
|
|
203
|
+
authentication_token: response['data']['at'].tap { Ewelink.logger.debug(self.class.name) { 'Authentication token found' } },
|
|
264
204
|
}
|
|
265
205
|
end
|
|
266
206
|
end
|
|
@@ -273,15 +213,22 @@ module Ewelink
|
|
|
273
213
|
def devices
|
|
274
214
|
synchronize(:devices) do
|
|
275
215
|
@devices ||= begin
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
216
|
+
devices = []
|
|
217
|
+
homes_ids.each do |home_id|
|
|
218
|
+
params = {
|
|
219
|
+
'familyid' => home_id,
|
|
220
|
+
'num' => 0,
|
|
221
|
+
}
|
|
222
|
+
headers = authentication_headers.merge(
|
|
223
|
+
'X-CK-Appid' => APP_ID,
|
|
224
|
+
'X-CK-Nonce' => nonce,
|
|
225
|
+
)
|
|
226
|
+
response = rest_request(:get, '/v2/device/thing', headers:, query: params)
|
|
227
|
+
raise('Invalid devices response') unless response['data'].is_a?(Hash) && response['data']['thingList'].is_a?(Array)
|
|
228
|
+
devices += response['data']['thingList']
|
|
229
|
+
end
|
|
230
|
+
Ewelink.logger.debug(self.class.name) { "Found #{devices.size} device(s)" }
|
|
231
|
+
devices
|
|
285
232
|
end
|
|
286
233
|
end
|
|
287
234
|
end
|
|
@@ -294,6 +241,20 @@ module Ewelink
|
|
|
294
241
|
switches.find { |switch| switch[:uuid] == uuid } || raise(Error.new("No such switch with UUID: #{uuid.inspect}"))
|
|
295
242
|
end
|
|
296
243
|
|
|
244
|
+
def homes_ids
|
|
245
|
+
synchronize(:homes_ids) do
|
|
246
|
+
@homes_ids ||= begin
|
|
247
|
+
headers = authentication_headers.merge(
|
|
248
|
+
'X-CK-Appid' => APP_ID,
|
|
249
|
+
'X-CK-Nonce' => nonce,
|
|
250
|
+
)
|
|
251
|
+
response = rest_request(:get, '/v2/family', headers:)
|
|
252
|
+
raise('Invalid homes response') unless response['data'].is_a?(Hash) && response['data']['familyList'].is_a?(Array)
|
|
253
|
+
response['data']['familyList'].map { |home| home['id'] }.compact.uniq
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
297
258
|
def nonce
|
|
298
259
|
SecureRandom.hex[0, 8]
|
|
299
260
|
end
|
|
@@ -311,191 +272,27 @@ module Ewelink
|
|
|
311
272
|
|
|
312
273
|
def rest_request(method, path, options = {})
|
|
313
274
|
url = "#{URL.gsub('#{region}', region)}#{path}"
|
|
275
|
+
url.gsub!(/#{Regexp.escape("#{region}-api")}/, "#{region}-disp") if options[:dispatch].present?
|
|
314
276
|
method = method.to_s.upcase
|
|
315
277
|
headers = (options[:headers] || {}).reverse_merge('Content-Type' => 'application/json')
|
|
316
278
|
Ewelink.logger.debug(self.class.name) { "#{method} #{url}" }
|
|
317
279
|
response = HTTParty.send(method.downcase, url, options.merge(headers:).reverse_merge(timeout: REQUEST_TIMEOUT))
|
|
318
280
|
raise(Error.new("#{method} #{url}: #{response.code}")) unless response.success?
|
|
319
|
-
if response['error'] ==
|
|
320
|
-
@region = response['region']
|
|
281
|
+
if response['error'] == 10_004 && response['data'].present? && response['data']['region'].present?
|
|
282
|
+
@region = response['data']['region']
|
|
321
283
|
Ewelink.logger.debug(self.class.name) { "Switched to region #{region.inspect}" }
|
|
322
284
|
return rest_request(method, path, options)
|
|
323
285
|
end
|
|
324
|
-
remove_instance_variable(:@authentication_infos) if instance_variable_defined?(:@authentication_infos) && [401, 403].include?(response['error'])
|
|
325
286
|
raise(Error.new("#{method} #{url}: #{response['error']} #{response['msg']}".strip)) if response['error'].present? && response['error'] != 0
|
|
326
287
|
response.to_h
|
|
327
288
|
rescue Errno::ECONNREFUSED, OpenSSL::OpenSSLError, SocketError, Timeout::Error => e
|
|
328
289
|
raise Error.new(e)
|
|
329
290
|
end
|
|
330
291
|
|
|
331
|
-
def send_to_web_socket(message)
|
|
332
|
-
web_socket.send(message)
|
|
333
|
-
rescue => e
|
|
334
|
-
reload
|
|
335
|
-
raise Error.new(e)
|
|
336
|
-
end
|
|
337
|
-
|
|
338
|
-
def start_web_socket_authentication_check_thread
|
|
339
|
-
raise Error.new('WebSocket authentication check must only be started once') if @web_socket_authentication_check_thread.present?
|
|
340
|
-
|
|
341
|
-
@web_socket_authentication_check_thread = Thread.new do
|
|
342
|
-
loop do
|
|
343
|
-
Ewelink.logger.debug(self.class.name) { 'Checking if WebSocket is authenticated' }
|
|
344
|
-
begin
|
|
345
|
-
web_socket_wait_for(-> { web_socket_authenticated? }, initialize_web_socket: true) do
|
|
346
|
-
Ewelink.logger.debug(self.class.name) { 'WebSocket is authenticated' }
|
|
347
|
-
end
|
|
348
|
-
rescue => e
|
|
349
|
-
Ewelink.logger.error(self.class.name) { e }
|
|
350
|
-
end
|
|
351
|
-
sleep(WEB_SOCKET_CHECK_AUTHENTICATION_TIMEOUT)
|
|
352
|
-
end
|
|
353
|
-
end
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
def start_web_socket_ping_thread(interval)
|
|
357
|
-
@last_web_socket_pong_at = Time.now
|
|
358
|
-
@web_socket_ping_interval = interval
|
|
359
|
-
Ewelink.logger.debug(self.class.name) { "Creating thread for WebSocket ping every #{@web_socket_ping_interval} seconds" }
|
|
360
|
-
@web_socket_ping_thread = Thread.new do
|
|
361
|
-
loop do
|
|
362
|
-
break if Thread.current[:stop]
|
|
363
|
-
sleep(@web_socket_ping_interval)
|
|
364
|
-
Ewelink.logger.debug(self.class.name) { 'Sending WebSocket ping' }
|
|
365
|
-
send_to_web_socket('ping')
|
|
366
|
-
end
|
|
367
|
-
end
|
|
368
|
-
end
|
|
369
|
-
|
|
370
292
|
def synchronize(name, &block)
|
|
371
293
|
(@mutexs[name] ||= Mutex.new).synchronize(&block)
|
|
372
294
|
end
|
|
373
295
|
|
|
374
|
-
def web_socket
|
|
375
|
-
if web_socket_outdated_ping?
|
|
376
|
-
Ewelink.logger.warn(self.class.name) { 'WebSocket ping is outdated' }
|
|
377
|
-
reload
|
|
378
|
-
end
|
|
379
|
-
|
|
380
|
-
synchronize(:web_socket) do
|
|
381
|
-
next @web_socket if @web_socket
|
|
382
|
-
|
|
383
|
-
# Initializes caches before opening WebSocket: important in order to
|
|
384
|
-
# NOT cumulate requests Timeouts from #web_socket_wait_for.
|
|
385
|
-
api_key
|
|
386
|
-
web_socket_url
|
|
387
|
-
|
|
388
|
-
Ewelink.logger.debug(self.class.name) { "Opening WebSocket to #{web_socket_url.inspect}" }
|
|
389
|
-
|
|
390
|
-
@web_socket_thread = Thread.new do
|
|
391
|
-
EventMachine.run do
|
|
392
|
-
@web_socket = Faye::WebSocket::Client.new(web_socket_url)
|
|
393
|
-
|
|
394
|
-
@web_socket.on(:close) do
|
|
395
|
-
Ewelink.logger.debug(self.class.name) { 'WebSocket closed' }
|
|
396
|
-
reload
|
|
397
|
-
end
|
|
398
|
-
|
|
399
|
-
@web_socket.on(:open) do
|
|
400
|
-
Ewelink.logger.debug(self.class.name) { 'WebSocket opened' }
|
|
401
|
-
@last_web_socket_pong_at = Time.now
|
|
402
|
-
authenticate_web_socket_api_key
|
|
403
|
-
end
|
|
404
|
-
|
|
405
|
-
@web_socket.on(:message) do |event|
|
|
406
|
-
message = event.data
|
|
407
|
-
|
|
408
|
-
if message == 'pong'
|
|
409
|
-
Ewelink.logger.debug(self.class.name) { "Received WebSocket #{message.inspect} message" }
|
|
410
|
-
@last_web_socket_pong_at = Time.now
|
|
411
|
-
next
|
|
412
|
-
end
|
|
413
|
-
|
|
414
|
-
begin
|
|
415
|
-
json = JSON.parse(message)
|
|
416
|
-
rescue
|
|
417
|
-
Ewelink.logger.error(self.class.name) { 'WebSocket JSON parse error' }
|
|
418
|
-
reload
|
|
419
|
-
next
|
|
420
|
-
end
|
|
421
|
-
|
|
422
|
-
if json.key?('error') && json['error'] != 0
|
|
423
|
-
Ewelink.logger.error(self.class.name) { "WebSocket message error: #{message.inspect}" }
|
|
424
|
-
reload
|
|
425
|
-
next
|
|
426
|
-
end
|
|
427
|
-
|
|
428
|
-
if !@web_socket_ping_thread && json.key?('config') && json['config']['hb'] == 1 && json['config']['hbInterval'].present?
|
|
429
|
-
start_web_socket_ping_thread(json['config']['hbInterval'] + 7)
|
|
430
|
-
end
|
|
431
|
-
|
|
432
|
-
if json['apikey'].present? && !@web_socket_authenticated && json['apikey'] == api_key
|
|
433
|
-
@web_socket_authenticated = true
|
|
434
|
-
Ewelink.logger.debug(self.class.name) { "WebSocket successfully authenticated API key: #{json['apikey'].truncate(16).inspect}" }
|
|
435
|
-
Thread.new { switches.each { |switch| switch_on?(switch[:uuid]) } } if update_devices_status_on_connect?
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
if json['deviceid'].present? && json['params'].is_a?(Hash) && json['params']['switch'].present?
|
|
439
|
-
switch = switches.find { |item| item[:device_id] == json['deviceid'] }
|
|
440
|
-
if switch.present?
|
|
441
|
-
@web_socket_switches_statuses[switch[:uuid]] = json['params']['switch']
|
|
442
|
-
Ewelink.logger.debug(self.class.name) { "Switch #{switch[:uuid].inspect} is #{@web_socket_switches_statuses[switch[:uuid]]}" }
|
|
443
|
-
end
|
|
444
|
-
end
|
|
445
|
-
end
|
|
446
|
-
end
|
|
447
|
-
end
|
|
448
|
-
|
|
449
|
-
web_socket_wait_for(-> { @web_socket.present? }) do
|
|
450
|
-
@web_socket
|
|
451
|
-
end
|
|
452
|
-
end
|
|
453
|
-
end
|
|
454
|
-
|
|
455
|
-
def web_socket_authenticated?
|
|
456
|
-
@web_socket_authenticated.present?
|
|
457
|
-
end
|
|
458
|
-
|
|
459
|
-
def web_socket_outdated_ping?
|
|
460
|
-
@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
|
|
461
|
-
end
|
|
462
|
-
|
|
463
|
-
def web_socket_sequence
|
|
464
|
-
(Time.now.to_f * 1000).round.to_s
|
|
465
|
-
end
|
|
466
|
-
|
|
467
|
-
def web_socket_url
|
|
468
|
-
synchronize(:web_socket_url) do
|
|
469
|
-
@web_socket_url ||= begin
|
|
470
|
-
params = {
|
|
471
|
-
'accept' => 'ws',
|
|
472
|
-
'appid' => APP_ID,
|
|
473
|
-
'nonce' => nonce,
|
|
474
|
-
'ts' => Time.now.to_i,
|
|
475
|
-
'version' => VERSION,
|
|
476
|
-
}
|
|
477
|
-
response = rest_request(:post, '/dispatch/app', body: JSON.generate(params), headers: authentication_headers)
|
|
478
|
-
raise('Error while getting WebSocket URL') unless response['error'] == 0
|
|
479
|
-
domain = response['domain'].presence || raise("Can't get WebSocket server domain")
|
|
480
|
-
port = response['port'].presence || raise("Can't get WebSocket server port")
|
|
481
|
-
"wss://#{domain}:#{port}/api/ws".tap { |url| Ewelink.logger.debug(self.class.name) { "WebSocket URL is: #{url.inspect}" } }
|
|
482
|
-
end
|
|
483
|
-
end
|
|
484
|
-
end
|
|
485
|
-
|
|
486
|
-
def web_socket_wait_for(condition, initialize_web_socket: false)
|
|
487
|
-
web_socket if initialize_web_socket
|
|
488
|
-
begin
|
|
489
|
-
Timeout.timeout(REQUEST_TIMEOUT) do
|
|
490
|
-
sleep(WEB_SOCKET_WAIT_INTERVAL) until condition.call
|
|
491
|
-
block_given? ? yield : true
|
|
492
|
-
end
|
|
493
|
-
rescue => e
|
|
494
|
-
reload
|
|
495
|
-
raise Error.new(e)
|
|
496
|
-
end
|
|
497
|
-
end
|
|
498
|
-
|
|
499
296
|
end
|
|
500
297
|
|
|
501
298
|
end
|
data/lib/ewelink.rb
CHANGED
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: 6.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:
|
|
11
|
+
date: 2023-04-06 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -30,26 +30,6 @@ dependencies:
|
|
|
30
30
|
- - "<"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
32
|
version: 8.0.0
|
|
33
|
-
- !ruby/object:Gem::Dependency
|
|
34
|
-
name: faye-websocket
|
|
35
|
-
requirement: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - ">="
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: 0.11.0
|
|
40
|
-
- - "<"
|
|
41
|
-
- !ruby/object:Gem::Version
|
|
42
|
-
version: 0.12.0
|
|
43
|
-
type: :runtime
|
|
44
|
-
prerelease: false
|
|
45
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
46
|
-
requirements:
|
|
47
|
-
- - ">="
|
|
48
|
-
- !ruby/object:Gem::Version
|
|
49
|
-
version: 0.11.0
|
|
50
|
-
- - "<"
|
|
51
|
-
- !ruby/object:Gem::Version
|
|
52
|
-
version: 0.12.0
|
|
53
33
|
- !ruby/object:Gem::Dependency
|
|
54
34
|
name: httparty
|
|
55
35
|
requirement: !ruby/object:Gem::Requirement
|