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 } return next }