-
Still Roasting
01/15/2025 at 17:58 • 0 commentsBeen 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
-
On The Road
10/23/2022 at 23:44 • 0 commentsFinally took the roaster on the road and had some unexpected bad results; the ESP-32 kept seeming to randomly reset while in the middle or roasting. I initially thought the adjustable voltage regulator had dropped too low from it riding in the trailer while driving, but this proved to not be the case. Then it dawned on me... "I wonder if it times out when it can't connect to my Home Assistant instance?" Some quick googling and I confirmed that by default, ESPHome has a 15 minute timeout when it can't connect. Adding a single line to my 'code' to set this to zero fixed things and I've been successfully roasting coffee while on the road! Huzzah!
-
Exhaust Vent Upgrade
09/15/2022 at 16:14 • 0 commentsWhile the weather where I live (central US) is currently pretty pleasant, it gets quite hot (and of even more import, quite cold) at times. Not really wanting to have to clear snow off of my table outside just to roast some coffee, combined with the fact that I'm not sure that the roaster could hit the proper temps having to heat COLD air, I decided a while ago that I wanted the option of roasting inside. While the extra heat isn't a problem in the winter (and the smell isn't particularly unpleasant), the chaff that comes off the beans makes a bit of a mess even when outside. A quick visit to a hardware store (and the salvaging of an extra soup can) as well as designing and printing up a simple slide to allow me to keep the window mostly closed and voila: I have a simple setup that allows me to roast inside but vent the exhaust air outside.
As an extra bonus, it was able to achieve my roast temp more quickly/accurately than before (there's normally been a little slowdown in the temp rise as it gets close to the roast temperature). My guess is it's the reduced airflow due to not sitting it on an open table (the air inlets are on the bottom of the roaster) and/or the restriction of the vent hose. Regardless of the exact cause, it gives me more precise control over the roast which is an unintended benefit!
-
It's Alive!
09/03/2022 at 14:27 • 0 commentsThe enclosure finally finished printing yesterday afternoon, and the day before the PCB arrived in the mail! While printing the lids, I was able to install and wire everything up last night too.
Soldered up the PCB last night while the enclosure finished printing While my initial test runs with no beans were less than great (I had to drop the fan speed to 4% to even be able to hit roasting temps), I decided to add some beans anyway this morning and give it a go even at the low fan speed. As expected, it didn't mix the beans very well at such a low speed and I actually ended up burning some before I even got to my 'pre-heat' temperature. I ended up bumping the fan speed to 65% and got much better mixing and went ahead and tried ramping it up to roasting temp; to my surprise, it didn't have any problems hitting it! Obviously having the beans in the chamber slowed the airflow down enough to allow it to hit temp, I just didn't expect it to make such a significant difference. Ended up getting a pretty decent/even first-roast and while waiting a day is 'how it's done' I couldn't resist brewing up a cup this morning. While it's not at the level of my favorite local roaster, it's WAY better than Folger's! I'll be curious how it tastes tomorrow.
-
Enclosure Designed
08/29/2022 at 18:31 • 0 commentsFinished up my design for the enclosure. It's decent size, and while I'd considered breaking it into a couple of pieces (minus the obvious lids, etc.) it fits neatly on the bed of my CR-10s, so I opted for keeping it in one piece. Normally I tend to print these types of things in an exciting color, like black or gray. With this being both a large print and something that's more akin to a consumer appliance, I decided to buy a roll of filament just for it (Cura says it should take just over half a roll). Opted for a dark green color; once it arrives I'll start printing and hopefully not to long thereafter the PCB will arrive.
In the meantime, I went ahead and printed the control panel piece. I opted to do so with translucent filament as the PCB will be underneath it: I'll use the integrated LED on the ESP32 to give me the status of the WiFi connection and it's also the part of the internals I wouldn't mind showing off a little. You can also see in the background that I printed a test piece for the fan/heater mount; glad I did this as I ended up tweaking it a little to make sure it fits well in the final print.
-
'Code' Basically Done
08/21/2022 at 15:35 • 0 commentsFrom the beginning, I had decided I was going to use ESPHome to control everything so I could have a pretty front-end and easily log data. Normally when I've done this, I've left the actual control to Home Assistant, but for this project I wanted all of the 'smarts' local. This is both because the automation is more involved and controlling mains power items (like a heater) and I explicitly want to be able to use it away from WiFi.
I spent a lot more time than I'd care to admit trying to figure out how to write the code locally in ESPHome. I think I just really overthought things as in the end, it ended up being pretty easy (minus the maddening formatting issues of YAML). I got the general outline of a roast profile setup, with buttons and lights to indicate at what stage things are at. I also added some safety features/checks to shut the whole thing down if something unsafe occurs, such as too high of a temperature or the heater being on but not the fan.
While I continue to wait for the PCBs to arrive, all that is left is to make a case for the whole thing. Shouldn't be complicated, but between the transformer, SSR, PCB and the actual roaster there's a decent bit of stuff I need to find a place for while keeping it compact and safe.
I also got some high-temp insulation to wrap around the metal bits. Considered just leaving it exposed, but felt like having a bunch of metal and high temps readily accessible is probably a bad idea. -
PCB Ordered!
08/17/2022 at 15:59 • 0 commentsAfter tearing apart a popcorn popper and in general, 'making it work the way I want' with an ESP32, I was able to add my other 'wants' such as some buttons and lights and put together a schematic in Eagle.
A few hours later and I had a PCB.
Now I just have to wait for it to arrive in a few weeks. In the meantime, I've got plenty to do: both the 3D printed enclosure to clean everything up needs to get designed and I need to write the actual code for the roasting process. This last part is dead-simple in practice, but since I'm making myself use ESPHome to control the ESP32 I think it will be a little more involved (I've never used ESPHome for anything other than making simple sensors, etc. in Home Assistant). It LOOKS like I can use a 'Generic Custom Component' to essentially just write some code like it's a normal Arduino project, but we'll see if this is actually the case.