Close

Still Roasting

A project log for Automated Coffee Bean Roaster

Roasting coffee beans at the push of a button

ben-brooksBen Brooks 01/15/2025 at 17:580 Comments

Been using the roaster for the last few years with overall good results. One thing that's definitely become obvious though, is the heating element is simply undersized for my needs. I've had to lower the fan speed a fair bit to even achieve my roast temps (and when it's cold, I have to lower the fan speed even further). This results in less agitation of the beans, which makes it more likely that some of them sit in one place too long and get slightly burnt. If we were dealing with more 'normal' temperatures, I could redesign the heating component of the roaster, but since the temps are so hot I really lack the tools or ability to make modifications to that part of the design (hence why a popcorn popper was used to begin with).

I'm still glad that I did this project and will continue to use it indefinitely, but when it eventually dies, I think I'll look into possibly just purchasing a roaster, or at the very least revisit the drawing board and see if I can find a way to increase the heating capacity (even if it's just finding a popcorn popper with a higher-wattage heating element). Having measured the power draw, even when keeping things below 15 amps I still have some room for more, not to mention if allowed myself to go all the way to 20 amps. There's also the possibility of adding some recirculation of the roasting air, but that adds a need to properly filter the air not to mention the fire hazard if some chaff makes it's way into the inlet.

I also had someone ask about the ESPHome YAML. While it's still a bit of a work-in-progress, it's mostly done. If I were starting from scratch, I'd definitely do things differently, but this was my first 'real' ESPHome project so I was still learning the nuances and best approaches to things. It's LONG as I have 3 separate roast profiles and essentially just copy-pasted things. Here's where it stands currently (with some redaction, as-needed):

esphome:
  name: coffee-roaster
  friendly_name: Coffee Roaster

# Added because of issues with the Neopixel turning blue on startup and not working properly afterwards
  on_boot:
    priority: 500
    then:
      - climate.control:
          id: pid_controller
          mode: "OFF"
          target_temperature: 15.5556°C
      - text_sensor.template.publish:
          id: roast_status
          state: "On"
      - repeat:
          count: 5
          then:
            - light.turn_off:
                id: neopixel_light
            - delay: 1s

esp32:
  board: esp32dev
  framework:
    type: arduino

# Enable logging
logger:

# Enable Home Assistant API. Changed reboot_timeout to 0s to keep it from rebooting every 15min when using away from home.
api:
  reboot_timeout: 0s
  encryption:
    key: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

ota:
  - platform: esphome
    password: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

wifi:
  networks:
  - ssid: !secret wifi_ssid
    password: !secret wifi_password
  - ssid: !secret trailer_wifi_ssid
    password: !secret wifi_password
  - ssid: !secret chucks_wifi_ssid
    password: !secret wifi_password

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Coffee-Roaster Fallback Hotspot"
    password: !secret wifi_password

#Removed so that when it can't connect to WiFi, the local AP it creates is for the web server and not setting up a different WiFi connection (i.e. allows the web server to be accessed without a WiFi network)
#captive_portal:
    
web_server:
  port: 80
  local: true


interval:
  - interval: 1s
    then:
    
    #Safety check to make sure that if the heater is on, the fan is also on
      - if:
          condition:
            and:            
              - lambda: 'return id(heat_value).state > 0;'
              - fan.is_off: fan_speed
          then:
            - script.stop: roast_1
            - script.stop: roast_2
            - script.stop: roast_3
#            - script.stop: fan_mixer
            - climate.control:
                id: pid_controller
                mode: "OFF"
            - fan.turn_on:
                id: fan_speed
                speed: 100
            - text_sensor.template.publish:
                id: roast_status
                state: "ERROR: Heater On, Fan Not On"
            - light.turn_on:
                id: neopixel_light
                brightness: 100%
                red: 100%
                green: 0%
                blue: 0%
                effect: "Blink"


    #Safety check to make sure that if the heater is 100% on for a while, the temperature actually increases.
      - if:
          condition:
            and:
              - lambda: 'return id(heat_value).state == 100;'
              - lambda: 'return id(heater_counter) == 0;' 
              - lambda: 'return id(temp_sensor).state < 100;'
          then:
            - lambda: |-
                id(heater_start_temp) = id(temp_sensor).state; 
            - lambda: |-
                id(heater_counter) = (id(heater_counter) + 1);
          else:
            - if:
                condition:
                  and:
                    - lambda: 'return id(heat_value).state == 100;'
                    - lambda: 'return id(heater_counter) > 0;'
                    - lambda: 'return id(temp_sensor).state < 100;'
                then:
                  - lambda: |-
                        id(heater_counter) = (id(heater_counter) + 1);
                else:
                  - lambda: |-
                        id(heater_counter) = 0;
                
      - if:
          condition:
            and:
              - lambda: 'return id(heater_counter) > 60;'
              - lambda: 'return id(temp_sensor).state <= id(heater_start_temp);'
              - lambda: 'return id(temp_sensor).state < 100;'
          then:
            - script.stop: roast_1
            - script.stop: roast_2
            - script.stop: roast_3
#            - script.stop: fan_mixer
            - climate.control:
                id: pid_controller
                mode: "OFF"
            - fan.turn_on:
                id: fan_speed
                speed: 100
            - text_sensor.template.publish:
                id: roast_status
                state: "ERROR: Heater On, Temperature Not Increasing"
            - light.turn_on:
                id: neopixel_light
                brightness: 100%
                red: 100%
                green: 0%
                blue: 0%
                effect: "Blink"
                
          else:
            - if:
                condition:
                  - lambda: 'return id(heater_counter) > 60;'
                then:
                  - lambda: |-
                      id(heater_counter) = 0;
   
number:

#Roast #1
  - platform: template
    name: Roast 1 Preheat Temp
    optimistic: true
    id: roast_1_preheat_temp
    device_class: temperature
    unit_of_measurement: °C
    max_value: 270
    min_value: 15
    step: 1
    initial_value: 120
    mode: box

  - platform: template
    name: Roast 1 Temp
    optimistic: true
    id: roast_1_temp
    device_class: temperature
    unit_of_measurement: °C
    max_value: 270
    min_value: 15
    step: 1
    initial_value: 221
    mode: box

  - platform: template
    name: Roast 1 Ramp
    optimistic: true
    id: roast_1_ramp
    icon: "mdi:thermometer-chevron-up"
    unit_of_measurement: °C/min
    max_value: 120
    min_value: 1
    step: 1
    initial_value: 10
    mode: box

  - platform: template
    name: Roast 1 Fan Speed
    optimistic: true
    id: roast_1_fan_speed
    icon: "mdi:fan-speed-1"
    unit_of_measurement: "%"
    max_value: 100
    min_value: 1
    step: 1
    initial_value: 75
    mode: box


#Roast #2
  - platform: template
    name: Roast 2 Preheat Temp
    optimistic: true
    id: roast_2_preheat_temp
    device_class: temperature
    unit_of_measurement: °C
    max_value: 270
    min_value: 15
    step: 1
    initial_value: 120
    mode: box

  - platform: template
    name: Roast 2 Temp
    optimistic: true
    id: roast_2_temp
    device_class: temperature
    unit_of_measurement: °C
    max_value: 270
    min_value: 15
    step: 1
    initial_value: 234
    mode: box

  - platform: template
    name: Roast 2 Ramp
    optimistic: true
    id: roast_2_ramp
    icon: "mdi:thermometer-chevron-up"
    unit_of_measurement: °C/min
    max_value: 120
    min_value: 1
    step: 1
    initial_value: 8
    mode: box

  - platform: template
    name: Roast 2 Fan Speed
    optimistic: true
    id: roast_2_fan_speed
    icon: "mdi:fan-speed-2"
    unit_of_measurement: "%"
    max_value: 100
    min_value: 1
    step: 1
    initial_value: 75
    mode: box


#Roast #3
  - platform: template
    name: Roast 3 Preheat Temp
    optimistic: true
    id: roast_3_preheat_temp
    device_class: temperature
    unit_of_measurement: °C
    max_value: 270
    min_value: 15
    step: 1
    initial_value: 120
    mode: box

  - platform: template
    name: Roast 3 Temp
    optimistic: true
    id: roast_3_temp
    device_class: temperature
    unit_of_measurement: °C
    max_value: 270
    min_value: 15
    step: 1
    initial_value: 221
    mode: box

  - platform: template
    name: Roast 3 Ramp
    optimistic: true
    id: roast_3_ramp
    icon: "mdi:thermometer-chevron-up"
    unit_of_measurement: °C/min
    max_value: 120
    min_value: 1
    step: 1
    initial_value: 10
    mode: box

  - platform: template
    name: Roast 3 Fan Speed
    optimistic: true
    id: roast_3_fan_speed
    icon: "mdi:fan-speed-3"
    unit_of_measurement: "%"
    max_value: 100
    min_value: 1
    step: 1
    initial_value: 50
    mode: box

button:
#Roast #1
  - platform: template
    name: "Start Roast #1"
    id: start_roast_1
    icon: "mdi:coffee"
    on_press:
        then:
            - if:
                condition:
                  lambda: 'return (id(currently_roasting) == 1);'
                then:
                else:
                    - script.execute: roast_1

#Roast #2
  - platform: template
    name: "Start Roast #2"
    id: start_roast_2
    icon: "mdi:coffee"
    on_press:
        then:
            - if:
                condition:
                  lambda: 'return (id(currently_roasting) == 1);'
                then:
                else:
                    - script.execute: roast_2

#Roast #3
  - platform: template
    name: "Start Roast #3"
    id: start_roast_3
    icon: "mdi:coffee"
    on_press:
        then:
            - if:
                condition:
                  lambda: 'return (id(currently_roasting) == 1);'
                then:
                else:
                    - script.execute: roast_3

globals:
    - id: currently_roasting
      type: int
      restore_value: no
      initial_value: '0'

    - id: current_temp_setpoint
      type: int
      restore_value: no
      initial_value: '0'
     
    #Used for heater safety interval check
    - id: heater_counter
      type: int
      restore_value: no
      initial_value: '0'
      
    - id: heater_start_temp
      type: int
      restore_value: no
      initial_value: '0'


script:

  - id: reduce_fan_speed
    mode: restart  
    then:
      - delay: 30s
      - fan.turn_on:
          id: fan_speed
          speed: 50
      - delay: 20s
      - fan.turn_on:
          id: fan_speed
          speed: 40
      - delay: 20s
      - fan.turn_on:
          id: fan_speed
          speed: 30


  - id: roast_timer
    mode: restart  
    then:
      - delay: 2700s
      - script.stop: roast_1
      - script.stop: roast_2
      - script.stop: roast_3
#      - script.stop: fan_mixer
      - climate.control:
          id: pid_controller
          mode: "OFF"
      - fan.turn_on:
          id: fan_speed
          speed: 100
      - text_sensor.template.publish:
          id: roast_status
          state: "ERROR: Roast Timeout"
      - light.turn_on:
          id: neopixel_light
          brightness: 100%
          red: 100%
          green: 0%
          blue: 0%
          effect: "Blink"


  - id: roast_1
    
    then:
      - globals.set:
          id: currently_roasting
          value: '1'

#      - globals.set:
#          id: current_fan_speed
#          value: !lambda |-
#                                return id(roast_1_fan_speed).state;

      - light.turn_on:
          id: button_light_switch_1
        
      - light.turn_off:
          id: button_light_switch_2
        
      - light.turn_off:
          id: button_light_switch_3
        
      - light.turn_on:
          id: neopixel_light
          brightness: 50%
          red: 100%
          green: 100%
          blue: 0%
        
      - fan.turn_on:
          id: fan_speed
          speed: !lambda |-
                                return id(roast_1_fan_speed).state;
        
      - delay: 5s
      
      - climate.control:
         id: pid_controller
         mode: HEAT
         target_temperature: !lambda |-
                                return id(roast_1_preheat_temp).state;
      
      - text_sensor.template.publish:
          id: roast_status
          state: "Roast #1 Preheat"
        
      - wait_until:
          condition:
            lambda: |-
              return id(temp_sensor).state > (id(roast_1_preheat_temp).state - 1);

          timeout: 300s

      - if:
          condition:
            - lambda: |-
                return id(temp_sensor).state < (id(roast_1_preheat_temp).state - 1);
                  
          then:
            - script.stop: roast_1
            - climate.control:
                id: pid_controller
                mode: "OFF"
            - fan.turn_on:
                id: fan_speed
                speed: 100
            - text_sensor.template.publish:
                id: roast_status
                state: "ERROR: Preheat Timeout"
            - light.turn_on:
                id: neopixel_light
                brightness: 100%
                red: 100%
                green: 0%
                blue: 0%
                effect: "Blink"
                  
          else:      
      
      - delay: 120s


#      - script.execute: fan_mixer


      - light.turn_on:
          id: neopixel_light
          brightness: 50%
          red: 100%
          green: 0%
          blue: 0%
        
      - globals.set:
                  id: current_temp_setpoint
                  value: !lambda |-
                            return id(roast_1_preheat_temp).state;
      
      - script.execute: roast_timer
      
      - text_sensor.template.publish:
          id: roast_status
          state: "Roast #1 Ramp-up"
      
      - while:
          condition:
            or:
              - lambda: |-
                  return id(temp_sensor).state < id(roast_1_temp).state;

#Was having issues with the roast ending early but not throwing errors. Changed from AND to OR above and added this to try at catch a situation where the temp sensor doesn't read a number (i.e. so it ISN'T less than the roast temp).              
              - lambda: |-  
                  return isnan(id(temp_sensor).state);
            
          then:
                
              - climate.control:
                  id: pid_controller
                  mode: HEAT
                  target_temperature: !lambda |-
                      return id(current_temp_setpoint);
                
              # delay will be calculated by dividing 60 by C/min value
              - delay: !lambda |-
                            return 60000 / id(roast_1_ramp).state;

              - if:
                  condition:
                    - lambda: |-
                        return id(current_temp_setpoint) == id(roast_1_temp).state;
                  
                  then:
                    - script.execute: reduce_fan_speed
                  
                  else:

              - if:
                  condition:
                    - lambda: |-
                        return id(current_temp_setpoint) < id(roast_1_temp).state;
                  
                  then:
                    - lambda: |-
                        id(current_temp_setpoint) = (id(current_temp_setpoint) + 1);
                  
                  else:
     
      - script.stop: roast_timer

      - script.stop: reduce_fan_speed
      

#      - script.stop: fan_mixer
      
      - light.turn_on:
          id: neopixel_light
          brightness: 50%
          red: 100%
          green: 65%
          blue: 0%
        
      - climate.control:
          id: pid_controller
          mode: "OFF"
          target_temperature: 15.5556°C

      - fan.turn_on:
          id: fan_speed
          speed: 100
      
      - text_sensor.template.publish:
          id: roast_status
          state: "Roast #1 Cooldown"
      
      - wait_until:
          condition:
              sensor.in_range:
                  id: temp_sensor
                  below: 40
          timeout: 1200s
      
      - delay: 60s
        
      - fan.turn_off: fan_speed

      - globals.set:
          id: currently_roasting
          value: '0'
      
      - text_sensor.template.publish:
          id: roast_status
          state: "Roast #1 Done"
      
      - light.turn_on:
          id: neopixel_light
          brightness: 50%
          red: 0%
          green: 100%
          blue: 0%


  - id: roast_2
    
    then:
      - globals.set:
          id: currently_roasting
          value: '1'


      - light.turn_on:
          id: button_light_switch_2
        
      - light.turn_off:
          id: button_light_switch_1
        
      - light.turn_off:
          id: button_light_switch_3
        
      - light.turn_on:
          id: neopixel_light
          brightness: 50%
          red: 100%
          green: 100%
          blue: 0%
        
      - fan.turn_on:
          id: fan_speed
          speed: !lambda |-
                                return id(roast_2_fan_speed).state;
        
      - delay: 5s
      
      - climate.control:
         id: pid_controller
         mode: HEAT
         target_temperature: !lambda |-
                                return id(roast_2_preheat_temp).state;
      
      - text_sensor.template.publish:
          id: roast_status
          state: "Roast #2 Preheat"
        
      - wait_until:
          condition:
            lambda: |-
              return id(temp_sensor).state > (id(roast_2_preheat_temp).state - 1);

          timeout: 300s

      - if:
          condition:
            - lambda: |-
                return id(temp_sensor).state < (id(roast_2_preheat_temp).state - 1);
                  
          then:
            - script.stop: roast_2
            - climate.control:
                id: pid_controller
                mode: "OFF"
            - fan.turn_on:
                id: fan_speed
                speed: 100
            - text_sensor.template.publish:
                id: roast_status
                state: "ERROR: Preheat Timeout"
            - light.turn_on:
                id: neopixel_light
                brightness: 100%
                red: 100%
                green: 0%
                blue: 0%
                effect: "Blink"
                  
          else:      
      
      - delay: 120s


      - light.turn_on:
          id: neopixel_light
          brightness: 50%
          red: 100%
          green: 0%
          blue: 0%
        
      - globals.set:
                  id: current_temp_setpoint
                  value: !lambda |-
                            return id(roast_2_preheat_temp).state;
      
      - script.execute: roast_timer
      
      - text_sensor.template.publish:
          id: roast_status
          state: "Roast #2 Ramp-up"
      
      - while:
          condition:
            or:
              - lambda: |-
                  return id(temp_sensor).state < id(roast_2_temp).state;

#Was having issues with the roast ending early but not throwing errors. Changed from AND to OR above and added this to try at catch a situation where the temp sensor doesn't read a number (i.e. so it ISN'T less than the roast temp).              
              - lambda: |-  
                  return isnan(id(temp_sensor).state);
            
          then:
                
              - climate.control:
                  id: pid_controller
                  mode: HEAT
                  target_temperature: !lambda |-
                      return id(current_temp_setpoint);
                
              # delay will be calculated by dividing 60 by C/min value
              - delay: !lambda |-
                            return 60000 / id(roast_2_ramp).state;

              - if:
                  condition:
                    - lambda: |-
                        return id(current_temp_setpoint) == id(roast_2_temp).state;
                  
                  then:
                    - script.execute: reduce_fan_speed
                  
                  else:

              - if:
                  condition:
                    - lambda: |-
                        return id(current_temp_setpoint) < id(roast_2_temp).state;
                  
                  then:
                    - lambda: |-
                        id(current_temp_setpoint) = (id(current_temp_setpoint) + 1);
                  
                  else:
      
      - script.stop: roast_timer

      - script.stop: reduce_fan_speed

    
      - light.turn_on:
          id: neopixel_light
          brightness: 50%
          red: 100%
          green: 65%
          blue: 0%
        
      - climate.control:
          id: pid_controller
          mode: "OFF"
          target_temperature: 15.5556°C

      - fan.turn_on:
          id: fan_speed
          speed: 100
      
      - text_sensor.template.publish:
          id: roast_status
          state: "Roast #2 Cooldown"
      
      - wait_until:
          condition:
              sensor.in_range:
                  id: temp_sensor
                  below: 40
          timeout: 1200s
      
      - delay: 60s
        
      - fan.turn_off: fan_speed

      - globals.set:
          id: currently_roasting
          value: '0'
      
      - text_sensor.template.publish:
          id: roast_status
          state: "Roast #2 Done"
      
      - light.turn_on:
          id: neopixel_light
          brightness: 50%
          red: 0%
          green: 100%
          blue: 0%


  - id: roast_3
    
    then:
      - globals.set:
          id: currently_roasting
          value: '1'


      - light.turn_on:
          id: button_light_switch_3
        
      - light.turn_off:
          id: button_light_switch_2
        
      - light.turn_off:
          id: button_light_switch_1
        
      - light.turn_on:
          id: neopixel_light
          brightness: 50%
          red: 100%
          green: 100%
          blue: 0%
        
      - fan.turn_on:
          id: fan_speed
          speed: !lambda |-
                                return id(roast_3_fan_speed).state;
        
      - delay: 5s
      
      - climate.control:
         id: pid_controller
         mode: HEAT
         target_temperature: !lambda |-
                                return id(roast_3_preheat_temp).state;
      
      - text_sensor.template.publish:
          id: roast_status
          state: "Roast #3 Preheat"
        
      - wait_until:
          condition:
            lambda: |-
              return id(temp_sensor).state > (id(roast_3_preheat_temp).state - 1);

          timeout: 300s

      - if:
          condition:
            - lambda: |-
                return id(temp_sensor).state < (id(roast_3_preheat_temp).state - 1);
                  
          then:
            - script.stop: roast_3
            - climate.control:
                id: pid_controller
                mode: "OFF"
            - fan.turn_on:
                id: fan_speed
                speed: 100
            - text_sensor.template.publish:
                id: roast_status
                state: "ERROR: Preheat Timeout"
            - light.turn_on:
                id: neopixel_light
                brightness: 100%
                red: 100%
                green: 0%
                blue: 0%
                effect: "Blink"
                  
          else:      

#Shortened from 120s to 30s to reduce energy usage while in trailer      
      - delay: 30s

      - light.turn_on:
          id: neopixel_light
          brightness: 50%
          red: 100%
          green: 0%
          blue: 0%
        
      - globals.set:
                  id: current_temp_setpoint
                  value: !lambda |-
                            return id(roast_3_preheat_temp).state;
      
      - script.execute: roast_timer
      
      - text_sensor.template.publish:
          id: roast_status
          state: "Roast #3 Ramp-up"
      
      - while:
          condition:
            or:
              - lambda: |-
                  return id(temp_sensor).state < id(roast_3_temp).state;

#Was having issues with the roast ending early but not throwing errors. Changed from AND to OR above and added this to try at catch a situation where the temp sensor doesn't read a number (i.e. so it ISN'T less than the roast temp).              
              - lambda: |-  
                  return isnan(id(temp_sensor).state);
            
          then:
                
              - climate.control:
                  id: pid_controller
                  mode: HEAT
                  target_temperature: !lambda |-
                      return id(current_temp_setpoint);
                
              # delay will be calculated by dividing 60 by C/min value
              - delay: !lambda |-
                            return 60000 / id(roast_3_ramp).state;

              - if:
                  condition:
                    - lambda: |-
                        return id(current_temp_setpoint) == id(roast_3_temp).state;
                  
                  then:
                    - script.execute: reduce_fan_speed
                  
                  else:


              - if:
                  condition:
                    - lambda: |-
                        return id(current_temp_setpoint) < id(roast_3_temp).state;
                  
                  then:
                    - lambda: |-
                        id(current_temp_setpoint) = (id(current_temp_setpoint) + 1);
                  
                  else:
      
      - script.stop: roast_timer

      - script.stop: reduce_fan_speed

      - light.turn_on:
          id: neopixel_light
          brightness: 50%
          red: 100%
          green: 65%
          blue: 0%
        
      - climate.control:
          id: pid_controller
          mode: "OFF"
          target_temperature: 15.5556°C

      - fan.turn_on:
          id: fan_speed
          speed: 100
      
      - text_sensor.template.publish:
          id: roast_status
          state: "Roast #3 Cooldown"
      
      - wait_until:
          condition:
              sensor.in_range:
                  id: temp_sensor
                  below: 40
          timeout: 1200s
      
      - delay: 60s
        
      - fan.turn_off: fan_speed

      - globals.set:
          id: currently_roasting
          value: '0'
      
      - text_sensor.template.publish:
          id: roast_status
          state: "Roast #3 Done"
      
      - light.turn_on:
          id: neopixel_light
          brightness: 50%
          red: 0%
          green: 100%
          blue: 0%


  - id: blank_script
    
    then:


light:

  - platform: binary
    name: Button Light 1
    output: button_light_1
    id: button_light_switch_1
    restore_mode: ALWAYS_OFF

  - platform: binary
    name: Button Light 2
    output: button_light_2
    id: button_light_switch_2
    restore_mode: ALWAYS_OFF

  - platform: binary
    name: Button Light 3
    output: button_light_3   
    id: button_light_switch_3
    restore_mode: ALWAYS_OFF
    
  - platform: neopixelbus
    type: RGB
    variant: SK6812
    pin: GPIO22
    num_leds: 1
    name: NeoPixel
    id: neopixel_light
    restore_mode: ALWAYS_OFF
    default_transition_length: 0s
    effects:
      - pulse:
          name: "Blink"
          transition_length: 0s
          update_interval: 1s


binary_sensor:
  - platform: gpio
    pin:
        number: GPIO27
        inverted: true
        mode:
            input: true
            pullup: true
    name: Button 1
    on_press:
        then:
            - if:
                condition:
                  lambda: 'return (id(currently_roasting) == 1);'
                then:
                else:
                    - script.execute: roast_1

  - platform: gpio
    pin:
        number: GPIO32
        inverted: true
        mode:
            input: true
            pullup: true
    name: Button 2
    on_press:
        then:
            - if:
                condition:
                  lambda: 'return (id(currently_roasting) == 1);'
                then:
                else:
                    - script.execute: roast_2

  - platform: gpio
    pin:
        number: GPIO33
        inverted: true
        mode:
            input: true
            pullup: true
    name: Button 3
    on_press:
        then:
            - if:
                condition:
                  lambda: 'return (id(currently_roasting) == 1);'
                then:
                else:
                    - script.execute: roast_3


output:


  - id: button_light_1
    platform: gpio
    pin: GPIO4

  - id: button_light_2
    platform: gpio
    pin: GPIO25
    
  - id: button_light_3
    platform: gpio
    pin: GPIO26


  - platform: ledc
    pin: GPIO16
    frequency: 1000 Hz #might need to change to get it to work?
    id: fan_pwm
    min_power: 0.2
    zero_means_zero: true

  - platform: slow_pwm
    pin: GPIO17
    id: heater_pwm
    period: 15s
    turn_on_action:
      - fan.turn_on:
          id: fan_speed


    
    #Required this action and leaving it blank threw errors, so I just made a script to call that does nothing.
    turn_off_action:
      - script.execute: blank_script
    

fan:
  - platform: speed
    output: fan_pwm
    name: Fan
    id: fan_speed
    restore_mode: ALWAYS_OFF

climate:
  - platform: pid
    name: PID Controller
    id: pid_controller
    sensor: temp_sensor
    default_target_temperature: 15.5556°C
    heat_output: heater_pwm
    control_parameters:
      kp: 0.07276 #0.05009
      ki: 0.00364 #0.00230
      kd: 0.36376 #0.27302
    visual:
      min_temperature: 15.5556°C
      max_temperature: 270°C
      temperature_step: 1°C
    

switch:
  - platform: template
    name: PID Controller Autotune
    turn_on_action:
      - climate.pid.autotune: pid_controller

spi:
  clk_pin: GPIO14 #14 #18
  mosi_pin: GPIO13 #13 #23
  miso_pin: GPIO12 #12 #19

sensor:
  - platform: max31855
    name: Temperature
    cs_pin: GPIO15 #15 #5
    update_interval: 1s
    #filters:
    #  - lambda: return x * (9.0/5.0) + 32.0;
    #unit_of_measurement: "°F"
    id: temp_sensor
    on_value_range:
      - above: 280
        then:
          - script.stop: roast_1
          - script.stop: roast_2
          - script.stop: roast_3
#          - script.stop: fan_mixer
          - climate.control:
              id: pid_controller
              mode: "OFF"
          - fan.turn_on:
              id: fan_speed
              speed: 100
          - text_sensor.template.publish:
              id: roast_status
              state: "ERROR: OVERHEAT PROTECTION"
          - light.turn_on:
                id: neopixel_light
                brightness: 100%
                red: 100%
                green: 0%
                blue: 0%
                effect: "Blink"

  - platform: wifi_signal
    name: WiFi Strength
    update_interval: 60s
    disabled_by_default: true
  - platform: uptime
    name: Uptime
    disabled_by_default: true

  - platform: pid
    name: PID Kp
    type: KP
  - platform: pid
    name: PID Ki
    type: KI
  - platform: pid
    name: PID Kd
    type: KD
  - platform: pid
    name: PID Heat
    type: HEAT
    id: heat_value

#heat_value is a percent but the state reports back 0-100, not 0-1
#Used a power meter to get the power draw at the various settings
  - platform: template
    name: Power
    unit_of_measurement: "W"
    device_class: "power"
    lambda: |-
      if (id(fan_speed).state) {
        return 1 + 57.5 + (id(heat_value).state)*13.805;
      } else {
        return 1;
      }
    update_interval: 1s

# Progress indicator sensors
  - platform: template
    name: "Roast Progress"
    id: roast_progress
    lambda: |-
      if (id(roast_timer).is_running() and id(roast_1).is_running()) {
        return (id(roast_1_percent_complete).state);
      } else if (id(roast_timer).is_running() and id(roast_2).is_running()){
        return (id(roast_2_percent_complete).state);
      } else if (id(roast_timer).is_running() and id(roast_3).is_running()){
        return (id(roast_3_percent_complete).state);
      } else if ((id(roast_status).state == "Roast #1 Done") or (id(roast_status).state == "Roast #2 Done") or (id(roast_status).state == "Roast #3 Done") or (id(roast_status).state == "Roast #1 Cooldown") or (id(roast_status).state == "Roast #2 Cooldown") or (id(roast_status).state == "Roast #3 Cooldown") ){
          return 100;
      } else {
        return 0;
      }
    update_interval: 5s
    unit_of_measurement: "%"
    icon: mdi:timer-sand
    accuracy_decimals: 0

  - platform: template
    name: "Roast #1 Percent Complete"
    id: roast_1_percent_complete
    lambda: return (id(temp_sensor).state);
    update_interval: 5s
    unit_of_measurement: "%"
    icon: mdi:timer-sand
    accuracy_decimals: 0
    internal: true
    filters:
      - calibrate_linear:
         method: least_squares
         datapoints:
          - 120 -> 0
          - 221 -> 100
      - clamp:
          min_value: 0
          max_value: 100

  - platform: template
    name: "Roast #2 Percent Complete"
    id: roast_2_percent_complete
    lambda: return (id(temp_sensor).state);
    update_interval: 5s
    unit_of_measurement: "%"
    icon: mdi:timer-sand
    accuracy_decimals: 0
    internal: true
    filters:
      - calibrate_linear:
         method: least_squares
         datapoints:
          - 120 -> 0
          - 234 -> 100
      - clamp:
          min_value: 0
          max_value: 100

  - platform: template
    name: "Roast #3 Percent Complete"
    id: roast_3_percent_complete
    lambda: return (id(temp_sensor).state);
    update_interval: 5s
    unit_of_measurement: "%"
    icon: mdi:timer-sand
    accuracy_decimals: 0
    internal: true
    filters:
      - calibrate_linear:
         method: least_squares
         datapoints:
          - 120 -> 0
          - 221 -> 100
      - clamp:
          min_value: 0
          max_value: 100

text_sensor:
  - platform: wifi_info
    ip_address:
      name: IP Address
  
  - platform: template
    name: Status
    id: roast_status
    icon: "mdi:card-text"
    

status_led:
  pin:
    number: GPIO2
    inverted: false

Discussions