Iāll maintain my master config here. May not maintain it well though.
Edit: Fresh paste on 2024-01-13
#Todo:
#Determine night time battery loss.
#More testing insitu
#HA alerts for low water, low battery, data updates not working.
#Check pump runs.
#Monitor review wet/dry/battery thresholds.
#Make debugging sensors internal.
substitutions:
sleep_time: 60min
auto_wake_time: 20s
pump_run_time: 10s
internal_mode: "true"
long_deep_sleep_duration: 24h
bh1750_i2c_scl_blue: GPIO27
bh1750_i2c_sda_green: GPIO25
vl53l0x_i2c_scl_blue: GPIO23
vl53l0x_i2c_sda_green: GPIO05
power_peripheral_pin: GPIO26
pump_switch_pin: GPIO18
batt_voltage_pin: GPIO33
soil_moisture_pin: GPIO34
# G # RST #1 tx # G
# NC # VP #3 rx #@ 27 bh1750 blue scl ${bh1750_i2c_scl_blue}
# VN #@ 26 ${power_peripheral_pin} #22 #@ 25 bh1750 green sda ${bh1750_i2c_sda_green}
# 35 #@ 18 ${pump_switch_pin} #21 #@ 32
#@ 33 ${batt_voltage_pin} #@ 19 #27 # TDI
# 34 ${soil_moisture_pin} #@ 23 ${vl53l0x_i2c_scl_blue} #25 #@ 4
# TMS #@ 5 ${vl53l0x_i2c_sda_green} #G # 0
# NC # 3v3 #5v # 2
# SD2 # TCK #TDO # SD1
# CMD # SD3 #SDO # CLK
esp32:
board: ttgo-t7-v14-mini32 #pinout: https://ae01.alicdn.com/kf/Ha204b20d14d243faa0c1a8760de1b187r.jpg
esphome:
name: "esplanty"
friendly_name: ESPlanty
comment: Balcony Solar Irrigation
on_boot:
# #Try this if sensors don't boot. https://community.home-assistant.io/t/add-sensor-delay-upon-power-on/158567/6?u=mahko_mahko
# https://community.home-assistant.io/t/i2c-bus-scan-after-deep-sleep-recovery/524925/7?u=mahko_mahko
- priority: 900
then:
- lambda: |-
Wire.begin();
delay(500);
- priority: -100
then:
#Reset sensor update counters. These are for debugging.
- lambda: id(count_irrigation_lux).publish_state(0);
- lambda: id(count_batt_voltage).publish_state(0);
- lambda: id(count_irrigation_tof).publish_state(0);
- delay: 1s
#Auto sensor updates are turned off and manually requested on boot. These are multi-sampled and then aggregated.
#The ESP then goes back to sleep when they're done (unless told to stay awake).
#Request sensor updates
- logger.log: "....Starting sensor updates"
- repeat:
count: 5 #Update cycles
then:
- component.update: batt_voltage #Battery level
- delay: 100ms
- component.update: soil_moisture_voltage #Moisture level
- delay: 100ms
- component.update: tof #Water tank level
- delay: 100ms
- component.update: irrigation_lux #Light level
- delay: 100ms
#second pass for ADC based sensors which benefit from more samples.
- component.update: batt_voltage
- delay: 200ms
- component.update: soil_moisture_voltage
- delay: 200ms
- component.update: tof #Water tank level
- delay: 100ms
on_shutdown: #Includes deep sleep
priority: -100
then:
- script.wait: run_pump_for_pump_run_time #Wait until pump run is done.
- if:
condition:
- binary_sensor.is_on: all_updates_recieved
then:
- logger.log: "Data updates ok..."
else:
- logger.log: "A sensor might be down...missing updates"
- binary_sensor.template.publish:
id: data_update_problem
state: ON
- delay: 1s
- switch.turn_off: pump #Probably not required....
#Turn off 5v peripheral power. It will retore as on when it wakes.
- logger.log: "Turning off peripheral power..."
- switch.turn_off: power_peripherals
#Turn off the "Fresh data recieved sensors"
- binary_sensor.template.publish:
id: water_tank_level_recieved
state: OFF
- binary_sensor.template.publish:
id: irrigation_lux_recieved
state: OFF
- binary_sensor.template.publish:
id: batt_level_recieved
state: OFF
- binary_sensor.template.publish:
id: solar_plant_moisture_level_recieved
state: OFF
- binary_sensor.template.publish:
id: all_updates_recieved
state: OFF
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
fast_connect: true
manual_ip:
static_ip: 192.168.1.123
gateway: 192.168.1.1
subnet: 255.255.255.0
api:
ota:
logger:
level: VERBOSE
baud_rate: 0
i2c:
- id: bh1750_i2c_bus
sda: GPIO25 #data > green wire
scl: GPIO27 #clock > blue wire
scan: true
- id: vl53l0x_i2c_bus
sda: ${vl53l0x_i2c_sda_green} #data > green wire
scl: ${vl53l0x_i2c_scl_blue} #clock > blue wire
scan: true
deep_sleep:
#Auto deep sleep shouldn't actually get used if data updates are happening properly (should sleep sooner).
#But if something is wrong with data updates this will kick-in to protect battery discharge.
id: deep_sleep_1
run_duration: ${auto_wake_time}
sleep_duration: ${sleep_time}
time:
- platform: homeassistant
id: esptime
on_time:
#Deep sleep at 8pm and wake up at 6am.
- hours: 20
then:
#Publish battery level to the end of day sensor since it will be our last update.
- lambda: return id(end_of_day_battery_percent).publish_state(id(batt_level).state);
#Reset culmulative light sensor reset
- sensor.integration.reset: irrigation_cul_lux_hours
- delay: 1s
#Rest up
- deep_sleep.enter:
id: deep_sleep_1
until: "06:00:00"
time_id: esptime
text_sensor:
# Reports the ESPHome Version with compile date
- platform: version
name: ESPHome Version
select:
#Watering Mode.
#Watering regime allows plant to become quite dry and then get a good watering,
#as opposed to more frequent bang bang watering cycles.
#TODO: More testing.
- platform: template
name: "Watering Mode"
id: watering_mode
optimistic: true
restore_value: true
options:
- Wetting Mode
- Drying Mode
initial_option: Drying Mode
number:
#Upper mositure target (once dry)
- platform: template
name: Moisture Upper Target
id: moisture_upper_target
icon: mdi:arrow-collapse-up
optimistic: true
unit_of_measurement: '%'
min_value: 40.0
max_value: 90.0
step: 5.0
restore_value: true
initial_value: 80.0
#Lower Water threshold (triggers watering regime). Need to establish good values.
- platform: template
name: Moisture Lower Threshold
id: moisture_lower_threshold
icon: mdi:arrow-collapse-down
optimistic: true
unit_of_measurement: '%'
min_value: 5.0
max_value: 60.0
step: 5.0
restore_value: true
initial_value: 50.0
#Battery Saving Mode Threshold. Trigger longer sleep cycles if below this and hope for sunlight.
- platform: template
name: Battery Saving Threshold
id: battery_saving_mode_threshold
icon: mdi:battery-alert
optimistic: true
unit_of_measurement: '%'
min_value: 10.0
max_value: 80.0
step: 5.0
restore_value: true
initial_value: 20.0
#Low water tank threshold. Don't water if below this.
- platform: template
name: Low tank water threshold
id: low_tank_water_threshold
icon: mdi:water-remove-outline
optimistic: true
unit_of_measurement: '%'
min_value: 0.0
max_value: 20.0
step: 5.0
restore_value: true
initial_value: 10.0
button:
#Deep sleep if "allowed".
- platform: template
name: Sleep If Allowed
internal: ${internal_mode}
id: sleep_if_allowed
icon: "mdi:sleep"
on_press:
then:
- if:
condition:
- binary_sensor.is_on: stay_awake_sensor
then:
- logger.log: "Sleep requested but STAY AWAKE mode is on. Skipping sleep."
- deep_sleep.prevent: deep_sleep_1
else:
- logger.log: "Sleep requested and ALLOWED. Going to sleep..."
- deep_sleep.enter:
id: deep_sleep_1
sleep_duration: ${sleep_time}
#Force Deep Sleep
- platform: template
name: Force Sleep
id: force_deep_sleep
icon: "mdi:bell-sleep"
internal: ${internal_mode}
on_press:
then:
- logger.log: "FORCE SLEEP requested...Going to sleep"
- deep_sleep.enter:
id: deep_sleep_1
sleep_duration: ${sleep_time}
#Long Sleep (part of battery saving mode)
- platform: template
name: Long Sleep
id: long_deep_sleep
internal: ${internal_mode}
icon: "mdi:bell-sleep"
on_press:
then:
- logger.log: "LONG SLEEP requested...Going to sleep"
#Publish battery level to the end of day sensor since it will be our last update.
- lambda: return id(end_of_day_battery_percent).publish_state(id(batt_level).state);
- deep_sleep.enter:
id: deep_sleep_1
sleep_duration: ${long_deep_sleep_duration}
#Reset culmulative light sesnor reset
- sensor.integration.reset: irrigation_cul_lux_hours
binary_sensor:
#Low tank water alert
- platform: template
id: tank_water_level_is_ok
name: Tank Level Ok
icon: mdi:water-remove-outline
device_class: moisture
lambda: |-
if (id(water_tank_level).state > id(low_tank_water_threshold).state)
{return true;}
else if (id(water_tank_level).state <= id(low_tank_water_threshold).state)
{return false;}
else
return {};
#Data updates alert. I've had the lux/tof playing up. Seems to be when battery < ~ 40%
- platform: template
id: data_update_problem
name: Sensor Problem
icon: mdi:database-alert-outline
device_class: problem
entity_category: diagnostic
#Low Battery alert
- platform: template
id: battery_level_is_low
name: Battery Level Is Low
icon: mdi:battery-alert
lambda: |-
if (id(batt_level).state < id(battery_saving_mode_threshold).state)
{return true;}
else if (id(batt_level).state >= id(battery_saving_mode_threshold).state)
{return false;}
else
return {};
on_press:
then:
- button.press: long_deep_sleep #Long deep sleep if battery low.
#Pump Sensor.
- platform: template
id: pump_is_on
name: Pump is On
icon: mdi:water-pump
device_class: running
lambda: |-
return id(pump).state;
##################################################
#Data Update Sensors.
#These were useful during dev. Might remove in prod.
#All updated via other sensors.
##################################################
#Moisture
- platform: template
name: Moisture updated
id: solar_plant_moisture_level_recieved
internal: ${internal_mode}
#Battery Level
- platform: template
name: Battery level updated
id: batt_level_recieved
internal: ${internal_mode}
#Light
- platform: template
name: Lux updated
id: irrigation_lux_recieved
internal: ${internal_mode}
#Water level
- platform: template
name: water level updated
id: water_tank_level_recieved
internal: ${internal_mode}
#All
- platform: template
name: Data updates done
id: all_updates_recieved
icon: mdi:database-refresh-outline
entity_category: diagnostic
lambda: |-
return
id(solar_plant_moisture_level_recieved).state &&
id(batt_level_recieved).state &&
id(irrigation_lux_recieved).state &&
id(water_tank_level_recieved).state
;
on_press:
then:
- binary_sensor.template.publish:
id: data_update_problem
state: OFF
#Log that updates are all done
- logger.log:
format: "All sensors updated after %.1f secs of uptime. Checking if watering is required"
args: [ 'id(uptime_sec).state']
- delay: 100ms
#Water if required and allowed then go to sleep. Note watering will only happen if total system is healthy (by design).
- if:
condition:
and:
#Check if our watering regime wants a water dose
- lambda: return (id(watering_mode).state == "Wetting Mode");
- binary_sensor.is_off: battery_level_is_low # Not in battery saving mode.
- binary_sensor.is_on: tank_water_level_is_ok #Tank Water is ok
- binary_sensor.is_on: auto_water_sensor #Auto water is enabled
then:
- logger.log: "Watering required and allowed"
- switch.turn_on: pump
- delay: ${pump_run_time}
- delay: 1s
- button.press: sleep_if_allowed
else:
- logger.log: "Watering not permitted!!.." #todo log reason?
- button.press: sleep_if_allowed
##################################################
#Deep sleep control
##################################################
#A lot of my logic is remashes of:
#https://www.wirewd.com/make/blog/esphome_sleep_modes
#https://tatham.blog/2021/02/06/esphome-batteries-deep-sleep-and-over-the-air-updates/
#Import HA Deep sleep control
- platform: homeassistant
name: "Stay Awake"
internal: false #I want to know that everyone is talking to each other;)
id: "stay_awake_sensor"
entity_id: input_boolean.keep_esps_awake_switch_ha
icon: "mdi:sleep-off"
entity_category: diagnostic
on_press:
then:
- logger.log: "STAY AWAKE requested from HA. Preventing deep sleep"
- deep_sleep.prevent: deep_sleep_1
on_release:
then:
- logger.log: "STAY AWAKE TURNED OFF. Going to sleep..."
- deep_sleep.enter:
id: deep_sleep_1
sleep_duration: ${sleep_time}
#Import Auto Water control
- platform: homeassistant
name: Auto Water
internal: false
id: "auto_water_sensor"
entity_id: input_boolean.auto_water_planty
icon: "mdi:auto-fix"
script:
##################################################
#Pump auto off timer
##################################################
- id: run_pump_for_pump_run_time
then:
- logger.log: "Running pump for ${pump_run_time}"
- delay: ${pump_run_time}
- switch.turn_off: pump
switch:
##################################################
#Pump
##################################################
- platform: gpio
pin: ${pump_switch_pin}
id: pump
name: Pump
icon: mdi:water-pump
internal: false
restore_mode: ALWAYS_OFF
on_turn_on: #Auto off timer. Flood protection.
- script.stop: run_pump_for_pump_run_time # Stop any existing timers.
- script.execute: run_pump_for_pump_run_time # Start new timer
##################################################
#Control peripheral power (on solar power manager)
##################################################
- platform: gpio
id: power_peripherals
name: Power Peripherals
pin: ${power_peripheral_pin}
internal: true
restore_mode: ALWAYS_ON #Power on when waking from sleep.
globals:
#For daily battery level change (below). i.e net energy production or loss.
#Globals survive deep sleep
- id: previous_value
type: float
restore_value: yes
initial_value: '0.0'
sensor:
#Track end of day battery
- platform: template
name: End of Day Battery
id: end_of_day_battery_percent
update_interval: never
icon: "mdi:solar-power"
unit_of_measurement: '%'
accuracy_decimals: 3
on_value:
then:
#Calculate change in end of day battery and publish to sensor
- lambda: id(change_in_end_of_day_battery_percent).publish_state(x - id(previous_value));
#Set previous value as current value.
- lambda: |-
id(previous_value) = x;
#Track daily changes in battery (i.e net solar production)
- platform: template
name: Change in End of Day Battery
id: change_in_end_of_day_battery_percent
icon: mdi:trending-up
internal: false
unit_of_measurement: '%'
accuracy_decimals: 3
#Uptime sensor
- platform: uptime
id: uptime_sec
name: Uptime Sensor
update_interval: 2s
accuracy_decimals: 0
unit_of_measurement: s
##################################################
#For counting data updates recieved for each wake cycle.
#Manually updated via publishing from other sensors.
##################################################
#Track Lux updates
- platform: template
name: "Count Lux Updates"
id: count_irrigation_lux
icon: "mdi:counter"
unit_of_measurement: count
entity_category: diagnostic
#Track ToF updates
- platform: template
name: "Count ToF Updates"
id: count_irrigation_tof
unit_of_measurement: count
entity_category: diagnostic
#Track battery updates
- platform: template
name: "Count Batt V Updates"
id: count_batt_voltage
unit_of_measurement: count
icon: "mdi:counter"
entity_category: diagnostic
##########################################################################################
# Time of Flight sensor - i2c
##########################################################################################
#Powered via 5v
- platform: vl53l0x
id: tof
i2c_id: vl53l0x_i2c_bus
# setup_priority: 300
name: ToF
internal: false
address: 0x29
timeout: 300ms
update_interval: never
entity_category: diagnostic
#never
# enable_pin: GPIO17 #Did not work: https://github.com/esphome/issues/issues/3644
accuracy_decimals: 1
unit_of_measurement: 'cm'
filters:
- multiply: 100 #Convert to cm
- median: #Moving median to smooth noise. Sample 5 points then push.
window_size: 5
send_every: 5
send_first_at: 5
on_raw_value:
then: #Push sensor update counter.
- lambda: id(count_irrigation_tof).publish_state(id(count_irrigation_tof).state +1);
#Convert the ToF distance to a water tank level (% full)
- platform: copy
source_id: tof
id: water_tank_level
internal: false
# icon: "mdi:battery"
name: Water Tank Level
unit_of_measurement: '%'
accuracy_decimals: 1
entity_category: ''
filters:
# Map from distance to % full. To calibrate.
- calibrate_linear:
- 3 -> 100
- 19.5 -> 0
##Overide values less than 0% and more than 100%. Round to 0.5%.
- lambda: |
if (x > 100) return 100;
else if (x < 0) return 0;
else return ceil(x / 5) * 5;
on_value:
then:
- binary_sensor.template.publish:
id: water_tank_level_recieved
state: ON
##########################################################################################
# bh1750 Lux/light sensor
##########################################################################################
- platform: bh1750
id: irrigation_lux
i2c_id: bh1750_i2c_bus
# - id: bh1750_i2c_bus
# sda: GPIO25 #data > green wire
# scl: GPIO27 #clock > blue wire
# scan: true
# - id: vl53l0x_i2c_bus
name: Lux
address: 0x23
update_interval: never
filters:
- median: #Use moving median to smooth noise. Sample 5 points then send.
window_size: 5
send_every: 5
send_first_at: 5
on_value:
then:
- binary_sensor.template.publish:
id: irrigation_lux_recieved
state: ON
on_raw_value:
then:
#Sensor update counter.
- lambda: id(count_irrigation_lux).publish_state(id(count_irrigation_lux).state +1);
#Measure of total light per day. Poor persons DLI https://en.wikipedia.org/wiki/Daily_light_integral
- platform: integration
name: "Light per day"
id: irrigation_cul_lux_hours
sensor: irrigation_lux
time_unit: s
unit_of_measurement: lux*s
restore: true
accuracy_decimals: 0
#Notes:
#Voltage divider: Used 2 x 300K Ohm resistors
- platform: adc
id: batt_voltage
name: Battery Voltage
internal: true
pin: ${batt_voltage_pin} #ADC1
update_interval: never
accuracy_decimals: 2
attenuation: auto
filters:
# #Scale it back up from voltage divided value 2 x 300K > 2.1. 4.2/2.1 = 2.
- multiply: 2
on_raw_value:
then:
#Sensor update counter.
- lambda: id(count_batt_voltage).publish_state(id(count_batt_voltage).state +1);
#Intermediate sensor. Might consolidate them later.
- platform: copy
source_id: batt_voltage
id: batt_voltage_filtered
icon: "mdi:battery"
internal: false
name: Battery Voltage
unit_of_measurement: V
accuracy_decimals: 2
filters:
- median: #Use moving median to smooth noise.
window_size: 10
send_every: 10
send_first_at: 10
#Convert the Voltage to a battery level (%)
- platform: copy
source_id: batt_voltage_filtered
id: batt_level
internal: false
icon: "mdi:battery"
name: Battery Percent
unit_of_measurement: '%'
accuracy_decimals: 0
filters:
# Map from voltage to Battery level
- calibrate_linear:
- 3.1 -> 0 #Set 3.0 to 0% even though it can go lower (2.4V), for life extention. There's not much capacity below this anyway.
- 4.1 -> 100 #Set 4.05 to 100% even though it can go higher (~4.2V), for life extention.
#Overide values less than 0% and more than 100%
- lambda: |
if (x < 0) return 0;
else if (x > 100) return 100;
else return ceil(x / 5) * 5;
on_value:
then:
#Publish that data is recieved
- binary_sensor.template.publish:
id: batt_level_recieved
state: ON
#Capacitive soil moisture sensor: https://www.aliexpress.com/item/32832538686.html?spm=a2g0o.order_list.0.0.55771802WgNqEA
#Voltage of the Capacitive soil moisture sensor
- platform: adc
name: Soil Moisture Volts
id: soil_moisture_voltage
pin: ${soil_moisture_pin}
internal: false
accuracy_decimals: 1
update_interval: never
# never
attenuation: auto
filters:
- median: #Use moving median to deal with noise. Sample 10, push 1.
window_size: 10
send_every: 10
send_first_at: 10
#Convert the Voltage to a moisture level (%)
- platform: copy
source_id: soil_moisture_voltage
id: solar_plant_moisture_level
name: Soil Moisture
internal: false
icon: "mdi:water-percent"
unit_of_measurement: '%'
accuracy_decimals: 1
filters:
#max and min values taken from testing with glass of water. Prob need to do in-situ tests.
- calibrate_linear:
- 1.66 -> 100.0
- 2.90 -> 0.0
#Handle/cap boundaries
- lambda: |
if (x < 0) return 0;
else if (x > 100) return 100;
else return (x);
on_value:
then:
#Data update recieved.
- binary_sensor.template.publish:
id: solar_plant_moisture_level_recieved
state: ON
#Help manage the watering mode.
#TODO: Monitor/test
- lambda: |-
if (x < id(moisture_lower_threshold).state)
{ return id(watering_mode).publish_state("Wetting Mode");}
else if (x > id(moisture_upper_target).state)
{ return id(watering_mode).publish_state("Drying Mode");}
on_value_range:
#Help manage the watering mode.
- below: !lambda return id(moisture_lower_threshold).state;
then:
- lambda: return id(watering_mode).publish_state("Wetting Mode");
- above: !lambda return id(moisture_upper_target).state;
then:
- lambda: return id(watering_mode).publish_state("Drying Mode");