package controllers import ( "context" "fmt" "log" "sync" "time" "git.joco.dk/sng/fermentord/internal/configuration" "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 configuration.ControllerConfig config configuration.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 configuration.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 configuration.ControllerConfig, 1), hub: sentry.CurrentHub().Clone(), } } func (p *ChamberController) Run(ctx context.Context, wg *sync.WaitGroup) { defer p.hub.Flush(10 * time.Second) defer wg.Done() 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.Sensors.Ambient: p.ambientTemperature = t.Degrees case p.config.Sensors.Chamber: p.chamberTemperature = t.Degrees case p.config.Sensors.Wort: p.wortTemperature = t.Degrees default: log.Printf("Unknown sensor: sensor=%v temp=%v", t.Sensor, t.Degrees) p.hub.CaptureMessage(fmt.Sprintf("Unknown sensor: sensor=%v temp=%v", t.Sensor, t.Degrees)) } } 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 }