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;
}