ecobee 0.1.1 → 0.1.6
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 +22 -14
- data/examples/bitbar_plugin.rb +252 -0
- data/examples/set_mode.rb +15 -13
- data/examples/test_token.rb +6 -1
- data/lib/ecobee.rb +30 -4
- data/lib/ecobee/client.rb +3 -2
- data/lib/ecobee/register.rb +17 -15
- data/lib/ecobee/token.rb +65 -22
- data/lib/ecobee/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 50df9ac248431f92cafec6438d42aa0bb04d964f
|
4
|
+
data.tar.gz: 1928432f79c576e9d6f88a04a00a5a8a8ca1d511
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dd700da725bca1f2b2cd79e371cbc14e9b8a85b2852c2433852d473fc561ec39b67a6dc90d454ef8235fc55a5fdb1945044535a14257a4ce5a6cdd627a2f13bb
|
7
|
+
data.tar.gz: d06a3c81087115ef8d741c749bcc99ea52efd0cc668c2543f10c4b524bfec85ac65c4dfb715a62b327136e4c536f834133207e120aee6246f24ba4cefac05a63
|
data/README.md
CHANGED
@@ -3,34 +3,42 @@
|
|
3
3
|
Ecobee API Ruby Gem. Implements:
|
4
4
|
- OAuth PIN-based token registration & renewal
|
5
5
|
- Persistent HTTP connection
|
6
|
-
- Methods for
|
6
|
+
- Methods for GET & POST requests
|
7
7
|
- Persistent storage for API key & refresh tokens
|
8
8
|
- Example usage scripts (see /examples/\*)
|
9
9
|
|
10
|
-
|
11
|
-
-
|
12
|
-
- Convert storage to generic hook
|
10
|
+
Status:
|
11
|
+
- Working, but is very basic. Contact me with feature requests.
|
13
12
|
|
13
|
+
TODO:
|
14
|
+
- Document API
|
15
|
+
- Convert token storage to optional block/proc
|
16
|
+
- Add timeout to Ecobee::Token#wait
|
17
|
+
- Add redirect based registration
|
18
|
+
- Implement throttling / blocking
|
19
|
+
- Helper methods/classes for building/reading requests
|
14
20
|
|
15
21
|
## Installation
|
16
22
|
|
17
|
-
|
23
|
+
The latest ecobee Ruby Gem is [available from Rubygems.org](https://rubygems.org/gems/ecobee).
|
18
24
|
|
19
|
-
|
20
|
-
|
25
|
+
To install from the command line, run:
|
26
|
+
```
|
27
|
+
gem install ecobee
|
21
28
|
```
|
22
29
|
|
23
|
-
|
24
|
-
|
25
|
-
$ bundle
|
30
|
+
## Usage
|
26
31
|
|
27
|
-
|
32
|
+
1. Obtain an Application Key from Ecobee by [registering your project](https://www.ecobee.com/developers).
|
28
33
|
|
29
|
-
|
34
|
+
2. Using Ecobee::Token, obtain an OAuth Access Token.
|
35
|
+
- Instantiate Ecobee::Token with the api_key and desired scope.
|
36
|
+
- Give user Ecobee::Token#pin and instructions to register your Application via the [Ecobee My Apps Portal](https://www.ecobee.com/consumerportal/index.html#/my-apps).
|
37
|
+
- You can call Ecobee::Token#wait to block until the user confirms the PIN code.
|
30
38
|
|
31
|
-
|
39
|
+
3. Instantiate Ecobee::Client with the token object.
|
32
40
|
|
33
|
-
|
41
|
+
4. Call Ecobee::Client#get or Ecobee::Client#post to interact with [Ecobee's API](https://www.ecobee.com/home/developer/api/introduction/index.shtml).
|
34
42
|
|
35
43
|
## Development
|
36
44
|
|
@@ -0,0 +1,252 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Allows for display and control of your Ecobee in the Mac OS X
|
4
|
+
# menubar, using BitBar (http://getbitbar.com). -- @robzr
|
5
|
+
#
|
6
|
+
# <bitbar.title>EcobeeStat</bitbar.title>
|
7
|
+
# <bitbar.version>v1.0</bitbar.version>
|
8
|
+
# <bitbar.author>Rob Zwissler</bitbar.author>
|
9
|
+
# <bitbar.author.github>robzr</bitbar.author.github>
|
10
|
+
# <bitbar.desc>Ecobee Thermostat Control</bitbar.desc>
|
11
|
+
# <bitbar.image>http://github.com/robzr/ecobee</bitbar.image>
|
12
|
+
# <bitbar.dependencies>ruby</bitbar.dependencies>
|
13
|
+
# <bitbar.abouturl>http://github.com/robzr/ecobee</bitbar.abouturl>
|
14
|
+
|
15
|
+
require 'pp'
|
16
|
+
require 'ecobee'
|
17
|
+
#require_relative '/Users/robzr/GitHub/ecobee/lib/ecobee.rb'
|
18
|
+
#require_relative '/Users/robzr/GitHub/ecobee/lib/ecobee/client.rb'
|
19
|
+
#require_relative '/Users/robzr/GitHub/ecobee/lib/ecobee/token.rb'
|
20
|
+
#require_relative '/Users/robzr/GitHub/ecobee/lib/ecobee/register.rb'
|
21
|
+
|
22
|
+
API_KEY = 'u2Krw0OumeliB0OnwiaogySvgExhy2K4'
|
23
|
+
HVAC_MODES = ['auto', 'auxHeatOnly', 'cool', 'heat', 'off', 'quit']
|
24
|
+
DEG = '°'
|
25
|
+
|
26
|
+
module Ecobee
|
27
|
+
class ResponseError < StandardError ; end
|
28
|
+
|
29
|
+
class BitBar
|
30
|
+
def initialize(client)
|
31
|
+
@client = client
|
32
|
+
end
|
33
|
+
|
34
|
+
def get_thermostat(args = {})
|
35
|
+
index = args.delete(:index) || 0
|
36
|
+
http_response = @client.get('thermostat',
|
37
|
+
Ecobee::Selection(args))
|
38
|
+
response = JSON.parse(http_response.body)
|
39
|
+
get_thermostat_list_index(index: index,
|
40
|
+
response: validate_status(response))
|
41
|
+
rescue JSON::ParserError => msg
|
42
|
+
raise ResponseError.new("JSON::ParserError => #{msg}")
|
43
|
+
end
|
44
|
+
|
45
|
+
def get_thermostat_list_index(index: 0, response: nil)
|
46
|
+
if !response.key? 'thermostatList'
|
47
|
+
raise ResponseError.new('Missing thermostatList')
|
48
|
+
elsif index >= response['thermostatList'].length
|
49
|
+
raise ResponseError.new(
|
50
|
+
"Missing thermostatList Index #{index} (Max Found: " +
|
51
|
+
"#{response['thermostatList'].length - 1})"
|
52
|
+
)
|
53
|
+
else
|
54
|
+
response['thermostatList'][index]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def validate_status(response)
|
59
|
+
if !response.key? 'status'
|
60
|
+
raise ResponseError.new('Missing Status')
|
61
|
+
elsif !response['status'].key? 'code'
|
62
|
+
raise ResponseError.new('Missing Status Code')
|
63
|
+
elsif response['status']['code'] != 0
|
64
|
+
raise ResponseError.new(
|
65
|
+
"GET Error: #{response['status']['code']} " +
|
66
|
+
"Message: #{response['status']['message']}"
|
67
|
+
)
|
68
|
+
else
|
69
|
+
response
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# puts "Heat: #{info['runtime']['desiredHeat'] / 10}#{DEG} | color=red"
|
74
|
+
|
75
|
+
def set_hold(cool_hold: nil, heat_hold: nil)
|
76
|
+
functions = [{
|
77
|
+
'type' => 'setHold',
|
78
|
+
'params' => {
|
79
|
+
'holdType' => 'nextTransition',
|
80
|
+
}
|
81
|
+
}]
|
82
|
+
functions[0]['params']['coolHoldTemp'] = cool_hold
|
83
|
+
functions[0]['params']['heatHoldTemp'] = heat_hold
|
84
|
+
http_response = @client.post(
|
85
|
+
'thermostat',
|
86
|
+
body: {
|
87
|
+
'selection' => {
|
88
|
+
'selectionType' => 'registered',
|
89
|
+
'selectionMatch' => '',
|
90
|
+
},
|
91
|
+
'functions' => functions
|
92
|
+
}
|
93
|
+
)
|
94
|
+
response = JSON.parse(http_response.body)
|
95
|
+
end
|
96
|
+
|
97
|
+
def update_mode(mode)
|
98
|
+
http_response = @client.post(
|
99
|
+
'thermostat',
|
100
|
+
body: {
|
101
|
+
'selection' => {
|
102
|
+
'selectionType' => 'registered',
|
103
|
+
'selectionMatch' => '',
|
104
|
+
},
|
105
|
+
'thermostat' => {
|
106
|
+
'settings' => {
|
107
|
+
'hvacMode' => mode
|
108
|
+
}
|
109
|
+
}
|
110
|
+
}
|
111
|
+
)
|
112
|
+
response = JSON.parse(http_response.body)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def header(info)
|
118
|
+
puts "#{info['runtime']['actualTemperature'] / 10.0}#{DEG}"
|
119
|
+
puts '---'
|
120
|
+
end
|
121
|
+
|
122
|
+
def cool_menu(info)
|
123
|
+
present_mode = info['settings']['hvacMode']
|
124
|
+
return unless ['auto', 'cool'].include? present_mode
|
125
|
+
puts "Cool: #{info['runtime']['desiredCool'] / 10}#{DEG} | color=blue"
|
126
|
+
cool_low = info['settings']['coolRangeLow'] / 10
|
127
|
+
cool_high = info['settings']['coolRangeHigh'] / 10
|
128
|
+
(cool_low..cool_high).reverse_each do |temp|
|
129
|
+
flag, color = ''
|
130
|
+
flag = ' :arrow_left:' if temp == info['runtime']['actualTemperature'] / 10
|
131
|
+
color = ' color=blue' if temp == info['runtime']['desiredCool'] / 10
|
132
|
+
puts("--#{temp}#{DEG}#{flag}|#{color} bash=\"#{$0}\" " +
|
133
|
+
"param1=\"set_cool=#{temp}\" refresh=true terminal=false")
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def heat_menu(info)
|
138
|
+
present_mode = info['settings']['hvacMode']
|
139
|
+
return unless ['auto', 'auxHeatOnly', 'heat'].include? present_mode
|
140
|
+
puts "Heat: #{info['runtime']['desiredHeat'] / 10}#{DEG} | color=red"
|
141
|
+
heat_low = info['settings']['heatRangeLow'] / 10
|
142
|
+
heat_high = info['settings']['heatRangeHigh'] / 10
|
143
|
+
(heat_low..heat_high).reverse_each do |temp|
|
144
|
+
flag, color = ''
|
145
|
+
flag = ' :arrow_left:' if temp == info['runtime']['actualTemperature'] / 10
|
146
|
+
color = ' color=red' if temp == info['runtime']['desiredHeat'] / 10
|
147
|
+
puts("--#{temp}#{DEG}#{flag}|#{color} bash=\"#{$0}\" " +
|
148
|
+
"param1=\"set_heat=#{temp}\" refresh=true terminal=false")
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def mode_menu(info)
|
153
|
+
puts "Mode: #{info['settings']['hvacMode']}"
|
154
|
+
Ecobee::HVAC_MODES.reject { |mode| mode == info['settings']['hvacMode'] }
|
155
|
+
.each do |mode|
|
156
|
+
puts("--#{mode} | bash=\"#{$0}\" param1=\"set_mode=#{mode}\" " +
|
157
|
+
"refresh=true terminal=false")
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def separator
|
162
|
+
puts '---'
|
163
|
+
end
|
164
|
+
|
165
|
+
def stat_info(info)
|
166
|
+
puts info['name']
|
167
|
+
info['remoteSensors'].each do |sensor|
|
168
|
+
temp = sensor['capability'].select do |cap|
|
169
|
+
cap['type'] == 'temperature'
|
170
|
+
end
|
171
|
+
temp = temp[0]['value'].to_i / 10.0
|
172
|
+
puts "--#{sensor['name']}: #{temp}#{DEG}"
|
173
|
+
end
|
174
|
+
puts "#{info['brand']} #{Ecobee::Model(info['modelNumber'])}"
|
175
|
+
puts "Status: #{info['equipmentStatus']}"
|
176
|
+
end
|
177
|
+
|
178
|
+
def website
|
179
|
+
puts 'Ecobee Web Portal|href="https://www.ecobee.com/consumerportal/index.html"'
|
180
|
+
end
|
181
|
+
|
182
|
+
token = Ecobee::Token.new(
|
183
|
+
app_key: API_KEY, app_name: API_KEY,
|
184
|
+
scope: :smartWrite,
|
185
|
+
token_file: '~/.ecobee_token'
|
186
|
+
)
|
187
|
+
if token.pin
|
188
|
+
puts "Ecobee | color=red"
|
189
|
+
puts "---"
|
190
|
+
puts "Registration Needed | color=red"
|
191
|
+
puts "---"
|
192
|
+
puts 'Login to Ecobee | href=\'https://www.ecobee.com/consumerportal/index.html\''
|
193
|
+
puts 'Select \'My Apps\' from the drop-down menu'
|
194
|
+
puts 'Press the \'Add Application\' button'
|
195
|
+
puts "Enter authorization code: #{token.pin}"
|
196
|
+
exit
|
197
|
+
end
|
198
|
+
|
199
|
+
ecobar = Ecobee::BitBar.new Ecobee::Client.new(token: token)
|
200
|
+
|
201
|
+
case arg = ARGV.shift
|
202
|
+
when /^dump/
|
203
|
+
pp ecobar.get_thermostat(
|
204
|
+
:includeRuntime => true,
|
205
|
+
:includeExtendedRuntime => true,
|
206
|
+
:includeElectricity => true,
|
207
|
+
:includeSettings => true,
|
208
|
+
:includeLocation => true,
|
209
|
+
:includeProgram => true,
|
210
|
+
:includeEvents => true,
|
211
|
+
:includeDevice => true,
|
212
|
+
:includeTechnician => true,
|
213
|
+
:includeUtility => true,
|
214
|
+
:includeAlerts => true,
|
215
|
+
:includeWeather => true,
|
216
|
+
:includeOemConfig => true,
|
217
|
+
:includeEquipmentStatus => true,
|
218
|
+
:includeNotificationSettings => true,
|
219
|
+
:includeVersion => true,
|
220
|
+
:includeSensors => true
|
221
|
+
)
|
222
|
+
when /^set_mode=/
|
223
|
+
mode = arg.sub(/^.*=/, '')
|
224
|
+
ecobar.update_mode mode
|
225
|
+
when /^set_cool=/
|
226
|
+
info = ecobar.get_thermostat(includeRuntime: true,
|
227
|
+
includeSettings: true)
|
228
|
+
cool_hold = arg.sub(/^.*=/, '').to_i * 10
|
229
|
+
heat_hold = info['runtime']['desiredHeat']
|
230
|
+
|
231
|
+
ecobar.set_hold(cool_hold: cool_hold, heat_hold: heat_hold)
|
232
|
+
when /^set_heat=/
|
233
|
+
info = ecobar.get_thermostat(includeRuntime: true,
|
234
|
+
includeSettings: true)
|
235
|
+
cool_hold = info['runtime']['desiredCool']
|
236
|
+
heat_hold = arg.sub(/^.*=/, '').to_i * 10
|
237
|
+
|
238
|
+
ecobar.set_hold(cool_hold: cool_hold, heat_hold: heat_hold)
|
239
|
+
else
|
240
|
+
info = ecobar.get_thermostat(includeRuntime: true,
|
241
|
+
includeSettings: true,
|
242
|
+
includeEquipmentStatus: true,
|
243
|
+
includeSensors: true)
|
244
|
+
header info
|
245
|
+
cool_menu info
|
246
|
+
heat_menu info
|
247
|
+
separator
|
248
|
+
stat_info info
|
249
|
+
mode_menu info
|
250
|
+
separator
|
251
|
+
website
|
252
|
+
end
|
data/examples/set_mode.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Loops through menu showing current thermostat mode, with option
|
4
|
+
# to change the mode. -- @robzr
|
2
5
|
|
3
6
|
require 'pp'
|
4
7
|
require 'ecobee'
|
5
8
|
|
6
|
-
|
9
|
+
@hvac_modes = Ecobee::HVAC_MODES + ['quit']
|
7
10
|
|
8
11
|
class TestFunctions
|
9
12
|
def initialize(client)
|
@@ -55,37 +58,36 @@ class TestFunctions
|
|
55
58
|
)
|
56
59
|
response = JSON.parse(http_response.body)
|
57
60
|
end
|
58
|
-
|
59
61
|
end
|
60
62
|
|
61
|
-
|
62
63
|
token = Ecobee::Token.new(
|
63
|
-
|
64
|
+
app_key: ENV['ECOBEE_APP_KEY'],
|
65
|
+
app_name: 'set_mode',
|
64
66
|
scope: :smartWrite,
|
65
67
|
token_file: '~/.ecobee_token'
|
66
68
|
)
|
67
69
|
|
68
|
-
puts token.pin_message if token.pin
|
69
|
-
|
70
|
-
|
71
|
-
|
70
|
+
puts token.pin_message if token.pin token.wait
|
71
|
+
test_functions = TestFunctions.new(
|
72
|
+
Ecobee::Client.new(token: token)
|
73
|
+
)
|
72
74
|
|
73
75
|
loop do
|
74
76
|
test_functions.print_summary
|
75
77
|
|
76
78
|
answer = -1
|
77
|
-
|
79
|
+
until answer.between?(0, @hvac_modes.length - 1)
|
78
80
|
puts
|
79
|
-
(1
|
80
|
-
printf "%d) %s\n", num,
|
81
|
+
(1..@hvac_modes.length).each do |num|
|
82
|
+
printf "%d) %s\n", num, @hvac_modes[num - 1]
|
81
83
|
end
|
82
84
|
print "Enter mode: "
|
83
85
|
answer = gets.to_i - 1
|
84
|
-
abort if answer == (
|
86
|
+
abort if answer == (@hvac_modes.length - 1)
|
85
87
|
end
|
86
88
|
puts
|
87
89
|
|
88
|
-
result = test_functions.update_mode(
|
90
|
+
result = test_functions.update_mode(@hvac_modes[answer])
|
89
91
|
|
90
92
|
unless result.key?('status') && (result['status']['code'] == 0)
|
91
93
|
puts "Unknown result: #{result.to_s}\n"
|
data/examples/test_token.rb
CHANGED
@@ -1,10 +1,15 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# Refreshes token; displays details on saved token. -- @robzr
|
2
4
|
|
3
5
|
require 'pp'
|
4
6
|
require 'ecobee'
|
7
|
+
require_relative '/Users/robzr/GitHub/ecobee/lib/ecobee/token.rb'
|
8
|
+
require_relative '/Users/robzr/GitHub/ecobee/lib/ecobee/register.rb'
|
5
9
|
|
6
10
|
token = Ecobee::Token.new(
|
7
|
-
|
11
|
+
app_key: ENV['ECOBEE_API_KEY'],
|
12
|
+
app_name: 'ecobee-gem',
|
8
13
|
token_file: '~/.ecobee_token'
|
9
14
|
)
|
10
15
|
|
data/lib/ecobee.rb
CHANGED
@@ -12,7 +12,7 @@ module Ecobee
|
|
12
12
|
API_PORT = 443
|
13
13
|
CONTENT_TYPE = ['application/json', { 'charset' => 'UTF-8' }]
|
14
14
|
|
15
|
-
|
15
|
+
HVAC_MODES = ['auto', 'auxHeatOnly', 'cool', 'heat', 'off']
|
16
16
|
|
17
17
|
REFRESH_INTERVAL_PAD = 60
|
18
18
|
REFRESH_TOKEN_CHECK = 10
|
@@ -22,9 +22,11 @@ module Ecobee
|
|
22
22
|
URL_BASE= "https://#{API_HOST}:#{API_PORT}"
|
23
23
|
|
24
24
|
URL_API = "#{URL_BASE}/1/"
|
25
|
-
URL_GET_PIN =
|
25
|
+
URL_GET_PIN = URL_BASE +
|
26
|
+
'/authorize?response_type=ecobeePin&client_id=%s&scope=%s'
|
26
27
|
URL_TOKEN = "#{URL_BASE}/token"
|
27
28
|
|
29
|
+
|
28
30
|
def self.Model(model)
|
29
31
|
{ 'idtSmart' => 'ecobee Smart',
|
30
32
|
'idtEms' => 'ecobee Smart EMS',
|
@@ -55,8 +57,32 @@ module Ecobee
|
|
55
57
|
15 => 'Duplicate data violation.',
|
56
58
|
16 => 'Invalid token. Token has been deauthorized by user. You must ' +
|
57
59
|
're-request authorization.'
|
58
|
-
}[
|
60
|
+
}[code] || 'Unknown Error.'
|
59
61
|
end
|
60
62
|
|
61
|
-
|
63
|
+
def self.Selection(arg = {})
|
64
|
+
{ 'selection' => {
|
65
|
+
'selectionType' => 'registered',
|
66
|
+
'selectionMatch' => '',
|
67
|
+
'includeRuntime' => 'false',
|
68
|
+
'includeExtendedRuntime' => 'false',
|
69
|
+
'includeElectricity' => 'false',
|
70
|
+
'includeSettings' => 'false',
|
71
|
+
'includeLocation' => 'false',
|
72
|
+
'includeProgram' => 'false',
|
73
|
+
'includeEvents' => 'false',
|
74
|
+
'includeDevice' => 'false',
|
75
|
+
'includeTechnician' => 'false',
|
76
|
+
'includeUtility' => 'false',
|
77
|
+
'includeAlerts' => 'false',
|
78
|
+
'includeWeather' => 'false',
|
79
|
+
'includeOemConfig' => 'false',
|
80
|
+
'includeEquipmentStatus' => 'false',
|
81
|
+
'includeNotificationSettings' => 'false',
|
82
|
+
'includeVersion' => 'false',
|
83
|
+
'includeSensors' => 'false',
|
84
|
+
}.merge(Hash[*arg.map { |k,v| [k.to_s, v.to_s] }.flatten])
|
85
|
+
}
|
86
|
+
end
|
62
87
|
|
88
|
+
end
|
data/lib/ecobee/client.rb
CHANGED
@@ -10,7 +10,8 @@ module Ecobee
|
|
10
10
|
def get(arg, options = nil)
|
11
11
|
new_uri = URL_API + arg.sub(/^\//, '')
|
12
12
|
new_uri += '?json=' + options.to_json if options
|
13
|
-
|
13
|
+
|
14
|
+
request = Net::HTTP::Get.new(URI(URI.escape(new_uri)))
|
14
15
|
request['Content-Type'] = *CONTENT_TYPE
|
15
16
|
request['Authorization'] = @token.authorization
|
16
17
|
http.request(request)
|
@@ -19,7 +20,7 @@ module Ecobee
|
|
19
20
|
def post(arg, options: {}, body: nil)
|
20
21
|
new_uri = URL_API + arg.sub(/^\//, '')
|
21
22
|
request = Net::HTTP::Post.new(URI new_uri)
|
22
|
-
request.set_form_data({ 'format'
|
23
|
+
request.set_form_data({ 'format' => 'json' }.merge(options))
|
23
24
|
request.body = JSON.generate(body) if body
|
24
25
|
request['Content-Type'] = *CONTENT_TYPE
|
25
26
|
request['Authorization'] = @token.authorization
|
data/lib/ecobee/register.rb
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
module Ecobee
|
2
|
+
require 'date'
|
2
3
|
|
3
4
|
class Register
|
4
|
-
attr_reader :result
|
5
|
+
attr_reader :expires_at, :result
|
5
6
|
|
6
|
-
def initialize(
|
7
|
-
raise ArgumentError.new('Missing
|
8
|
-
|
9
|
-
@
|
7
|
+
def initialize(app_key: nil, scope: SCOPES[0])
|
8
|
+
raise ArgumentError.new('Missing app_key') unless app_key
|
9
|
+
@result = get_pin(app_key: app_key, scope: scope)
|
10
|
+
@expires_at = DateTime.now.strftime('%s').to_i + result['expires_in'] * 60
|
10
11
|
end
|
11
12
|
|
12
13
|
def code
|
@@ -21,27 +22,28 @@ module Ecobee
|
|
21
22
|
@result['ecobeePin']
|
22
23
|
end
|
23
24
|
|
25
|
+
def scope
|
26
|
+
@result['scope']
|
27
|
+
end
|
28
|
+
|
24
29
|
private
|
25
30
|
|
26
|
-
def get_pin(
|
27
|
-
uri_pin = URI(URL_GET_PIN % [
|
31
|
+
def get_pin(app_key: nil, scope: nil)
|
32
|
+
uri_pin = URI(URL_GET_PIN % [app_key, scope.to_s])
|
28
33
|
result = JSON.parse Net::HTTP.get(uri_pin)
|
29
34
|
if result.key? 'error'
|
30
|
-
raise Ecobee::
|
31
|
-
"
|
35
|
+
raise Ecobee::TokenError.new(
|
36
|
+
"Register Error: (%s) %s" % [result['error'], result['error_description']]
|
32
37
|
)
|
33
38
|
end
|
34
39
|
result
|
35
40
|
rescue SocketError => msg
|
36
|
-
raise Ecobee::
|
41
|
+
raise Ecobee::TokenError.new("GET failed: #{msg}")
|
37
42
|
rescue JSON::ParserError => msg
|
38
|
-
raise Ecobee::
|
43
|
+
raise Ecobee::TokenError.new("Parse Error: #{msg}")
|
39
44
|
rescue Exception => msg
|
40
|
-
raise Ecobee::
|
45
|
+
raise Ecobee::TokenError.new("Unknown Error: #{msg}")
|
41
46
|
end
|
42
47
|
end
|
43
48
|
|
44
|
-
class RegisterError < StandardError
|
45
|
-
end
|
46
|
-
|
47
49
|
end
|
data/lib/ecobee/token.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
module Ecobee
|
2
|
+
require 'date'
|
2
3
|
|
3
4
|
class Token
|
4
5
|
attr_reader :access_token,
|
@@ -12,26 +13,26 @@ module Ecobee
|
|
12
13
|
:type
|
13
14
|
|
14
15
|
def initialize(
|
15
|
-
|
16
|
-
app_name:
|
16
|
+
app_key: nil,
|
17
|
+
app_name: nil,
|
17
18
|
code: nil,
|
18
19
|
refresh_token: nil,
|
19
20
|
scope: SCOPES[0],
|
20
21
|
token_file: nil
|
21
22
|
)
|
22
|
-
@
|
23
|
+
@app_key = app_key
|
23
24
|
@app_name = app_name
|
24
25
|
@code = code
|
25
|
-
@access_token, @expires_at, @pin, @type = nil
|
26
|
+
@access_token, @code_expires_at, @expires_at, @pin, @type = nil
|
26
27
|
@refresh_token = refresh_token
|
27
28
|
@scope = scope
|
28
29
|
@status = :authorization_pending
|
29
30
|
@token_file = File.expand_path(token_file)
|
30
|
-
|
31
|
+
parse_token_file unless @refresh_token
|
31
32
|
if @refresh_token
|
32
33
|
refresh
|
33
34
|
else
|
34
|
-
register unless
|
35
|
+
register unless pin_is_valid
|
35
36
|
check_for_token
|
36
37
|
launch_monitor_thread unless @status == :ready
|
37
38
|
end
|
@@ -46,6 +47,14 @@ module Ecobee
|
|
46
47
|
"#{@type} #{@access_token}"
|
47
48
|
end
|
48
49
|
|
50
|
+
def pin_is_valid
|
51
|
+
if @pin && @code && @code_expires_at
|
52
|
+
@code_expires_at.to_i >= DateTime.now.strftime('%s').to_i
|
53
|
+
else
|
54
|
+
false
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
49
58
|
def pin_message
|
50
59
|
"Log into Ecobee web portal, select My Apps widget, Add Application, " +
|
51
60
|
"enter the PIN #{@pin || ''}"
|
@@ -56,7 +65,7 @@ module Ecobee
|
|
56
65
|
URI(URL_TOKEN),
|
57
66
|
'grant_type' => 'refresh_token',
|
58
67
|
'refresh_token' => @refresh_token,
|
59
|
-
'client_id' => @
|
68
|
+
'client_id' => @app_key
|
60
69
|
)
|
61
70
|
result = JSON.parse(response.body)
|
62
71
|
if result.key? 'error'
|
@@ -69,6 +78,7 @@ module Ecobee
|
|
69
78
|
@access_token = result['access_token']
|
70
79
|
@expires_at = Time.now + result['expires_in']
|
71
80
|
@refresh_token = result['refresh_token']
|
81
|
+
@pin, @code, @code_expires_at = nil
|
72
82
|
@scope = result['scope']
|
73
83
|
@type = result['token_type']
|
74
84
|
@status = :ready
|
@@ -94,12 +104,13 @@ module Ecobee
|
|
94
104
|
URI(URL_TOKEN),
|
95
105
|
'grant_type' => 'ecobeePin',
|
96
106
|
'code' => @code,
|
97
|
-
'client_id' => @
|
107
|
+
'client_id' => @app_key
|
98
108
|
)
|
99
109
|
result = JSON.parse(response.body)
|
100
110
|
if result.key? 'error'
|
101
111
|
unless ['slow_down', 'authorization_pending'].include? result['error']
|
102
|
-
|
112
|
+
# TODO: throttle or just ignore...?
|
113
|
+
pp result
|
103
114
|
raise Ecobee::TokenError.new(
|
104
115
|
"Result Error: (%s) %s" % [result['error'],
|
105
116
|
result['error_description']]
|
@@ -112,6 +123,7 @@ pp result
|
|
112
123
|
@expires_at = Time.now + result['expires_in']
|
113
124
|
@refresh_token = result['refresh_token']
|
114
125
|
@scope = result['scope']
|
126
|
+
@pin, @code, @code_expires_at = nil
|
115
127
|
write_token_file
|
116
128
|
end
|
117
129
|
rescue SocketError => msg
|
@@ -132,32 +144,63 @@ pp result
|
|
132
144
|
}
|
133
145
|
end
|
134
146
|
|
135
|
-
def
|
136
|
-
|
137
|
-
config =
|
138
|
-
|
139
|
-
|
140
|
-
@
|
147
|
+
def parse_token_file
|
148
|
+
#puts "Before Parse: app_key:#{@app_key} refresh_token:#{@refresh_token} pin:#{pin}"
|
149
|
+
return unless (config = read_token_file).is_a? Hash
|
150
|
+
section = (@app_name && config.key?(@app_name)) ? @app_name : @app_key
|
151
|
+
if config.key?(section)
|
152
|
+
@app_key ||= if config[section].key?('app_key')
|
153
|
+
config[section]['app_key']
|
154
|
+
else
|
155
|
+
@app_name
|
156
|
+
end
|
157
|
+
if config[section].key?('refresh_token')
|
158
|
+
@refresh_token ||= config[section]['refresh_token']
|
159
|
+
elsif config[section].key?('pin')
|
160
|
+
@pin ||= config[section]['pin']
|
161
|
+
@code ||= config[section]['code']
|
162
|
+
@code_expires_at ||= config[section]['code_expires_at'].to_i
|
163
|
+
end
|
141
164
|
end
|
165
|
+
#puts "After Parse: app_key:#{@app_key} refresh_token:#{@refresh_token} pin:#{pin}"
|
166
|
+
end
|
167
|
+
|
168
|
+
def read_token_file
|
169
|
+
JSON.parse(
|
170
|
+
File.open(@token_file, 'r').read(16 * 1024)
|
171
|
+
)
|
142
172
|
rescue Errno::ENOENT
|
173
|
+
{}
|
143
174
|
end
|
144
175
|
|
145
176
|
def register
|
146
|
-
result = Register.new(
|
177
|
+
result = Register.new(app_key: @app_key, scope: @scope)
|
147
178
|
@pin = result.pin
|
148
179
|
@code = result.code
|
180
|
+
@code_expires_at = result.expires_at
|
181
|
+
@scope = result.scope
|
182
|
+
write_token_file
|
149
183
|
result
|
150
184
|
end
|
151
185
|
|
152
186
|
def write_token_file
|
153
187
|
return unless @token_file
|
188
|
+
if config = read_token_file
|
189
|
+
config.delete(@app_name)
|
190
|
+
config.delete(@app_key)
|
191
|
+
end
|
192
|
+
section = @app_name || @app_key
|
193
|
+
config[section] = {}
|
194
|
+
config[section]['app_key'] = @app_key if @app_key && section != @app_key
|
195
|
+
if @refresh_token
|
196
|
+
config[section]['refresh_token'] = @refresh_token
|
197
|
+
elsif @pin
|
198
|
+
config[section]['pin'] = @pin
|
199
|
+
config[section]['code'] = @code
|
200
|
+
config[section]['code_expires_at'] = @code_expires_at
|
201
|
+
end
|
154
202
|
File.open(@token_file, 'w') do |tf|
|
155
|
-
tf.puts JSON.pretty_generate(
|
156
|
-
@app_name => {
|
157
|
-
'app_key' => @app_key,
|
158
|
-
'refresh_token' => @refresh_token
|
159
|
-
}
|
160
|
-
})
|
203
|
+
tf.puts JSON.pretty_generate(config)
|
161
204
|
end
|
162
205
|
end
|
163
206
|
|
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.1.
|
4
|
+
version: 0.1.6
|
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-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -70,6 +70,7 @@ files:
|
|
70
70
|
- bin/console
|
71
71
|
- bin/setup
|
72
72
|
- ecobee.gemspec
|
73
|
+
- examples/bitbar_plugin.rb
|
73
74
|
- examples/set_mode.rb
|
74
75
|
- examples/test_token.rb
|
75
76
|
- lib/ecobee.rb
|