Hello. Everyone. If anyone is interested, I’ve migrated my Arduino MKR 1000-based code to esphome.
Designed as a controller for a cold room or walk-in refrigerator. It’s similar in concept to a CoolBot unit that tricks a standard window AC unit into getting colder than it normally will (shuts off at 60°F). It does this by turning on a small heater (a 5w power resistor) strapped to the main temp sensor on the AC unit. By turning this heater on, it tricks the AC into thinking the room is warmer than it actually is, and will run the AC down to whatever temp you set. There’s an additional temp sensor on the fins of the AC unit to sense if they are icing up, and to defrost them if necessary. This will chill down to 34°, if desired. A set of two small fans that vent outside air into the room, and a small fan that vents air out. If it’s cold enough outside, these two fans instead of the more expensive to run AC unit.
The code is designed to Config is below. The project utilizes a 5v 2-relay board, 3 Dallas Temperature sensors and a SSD1306 display.
# === Substitutions ===
# Define reusable variables for the project name and type to simplify configuration updates.
substitutions:
name: "smart-cooler"
type: ESP
# === ESPHome Configuration ===
# Basic settings for the ESPHome integration, defining the device name and its user-friendly display name.
esphome:
name: "${name}"
friendly_name: "smart-cooler"
comment: Smart Cooler - Cold Room Control System
# === ESP32 Board and Framework ===
# Specify the board and framework type for the ESP32 device.
esp32:
board: esp32-c3-devkitm-1
framework:
type: arduino
# === Global Variables ===
# Define global variables to store and manage device states.
globals:
- id: ac_state # Tracks whether the AC (or heater) is active.
type: bool
initial_value: 'false'
- id: fan_state # Tracks whether the fan is active.
type: bool
initial_value: 'false'
- id: deadband_state # Manages the deadband logic to prevent rapid toggling.
type: bool
initial_value: 'false'
- id: freeze_recovery # Indicates if freeze recovery mode is active.
type: bool
initial_value: 'false'
- id: hysteresis # Temperature hysteresis range to avoid frequent toggling.
type: float
initial_value: '1.0'
- id: margin # Safety margin to prevent fins from freezing.
type: float
initial_value: '4.0'
# === Logging Configuration ===
# Enable logging to monitor the device's behavior.
logger:
level: WARN
# === API Configuration ===
# Enable the API for integration with Home Assistant, including encryption for secure communication.
api:
encryption:
key: "p97Kk5PMQqd+PrebH7TEIzNhwPrxHVqRQnxLKiWZhq0="
# === OTA Updates ===
# Allow Over-The-Air updates with a secure password.
ota:
- platform: esphome
password: "994a754fd02223e9e8143b409cd1ec76"
# === Wi-Fi Configuration ===
# Configure Wi-Fi settings, including a fallback hotspot in case of connection failure.
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none
reboot_timeout: 0s
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "smart-cooler"
password: "BnHdWV9EqyDt"
# === Text Sensors ===
# Provide network-related information (e.g., IP, SSID, MAC address) for diagnostics.
text_sensor:
- platform: wifi_info
ip_address:
name: "IP Address"
icon: "mdi:ip"
ssid:
name: "Connected SSID"
icon: "mdi:wifi"
bssid:
name: "Connected BSSID"
icon: "mdi:wifi"
mac_address:
name: "Mac Wifi Address"
icon: "mdi:lan-connect"
dns_address:
name: "DNS Address"
icon: "mdi:ip"
- platform: template
name: " Currently Running"
id: currently_running
icon: "mdi:earth"
lambda: |-
if (id(ac_state)) {
return {"AC"};
} else if (id(fan_state)) {
return {"FANS"};
} else if (id(deadband_state)) {
return {"DEADBAND"};
} else if (id(freeze_recovery)) {
return {"FREEZE"};
} else {
return {"OFF"};
}
update_interval: 5s
web_server:
sorting_group_id: state_settings
# === Captive Portal ===
# Enable a fallback interface for device setup if Wi-Fi fails.
captive_portal:
# === Web Server ===
# Set up a web server for direct control and monitoring, with authentication.
web_server:
version: 3
auth:
username: saintmoor
password: saintmoor
log: true
sorting_groups:
- id: relay_settings
name: "Relays"
sorting_weight: -25
- id: temperature_settings
name: "Temperature"
sorting_weight: -20
- id: set_point_settings
name: "Temperature SetPoints"
sorting_weight: -15
- id: state_settings
name: "Status"
sorting_weight: -10
# === HTTP Request ===
# Enable HTTP requests for external integrations.
http_request:
useragent: esphome/device
timeout: 10s
verify_ssl: false
# === Time Synchronization ===
# Synchronize the device time with NTP servers.
time:
- platform: sntp
id: sntp_time
timezone: America/New_York
servers:
- 0.pool.ntp.org
- 1.pool.ntp.org
- 2.pool.ntp.org
# === Number Controls ===
# Define configurable settings for temperature setpoints and hysteresis.
number:
- platform: template
name: "Target Temp" # Target room temperature.
id: target_temp
unit_of_measurement: "°F"
icon: "mdi:temperature-fahrenheit"
optimistic: true
min_value: 34
max_value: 121
step: 1
initial_value: 34
restore_value: true
web_server:
sorting_group_id: set_point_settings
- platform: template
name: "Hysteresis Range" # Range for temperature hysteresis.
id: hysteresis_range
unit_of_measurement: "°F"
icon: "mdi:temperature-fahrenheit"
optimistic: true
min_value: 0.0
max_value: 2.0
step: 0.5
initial_value: 0.5
restore_value: true
web_server:
sorting_group_id: set_point_settings
- platform: template
name: "Freeze Recovery Margin" # Range for freezing temperature value addition.
id: safety_margin
unit_of_measurement: "°F"
icon: "mdi:temperature-fahrenheit"
optimistic: true
min_value: 0
max_value: 4
step: 1
initial_value: 4
restore_value: true
web_server:
sorting_group_id: set_point_settings
# === 1-Wire Sensors ===
# Configure temperature sensors for monitoring room, fins, and outside temperatures.
one_wire:
- platform: gpio
pin: GPIO10
id: dallas_1
# === i2c Sensors ===
# Configure temperature sensors for monitoring room, fins, and outside temperatures.
i2c:
sda: 9
scl: 7
scan: true
id: bus_a
frequency: 400kHz
# === LED Control ===
# Configure an onboard LED with color effects for status indication.
light:
- platform: neopixelbus
type: GRB
variant: WS2811
pin: GPIO08
name: "Onboard LED"
id: onboard_led
icon: "mdi:led-strip"
num_leds: 1
restore_mode: ALWAYS_ON
on_turn_on:
then:
- light.control:
brightness: 25%
id: onboard_led
effect: Green
effects: # Predefined LED effects for visual feedback.
- addressable_color_wipe: # Solid color effects.
name: Red
colors:
- red: 100%
green: 0%
blue: 0%
num_leds: 1
reverse: false
- addressable_color_wipe:
name: Green
colors:
- red: 0%
green: 100%
blue: 0%
num_leds: 1
reverse: false
- addressable_color_wipe:
name: Blue
colors:
- red: 0%
green: 0%
blue: 100%
num_leds: 1
reverse: false
- addressable_color_wipe:
name: White
colors:
- red: 100%
green: 100%
blue: 100%
num_leds: 1
reverse: false
- pulse: # Blinking effects.
name: "Fast Pulse"
transition_length: 0.5s
update_interval: 0.5s
min_brightness: 0%
max_brightness: 100%
- pulse:
name: "Slow Pulse"
transition_length: 1s # defaults to 1s
update_interval: 2s
# === Binary Sensors ===
# Monitor various states, such as AC, fan, and freeze recovery status.
binary_sensor:
- platform: status
name: "Status"
icon: "mdi:state-machine"
- platform: template
name: "AC State"
id: ac_state_value
icon: "mdi:earth"
lambda: |-
if (id(ac_state)) {
return true;
} else {
return false;
}
web_server:
sorting_group_id: state_settings
- platform: template
name: "FAN State"
id: fan_state_value
icon: "mdi:earth"
lambda: |-
if (id(fan_state)) {
return true;
} else {
return false;
}
web_server:
sorting_group_id: state_settings
- platform: template
name: "DEADBAND State"
id: deadband_state_value
icon: "mdi:earth"
lambda: |-
if (id(deadband_state)) {
return true;
} else {
return false;
}
web_server:
sorting_group_id: state_settings
- platform: template
name: "Freeze Recovery"
id: freeze_recovery_value
icon: "mdi:earth"
lambda: |-
if (id(freeze_recovery)) {
return true;
} else {
return false;
}
web_server:
sorting_group_id: state_settings
#=====CONFIG FOR OLED====#
# Control display output for sensor and state information.
font:
- file: 'fonts/arial.ttf'
id: arial_medium
size: 14
- file: "fonts/OpenSans-Regular.ttf"
id: opensans_medium
size: 12
- file: "fonts/OpenSans-Regular.ttf"
id: opensans_small
size: 10
- file: "gfonts://Roboto" # gfonts://family[@weight]
id: roboto
size: 20
- file: "gfonts://Roboto"
id: roboto_medium
size: 15
- file: "gfonts://Roboto"
id: roboto_small
size: 12
- file: "gfonts://Roboto"
id: roboto_smallest
size: 10
- file: 'fonts/BebasNeue-Regular.ttf'
id: bebasneue_large
size: 48
- file: 'fonts/BebasNeue-Regular.ttf'
id: bebasneue_medium
size: 32
- file: 'fonts/Silkscreen-Regular.ttf'
id: silkscreen_medium
size: 10
- file: 'fonts/Silkscreen-Regular.ttf'
id: silkscreen_small
size: 8
- file: 'fonts/arial.ttf'
id: arial_large
size: 16
image:
- file: mdi:alert-outline
id: alert_image
resize: 60x60
- file: mdi:snowflake
id: snowflake_image
resize: 60x60
- file: mdi:radiator
id: radiator_image
resize: 60x60
- file: mdi:power-off
id: power_off_image
resize: 60x60
- file: mdi:power-on
id: power_on_image
resize: 60x60
display:
- platform: ssd1306_i2c
model: "SSD1306 128x64"
address: 0x3C
i2c_id: bus_a
# reset_pin: D0
id: oled_display
auto_clear_enabled: True
show_test_card: true
# update_interval: 5s
pages:
- id: page1
lambda: |-
// it.printf(X, Y,.. (X (Row) and Y (Column))
// Print "Smart Cooler" in top center.
it.printf(it.get_width() / 2, 8, id(roboto_medium), TextAlign::TOP_CENTER, "Smart Cooler");
// Print time in HH:MM format
it.strftime(0, 60, id(bebasneue_large), TextAlign::BASELINE_LEFT, "%H:%M", id(sntp_time).now());
// Print ROOM temperature
if (id(room_temp).has_state()) {
it.printf(127, 23, id(arial_medium), TextAlign::TOP_RIGHT, "%.1f°", id(room_temp).state);
}
// Print Target temperature
if (id(target_temp).has_state()) {
it.printf(127, 60, id(arial_medium), TextAlign::BASELINE_RIGHT, "%.1f°", id(target_temp).state);
}
- id: page2
lambda: |-
// Print "Currently Running" in the top center
it.printf(it.get_width() / 2, 8, id(roboto_medium), TextAlign::TOP_CENTER, "Currently Running");
// Determine the text to display
std::string status_text;
if (id(ac_state)) {
status_text = "AC";
} else if (id(fan_state)) {
status_text = "FANS";
} else if (id(deadband_state)) {
status_text = "DEADBAND";
} else if (id(freeze_recovery)) {
status_text = "FREEZE";
} else {
status_text = "OFF";
}
// Print the status text centered at the bottom
it.printf(it.get_width() / 2, it.get_height() - 4, id(bebasneue_medium), TextAlign::BASELINE_CENTER, "%s", status_text.c_str());
- id: page3
lambda: |-
// Print "Target Temp" in top center.
it.printf(it.get_width() / 2, 8, id(roboto_medium), TextAlign::TOP_CENTER, "Target Temp");
// Print Target temperature in baseline center
if (id(target_temp).has_state()) {
it.printf(it.get_width() / 2, it.get_height() - 4, id(bebasneue_large), TextAlign::BASELINE_CENTER, "%.1f°", id(target_temp).state);
}
- id: page4
lambda: |-
// Print "Room Temp" in top center.
it.printf(it.get_width() / 2, 8, id(roboto_medium), TextAlign::TOP_CENTER, "Room Temp");
// Print Room temperature in baseline center
if (id(room_temp).has_state()) {
it.printf(it.get_width() / 2, it.get_height() - 4, id(bebasneue_large), TextAlign::BASELINE_CENTER, "%.1f°", id(room_temp).state);
}
- id: page5
lambda: |-
// Print "FINS Temp" in top center.
it.printf(it.get_width() / 2, 8, id(roboto_medium), TextAlign::TOP_CENTER, "Fins Temp");
// Print Room temperature in baseline center
if (id(fins_temp).has_state()) {
it.printf(it.get_width() / 2, it.get_height() - 4, id(bebasneue_large), TextAlign::BASELINE_CENTER, "%.1f°", id(fins_temp).state);
}
- id: page6
lambda: |-
// Print "Outside Temp" in top center.
it.printf(it.get_width() / 2, 8, id(roboto_medium), TextAlign::TOP_CENTER, "Outside Temp");
// Print Outside temperature in baseline center
if (id(ext_temp).has_state()) {
it.printf(it.get_width() / 2, it.get_height() - 4, id(bebasneue_large), TextAlign::BASELINE_CENTER, "%.1f°", id(ext_temp).state);
}
- id: page7
lambda: |-
// Print "Hysteresis" in top center.
it.printf(it.get_width() / 2, 8, id(roboto_medium), TextAlign::TOP_CENTER, "Hysteresis");
// Print Hystereis Range in baseline center
if (id(hysteresis_range).has_state()) {
it.printf(it.get_width() / 2, it.get_height() - 4, id(bebasneue_large), TextAlign::BASELINE_CENTER, "%.1f°", id(hysteresis_range).state);
}
- id: page8
lambda: |-
// Print "Recovery Margin" in top center.
it.printf(it.get_width() / 2, 8, id(roboto_medium), TextAlign::TOP_CENTER, "Recovery Margin");
// Print Freeze Recovery Margin in baseline center
if (id(safety_margin).has_state()) {
it.printf(it.get_width() / 2, it.get_height() - 4, id(bebasneue_large), TextAlign::BASELINE_CENTER, "%.1f°", id(safety_margin).state);
}
- id: starting
lambda: |-
it.printf(it.get_width() / 2, 8, id(roboto), TextAlign::TOP_CENTER, "Smart Cooler");
- id: snowflake
lambda: |-
// Draw the image snowflake at position [x=0,y=0]
it.image(64, 60, id(snowflake_image));
- id: alert
lambda: |-
// Draw the image alert at position [x=0,y=0]
it.image(0, 0, id(alert_image));
- id: power_off
lambda: |-
// Draw the image power_off at position [x=0,y=0]
it.image(0, 0, id(power_off_image));
- id: power_on
lambda: |-
// Draw the image power_on at position [x=0,y=0]
it.image(0, 0, id(power_on_image));
- id: radiator
lambda: |-
// Draw the image radiator at position [x=0,y=0]
it.image(0, 0, id(radiator_image));
# === Sensors ===
# Read and display temperature sensor values.
sensor:
- platform: uptime
name: "Uptime" # Device uptime in seconds
- platform: internal_temperature
name: "Internal Temperature" # ESP32 internal temperature in °F
id: "internal_temperature_f"
unit_of_measurement: "°F"
icon: "mdi:temperature-fahrenheit"
accuracy_decimals: 0
device_class: "temperature"
state_class: "measurement"
filters:
- lambda: return x * (9.0/5.0) + 32.0; # Convert Celsius to Fahrenheit
- filter_out: nan
- platform: dallas_temp
address: 0x690417a1115aff28 # ROOM_SENSOR
one_wire_id: dallas_1
name: "Room Temperature"
id: room_temp
icon: "mdi:thermometer"
device_class: "temperature"
state_class: "measurement"
accuracy_decimals: 1
update_interval: 10s
resolution: 9
filters:
- throttle: 1s
- heartbeat: 5s
- debounce: 0.1s
- lambda: return x * (9.0/5.0) + 32.0; # Convert temperature readings to Fahrenheit.
- filter_out: nan
unit_of_measurement: "°F"
on_value_range:
- above: 125
then:
- logger.log:
level: ERROR
format: "Sensor reading too high! Value: %.2f"
args: [x]
- delay: 30s
- logger.log: "Retrying sensor..."
web_server:
sorting_group_id: temperature_settings
- platform: dallas_temp
address: 0x990417a10ca3ff28 # FINS_SENSOR
one_wire_id: dallas_1
name: "Fins Temperature"
id: fins_temp
icon: "mdi:thermometer"
device_class: "temperature"
state_class: "measurement"
accuracy_decimals: 1
update_interval: 10s
resolution: 9
filters:
- throttle: 1s
- heartbeat: 5s
- debounce: 0.1s
- lambda: return x * (9.0/5.0) + 32.0; # Convert temperature readings to Fahrenheit.
- filter_out: nan
unit_of_measurement: "°F"
on_value_range:
- above: 125
then:
- logger.log:
level: ERROR
format: "Sensor reading too high! Value: %.2f"
args: [x]
- delay: 30s
- logger.log: "Retrying sensor..."
web_server:
sorting_group_id: temperature_settings
- platform: dallas_temp
address: 0x0e0417a116ffff28 # EXT_SENSOR
one_wire_id: dallas_1
name: "Outside Temperature"
id: ext_temp
icon: "mdi:thermometer"
device_class: "temperature"
state_class: "measurement"
accuracy_decimals: 1
update_interval: 10s
resolution: 9
filters:
- throttle: 1s
- heartbeat: 5s
- debounce: 0.1s
- lambda: return x * (9.0/5.0) + 32.0; # Convert temperature readings to Fahrenheit.
- filter_out: nan
unit_of_measurement: "°F"
on_value_range:
- above: 125
then:
- logger.log:
level: ERROR
format: "Sensor reading too high! Value: %.2f"
args: [x]
- delay: 30s
- logger.log: "Retrying sensor..."
web_server:
sorting_group_id: temperature_settings
# == Other Sensors ==
# Configure target temp, and other sensors
- platform: template
name: "Target Temp"
id: target_temp_value
icon: "mdi:earth"
lambda: |-
return id(target_temp).state;
on_value:
then:
- globals.set:
id: target
value: !lambda 'return x;'
update_interval: 5s
unit_of_measurement: "°F"
accuracy_decimals: 0
web_server:
sorting_group_id: state_settings
- platform: template
name: "Hysteresis"
id: hysteresis_range_value
icon: "mdi:earth"
lambda: |-
return id(hysteresis_range).state;
on_value:
then:
- globals.set:
id: hysteresis
value: !lambda 'return x;'
update_interval: 5s
unit_of_measurement: "°F"
accuracy_decimals: 1
web_server:
sorting_group_id: state_settings
- platform: template
name: "Freeze Recovery Temp"
id: recovery_temp
icon: "mdi:earth"
lambda: |-
return 32.0 + id(safety_margin).state;
on_value:
then:
- globals.set:
id: margin
value: !lambda 'return x;'
update_interval: 5s
unit_of_measurement: "°F"
accuracy_decimals: 0
web_server:
sorting_group_id: state_settings
# === Switches ===
# Control relays for heater and outside fans.
switch:
- platform: restart
name: "Restart"
- platform: gpio
pin: GPIO04
id: heater
name: "Heater (AC)"
icon: "mdi:radiator"
restore_mode: ALWAYS_OFF
web_server:
sorting_group_id: relay_settings
- platform: gpio
pin: GPIO05
id: ext_fans
name: "Outside Fans"
icon: "mdi:fan"
restore_mode: ALWAYS_OFF
web_server:
sorting_group_id: relay_settings
- platform: template
name: " Power Switch"
id: power_switch
icon: "mdi:power"
optimistic: True
restore_mode: RESTORE_DEFAULT_OFF
turn_on_action:
- logger.log: "Power Turned On!"
- light.turn_on:
id: onboard_led
brightness: 25%
effect: Green
turn_off_action:
- logger.log: "Power Turned Off!"
- light.turn_on:
id: onboard_led
brightness: 25%
effect: Red
web_server:
sorting_group_id: state_settings
# === Scripts ===
# Define scripts for handling specific tasks, like freeze recovery.
script:
- id: freeze_recovery_script
mode: queued
then:
- lambda: |-
id(freeze_recovery) = true;
ESP_LOGW("freeze_recovery", "Starting freeze recovery process.");
- switch.turn_off: heater
- switch.turn_off: ext_fans
- delay: 60s
- while:
condition:
lambda: "return id(fins_temp).state <= 32.0 + id(safety_margin).state;"
then: # Logic to recover from fins freezing.
- delay: 60s
- lambda: |-
id(freeze_recovery) = false;
ESP_LOGI("freeze_recovery", "Recovery process complete.");
# === Main Logic ===
# Control relays and temperature settings based on sensor data and state variables.
interval:
- interval: 16s
then:
- display.page.show: page1
- component.update: oled_display
- delay: 3s
- display.page.show: page2
- component.update: oled_display
- delay: 3s
- display.page.show: page3
- component.update: oled_display
- delay: 2s
- display.page.show: page4
- component.update: oled_display
- delay: 2s
- display.page.show: page5
- component.update: oled_display
- delay: 2s
- display.page.show: page6
- component.update: oled_display
- delay: 2s
- display.page.show: page7
- component.update: oled_display
- delay: 2s
- display.page.show: page8
- component.update: oled_display
- delay: 2s
- interval: 15s
then:
- lambda: |-
float upper_threshold = id(target) + id(hysteresis);
float lower_threshold = id(target) - id(hysteresis);
float outside_temp = id(ext_temp).state;
float cooler_temp = id(room_temp).state;
bool is_ac_active = id(ac_state);
bool is_fan_active = id(fan_state);
bool is_deadband_active = id(deadband_state);
bool is_freezing = id(fins_temp).state <= 32.0;
bool is_freeze_recovery_active = id(freeze_recovery);
bool is_power_on = id(power_switch);
bool is_ext_temp_cooling = outside_temp <= (cooler_temp - 20);
bool is_room_above_upper = cooler_temp > upper_threshold;
bool is_room_below_lower = cooler_temp <= lower_threshold;
// Exterior Cooling Logic
if (id(power_switch).state == 1) {
if (is_ext_temp_cooling && is_room_above_upper && !is_freeze_recovery_active) {
if (id(ac_state)) {
ESP_LOGD("logic", "FANS are on; turning off heater.");
id(heater).turn_off();
id(ac_state) = 0;
}
id(ext_fans).turn_on();
id(fan_state) = 1;
} else {
id(ext_fans).turn_off();
id(fan_state) = 0;
}
// Heater Control Logic with Hysteresis
if (is_deadband_active && !is_freezing && id(room_temp).state >= upper_threshold && !is_freeze_recovery_active) {
ESP_LOGW("logic", "Room temperature above upper threshold; turning on heater.");
id(heater).turn_on();
id(ac_state) = 1;
// id(ext_fans).turn_off();
// id(fan_state) = 0;
id(deadband_state) = 0;
} else if (!is_freezing && !is_deadband_active && id(room_temp).state > id(target_temp).state && !is_freeze_recovery_active) {
ESP_LOGW("logic", "Room temperature above target; keeping heater on.");
id(heater).turn_on();
id(ac_state) = 1;
// id(ext_fans).turn_off();
// id(fan_state) = 0;
} else if (is_room_below_lower && is_ac_active) {
ESP_LOGW("logic", "Room temperature below lower threshold; turning off heater.");
id(heater).turn_off();
id(ac_state) = 0;
id(deadband_state) = 1;
id(ext_fans).turn_off();
id(fan_state) = 0;
} else if (is_room_below_lower && !is_ac_active) {
ESP_LOGW("logic", "Room temperature below lower threshold; keeping heater off.");
id(heater).turn_off();
id(ac_state) = 0;
id(deadband_state) = 1;
id(ext_fans).turn_off();
id(fan_state) = 0;
}
} else if (id(power_switch).state == 0) {
ESP_LOGW("logic", "Power is off. Controller is not running.");
id(heater).turn_off();
id(ac_state) = 0;
id(deadband_state) = 0;
id(ext_fans).turn_off();
id(fan_state) = 0;
}