Last active
May 9, 2025 23:42
-
-
Save pavax/54a2443fc1fc879cdb336d503ffd07d2 to your computer and use it in GitHub Desktop.
smart-plant-watering-mini-1.yaml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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