Last active
May 15, 2025 21:51
-
-
Save pavax/7db3cd531fd94854c609c8274d4b8136 to your computer and use it in GitHub Desktop.
ESPHome Config for Plant Watering
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: | |
friendly_name: "Smart Plant Watering 2" | |
name: "smart-plant-watering-2" | |
initial_sleep_duration_minutes: "60" # For how long (minutes) should the node sleep | |
initial_rewatering_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_max_watering_time_seconds: "10" # How long should the watering process run (seconds) | |
initial_max_running_time_minutes: "5" # Max time (minutes) for the node to stay awake | |
battery_max: "4.1" # Battery voltage indicating 100% | |
battery_min: "3.3" # Battery voltage indicating 0% | |
uptime_update_interval: 60s | |
esp32: | |
board: lolin_c3_mini | |
framework: | |
type: esp-idf | |
wifi: | |
ssid: !secret wifi_ssid | |
password: !secret wifi_password | |
fast_connect: true | |
output_power: 8.5dB | |
esphome: | |
name: ${name} | |
friendly_name: ${friendly_name} | |
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: -200.0 | |
then: | |
- light.turn_off: | |
id: status_led | |
transition_length: 0s | |
- switch.turn_off: external_modules | |
- component.update: uptime_seconds | |
#- component.update: uptime_text_sensor | |
- logger.log: | |
level: INFO | |
format: "Going to sleep - Good Night!🌛" | |
logger: | |
level: DEBUG | |
globals: | |
- id: plant1_status | |
type: std::string | |
restore_value: no | |
initial_value: '"unknown"' | |
- id: plant2_status | |
type: std::string | |
restore_value: no | |
initial_value: '"unknown"' | |
- id: plant3_status | |
type: std::string | |
restore_value: no | |
initial_value: '"unknown"' | |
- id: plant4_status | |
type: std::string | |
restore_value: no | |
initial_value: '"unknown"' | |
- id: plant5_status | |
type: std::string | |
restore_value: no | |
initial_value: '"unknown"' | |
- id: plant1_last_watered | |
type: unsigned long | |
restore_value: yes | |
initial_value: "0" | |
- id: plant2_last_watered | |
type: unsigned long | |
restore_value: yes | |
initial_value: "0" | |
- id: plant3_last_watered | |
type: unsigned long | |
restore_value: yes | |
initial_value: "0" | |
- id: plant4_last_watered | |
type: unsigned long | |
restore_value: yes | |
initial_value: "0" | |
- id: plant5_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" | |
- id: elapsed_watering_time | |
type: int | |
restore_value: no | |
initial_value: "0" | |
interval: | |
- interval: 2sec | |
id: "stop_scanning_interval" | |
startup_delay: 5sec | |
then: | |
- if: | |
condition: | |
- and: | |
- lambda: "return !isnan(id(plant1_sensor_moisture).state);" | |
- lambda: "return !isnan(id(plant2_sensor_moisture).state);" | |
- lambda: "return !isnan(id(plant3_sensor_moisture).state);" | |
- lambda: "return !isnan(id(plant4_sensor_moisture).state);" | |
- lambda: "return !isnan(id(plant5_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: 5s | |
id: "max_running_time_reached_interval" | |
startup_delay: 60s | |
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: 20s | |
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") || id(plant1_status) == std::string("watering_canceled");' | |
- lambda: 'return id(plant2_status) == std::string("ok") || id(plant2_status) == std::string("watered") || id(plant2_status) == std::string("recently_watered") || id(plant2_status) == std::string("watering_canceled");' | |
- lambda: 'return id(plant3_status) == std::string("ok") || id(plant3_status) == std::string("watered") || id(plant3_status) == std::string("recently_watered") || id(plant3_status) == std::string("watering_canceled");' | |
- lambda: 'return id(plant4_status) == std::string("ok") || id(plant4_status) == std::string("watered") || id(plant4_status) == std::string("recently_watered") || id(plant4_status) == std::string("watering_canceled");' | |
- lambda: 'return id(plant5_status) == std::string("ok") || id(plant5_status) == std::string("watered") || id(plant5_status) == std::string("recently_watered") || id(plant5_status) == std::string("watering_canceled");' | |
- lambda: "return id(all_plants_scanned);" | |
then: | |
- esp32_ble_tracker.stop_scan | |
- 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 | |
light: | |
- platform: esp32_rmt_led_strip | |
id: status_led | |
rgb_order: GRB | |
pin: GPIO7 | |
num_leds: 30 | |
#rmt_channel: 0 | |
chipset: ws2812 | |
name: "Onboard RGB" | |
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: neopixelbus | |
# id: status_led | |
# type: GRB | |
# pin: GPIO7 | |
# num_leds: 1 | |
# name: "Onboard RGB" | |
# 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% | |
output: | |
- platform: gpio | |
id: battery_gnd | |
pin: | |
number: GPIO2 | |
inverted: true | |
mode: | |
output: true | |
pullup: false | |
pulldown: false | |
ignore_strapping_warning: true | |
binary_sensor: | |
- platform: status | |
name: "Status" | |
- 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: GPIO1 | |
inverted: false | |
mode: | |
input: true | |
pullup: true | |
filters: | |
- delayed_on: 250ms | |
- delayed_off: 250ms | |
on_release: | |
then: | |
- script.execute: led_status_light | |
on_press: | |
then: | |
- script.execute: | |
id: set_relay_state | |
relay_id: "all" | |
state: "OFF" | |
use_internal: false | |
- script.execute: led_status_light | |
sensor: | |
- platform: adc | |
pin: GPIO3 | |
name: "Battery Voltage" | |
id: battery_voltage | |
icon: "mdi:flash" | |
attenuation: 12db | |
accuracy_decimals: 2 | |
update_interval: never | |
filters: | |
- median: | |
window_size: 5 | |
send_every: 5 | |
send_first_at: 1 | |
- multiply: 2.0 | |
- round: 2 | |
- platform: copy | |
source_id: battery_voltage | |
unit_of_measurement: "%" | |
icon: "mdi:battery" | |
name: "Battery Percentage" | |
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: uptime | |
id: uptime_seconds | |
type: seconds | |
name: Uptime Seconds | |
update_interval: ${uptime_update_interval} | |
- platform: xiaomi_hhccjcy01 | |
mac_address: "C4:7C:8D:65:FD:DF" | |
moisture: | |
name: "Plant 1 Soil Moisture" | |
id: plant1_sensor_moisture | |
on_value: | |
then: | |
- 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: !lambda "return id(plant1_relay).state;" | |
- if: | |
condition: | |
and: | |
- lambda: 'return id(plant1_status) == std::string("unknown");' | |
- lambda: "return x > id(plant_1_min_moisture_level).state;" | |
then: | |
- globals.set: | |
id: plant1_status | |
value: '"ok"' | |
- component.update: plant1_status_sensor | |
- platform: xiaomi_hhccjcy01 | |
mac_address: "C4:7C:8D:6B:A8:3C" | |
moisture: | |
name: "Plant 2 Soil Moisture" | |
id: plant2_sensor_moisture | |
on_value: | |
then: | |
- if: | |
condition: | |
and: | |
- lambda: 'return id(plant2_status) == std::string("unknown");' | |
- lambda: "return x <= id(plant_2_min_moisture_level).state;" | |
then: | |
- logger.log: | |
level: WARN | |
format: "Plant 2: Soil dry - Schedule watering!" | |
- globals.set: | |
id: plant2_status | |
value: '"needs_water"' | |
- script.execute: | |
id: schedule_watering | |
relay_id: !lambda "return id(plant2_relay).state;" | |
- if: | |
condition: | |
and: | |
- lambda: 'return id(plant2_status) == std::string("unknown");' | |
- lambda: "return x > id(plant_2_min_moisture_level).state;" | |
then: | |
- globals.set: | |
id: plant2_status | |
value: '"ok"' | |
- component.update: plant2_status_sensor | |
- platform: xiaomi_hhccjcy01 | |
mac_address: "C4:7C:8D:6B:8E:EE" | |
moisture: | |
name: "Plant 3 Soil Moisture" | |
id: plant3_sensor_moisture | |
on_value: | |
then: | |
- if: | |
condition: | |
and: | |
- lambda: 'return id(plant3_status) == std::string("unknown");' | |
- lambda: "return x <= id(plant_3_min_moisture_level).state;" | |
then: | |
- logger.log: | |
level: WARN | |
format: "Plant 3: Soil dry - Schedule watering!" | |
- globals.set: | |
id: plant3_status | |
value: '"needs_water"' | |
- script.execute: | |
id: schedule_watering | |
relay_id: !lambda "return id(plant3_relay).state;" | |
- if: | |
condition: | |
and: | |
- lambda: 'return id(plant3_status) == std::string("unknown");' | |
- lambda: "return x > id(plant_3_min_moisture_level).state;" | |
then: | |
- globals.set: | |
id: plant3_status | |
value: '"ok"' | |
- component.update: plant3_status_sensor | |
- platform: xiaomi_hhccjcy01 | |
mac_address: "C4:7C:8D:6B:96:05" | |
moisture: | |
name: "Plant 4 Soil Moisture" | |
id: plant4_sensor_moisture | |
on_value: | |
then: | |
- if: | |
condition: | |
and: | |
- lambda: 'return id(plant4_status) == std::string("unknown");' | |
- lambda: "return x <= id(plant_4_min_moisture_level).state;" | |
then: | |
- logger.log: | |
level: WARN | |
format: "Plant 4: Soil dry - Schedule watering!" | |
- globals.set: | |
id: plant4_status | |
value: '"needs_water"' | |
- script.execute: | |
id: schedule_watering | |
relay_id: !lambda "return id(plant4_relay).state;" | |
- if: | |
condition: | |
and: | |
- lambda: 'return id(plant4_status) == std::string("unknown");' | |
- lambda: "return x > id(plant_4_min_moisture_level).state;" | |
then: | |
- globals.set: | |
id: plant4_status | |
value: '"ok"' | |
- component.update: plant4_status_sensor | |
- platform: xiaomi_hhccjcy01 | |
mac_address: "80:EA:CA:88:B8:DF" | |
moisture: | |
name: "Plant 5 Soil Moisture" | |
id: plant5_sensor_moisture | |
on_value: | |
then: | |
- if: | |
condition: | |
and: | |
- lambda: 'return id(plant5_status) == std::string("unknown");' | |
- lambda: "return x <= id(plant_5_min_moisture_level).state;" | |
then: | |
- logger.log: | |
level: WARN | |
format: "Plant 5: Soil dry - Schedule watering!" | |
- globals.set: | |
id: plant5_status | |
value: '"needs_water"' | |
- script.execute: | |
id: schedule_watering | |
relay_id: !lambda "return id(plant5_relay).state;" | |
- if: | |
condition: | |
and: | |
- lambda: 'return id(plant5_status) == std::string("unknown");' | |
- lambda: "return x > id(plant_5_min_moisture_level).state;" | |
then: | |
- globals.set: | |
id: plant5_status | |
value: '"ok"' | |
- component.update: plant5_status_sensor | |
- platform: template | |
name: "Plant 1 Last Watered Timestamp" | |
id: plant1_last_watered_timestamp_sensor | |
icon: "mdi:clock-time-eight-outline" | |
unit_of_measurement: "s" | |
accuracy_decimals: 0 | |
entity_category: diagnostic | |
update_interval: never | |
lambda: return id(plant1_last_watered); | |
- platform: template | |
name: "Plant 2 Last Watered Timestamp" | |
id: plant2_last_watered_timestamp_sensor | |
icon: "mdi:clock-time-eight-outline" | |
unit_of_measurement: "s" | |
accuracy_decimals: 0 | |
entity_category: diagnostic | |
update_interval: never | |
lambda: return id(plant2_last_watered); | |
- platform: template | |
name: "Plant 3 Last Watered Timestamp" | |
id: plant3_last_watered_timestamp_sensor | |
icon: "mdi:clock-time-eight-outline" | |
unit_of_measurement: "s" | |
accuracy_decimals: 0 | |
entity_category: diagnostic | |
update_interval: never | |
lambda: return id(plant3_last_watered); | |
- platform: template | |
name: "Plant 4 Last Watered Timestamp" | |
id: plant4_last_watered_timestamp_sensor | |
unit_of_measurement: "s" | |
accuracy_decimals: 0 | |
entity_category: diagnostic | |
update_interval: never | |
lambda: return id(plant4_last_watered); | |
- platform: template | |
name: "Plant 5 Last Watered Timestamp" | |
id: plant5_last_watered_timestamp_sensor | |
unit_of_measurement: "s" | |
accuracy_decimals: 0 | |
entity_category: diagnostic | |
update_interval: never | |
lambda: return id(plant5_last_watered); | |
text_sensor: | |
# - platform: wifi_info | |
# ip_address: | |
# name: "IP Address" | |
# ssid: | |
# name: "SSID" | |
# - platform: version | |
# name: "ESPHome Version" | |
# - platform: uptime | |
# id: uptime_text_sensor | |
# name: Uptime | |
# 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 | |
id: plant2_status_sensor | |
name: "Plant-2 Status" | |
update_interval: never | |
icon: "mdi:flower" | |
lambda: return id(plant2_status); | |
- platform: template | |
id: plant3_status_sensor | |
name: "Plant-3 Status" | |
update_interval: never | |
icon: "mdi:flower" | |
lambda: return id(plant3_status); | |
- platform: template | |
id: plant4_status_sensor | |
name: "Plant-4 Status" | |
update_interval: never | |
icon: "mdi:flower" | |
lambda: return id(plant4_status); | |
- platform: template | |
id: plant5_status_sensor | |
name: "Plant-5 Status" | |
update_interval: never | |
icon: "mdi:flower" | |
lambda: return id(plant5_status); | |
switch: | |
- platform: template | |
name: "Relay 1" | |
id: relay1 | |
lambda: "return id(watering_process_relay1).is_running();" | |
icon: "mdi:water-pump" | |
turn_on_action: | |
- if: | |
condition: | |
and: | |
- binary_sensor.is_off: water_tank_empty | |
- lambda: "return id(has_booted);" | |
- not: | |
- script.is_running: watering_process | |
then: | |
- script.execute: | |
id: watering_process_relay1 | |
turn_off_action: | |
- switch.turn_off: relay1_internal | |
- if: | |
condition: | |
- script.is_running: watering_process_relay1 | |
then: | |
- script.execute: | |
id: update_plants_state | |
relay_id: "relay1" | |
newState: "watering_canceled" | |
update_timestamp: false | |
- script.execute: | |
id: stop_watering_scripts | |
- script.execute: led_status_light | |
- platform: template | |
name: "Relay 2" | |
id: relay2 | |
lambda: "return id(watering_process_relay2).is_running();" | |
icon: "mdi:water-pump" | |
turn_on_action: | |
- if: | |
condition: | |
and: | |
- binary_sensor.is_off: water_tank_empty | |
- lambda: "return id(has_booted);" | |
- not: | |
- script.is_running: watering_process | |
then: | |
- script.execute: | |
id: watering_process_relay2 | |
turn_off_action: | |
- switch.turn_off: relay2_internal | |
- if: | |
condition: | |
- script.is_running: watering_process_relay2 | |
then: | |
- script.execute: | |
id: update_plants_state | |
relay_id: "relay2" | |
newState: "watering_canceled" | |
update_timestamp: false | |
- script.execute: | |
id: stop_watering_scripts | |
- script.execute: led_status_light | |
- platform: template | |
name: "Relay 3" | |
id: relay3 | |
lambda: "return id(watering_process_relay3).is_running();" | |
icon: "mdi:water-pump" | |
turn_on_action: | |
- if: | |
condition: | |
and: | |
- binary_sensor.is_off: water_tank_empty | |
- lambda: "return id(has_booted);" | |
- not: | |
- script.is_running: watering_process | |
then: | |
- script.execute: | |
id: watering_process_relay3 | |
turn_off_action: | |
- switch.turn_off: relay3_internal | |
- if: | |
condition: | |
- script.is_running: watering_process_relay3 | |
then: | |
- script.execute: | |
id: update_plants_state | |
relay_id: "relay3" | |
newState: "watering_canceled" | |
update_timestamp: false | |
- script.execute: | |
id: stop_watering_scripts | |
- script.execute: led_status_light | |
- platform: template | |
name: "Relay 4" | |
id: relay4 | |
lambda: "return id(watering_process_relay4).is_running();" | |
icon: "mdi:water-pump" | |
turn_on_action: | |
- if: | |
condition: | |
and: | |
- binary_sensor.is_off: water_tank_empty | |
- lambda: "return id(has_booted);" | |
- not: | |
- script.is_running: watering_process | |
then: | |
- script.execute: | |
id: watering_process_relay4 | |
turn_off_action: | |
- switch.turn_off: relay4_internal | |
- if: | |
condition: | |
- script.is_running: watering_process_relay4 | |
then: | |
- script.execute: | |
id: update_plants_state | |
relay_id: "relay4" | |
newState: "watering_canceled" | |
update_timestamp: false | |
- script.execute: | |
id: stop_watering_scripts | |
- script.execute: led_status_light | |
- platform: gpio | |
name: "Relay 1 (Internal)" | |
id: relay1_internal | |
restore_mode: ALWAYS_OFF | |
pin: GPIO4 | |
inverted: true | |
interlock: [relay2_internal, relay3_internal, relay4_internal] | |
internal: true | |
- platform: gpio | |
name: "Relay 2 (internal)" | |
id: relay2_internal | |
restore_mode: ALWAYS_OFF | |
pin: | |
number: GPIO8 | |
ignore_strapping_warning: true | |
inverted: true | |
interlock: [relay1_internal, relay3_internal, relay4_internal] | |
internal: true | |
- platform: gpio | |
name: "Relay 3 (internal)" | |
id: relay3_internal | |
restore_mode: ALWAYS_OFF | |
pin: GPIO6 | |
inverted: true | |
interlock: [relay1_internal, relay2_internal, relay4_internal] | |
internal: true | |
- platform: gpio | |
name: "Relay 4 (internal)" | |
id: relay4_internal | |
restore_mode: ALWAYS_OFF | |
pin: GPIO10 | |
inverted: true | |
interlock: [relay1_internal, relay2_internal, relay3_internal] | |
internal: true | |
- platform: gpio | |
name: "Power external modules" | |
id: external_modules | |
restore_mode: ALWAYS_OFF | |
pin: | |
number: GPIO5 | |
inverted: false | |
on_turn_off: | |
- script.execute: | |
id: set_relay_state | |
relay_id: "all" | |
state: "OFF" | |
use_internal: false | |
- 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: | |
- deep_sleep.prevent: sleep_control | |
on_turn_off: | |
then: | |
- deep_sleep.allow: sleep_control | |
number: | |
# | |
# Relay 1 Configuration | |
# | |
- platform: template | |
name: "Relay1 Watering Time Total" | |
id: relay1_watering_time_total | |
unit_of_measurement: "sec" | |
entity_category: config | |
min_value: 0 | |
max_value: 90 | |
step: 1 | |
optimistic: true | |
mode: box | |
initial_value: ${initial_max_watering_time_seconds} | |
restore_value: yes | |
- platform: template | |
name: "Relay 1 Watering Pulse Time (ON)" | |
id: relay1_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: "Relay 1 Watering Pulse Time (OFF)" | |
id: relay1_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 | |
# | |
# Relay 2 Configuration | |
# | |
- platform: template | |
name: "Relay2 Watering Time Total" | |
id: relay2_watering_time_total | |
unit_of_measurement: "sec" | |
entity_category: config | |
min_value: 0 | |
max_value: 90 | |
step: 1 | |
optimistic: true | |
mode: box | |
initial_value: ${initial_max_watering_time_seconds} | |
restore_value: yes | |
- platform: template | |
name: "Relay 2 Watering Pulse Time (ON)" | |
id: relay2_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: "Relay 2 Watering Pulse Time (OFF)" | |
id: relay2_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 | |
# | |
# Relay 3 Configuration | |
# | |
- platform: template | |
name: "Relay3 Watering Time Total" | |
id: relay3_watering_time_total | |
unit_of_measurement: "sec" | |
entity_category: config | |
min_value: 0 | |
max_value: 90 | |
step: 1 | |
optimistic: true | |
mode: box | |
initial_value: ${initial_max_watering_time_seconds} | |
restore_value: yes | |
- platform: template | |
name: "Relay 3 Watering Pulse Time (ON)" | |
id: relay3_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: "Relay 3 Watering Pulse Time (OFF)" | |
id: relay3_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 | |
# | |
# Relay 4 Configuration | |
# | |
- platform: template | |
name: "Relay 4 Watering Time Total" | |
id: relay4_watering_time_total | |
unit_of_measurement: "sec" | |
entity_category: config | |
min_value: 0 | |
max_value: 90 | |
step: 1 | |
optimistic: true | |
mode: box | |
initial_value: ${initial_max_watering_time_seconds} | |
restore_value: yes | |
- platform: template | |
name: "Relay 4 Watering Pulse Time (ON)" | |
id: relay4_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: "Relay 4 Watering Pulse Time (OFF)" | |
id: relay4_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 | |
# | |
# System Configuration | |
# | |
- 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: "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_rewatering_wait_time_minutes} | |
restore_value: yes | |
# | |
# Plants Configuration | |
# | |
- 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: "Plant 2 min. Moisture Level" | |
id: plant_2_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: "Plant 3 min. Moisture Level" | |
id: plant_3_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: "Plant 4 min. Moisture Level" | |
id: plant_4_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: "Plant 5 min. Moisture Level" | |
id: plant_5_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 | |
select: | |
- platform: template | |
name: "Plant-1" | |
id: plant1_relay | |
options: | |
- "relay1" | |
- "relay2" | |
- "relay3" | |
- "relay4" | |
entity_category: config | |
initial_option: "relay1" | |
restore_value: yes | |
optimistic: true | |
- platform: template | |
name: "Plant-2" | |
id: plant2_relay | |
options: | |
- "relay1" | |
- "relay2" | |
- "relay3" | |
- "relay4" | |
entity_category: config | |
initial_option: "relay1" | |
restore_value: yes | |
optimistic: true | |
- platform: template | |
name: "Plant-3" | |
id: plant3_relay | |
options: | |
- "relay1" | |
- "relay2" | |
- "relay3" | |
- "relay4" | |
entity_category: config | |
initial_option: "relay1" | |
restore_value: yes | |
optimistic: true | |
- platform: template | |
name: "Plant-4" | |
id: plant4_relay | |
options: | |
- "relay1" | |
- "relay2" | |
- "relay3" | |
- "relay4" | |
entity_category: config | |
initial_option: "relay2" | |
restore_value: yes | |
optimistic: true | |
- platform: template | |
name: "Plant-5" | |
id: plant5_relay | |
options: | |
- "relay1" | |
- "relay2" | |
- "relay3" | |
- "relay4" | |
entity_category: config | |
initial_option: "relay2" | |
restore_value: yes | |
optimistic: true | |
ota: | |
platform: esphome | |
password: !secret api_password | |
on_begin: | |
then: | |
- deep_sleep.prevent: sleep_control | |
on_end: | |
then: | |
- deep_sleep.allow: sleep_control | |
on_error: | |
then: | |
- deep_sleep.allow: sleep_control | |
api: | |
id: api_id | |
encryption: | |
key: !secret encryption_key | |
on_client_connected: | |
- script.execute: led_status_light | |
- esp32_ble_tracker.start_scan: | |
continuous: true | |
- component.update: plant1_last_watered_timestamp_sensor | |
- component.update: plant2_last_watered_timestamp_sensor | |
- component.update: plant3_last_watered_timestamp_sensor | |
- component.update: plant4_last_watered_timestamp_sensor | |
- component.update: plant5_last_watered_timestamp_sensor | |
on_client_disconnected: | |
- script.execute: led_status_light | |
- 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 | |
qos: 1 | |
payload: "plant1" | |
then: | |
- script.execute: | |
id: schedule_watering | |
relay_id: !lambda "return id(plant1_relay).state;" | |
- mqtt.publish: | |
topic: ${name}/schedule-watering | |
payload: "ACK" | |
retain: true | |
- topic: ${name}/schedule-watering | |
qos: 1 | |
payload: "plant4" | |
then: | |
- script.execute: | |
id: schedule_watering | |
relay_id: !lambda "return id(plant4_relay).state;" | |
- 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 | |
script: | |
- id: measure_battery | |
mode: single | |
then: | |
- output.turn_on: battery_gnd | |
- repeat: | |
count: 20 | |
then: | |
- delay: 25ms | |
- component.update: battery_voltage | |
- output.turn_off: battery_gnd | |
- id: set_relay_state | |
mode: restart | |
parameters: | |
relay_id: string | |
state: string | |
use_internal: bool | |
then: | |
- lambda: |- | |
std::map<std::string, switch_::Switch*> relays = { | |
{"relay1", use_internal ? static_cast<switch_::Switch*>(id(relay1_internal)) : static_cast<switch_::Switch*>(id(relay1))}, | |
{"relay2", use_internal ? static_cast<switch_::Switch*>(id(relay2_internal)) : static_cast<switch_::Switch*>(id(relay2))}, | |
{"relay3", use_internal ? static_cast<switch_::Switch*>(id(relay3_internal)) : static_cast<switch_::Switch*>(id(relay3))}, | |
{"relay4", use_internal ? static_cast<switch_::Switch*>(id(relay4_internal)) : static_cast<switch_::Switch*>(id(relay4))} | |
}; | |
bool handled = false; | |
if (relay_id == "all") { | |
handled = true; | |
for (auto& [name, relay] : relays) { | |
if (state == "on" || state == "ON") { | |
if (!relay->state) relay->turn_on(); | |
} else if (state == "off" || state == "OFF") { | |
if (relay->state) relay->turn_off(); | |
} | |
} | |
} else { | |
auto relay_it = relays.find(relay_id); | |
if (relay_it != relays.end()) { | |
handled = true; | |
switch_::Switch* relay = relay_it->second; | |
if (( state == "on" || state == "ON" ) && !relay->state) { | |
relay->turn_on(); | |
} else if ((state == "off" || state == "OFF" ) && relay->state) { | |
relay->turn_off(); | |
} | |
} | |
} | |
if (!handled) { | |
ESP_LOGW("set_relay_state", "Unknown relay_id: %s", relay_id.c_str()); | |
} | |
- 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: led_status_light | |
mode: restart | |
then: | |
- delay: 100ms | |
- 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) | |
.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) | |
.perform(); | |
return; | |
} | |
// watering process is running -> yellow | |
if (id(watering_process).is_running()) { | |
id(status_led).turn_on() | |
.set_effect("Breathing") | |
.set_red(1.0) | |
.set_green(1.0) | |
.set_blue(0.0) | |
.set_brightness(1.0) | |
.perform(); | |
return; | |
} | |
// All Plants Scanned -> Cyan | |
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) | |
.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(); | |
- id: update_plants_state | |
mode: queued | |
parameters: | |
relay_id: string | |
newState: string | |
update_timestamp: boolean | |
then: | |
- lambda: |- | |
time_t now = id(esptime).now().timestamp; | |
std::map<std::string, std::tuple<std::string*, template_::TemplateTextSensor*, unsigned long*, template_::TemplateSensor*>> plant_map = { | |
{id(plant1_relay).state, std::make_tuple( &id(plant1_status), id(plant1_status_sensor), &id(plant1_last_watered), id(plant1_last_watered_timestamp_sensor) )}, | |
{id(plant2_relay).state, std::make_tuple( &id(plant2_status), id(plant2_status_sensor), &id(plant2_last_watered), id(plant2_last_watered_timestamp_sensor) )}, | |
{id(plant3_relay).state, std::make_tuple( &id(plant3_status), id(plant3_status_sensor), &id(plant3_last_watered), id(plant3_last_watered_timestamp_sensor) )}, | |
{id(plant4_relay).state, std::make_tuple( &id(plant4_status), id(plant4_status_sensor), &id(plant4_last_watered), id(plant4_last_watered_timestamp_sensor) )}, | |
{id(plant5_relay).state, std::make_tuple( &id(plant5_status), id(plant5_status_sensor), &id(plant5_last_watered), id(plant5_last_watered_timestamp_sensor) )} | |
}; | |
bool handled = false; | |
for (auto& [plantRelayId, plant_data] : plant_map) { | |
if (plantRelayId == relay_id) { | |
handled = true; | |
// Update status | |
*std::get<0>(plant_data) = newState; | |
std::get<1>(plant_data)->publish_state(newState); | |
// Conditionally update timestamp | |
if (update_timestamp) { | |
*std::get<2>(plant_data) = now; | |
std::get<3>(plant_data)->publish_state(now); | |
} | |
} | |
} | |
if (!handled) { | |
ESP_LOGW("update_plants_state", "Unknown relay_id: %s", relay_id.c_str()); | |
} | |
- id: schedule_watering | |
parameters: | |
relay_id: string | |
mode: queued | |
max_runs: 5 | |
then: | |
- logger.log: | |
level: INFO | |
tag: schedule_watering | |
format: "%s: Schedule watering process" | |
args: | |
- relay_id.c_str() | |
- wait_until: | |
- lambda: "return id(has_booted) = true;" | |
- wait_until: | |
condition: | |
- not: | |
- script.is_running: watering_process | |
- if: | |
condition: | |
lambda: |- | |
time_t now = id(esptime).now().timestamp; | |
int rewateringWaitTime = id(rewatering_wait_time_minutes).state * 60; | |
std::map<std::string, time_t> plant_last_watered_map = { | |
{ id(plant1_relay).state, static_cast<time_t>(id(plant1_last_watered)) }, | |
{ id(plant2_relay).state, static_cast<time_t>(id(plant2_last_watered)) }, | |
{ id(plant3_relay).state, static_cast<time_t>(id(plant3_last_watered)) }, | |
{ id(plant4_relay).state, static_cast<time_t>(id(plant4_last_watered)) }, | |
{ id(plant5_relay).state, static_cast<time_t>(id(plant5_last_watered)) }, | |
}; | |
auto it = plant_last_watered_map.find(relay_id); | |
time_t last_watered = (it != plant_last_watered_map.end()) ? it->second : 0; | |
return (now - last_watered) >= rewateringWaitTime; | |
then: | |
- wait_until: | |
binary_sensor.is_off: water_tank_empty | |
- script.execute: | |
id: set_relay_state | |
relay_id: !lambda "return relay_id;" | |
state: "ON" | |
use_internal: false | |
else: | |
- logger.log: | |
level: WARN | |
tag: schedule_watering | |
format: "%s was watered recently (last watered < %d min ago)" | |
args: | |
- relay_id.c_str() | |
- int(id(rewatering_wait_time_minutes).state) | |
- script.execute: | |
id: update_plants_state | |
relay_id: !lambda "return relay_id;" | |
newState: "recently_watered" | |
update_timestamp: false | |
- id: watering_process_relay1 | |
mode: single | |
then: | |
- script.execute: | |
id: watering_process | |
relay_id: "relay1" | |
- script.execute: led_status_light | |
- script.wait: watering_process | |
- script.execute: led_status_light | |
- id: watering_process_relay2 | |
mode: single | |
then: | |
- script.execute: | |
id: watering_process | |
relay_id: "relay2" | |
- script.execute: led_status_light | |
- script.wait: watering_process | |
- script.execute: led_status_light | |
- id: watering_process_relay3 | |
mode: single | |
then: | |
- script.execute: | |
id: watering_process | |
relay_id: "relay3" | |
- script.execute: led_status_light | |
- script.wait: watering_process | |
- script.execute: led_status_light | |
- id: watering_process_relay4 | |
mode: single | |
then: | |
- script.execute: | |
id: watering_process | |
relay_id: "relay4" | |
- script.execute: led_status_light | |
- script.wait: watering_process | |
- script.execute: led_status_light | |
- id: watering_process | |
parameters: | |
relay_id: string | |
mode: single | |
then: | |
- logger.log: | |
level: INFO | |
tag: watering_process | |
format: "%s: Start watering process" | |
args: | |
- relay_id.c_str() | |
- switch.turn_on: external_modules | |
- script.execute: | |
id: update_plants_state | |
relay_id: !lambda "return relay_id;" | |
newState: "watering" | |
update_timestamp: false | |
- lambda: |- | |
std::map<std::string, std::tuple<float, float, float>> relay_params = { | |
{"relay1", std::make_tuple(id(relay1_watering_pulse_time_on).state, id(relay1_watering_pulse_time_off).state, id(relay1_watering_time_total).state)}, | |
{"relay2", std::make_tuple(id(relay2_watering_pulse_time_on).state, id(relay2_watering_pulse_time_off).state, id(relay2_watering_time_total).state)}, | |
{"relay3", std::make_tuple(id(relay3_watering_pulse_time_on).state, id(relay3_watering_pulse_time_off).state, id(relay3_watering_time_total).state)}, | |
{"relay4", std::make_tuple(id(relay4_watering_pulse_time_on).state, id(relay4_watering_pulse_time_off).state, id(relay4_watering_time_total).state)} | |
}; | |
auto it = relay_params.find(relay_id); | |
if (it != relay_params.end()) { | |
float pulse_on = std::get<0>(it->second); | |
float pulse_off = std::get<1>(it->second); | |
float total = std::get<2>(it->second); | |
if (pulse_on > 0 && pulse_off > 0) { | |
id(pulsed_watering)->execute(relay_id, pulse_on, pulse_off, total); | |
} else { | |
id(continous_watering)->execute(relay_id, total); | |
} | |
} else { | |
ESP_LOGW("watering_process", "Unknown relay_id: %s", relay_id.c_str()); | |
} | |
- script.wait: continous_watering | |
- script.wait: pulsed_watering | |
- script.execute: | |
id: update_plants_state | |
relay_id: !lambda "return relay_id;" | |
newState: "watered" | |
update_timestamp: true | |
- id: pulsed_watering | |
mode: single | |
parameters: | |
relay_id: string | |
pulse_time_on: float | |
pulse_time_off: float | |
total_time: float | |
then: | |
- logger.log: | |
level: INFO | |
tag: pulsed_watering | |
format: "%s: Use watering pulse with ON: %0.0fs - OFF: %0.0fs - Total: %0.0fs" | |
args: | |
- relay_id.c_str() | |
- pulse_time_on | |
- pulse_time_off | |
- total_time | |
- lambda: "id(elapsed_watering_time) = 0;" | |
- while: | |
condition: | |
- lambda: "return ( total_time * 1000 ) > id(elapsed_watering_time);" | |
then: | |
- script.execute: | |
id: set_relay_state | |
relay_id: !lambda "return relay_id;" | |
state: "ON" | |
use_internal: true | |
- delay: !lambda "return pulse_time_on * 1000;" | |
- script.execute: | |
id: set_relay_state | |
relay_id: !lambda "return relay_id;" | |
state: "OFF" | |
use_internal: true | |
- lambda: "id(elapsed_watering_time) += pulse_time_on * 1000;" | |
- if: | |
condition: | |
lambda: "return total_time * 1000 > id(elapsed_watering_time);" | |
then: | |
- delay: !lambda "return pulse_time_off * 1000;" | |
- logger.log: | |
level: INFO | |
tag: pulsed_watering | |
format: "%s: Total watering time reached." | |
args: | |
- relay_id.c_str() | |
- script.execute: | |
id: set_relay_state | |
relay_id: !lambda "return relay_id;" | |
state: "OFF" | |
use_internal: true | |
- id: continous_watering | |
mode: single | |
parameters: | |
relay_id: string | |
total_time: float | |
then: | |
- logger.log: | |
level: INFO | |
tag: continous_watering | |
format: "%s: Use continous watering for %0.0fs" | |
args: | |
- relay_id.c_str() | |
- total_time | |
- script.execute: | |
id: set_relay_state | |
relay_id: !lambda "return relay_id;" | |
state: "ON" | |
use_internal: true | |
- delay: !lambda "return total_time * 1000;" | |
- logger.log: | |
level: INFO | |
tag: continous_watering | |
format: "%s: Total watering time reached." | |
args: | |
- relay_id.c_str() | |
- script.execute: | |
id: set_relay_state | |
relay_id: !lambda "return relay_id;" | |
state: "OFF" | |
use_internal: true | |
- id: stop_watering_scripts | |
mode: single | |
then: | |
- script.stop: watering_process | |
- script.stop: pulsed_watering | |
- script.stop: continous_watering | |
button: | |
- platform: restart | |
name: "Restart" | |
- platform: shutdown | |
name: "Shutdown" | |
time: | |
- platform: sntp | |
id: esptime | |
servers: | |
- 0.pool.ntp.org | |
- 1.pool.ntp.org | |
- 2.pool.ntp.org |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment