Skip to content

Instantly share code, notes, and snippets.

@pavax
Last active May 9, 2025 23:42
Show Gist options
  • Save pavax/54a2443fc1fc879cdb336d503ffd07d2 to your computer and use it in GitHub Desktop.
Save pavax/54a2443fc1fc879cdb336d503ffd07d2 to your computer and use it in GitHub Desktop.
smart-plant-watering-mini-1.yaml
substitutions:
name: "smart-plant-watering-mini-1"
friendly_name: "Smart Plant Watering Mini 1"
initial_sleep_duration_minutes: "120" # For how long (minutes) should the MCU sleep
initial_watering_wait_time_minutes: "240" # Time (minutes) to wait before watering the plant again
initial_min_moisture_level: "15" # Threshold that defines when to water a plant
initial_watering_time_seconds: "10" # How long should the watering process run (seconds)
initial_max_running_time_minutes: "5" # Max time (minutes) for the MCU to stay awake
battery_max: "8.2" # Battery voltage indicating 100%
battery_min: "6.6" # Battery voltage indicating 0%
uptime_update_interval: 60s
pump_min_power: "0.2"
pump_prestarting_time_ms: "1500" # Time for the pump to start with 100%
esp32:
board: lolin_c3_mini
framework:
type: arduino
packages:
device_base: !include common/device_base.yaml
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
fast_connect: true
output_power: 8.5dB
esphome:
on_boot:
- priority: 600
then:
- script.execute: measure_battery
- light.turn_on:
id: status_led
brightness: 30%
transition_length: 0s
blue: 1
red: 1
green: 1
- delay: 2s
- script.execute: led_status_light
- priority: -100
then:
- lambda: "id(has_booted) = true;"
on_shutdown:
- priority: -100
then:
- light.turn_off:
id: status_led
transition_length: 0s
- lambda: |-
if (id(pump_switch_1).state) {
id(pump_switch_1).turn_off();
}
safe_mode:
disabled: true
logger:
level: DEBUG
globals:
- id: plant1_status
type: std::string
restore_value: no
initial_value: '"unknown"'
- id: elapsed_watering_time
type: int
restore_value: no
initial_value: "0"
- id: plant1_last_watered
type: unsigned long
restore_value: yes
initial_value: "0"
- id: has_booted
type: bool
restore_value: no
initial_value: "false"
- id: all_plants_scanned
type: bool
restore_value: no
initial_value: "false"
api:
on_client_connected:
- script.execute: led_status_light
- esp32_ble_tracker.start_scan:
continuous: true
on_client_disconnected:
- esp32_ble_tracker.stop_scan:
mqtt:
broker: !secret mqtt_broker
username: !secret mqtt_username
password: !secret mqtt_password
discovery: false
topic_prefix: null
on_message:
- topic: ${name}/schedule-watering
payload: 'ON'
qos: 1
then:
- wait_until:
- lambda: "return id(has_booted) = true;"
- logger.log:
level: INFO
format: "Received Schedule Watering MQTT Command"
- switch.turn_on: pump_switch_1
- mqtt.publish:
topic: ${name}/schedule-watering
payload: "ACK"
retain: true
- topic: ${name}/deep-sleep
qos: 1
payload: "ON"
then:
- switch.turn_off: prevent_deep_sleep_switch
- mqtt.publish:
topic: ${name}/deep-sleep
payload: "ACK"
retain: true
- topic: ${name}/deep-sleep
qos: 1
payload: "OFF"
then:
- switch.turn_on: prevent_deep_sleep_switch
- mqtt.publish:
topic: ${name}/deep-sleep
payload: "ACK"
retain: true
deep_sleep:
id: sleep_control
esp32_ble_tracker:
id: ble_tracker
scan_parameters:
# When using this component on single core chips such as the ESP32-C3 both WiFi and ble_tracker must run on the same core,
# and this has been known to cause issues when connecting to WiFi. A work-around for this is to enable the tracker only after
# the native API is connected.
continuous: false
active: false
interval: 300ms
window: 300ms
interval:
- interval: 2sec
id: "stop_scanning_interval"
startup_delay: 5sec
then:
- if:
condition:
- and:
- lambda: "return !isnan(id(plant1_sensor_moisture).state);"
then:
- lambda: |-
static bool executed = false;
if (!executed) {
ESP_LOGI("main", "Stop BLE Scanning since all plants moisture levels have been fetched.");
id(all_plants_scanned) = true;
id(ble_tracker).stop_scan();
id(led_status_light).execute();
executed = true;
}
- interval: 5sec
id: "max_running_time_reached_interval"
startup_delay: 60sec
then:
- if:
condition:
- and:
- lambda: "return id(uptime_seconds).state >= (id(max_running_time_minutes).state * 60);"
- not:
- script.is_running: watering_process
then:
- logger.log:
level: WARN
format: "Max running time reached: %0.0fs of max. %0.0fs"
args:
- id(uptime_seconds).state
- id(max_running_time_minutes).state * 60
- script.execute: prepare_shutdown
- interval: 5sec
id: "duty_done_interval"
startup_delay: 10sec
then:
- if:
condition:
and:
- lambda: 'return id(plant1_status) == std::string("ok") || id(plant1_status) == std::string("watered") || id(plant1_status) == std::string("recently_watered");'
- lambda: "return id(all_plants_scanned);"
then:
- if:
condition:
switch.is_off: prevent_deep_sleep_switch
then:
- logger.log:
level: INFO
format: "Everything is done - Prepare Deep Sleep!"
- script.execute: prepare_shutdown
binary_sensor:
- platform: gpio
id: water_tank_empty
name: "Water Tank"
icon: "mdi:bucket"
# ON means problem detected (tank is empty) whereas OFF means no problem (tank is full).
device_class: problem
pin:
number: GPIO4
inverted: false
mode:
input: true
pullup: true
filters:
- delayed_on: 500ms
- delayed_off: 500ms
on_release:
then:
- script.execute: led_status_light
- logger.log:
level: DEBUG
format: "Wasserstand OK"
on_press:
then:
- script.execute: led_status_light
- logger.log:
level: DEBUG
format: "Wasserstand niedrig"
- lambda: |-
if (id(pump_switch_1).state) {
id(pump_switch_1).turn_off();
}
switch:
- platform: template
id: prevent_deep_sleep_switch
name: "Prevent Deep Sleep"
restore_mode: RESTORE_DEFAULT_OFF
optimistic: true
entity_category: config
icon: "mdi:sleep-off"
on_turn_on:
then:
- logger.log:
level: DEBUG
format: "Preventing Deep Sleep"
- deep_sleep.prevent: sleep_control
on_turn_off:
then:
- logger.log:
level: DEBUG
format: "Allowing Deep Sleep"
- deep_sleep.allow: sleep_control
- platform: template
id: pump_switch_1
name: "Water Pump"
lambda: "return id(pump_control).current_values.is_on();"
icon: "mdi:water-pump"
turn_on_action:
- if:
condition:
and:
- binary_sensor.is_off: water_tank_empty
- lambda: "return id(has_booted);"
then:
- script.execute:
id: watering_process
else:
- logger.log:
level: WARN
format: "Blocking watering_process activation!"
turn_off_action:
- script.stop: watering_process
- light.turn_off:
id: pump_control
transition_length: 0s
- if:
condition:
- lambda: "return id(has_booted);"
then:
- script.execute:
id: mark_plants_as_watered
output:
- platform: ledc
id: pwm_pump
pin: GPIO5
frequency: 100 Hz
inverted: false
zero_means_zero: true
min_power: ${pump_min_power}
max_power: 1.0
sensor:
- platform: adc
id: battery_voltage
name: "Battery Voltage"
pin: GPIO3
icon: "mdi:flash"
attenuation: 12db
accuracy_decimals: 2
update_interval: never
filters:
- median:
window_size: 5
send_every: 5
send_first_at: 1
- multiply: 3.301369863013699
- round: 2
- platform: copy
name: "Battery Percentage"
source_id: battery_voltage
unit_of_measurement: "%"
icon: "mdi:battery"
accuracy_decimals: 0
filters:
- lambda: |-
const float max_voltage = ${battery_max};
const float min_voltage = ${battery_min};
float battery_percentage = (x - min_voltage) / (max_voltage - min_voltage) * 100.0;
return battery_percentage > 100.0 ? 100.0 : (battery_percentage < 0.0 ? 0.0 : battery_percentage);
- round: 0
- platform: xiaomi_hhccjcy01
id: plant1_sensor
mac_address: "C4:7C:8D:65:FA:0C"
moisture:
id: plant1_sensor_moisture
name: "Plant 1 Soil Moisture"
on_value:
then:
- logger.log:
level: DEBUG
format: "Plant: Moisture received: %.1f"
args: [x]
- if:
condition:
and:
- lambda: 'return id(plant1_status) == std::string("unknown");'
- lambda: "return x < id(plant_1_min_moisture_level).state;"
then:
- logger.log:
level: WARN
format: "Plant 1: Soil dry - Schedule watering!"
- globals.set:
id: plant1_status
value: '"needs_water"'
- script.execute:
id: schedule_watering
relay_id: "pump_switch_1"
- if:
condition:
and:
- lambda: 'return id(plant1_status) == std::string("unknown");'
- lambda: "return x > id(plant_1_min_moisture_level).state;"
then:
- logger.log:
level: INFO
format: "Plant 1: Soil moisture OK — no watering needed."
- globals.set:
id: plant1_status
value: '"ok"'
- component.update: plant1_status_sensor
- platform: uptime
id: uptime_seconds
type: seconds
name: Uptime Seconds
update_interval: ${uptime_update_interval}
- platform: template
id: plant1_last_watered_timestamp_sensor
name: "Plant 1 Last Watered Timestamp"
unit_of_measurement: "s"
accuracy_decimals: 0
entity_category: diagnostic
update_interval: never
lambda: |-
return id(plant1_last_watered);
light:
- platform: neopixelbus
id: status_led
name: "Status Led"
type: GRB
pin: GPIO7
num_leds: 1
variant: ws2812
effects:
- pulse:
name: "Pulse"
transition_length: 500ms
update_interval: 500ms
- pulse:
name: "Breathing"
transition_length: 2000ms
update_interval: 2000ms
min_brightness: 20%
max_brightness: 100%
- platform: monochromatic
id: pump_control
name: "Pump Control"
output: pwm_pump
gamma_correct: 1.0
icon: "mdi:pump"
time:
- platform: sntp
id: esptime
servers:
- 0.pool.ntp.org
- 1.pool.ntp.org
- 2.pool.ntp.org
number:
- platform: template
name: "Watering Pulse Time (ON)"
id: watering_pulse_time_on
unit_of_measurement: "sec"
entity_category: config
min_value: 0
max_value: 120
step: 1
optimistic: true
mode: box
initial_value: 0
restore_value: yes
- platform: template
name: "Watering Pulse Time (OFF)"
id: watering_pulse_time_off
unit_of_measurement: "sec"
entity_category: config
min_value: 0
max_value: 120
step: 1
optimistic: true
mode: box
initial_value: 0
restore_value: yes
- platform: template
name: "Sleep Duration"
id: sleep_duration_minutes
unit_of_measurement: "min"
entity_category: config
min_value: 1
max_value: 1440
step: 5
optimistic: true
mode: box
initial_value: ${initial_sleep_duration_minutes}
restore_value: yes
- platform: template
name: "Max Running Time"
id: max_running_time_minutes
unit_of_measurement: "min"
entity_category: config
min_value: 1
max_value: 15
step: 1
optimistic: true
mode: box
initial_value: ${initial_max_running_time_minutes}
restore_value: yes
- platform: template
name: "Total Watering Time"
id: total_watering_time
unit_of_measurement: "sec"
entity_category: config
min_value: 0
max_value: 120
step: 1
optimistic: true
mode: box
initial_value: ${initial_watering_time_seconds}
restore_value: yes
- platform: template
name: "Re-watering wait time"
id: rewatering_wait_time_minutes
entity_category: config
unit_of_measurement: "min"
min_value: 0
max_value: 10080
step: 10
optimistic: true
mode: box
initial_value: ${initial_watering_wait_time_minutes}
restore_value: yes
- platform: template
name: "Plant 1 min. Moisture Level"
id: plant_1_min_moisture_level
entity_category: config
unit_of_measurement: "%"
min_value: 0
max_value: 100
step: 1
optimistic: true
initial_value: ${initial_min_moisture_level}
restore_value: yes
- platform: template
name: "Pump Power"
id: pump_power
entity_category: config
unit_of_measurement: "%"
min_value: 0
max_value: 100
step: 1
optimistic: true
initial_value: 100
restore_value: yes
text_sensor:
- id: !extend uptime_text_sensor
update_interval: ${uptime_update_interval}
- platform: template
id: plant1_status_sensor
name: "Plant-1 Status"
update_interval: never
icon: "mdi:flower"
lambda: return id(plant1_status);
- platform: template
name: "Plant 1 Last Watered"
id: plant1_last_watered_text_sensor
entity_category: diagnostic
update_interval: never
icon: "mdi:flower-pollen"
lambda: |-
if (id(plant1_last_watered) == 0) {
return std::string("Never");
} else {
time_t last_time = id(plant1_last_watered);
struct tm *timeinfo = localtime(&last_time);
char buffer[30];
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", timeinfo);
return std::string(buffer);
}
script:
- id: measure_battery
mode: single
then:
- repeat:
count: 20
then:
- delay: 25ms
- component.update: battery_voltage
- id: prepare_shutdown
mode: single
then:
- light.turn_off:
id: status_led
transition_length: 0s
- component.update: uptime_seconds
- component.update: uptime_text_sensor
- delay: 2s
- deep_sleep.allow: sleep_control
- deep_sleep.enter:
id: sleep_control
sleep_duration: !lambda "return id(sleep_duration_minutes).state * 1000 * 60;"
- id: mark_plants_as_watered
mode: queued
then:
- lambda: |-
time_t now = id(esptime).now().timestamp;
std::string watered_state = std::string("watered");
id(plant1_status) = watered_state;
id(plant1_last_watered) = now;
id(plant1_status_sensor).update();
id(plant1_last_watered_text_sensor).update();
id(plant1_last_watered_timestamp_sensor).update();
- id: watering_process
mode: single
then:
- logger.log:
level: INFO
tag: watering_process
format: "Start watering"
- globals.set:
id: plant1_status
value: '"watering"'
- component.update: plant1_status_sensor
- globals.set:
id: elapsed_watering_time
value: "0"
- if:
condition:
- lambda: "return id(watering_pulse_time_on).state > 0 && id(watering_pulse_time_off).state > 0 ;"
then:
- logger.log:
level: INFO
tag: watering_process
format: "Use watering pulse with ON: %0.0fs / OFF: %0.0fs"
args:
- id(watering_pulse_time_on).state
- id(watering_pulse_time_off).state
- while:
condition:
- lambda: "return ( id(total_watering_time).state * 1000 ) > id(elapsed_watering_time);"
then:
# Start Pump with 100% to push to water upwards
- light.turn_on:
id: pump_control
transition_length: 0s
brightness: "1.0"
- delay:
milliseconds: ${pump_prestarting_time_ms}
- light.turn_on:
id: pump_control
transition_length: 0s
brightness: !lambda return id(pump_power).state / 100;
- delay: !lambda "return id(watering_pulse_time_on).state * 1000;"
- light.turn_off:
id: pump_control
transition_length: 0s
- lambda: |-
id(elapsed_watering_time) += ( id(watering_pulse_time_on).state * 1000 ) + ${pump_prestarting_time_ms};
- if:
condition:
lambda: |-
// Check if there's enough time left for another full cycle
return id(total_watering_time).state * 1000 > id(elapsed_watering_time);
then:
- delay: !lambda "return id(watering_pulse_time_off).state * 1000;"
else:
- logger.log:
level: INFO
tag: watering_process
format: "Use continous watering for %0.0fs"
args: id(total_watering_time).state
# Start Pump with 100% to push to water upwards
- light.turn_on:
id: pump_control
transition_length: 0s
brightness: "1.0"
- delay:
milliseconds: ${pump_prestarting_time_ms}
- light.turn_on:
id: pump_control
transition_length: 0s
brightness: !lambda return id(pump_power).state / 100;
- delay: !lambda "return id(total_watering_time).state * 1000;"
- logger.log:
level: INFO
tag: watering_process
format: "Total watering time reached."
- switch.turn_off: pump_switch_1
- id: schedule_watering
mode: queued
max_runs: 4
then:
- logger.log:
level: INFO
tag: schedule_watering
format: "Schedule watering process"
- logger.log:
level: INFO
tag: schedule_watering
format: "Wait for system has booted"
- wait_until:
- lambda: "return id(has_booted) = true;"
- logger.log:
level: INFO
tag: schedule_watering
format: "Wait for Pump to finish"
- wait_until:
condition:
and:
- switch.is_off: pump_switch_1
- if:
condition:
lambda: |-
time_t now = id(esptime).now().timestamp;
int rewateringWaitTime = id(rewatering_wait_time_minutes).state * 60;
time_t last_watered = std::max({
static_cast<time_t>(id(plant1_last_watered)) //,
//static_cast<time_t>(id(plant2_last_watered)),
//static_cast<time_t>(id(plant3_last_watered))
});
return (now - last_watered) >= rewateringWaitTime;
then:
- logger.log:
level: INFO
tag: schedule_watering
format: "Wait for water tank"
- wait_until:
binary_sensor.is_off: water_tank_empty
- switch.turn_on: pump_switch_1
else:
- logger.log:
level: WARN
tag: schedule_watering
format: "was watered too recently (last watered < %d min ago)"
args:
- int(id(rewatering_wait_time_minutes).state)
- lambda: |-
std::string recently_watered_state = std::string("recently_watered");
id(plant1_status) = recently_watered_state;
id(plant1_status_sensor).update();
- id: led_status_light
mode: restart
then:
- lambda: |-
// Only proceed if status_led is currently on
if (!id(status_led).current_values.is_on()) {
return;
}
// Tank Empty -> Red
if (id(water_tank_empty).state) {
id(status_led).turn_on()
.set_effect("Pulse")
.set_red(1.0)
.set_green(0.0)
.set_blue(0.0)
.set_brightness(1.0)
.set_transition_length(0)
.perform();
return;
}
// Not Connected -> Magenta
if (!id(api_id).is_connected()) {
id(status_led).turn_on()
.set_effect("Pulse")
.set_red(1.0)
.set_green(0.0)
.set_blue(1.0)
.set_brightness(1.0)
.set_transition_length(0)
.perform();
return;
}
// All Plants Scanned
if (id(all_plants_scanned)){
id(status_led).turn_on()
.set_effect("Breathing")
.set_red(0.0)
.set_green(1.0)
.set_blue(0.8)
.set_brightness(1.0)
.set_transition_length(0)
.perform();
return;
}
// OK -> Blue
id(status_led).turn_on()
.set_effect("Breathing")
.set_red(0.0)
.set_green(0.0)
.set_blue(1.0)
.set_brightness(1.0)
.perform();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment