ewelink 4.1.0 → 5.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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/lib/ewelink/api.rb +79 -55
  4. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8cba69a24adaf18a0be08cbf1618b612d6d0e90fd788d8b9f2360f7d19f67406
4
- data.tar.gz: adc650eac1533d343320490132302827a79f0fc4a0571d0dcba91ef926e5f682
3
+ metadata.gz: f2f26d6875823958f38a894377c7dfbce3531d7f98b1c1c69b18bd88caf44c1d
4
+ data.tar.gz: d25335eb91529c57d2780c42e8cef5856cb7bfa7f222e5282816bff122854b08
5
5
  SHA512:
6
- metadata.gz: 39ab9a66085401bb70f34f74b9220e70ad691c5db65b180816d5a284462162064364d6e6ed3f2b5d990f9c3fa3450472fb0b13ba0b3ecbca1a22042de3926d28
7
- data.tar.gz: 19b1627d07a411ef4fe04578f924a7d339413d65d7132b8cd7614877b1e72300b5cbd51f1f9a6469807fe3179e37a65c048f1e699601c4eb3b4f05b82b0e663f
6
+ metadata.gz: 42786625ddc6d8482a5d85922e25b4b650e080866148b378aeb579ad25e0dcee00d4f12b8ebf74492e207d80e6ed0d65b6f0c71ccdaf5c3f81280b1efbd4bc7b
7
+ data.tar.gz: 8eb9cc344312a30a7b0924ff02b052516bf5123a7d706acb15e000b91db2c91ecf56a2173d22543403f7fbbc31773e30d97d3c5379ad37ab42e34b55f8f67366
data/VERSION CHANGED
@@ -1 +1 @@
1
- 4.1.0
1
+ 5.0.0
data/lib/ewelink/api.rb CHANGED
@@ -2,13 +2,14 @@ module Ewelink
2
2
 
3
3
  class Api
4
4
 
5
- APP_ID = 'oeVkj2lYFGnJu5XUtWisfW4utiN4u9Mq'.freeze
6
- APP_SECRET = '6Nz4n0xA8s8qdxQf2GqurZj2Fs55FUvM'.freeze
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}-api.coolkit.cc:8080'.freeze
12
+ URL = 'https://#{region}-apia.coolkit.cc'.freeze
12
13
  VERSION = 8
13
14
  WEB_SOCKET_CHECK_AUTHENTICATION_TIMEOUT = 30.seconds
14
15
  WEB_SOCKET_PING_TOLERANCE_FACTOR = 1.5
@@ -40,23 +41,21 @@ module Ewelink
40
41
  process_action do
41
42
  synchronize(:press_rf_bridge_button) do
42
43
  button = find_rf_bridge_button!(uuid)
43
- web_socket_wait_for(-> { web_socket_authenticated? }, initialize_web_socket: true) do
44
- params = {
45
- 'action' => 'update',
46
- 'apikey' => button[:api_key],
47
- 'deviceid' => button[:device_id],
48
- 'params' => {
49
- 'cmd' => 'transmit',
50
- 'rfChl' => button[:channel],
51
- },
52
- 'sequence' => web_socket_sequence,
53
- 'ts' => 0,
54
- 'userAgent' => 'app',
55
- }
56
- Ewelink.logger.debug(self.class.name) { "Pressing RF bridge button #{button[:uuid].inspect}" }
57
- send_to_web_socket(JSON.generate(params))
58
- true
59
- end
44
+ headers = authentication_headers.merge(
45
+ 'X-CK-Appid' => APP_ID,
46
+ 'X-CK-Nonce' => nonce,
47
+ )
48
+ params = {
49
+ 'type' => 1,
50
+ 'id' => button[:device_id],
51
+ 'params' => {
52
+ 'cmd' => 'transmit',
53
+ 'rfChl' => button[:channel],
54
+ },
55
+ }
56
+ body = JSON.generate(params)
57
+ response = rest_request(:post, '/v2/device/thing/status', headers:, body:)
58
+ response['error'] == 0
60
59
  end
61
60
  end
62
61
  end
@@ -87,6 +86,7 @@ module Ewelink
87
86
  %i(
88
87
  @authentication_infos
89
88
  @devices
89
+ @homes_ids
90
90
  @last_web_socket_pong_at
91
91
  @region
92
92
  @rf_bridge_buttons
@@ -105,21 +105,21 @@ module Ewelink
105
105
  def rf_bridge_buttons
106
106
  synchronize(:rf_bridge_buttons) do
107
107
  @rf_bridge_buttons ||= [].tap do |buttons|
108
- rf_bridge_devices = devices.select { |device| device['uiid'] == RF_BRIDGE_DEVICE_UIID }.tap do |devices|
108
+ rf_bridge_devices = devices.select { |device| device['itemData']['extra']['uiid'] == RF_BRIDGE_DEVICE_UIID }.tap do |devices|
109
109
  Ewelink.logger.debug(self.class.name) { "Found #{devices.size} RF 433MHz bridge device(s)" }
110
110
  end
111
111
  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|
112
+ api_key = device['itemData']['apikey'].presence || next
113
+ device_id = device['itemData']['deviceid'].presence || next
114
+ device_name = device['itemData']['name'].presence || next
115
+ buttons = device['itemData']['params']['rfList'].each do |rf|
116
116
  button = {
117
117
  api_key:,
118
118
  channel: rf['rfChl'],
119
119
  device_id:,
120
120
  device_name:,
121
121
  }
122
- remote_info = device['tags']['zyx_info'].find { |info| info['buttonName'].find { |data| data.key?(button[:channel].to_s) } }.presence || next
122
+ remote_info = device['itemData']['tags']['zyx_info'].find { |info| info['buttonName'].find { |data| data.key?(button[:channel].to_s) } }.presence || next
123
123
  remote_name = remote_info['name'].try(:squish).presence || next
124
124
  button_info = remote_info['buttonName'].find { |info| info.key?(button[:channel].to_s) }.presence || next
125
125
  button_name = button_info.values.first.try(:squish).presence || next
@@ -160,15 +160,15 @@ module Ewelink
160
160
  def switches
161
161
  synchronize(:switches) do
162
162
  @switches ||= [].tap do |switches|
163
- switch_devices = devices.select { |device| SWITCH_DEVICES_UIIDS.include?(device['uiid']) }
163
+ switch_devices = devices.select { |device| SWITCH_DEVICES_UIIDS.include?(device['itemData']['extra']['uiid']) }
164
164
  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
165
+ api_key = device['itemData']['apikey'].presence || next
166
+ device_id = device['itemData']['deviceid'].presence || next
167
+ name = device['itemData']['name'].presence || next
168
168
  switch = {
169
169
  api_key:,
170
170
  device_id:,
171
- model: device['productModel'],
171
+ model: device['itemData']['productModel'],
172
172
  name:,
173
173
  }
174
174
  switch[:uuid] = Digest::UUID.uuid_v5(Digest::UUID::DNS_NAMESPACE, switch[:device_id])
@@ -242,12 +242,8 @@ module Ewelink
242
242
  synchronize(:authentication_infos) do
243
243
  @authentication_infos ||= begin
244
244
  params = {
245
- 'appid' => APP_ID,
246
- 'imei' => SecureRandom.uuid.upcase,
247
- 'nonce' => nonce,
245
+ 'countryCode' => DEFAULT_COUNTRY_CODE,
248
246
  'password' => password,
249
- 'ts' => Time.now.to_i,
250
- 'version' => VERSION,
251
247
  }
252
248
  if email.present?
253
249
  params['email'] = email
@@ -255,12 +251,20 @@ module Ewelink
255
251
  params['phoneNumber'] = phone_number
256
252
  end
257
253
  body = JSON.generate(params)
258
- response = rest_request(:post, '/api/user/login', { body:, headers: { 'Authorization' => "Sign #{Base64.encode64(OpenSSL::HMAC.digest('SHA256', APP_SECRET, body))}" } })
259
- raise(Error.new('Authentication token not found')) if response['at'].blank?
260
- raise(Error.new('API key not found')) if response['user'].blank? || response['user']['apikey'].blank?
254
+ response = rest_request(:post, '/v2/user/login', {
255
+ body:,
256
+ headers: {
257
+ 'Authorization' => "Sign #{Base64.encode64(OpenSSL::HMAC.digest('SHA256', APP_SECRET, body))}",
258
+ 'X-CK-Appid': APP_ID,
259
+ 'X-CK-Nonce': nonce,
260
+ },
261
+ })
262
+ raise(Error.new('Invalid authentication response')) unless response['data'].is_a?(Hash) && response['data']['user'].is_a?(Hash)
263
+ raise(Error.new('Authentication token not found')) unless response['data']['at'].present?
264
+ raise(Error.new('API key not found')) unless response['data']['user']['apikey'].present?
261
265
  {
262
- authentication_token: response['at'].tap { Ewelink.logger.debug(self.class.name) { 'Authentication token found' } },
263
- api_key: response['user']['apikey'].tap { Ewelink.logger.debug(self.class.name) { 'API key found' } },
266
+ api_key: response['data']['user']['apikey'].tap { Ewelink.logger.debug(self.class.name) { 'API key found' } },
267
+ authentication_token: response['data']['at'].tap { Ewelink.logger.debug(self.class.name) { 'Authentication token found' } },
264
268
  }
265
269
  end
266
270
  end
@@ -273,15 +277,22 @@ module Ewelink
273
277
  def devices
274
278
  synchronize(:devices) do
275
279
  @devices ||= begin
276
- params = {
277
- 'appid' => APP_ID,
278
- 'getTags' => 1,
279
- 'nonce' => nonce,
280
- 'ts' => Time.now.to_i,
281
- 'version' => VERSION,
282
- }
283
- response = rest_request(:get, '/api/user/device', headers: authentication_headers, query: params)
284
- response['devicelist'].tap { |devices| Ewelink.logger.debug(self.class.name) { "Found #{devices.size} device(s)" } }
280
+ devices = []
281
+ homes_ids.each do |home_id|
282
+ params = {
283
+ 'familyid' => home_id,
284
+ 'num' => 0,
285
+ }
286
+ headers = authentication_headers.merge(
287
+ 'X-CK-Appid' => APP_ID,
288
+ 'X-CK-Nonce' => nonce,
289
+ )
290
+ response = rest_request(:get, '/v2/device/thing', headers:, query: params)
291
+ raise('Invalid devices response') unless response['data'].is_a?(Hash) && response['data']['thingList'].is_a?(Array)
292
+ devices += response['data']['thingList']
293
+ end
294
+ Ewelink.logger.debug(self.class.name) { "Found #{devices.size} device(s)" }
295
+ devices
285
296
  end
286
297
  end
287
298
  end
@@ -294,6 +305,20 @@ module Ewelink
294
305
  switches.find { |switch| switch[:uuid] == uuid } || raise(Error.new("No such switch with UUID: #{uuid.inspect}"))
295
306
  end
296
307
 
308
+ def homes_ids
309
+ synchronize(:homes_ids) do
310
+ @homes_ids ||= begin
311
+ headers = authentication_headers.merge(
312
+ 'X-CK-Appid' => APP_ID,
313
+ 'X-CK-Nonce' => nonce,
314
+ )
315
+ response = rest_request(:get, '/v2/family', headers:)
316
+ raise('Invalid homes response') unless response['data'].is_a?(Hash) && response['data']['familyList'].is_a?(Array)
317
+ response['data']['familyList'].map { |home| home['id'] }.compact.uniq
318
+ end
319
+ end
320
+ end
321
+
297
322
  def nonce
298
323
  SecureRandom.hex[0, 8]
299
324
  end
@@ -311,17 +336,17 @@ module Ewelink
311
336
 
312
337
  def rest_request(method, path, options = {})
313
338
  url = "#{URL.gsub('#{region}', region)}#{path}"
339
+ url.gsub!(/#{Regexp.escape("#{region}-api")}/, "#{region}-disp") if options[:dispatch].present?
314
340
  method = method.to_s.upcase
315
341
  headers = (options[:headers] || {}).reverse_merge('Content-Type' => 'application/json')
316
342
  Ewelink.logger.debug(self.class.name) { "#{method} #{url}" }
317
343
  response = HTTParty.send(method.downcase, url, options.merge(headers:).reverse_merge(timeout: REQUEST_TIMEOUT))
318
344
  raise(Error.new("#{method} #{url}: #{response.code}")) unless response.success?
319
- if response['error'] == 301 && response['region'].present?
320
- @region = response['region']
345
+ if response['error'] == 10_004 && response['data'].present? && response['data']['region'].present?
346
+ @region = response['data']['region']
321
347
  Ewelink.logger.debug(self.class.name) { "Switched to region #{region.inspect}" }
322
348
  return rest_request(method, path, options)
323
349
  end
324
- remove_instance_variable(:@authentication_infos) if instance_variable_defined?(:@authentication_infos) && [401, 403].include?(response['error'])
325
350
  raise(Error.new("#{method} #{url}: #{response['error']} #{response['msg']}".strip)) if response['error'].present? && response['error'] != 0
326
351
  response.to_h
327
352
  rescue Errno::ECONNREFUSED, OpenSSL::OpenSSLError, SocketError, Timeout::Error => e
@@ -468,13 +493,12 @@ module Ewelink
468
493
  synchronize(:web_socket_url) do
469
494
  @web_socket_url ||= begin
470
495
  params = {
471
- 'accept' => 'ws',
472
496
  'appid' => APP_ID,
473
497
  'nonce' => nonce,
474
498
  'ts' => Time.now.to_i,
475
499
  'version' => VERSION,
476
500
  }
477
- response = rest_request(:post, '/dispatch/app', body: JSON.generate(params), headers: authentication_headers)
501
+ response = rest_request(:post, '/dispatch/app', body: JSON.generate(params), dispatch: true, headers: authentication_headers)
478
502
  raise('Error while getting WebSocket URL') unless response['error'] == 0
479
503
  domain = response['domain'].presence || raise("Can't get WebSocket server domain")
480
504
  port = response['port'].presence || raise("Can't get WebSocket server port")
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.1.0
4
+ version: 5.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: 2022-03-20 00:00:00.000000000 Z
11
+ date: 2022-07-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport