Skip to content

Instantly share code, notes, and snippets.

@mekaneck
Last active November 2, 2023 14:46
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mekaneck/f7ea83b5aa331fdc357da097f3651d93 to your computer and use it in GitHub Desktop.
Save mekaneck/f7ea83b5aa331fdc357da097f3651d93 to your computer and use it in GitHub Desktop.
ESPHome YAML code for monitor of sump pump & battery-powered backup sump pump
substitutions:
# Device Naming
devicename: sump-pump
friendly_name: Sump Pump
device_description: Sump Pump Level Sensor, Counter, and Alarm
# Limits for pump levels and alarm levels
primary_pumpout_below_height: '0.100' #meters; the primary pump will empty the sump to below this level
primary_pumpout_reset_height: '0.140' #meters; primary pump counter is reset when water rises above this level (enabling it to be triggered again)
primary_pumpout_duration_limit: '60.0' #seconds; maximum duration for the primary pump to drop the level from 'reset_height' to 'below_height' (otherwise the sump level dropped by some other means e.g. evaporation)
backup_pumpout_below_height: '0.220' #meters; the backup pump will empty the sump to below this level
backup_pumpout_reset_height: '0.300' #meters; backup pump counter is reset when water rises above this level (enabling it to be triggered again)
backup_pumpout_duration_limit: '60.0' #seconds; maximum duration for the backup pump to drop the level from 'reset_height' to 'below_height' (otherwise the sump level dropped by some other means e.g. evaporation)
primary_height_limit: '0.18' #meters; primary pump isn't working when water is above this level (alarm will sound)
backup_height_limit: '0.35' #meters; backup pump isn't working when water is above this level (alarm will sound)
battery_not_charging_voltage: '0.0' #12.1 volts; when 12V battery drops below this voltage it means it is not charging (30 sec average) (alarm will sound)
battery_critical_low_voltage: '0.0' #11.0 volts; 12V battery is getting ready to die at this voltage (30 sec average) (Barracuda pump stops working at 10.8V) (alarm will sound)
batt_volt_charge_max: '14.2' #volts; maximum voltage expected to be seen when battery is connected to the trickle charger
batt_volt_charge_min: '12.5' #volts; minimum voltage expected to be seen when battery is connected to the trickle charger
batt_volt_report: '12.7' #volts; voltage to report if the measured voltage is between the previous two limits
alarm_snooze_duration: '4.0' #hours to snooze the siren if it is going off. Snoozing only applies to 'warning' alarms (primary pump failing or the battery not charging). Snoozing 'critical' alarms is not possible.
# Configuration values for the sensors connected to the ESP32
level_sensor_res_val: '239.9' #ohms; resistance in series with the level sensor
batt_volt_high_res_val: '21820.0' #ohms; resistor between 12V and battery voltage measurement point
batt_volt_low_res_val: '9830.0' #ohms; resistor between battery voltage measurement point and ground
level_sensor_dist_offset: '0.007' #meters; depth water must be before sensor reports nonzero values
level_sensor_max_depth: '1.0' #meters; maximum water depth reading per the sensor spec
level_sensor_min_current: '0.004' #amps; the current sourced by the depth sensor when reading zero
level_sensor_max_current: '0.020' #amps; the current sourced by the depth sensor when reading max depth
esphome:
name: $devicename
comment: ${device_description}
on_boot:
priority: 250.0 # 250=after sensors are set up; when wifi is initializing
then:
# Publish the friendly-formatted snooze time based on the value of the snooze timestamp
- lambda: |-
id(primary_pumpout_counter).publish_state(id(primary_pumpout_counter_var));
id(backup_pumpout_counter).publish_state(id(backup_pumpout_counter_var));
id(primary_pumpout_interval).publish_state(id(primary_pumpout_interval_var));
id(backup_pumpout_interval).publish_state(id(backup_pumpout_interval_var));
time_t snoozeExpTime = id(snooze_expiration_timestamp_var);
time_t boot_time = id(homeassistant_time).now().timestamp;
id(boot_time_var) = boot_time;
// If snooze time is in the past, then snooze has expired
if (snoozeExpTime < boot_time) {
id(snooze_expiration_timestamp_var) = 0;
id(snooze_expiration_text_sensor_friendly).publish_state("N/A");
// Snooze time is later or equal to now, so snooze is active
} else {
// Create a friendly-formatted version of the timestamp
char snoozeExpTimeTextFriendly[23];
strftime(snoozeExpTimeTextFriendly, sizeof(snoozeExpTimeTextFriendly), "%Y-%m-%d %I:%M:%S %p", localtime(&snoozeExpTime));
// Publish the friendly-formatted timestamp
id(snooze_expiration_text_sensor_friendly).publish_state(snoozeExpTimeTextFriendly);
}
esp32:
board: esp32doit-devkit-v1
framework:
type: arduino
# Enable logging
logger:
level: WARN
logs:
ads1115: WARN
# Enable Home Assistant API
api:
ota:
safe_mode: true
password: "some_password"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
power_save_mode: none # default is LIGHT for ESP32
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: $devicename
password: "some_other_password"
captive_portal:
# Enable Web server.
web_server:
port: 80
# Sync time with Home Assistant.
time:
- platform: homeassistant
id: homeassistant_time
on_time:
#Every day at 12:00am, reset sump pump counter
- seconds: 0
minutes: 0
hours: 0
then:
- lambda: |-
id(primary_pumpout_counter_var) = 0;
id(primary_pumpout_counter).publish_state(id(primary_pumpout_counter_var));
id(backup_pumpout_counter_var) = 0;
id(backup_pumpout_counter).publish_state(id(backup_pumpout_counter_var));
float primary_pumpout_interval_now = (id(homeassistant_time).now().timestamp - id(primary_pumpout_begin_var))/60.0;
//If primary pumpout interval has passed the limit
if ((primary_pumpout_interval_now > 720.0) && (id(primary_pumpout_interval_var) > 0.0)) {
id(primary_pumpout_interval_var) = 0.0;
id(primary_pumpout_interval).publish_state(0.0);
}
float backup_pumpout_interval_now = (id(homeassistant_time).now().timestamp - id(backup_pumpout_begin_var))/60.0;
//If backup pumpout interval has passed the limit
if ((backup_pumpout_interval_now > 720.0) && (id(backup_pumpout_interval_var) > 0.0)) {
id(backup_pumpout_interval_var) = 0.0;
id(backup_pumpout_interval).publish_state(0.0);
}
# I2C bus setup for ADC chip
i2c:
- id: bus_a
sda: 32
scl: 33
scan: true
# ADC chip setup
ads1115:
- address: 0x48
continuous_mode: false
# Global Variables
globals:
- id: primary_pumpout_begin_var
type: time_t
initial_value: '0'
restore_value: true
- id: primary_pumpout_counter_var
type: int
restore_value: true
initial_value: '0'
- id: primary_pumpout_last_timestamp_var
type: time_t
initial_value: '0'
restore_value: true
- id: primary_pumpout_interval_var
type: float
initial_value: '0.0'
restore_value: true
- id: backup_pumpout_begin_var
type: time_t
initial_value: '0'
restore_value: true
- id: backup_pumpout_counter_var
type: int
restore_value: true
initial_value: '0'
- id: backup_pumpout_last_timestamp_var
type: time_t
initial_value: '0'
restore_value: true
- id: backup_pumpout_interval_var
type: float
initial_value: '0.0'
restore_value: true
- id: snooze_expiration_timestamp_var
type: time_t
initial_value: '0'
restore_value: true
- id: alarm_snooze_loops_var
type: int
initial_value: '0'
restore_value: false
- id: boot_time_var
type: time_t
restore_value: false
################################################################################
# Binary Sensors
################################################################################
binary_sensor:
# Physical alarm snooze button
- platform: gpio
pin:
number: 23
mode:
input: true
pullup: true
inverted: true
name: "${friendly_name} Alarm Snooze Physical Button"
id: physical_snooze_button
internal: true
on_press:
# Set snooze by pressing the virtual snooze button
- button.press: virtual_snooze_button
# If the physical snooze is held, cancel the snooze after 3 sec & chirp the alarm
- while:
condition:
binary_sensor.is_on: physical_snooze_button
then:
- lambda: |-
if (id(alarm_snooze_loops_var) == 3) {
// button held for 3s, so cancel the snooze
// set to 1.0 so that the alarm_snoozed template sensor will take care of the publishing details and will then set to 0.
id(snooze_expiration_timestamp_var) = 1.0;
// Chirp the alarm so you know the snooze is reset
id(alarm_chirp).execute();
}
id(alarm_snooze_loops_var) += 1;
- delay: 1s
on_release:
- lambda: id(alarm_snooze_loops_var) = 0;
# Hysteresis sensor to trip when sump pump is emptied by primary pump. When tripped, execute the 'primary_pumpout_increment' script
- platform: template
name: "${friendly_name} Primary Pumpout Event Enable"
id: primary_pumpout_event_enable
internal: true
lambda: |-
if (id(level_actual).state < $primary_pumpout_below_height) {
return true;
} else if (id(level_actual).state > $primary_pumpout_reset_height) {
return false;
} else {
return {};
}
filters:
- delayed_on: 5s #debounce after falling below_height
- delayed_off: 10s #debounce after rising above reset_height
on_press:
# when state transisions from false to true, a pumpout event occurred.
lambda: |-
time_t pumpout_duration = id(homeassistant_time).now().timestamp - id(primary_pumpout_begin_var);
ESP_LOGW("primary_hyst", "duration is %li", pumpout_duration);
ESP_LOGW("primary_hyst", "timestamp is %li", id(homeassistant_time).now().timestamp);
ESP_LOGW("primary_hyst", "primary_begin is %li", id(primary_pumpout_begin_var));
//If pumpout was quicker than the duration limit
if (pumpout_duration < $primary_pumpout_duration_limit) {
id(primary_pumpout_increment).execute();
}
# Hysteresis sensor to trip when sump pump is emptied by backup pump. When tripped, execute the 'backup_pumpout_increment' script
- platform: template
name: "${friendly_name} Backup Pumpout Event Enable"
id: backup_pumpout_event_enable
internal: true
lambda: |-
if (id(level_actual).state < $backup_pumpout_below_height) {
return true;
} else if (id(level_actual).state > $backup_pumpout_reset_height) {
return false;
} else {
return {};
}
filters:
- delayed_on: 5s #debounce after falling below_height
- delayed_off: 10s #debounce after rising above reset_height
on_press:
# when state transisions from false to true, a pumpout event occurred.
lambda: |-
time_t pumpout_duration = id(homeassistant_time).now().timestamp - id(backup_pumpout_begin_var);
ESP_LOGD("backup_hyst", "duration is %li", pumpout_duration);
//If pumpout was quicker than the duration limit
if (pumpout_duration < $backup_pumpout_duration_limit) {
id(backup_pumpout_increment).execute();
}
# Sensor to determine if the Alarm Snooze is Active
- platform: template
name: "${friendly_name} Alarm Snoozed"
id: alarm_snoozed
internal: false
lambda: |-
// If snooze time is zero, then snooze is not active
if (id(snooze_expiration_timestamp_var) == 0) {
return false;
// If snooze time is in the past, then snooze has expired
} else if (id(snooze_expiration_timestamp_var) < id(homeassistant_time).now().timestamp) {
id(snooze_expiration_timestamp_var) = 0;
id(snooze_expiration_text_sensor_friendly).publish_state("N/A");
return false;
// Snooze time is later or equal to now, so snooze is active
} else {
return true;
}
# Primary pump not working
- platform: template
name: "${friendly_name} Fault: Primary Pump Not Operating"
id: primary_pump_alarm
internal: false
lambda: |-
if ( id(level_actual).state > $primary_height_limit ) {
return true;
} else {
return false;
}
filters:
- delayed_on: 10s
- delayed_off: 2s
# Backup pump not working
- platform: template
name: "${friendly_name} Fault: Backup Pump Not Operating"
id: backup_pump_alarm
internal: false
lambda: |-
if ( id(level_actual).state > $backup_height_limit ) {
return true;
} else {
return false;
}
filters:
- delayed_on: 10s
- delayed_off: 2s
# Battery not charging
- platform: template
name: "${friendly_name} Fault: Battery Not Charging"
id: not_charging_alarm
internal: false
lambda: |-
if ( id(batt_voltage).state < $battery_not_charging_voltage ) {
return true;
} else {
return false;
}
filters:
#- delayed_on: 10s
#- delayed_off: 10s
# Low Battery
- platform: template
name: "${friendly_name} Fault: Critically Low Battery"
id: low_battery_alarm
internal: false
lambda: |-
if ( id(batt_voltage).state < $battery_critical_low_voltage ) {
return true;
} else {
return false;
}
filters:
#- delayed_on: 10s
#- delayed_off: 10s
# Arbitration to determime if alarm should be sounding
- platform: template
name: "${friendly_name} Alarm Sounding"
id: alarm_sounding
internal: false
lambda: |-
if ( ( !id(alarm_snoozed).state &&
// put stuff on the next line that will only sound the alarm if it's not snoozed
( id(not_charging_alarm).state || id(primary_pump_alarm).state )
) ||
// put stuff on the next line that will sound the alarm regardless of snooze state
( id(low_battery_alarm).state || id(backup_pump_alarm).state )
) {
return true;
} else {
return false;
}
on_press:
lambda: |-
id(sump_pump_alarm).turn_on();
on_release:
lambda: |-
id(sump_pump_alarm).turn_off();
################################################################################
# Buttons.
################################################################################
button:
# Virtual Button to restart the sump_pump_sensor.
- platform: restart
name: "${friendly_name} Restart"
# Virtual Snooze Button (shown on the ESP's webpage and in HA)
- platform: template
name: "${friendly_name} Alarm Snooze Button"
id: virtual_snooze_button
on_press:
lambda: |-
// Calculate snooze expiration time and set as a variable
time_t snoozeExpTime = id(homeassistant_time).now().timestamp+(60.0*60.0*$alarm_snooze_duration);
// Save the snooze expiration timestamp to a global variable
id(snooze_expiration_timestamp_var) = snoozeExpTime;
// Create a friendly-formatted version of the timestamp
char snoozeExpTimeTextFriendly[23];
strftime(snoozeExpTimeTextFriendly, sizeof(snoozeExpTimeTextFriendly), "%Y-%m-%d %I:%M:%S %p", localtime(&snoozeExpTime));
// Publish the friendly-formatted timestamp
id(snooze_expiration_text_sensor_friendly).publish_state(snoozeExpTimeTextFriendly);
################################################################################
# Outputs
################################################################################
output:
# Output for sump pump physical alarm
- platform: gpio
pin: 25 #yellow wire
inverted: true #false means 3.2V when ON
id: sump_pump_alarm
################################################################################
#Scripts
################################################################################
script:
- id: alarm_chirp
mode: single
then:
- if:
condition:
lambda: return id(alarm_sounding).state == true;
then:
- lambda: |-
id(sump_pump_alarm).turn_off();
- delay: 250ms
- lambda: |-
id(sump_pump_alarm).turn_on();
- delay: 50ms
- lambda: |-
id(sump_pump_alarm).turn_off();
- delay: 250ms
- lambda: |-
id(sump_pump_alarm).turn_on();
else:
- lambda: |-
id(sump_pump_alarm).turn_on();
- delay: 50ms
- lambda: |-
id(sump_pump_alarm).turn_off();
- id: primary_pumpout_increment
mode: single
then:
lambda: |-
// Save the timestamp as a local variable
time_t execution_timestamp = id(homeassistant_time).now().timestamp;
// Only exectute if at least 5 sec after bootup
if (execution_timestamp - id(boot_time_var) > 5.0) {
// Save the timestamp as a local variable
time_t execution_timestamp = id(homeassistant_time).now().timestamp;
// Add one to the global integer
id(primary_pumpout_counter_var) += 1;
// Force the sensor to publish a new state
id(primary_pumpout_counter).publish_state(id(primary_pumpout_counter_var));
// Calculate the minutes since last pumpout
id(primary_pumpout_interval_var) = difftime(execution_timestamp,id(primary_pumpout_last_timestamp_var))/60.0;
// If over 12 hours, simply report zero since I don't care at that point and don't want my nice plots to have outrageous y-axis scaling
if (id(primary_pumpout_interval_var) > 720.0) {
id(primary_pumpout_interval_var) = 0.0;
}
// Force the interval sensor to publish a new state
id(primary_pumpout_interval).publish_state(id(primary_pumpout_interval_var));
// Store the pumpout timestamp to a global variable
id(primary_pumpout_last_timestamp_var) = execution_timestamp;
}
- id: backup_pumpout_increment
mode: single
then:
lambda: |-
// Save the timestamp as a local variable
time_t execution_timestamp = id(homeassistant_time).now().timestamp;
// Only exectute if at least 5 sec after bootup
if (execution_timestamp - id(boot_time_var) > 5.0) {
// Add one to the global integer
id(backup_pumpout_counter_var) += 1;
// Force the sensor to publish a new state
id(backup_pumpout_counter).publish_state(id(backup_pumpout_counter_var));
// Calculate the minutes since last pumpout
id(backup_pumpout_interval_var) = difftime(execution_timestamp,id(backup_pumpout_last_timestamp_var))/60.0;
// If over 12 hours, simply report zero since I don't care at that point and don't want my nice plots to have outrageous y-axis scaling
if (id(backup_pumpout_interval_var) > 720.0) {
id(backup_pumpout_interval_var) = 0.0;
}
// Force the interval sensor to publish a new state
id(backup_pumpout_interval).publish_state(id(backup_pumpout_interval_var));
// Store the pumpout timestamp to a global variable
id(backup_pumpout_last_timestamp_var) = execution_timestamp;
}
################################################################################
#Sensors
################################################################################
sensor:
# Uptime sensor.
- platform: uptime
name: "${friendly_name} Uptime"
id: uptime_sensor
update_interval: 5min
# WiFi Signal sensor.
- platform: wifi_signal
name: "${friendly_name} WiFi Signal"
update_interval: 5min
# Primary Pumpout Counter to count number of times it has turned on
- platform: template
name: "${friendly_name} Primary Pumpout Daily Counter"
id: primary_pumpout_counter
update_interval: never
accuracy_decimals: 0
state_class: total_increasing
lambda: return id(primary_pumpout_counter_var);
# Primary Pumpout Interval
- platform: template
name: "${friendly_name} Primary Pumpout Interval"
id: primary_pumpout_interval
unit_of_measurement: min
update_interval: never
accuracy_decimals: 1
lambda: return id(primary_pumpout_interval_var);
# Backup Pumpout Counter to count number of times it has turned on
- platform: template
name: "${friendly_name} Backup Pumpout Daily Counter"
id: backup_pumpout_counter
update_interval: never
accuracy_decimals: 0
state_class: total_increasing
lambda: return id(backup_pumpout_counter_var);
# Backup Pumpout Interval
- platform: template
name: "${friendly_name} Backup Pumpout Interval"
id: backup_pumpout_interval
unit_of_measurement: min
update_interval: never
accuracy_decimals: 1
lambda: return id(backup_pumpout_interval_var);
# ADC: 5V Supply Voltage Measurement
- platform: ads1115
multiplexer: 'A0_GND'
gain: 6.144
name: "${friendly_name} 5V Supply Voltage"
update_interval: 1s
accuracy_decimals: 1
internal: false
state_class: measurement
device_class: voltage
unit_of_measurement: V
filters:
- sliding_window_moving_average:
window_size: 10
send_every: 10
# ADC: 12V Battery Voltage Measurement
- platform: ads1115
multiplexer: 'A2_A3'
gain: 6.144
name: "${friendly_name} 12V Battery Voltage"
id: batt_voltage
update_interval: 5s
accuracy_decimals: 3
internal: false
state_class: measurement
device_class: voltage
unit_of_measurement: V
filters:
- filter_out: nan
- sliding_window_moving_average:
window_size: 6
send_every: 6 # 30s
- lambda: |-
// Calculate the battery voltage based off the resistor divider circuit
float batt_voltage = x*($batt_volt_high_res_val/$batt_volt_low_res_val+1.0);
// Filter out the intermittent voltage spikes due to the battery trickle charger
if ( batt_voltage < $batt_volt_charge_max && batt_voltage > $batt_volt_charge_min ) {
batt_voltage = $batt_volt_report;
}
return batt_voltage;
# ADC: Sump level sensor voltage measurement
- platform: ads1115
multiplexer: 'A1_A3'
gain: 6.144
name: "${friendly_name} Voltage"
id: level_voltage
update_interval: 100ms
accuracy_decimals: 4
internal: true
state_class: measurement
device_class: voltage
unit_of_measurement: V
filters:
- filter_out: nan
- sliding_window_moving_average:
window_size: 10
send_every: 10 # every 1s
# Calculation of sensor current based on measured voltage
- platform: template
name: "${friendly_name} Level Sensor Current"
id: level_current
update_interval: 1s
accuracy_decimals: 4
internal: true
state_class: measurement
device_class: current
unit_of_measurement: A
lambda: return (id(level_voltage).state / $level_sensor_res_val);
# Calculation of sump water level based on sensor current
- platform: template
name: "${friendly_name} Level"
id: level_actual
update_interval: 1s
accuracy_decimals: 3
internal: false
state_class: measurement
device_class: ""
unit_of_measurement: m
lambda: |-
// Calculate raw level from current
float raw_level = ((id(level_current).state-$level_sensor_min_current)/($level_sensor_max_current-$level_sensor_min_current)*$level_sensor_max_depth + $level_sensor_dist_offset);
if (raw_level < $level_sensor_dist_offset) {
return 0.0;
} else if (raw_level > $level_sensor_max_depth+$level_sensor_dist_offset) {
return $level_sensor_max_depth + $level_sensor_dist_offset;
} else {
return (raw_level);
}
filters:
- sliding_window_moving_average:
window_size: 2
send_every: 2 #every 2 seconds
- delta: 0.002
- lambda: |-
if (x > 0) return x;
else return 0;
on_value_range:
- below: $primary_pumpout_reset_height
then:
- lambda: |-
//Store the timestamp for the start of pumpout in a global variable
id(primary_pumpout_begin_var) = id(homeassistant_time).now().timestamp;
ESP_LOGD("level", "primary_begin is %li", id(primary_pumpout_begin_var));
- below: $backup_pumpout_reset_height
then:
- lambda: |-
//Store the timestamp for the start of pumpout in a global variable
id(backup_pumpout_begin_var) = id(homeassistant_time).now().timestamp;
ESP_LOGD("level", "backup_begin is %li", id(backup_pumpout_begin_var));
################################################################################
# Text sensors
################################################################################
text_sensor:
# Expose ESPHome version as sensor.
- platform: version
name: "${friendly_name} ESPHome Version"
# Expose WiFi information as sensors.
- platform: wifi_info
ip_address:
name: "${friendly_name} IP"
ssid:
name: "${friendly_name} SSID"
bssid:
name: "${friendly_name} BSSID"
# Snooze Expiration Friendly Time
- platform: template
name: "${friendly_name} Snooze Expiration"
id: snooze_expiration_text_sensor_friendly
update_interval: never
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment