Pumphouse Monitor

Hi, all. Here’s another project code for esphome. It is a pumhouse monitor that turns on heat if the temperature gets to a setpoint. The devices used include 5 dallas temperature sensors, a 5v 4-relay (only 2 used), a 145PSI pressure transducer to monitor the pressure in the water line, and a SSD1306 oled display.

# === Substitutions ===
# Global placeholders for device configuration, making it easier to reuse values across the code.
substitutions:
  name: "pumphouse-monitor"
  type: ESP

# === ESPHome Core Configuration ===
esphome:
  name: "pumphouse-monitor"
  friendly_name: "${name}"
  comment: Pumphouse pressure and temperature monitor with heaters.
#  on_boot:
#    priority: 250
#    then:
#      - display.page.show: startup
#      - component.update: oled_display

# === Hardware Details ===
# Configuring the ESP32 board and its framework.
esp32:
  board: esp32dev
  framework:
    type: arduino

# === Global Variables ===
# Define global variables to store and manage device states.
globals:
  - id: heater1_state # Tracks whether the AC (or heater) is active.
    type: bool
    initial_value: 'false'
  - id: heater2_state # Tracks whether the fan is active.
    type: bool
    initial_value: 'false'
  - id: power_state # Tracks whether power is active.
    type: bool
    initial_value: 'true'
  - id: offset_pressure
    type: float
    initial_value: '0.0' # Adjustable offset
  - id: offset_volts
    type: float
    initial_value: '0.95' # Adjustable offset
  - id: hysteresis # Temperature hysteresis range to avoid frequent toggling.
    type: float
    initial_value: '1.0'
  - id: upper_threshold # Temperature upper threshold.
    type: float
    initial_value: '45.0'
  - id: lower_threshold # Temperature lower threshold.
    type: float
    initial_value: '35.0'

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

api:
  encryption:
    key: "5O3dyYlDQPVyRDLtfCOxfjxCB/YGShlaUbny2e4stLo="

ota:
  - platform: esphome
    password: "42328bff318e2e68a427d48c4d5ecab3"

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  ap:
    ssid: "T-Relay 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
  auth:
    username: saintmoor
    password: saintmoor
  sorting_groups:
    - id: relay_settings
      name: "Relays"
      sorting_weight: -35
    - id: pressure_settings
      name: "Pressure"
      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: "Status"
      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
  - 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
    max_value: 2
    step: 0.5
    initial_value: 1
    restore_value: true
    web_server:
      sorting_group_id: set_point_settings
    internal: false

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

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

# === 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"
  - platform: template
    name: " Currently Running"
    id: currently_running
    icon: "mdi:earth"
    lambda: |-
      if (id(heater1_state) == 1 && id(heater2_state) == 1) {
        return {"HEAT"};
      } else if (id(heater1_state) == 1 && id(heater2_state) == 0 && id(power_switch).state == 1) {
        return {"HEAT"};
      } else if (id(heater1_state) == 0 && id(heater2_state) == 1 && id(power_switch).state == 1) {
        return {"HEAT"};
      } else if (id(heater1_state) == 0 && id(heater2_state) == 0 && id(power_switch).state == 1) {
        return {"NONE"};
      } else if (id(power_switch).state == 0) {
        return {"OFF"};
      } else {
        return {"OFF"};
      }
    update_interval: 5s
    web_server:
      sorting_group_id: state_settings

# === Binary Sensors ===
# Status monitoring and heater state.
binary_sensor:
  - platform: status
    name: "Status" # Device online/offline status
    icon: "mdi:state-machine"
  - platform: template
    name: "HEATER 1 State"
    id: heater1_state_value
    icon: "mdi:earth"
    lambda: |-
      if (id(heater1_state)) {
        return true;      
      } else {
        return false;
      }
    web_server:
      sorting_group_id: state_settings
  - platform: template
    name: "HEATER 2 State"
    id: heater2_state_value
    icon: "mdi:earth"
    lambda: |-
      if (id(heater2_state)) {
        return true;      
      } else {
        return false;
      }
    web_server:
      sorting_group_id: state_settings
  - platform: template
    name: "Power State"
    id: power_state_value
    icon: "mdi:earth"
    internal: false
    lambda: |-
      if (id(power_state)) {
        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

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 "Pumphouse" in top center.
          it.printf(it.get_width() / 2, 8, id(roboto_medium), TextAlign::TOP_CENTER, "Pumphouse");
          // 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(ambient1_temp).has_state()) {
          it.printf(127, 23, id(arial_medium), TextAlign::TOP_RIGHT, "%.1f°", id(ambient1_temp).state);
          }
          // Print Target 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(heater1_state) == 1 && id(heater2_state) == 1 && id(power_switch).state == 1) {
          status_text = "HEAT";
          } else if (id(heater1_state) == 1 && id(heater2_state) == 0 && id(power_switch).state == 1) {
          status_text = "HEAT";
          } else if (id(heater1_state) == 0 && id(heater2_state) == 1 && id(power_switch).state == 1) {
          status_text = "HEAT";
          } else if (id(heater1_state) == 0 && id(heater2_state) == 0 && id(power_switch).state == 1) {
          status_text = "NONE";
          } else if (id(power_switch).state == 0) {
          status_text = "OFF";
          } else {
          status_text = "OFF";
          }
          // 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 "Ambient1 Temp" in top center.
          it.printf(it.get_width() / 2, 8, id(roboto_medium), TextAlign::TOP_CENTER, "Ambient1 Temp");
          // Print Ambient1 temperature
          if (id(ambient1_temp).has_state()) {
          it.printf(it.get_width() / 2, it.get_height() - 4, id(bebasneue_large), TextAlign::BASELINE_CENTER, "%.1f°", id(ambient1_temp).state);
          }
      - id: page5
        lambda: |-
          // Print "Ambient2 Temp" in top center.
          it.printf(it.get_width() / 2, 8, id(roboto_medium), TextAlign::TOP_CENTER, "Ambient2 Temp");
          // Print Ambient2 temperature
          if (id(ambient2_temp).has_state()) {
          it.printf(it.get_width() / 2, it.get_height() - 4, id(bebasneue_large), TextAlign::BASELINE_CENTER, "%.1f°", id(ambient2_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
          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 "Pipe Temp" in top center.
          it.printf(it.get_width() / 2, 8, id(roboto_medium), TextAlign::TOP_CENTER, "Pipe Temp");
          // Print Pipe temperature
          if (id(pipe_temp).has_state()) {
          it.printf(it.get_width() / 2, it.get_height() - 4, id(bebasneue_large), TextAlign::BASELINE_CENTER, "%.1f°", id(pipe_temp).state);
          }
      - id: page8
        lambda: |-
          // Print "Tank Temp" in top center.
          it.printf(it.get_width() / 2, 8, id(roboto_medium), TextAlign::TOP_CENTER, "Tank Temp");
          // Print Tank temperature
          if (id(tank_temp).has_state()) {
          it.printf(it.get_width() / 2, it.get_height() - 4, id(bebasneue_large), TextAlign::BASELINE_CENTER, "%.1f°", id(tank_temp).state);
          }
      - id: page9
        lambda: |-
          // Print "Hysteresis Range" in top center.
          it.printf(it.get_width() / 2, 8, id(roboto_medium), TextAlign::TOP_CENTER, "Hysteresis Range");
          // Print Hysteresis Range value
          if (id(tank_temp).has_state()) {
          it.printf(it.get_width() / 2, it.get_height() - 4, id(bebasneue_large), TextAlign::BASELINE_CENTER, "%.1f°", id(hysteresis_range).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);

# === Sensors ===
# Monitoring uptime, temperatures, and pressure.
sensor:
# === DS18B20 Sensors ===
# Configure temperature and humidity sensors
# === 1 Ambient1 - Pumphouse
# === 2 Ambient2 - Enclosure
# === 3 Pipe
# === 4 Outside
# === 5 Tank
  - platform: dallas_temp
    address: 0x790623b285d4d028
    name: "Pumphouse Temperature" # Temperature reading from Dallas sensor
    id: ambient1_temp
    icon: "mdi:thermometer"
    device_class: "temperature"
    state_class: "measurement"
    resolution: 9
    accuracy_decimals: 0
    update_interval: 10s
    filters:
      - throttle: 1s
      - heartbeat: 5s
      - debounce: 0.1s
      - lambda: return x * (9.0/5.0) + 32.0; # Convert Celsius 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: 0x520923b09941f428
    name: "Enclosure Temperature" # Temperature reading from Dallas sensor
    id: ambient2_temp
    icon: "mdi:thermometer"
    device_class: "temperature"
    state_class: "measurement"
    resolution: 9
    accuracy_decimals: 0
    update_interval: 10s
    filters:
      - throttle: 1s
      - heartbeat: 5s
      - debounce: 0.1s
      - lambda: return x * (9.0/5.0) + 32.0; # Convert Celsius 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: 0x360923b09641ca28
    name: "Pipe Temperature" # Another Dallas sensor for pipe temperature
    id: pipe_temp
    icon: "mdi:thermometer"
    device_class: "temperature"
    state_class: "measurement"
    resolution: 9
    accuracy_decimals: 0
    update_interval: 10s
    filters:
      - throttle: 1s
      - heartbeat: 5s
      - debounce: 0.1s
      - lambda: return x * (9.0/5.0) + 32.0; # Convert Celsius to Fahrenheit
      - filter_out: nan
    unit_of_measurement: "°F"
    web_server:
      sorting_group_id: temperature_settings
  - platform: dallas_temp
    address: 0xf60723b0adda1a28
    name: "Outside Temperature"
    id: ext_temp
    icon: "mdi:thermometer"
    device_class: "temperature"
    state_class: "measurement"
    resolution: 9
    accuracy_decimals: 0
    update_interval: 10s
    filters:
      - throttle: 1s
      - heartbeat: 5s
      - debounce: 0.1s
      - lambda: return x * (9.0/5.0) + 32.0; # Convert Celsius 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: 0x470923b099a59a28
    name: "Tank Temperature"
    id: tank_temp
    icon: "mdi:thermometer"
    device_class: "temperature"
    state_class: "measurement"
    resolution: 9
    accuracy_decimals: 0
    update_interval: 10s
    filters:
      - throttle: 1s
      - heartbeat: 5s
      - debounce: 0.1s
      - lambda: return x * (9.0/5.0) + 32.0; # Convert Celsius 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

# === Pressure Sensor ===
# Configure pressure sensor
  - platform: adc
    pin: GPIO32
    name: "Pressure (raw)" # Raw pressure sensor reading in volts
    id: pressure_raw
    device_class: voltage
    attenuation: auto
    filters:
      - lambda: return x; # No filtering applied
      - round: 1
    unit_of_measurement: "v"
    icon: "mdi:gauge"
    accuracy_decimals: 1
    update_interval: 2s
    internal: true
    web_server:
      sorting_group_id: pressure_settings
  - platform: template
    name: "Pressure (psi)" # Calculated pressure in psi
    id: pressure_psi
    device_class: pressure
    lambda: |-
      if (id(pressure_raw).state < id(offset_volts)) return 0;
      return (id(pressure_raw).state - id(offset_volts));
    update_interval: 1s
    unit_of_measurement: 'psi'
    icon: "mdi:gauge"
    accuracy_decimals: 0
    filters:
          # Remove small fluctuations in voltage
      - median:
          window_size: 7
          send_every: 1
          send_first_at: 1
      # Smooth the signal using an exponential moving average
      - exponential_moving_average:
          alpha: 0.1 # Adjust alpha for desired smoothing (lower = smoother)
          send_every: 1
      - calibrate_linear:
          # Calibration points in volts to pressure (in MPa)
          - 0.00 -> 0.0 # Sensor outputs 0.5V at 0 MPa
          - 3.30 -> 145.0 # Sensor outputs 4.5V at 1 MPa
    web_server:
      sorting_group_id: pressure_settings

# === SHT-30 Sensors ===
# Configure temperature and humidity sensors
  - platform: sht3xd
    address: 0x44
    update_interval: 10s
    temperature:
      name: "Ambient Temperature"
      id: ambient_temp
      icon: "mdi:thermometer"
      device_class: "temperature"
      accuracy_decimals: 1
      filters:
        - throttle: 1s
        - heartbeat: 5s
        - debounce: 0.1s
        - filter_out: nan
        - delta: 5.0
        - lambda: return x * (9.0/5.0) + 32.0; # Convert Celsius to Fahrenheit
      unit_of_measurement: "°C"
      web_server:
        sorting_group_id: temperature_settings
      internal: true
    humidity:
      name: "Relative Humidity"
      id: relative_humidity
      accuracy_decimals: 0
      icon: "mdi:water-percent"
      web_server:
        sorting_group_id: humidity_settings
  - platform: absolute_humidity
    name: "Absolute Humidity"
    temperature: ambient_temp
    humidity: relative_humidity
    web_server:
      sorting_group_id: humidity_settings
  - platform: template
    name: "Temperature Difference" # Difference between two temperature readings
    id: "temp_delta"
    unit_of_measurement: "°F"
    icon: "mdi:thermometer"
    accuracy_decimals: 0
    device_class: "temperature"
    state_class: "measurement"
    update_interval: 15s
    lambda: |-
      return id(ext_temp).state - id(ambient1_temp).state;
    filters:
      - filter_out: nan  
    web_server:
      sorting_group_id: temperature_settings

# === Other Sensors ===
# Configure uptime, internal temp, and other sensors
  - 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: "Hysteresis"
    id: hysteresis_range_value
    unit_of_measurement: "°F"
    icon: "mdi:earth"
    lambda: |-
      return id(hysteresis_range).state;
    on_value:
      then:
        - globals.set:
            id: hysteresis
            value: !lambda 'return x;'
    update_interval: 5s
    web_server:
      sorting_group_id: state_settings
  - platform: template
    name: "Lower Threshold"
    id: lower_threshold_value
    unit_of_measurement: "°F"
    icon: "mdi:temperature-fahrenheit"
    accuracy_decimals: 0
    lambda: |-
      return id(setpoint_temp).state;
    on_value:
      then:
        - globals.set:
            id: lower_threshold
            value: !lambda 'return x;'
    update_interval: 5s
    web_server:
      sorting_group_id: state_settings
  - platform: template
    name: "Upper Threshold"
    id: upper_threshold_value
    unit_of_measurement: "°F"
    icon: "mdi:temperature-fahrenheit"
    accuracy_decimals: 0
    lambda: |-
      return id(setpoint_temp).state + 5;
    on_value:
      then:
        - globals.set:
            id: upper_threshold
            value: !lambda 'return x;'
    update_interval: 5s
    web_server:
      sorting_group_id: state_settings

# === Switches ===
# Configuring GPIO relays and a restart switch.
switch:
  - platform: restart
    icon: mdi:reload-alert
    name: "Restart" # Manual restart switch
  - platform: gpio
    name: "Heater 1"
    id: heater1
    icon: "mdi:radiator"
    restore_mode: ALWAYS_OFF
    pin: 
      number: 21
      inverted: false
    on_turn_on:
        - logger.log: "Heater 1 Turned On!"  # Log relay activation
    on_turn_off:
      - logger.log: "Heater 1 Turned Off!"  # Log relay deactivation
    web_server:
      sorting_group_id: relay_settings
  - platform: gpio
    name: "Heater 2"
    id: heater2
    icon: "mdi:radiator"
    restore_mode: ALWAYS_OFF
    pin: 
      number: 19
      inverted: false
    on_turn_on:
      - logger.log: "Heater 2 Turned On!"  # Log relay activation
    on_turn_off:
      - logger.log: "Heater 2 Turned Off!"  # Log relay deactivation  
    web_server:
      sorting_group_id: relay_settings
  - platform: gpio
    name: "Relay 3"
    id: relay3
    icon: "mdi:electric-switch"
    restore_mode: ALWAYS_OFF
    pin: 
      number: 18
      inverted: false
    on_turn_on:
      - logger.log: "Relay 3 Turned On!"  # Log relay activation
    on_turn_off:
      - logger.log: "Relay 3 Turned Off!"  # Log relay deactivation
    internal: true  
    web_server:
      sorting_group_id: relay_settings
  - platform: gpio
    name: "Relay 4"
    id: relay4
    icon: "mdi:electric-switch"
    restore_mode: ALWAYS_OFF
    pin: 
      number: 5
      inverted: false
    on_turn_on:
      - logger.log: "Relay 4 Relay Turned On!"  # Log relay activation
    on_turn_off:
      - logger.log: "Relay 4 Relay Turned Off!"  # Log relay deactivation
    internal: true
    web_server:
      sorting_group_id: relay_settings
  - platform: template
    name: "  Power Switch"
    id: power_switch
    icon: "mdi:power"
    optimistic: True
    restore_mode: ALWAYS_ON
    turn_on_action:
      - logger.log: "Power Turned On!"
#      - light.turn_on: onboard_led
    turn_off_action:
      - logger.log: "Power Turned Off!"
#      - light.turn_off: onboard_led
    web_server:
      sorting_group_id: state_settings

# === Intervals ===
# Run actions at fixed time intervals
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
      - 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_temp = id(upper_threshold) + 5 - id(hysteresis);
          float lower_threshold_temp = id(lower_threshold) + id(hysteresis);
          float is_ext_temp = id(ext_temp).state;
          bool is_heating1_active = id(heater1_state);
          bool is_heating2_active = id(heater2_state);
          bool is_power_on = id(power_state);
          bool is_pumphouse_above_upper = is_ext_temp > upper_threshold_temp;
          bool is_pumphouse_below_lower = is_ext_temp <= lower_threshold_temp;

          // Heater Control Logic with Hysteresis
          if (id(power_switch).state == 1) {
            if (is_pumphouse_below_lower) {
              ESP_LOGW("logic", "Temperature below lower threshold; turning on heaters.");
              id(power_state) = 1;
              id(heater1).turn_on();
              id(heater1_state) = 1;
              id(heater2).turn_on();
              id(heater2_state) = 1;
            } else if (is_pumphouse_below_lower && is_heating1_active && is_heating2_active) {
              ESP_LOGW("logic", "Temperature below lower threshold; ; keeping heaters on.");
              id(power_state) = 1;
              id(heater1).turn_on();
              id(heater1_state) = 1;
              id(heater2).turn_on();
              id(heater2_state) = 1;
            } else if (is_pumphouse_above_upper) {
              ESP_LOGW("logic", "Temperature above upper threshold; ; turning off heaters.");
              id(power_state) = 1;
              id(heater1).turn_off();
              id(heater1_state) = 0;
              id(heater2).turn_off();
              id(heater2_state) = 0;
            } 
          } else if (id(power_switch).state == 0) {
            ESP_LOGW("logic", "Power is off.  Controller is not running.");
            id(power_state) = 0;
            id(heater1).turn_off();
            id(heater1_state) = 0;
            id(heater2).turn_off();
            id(heater2_state) = 0;
          }