ecobee 0.2.3 → 0.3.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.md +2 -5
- data/examples/set_mode.rb +17 -19
- data/lib/ecobee.rb +12 -8
- data/lib/ecobee/http.rb +143 -0
- data/lib/ecobee/register.rb +19 -20
- data/lib/ecobee/thermostat.rb +7 -7
- data/lib/ecobee/token.rb +79 -62
- data/lib/ecobee/version.rb +1 -1
- metadata +3 -3
- data/lib/ecobee/client.rb +0 -75
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 48dab0a8186e1abe5f05115a744bf06a6167b718
|
4
|
+
data.tar.gz: d76e8de7fbf97ae44095436197f1d7085a324d9d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7e5ca066357160e4f43a67d90f6186437cf98f7a1105f1dba652eac447c588cec03c43f8c2e44f3caf791a0eabca9fc4b64f213f4a588e32c0ad9411cade0c05
|
7
|
+
data.tar.gz: 0bbc4eb430f37858787f915b10ab4af799fac335651344357e41becf7a883e3dd090d9359438e04ab0b9de50671df6cd4ab084b051b8ed244be8b777142c157a
|
data/README.md
CHANGED
@@ -5,17 +5,14 @@ Ecobee API Ruby Gem. Implements:
|
|
5
5
|
- Persistent HTTP connection management
|
6
6
|
- Methods for GET & POST requests w/ JSON parsing & error handling
|
7
7
|
- Persistent storage for API key & refresh tokens
|
8
|
+
- Storage uses iCloud Drive if available for shared computer use
|
8
9
|
- Block/Proc hooks for token storage load/save to add app config data
|
9
|
-
-
|
10
|
+
- Thermostat abstraction class for simple thermostat interaction
|
10
11
|
- Example usage scripts (see /examples/\*)
|
11
12
|
|
12
13
|
TODO:
|
13
|
-
- Add dedicated symbol class to Thermostat
|
14
14
|
- Add RDoc documentation
|
15
|
-
- Add timeout to Ecobee::Token#wait
|
16
15
|
- Add redirect based registration
|
17
|
-
- Implement throttling algorithm based on API feedback
|
18
|
-
- Create examples of proper error handling
|
19
16
|
|
20
17
|
## Installation
|
21
18
|
|
data/examples/set_mode.rb
CHANGED
@@ -1,25 +1,25 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
#
|
3
|
+
# Example of using the Ecobee::HTTP API directly (bypassing the
|
4
|
+
# Ecobee::Thermostat abstraction class).
|
5
|
+
#
|
3
6
|
# Loops through menu showing current thermostat mode, with option
|
4
|
-
# to change the mode.
|
7
|
+
# to change the mode.
|
5
8
|
|
6
9
|
require 'pp'
|
7
|
-
|
8
10
|
require_relative '../lib/ecobee'
|
9
|
-
|
10
11
|
@hvac_modes = Ecobee::HVAC_MODES + ['quit']
|
11
12
|
|
12
13
|
class TestFunctions
|
13
|
-
def initialize(
|
14
|
-
@
|
14
|
+
def initialize(token)
|
15
|
+
@http = token.http
|
15
16
|
end
|
16
17
|
|
17
18
|
def print_summary
|
18
|
-
response = @
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
))
|
19
|
+
response = @http.get(arg: :thermostat,
|
20
|
+
options: Ecobee::Selection(
|
21
|
+
includeEquipmentStatus: true,
|
22
|
+
includeSettings: true))
|
23
23
|
|
24
24
|
puts "Found %d thermostats." % response['thermostatList'].length
|
25
25
|
|
@@ -36,12 +36,12 @@ class TestFunctions
|
|
36
36
|
end
|
37
37
|
|
38
38
|
def update_mode(mode)
|
39
|
-
@
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
39
|
+
@http.post(arg: :thermostat,
|
40
|
+
body: {
|
41
|
+
'thermostat' => {
|
42
|
+
'settings' => { 'hvacMode' => mode }
|
43
|
+
}
|
44
|
+
}.merge(Ecobee::Selection()))
|
45
45
|
end
|
46
46
|
end
|
47
47
|
|
@@ -52,9 +52,7 @@ if token.pin
|
|
52
52
|
token.wait
|
53
53
|
end
|
54
54
|
|
55
|
-
test_functions = TestFunctions.new(
|
56
|
-
Ecobee::Client.new(token: token)
|
57
|
-
)
|
55
|
+
test_functions = TestFunctions.new(token)
|
58
56
|
|
59
57
|
loop do
|
60
58
|
test_functions.print_summary
|
data/lib/ecobee.rb
CHANGED
@@ -2,18 +2,26 @@ require 'pp'
|
|
2
2
|
require 'json'
|
3
3
|
require 'net/http'
|
4
4
|
|
5
|
-
require_relative 'ecobee/
|
5
|
+
require_relative 'ecobee/http'
|
6
6
|
require_relative 'ecobee/register'
|
7
7
|
require_relative 'ecobee/thermostat'
|
8
8
|
require_relative 'ecobee/token'
|
9
9
|
require_relative 'ecobee/version'
|
10
10
|
|
11
11
|
module Ecobee
|
12
|
+
|
13
|
+
class HTTPError < StandardError ; end
|
14
|
+
|
15
|
+
class AuthError < HTTPError ; end
|
16
|
+
|
12
17
|
API_HOST = 'api.ecobee.com'
|
13
18
|
API_PORT = 443
|
19
|
+
API_URI_BASE= "https://#{API_HOST}:#{API_PORT}"
|
14
20
|
|
15
21
|
CONTENT_TYPE = ['application/json', { 'charset' => 'UTF-8' }]
|
16
22
|
|
23
|
+
DEFAULT_POLL_INTERVAL = 30
|
24
|
+
|
17
25
|
DEFAULT_FILES = [
|
18
26
|
'~/Library/Mobile Documents/com~apple~CloudDocs/.ecobee_token',
|
19
27
|
'~/.ecobee_token'
|
@@ -30,16 +38,12 @@ module Ecobee
|
|
30
38
|
|
31
39
|
HVAC_MODES = %w{auto auxHeatOnly cool heat off}
|
32
40
|
|
41
|
+
MAX_LOG_LENGTH = 1200
|
42
|
+
|
33
43
|
REFRESH_PAD = 30
|
34
|
-
REFRESH_TOKEN_CHECK = 10
|
35
44
|
|
36
45
|
SCOPES = [:smartWrite, :smartRead]
|
37
|
-
|
38
|
-
URL_BASE= "https://#{API_HOST}:#{API_PORT}"
|
39
|
-
URL_API = "#{URL_BASE}/1/"
|
40
|
-
URL_GET_PIN = URL_BASE +
|
41
|
-
'/authorize?response_type=ecobeePin&client_id=%s&scope=%s'
|
42
|
-
URL_TOKEN = "#{URL_BASE}/token"
|
46
|
+
DEFAULT_SCOPE = SCOPES[1]
|
43
47
|
|
44
48
|
def self.FanMode(mode)
|
45
49
|
{ 'auto' => 'Auto',
|
data/lib/ecobee/http.rb
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
module Ecobee
|
2
|
+
|
3
|
+
class HTTPError < StandardError ; end
|
4
|
+
class AuthError < HTTPError ; end
|
5
|
+
|
6
|
+
class HTTP
|
7
|
+
|
8
|
+
def initialize(log_file: nil, token: nil)
|
9
|
+
raise ArgumentError.new('Missing token') unless token
|
10
|
+
@token = token
|
11
|
+
open_log log_file
|
12
|
+
http
|
13
|
+
end
|
14
|
+
|
15
|
+
def get(
|
16
|
+
arg: nil,
|
17
|
+
no_auth: false,
|
18
|
+
resource_prefix: '1/',
|
19
|
+
retries: 3,
|
20
|
+
options: nil,
|
21
|
+
validate_status: true
|
22
|
+
)
|
23
|
+
uri = URI.escape(sprintf("#{Ecobee::API_URI_BASE}/%s%s%s",
|
24
|
+
resource_prefix,
|
25
|
+
arg.to_s.sub(/^\//, ''),
|
26
|
+
options ? "?json=#{options.to_json}" : ''))
|
27
|
+
log "http.get uri=#{uri}"
|
28
|
+
request = Net::HTTP::Get.new(URI(uri))
|
29
|
+
request['Content-Type'] = *CONTENT_TYPE
|
30
|
+
request['Authorization'] = @token.authorization unless no_auth
|
31
|
+
response = nil
|
32
|
+
retries.times do
|
33
|
+
http_response = http.request request
|
34
|
+
response = JSON.parse(http_response.body)
|
35
|
+
log "http.get response=#{response.pretty_inspect}"
|
36
|
+
response = validate_status(response) if validate_status
|
37
|
+
break unless response == :retry
|
38
|
+
sleep 3
|
39
|
+
end
|
40
|
+
case response
|
41
|
+
when :retry
|
42
|
+
raise Ecobee::HTTPError.new('HTTP.get: retries exhausted')
|
43
|
+
else
|
44
|
+
response
|
45
|
+
end
|
46
|
+
rescue SocketError => msg
|
47
|
+
raise Ecobee::HTTPError.new("HTTP.get SocketError => #{msg}")
|
48
|
+
rescue JSON::ParserError => msg
|
49
|
+
raise Ecobee::HTTPError.new("HTTP.get JSON::ParserError => #{msg}")
|
50
|
+
end
|
51
|
+
|
52
|
+
def log(arg)
|
53
|
+
return unless @log_fh
|
54
|
+
if arg.length > MAX_LOG_LENGTH
|
55
|
+
arg = arg.slice(0, MAX_LOG_LENGTH).chomp + "\n ...truncated..."
|
56
|
+
end
|
57
|
+
@log_fh.puts "#{Time.now} #{arg.chomp}"
|
58
|
+
@log_fh.flush
|
59
|
+
end
|
60
|
+
|
61
|
+
def post(
|
62
|
+
arg: nil,
|
63
|
+
body: nil,
|
64
|
+
no_auth: false,
|
65
|
+
resource_prefix: '1/',
|
66
|
+
retries: 3,
|
67
|
+
options: {},
|
68
|
+
validate_status: true
|
69
|
+
)
|
70
|
+
uri = URI.escape(sprintf("#{Ecobee::API_URI_BASE}/%s%s%s",
|
71
|
+
resource_prefix,
|
72
|
+
arg.to_s.sub(/^\//, ''),
|
73
|
+
options.length > 0 ? "?json=#{options.to_json}" : ''))
|
74
|
+
log "http.post uri=#{uri}"
|
75
|
+
request = Net::HTTP::Post.new(URI(uri))
|
76
|
+
request['Content-Type'] = *CONTENT_TYPE
|
77
|
+
request['Authorization'] = @token.authorization unless no_auth
|
78
|
+
if body
|
79
|
+
log "http.post body=#{body.pretty_inspect}"
|
80
|
+
request.body = JSON.generate(body)
|
81
|
+
elsif options.length > 0
|
82
|
+
request.set_form_data({ 'format' => 'json' }.merge(options))
|
83
|
+
end
|
84
|
+
response = nil
|
85
|
+
retries.times do
|
86
|
+
http_response = http.request request
|
87
|
+
response = JSON.parse(http_response.body)
|
88
|
+
log "http.post response=#{response.pretty_inspect}"
|
89
|
+
response = validate_status(response) if validate_status
|
90
|
+
break unless response == :retry
|
91
|
+
sleep 3
|
92
|
+
end
|
93
|
+
case response
|
94
|
+
when :retry
|
95
|
+
raise Ecobee::HTTPError.new('HTTP.get: retries exhausted')
|
96
|
+
else
|
97
|
+
response
|
98
|
+
end
|
99
|
+
rescue SocketError => msg
|
100
|
+
raise Ecobee::HTTPError.new("HTTP.get SocketError => #{msg}")
|
101
|
+
rescue JSON::ParserError => msg
|
102
|
+
raise Ecobee::HTTPError.new("HTTP.get JSON::ParserError => #{msg}")
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
def http
|
108
|
+
@http ||= Net::HTTP.new(API_HOST, API_PORT)
|
109
|
+
unless @http.active?
|
110
|
+
@http.use_ssl = true
|
111
|
+
Net::HTTP.start(API_HOST, API_PORT)
|
112
|
+
end
|
113
|
+
@http
|
114
|
+
end
|
115
|
+
|
116
|
+
def open_log(log_file)
|
117
|
+
return unless log_file
|
118
|
+
log_file = File.expand_path log_file
|
119
|
+
@log_fh = File.new(log_file, 'a')
|
120
|
+
rescue Exception => msg
|
121
|
+
raise Ecobee::HTTPError.new("open_log: #{msg}")
|
122
|
+
end
|
123
|
+
|
124
|
+
def validate_status(response)
|
125
|
+
if !response.key? 'status'
|
126
|
+
raise Ecobee::HTTPError.new('Validate Error: Missing Status')
|
127
|
+
elsif !response['status'].key? 'code'
|
128
|
+
raise Ecobee::HTTPError.new('Validate Error: Missing Status Code')
|
129
|
+
elsif response['status']['code'] == 14
|
130
|
+
:retry
|
131
|
+
elsif response['status']['code'] != 0
|
132
|
+
raise Ecobee::HTTPError.new(
|
133
|
+
"Validate Error: (Code #{response['status']['code']}) " +
|
134
|
+
"#{response['status']['message']}"
|
135
|
+
)
|
136
|
+
else
|
137
|
+
response
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
data/lib/ecobee/register.rb
CHANGED
@@ -1,12 +1,15 @@
|
|
1
1
|
module Ecobee
|
2
2
|
|
3
3
|
class Register
|
4
|
-
attr_reader :
|
5
|
-
|
6
|
-
def initialize(
|
7
|
-
|
8
|
-
|
9
|
-
|
4
|
+
attr_reader :expire, :result
|
5
|
+
|
6
|
+
def initialize(
|
7
|
+
app_key: nil,
|
8
|
+
http: nil,
|
9
|
+
scope: DEFAULT_SCOPE
|
10
|
+
)
|
11
|
+
@result = get_pin(app_key: app_key, http: http, scope: scope)
|
12
|
+
@expire = Time.now.to_i + result['expires_in'] * 60
|
10
13
|
end
|
11
14
|
|
12
15
|
def code
|
@@ -25,26 +28,22 @@ module Ecobee
|
|
25
28
|
@result['scope']
|
26
29
|
end
|
27
30
|
|
28
|
-
private
|
31
|
+
private
|
29
32
|
|
30
|
-
def get_pin(app_key: nil, scope: nil)
|
31
|
-
|
32
|
-
|
33
|
+
def get_pin(app_key: nil, http: nil, scope: nil)
|
34
|
+
scope = scope.to_s if scope.is_a? Symbol
|
35
|
+
arg = "?response_type=ecobeePin&client_id=#{app_key}&scope=#{scope}"
|
36
|
+
result = http.get(arg: arg,
|
37
|
+
no_auth: true,
|
38
|
+
resource_prefix: 'authorize',
|
39
|
+
validate_status: false)
|
33
40
|
if result.key? 'error'
|
34
|
-
raise Ecobee::
|
35
|
-
|
36
|
-
result['error'],
|
37
|
-
result['error_description'])
|
41
|
+
raise Ecobee::AuthError.new(
|
42
|
+
"Register Error: (#{result['error']}) #{result['error_description']}"
|
38
43
|
)
|
39
44
|
else
|
40
45
|
result
|
41
46
|
end
|
42
|
-
rescue SocketError => msg
|
43
|
-
raise Ecobee::TokenError.new("GET failed: #{msg}")
|
44
|
-
rescue JSON::ParserError => msg
|
45
|
-
raise Ecobee::TokenError.new("Parse Error: #{msg}")
|
46
|
-
# rescue Exception => msg
|
47
|
-
# raise Ecobee::TokenError.new("Unknown Error: #{msg}")
|
48
47
|
end
|
49
48
|
|
50
49
|
end
|
data/lib/ecobee/thermostat.rb
CHANGED
@@ -24,12 +24,10 @@ module Ecobee
|
|
24
24
|
includeSensors: true
|
25
25
|
}
|
26
26
|
|
27
|
-
|
28
|
-
attr_reader :auto_refresh
|
27
|
+
attr_reader :auto_refresh, :http
|
29
28
|
|
30
29
|
def initialize(
|
31
30
|
auto_refresh: 0,
|
32
|
-
client: nil,
|
33
31
|
fake_index: nil,
|
34
32
|
index: 0,
|
35
33
|
fake_max_index: 0,
|
@@ -37,9 +35,11 @@ module Ecobee
|
|
37
35
|
selection_args: {},
|
38
36
|
token: nil
|
39
37
|
)
|
40
|
-
@auto_refresh = auto_refresh
|
41
38
|
# TODO: add auto-refresh thread handling
|
42
|
-
@
|
39
|
+
@auto_refresh = auto_refresh
|
40
|
+
|
41
|
+
raise ArgumentError.new('No token: specified') unless token
|
42
|
+
@http = token.http
|
43
43
|
@fake_index = fake_index
|
44
44
|
@fake_max_index = fake_max_index
|
45
45
|
@index = index
|
@@ -156,7 +156,7 @@ module Ecobee
|
|
156
156
|
end
|
157
157
|
|
158
158
|
def refresh
|
159
|
-
response = @
|
159
|
+
response = @http.get(arg: :thermostat, options: @selection)
|
160
160
|
if @index + 1 > response['thermostatList'].length
|
161
161
|
raise ThermostatError.new('No such thermostat')
|
162
162
|
end
|
@@ -240,7 +240,7 @@ module Ecobee
|
|
240
240
|
body = my_selection
|
241
241
|
body.merge!({ 'functions' => functions }) if functions
|
242
242
|
body.merge!({ 'thermostat' => thermostat }) if thermostat
|
243
|
-
@
|
243
|
+
@http.post(:thermostat, body: body)
|
244
244
|
end
|
245
245
|
|
246
246
|
end
|
data/lib/ecobee/token.rb
CHANGED
@@ -5,15 +5,21 @@ module Ecobee
|
|
5
5
|
:access_token_expire,
|
6
6
|
:app_key,
|
7
7
|
:callbacks,
|
8
|
+
:http,
|
8
9
|
:pin,
|
9
10
|
:refresh_token,
|
10
11
|
:result,
|
11
12
|
:status,
|
12
13
|
:scope,
|
14
|
+
:token_file,
|
13
15
|
:token_type
|
14
16
|
|
15
17
|
#AUTH_ERRORS = %w(slow_down authorization_pending authorization_expired)
|
16
|
-
|
18
|
+
|
19
|
+
@STATUSES = {
|
20
|
+
authorization_pending: 'Registration begun but has not been approved.',
|
21
|
+
ready: 'Registration approved and valid token received.'
|
22
|
+
}
|
17
23
|
|
18
24
|
def initialize(
|
19
25
|
access_token: nil,
|
@@ -33,6 +39,8 @@ module Ecobee
|
|
33
39
|
@token_file = expand_files token_file
|
34
40
|
|
35
41
|
@authorization_thread, @pin, @status, @token_type = nil
|
42
|
+
@poll_interval = DEFAULT_POLL_INTERVAL
|
43
|
+
@http = Ecobee::HTTP.new(log_file: "/tmp/token.log", token: self)
|
36
44
|
|
37
45
|
@refresh_pad = REFRESH_PAD + rand(REFRESH_PAD)
|
38
46
|
|
@@ -41,15 +49,33 @@ module Ecobee
|
|
41
49
|
end
|
42
50
|
|
43
51
|
def access_token
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
52
|
+
if @access_token
|
53
|
+
if access_token_expired?
|
54
|
+
if @refresh_token
|
55
|
+
refresh_access_token
|
56
|
+
else
|
57
|
+
token_register
|
58
|
+
end
|
59
|
+
else
|
60
|
+
desired_status = (@refresh_token ? :ready : :authorization_pending)
|
61
|
+
if @refresh_token
|
62
|
+
if @status != desired_status
|
63
|
+
puts "Status: MISMATCH: #{@status} vs #{desired_status}" if @status
|
64
|
+
@status = desired_status
|
65
|
+
end
|
66
|
+
@access_token
|
67
|
+
else
|
68
|
+
check_for_authorization
|
69
|
+
end
|
70
|
+
end
|
71
|
+
else
|
72
|
+
@status = :authorization_pending
|
73
|
+
token_register
|
74
|
+
end
|
50
75
|
end
|
51
76
|
|
52
77
|
def access_token_expired?
|
78
|
+
return true unless @access_token_expire
|
53
79
|
Time.now.to_i > @access_token_expire - @refresh_pad
|
54
80
|
end
|
55
81
|
|
@@ -85,27 +111,20 @@ module Ecobee
|
|
85
111
|
end
|
86
112
|
|
87
113
|
def refresh_access_token
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
114
|
+
arg = sprintf("?grant_type=refresh_token&refresh_token=%s&client_id=%s",
|
115
|
+
@refresh_token,
|
116
|
+
@app_key)
|
117
|
+
result = @http.post(arg: arg,
|
118
|
+
no_auth: true,
|
119
|
+
resource_prefix: 'token',
|
120
|
+
validate_status: false)
|
95
121
|
if result.key? 'error'
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
else
|
103
|
-
puts "DUMPING(result): #{result.pretty_inspect}"
|
104
|
-
raise Ecobee::TokenError.new(
|
105
|
-
"Result Error: (%s) %s" % [result['error'],
|
106
|
-
result['error_description']]
|
107
|
-
)
|
108
|
-
end
|
122
|
+
@access_token, @access_token_expire, @pin, @scope, @refresh_token = nil
|
123
|
+
config_save
|
124
|
+
raise Ecobee::AuthError.new(
|
125
|
+
"Result Error: (%s) %s" % [result['error'],
|
126
|
+
result['error_description']]
|
127
|
+
)
|
109
128
|
else
|
110
129
|
@access_token = result['access_token']
|
111
130
|
@access_token_expire = Time.now.to_i + result['expires_in']
|
@@ -117,12 +136,6 @@ module Ecobee
|
|
117
136
|
config_save
|
118
137
|
@access_token
|
119
138
|
end
|
120
|
-
rescue SocketError => msg
|
121
|
-
raise Ecobee::TokenError.new("POST failed: #{msg}")
|
122
|
-
rescue JSON::ParserError => msg
|
123
|
-
raise Ecobee::TokenError.new("Result parsing: #{msg}")
|
124
|
-
# rescue Exception => msg
|
125
|
-
# raise Ecobee::TokenError.new("Unknown Error: #{msg}")
|
126
139
|
end
|
127
140
|
|
128
141
|
def register_callback(type, *callback, &block)
|
@@ -134,8 +147,14 @@ module Ecobee
|
|
134
147
|
end
|
135
148
|
end
|
136
149
|
|
137
|
-
def wait
|
138
|
-
|
150
|
+
def wait(timeout: nil)
|
151
|
+
if timeout
|
152
|
+
Timeout::timeout(timeout) { wait(timeout: nil) }
|
153
|
+
else
|
154
|
+
sleep 0.01 while @status == :authorization_pending
|
155
|
+
end
|
156
|
+
rescue Timeout::Error
|
157
|
+
ensure
|
139
158
|
@status
|
140
159
|
end
|
141
160
|
|
@@ -148,9 +167,10 @@ module Ecobee
|
|
148
167
|
unless @authorization_thread && @authorization_thread.alive?
|
149
168
|
@authorization_thread = Thread.new {
|
150
169
|
loop do
|
151
|
-
|
152
|
-
sleep
|
170
|
+
puts "Sleeping for #{@poll_interval}"
|
171
|
+
sleep @poll_interval
|
153
172
|
break if @status == :ready
|
173
|
+
puts "check_for_authorization_single"
|
154
174
|
check_for_authorization_single
|
155
175
|
end
|
156
176
|
}
|
@@ -159,21 +179,23 @@ module Ecobee
|
|
159
179
|
end
|
160
180
|
|
161
181
|
def check_for_authorization_single
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
182
|
+
arg = sprintf("?grant_type=ecobeePin&code=%s&client_id=%s",
|
183
|
+
@access_token,
|
184
|
+
@app_key)
|
185
|
+
result = @http.post(arg: arg,
|
186
|
+
no_auth: true,
|
187
|
+
resource_prefix: 'token',
|
188
|
+
validate_status: false)
|
169
189
|
if result.key? 'error'
|
170
190
|
@status = :authorization_pending
|
171
191
|
if result['error'] == 'invalid_client'
|
172
|
-
|
173
|
-
elsif
|
174
|
-
|
175
|
-
|
176
|
-
|
192
|
+
token_register
|
193
|
+
elsif ['slow_down', 'authorization_pending'].include? result['error']
|
194
|
+
nil
|
195
|
+
else
|
196
|
+
@access_token, @access_token_expire, @pin, @scope, @refresh_token = nil
|
197
|
+
config_save
|
198
|
+
raise Ecobee::AuthError.new(
|
177
199
|
"Result Error: (%s) %s" % [result['error'],
|
178
200
|
result['error_description']]
|
179
201
|
)
|
@@ -189,12 +211,6 @@ module Ecobee
|
|
189
211
|
config_save
|
190
212
|
@access_token
|
191
213
|
end
|
192
|
-
rescue SocketError => msg
|
193
|
-
raise Ecobee::TokenError.new("POST failed: #{msg}")
|
194
|
-
rescue JSON::ParserError => msg
|
195
|
-
raise Ecobee::TokenError.new("Result parsing: #{msg}")
|
196
|
-
# rescue Exception => msg
|
197
|
-
# raise Ecobee::TokenError.new("Unknown Error: #{msg}")
|
198
214
|
end
|
199
215
|
|
200
216
|
def config_load_to_memory(config)
|
@@ -246,7 +262,7 @@ module Ecobee
|
|
246
262
|
File.open(file, 'r').read(16 * 1024)
|
247
263
|
)
|
248
264
|
rescue JSON::ParserError => msg
|
249
|
-
raise Ecobee::
|
265
|
+
raise Ecobee::AuthError.new("Result parsing: #{msg}")
|
250
266
|
rescue Errno::ENOENT
|
251
267
|
{}
|
252
268
|
end
|
@@ -295,21 +311,22 @@ module Ecobee
|
|
295
311
|
end
|
296
312
|
end
|
297
313
|
|
298
|
-
def
|
314
|
+
def token_register
|
299
315
|
@status = :authorization_pending
|
300
|
-
result = Register.new(app_key: @app_key,
|
316
|
+
result = Ecobee::Register.new(app_key: @app_key,
|
317
|
+
http: @http,
|
318
|
+
scope: @scope)
|
319
|
+
@poll_interval = result.interval
|
301
320
|
@pin = result.pin
|
302
321
|
@access_token = result.code
|
303
|
-
@access_token_expire = result.
|
322
|
+
@access_token_expire = result.expire
|
304
323
|
@refresh_token = nil
|
305
324
|
@scope = result.scope
|
306
325
|
check_for_authorization
|
307
326
|
config_save
|
308
|
-
@access_token
|
327
|
+
@access_token
|
309
328
|
end
|
310
329
|
|
311
330
|
end
|
312
331
|
|
313
|
-
class TokenError < StandardError ; end
|
314
|
-
|
315
332
|
end
|
data/lib/ecobee/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ecobee
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rob Zwissler
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-08-
|
11
|
+
date: 2016-08-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -75,7 +75,7 @@ files:
|
|
75
75
|
- examples/test_thermostat_object.rb
|
76
76
|
- examples/test_token.rb
|
77
77
|
- lib/ecobee.rb
|
78
|
-
- lib/ecobee/
|
78
|
+
- lib/ecobee/http.rb
|
79
79
|
- lib/ecobee/register.rb
|
80
80
|
- lib/ecobee/thermostat.rb
|
81
81
|
- lib/ecobee/token.rb
|
data/lib/ecobee/client.rb
DELETED
@@ -1,75 +0,0 @@
|
|
1
|
-
module Ecobee
|
2
|
-
|
3
|
-
class Client
|
4
|
-
def initialize(token: nil)
|
5
|
-
raise ArgumentError.new('Missing token') unless token
|
6
|
-
@token = token
|
7
|
-
end
|
8
|
-
|
9
|
-
def get(arg, options = nil)
|
10
|
-
new_uri = URL_API + arg.to_s.sub(/^\//, '')
|
11
|
-
new_uri += '?json=' + options.to_json if options
|
12
|
-
request = Net::HTTP::Get.new(URI(URI.escape(new_uri)))
|
13
|
-
request['Content-Type'] = *CONTENT_TYPE
|
14
|
-
request['Authorization'] = @token.authorization
|
15
|
-
http_response = http.request request
|
16
|
-
response = validate_status JSON.parse(http_response.body)
|
17
|
-
# if response == :retry
|
18
|
-
# get(arg, options)
|
19
|
-
# else
|
20
|
-
# response
|
21
|
-
# end
|
22
|
-
rescue JSON::ParserError => msg
|
23
|
-
raise ClientError.new("JSON::ParserError => #{msg}")
|
24
|
-
end
|
25
|
-
|
26
|
-
def post(arg, options: {}, body: nil)
|
27
|
-
new_uri = URL_API + arg.to_s.sub(/^\//, '')
|
28
|
-
request = Net::HTTP::Post.new(URI new_uri)
|
29
|
-
request.set_form_data({ 'format' => 'json' }.merge(options))
|
30
|
-
request.body = JSON.generate(body) if body
|
31
|
-
request['Content-Type'] = *CONTENT_TYPE
|
32
|
-
request['Authorization'] = @token.authorization
|
33
|
-
http_response = http.request request
|
34
|
-
response = validate_status JSON.parse http_response.body
|
35
|
-
# if response == :retry
|
36
|
-
# post(arg, options: options, body: body)
|
37
|
-
# else
|
38
|
-
# response
|
39
|
-
# end
|
40
|
-
rescue JSON::ParserError => msg
|
41
|
-
raise ClientError.new("JSON::ParserError => #{msg}")
|
42
|
-
end
|
43
|
-
|
44
|
-
def validate_status(response)
|
45
|
-
if !response.key? 'status'
|
46
|
-
raise ClientError.new('Missing Status')
|
47
|
-
elsif !response['status'].key? 'code'
|
48
|
-
raise ClientError.new('Missing Status Code')
|
49
|
-
elsif response['status']['code'] == 14
|
50
|
-
:retry
|
51
|
-
elsif response['status']['code'] != 0
|
52
|
-
raise ClientError.new(
|
53
|
-
"GET Error: #{response['status']['code']} " +
|
54
|
-
"Message: #{response['status']['message']}"
|
55
|
-
)
|
56
|
-
else
|
57
|
-
response
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
private
|
62
|
-
|
63
|
-
def http
|
64
|
-
@http ||= Net::HTTP.new(API_HOST, API_PORT)
|
65
|
-
unless @http.active?
|
66
|
-
@http.use_ssl = true
|
67
|
-
Net::HTTP.start(API_HOST, API_PORT)
|
68
|
-
end
|
69
|
-
@http
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
class ClientError < StandardError ; end
|
74
|
-
|
75
|
-
end
|