Freezer Monitor

Hi, all. Here is a another project for esphome. This is a freezer monitor (Outdoor, garage, etc) that monitors the ambient temparture and turns on a heater (light bulb, etc) if the temperature hits the setpoint. The devices used includes dallas temperature sensors, a 5v 2-relay and a SSD1306 oled display.

substitutions:
  name: "freezer-monitor"
  type: ESP

esphome:
  name: freezer-monitor
  friendly_name: "freezer-monitor"
  comment: Freezer temperature monitor.  Freezer always on, heaters on below setpoint temperature.

esp32:
  board: esp32dev
  framework:
    type: arduino

# === Global Variables ===
# Define global variables to store and manage device states.
globals:
  - id: freezer_state # Tracks whether the freezer is active.
    type: int
    initial_value: '1'
  - id: heater_state # Tracks whether the heater is active.
    type: int
    initial_value: '0'
  - id: upper_threshold_value # Temperature hysteresis range to avoid frequent toggling.
    type: float
    initial_value: '45.0'
  - id: lower_threshold_value # Temperature hysteresis range to avoid frequent toggling.
    type: float
    initial_value: '35.0'

# === Logging Configuration ===
# Enable logging to monitor the device's behavior.
logger:
#  level: WARN

# === Remote Control and Updates ===
api:
  encryption:
    key: "xxxx"

# === Wi-Fi Configuration ===
# Setting up Wi-Fi connection details and a fallback hotspot.
ota:
  platform: esphome

wifi:
  ssid: !secret wifi_ssid2
  password: !secret wifi_password2
  ap:
    ssid: "${name} Fallback Hotspot"
    password: "password"

# === Captive Portal ===
# For fallback access when not connected to Wi-Fi.
captive_portal:

# === Web Server ===
# Local web interface for monitoring and control.
web_server:
  version: 3
  sorting_groups:
    - id: relay_settings
      name: "Relays"
      sorting_weight: -30
    - id: temperature_settings
      name: "Temperature"
      sorting_weight: -25
    - id: set_point_settings
      name: "Temperature SetPoints"
      sorting_weight: -20
    - id: humidity_settings
      name: "Humidity"
      sorting_weight: -15
    - id: state_settings
      name: "State"
      sorting_weight: -10

# === Time Configuration ===
# Synchronize time using SNTP.
time:
  - platform: sntp
    id: sntp_time
    timezone: America/New_York
    servers:
     - 0.pool.ntp.org
     - 1.pool.ntp.org
     - 2.pool.ntp.org

# === Number Component ===
# Adjustable temperature setpoint for controlling heaters.
number:
  - platform: template
    name: "SetPoint Temp"
    id: setpoint_temp
    unit_of_measurement: "°F"
    icon: "mdi:temperature-fahrenheit"
    optimistic: true
    min_value: 35
    max_value: 45
    step: 1
    initial_value: 45
    restore_value: true
    web_server:
      sorting_group_id: set_point_settings

# === GPIO Outputs ===
# Configuring an LED output.
output:
  - platform: gpio
    pin:
      number: 25
      mode: output
    id: LED

# === Text Sensors ===
# Providing network-related information.
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"

# === Binary Sensors ===
# Status monitoring and heater state.
binary_sensor:
  - platform: status
    name: "${type} Status"
    icon: "mdi:state-machine"

  - platform: template
    name: "Heater Status"
    icon: "mdi:earth"
    id: heater_state_value
    lambda: |-
      if (id(heater).state)  {
        return true;
      } else {
        return false;
      }
    web_server:
      sorting_group_id: state_settings

  - platform: template
    name: "Freezer Power Status"
    icon: "mdi:earth"
    id: freezer_state_value
    lambda: |-
      if (id(freezer).state) {
        return true;
      } else {
        return false;
      }
    web_server:
      sorting_group_id: state_settings

# === Switches ===
# Configuring GPIO relays and a restart switch.
switch:
  - platform: restart
    icon: mdi:reload-alert
    name: "${type} Restart"
  - platform: gpio
    name: "Freezer Power"
    id: freezer
    icon: "mdi:electric-switch"
    restore_mode: ALWAYS_ON
    pin: 
      number: 21
      inverted: false
    on_turn_on:
    - logger.log: "Freezer Power Turned On!"
    on_turn_off:
    - logger.log: "Freezer Power Turned Off!"
    web_server:
      sorting_group_id: relay_settings
  - platform: gpio
    name: "Heater"
    id: heater
    icon: "mdi:electric-switch"
    restore_mode: ALWAYS_OFF
    pin: 
      number: 19
      inverted: true
    on_turn_on:
    - logger.log: "Heater Turned On!"
    on_turn_off:
    - logger.log: "Heater Turned Off!"
    web_server:
      sorting_group_id: relay_settings
  - platform: gpio
    name: "Relay 3"
    id: relay3
    icon: "mdi:electric-switch"
    pin: 
      number: 18
      inverted: false
    on_turn_on:
    - logger.log: "Relay 3 Turned On!"
    on_turn_off:
    - logger.log: "Relay 3 Turned Off!"
    internal: true
    web_server:
      sorting_group_id: relay_settings
  - platform: gpio
    name: "Relay 4"
    id: relay4
    icon: "mdi:electric-switch"
    pin: 
      number: 5
      inverted: false
    on_turn_on:
    - logger.log: "Relay 4 Turned On!"
    on_turn_off:
    - logger.log: "Relay 4 Turned Off!"
    internal: true
    web_server:
      sorting_group_id: relay_settings

# === One-Wire Bus ===
# Setting up one-wire protocol for Dallas sensors.
one_wire:
  - platform: gpio
    pin: 02
    id: dallas_1

# === Sensors ===
# Monitoring uptime, temperatures, and pressure.
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: template
    name: "Lower Threshold"
    id: lower_threshold
    unit_of_measurement: "°F"
    icon: "mdi:temperature-fahrenheit"
    accuracy_decimals: 0
    lambda: "return id(setpoint_temp).state;"
    update_interval: 5s
    web_server:
      sorting_group_id: state_settings
  - platform: template
    name: "Upper Threshold"
    id: upper_threshold
    unit_of_measurement: "°F"
    icon: "mdi:temperature-fahrenheit"
    accuracy_decimals: 0
    lambda: "return id(setpoint_temp).state + 5;"
    update_interval: 5s
    web_server:
      sorting_group_id: state_settings

  - platform: dallas_temp
    address: 0xd70313949761c028
    one_wire_id: dallas_1
    name: "Ambient Temperature"
    id: ambient_temp
    icon: "mdi:thermometer"
    device_class: "temperature"
    state_class: "measurement"
    accuracy_decimals: 0
    update_interval: 15s
    resolution: 9
    filters:
      - throttle: 1s
      - heartbeat: 5s
      - debounce: 0.1s
      - lambda: return x * (9.0/5.0) + 32.0;
      - filter_out: nan
    unit_of_measurement: "°F"
    web_server:
      sorting_group_id: temperature_settings

  - platform: dallas_temp
    address: 0x940309949743e728
    one_wire_id: dallas_1
    name: "Freezer Temperature"
    id: freezer_temp
    icon: "mdi:thermometer"
    device_class: "temperature"
    state_class: "measurement"
    accuracy_decimals: 0
    update_interval: 15s
    resolution: 9
    filters:
      - throttle: 1s
      - heartbeat: 5s
      - debounce: 0.1s
      - lambda: return x * (9.0/5.0) + 32.0;
      - filter_out: nan
    unit_of_measurement: "°F"
    web_server:
      sorting_group_id: temperature_settings

  - platform: dallas_temp
    address: 0xed031294973fff28
    one_wire_id: dallas_1
    name: "Compressor Temperature"
    id: compressor_temp
    icon: "mdi:thermometer"
    device_class: "temperature"
    state_class: "measurement"
    accuracy_decimals: 0
    update_interval: 15s
    resolution: 9
    filters:
      - throttle: 1s
      - heartbeat: 5s
      - debounce: 0.1s
      - lambda: return x * (9.0/5.0) + 32.0;
      - filter_out: nan
    unit_of_measurement: "°F"
    web_server:
      sorting_group_id: temperature_settings

# === i2c Sensors ===
# Configure temperature sensors for monitoring room, fins, and exterior temperatures.  
i2c:
  sda: 15
  scl: 14
  scan: true
  id: bus_a
  frequency: 400kHz

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

display:
  - platform: ssd1306_i2c
    model: "SSD1306 128x64"
    address: 0x3C
    i2c_id: bus_a
    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 "Freezer" in top center.
          it.printf(it.get_width() / 2, 8, id(roboto_medium), TextAlign::TOP_CENTER, "Freezer");
          // 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(ambient_temp).has_state()) {
          it.printf(127, 23, id(arial_medium), TextAlign::TOP_RIGHT, "%.1f°", id(ambient_temp).state);
          }
          // Print Freezer temperature
          if (id(setpoint_temp).has_state()) {
          it.printf(127, 60, id(arial_medium), TextAlign::BASELINE_RIGHT, "%.1f°", id(setpoint_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(heater_state) == 1 && id(heater_state) == 1) {
          status_text = "HEAT";
          } else {
          status_text = "NONE";
          }
          // Print the status text centered at the bottom
          it.printf(it.get_width() / 2, it.get_height() - 4, id(bebasneue_large), 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
          if (id(setpoint_temp).has_state()) {
          it.printf(it.get_width() / 2, it.get_height() - 4, id(bebasneue_large), TextAlign::BASELINE_CENTER, "%.1f°", id(setpoint_temp).state);
          }
      - id: page4
        lambda: |-
          // Print "Ambient Temp" in top center.
          it.printf(it.get_width() / 2, 8, id(roboto_medium), TextAlign::TOP_CENTER, "Ambient Temp");
          // Print Ambient1 temperature
          if (id(ambient_temp).has_state()) {
          it.printf(it.get_width() / 2, it.get_height() - 4, id(bebasneue_large), TextAlign::BASELINE_CENTER, "%.1f°", id(ambient_temp).state);
          }
      - id: page5
        lambda: |-
          // Print "Freezer Temp" in top center.
          it.printf(it.get_width() / 2, 8, id(roboto_medium), TextAlign::TOP_CENTER, "Freezer Temp");
          // Print Freezer temperature
          if (id(freezer_temp).has_state()) {
          it.printf(it.get_width() / 2, it.get_height() - 4, id(bebasneue_large), TextAlign::BASELINE_CENTER, "%.1f°", id(freezer_temp).state);
          }
      - id: page6
        lambda: |-
          // Print "Compressor Temp" in top center.
          it.printf(it.get_width() / 2, 8, id(roboto_medium), TextAlign::TOP_CENTER, "Compressor Temp");
          // Print Compressor temperature
          if (id(compressor_temp).has_state()) {
          it.printf(it.get_width() / 2, it.get_height() - 4, id(bebasneue_large), TextAlign::BASELINE_CENTER, "%.1f°", id(compressor_temp).state);
          }
      - id: name
        lambda: |-
            it.print(64, 0, id(roboto), TextAlign::TOP_CENTER, "Pumphouse");
      - id: startup
        lambda: |-
            // Draw a circle in the middle of the display
            it.filled_circle(it.get_width() / 2, it.get_height() / 2, 20);

interval:
  - interval: 20s
    then:
      - display.page.show: page1
      - component.update: oled_display
      - delay: 4s
      - display.page.show: page2
      - component.update: oled_display
      - delay: 4s
      - 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
  - interval: 15s
    then:
      - lambda: |-
          float upper_threshold = id(upper_threshold);
          float lower_threshold = id(lower_threshold);
          bool is_heating_active = id(heater_state);
          bool is_freezer_active = id(freezer_state);
          float is_ambient = id(ambient_temp).state;
          float is_ambient_above_upper = id(ambient_temp).state > upper_threshold;
          float is_ambient_below_lower = id(ambient_temp).state <= lower_threshold;

          // Heater Control Logic
          if (id(ambient_temp).state <= id(setpoint_temp).state) {
            ESP_LOGW("logic", "Temperature above upper threshold; turning on heater.");
            id(heater).turn_on();
            id(heater_state) = 1;
          } else if (id(ambient_temp).state > id(setpoint_temp).state + 5) {
            ESP_LOGW("logic", "Temperature above target; ; turning off heater.");
            id(heater).turn_off();
            id(heater_state) = 0;
          }