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. } } }