htcc 0.1.1
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 +7 -0
- data/htcc.gemspec +12 -0
- data/lib/htcc.rb +4 -0
- data/lib/htcc/client.rb +81 -0
- data/lib/htcc/scheduler.rb +95 -0
- data/lib/htcc/settings.rb +61 -0
- data/lib/htcc/thermostat.rb +253 -0
- metadata +49 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7ab75c3a400f4719a86ea5b1757c17dc2e2d4e6366f1aed56adb2da193bcd79b
|
4
|
+
data.tar.gz: f713bf7a5e3fcbab393b17a5fddce1f2211fe6c823ba3a805e14918748e4e635
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1bea671742650e7cbb022f96175b7161f065b0dccb31aa9dfb2c9a7eff459980b3d261130bca230d27169b8206a788e97346e8e5f33e4a6994575cb74748a768
|
7
|
+
data.tar.gz: '09cc41bed0804b02a6ed2e0a11383afb28bfcdd2831c7588194a33756607935fb75f3f92c197583c4973c3df4940ab1ce5eddd715c5a4cabaff38f297b620f55'
|
data/htcc.gemspec
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'htcc'
|
3
|
+
s.version = '0.1.1'
|
4
|
+
s.summary = "A Ruby client for the Honeywell Total Connect Comfort API"
|
5
|
+
s.description = "This gem can be used to control Honeywell thermostats that use the Total Connect Comfort platform."
|
6
|
+
s.author = 'Lee Folkman'
|
7
|
+
s.email = 'lee.folkman@gmail.com'
|
8
|
+
s.files = Dir['README.md', 'LICENSE', 'CHANGELOG.md', 'lib/**/*.rb', 'htcc.gemspec']
|
9
|
+
s.homepage = 'https://github.com/Folkman/htcc'
|
10
|
+
s.license = 'MIT'
|
11
|
+
s.platform = Gem::Platform::RUBY
|
12
|
+
end
|
data/lib/htcc.rb
ADDED
data/lib/htcc/client.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'net/http'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module HTCC
|
7
|
+
class Client
|
8
|
+
BASE_URL = 'https://mytotalconnectcomfort.com/portal'.freeze
|
9
|
+
HEADERS = { 'X-Requested-With': 'XMLHttpRequest' }.freeze
|
10
|
+
|
11
|
+
attr_reader :devices
|
12
|
+
|
13
|
+
def initialize(username, password, debug: false, debug_output: nil)
|
14
|
+
@debug = debug
|
15
|
+
@debug_output = nil
|
16
|
+
@devices = []
|
17
|
+
login(username, password)
|
18
|
+
get_devices if logged_in?
|
19
|
+
end
|
20
|
+
|
21
|
+
def debug=(val)
|
22
|
+
@debug = val
|
23
|
+
end
|
24
|
+
|
25
|
+
def logged_in?
|
26
|
+
@logged_in
|
27
|
+
end
|
28
|
+
|
29
|
+
def refresh_devices
|
30
|
+
@devices = []
|
31
|
+
get_devices
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def get_devices
|
37
|
+
resp = request(
|
38
|
+
'/Location/GetLocationListData',
|
39
|
+
method: 'post',
|
40
|
+
data: { 'page' => '1', 'filter' => '' }
|
41
|
+
)
|
42
|
+
locations = ::JSON.parse(resp.body)
|
43
|
+
@devices = locations.flat_map { |loc| loc['Devices'] }
|
44
|
+
@devices.map! do |device|
|
45
|
+
case device['DeviceType']
|
46
|
+
when 24
|
47
|
+
Thermostat.new(device, self)
|
48
|
+
else # Other devices?
|
49
|
+
Thermostat.new(device, self)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def login(username, password)
|
55
|
+
resp = request(method: 'post', data: {
|
56
|
+
'UserName': username, 'Password': password, 'timeOffset': '240', 'RememberMe': 'false'
|
57
|
+
})
|
58
|
+
@cookies = get_cookies(resp)
|
59
|
+
@logged_in = resp.get_fields('content-length')[0].to_i < 25 # Successful login
|
60
|
+
end
|
61
|
+
|
62
|
+
def get_cookies(response)
|
63
|
+
response.get_fields('set-cookie')
|
64
|
+
.map { |c| cookie = c.split(/;|,/)[0]; cookie.split('=')[1] ? cookie : nil }
|
65
|
+
.compact
|
66
|
+
.join(';')
|
67
|
+
end
|
68
|
+
|
69
|
+
def request(path = '', method: 'get', data: nil, headers: {})
|
70
|
+
uri = URI("#{BASE_URL}#{path}")
|
71
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
72
|
+
http.use_ssl = true
|
73
|
+
http.set_debug_output(@debug_output || $stdout) if @debug
|
74
|
+
klass = method == 'get' ? Net::HTTP::Get : Net::HTTP::Post
|
75
|
+
request = klass.new(uri.request_uri, HEADERS.merge(headers))
|
76
|
+
request['Cookie'] = @cookies if @cookies
|
77
|
+
request.set_form_data(data) if data
|
78
|
+
http.request(request)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTCC
|
4
|
+
class Scheduler
|
5
|
+
attr_reader :device_id
|
6
|
+
|
7
|
+
def initialize(device_id, client)
|
8
|
+
@device_id = device_id
|
9
|
+
@client = client
|
10
|
+
end
|
11
|
+
|
12
|
+
def get_schedule
|
13
|
+
resp = @client.send(:request, "/Device/Menu/GetScheduleData/#{device_id}", method: 'post')
|
14
|
+
@schedule = JSON.parse(resp.body)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# TODO:
|
20
|
+
|
21
|
+
# EDIT_SCHEDULE_PATH = '/Device/Menu/EditSchedule/' + device_id
|
22
|
+
# CONFIRM_SCHEDULE_PATH = '/Device/Menu/SendSchedule?deviceId=' + device_id
|
23
|
+
# DISCARD_SCHEDULE_PATH = '/Device/Menu/DiscardChangesInSchedule?deviceID=' + device_id
|
24
|
+
|
25
|
+
# FormData Encoded:
|
26
|
+
|
27
|
+
# DeviceID: 123456
|
28
|
+
# Days[0]: True
|
29
|
+
# Days[1]: True
|
30
|
+
# Days[2]: True
|
31
|
+
# Days[3]: True
|
32
|
+
# Days[4]: True
|
33
|
+
# Days[5]: True
|
34
|
+
# Days[6]: True
|
35
|
+
# DayChange: True
|
36
|
+
# Templates[0].Editable: True
|
37
|
+
# Templates[0].OrigIsCancelled: false
|
38
|
+
# Templates[0].Editable: True
|
39
|
+
# Templates[0].Type: WakeOcc1
|
40
|
+
# Templates[0].OrigStartTime: 05:45:00
|
41
|
+
# Templates[0].OrigFanMode: Auto
|
42
|
+
# Templates[0].OrigHeatSetpoint: 68
|
43
|
+
# Templates[0].OrigCoolSetpoint: 79
|
44
|
+
# Templates[0].StartTime: 05:45:00
|
45
|
+
# Templates[0].IsCancelled: false
|
46
|
+
# Templates[1].Editable: True
|
47
|
+
# Templates[1].OrigIsCancelled: true
|
48
|
+
# Templates[1].Editable: True
|
49
|
+
# Templates[1].Type: LeaveUnocc1
|
50
|
+
# Templates[1].OrigStartTime: 08:00:00
|
51
|
+
# Templates[1].OrigFanMode: Auto
|
52
|
+
# Templates[1].OrigHeatSetpoint: 62
|
53
|
+
# Templates[1].OrigCoolSetpoint: 85
|
54
|
+
# Templates[1].StartTime: 08:00:00
|
55
|
+
# Templates[1].IsCancelled: true
|
56
|
+
# Templates[2].Editable: True
|
57
|
+
# Templates[2].OrigIsCancelled: true
|
58
|
+
# Templates[2].Editable: True
|
59
|
+
# Templates[2].Type: ReturnOcc2
|
60
|
+
# Templates[2].OrigStartTime: 18:00:00
|
61
|
+
# Templates[2].OrigFanMode: Auto
|
62
|
+
# Templates[2].OrigHeatSetpoint: 70
|
63
|
+
# Templates[2].OrigCoolSetpoint: 78
|
64
|
+
# Templates[2].StartTime: 18:00:00
|
65
|
+
# Templates[2].IsCancelled: true
|
66
|
+
# Templates[3].Editable: True
|
67
|
+
# Templates[3].OrigIsCancelled: false
|
68
|
+
# Templates[3].Editable: True
|
69
|
+
# Templates[3].Type: SleepUnocc2
|
70
|
+
# Templates[3].OrigStartTime: 21:30:00
|
71
|
+
# Templates[3].OrigFanMode: Auto
|
72
|
+
# Templates[3].OrigHeatSetpoint: 64
|
73
|
+
# Templates[3].OrigCoolSetpoint: 78
|
74
|
+
# Templates[3].StartTime: 21:30:00
|
75
|
+
# Templates[3].IsCancelled: false
|
76
|
+
# Templates[0].Type: WakeOcc1
|
77
|
+
# Templates[0].HeatSetpoint: 68
|
78
|
+
# Templates[0].CoolSetpoint: 79
|
79
|
+
# Templates[1].Type: LeaveUnocc1
|
80
|
+
# Templates[1].HeatSetpoint: 62
|
81
|
+
# Templates[1].CoolSetpoint: 85
|
82
|
+
# Templates[2].Type: ReturnOcc2
|
83
|
+
# Templates[2].HeatSetpoint: 70
|
84
|
+
# Templates[2].CoolSetpoint: 78
|
85
|
+
# Templates[3].Type: SleepUnocc2
|
86
|
+
# Templates[3].HeatSetpoint: 64
|
87
|
+
# Templates[3].CoolSetpoint: 78
|
88
|
+
# DisplayUnits: Fahrenheit
|
89
|
+
# IsCommercial: False
|
90
|
+
# ScheduleFan: True
|
91
|
+
# Templates[0].FanMode: Auto
|
92
|
+
# Templates[1].FanMode: Auto
|
93
|
+
# Templates[2].FanMode: Auto
|
94
|
+
# Templates[3].FanMode: Auto
|
95
|
+
# ScheduleOtherDays: False
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTCC
|
4
|
+
class Settings
|
5
|
+
attr_reader :device_id
|
6
|
+
|
7
|
+
def initialize(device_id, client)
|
8
|
+
@device_id = device_id
|
9
|
+
@client = client
|
10
|
+
end
|
11
|
+
|
12
|
+
def update(payload)
|
13
|
+
resp = @client.send(:request, '/Device/Menu/Settings', method: 'post', data: payload)
|
14
|
+
JSON.parse(resp.body)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
# TODO:
|
20
|
+
|
21
|
+
# Payload that can be posted to '/Device/Menu/Settings'
|
22
|
+
# {
|
23
|
+
# "Name":"THERMOSTAT",
|
24
|
+
# "ApplySettingsToAllZones":false,
|
25
|
+
# "DeviceID":123456,
|
26
|
+
# "DisplayUnits":1,
|
27
|
+
# "TempHigherThanActive":true,
|
28
|
+
# "TempHigherThan":85,
|
29
|
+
# "TempHigherThanMinutes":15,
|
30
|
+
# "TempLowerThanActive":true,
|
31
|
+
# "TempLowerThan":55,
|
32
|
+
# "TempLowerThanMinutes":15,
|
33
|
+
# "HumidityHigherThanActive":null,
|
34
|
+
# "HumidityHigherThan":null,
|
35
|
+
# "HumidityHigherThanMinutes":null,
|
36
|
+
# "HumidityLowerThanActive":null,
|
37
|
+
# "HumidityLowerThan":null,
|
38
|
+
# "HumidityLowerThanMinutes":null,
|
39
|
+
# "FaultConditionExistsActive":false,
|
40
|
+
# "FaultConditionExistsHours":1,
|
41
|
+
# "NormalConditionsActive":true,
|
42
|
+
# "ThermostatAlertActive":null,
|
43
|
+
# "CommunicationFailureActive":true,
|
44
|
+
# "CommunicationFailureMinutes":15,
|
45
|
+
# "CommunicationLostActive":true,
|
46
|
+
# "CommunicationLostHours":1,
|
47
|
+
# "DeviceLostActive":null,
|
48
|
+
# "DeviceLostHours":null,
|
49
|
+
# "TempHigherThanValue":"85°",
|
50
|
+
# "TempLowerThanValue":"55°",
|
51
|
+
# "HumidityHigherThanValue":"--%",
|
52
|
+
# "HumidityLowerThanValue":"--%",
|
53
|
+
# "TempHigherThanMinutesText":"For 15 Minutes",
|
54
|
+
# "TempLowerThanMinutesText":"For 15 Minutes",
|
55
|
+
# "HumidityHigherThanMinutesText":"For 0 Minutes",
|
56
|
+
# "HumidityLowerThanMinutesText":"For 0 Minutes",
|
57
|
+
# "FaultConditionExistsHoursText":"Every 1 Hour",
|
58
|
+
# "CommunicationFailureMinutesText":"For 15 Minutes",
|
59
|
+
# "CommunicationLostHoursText":"After 1 Hour",
|
60
|
+
# "DeviceLostHoursText":"After 1 Hour"
|
61
|
+
# }
|
@@ -0,0 +1,253 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HTCC
|
4
|
+
class Thermostat
|
5
|
+
SYSTEM_MODES = %i[emergency_heat heat off cool auto]
|
6
|
+
FAN_MODES = %i[auto on circulate schedule]
|
7
|
+
HOLD_TYPES = %i[none temporary permanent]
|
8
|
+
EQUIPMENT_OUTPUT_STATUS = %i[off heating cooling fan_running]
|
9
|
+
|
10
|
+
attr_reader :info
|
11
|
+
|
12
|
+
def initialize(info, client)
|
13
|
+
@info = info
|
14
|
+
@client = client
|
15
|
+
@refresh = true
|
16
|
+
@status = {}
|
17
|
+
end
|
18
|
+
|
19
|
+
def Scheduler
|
20
|
+
@scheduler ||= Scheduler.new(id, @client)
|
21
|
+
end
|
22
|
+
|
23
|
+
def Settings
|
24
|
+
@settings ||= Settings.new(id, @client)
|
25
|
+
end
|
26
|
+
|
27
|
+
def id
|
28
|
+
@info['DeviceID']
|
29
|
+
end
|
30
|
+
|
31
|
+
def mac_address
|
32
|
+
@info['MacID']
|
33
|
+
end
|
34
|
+
|
35
|
+
def name
|
36
|
+
@info['Name']
|
37
|
+
end
|
38
|
+
|
39
|
+
def connected?
|
40
|
+
get_status
|
41
|
+
@status['deviceLive'] && !@status['communicationLost']
|
42
|
+
end
|
43
|
+
|
44
|
+
def status(refresh = false)
|
45
|
+
get_status if @status.empty? || refresh
|
46
|
+
@status
|
47
|
+
end
|
48
|
+
|
49
|
+
def system_mode
|
50
|
+
get_status
|
51
|
+
SYSTEM_MODES[@status['latestData']['uiData']['SystemSwitchPosition']]
|
52
|
+
end
|
53
|
+
|
54
|
+
def system_mode=(mode)
|
55
|
+
unless system_modes.index(mode)
|
56
|
+
raise SystemError.new("Unknown mode: #{mode.inspect}. Allowed modes: #{system_modes.inspect}")
|
57
|
+
end
|
58
|
+
change_setting(system_mode: mode)
|
59
|
+
end
|
60
|
+
|
61
|
+
def has_fan?
|
62
|
+
return @has_fan unless @has_fan.nil?
|
63
|
+
|
64
|
+
get_status if @status.empty?
|
65
|
+
@has_fan = @status['latestData']['hasFan']
|
66
|
+
end
|
67
|
+
|
68
|
+
def fan_running?
|
69
|
+
get_status
|
70
|
+
@status['latestData']['fanData']['fanIsRunning']
|
71
|
+
end
|
72
|
+
|
73
|
+
def fan_mode
|
74
|
+
get_status
|
75
|
+
FAN_MODES[@status['latestData']['fanData']['fanMode']]
|
76
|
+
end
|
77
|
+
|
78
|
+
def fan_mode=(mode)
|
79
|
+
unless fan_modes.index(mode)
|
80
|
+
raise FanError.new("Unknown mode: #{mode.inspect}. Allowed modes: #{fan_modes.inspect}")
|
81
|
+
end
|
82
|
+
change_setting(fan_mode: mode)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Current ambient temperature
|
86
|
+
def current_temperature
|
87
|
+
get_status
|
88
|
+
@status['latestData']['uiData']['DispTemperature']
|
89
|
+
end
|
90
|
+
|
91
|
+
def temperature_unit
|
92
|
+
get_status
|
93
|
+
@status['latestData']['uiData']['DisplayUnits']
|
94
|
+
end
|
95
|
+
|
96
|
+
# Cooling temperature setting
|
97
|
+
def cool_setpoint
|
98
|
+
get_status
|
99
|
+
@status['latestData']['uiData']['CoolSetpoint']
|
100
|
+
end
|
101
|
+
|
102
|
+
def cool_setpoint=(temp)
|
103
|
+
raise_min_setpoint(min_cool_setpoint, temp) if temp < min_cool_setpoint
|
104
|
+
raise_max_setpoint(max_cool_setpoint, temp) if temp > max_cool_setpoint
|
105
|
+
change_setting(cool_setpoint: temp, hold: :temporary)
|
106
|
+
end
|
107
|
+
|
108
|
+
# Heating temperature setting
|
109
|
+
def heat_setpoint
|
110
|
+
get_status
|
111
|
+
@status['latestData']['uiData']['HeatSetpoint']
|
112
|
+
end
|
113
|
+
|
114
|
+
def heat_setpoint=(temp)
|
115
|
+
raise_min_setpoint(min_heat_setpoint, temp) if temp < min_heat_setpoint
|
116
|
+
raise_max_setpoint(max_heat_setpoint, temp) if temp > max_heat_setpoint
|
117
|
+
change_setting(heat_setpoint: temp, hold: :temporary)
|
118
|
+
end
|
119
|
+
|
120
|
+
def resume_schedule
|
121
|
+
change_setting(hold: :none)
|
122
|
+
end
|
123
|
+
|
124
|
+
def hold
|
125
|
+
get_status
|
126
|
+
HOLD_TYPES[@status['latestData']['uiData']['StatusHeat']] # Both status are the same
|
127
|
+
end
|
128
|
+
|
129
|
+
def hold=(mode)
|
130
|
+
unless HOLD_TYPES.index(mode)
|
131
|
+
raise HoldError.new("Unknown mode: #{mode.inspect}. Allowed modes: #{HOLD_TYPES.inspect}")
|
132
|
+
end
|
133
|
+
change_setting(hold: mode)
|
134
|
+
end
|
135
|
+
|
136
|
+
def output_status
|
137
|
+
get_status
|
138
|
+
status = @status['latestData']['uiData']['EquipmentOutputStatus']
|
139
|
+
status = no_refresh { fan_running? ? 3 : status } if status.zero?
|
140
|
+
EQUIPMENT_OUTPUT_STATUS[@status['latestData']['uiData']['EquipmentOutputStatus']]
|
141
|
+
end
|
142
|
+
|
143
|
+
def system_modes
|
144
|
+
return @system_modes if @system_modes
|
145
|
+
|
146
|
+
get_status if @status.empty?
|
147
|
+
allowed_modes = [
|
148
|
+
@status['latestData']['uiData']['SwitchEmergencyHeatAllowed'],
|
149
|
+
@status['latestData']['uiData']['SwitchHeatAllowed'],
|
150
|
+
@status['latestData']['uiData']['SwitchOffAllowed'],
|
151
|
+
@status['latestData']['uiData']['SwitchCoolAllowed'],
|
152
|
+
@status['latestData']['uiData']['SwitchAutoAllowed'],
|
153
|
+
]
|
154
|
+
@system_modes = SYSTEM_MODES.select.with_index { |_, i| allowed_modes[i] }
|
155
|
+
end
|
156
|
+
|
157
|
+
def fan_modes
|
158
|
+
return @fan_modes if @fan_modes
|
159
|
+
|
160
|
+
get_status if @status.empty?
|
161
|
+
allowed_modes = [
|
162
|
+
@status['latestData']['fanData']['fanModeAutoAllowed'],
|
163
|
+
@status['latestData']['fanData']['fanModeOnAllowed'],
|
164
|
+
@status['latestData']['fanData']['fanModeCirculateAllowed'],
|
165
|
+
@status['latestData']['fanData']['fanModeFollowScheduleAllowed']
|
166
|
+
]
|
167
|
+
@fan_modes = FAN_MODES.select.with_index { |_, i| allowed_modes[i] }
|
168
|
+
end
|
169
|
+
|
170
|
+
def no_refresh(&block)
|
171
|
+
@refresh = false
|
172
|
+
result = yield
|
173
|
+
@refresh = true
|
174
|
+
result
|
175
|
+
end
|
176
|
+
|
177
|
+
private
|
178
|
+
|
179
|
+
def get_status
|
180
|
+
return @status unless @refresh || @status.empty?
|
181
|
+
|
182
|
+
resp = @client.send(:request, "/Device/CheckDataSession/#{id}?_=#{Time.now.to_i}")
|
183
|
+
@status = JSON.parse(resp.body)
|
184
|
+
end
|
185
|
+
|
186
|
+
# Required separation between high and low setpoints
|
187
|
+
def deadband
|
188
|
+
return @deadband if @deadband
|
189
|
+
|
190
|
+
get_status if @status.empty?
|
191
|
+
@deadband = @status['latestData']['uiData']['Deadband']
|
192
|
+
end
|
193
|
+
|
194
|
+
def min_cool_setpoint
|
195
|
+
@info['ThermostatData']['MinCoolSetpoint']
|
196
|
+
end
|
197
|
+
|
198
|
+
def max_cool_setpoint
|
199
|
+
@info['ThermostatData']['MaxCoolSetpoint']
|
200
|
+
end
|
201
|
+
|
202
|
+
def min_heat_setpoint
|
203
|
+
@info['ThermostatData']['MinHeatSetpoint']
|
204
|
+
end
|
205
|
+
|
206
|
+
def max_heat_setpoint
|
207
|
+
@info['ThermostatData']['MaxHeatSetpoint']
|
208
|
+
end
|
209
|
+
|
210
|
+
def raise_min_setpoint(min_temp, given_temp)
|
211
|
+
raise TemperatureError.new("Minimum setpoint is #{min_temp}. Given: #{given_temp}")
|
212
|
+
end
|
213
|
+
|
214
|
+
def raise_max_setpoint(max_temp, given_temp)
|
215
|
+
raise TemperatureError.new("Maximum setpoint is #{max_temp}. Given: #{given_temp}")
|
216
|
+
end
|
217
|
+
|
218
|
+
def payload(
|
219
|
+
system_mode: nil,
|
220
|
+
heat_setpoint: nil,
|
221
|
+
cool_setpoint: nil,
|
222
|
+
heat_next_period: nil,
|
223
|
+
cool_next_period: nil,
|
224
|
+
hold: nil,
|
225
|
+
fan_mode: nil
|
226
|
+
)
|
227
|
+
{
|
228
|
+
'DeviceID': id,
|
229
|
+
'SystemSwitch': SYSTEM_MODES.index(system_mode),
|
230
|
+
'HeatSetpoint': heat_setpoint,
|
231
|
+
'CoolSetpoint': cool_setpoint,
|
232
|
+
'HeatNextPeriod': heat_next_period, # 0 = hold until 00:00, ..., 92 = hold until 23:45
|
233
|
+
'CoolNextPeriod': cool_next_period, # 0 = hold until 00:00, ..., 92 = hold until 23:45
|
234
|
+
'StatusHeat': HOLD_TYPES.index(hold),
|
235
|
+
'StatusCool': HOLD_TYPES.index(hold),
|
236
|
+
'FanMode': FAN_MODES.index(fan_mode)
|
237
|
+
}
|
238
|
+
end
|
239
|
+
|
240
|
+
def change_setting(**data)
|
241
|
+
resp = @client.send(:request, '/Device/SubmitControlScreenChanges',
|
242
|
+
method: 'post',
|
243
|
+
data: payload(**data)
|
244
|
+
)
|
245
|
+
JSON.parse(resp.body)['success'] == 1
|
246
|
+
end
|
247
|
+
|
248
|
+
class FanError < StandardError; end
|
249
|
+
class HoldError < StandardError; end
|
250
|
+
class SystemError < StandardError; end
|
251
|
+
class TemperatureError < StandardError; end
|
252
|
+
end
|
253
|
+
end
|
metadata
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: htcc
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Lee Folkman
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-03-29 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: This gem can be used to control Honeywell thermostats that use the Total
|
14
|
+
Connect Comfort platform.
|
15
|
+
email: lee.folkman@gmail.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- htcc.gemspec
|
21
|
+
- lib/htcc.rb
|
22
|
+
- lib/htcc/client.rb
|
23
|
+
- lib/htcc/scheduler.rb
|
24
|
+
- lib/htcc/settings.rb
|
25
|
+
- lib/htcc/thermostat.rb
|
26
|
+
homepage: https://github.com/Folkman/htcc
|
27
|
+
licenses:
|
28
|
+
- MIT
|
29
|
+
metadata: {}
|
30
|
+
post_install_message:
|
31
|
+
rdoc_options: []
|
32
|
+
require_paths:
|
33
|
+
- lib
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
requirements: []
|
45
|
+
rubygems_version: 3.2.3
|
46
|
+
signing_key:
|
47
|
+
specification_version: 4
|
48
|
+
summary: A Ruby client for the Honeywell Total Connect Comfort API
|
49
|
+
test_files: []
|