ewelink 1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e6cbd50164b2f25247423b0cece7baee8ec1e6bbc49faa2545ad4c7450feaf3d
4
+ data.tar.gz: cf1e6148a4cebdb5efd52acfb66feacaa096d51ca2f1cd0b06e7c45b002c6fa1
5
+ SHA512:
6
+ metadata.gz: e42fdd3244a0a9bb3a176c4ab760ba6100b8a8e9146c68b135044610075a4963d7e2d03df4cec6ae97febdcff21966d638ba9f4b6dd8e774c436acdf75174e0b
7
+ data.tar.gz: 204e83b06e6b788095352b97f5553291ec20bca0e4191695abc30f72cf898be9194bc04b3518fd61220f45250a90dac6054cfc184bdc10400639c8dca70efbaa
data/README.mdown ADDED
@@ -0,0 +1,84 @@
1
+ # Ewelink
2
+
3
+ Ruby API to manage eWeLink smart home devices.
4
+
5
+ ## Installation
6
+
7
+ Just add this into your `Gemfile`:
8
+
9
+ ```ruby
10
+ gem 'ewelink'
11
+ ```
12
+
13
+ Then, just run `bundle install`.
14
+
15
+ ## Examples
16
+
17
+ ### Displaying all switches
18
+
19
+ ```ruby
20
+ require 'ewelink'
21
+
22
+ api = Ewelink::Api.new(email: 'john@example.com', password: 'secr$t')
23
+ api.switches.each do |switch|
24
+ puts switch[:name]
25
+ puts switch[:uuid]
26
+ end
27
+ ```
28
+
29
+ `email` or `phone_number` must be specified for authentication.
30
+
31
+ ### Displaying all RF bridge buttons
32
+
33
+ ```ruby
34
+ require 'ewelink'
35
+
36
+ api = Ewelink::Api.new(email: 'john@example.com', password: 'secr$t')
37
+ api.rf_bridge_buttons.each do |button|
38
+ puts button[:name]
39
+ puts button[:uuid]
40
+ end
41
+ ```
42
+
43
+ ### Set switch on or off
44
+
45
+ ```ruby
46
+ require 'ewelink'
47
+
48
+ api = Ewelink::Api.new(phone_number: '+687 414243', password: 'secr$t')
49
+ api.switch_on!(switch[:uuid])
50
+ api.switch_off!(switch[:uuid])
51
+ ```
52
+
53
+ ### Check if switch is on or off
54
+
55
+ ```ruby
56
+ require 'ewelink'
57
+
58
+ api = Ewelink::Api.new(phone_number: '+687 414243', password: 'secr$t')
59
+ puts api.switch_on?(switch[:uuid])
60
+ puts api.switch_off?(switch[:uuid])
61
+ ```
62
+
63
+ ### Press RF bridge button
64
+
65
+ ```ruby
66
+ require 'ewelink'
67
+
68
+ api = Ewelink::Api.new(email: 'john@example.com', password: 'secr$t')
69
+ api.press_rf_bridge_button!(button[:uuid])
70
+ ```
71
+
72
+ ### Configuring logger
73
+
74
+ In order to have some debug informations about what kagu does, you could
75
+ configure its logger:
76
+
77
+ ```ruby
78
+ Ewelink.logger = Logger.new(STDERR)
79
+ ```
80
+
81
+ ### Executable
82
+
83
+ This gem also provides a `ewelink` executable, just run it with
84
+ `--help` option to get all available options.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
data/bin/ewelink ADDED
@@ -0,0 +1,17 @@
1
+ #! /usr/bin/env ruby
2
+
3
+ require_relative '../lib/ewelink'
4
+
5
+ Ewelink.logger = Logger.new(STDOUT, formatter: -> (severity, time, progname, message) {
6
+ text = ''
7
+ text << "[#{progname}] " if progname.present?
8
+ text << message.to_s << "\n"
9
+ })
10
+ Ewelink.logger.level = :warn
11
+
12
+ begin
13
+ Ewelink::Runner.new.run
14
+ rescue => e
15
+ Ewelink.logger.fatal(Ewelink::Runner.name) { e }
16
+ exit(1)
17
+ end
data/ewelink.gemspec ADDED
@@ -0,0 +1,23 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'ewelink'
3
+ s.version = File.read("#{File.dirname(__FILE__)}/VERSION").strip
4
+ s.platform = Gem::Platform::RUBY
5
+ s.author = 'Alexis Toulotte'
6
+ s.email = 'al@alweb.org'
7
+ s.homepage = 'https://github.com/alexistoulotte/ewelink'
8
+ s.summary = 'Manage eWeLink devices'
9
+ s.description = 'Manage eWeLink smart home devices'
10
+ s.license = 'MIT'
11
+
12
+ s.files = `git ls-files | grep -vE '^(spec/|test/|\\.|Gemfile|Rakefile)'`.split("\n")
13
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
14
+ s.require_paths = ['lib']
15
+
16
+ s.required_ruby_version = '>= 2.0.0'
17
+
18
+ s.add_dependency 'activesupport', '>= 6.0.0', '< 7.0.0'
19
+ s.add_dependency 'httparty', '>= 0.18.0', '< 0.19.0'
20
+
21
+ s.add_development_dependency 'byebug', '>= 11.0.0', '< 12.0.0'
22
+ s.add_development_dependency 'rake', '>= 12.0.0', '< 13.0.0'
23
+ end
@@ -0,0 +1,226 @@
1
+ module Ewelink
2
+
3
+ class Api
4
+
5
+ APP_ID = 'oeVkj2lYFGnJu5XUtWisfW4utiN4u9Mq'
6
+ APP_SECRET = '6Nz4n0xA8s8qdxQf2GqurZj2Fs55FUvM'
7
+ DEFAULT_REGION = 'us'
8
+ RF_BRIDGE_DEVICE_UIID = 28
9
+ SWITCH_DEVICES_UIIDS = [1, 5, 6, 24]
10
+ TIMEOUT = 10
11
+ URL = 'https://#{region}-api.coolkit.cc:8080'
12
+ UUID_NAMESPACE = 'e25750fb-3710-41af-b831-23224f4dd609';
13
+ VERSION = 8
14
+
15
+ attr_reader :email, :password, :phone_number
16
+
17
+ def initialize(email: nil, password:, phone_number: nil)
18
+ @email = email.presence.try(:strip)
19
+ @mutexs = {}
20
+ @password = password.presence || raise(Error.new(":password must be specified"))
21
+ @phone_number = phone_number.presence.try(:strip)
22
+ raise(Error.new(":email or :phone_number must be specified")) if email.blank? && phone_number.blank?
23
+ end
24
+
25
+ def press_rf_bridge_button!(uuid)
26
+ button = find_rf_bridge_button!(uuid)
27
+ params = {
28
+ 'appid' => APP_ID,
29
+ 'deviceid' => button[:device_id],
30
+ 'nonce' => nonce,
31
+ 'params' => {
32
+ 'cmd' => 'transmit',
33
+ 'rfChl' => button[:channel],
34
+ },
35
+ 'ts' => Time.now.to_i,
36
+ 'version' => VERSION,
37
+ }
38
+ http_request(:post, '/api/user/device/status', body: JSON.generate(params), headers: authentication_headers)
39
+ true
40
+ end
41
+
42
+ def reload
43
+ Ewelink.logger.debug(self.class.name) { 'Reloading API (authentication token, devices & region cache)' }
44
+ [:@authentication_token, :@devices, :@rf_bridge_buttons, :@region, :@switches].each do |variable|
45
+ remove_instance_variable(variable) if instance_variable_defined?(variable)
46
+ end
47
+ self
48
+ end
49
+
50
+ def rf_bridge_buttons
51
+ @rf_bridge_buttons ||= [].tap do |buttons|
52
+ rf_bridge_devices = devices.select { |device| device['uiid'] == RF_BRIDGE_DEVICE_UIID }.tap do |devices|
53
+ Ewelink.logger.debug(self.class.name) { "Found #{devices.size} RF 433MHz Bridge device(s)" }
54
+ end
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
75
+ end
76
+ end
77
+ end.tap { |buttons| Ewelink.logger.debug(self.class.name) { "Found #{buttons.size} RF 433MHz bridge button(s)" } }
78
+ end
79
+
80
+ def switch_off!(uuid)
81
+ update_switch_on!(uuid, false)
82
+ end
83
+
84
+ def switch_off?(uuid)
85
+ !switch_on?(uuid)
86
+ end
87
+
88
+ def switch_on!(uuid)
89
+ update_switch_on!(uuid, true)
90
+ end
91
+
92
+ def switch_on?(uuid)
93
+ switch = find_switch!(uuid)
94
+ params = {
95
+ 'appid' => APP_ID,
96
+ 'deviceid' => switch[:device_id],
97
+ 'nonce' => nonce,
98
+ 'ts' => Time.now.to_i,
99
+ 'version' => VERSION,
100
+ }
101
+ response = http_request(:get, '/api/user/device/status', headers: authentication_headers, query: params)
102
+ response['params']['switch'] == 'on'
103
+ end
104
+
105
+ def switches
106
+ @switches ||= [].tap do |switches|
107
+ switch_devices = devices.select { |device| SWITCH_DEVICES_UIIDS.include?(device['uiid']) }.tap do |devices|
108
+ Ewelink.logger.debug(self.class.name) { "Found #{devices.size} switch device(s)" }
109
+ end
110
+ switch_devices.each do |device|
111
+ device_id = device['deviceid'].presence || next
112
+ name = device['name'].presence || next
113
+ switch = {
114
+ device_id: device_id,
115
+ name: name,
116
+ }
117
+ switch[:uuid] = Digest::UUID.uuid_v5(UUID_NAMESPACE, switch[:device_id])
118
+ switches << switch
119
+ end
120
+ end.tap { |switches| Ewelink.logger.debug(self.class.name) { "Found #{switches.size} switch(es)" } }
121
+ end
122
+
123
+ private
124
+
125
+ def authentication_headers
126
+ { 'Authorization' => "Bearer #{authentication_token}" }
127
+ end
128
+
129
+ def authentication_token
130
+ synchronize(:authentication_token) do
131
+ @authentication_token ||= begin
132
+ params = {
133
+ 'appid' => APP_ID,
134
+ 'imei' => SecureRandom.uuid.upcase,
135
+ 'nonce' => nonce,
136
+ 'password' => password,
137
+ 'ts' => Time.now.to_i,
138
+ 'version' => VERSION,
139
+ }
140
+ if email.present?
141
+ params['email'] = email
142
+ else
143
+ params['phoneNumber'] = phone_number
144
+ end
145
+ body = JSON.generate(params)
146
+ response = http_request(:post, '/api/user/login', { body: body, headers: { 'Authorization' => "Sign #{Base64.encode64(OpenSSL::HMAC.digest('SHA256', APP_SECRET, body))}" } })
147
+ raise(Error.new('Authentication token not found')) if response['at'].blank?
148
+ response['at'].tap { Ewelink.logger.debug(self.class.name) { 'Authentication token found' } }
149
+ end
150
+ end
151
+ end
152
+
153
+ def devices
154
+ synchronize(:devices) do
155
+ @devices ||= begin
156
+ params = {
157
+ 'appid' => APP_ID,
158
+ 'getTags' => 1,
159
+ 'nonce' => nonce,
160
+ 'ts' => Time.now.to_i,
161
+ 'version' => VERSION,
162
+ }
163
+ response = http_request(:get, '/api/user/device', headers: authentication_headers, query: params)
164
+ response['devicelist'].tap { |devices| Ewelink.logger.debug(self.class.name) { "Found #{devices.size} device(s)" } }
165
+ end
166
+ end
167
+ end
168
+
169
+ def find_rf_bridge_button!(uuid)
170
+ rf_bridge_buttons.find { |button| button[:uuid] == uuid } || raise(Error.new("No such RF bridge button with UUID: #{uuid.inspect}"))
171
+ end
172
+
173
+ def find_switch!(uuid)
174
+ switches.find { |switch| switch[:uuid] == uuid } || raise(Error.new("No such switch with UUID: #{uuid.inspect}"))
175
+ end
176
+
177
+ def nonce
178
+ SecureRandom.hex[0, 8]
179
+ end
180
+
181
+ def region
182
+ @region ||= DEFAULT_REGION
183
+ end
184
+
185
+ def http_request(method, path, options = {})
186
+ url = "#{URL.gsub('#{region}', region)}#{path}"
187
+ method = method.to_s.upcase
188
+ headers = (options[:headers] || {}).reverse_merge('Content-Type' => 'application/json')
189
+ Ewelink.logger.debug(self.class.name) { "#{method} #{url}" }
190
+ response = synchronize(:http_request) { HTTParty.send(method.downcase, url, options.merge(headers: headers).reverse_merge(timeout: TIMEOUT)) }
191
+ raise(Error.new("#{method} #{url}: #{response.code}")) unless response.success?
192
+ if response['error'] == 301 && response['region'].present?
193
+ @region = response['region']
194
+ Ewelink.logger.debug(self.class.name) { "Switched to region #{region.inspect}" }
195
+ return http_request(method, path, options)
196
+ end
197
+ remove_instance_variable(:@authentication_token) if instance_variable_defined?(:@authentication_token) && [401, 403].include?(response['error'])
198
+ raise(Error.new("#{method} #{url}: #{response['error']} #{response['msg']}".strip)) if response['error'].present? && response['error'] != 0
199
+ response
200
+ rescue Errno::ECONNREFUSED, OpenSSL::OpenSSLError, SocketError, Timeout::Error => e
201
+ raise Error.new(e)
202
+ end
203
+
204
+ def synchronize(name, &block)
205
+ (@mutexs[name] ||= Mutex.new).synchronize(&block)
206
+ end
207
+
208
+ def update_switch_on!(uuid, on)
209
+ switch = find_switch!(uuid)
210
+ params = {
211
+ 'appid' => APP_ID,
212
+ 'deviceid' => switch[:device_id],
213
+ 'nonce' => nonce,
214
+ 'params' => {
215
+ 'switch' => on ? 'on' : 'off',
216
+ },
217
+ 'ts' => Time.now.to_i,
218
+ 'version' => VERSION,
219
+ }
220
+ http_request(:post, '/api/user/device/status', body: JSON.generate(params), headers: authentication_headers)
221
+ true
222
+ end
223
+
224
+ end
225
+
226
+ end
@@ -0,0 +1,6 @@
1
+ module Ewelink
2
+
3
+ class Error < StandardError
4
+ end
5
+
6
+ end
@@ -0,0 +1,78 @@
1
+ module Ewelink
2
+
3
+ class Runner
4
+
5
+ def run
6
+ api = Api.new(options.slice(:email, :password, :phone_number))
7
+ puts(JSON.pretty_generate(api.switches)) if options[:list_switches]
8
+ puts(JSON.pretty_generate(api.rf_bridge_buttons)) if options[:list_rf_bridge_buttons]
9
+ options[:switches_on_uuids].each { |uuid| api.switch_on!(uuid) }
10
+ options[:switches_off_uuids].each { |uuid| api.switch_off!(uuid) }
11
+ options[:press_rf_bridge_buttons_uuids].each { |uuid| api.press_rf_bridge_button!(uuid) }
12
+ end
13
+
14
+ private
15
+
16
+ def options
17
+ @options ||= begin
18
+ options = { press_rf_bridge_buttons_uuids: [], switches_off_uuids: [], switches_on_uuids: [] }
19
+ parser = OptionParser.new do |opts|
20
+ opts.banner = 'Manage eWeLink smart home devices'
21
+ opts.version = File.read(File.expand_path('../../VERSION', __dir__)).strip
22
+ opts.separator('')
23
+ opts.separator('Usage: ewelink [options]')
24
+ opts.separator('')
25
+ opts.on('-e', '--email EMAIL', "eWeLink account's email (mandatory if phone number is not specified)") do |email|
26
+ options[:email] = email
27
+ end
28
+ opts.on('-p', '--password PASSWORD', "eWeLink account's password (mandatory, prompted if not specified on command line)") do |password|
29
+ options[:password] = password
30
+ end
31
+ opts.on('-n', '--phone-number PHONE_NUMBER', "eWeLink account's phone number (mandatory if email is not specified)") do |phone_number|
32
+ options[:phone_number] = phone_number
33
+ end
34
+ opts.on('--list-switches', 'List all switches in JSON format') do
35
+ options[:list_switches] = true
36
+ end
37
+ opts.on('--list-rf-bridge-buttons', 'List all RF 433MHz bridge buttons in JSON format') do
38
+ options[:list_rf_bridge_buttons] = true
39
+ end
40
+ opts.on('--switch-on SWITCH_UUID', 'Set the switch with specified UUID on') do |uuid|
41
+ options[:switches_on_uuids] << uuid
42
+ end
43
+ opts.on('--switch-off SWITCH_UUID', 'Set the switch with specified UUID off') do |uuid|
44
+ options[:switches_off_uuids] << uuid
45
+ end
46
+ opts.on('--press-rf-bridge-button BUTTON_UUID', 'Press RF 433MHz bridge button with specified UUID') do |uuid|
47
+ options[:press_rf_bridge_buttons_uuids] << uuid
48
+ end
49
+ opts.on('-v', '--verbose', 'Verbose mode') do
50
+ Ewelink.logger.level = :debug
51
+ end
52
+ end
53
+ arguments = parser.parse!
54
+ if arguments.any?
55
+ STDERR.puts("Invalid option specified: #{arguments.first}")
56
+ STDERR.puts(parser.summarize)
57
+ exit(1)
58
+ end
59
+ if options[:email].blank? && options[:phone_number].blank?
60
+ STDERR.puts('Email or phone number must be specified')
61
+ STDERR.puts(parser.summarize)
62
+ exit(1)
63
+ end
64
+ if [:list_switches, :list_rf_bridge_buttons, :switches_on_uuids, :switches_off_uuids, :press_rf_bridge_buttons_uuids].map { |action| options[action] }.all?(&:blank?)
65
+ STDERR.puts('An action must be specified (listing switches, press RF bridge button, etc.)')
66
+ STDERR.puts(parser.summarize)
67
+ exit(1)
68
+ end
69
+ while options[:password].blank?
70
+ options[:password] = IO::console.getpass("Enter eWeLink account's password: ")
71
+ end
72
+ options
73
+ end
74
+ end
75
+
76
+ end
77
+
78
+ end
data/lib/ewelink.rb ADDED
@@ -0,0 +1,20 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext'
3
+ require 'byebug' if ENV['DEBUGGER']
4
+ require 'httparty'
5
+ require 'io/console'
6
+ require 'json'
7
+ require 'logger'
8
+ require 'openssl'
9
+ require 'optparse'
10
+
11
+ module Ewelink
12
+
13
+ mattr_accessor :logger
14
+ self.logger = Logger.new(nil)
15
+
16
+ end
17
+
18
+ require_relative 'ewelink/api'
19
+ require_relative 'ewelink/error'
20
+ require_relative 'ewelink/runner'
metadata ADDED
@@ -0,0 +1,131 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ewelink
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Alexis Toulotte
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-04-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 6.0.0
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: 7.0.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 6.0.0
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: 7.0.0
33
+ - !ruby/object:Gem::Dependency
34
+ name: httparty
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: 0.18.0
40
+ - - "<"
41
+ - !ruby/object:Gem::Version
42
+ version: 0.19.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.18.0
50
+ - - "<"
51
+ - !ruby/object:Gem::Version
52
+ version: 0.19.0
53
+ - !ruby/object:Gem::Dependency
54
+ name: byebug
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: 11.0.0
60
+ - - "<"
61
+ - !ruby/object:Gem::Version
62
+ version: 12.0.0
63
+ type: :development
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: 11.0.0
70
+ - - "<"
71
+ - !ruby/object:Gem::Version
72
+ version: 12.0.0
73
+ - !ruby/object:Gem::Dependency
74
+ name: rake
75
+ requirement: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: 12.0.0
80
+ - - "<"
81
+ - !ruby/object:Gem::Version
82
+ version: 13.0.0
83
+ type: :development
84
+ prerelease: false
85
+ version_requirements: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: 12.0.0
90
+ - - "<"
91
+ - !ruby/object:Gem::Version
92
+ version: 13.0.0
93
+ description: Manage eWeLink smart home devices
94
+ email: al@alweb.org
95
+ executables:
96
+ - ewelink
97
+ extensions: []
98
+ extra_rdoc_files: []
99
+ files:
100
+ - README.mdown
101
+ - VERSION
102
+ - bin/ewelink
103
+ - ewelink.gemspec
104
+ - lib/ewelink.rb
105
+ - lib/ewelink/api.rb
106
+ - lib/ewelink/error.rb
107
+ - lib/ewelink/runner.rb
108
+ homepage: https://github.com/alexistoulotte/ewelink
109
+ licenses:
110
+ - MIT
111
+ metadata: {}
112
+ post_install_message:
113
+ rdoc_options: []
114
+ require_paths:
115
+ - lib
116
+ required_ruby_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: 2.0.0
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ requirements: []
127
+ rubygems_version: 3.0.3
128
+ signing_key:
129
+ specification_version: 4
130
+ summary: Manage eWeLink devices
131
+ test_files: []