Last active
May 9, 2025 23:41
-
-
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: "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_max_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: "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: 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: -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!🌛" | |
safe_mode: | |
disabled: true | |
logger: | |
level: INFO | |
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: 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: 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" | |
#web_server: | |
# port: 80 | |
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);" | |
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");' | |
- lambda: 'return id(plant2_status) == std::string("ok") || id(plant2_status) == std::string("watered") || id(plant2_status) == std::string("recently_watered");' | |
- lambda: 'return id(plant3_status) == std::string("ok") || id(plant3_status) == std::string("watered") || id(plant3_status) == std::string("recently_watered");' | |
- lambda: 'return id(plant4_status) == std::string("ok") || id(plant4_status) == std::string("watered") || id(plant4_status) == std::string("recently_watered");' | |
- 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: 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: 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 | |
- logger.log: | |
level: DEBUG | |
format: "Wasserstand OK" | |
on_press: | |
then: | |
- script.execute: led_status_light | |
- logger.log: | |
level: DEBUG | |
format: "Wasserstand niedrig" | |
- script.execute: | |
id: set_relay_state | |
relay_id: "any" | |
state: "OFF" | |
use_internal: false | |
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: | |
- logger.log: | |
level: DEBUG | |
format: "Plant 1: 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: !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: | |
- 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: xiaomi_hhccjcy01 | |
mac_address: "C4:7C:8D:6B:A8:3C" | |
moisture: | |
name: "Plant 2 Soil Moisture" | |
id: plant2_sensor_moisture | |
on_value: | |
then: | |
- logger.log: | |
level: INFO | |
format: "Plant 2: Moisture received: %.1f" | |
args: [x] | |
- 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: | |
- logger.log: | |
level: INFO | |
format: "Plant 2: Soil moisture OK — no watering needed." | |
- 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: | |
- logger.log: | |
level: INFO | |
format: "Plant 3: Moisture received: %.1f" | |
args: [x] | |
- 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: | |
- logger.log: | |
level: INFO | |
format: "Plant 3: Soil moisture OK — no watering needed." | |
- 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: | |
- logger.log: | |
level: INFO | |
format: "Plant 4: Moisture received: %.1f" | |
args: [x] | |
- 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: | |
- logger.log: | |
level: INFO | |
format: "Plant 4: Soil moisture OK — no watering needed." | |
- globals.set: | |
id: plant4_status | |
value: '"ok"' | |
- component.update: plant4_status_sensor | |
- platform: template | |
name: "Wakeup Cause" | |
accuracy_decimals: 0 | |
entity_category: diagnostic | |
lambda: return esp_sleep_get_wakeup_cause(); | |
- platform: template | |
name: "Plant 1 Last Watered Timestamp" | |
id: plant1_last_watered_timestamp_sensor | |
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 | |
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 | |
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); | |
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 | |
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 | |
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); | |
} | |
- platform: template | |
name: "Plant 2 Last Watered" | |
id: plant2_last_watered_text_sensor | |
entity_category: diagnostic | |
update_interval: never | |
icon: "mdi:flower-pollen" | |
lambda: |- | |
if (id(plant2_last_watered) == 0) { | |
return std::string("Never"); | |
} else { | |
time_t last_time = id(plant2_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); | |
} | |
- platform: template | |
name: "Plant 3 Last Watered" | |
id: plant3_last_watered_text_sensor | |
entity_category: diagnostic | |
update_interval: never | |
icon: "mdi:flower-pollen" | |
lambda: |- | |
if (id(plant3_last_watered) == 0) { | |
return std::string("Never"); | |
} else { | |
time_t last_time = id(plant3_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); | |
} | |
- platform: template | |
name: "Plant 4 Last Watered" | |
id: plant4_last_watered_text_sensor | |
entity_category: diagnostic | |
update_interval: never | |
icon: "mdi:flower-pollen" | |
lambda: |- | |
if (id(plant4_last_watered) == 0) { | |
return std::string("Never"); | |
} else { | |
time_t last_time = id(plant4_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); | |
} | |
switch: | |
- platform: template | |
name: "Relay 1" | |
id: relay1 | |
lambda: "return id(relay1_internal).state;" | |
icon: "mdi:water-pump" | |
turn_on_action: | |
- if: | |
condition: | |
and: | |
- binary_sensor.is_off: water_tank_empty | |
- lambda: "return id(has_booted);" | |
then: | |
- switch.turn_on: external_modules | |
- switch.turn_on: relay1_internal | |
- script.execute: | |
id: watering_process | |
relay_id: "relay1" | |
else: | |
- logger.log: | |
level: WARN | |
format: "Blocking relay1 activation!" | |
turn_off_action: | |
- script.stop: watering_process | |
- switch.turn_off: relay1_internal | |
- if: | |
condition: | |
- lambda: "return id(has_booted);" | |
then: | |
- script.execute: | |
id: update_plants_state | |
relay_id: "relay1" | |
newState: "watered" | |
update_timestamp: true | |
- platform: template | |
name: "Relay 2" | |
id: relay2 | |
lambda: "return id(relay2_internal).state;" | |
icon: "mdi:water-pump" | |
turn_on_action: | |
- if: | |
condition: | |
and: | |
- binary_sensor.is_off: water_tank_empty | |
- lambda: "return id(has_booted);" | |
then: | |
- switch.turn_on: external_modules | |
- switch.turn_on: relay2_internal | |
- script.execute: | |
id: watering_process | |
relay_id: "relay2" | |
else: | |
- logger.log: | |
level: WARN | |
format: "Blocking relay2 activation!" | |
turn_off_action: | |
- switch.turn_off: relay2_internal | |
- script.stop: watering_process | |
- if: | |
condition: | |
- lambda: "return id(has_booted);" | |
then: | |
- script.execute: | |
id: update_plants_state | |
relay_id: "relay2" | |
newState: "watered" | |
update_timestamp: true | |
- platform: template | |
name: "Relay 3" | |
id: relay3 | |
lambda: "return id(relay3_internal).state;" | |
icon: "mdi:water-pump" | |
turn_on_action: | |
- if: | |
condition: | |
and: | |
- binary_sensor.is_off: water_tank_empty | |
- lambda: "return id(has_booted);" | |
then: | |
- switch.turn_on: external_modules | |
- switch.turn_on: relay3_internal | |
- script.execute: | |
id: watering_process | |
relay_id: "relay3" | |
else: | |
- logger.log: | |
level: WARN | |
format: "Blocking relay3 activation!" | |
turn_off_action: | |
- switch.turn_off: relay3_internal | |
- script.stop: watering_process | |
- if: | |
condition: | |
- lambda: "return id(has_booted);" | |
then: | |
- script.execute: | |
id: update_plants_state | |
relay_id: "relay3" | |
newState: "watered" | |
update_timestamp: true | |
- platform: gpio | |
name: "Relay 1 (Internal)" | |
id: relay1_internal | |
restore_mode: ALWAYS_OFF | |
pin: GPIO4 | |
inverted: true | |
interlock: [relay2_internal, relay3_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] | |
internal: true | |
- platform: gpio | |
name: "Relay 3 (internal)" | |
id: relay3_internal | |
restore_mode: ALWAYS_OFF | |
pin: GPIO6 | |
inverted: true | |
interlock: [relay1_internal, relay2_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: "any" | |
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: | |
- 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 | |
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: 90 | |
step: 1 | |
optimistic: true | |
mode: box | |
initial_value: ${initial_max_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: "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 | |
select: | |
- platform: template | |
name: "Plant-1" | |
id: plant1_relay | |
options: | |
- "relay1" | |
- "relay2" | |
- "relay3" | |
entity_category: config | |
initial_option: "relay1" | |
restore_value: yes | |
optimistic: true | |
- platform: template | |
name: "Plant-2" | |
id: plant2_relay | |
options: | |
- "relay1" | |
- "relay2" | |
- "relay3" | |
entity_category: config | |
initial_option: "relay1" | |
restore_value: yes | |
optimistic: true | |
- platform: template | |
name: "Plant-3" | |
id: plant3_relay | |
options: | |
- "relay1" | |
- "relay2" | |
- "relay3" | |
entity_category: config | |
initial_option: "relay1" | |
restore_value: yes | |
optimistic: true | |
- platform: template | |
name: "Plant-4" | |
id: plant4_relay | |
options: | |
- "relay1" | |
- "relay2" | |
- "relay3" | |
entity_category: config | |
initial_option: "relay2" | |
restore_value: yes | |
optimistic: true | |
api: | |
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: plant1_last_watered_text_sensor | |
- component.update: plant2_last_watered_text_sensor | |
- component.update: plant3_last_watered_text_sensor | |
- component.update: plant4_last_watered_text_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::string state_lc = state; | |
std::transform(state_lc.begin(), state_lc.end(), state_lc.begin(), ::tolower); | |
bool handled = false; | |
// Relay 1 | |
if (relay_id == "relay1") { | |
handled = true; | |
if (use_internal) { | |
if (state_lc == "on" && !id(relay1_internal).state) { | |
id(relay1_internal).turn_on(); | |
} else if (state_lc == "off" && id(relay1_internal).state) { | |
id(relay1_internal).turn_off(); | |
} | |
} else { | |
if (state_lc == "on" && !id(relay1).state) { | |
id(relay1).turn_on(); | |
} else if (state_lc == "off" && id(relay1).state) { | |
id(relay1).turn_off(); | |
} | |
} | |
} | |
// Relay 2 | |
else if (relay_id == "relay2") { | |
handled = true; | |
if (use_internal) { | |
if (state_lc == "on" && !id(relay2_internal).state) { | |
id(relay2_internal).turn_on(); | |
} else if (state_lc == "off" && id(relay2_internal).state) { | |
id(relay2_internal).turn_off(); | |
} | |
} else { | |
if (state_lc == "on" && !id(relay2).state) { | |
id(relay2).turn_on(); | |
} else if (state_lc == "off" && id(relay2).state) { | |
id(relay2).turn_off(); | |
} | |
} | |
} | |
// Relay 3 | |
else if (relay_id == "relay3") { | |
handled = true; | |
if (use_internal) { | |
if (state_lc == "on" && !id(relay3_internal).state) { | |
id(relay3_internal).turn_on(); | |
} else if (state_lc == "off" && id(relay3_internal).state) { | |
id(relay3_internal).turn_off(); | |
} | |
} else { | |
if (state_lc == "on" && !id(relay3).state) { | |
id(relay3).turn_on(); | |
} else if (state_lc == "off" && id(relay3).state) { | |
id(relay3).turn_off(); | |
} | |
} | |
} | |
// "any" | |
else if (relay_id == "any") { | |
handled = true; | |
if (use_internal) { | |
if (state_lc == "on") { | |
if (!id(relay1_internal).state) id(relay1_internal).turn_on(); | |
if (!id(relay2_internal).state) id(relay2_internal).turn_on(); | |
if (!id(relay3_internal).state) id(relay3_internal).turn_on(); | |
} else if (state_lc == "off") { | |
if (id(relay1_internal).state) id(relay1_internal).turn_off(); | |
if (id(relay2_internal).state) id(relay2_internal).turn_off(); | |
if (id(relay3_internal).state) id(relay3_internal).turn_off(); | |
} | |
} else { | |
if (state_lc == "on") { | |
if (!id(relay1).state) id(relay1).turn_on(); | |
if (!id(relay2).state) id(relay2).turn_on(); | |
if (!id(relay3).state) id(relay3).turn_on(); | |
} else if (state_lc == "off") { | |
if (id(relay1).state) id(relay1).turn_off(); | |
if (id(relay2).state) id(relay2).turn_off(); | |
if (id(relay3).state) id(relay3).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: | |
- 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 -> Cyab | |
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(); | |
- id: update_plants_state | |
mode: queued | |
parameters: | |
relay_id: string | |
newState: string | |
update_timestamp: boolean | |
then: | |
- lambda: |- | |
std::string plant1RelayId = id(plant1_relay).state; | |
std::string plant2RelayId = id(plant2_relay).state; | |
std::string plant3RelayId = id(plant3_relay).state; | |
std::string plant4RelayId = id(plant4_relay).state; | |
time_t now = id(esptime).now().timestamp; | |
boolean foundAMatch = false; | |
if (plant1RelayId == relay_id){ | |
foundAMatch = true; | |
id(plant1_status) = newState; | |
id(plant1_status_sensor).update(); | |
if (update_timestamp){ | |
id(plant1_last_watered) = now; | |
id(plant1_last_watered_text_sensor).update(); | |
id(plant1_last_watered_timestamp_sensor).update(); | |
} | |
} | |
if (plant2RelayId == relay_id){ | |
foundAMatch = true; | |
id(plant2_status) = newState; | |
id(plant2_status_sensor).update(); | |
if (update_timestamp){ | |
id(plant2_last_watered) = now; | |
id(plant2_last_watered_text_sensor).update(); | |
id(plant2_last_watered_timestamp_sensor).update(); | |
} | |
} | |
if (plant3RelayId == relay_id){ | |
foundAMatch = true; | |
id(plant3_status) = newState; | |
id(plant3_status_sensor).update(); | |
if (update_timestamp){ | |
id(plant3_last_watered) = now; | |
id(plant3_last_watered_text_sensor).update(); | |
id(plant3_last_watered_timestamp_sensor).update(); | |
} | |
} | |
if (plant4RelayId == relay_id){ | |
foundAMatch = true; | |
id(plant4_status) = newState; | |
id(plant4_status_sensor).update(); | |
if (update_timestamp){ | |
id(plant4_last_watered) = now; | |
id(plant4_last_watered_text_sensor).update(); | |
id(plant4_last_watered_timestamp_sensor).update(); | |
} | |
} | |
if (!foundAMatch){ | |
ESP_LOGW("update_plants_state", "No Plant configured for: %s", relay_id.c_str()); | |
} | |
- id: schedule_watering | |
parameters: | |
relay_id: string | |
mode: queued | |
max_runs: 4 | |
then: | |
- logger.log: | |
level: INFO | |
tag: schedule_watering | |
format: "%s: Schedule watering process" | |
args: | |
- relay_id.c_str() | |
- logger.log: | |
level: INFO | |
tag: schedule_watering | |
format: "%s: Wait for system has booted" | |
args: | |
- relay_id.c_str() | |
- wait_until: | |
- lambda: "return id(has_booted) = true;" | |
- logger.log: | |
level: INFO | |
tag: schedule_watering | |
format: "%s: Wait for all relays to finish" | |
args: | |
- relay_id.c_str() | |
- wait_until: | |
condition: | |
and: | |
- switch.is_off: relay1 | |
- switch.is_off: relay2 | |
- switch.is_off: relay3 | |
- logger.log: | |
level: INFO | |
tag: schedule_watering | |
format: "%s: All other relays are off, proceeding with watering process" | |
args: | |
- relay_id.c_str() | |
- if: | |
condition: | |
lambda: |- | |
time_t now = id(esptime).now().timestamp; | |
int rewateringWaitTime = id(rewatering_wait_time_minutes).state * 60; | |
time_t last_watered = 0; | |
std::vector<std::string> plant_relays = { | |
id(plant1_relay).state, | |
id(plant2_relay).state, | |
id(plant3_relay).state, | |
id(plant4_relay).state | |
}; | |
std::vector<time_t> plant_last_watered = { | |
static_cast<time_t>(id(plant1_last_watered)), | |
static_cast<time_t>(id(plant2_last_watered)), | |
static_cast<time_t>(id(plant3_last_watered)), | |
static_cast<time_t>(id(plant4_last_watered)) | |
}; | |
for (size_t i = 0; i < plant_relays.size(); i++) { | |
if (plant_relays[i] == relay_id && plant_last_watered[i] > last_watered) { | |
last_watered = plant_last_watered[i]; | |
} | |
} | |
return (now - last_watered) >= rewateringWaitTime; | |
then: | |
- logger.log: | |
level: INFO | |
tag: schedule_watering | |
format: "%s: Wait for water tank" | |
args: | |
- relay_id.c_str() | |
- wait_until: | |
binary_sensor.is_off: water_tank_empty | |
- logger.log: | |
level: INFO | |
tag: schedule_watering | |
format: "%s: Water tank ready" | |
args: | |
- relay_id.c_str() | |
- 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 | |
parameters: | |
relay_id: string | |
mode: single | |
then: | |
- logger.log: | |
level: INFO | |
tag: watering_process | |
format: "Start watering using: %s" | |
args: | |
- relay_id.c_str() | |
- script.execute: | |
id: update_plants_state | |
relay_id: !lambda "return relay_id;" | |
newState: "watering" | |
update_timestamp: false | |
- 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: | |
- script.execute: | |
id: set_relay_state | |
relay_id: !lambda "return relay_id;" | |
state: "ON" | |
use_internal: true | |
- delay: !lambda "return id(watering_pulse_time_on).state * 1000;" | |
- script.execute: | |
id: set_relay_state | |
relay_id: !lambda "return relay_id;" | |
state: "OFF" | |
use_internal: true | |
- lambda: |- | |
id(elapsed_watering_time) += ( id(watering_pulse_time_on).state * 1000 ); | |
- 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 | |
- script.execute: | |
id: set_relay_state | |
relay_id: !lambda "return relay_id;" | |
state: "ON" | |
use_internal: true | |
- delay: !lambda "return id(total_watering_time).state * 1000;" | |
- logger.log: | |
level: INFO | |
tag: watering_process | |
format: "Total watering time reached." | |
- script.execute: | |
id: set_relay_state | |
relay_id: !lambda "return relay_id;" | |
state: "OFF" | |
use_internal: false | |
ota: | |
on_begin: | |
then: | |
- logger.log: "OTA started — prevent deep-sleep!" | |
- deep_sleep.prevent: sleep_control | |
on_end: | |
then: | |
- logger.log: "OTA ended — allow deep-sleep!" | |
- deep_sleep.allow: sleep_control | |
on_error: | |
then: | |
- logger.log: "OTA failed — allow deep-sleep!" | |
- deep_sleep.allow: sleep_control | |
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