fermentord/internal/controllers/hysteresis.go

227 lines
6.5 KiB
Go
Raw Normal View History

2021-08-30 20:46:38 +00:00
package controllers
import (
"sync"
"time"
"github.com/rs/zerolog/log"
)
// Hysteresis will only run when the ambient temperature is above the
// fermentation temperature. The compressor will run until the wort
// is cooled to FermentationTemperature, the chamber is cooled to
// MinChamberTemperature or the compressor has run for MaxCoolerRuntimeSecs.
// The compressor will run for min. MinCoolerRuntimeSecs before switching off,
// regardless of temperatures. Once turned off, the compressor will not be
// turned on before MinCoolerCooldownSecs has expired, regardless of temperatures.
type Hysteresis struct {
config *Config
// Current state.
coolerState bool
lastCoolerStateChange time.Time
C chan bool
mutex *sync.Mutex
name string
// Current temperature readings.
ambientTemperature float64
chamberTemperature float64
wortTemperature float64
}
func NewHysteresis(name string) *Hysteresis {
return &Hysteresis{
C: make(chan bool),
name: name,
mutex: &sync.Mutex{},
}
}
func (h *Hysteresis) SetConfig(newConfig *Config) {
h.lock()
h.config = newConfig
h.unlock()
h.update()
}
func (h *Hysteresis) UpdateAmbientTemperature(value float64) {
h.lock()
h.ambientTemperature = value
h.unlock()
h.update()
}
func (h *Hysteresis) UpdateChamberTemperature(value float64) {
h.lock()
h.chamberTemperature = value
h.unlock()
h.update()
}
func (h *Hysteresis) UpdateWortTemperature(value float64) {
h.lock()
h.wortTemperature = value
h.unlock()
h.update()
}
func (h *Hysteresis) lock() {
h.mutex.Lock()
}
func (h *Hysteresis) unlock() {
h.mutex.Unlock()
}
func (h *Hysteresis) GetCoolerState() bool {
return h.coolerState
}
func (h *Hysteresis) setState(state bool) {
h.lock()
if state == h.coolerState {
h.unlock()
return
}
log.Debug().Bool("state", state).Msg("Setting state")
h.coolerState = state
h.lastCoolerStateChange = time.Now()
h.unlock()
h.C <- state
}
func (h *Hysteresis) update() {
// Compressor runtime has highest priority.
lastStateChangeAgeSecs := time.Since(h.lastCoolerStateChange).Seconds()
if h.coolerState {
// Keep the cooler running until min. runtime is reached.
if lastStateChangeAgeSecs < h.config.MinCoolerRuntimeSecs {
log.Debug().
Str("name", h.name).
Float64("runtime", lastStateChangeAgeSecs).
Float64("min_runtime", h.config.MinCoolerRuntimeSecs).
Bool("state", h.coolerState).
Msg("Cooler kept running as it's runtime threshold is not yet reached.")
return
}
// Stop the cooler if it's runtime exceeds max.
if lastStateChangeAgeSecs > h.config.MaxCoolerRuntimeSecs {
log.Info().
Str("name", h.name).
Float64("runtime", lastStateChangeAgeSecs).
Float64("max_runtime", h.config.MaxCoolerRuntimeSecs).
Bool("state", false).
Msg("Cooler stopped as it's runtime exceeds max. threshold.")
h.setState(false)
return
}
} else {
// Keep the cooler off until min. cooldown time is reached.
if lastStateChangeAgeSecs < h.config.MinCoolerCooldownSecs {
log.Debug().
Str("name", h.name).
Float64("runtime", lastStateChangeAgeSecs).
Float64("min_cooldown", h.config.MinCoolerCooldownSecs).
Bool("state", h.coolerState).
Msg("Cooler kept off as it's cooldown time threshold is not yet reached.")
return
}
}
// Do not allow the cooler to run when the ambient temperature is below
// the fermentation temperature.
if h.ambientTemperature < h.config.FermentationTemperature {
if h.coolerState {
log.Info().
Str("name", h.name).
Float64("ambient_temp", h.ambientTemperature).
Float64("fermentation_temp", h.config.FermentationTemperature).
Msg("Turn off cooler as ambient temperature is less than fermentation temperature.")
h.setState(false)
} else {
log.Debug().
Str("name", h.name).
Float64("ambient_temp", h.ambientTemperature).
Float64("fermentation_temp", h.config.FermentationTemperature).
Msg("Cooler kept off as ambient temperature is less than fermentation temperature.")
}
return
}
// Do not allow the cooler to run if the chamber temperature is below
// minimum.
if h.chamberTemperature <= h.config.MinChamberTemperature {
if h.coolerState {
log.Info().
Str("name", h.name).
Float64("chamber_temp", h.chamberTemperature).
Float64("min_chamber_temp", h.config.MinChamberTemperature).
Msg("Cooler turned off as chamber temperature dropped below threshold.")
h.setState(false)
} else {
log.Debug().
Str("name", h.name).
Float64("ambient_temp", h.ambientTemperature).
Float64("fermentation_temp", h.config.FermentationTemperature).
Msg("Cooler kept off as chamber temperature is below threshold.")
}
return
}
// Keep the cooler stopped until the wort delta temperature reaches it's threshold.
delta := h.wortTemperature - h.config.FermentationTemperature
if h.coolerState {
// Keep the cooler running until the wort reaches the desired temp.
if delta <= 0 {
// TODO Investigate how much the wort temp. drops below fermentation temp.
// TODO A threshold may be needed.
log.Info().
Str("name", h.name).
Float64("wort_temp", h.wortTemperature).
Float64("fermentation_temp", h.config.FermentationTemperature).
Float64("delta", delta).
Float64("max_wort_delta", h.config.MaxWortDelta).
Msg("Cooler stopped as wort is cooled down to fermentation temp.")
h.setState(false)
return
} else {
log.Debug().
Str("name", h.name).
Float64("wort_temp", h.wortTemperature).
Float64("fermentation_temp", h.config.FermentationTemperature).
Float64("delta", delta).
Float64("max_wort_delta", h.config.MaxWortDelta).
Msg("Cooler kept running as the wort has not yet reached fermentation temp.")
}
} else {
// Start the cooler when delta exceeds threshold.
if delta > h.config.MaxWortDelta {
log.Info().
Str("name", h.name).
Float64("wort_temp", h.wortTemperature).
Float64("fermentation_temp", h.config.FermentationTemperature).
Float64("delta", delta).
Float64("max_wort_delta", h.config.MaxWortDelta).
Msg("Cooler started as wort delta exceeds threshold.")
h.setState(true)
return
} else {
log.Debug().
Str("name", h.name).
Float64("wort_temp", h.wortTemperature).
Float64("fermentation_temp", h.config.FermentationTemperature).
Float64("delta", delta).
Float64("max_wort_delta", h.config.MaxWortDelta).
Msg("Cooler kept off as delta does not yet exceed threshold.")
// TODO If this is reached, then we can lower the delta as min. cooldown is reached.
// TODO This is relevant for PID.
}
}
}