-
Notifications
You must be signed in to change notification settings - Fork 4
/
thermostat.go
202 lines (177 loc) · 6.69 KB
/
thermostat.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
package thermostat
import (
"fmt"
"log"
"time"
"github.com/alittlebrighter/thermostat/controller"
tmeter "github.com/alittlebrighter/thermostat/thermometer"
"github.com/alittlebrighter/thermostat/util"
)
type Config struct {
Thermostat *Thermostat
Controller struct{ Pins struct{ Fan, Cool, Heat int } }
Thermometer struct{ Type, Endpoint string }
}
// Thermostat is the primary struct that contains all of the data required to operate a smart thermostat system.
type Thermostat struct {
Modes `json:"modes"`
DefaultMode string `json:"defaultMode"`
Schedule []*ScheduleEvent `json:"schedule"`
Overshoot float64 `json:"overshoot"`
PollInterval util.Duration `json:"pollInterval"`
MinFan util.Duration `json:"minFan"`
LastFan time.Time `json:"lastFan"`
MaxErrors, errorCount uint8 `json:"maxErrors"`
UnitPreference util.TemperatureUnits `json:"unitPreference"`
control controller.Controller
thermometer tmeter.Thermometer
Events *util.RingBuffer `json:"events"`
}
// Modes are a collection of Windows referenced by a string label/key
type Modes map[string]*Window
// Window defines low and high temperatures
type Window struct {
LowTemp float64 `json:"low"`
HighTemp float64 `json:"high"`
}
// ScheduleEvent defines a block of time from Start to End on the specified Days each week when the specified
// mode (ModeName) should be applied.
type ScheduleEvent struct {
Days []time.Weekday `json:"days"`
ModeName string `json:"mode"`
Start util.ClockTime `json:"start"`
End util.ClockTime `json:"end"`
}
func (stat *Thermostat) SetController(c controller.Controller) {
stat.control = c
}
func (stat *Thermostat) SetThermometer(t tmeter.Thermometer) {
stat.thermometer = t
}
// CurrentTemperatureWindow calculates what the current desired low and high temperatures should be based
// on the configured modes and schedule.
func (stat *Thermostat) CurrentTemperatureWindow(t time.Time) *Window {
for _, spec := range stat.Schedule {
if _, ok := stat.Modes[spec.ModeName]; !ok {
continue
}
dayMatch := false
for _, day := range spec.Days {
if t.Weekday() == day {
dayMatch = true
break
}
}
if !dayMatch {
continue
}
switch {
case t.Hour() < spec.Start.Hour() || t.Hour() > spec.End.Hour():
fallthrough
case t.Hour() == spec.Start.Hour() && t.Minute() < spec.Start.Minute():
fallthrough
case t.Hour() == spec.End.Hour() && t.Minute() > spec.End.Minute():
continue
default:
return stat.Modes[spec.ModeName]
}
}
return stat.Modes[stat.DefaultMode]
}
// ProcessTemperatureReading takes a temperature reading and the units the reading was measured at and determines
// what commands to send to the HVAC controller to keep the temperature inside of the configured range.
func (stat *Thermostat) ProcessTemperatureReading(ambientTemp float64, units util.TemperatureUnits) {
var temp float64
if string(units) == string(util.Celsius) && string(stat.UnitPreference) != string(util.Celsius) {
temp = util.TempCToF(ambientTemp)
} else if string(units) == string(util.Fahrenheit) && string(stat.UnitPreference) != string(util.Fahrenheit) {
temp = util.TempFToC(ambientTemp)
} else {
temp = ambientTemp
}
window := stat.CurrentTemperatureWindow(time.Now())
log.Printf("Current Temperature (%s): %f, Target: %f to %f", stat.UnitPreference, temp, window.LowTemp, window.HighTemp)
switch {
case (stat.control.Direction() == controller.Heating && temp > window.LowTemp+stat.Overshoot) /* done heating */ ||
(stat.control.Direction() == controller.Cooling && temp < window.HighTemp-stat.Overshoot) /* done cooling */ ||
(time.Duration(stat.MinFan).Nanoseconds() > 0 &&
stat.control.Direction() == controller.Fan &&
time.Since(stat.LastFan) > 0 &&
time.Since(stat.LastFan) <= (time.Duration(1)*time.Hour)-time.Duration(stat.MinFan)) /* done running fan */ :
log.Println("turning OFF")
stat.control.Off()
stat.LastFan = time.Now()
case temp < window.LowTemp:
log.Println("turning on HEAT")
stat.control.Heat()
stat.LastFan = time.Now()
case temp > window.HighTemp:
log.Println("turning on COOL")
stat.control.Cool()
stat.LastFan = time.Now()
case time.Duration(stat.MinFan).Nanoseconds() > 0 &&
time.Since(stat.LastFan) > (time.Duration(1)*time.Hour)-time.Duration(stat.MinFan):
log.Println("turning on FAN")
stat.control.Fan()
stat.LastFan = time.Now().Add(time.Duration(stat.MinFan))
default:
log.Println("doing NOTHING")
}
stat.Events.Add(&util.EventLog{AmbientTemperature: temp, Units: stat.UnitPreference, Direction: stat.control.Direction()})
}
// HandleError manages errors received from temperature readings to make sure the system does not stay on in the event of
// not being able to acquire a temperature reading.
func (stat *Thermostat) HandleError() {
stat.errorCount++
if stat.errorCount > stat.MaxErrors {
stat.control.Off()
stat.errorCount = 0
}
}
// Run starts the main event loop to run the thermostat.
func (stat *Thermostat) Run(cancel <-chan bool) {
// we want to do something right away
temp, units, err := stat.thermometer.ReadTemperature()
if err != nil {
log.Println("Error reading Temperature: " + err.Error())
stat.Events.Add(&util.EventLog{AmbientTemperature: -1, Units: stat.UnitPreference, Direction: stat.control.Direction()})
stat.HandleError()
} else {
stat.ProcessTemperatureReading(temp, units)
}
ticker := time.NewTicker(time.Duration(stat.PollInterval))
for {
select {
case <-ticker.C:
temp, units, err := stat.thermometer.ReadTemperature()
if err != nil {
log.Println("Error reading Temperature: " + err.Error())
stat.Events.Add(&util.EventLog{AmbientTemperature: -1, Units: stat.UnitPreference, Direction: stat.control.Direction()})
stat.HandleError()
continue
}
stat.ProcessTemperatureReading(temp, units)
case <-cancel:
return
}
}
}
// Validate checks that a thermostat has a valid configuration and returns a string explaining any issues. An empty string denotes a valid configuration.
func (stat *Thermostat) Validate() string {
if _, ok := stat.Modes[stat.DefaultMode]; !ok {
return "DefaultMode definition not found!"
}
for key, window := range stat.Modes {
if window.LowTemp >= window.HighTemp {
return fmt.Sprintf("%s mode is not valid.", key)
}
}
for i, spec := range stat.Schedule {
if time.Time(spec.Start).Unix() >= time.Time(spec.End).Unix() {
return fmt.Sprintf("Schedule entry #%d not valid.", i+1)
} else if _, ok := stat.Modes[spec.ModeName]; !ok {
return fmt.Sprintf("Schedule entry #%d not valid.", i+1)
}
}
return ""
}