fermentord/internal/controllers/chamber.go

182 lines
4.7 KiB
Go

package controllers
import (
"context"
"fmt"
"log"
"sync"
"time"
"git.joco.dk/sng/fermentord/pkg/temperature"
"github.com/getsentry/sentry-go"
)
type ChamberState int
const (
ChamberStateIdle ChamberState = iota
ChamberStateCooling
ChamberStateHeating
)
var ChamberStateMap = map[ChamberState]string{
ChamberStateIdle: "IDLE",
ChamberStateCooling: "COOLING",
ChamberStateHeating: "HEATING",
}
type ChamberController struct {
ConfigUpdates chan ControllerConfig
config ControllerConfig
// Current state.
chamberState ChamberState
lastChamberStateChange time.Time
lastCoolerStateChange time.Time
C chan ChamberState
name string
// Current temperature readings.
ambientTemperature float64
chamberTemperature float64
wortTemperature float64
chTemp <-chan temperature.TemperatureReading
pid *PIDController
hub *sentry.Hub
}
func NewChamberController(name string, config ControllerConfig, chTemp <-chan temperature.TemperatureReading) *ChamberController {
return &ChamberController{
C: make(chan ChamberState),
name: name,
config: config,
pid: NewPIDController(config.PID.Kp, config.PID.Ki, config.PID.Kd),
chTemp: chTemp,
chamberState: ChamberStateIdle,
ConfigUpdates: make(chan ControllerConfig, 1),
hub: sentry.CurrentHub().Clone(),
}
}
func (p *ChamberController) Run(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
defer p.hub.Flush(10 * time.Second)
ticker := time.NewTicker(1 * time.Second)
for {
select {
case <-ticker.C:
state := p.computeChamberState()
p.setChamberState(state)
case temp := <-p.chTemp:
p.setTemperature(temp)
case c := <-p.ConfigUpdates:
p.config = c
case <-ctx.Done():
ticker.Stop()
p.setChamberState(ChamberStateIdle)
return
}
}
}
func (p *ChamberController) setTemperature(t temperature.TemperatureReading) {
switch t.Sensor {
case p.config.Sensor.Ambient:
p.ambientTemperature = t.Degrees()
case p.config.Sensor.Chamber:
p.chamberTemperature = t.Degrees()
case p.config.Sensor.Wort:
p.wortTemperature = t.Degrees()
default:
log.Printf("Unknown sensor: sensor=%v temp=%v", t.Sensor, t.MilliDegrees)
p.hub.CaptureMessage(fmt.Sprintf("Unknown sensor: sensor=%v temp=%v", t.Sensor, t.MilliDegrees))
}
}
func (p *ChamberController) setChamberState(state ChamberState) {
if state == p.chamberState {
return
}
log.Printf("State changed from %v to %v", p.chamberState, state)
if p.chamberState == ChamberStateCooling || state == ChamberStateCooling {
p.lastCoolerStateChange = time.Now()
}
p.chamberState = state
p.lastChamberStateChange = time.Now()
p.C <- state
}
func (p *ChamberController) computeChamberState() ChamberState {
offset := p.pid.Compute(p.chamberTemperature, p.config.FermentationTemperature)
chamberTargetTemp := p.config.FermentationTemperature + offset
runtimeSecs := time.Since(p.lastChamberStateChange).Seconds()
if p.chamberState == ChamberStateCooling {
// Ensure compressor min. runtime
if runtimeSecs < p.config.Limits.MinCoolerRuntimeSecs {
return p.chamberState
}
// Limit compressor runtime
if runtimeSecs > p.config.Limits.MaxCoolerRuntimeSecs {
return ChamberStateIdle
}
// Limit chamber min. temp.
if p.chamberTemperature < p.config.Limits.MinChamberTemperature {
return ChamberStateIdle
}
// Limit chamber max. temp.
if p.chamberTemperature > p.config.Limits.MaxChamberTemperature {
return ChamberStateIdle
}
}
var next ChamberState
heater := p.chamberTemperature < p.config.FermentationTemperature-p.config.DeltaTemperatureHeat &&
p.chamberTemperature < chamberTargetTemp-p.config.DeltaTemperatureHeat
cooler := p.chamberTemperature > p.config.FermentationTemperature+p.config.DeltaTemperatureCool &&
p.chamberTemperature > chamberTargetTemp+p.config.DeltaTemperatureCool
if cooler && heater {
// This should not happen!
log.Print("Trying to set cooler and heater on at the same time. This should NOT happen! Setting IDLE mode.")
p.hub.CaptureMessage("Trying to set cooler and heater on at the same time")
next = ChamberStateIdle
} else if !cooler && !heater {
next = ChamberStateIdle
} else if cooler {
next = ChamberStateCooling
} else if heater {
next = ChamberStateHeating
}
// Ensure compressor cooldown
if p.chamberState != ChamberStateCooling && next == ChamberStateCooling &&
time.Since(p.lastCoolerStateChange).Seconds() < p.config.Limits.MinCoolerCooldownSecs {
return ChamberStateIdle
}
// Ensure that heater wont run just after cooling cycle
if p.chamberState != ChamberStateHeating && next == ChamberStateHeating && time.Since(p.lastCoolerStateChange).Seconds() < p.config.Limits.HeaterGraceTimeSecs {
return ChamberStateIdle
}
return next
}