Solar Powered Self-watering Plant (no plumbing, no power) [WIP]

I like plants but I kill them. I want some on my apartment balcony, which has no plumbing and no power. Therefore, I’m having a crack at my first solar powered esp32 project with water reservoir
I’ll drop my progress here as I go along and refine a few posts as I go.


Key progress:

Draft System diagram:

  1. The mobile base with conceal water reservoir has been built.
  2. Work-in-progress ESPHome config is here.
  3. I’m tinkering with irrigation rings

I’m considering adding a light sensor and water level monitoring (ToF).


Various resources (parts, similar projects)

Power manager’s I’m looking at:

  1. Sunflower: Solar Power Manager 5V (current favourite)
  2. TP4056
  3. Solar Power Management Module

Solar Panels:

Moisture sensor:
https://www.aliexpress.us/item/2251832646223934.html?spm=a2g0o.order_list.0.0.32021802daZKsE&gatewayAdapt=glo2usa4itemAdapt&_randl_shipto=US

Light sensor:

8 Likes

I’ve built the base with the concealed 20lt water reservoir. I went with treated pine sleepers, which my local hardware store cut to size for me. Decking screws to hold it together. This side will face the wall, as a balance between asthetics and accesibility. I’ve got the castors on…

Will update a work-in progress config and high level logic in this post periodically:

Main features/logic:

Done:

  1. Deep sleep at night
  2. Sleep wake/on interval during day
  3. Go back to sleep when data has been sent
  4. “Stay awake” swtiches in HA so next time node wakes up it will stay awake (for updates.OTA).
  5. Multi-sample sensors that have variability/spikes

To do:

  1. Optimise pump speed to feed “drip watering” style system.
  2. Pump control based on moisture, water tank, and battery level.
  3. Consolidate and minimise code once all testing is done (optimise for energy)
substitutions:
  devicename: irrigation-balcony-quinled
  friendly_name: Bedroom Balcony Irrigation
  device_description: Bedroom Balcony Irrigation
  
  #I find it easier to manage some settings up here.
  sleep_time: 10min
  auto_wake_time: 30s
  pump_run_time: 10s 

esp32:
  board: mhetesp32devkit  
esphome:
  name: $devicename
  comment: ${device_description}
  on_boot:
    - priority: 900
      then:
        - lambda: |-
            Wire.begin(); 
        #^^Seems to solve bug with BH1750 not waking up from deep sleep. https://community.home-assistant.io/t/bh1750-no-communication-after-switching-it-off-and-on-again/412282/3?u=mahko_mahko
    - priority: -100
      then:
      #Sensor updates are turned off for most sensors and just manually requested on boot. Then the ESP goes back to sleep when they're done (unless told to stay 
        #Reset sensor update counters. These are for debugging.
        - lambda: id(count_irrigation_lux).publish_state(0);
        - lambda: id(count_batt_voltage).publish_state(0);
        - delay: 1s
        - logger.log: "....Starting sensor updates"
        - repeat:
            #Request sensor updates
            count: 5
            then:
              - component.update: batt_voltage #for battery level
              - delay: 100ms
              - component.update: soil_moisture_voltage #for moisture level
              - delay: 100ms
              - component.update: tof #for water tank level
              - delay: 100ms
              - component.update: irrigation_lux #for light level
              - delay: 100ms
            #and again for ADC based sensors which benefit from more samples.
              - component.update: batt_voltage
              - delay: 250ms
              - component.update: soil_moisture_voltage
              - delay: 250ms

  on_shutdown:
    #Turn off 5v peripheral power. It will retore as on when it wakes.
    priority: -100
    then:
      - logger.log: "Turning off peripheral power..."
      - switch.turn_off: power_peripherals
    #Turn off the "Fresh data recieved sensors"
      - binary_sensor.template.publish:
          id: water_tank_level_recieved
          state: OFF
      - binary_sensor.template.publish:
          id: irrigation_lux_recieved
          state: OFF
      - binary_sensor.template.publish:
          id: batt_level_recieved
          state: OFF
      - binary_sensor.template.publish:
          id: solar_plant_moisture_level_recieved
          state: OFF
      - binary_sensor.template.publish:
          id: all_updates_recieved
          state: OFF
    
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  fast_connect: true
  manual_ip:
      static_ip: 192.168.1.xxx
      gateway: 192.168.1.xxx
      subnet: 255.255.255.0

api:
ota:

logger:
  # level: VERBOSE
  # baud_rate: 0 
  
i2c:
#Shared by multiple devices (wires hopped)
  sda: GPIO25 #data > green wire
  scl: GPIO27 #clock > blue wire
  scan: true 
  
text_sensor:
# Reports the ESPHome Version with compile date
  - platform: version
    name: ${friendly_name} ESPHome Version

time:
  - platform: homeassistant
    id: esptime
    on_time:
    #Deep sleep at 8pm and wake up at 5am. No real need to measure overnight.
      - hours: 20
        then:
          - deep_sleep.enter:
              id: deep_sleep_1
              until: "05:00:00"
              time_id: esptime

deep_sleep:
  id: deep_sleep_1
  run_duration: ${auto_wake_time}
  sleep_duration: ${sleep_time}

output:
##################################################
#Pump
##################################################
#5v pump attached to a l298n. This can control pump speed via pwm.
#Pump on/off
- platform: gpio
  pin: GPIO18
  id: pump_on
#Pump Speed.
- platform: ledc 
  pin: GPIO26
  id: pump_speed

button: 
##################################################
#Pump Control
##################################################
#Run pump for fixed time interval
  - platform: template
    name: Run Irrigation Pump For Fixed Time
    id: run_pump_for_Interval
    on_press:
      then:
        - logger.log: "Running Irrigation Pump For Fixed Time"
        - output.turn_on: pump_on
        #Set speed
        - output.set_level:
            id: pump_speed
            level: 50%
        - output.ledc.set_frequency:
            id: pump_speed
            frequency: 500Hz
        - delay: ${pump_run_time}
        - output.turn_off: pump_on
        - output.turn_off: pump_speed
#Stop Pump
  - platform: template
    name: Stop Irrigation Pump
    id: stop_irrigation_pump
    on_press:
      then:
        - output.turn_off: pump_on
        - output.turn_off: pump_speed
        - logger.log: "Stopping Pump"

##################################################
#Deep Sleep
##################################################
#Prevent Deep Sleep via HA button
  - platform: template
    name: Prevent Deep Sleep
    id: prevent_deep_sleep
    on_press:
      then:
        - logger.log: "Deep Sleep PREVENTED via remote button push."
        - deep_sleep.prevent: deep_sleep_1
        
#Allow Deep Sleep
  - platform: template
    name: Allow Deep Sleep
    id: allow_deep_sleep
    on_press:
      then:
        - logger.log: "Deep Sleep ALLOWED via remote button push."
        - deep_sleep.allow: deep_sleep_1

#Sleep if allowed.
  - platform: template
    name: Sleep If Allowed
    id: sleep_if_allowed
    on_press:
      then:
         - if:
            condition:
              or:
                - binary_sensor.is_on: remote_defeat
                - binary_sensor.is_on: defeat
            then:
              - logger.log: "Sleep requested but STAY AWAKE is on. Skipping sleep."
              - deep_sleep.prevent: deep_sleep_1
            else:
              - logger.log: "Sleep requested and ALLOWED. Going to sleep..."     
              - deep_sleep.enter:
                  id: deep_sleep_1
                  sleep_duration: ${sleep_time}
                  
#Force Deep Sleep
  - platform: template
    name: Force Deep Sleep
    id: force_deep_sleep
    on_press:
      then:
        - logger.log: "FORCE SLEEP requested...Going to sleep"     
        - deep_sleep.enter:
            id: deep_sleep_1
            sleep_duration: ${sleep_time}


binary_sensor:
##################################################
#Data Update Sensors. These were useful during dev. Might remove in prod.
#They are all updated via other sensors.
##################################################
  #Moisture
  - platform: template
    name: ${friendly_name} moisture updated
    id: solar_plant_moisture_level_recieved
  #Battery Level
  - platform: template
    name: ${friendly_name} battery level updated
    id: batt_level_recieved
  #Light
  - platform: template
    name: ${friendly_name} lux updated
    id: irrigation_lux_recieved
  #Water level
  - platform: template
    name: ${friendly_name} water level updated
    id: water_tank_level_recieved
  #All
  #Once all updates are recieved, then chack if you can go back to sleep.
  - platform: template
    name: ${friendly_name} all updates recieved
    id: all_updates_recieved
    lambda: |-
      return
      id(solar_plant_moisture_level_recieved).state &&
      id(batt_level_recieved).state &&
      id(irrigation_lux_recieved).state &&
      id(water_tank_level_recieved).state
      ;
    on_press:
    #Sleep esp if all data recieved 
      then:
        - logger.log:
            format: "All sensors updated after %.1f seconds of uptime. Checking if sleep is allowed"
            args: [ 'id(uptime_sec).state']
        # - delay: 1s
        - button.press: sleep_if_allowed

##################################################
#Deep sleep control
##################################################
#A lot of my logic is remashes of:
  #https://www.wirewd.com/make/blog/esphome_sleep_modes
  #https://tatham.blog/2021/02/06/esphome-batteries-deep-sleep-and-over-the-air-updates/
#Some of it still needs a clean up.

#HA Deep sleep control
  - platform: homeassistant
    name: "Remote Defeat Sleep"
    internal: True
    id: "remote_defeat"
    entity_id: input_boolean.defeat_sleep
    on_press:
      then:
        - logger.log: "remote press defeat"
        - deep_sleep.prevent: deep_sleep_1
        
 #HA Deep sleep control (not currently used)
  - platform: gpio
    name: "Defeat"
    id: "defeat"
    internal: True
    pin:
      number: 4
      mode: INPUT_PULLUP
      inverted: True
    on_press:
      then:
        - logger.log: "press defeat"
        - deep_sleep.prevent: deep_sleep_1

switch:
##################################################
#Control peripheral power (on solar power manager)
##################################################
  - platform: gpio
    pin:
      number: 17
    name: ${friendly_name} Power Peripherals
    id: power_peripherals
    restore_mode: ALWAYS_ON #This turns on the power on boot/wake in time for sensor set-up.


sensor:
#Uptime sensor
  - platform: uptime
    id: uptime_sec
    name: ${friendly_name} Uptime Sensor
    update_interval: 2s
    accuracy_decimals: 1
    unit_of_measurement: s

##################################################
#For counting data updates recieved for each wake cycle.
##################################################
  - platform: template
    name: "Count Lux Updates"
    id: count_irrigation_lux
    unit_of_measurement: count
  - platform: template
    name: "Count Batt V Updates"
    id: count_batt_voltage
    unit_of_measurement: count
    
##########################################################################################
# Time of Flight sensor  - i2c
##########################################################################################
#Powered via 5v
  - platform: vl53l0x
    id: tof
    name: ${friendly_name} ToF
    internal: false 
    address: 0x29
    update_interval: never
    # enable_pin: GPIO17 #Did not work: https://github.com/esphome/issues/issues/3644
    accuracy_decimals: 1
    unit_of_measurement: 'cm'
    filters:
    #Convert to cm
      - multiply: 100 
    #Then use moving median to smooth noise.  Sample 5 points then send.
      - median:
          window_size: 5
          send_every: 5
          send_first_at: 5
      
#Convert the ToF distance to a water tank level (percent full)
  - platform: copy
    source_id: tof
    id: water_tank_level
    # icon: "mdi:battery"
    name: ${friendly_name} Water Level
    unit_of_measurement: '%'
    accuracy_decimals: 0
    filters:
      # Map from distance to % full. To calibrate.
      - calibrate_linear:
          - 3 -> 100 
          - 20 -> 0
      # #Overide values less than 0% and more than 100%
      # - lambda: |
          # if (x < 3) return 100; 
          # else if (x > 20) return 0;
          # else return ceil(x / 0.5) * 0.5;
    on_value:
      then:
       - binary_sensor.template.publish:
          id: water_tank_level_recieved
          state: ON

# # ##########################################################################################
# # # bh1750 Lux/light sensor
# # ##########################################################################################     
  - platform: bh1750
    id: irrigation_lux
    name: ${friendly_name} Illuminance
    address: 0x23
    update_interval: never
    filters:
    #Use moving median to smooth noise. Sample 5 points then send.
      - median:
          window_size: 5
          send_every: 5
          send_first_at: 5
    on_value:
      then:
       - binary_sensor.template.publish:
          id: irrigation_lux_recieved
          state: ON
    on_raw_value:
      then:
      #Sensor update counter.
        - lambda: id(count_irrigation_lux).publish_state(id(count_irrigation_lux).state +1);

#Notes:
#Voltage divider: Used 2 x 300K Ohm resistors
  - platform: adc
    id: batt_voltage
    name: ${friendly_name} Battery Voltage
    internal: false
    pin: GPIO33 #ADC1
    update_interval: never
    accuracy_decimals: 3
    attenuation: auto
    filters:
      # #The scale it back up from voltage divided value 2 x 300K > 2.1. 4.2/2.1 = 2.
      - multiply: 2
    on_raw_value:
      then:
      #Sensor update counter.
        - lambda: id(count_batt_voltage).publish_state(id(count_batt_voltage).state +1);
      
#Intermediate sensor. Might consolidate them later.
  - platform: copy
    source_id: batt_voltage
    id: batt_voltage_filtered
    icon: "mdi:battery"
    internal: false
    name: ${friendly_name} Battery Voltage Filtered  
    unit_of_measurement: V
    accuracy_decimals: 3
    filters:
    #Use moving median to deal with noise.
      - median:
          window_size: 10
          send_every: 10
          send_first_at: 10
       
#Convert the Voltage to a battery  level (%)
  - platform: copy
    source_id: batt_voltage_filtered
    id: batt_level
    internal: false 
    icon: "mdi:battery"
    name: ${friendly_name} Battery Level
    unit_of_measurement: '%'
    accuracy_decimals: 1
    filters:
      # Map from voltage to Battery level
      - calibrate_linear:
          - 3.0 -> 0 #Set 3.0 to 0% even though it can go lower (2.4V), for life extention. There's not much capacity below this anyway.
          - 4.05 -> 100 #Set 4.05 to 100% even though it can go higher (~4.2V), for life extention.
       
      #Overide values less than 0% and more than 100%
      - lambda: |
          if (x < 0) return 0; 
          else if (x > 100) return 100;
          else return ceil(x / 5) * 5;
    on_value:
      then:
       - binary_sensor.template.publish:
          id: batt_level_recieved
          state: ON
        
#Capacitive soil moisture sensor: https://www.aliexpress.com/item/32832538686.html?spm=a2g0o.order_list.0.0.55771802WgNqEA
#Voltage of the Capacitive soil moisture sensor
  - platform: adc
    pin: GPIO34
    name: ${friendly_name} Soil Moisture Volts
    id: soil_moisture_voltage
    internal: false 
    accuracy_decimals: 1
    update_interval: never
    attenuation: auto
    filters:
    #Use moving median to deal with noise.
      - median:
          window_size: 10
          send_every: 10
          send_first_at: 10

#Convert the Voltage to a moisture  level (%)
  - platform: copy
    source_id: soil_moisture_voltage
    id: solar_plant_moisture_level
    name: ${friendly_name} Moisture Level
    internal: false     
    icon: "mdi:battery"
    unit_of_measurement: '%'
    accuracy_decimals: 1
    filters:
      #max and min values taken from testing with glass of water. Prob need to do more in situ tests.
      - calibrate_linear:
          - 1.66 -> 100.0
          - 2.90 -> 0.0
      #Handle/cap boundaries
      - lambda: |
          if (x < 0) return 0; 
          else if (x > 100) return 100;
          else return (x);
    on_value:
      then:
       - binary_sensor.template.publish:
          id: solar_plant_moisture_level_recieved
          state: ON  

I got interested in “irrigation rings” to disperse water more evenly so am tineking with some 3d printable designs based on what I’ve seen online. Doubt they’ll hold up super well in the long term, but let’s see…

image

1 Like

List of related projects:

Some more progress pics.

Main control box with solar panel.

How it assembles.

Getting there…Almost ready for an actual plant;)

PLA enclosure warped in the sun and the hot glue goes way too soft too.

Back to the drawing board for an enclosure;)

Gonna be a bit painful as I’m really glued in lol… Oh well, lesson learnt. All obvious in hindsight.

Progress pics for version 2 enclosure.

For version 2 of the enclosure, I’m going with the “Andreas Spiess Ikea approach”

• But I actually got a similar one from my local Kmart.
https://www.kmart.com.au/product/14-litre-glass-food-storage-container-42567158/

Here’s some pics.

Dear @Mahko_Mahko
Very nice post.
I was just testing you ESP Code. It is very nice!!!
I was wondering why you are using “input_boolean.defeat_sleep” with a helper and not a dedicated switch for this device?
Did you tried such a solution?
Regards
Adrian

Thanks!

I’ve refined the config more which I’ll repost soonish.

I was considering having both wake options.

The remote virtual switch doesn’t require any hardware.

A button/switch would need to be weatherproofed.

I may still add one.

I recall that a wake button is better than a wake switch. Something about it being hard to determine the true state of a switch on boot. Might be wrong though.

You’re welcome.

I was more thinking about a “remote” virtual switch on the device, and not created with a helper.
grafik
There where i have my switch Test.

Oh right I see.

The virtual switch can’t be on the device as when it is in deep sleep it will not track the state as changed in HA / retrieve the changed state when it wakes (I think).

I would prefer that but it doesn’t seem to work as you might want. Let me know if you find otherwise.

Till now, i couldn’t fine a solution.
Maybe someone else?
I will open a new Topic with this question.

1 Like

My master config.

I’ll maintain a copy of my config in this post here and update it periodically.

#Todo: 
#Determine night time battery loss.
#More testing
#Debug ToF.

substitutions:
  devicename: irrigation-balcony-quinled
  friendly_name: Planty
  device_description: Planty | Bedroom Balcony Irrigation
  
#I find it easier to manage some settings up here.
  sleep_time: 60min
  auto_wake_time: 30s
  pump_run_time: 2s
  internal_mode: "true"
 
esp32:
  board: mhetesp32devkit  
  #Quinled pinout: https://quinled.info/quinled-esp32-board-details/
esphome:
  name: $devicename
  comment: ${device_description}
  on_boot:
    # - priority: 900
      # then:
        # #Seems to solve bug with BH1750 not waking up from deep sleep. 
        # #https://community.home-assistant.io/t/bh1750-no-communication-after-switching-it-off-and-on-again/412282/3?u=mahko_mahko      
        # - lambda: |-
            # Wire.begin(); 
      
    # #Try this is sensors don't boot. https://community.home-assistant.io/t/add-sensor-delay-upon-power-on/158567/6?u=mahko_mahko
    # - priority: 200 
      # then:
        # - delay: 500ms
        
    - priority: -100
      then:
    #Sensor updates are turned off and manually requested on boot. 
    #Then the ESP goes back to sleep when they're done (unless told to stay awake)
    
        #Reset sensor update counters. These are for debugging.
        - lambda: id(count_irrigation_lux).publish_state(0);
        - lambda: id(count_batt_voltage).publish_state(0);
        - lambda: id(count_irrigation_tof).publish_state(0);
        - delay: 1s
        
        #Request sensor updates        
        - logger.log: "....Starting sensor updates"
        - repeat:
            count: 5 
            then:
              - component.update: batt_voltage #for battery level
              - delay: 100ms
              - component.update: soil_moisture_voltage #for moisture level
              - delay: 100ms
              - component.update: tof #for water tank level
              - delay: 100ms
              - component.update: irrigation_lux #for light level
              - delay: 100ms
              #second pass for ADC based sensors which benefit from more samples.
              - component.update: batt_voltage
              - delay: 250ms
              - component.update: soil_moisture_voltage
              - delay: 250ms

  on_shutdown:
    priority: -100
    then:
      - if:
          condition:
            - binary_sensor.is_on: all_updates_recieved           
          then:
            - logger.log: "Data updates ok..."
          else:
            - logger.log: "A sensor might be down...missing updates" 
            - binary_sensor.template.publish:
                id: data_update_problem
                state: ON
            - delay: 1s   
              
      - switch.turn_off: pump #Not really required I think...
      #Turn off 5v peripheral power. It will retore as on when it wakes.
      - logger.log: "Turning off peripheral power..."
      - switch.turn_off: power_peripherals
      #Turn off the "Fresh data recieved sensors"
      - binary_sensor.template.publish:
          id: water_tank_level_recieved
          state: OFF
      - binary_sensor.template.publish:
          id: irrigation_lux_recieved
          state: OFF
      - binary_sensor.template.publish:
          id: batt_level_recieved
          state: OFF
      - binary_sensor.template.publish:
          id: solar_plant_moisture_level_recieved
          state: OFF
      - binary_sensor.template.publish:
          id: all_updates_recieved
          state: OFF
          
    
wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  fast_connect: true
  manual_ip:
      static_ip: 192.168.1.xxx
      gateway: 192.168.1.z
      subnet: 255.255.255.0

api:
ota:

logger:
  # level: VERBOSE
  baud_rate: 0 
  
i2c:
#Shared by multiple devices (wires hopped)
  sda: GPIO25 #data > green wire
  scl: GPIO27 #clock > blue wire
  scan: true 

deep_sleep: 
#Auto deep sleep shouldn't actually get used if data updates are happening properly (should sleep sooner).
  id: deep_sleep_1
  run_duration: ${auto_wake_time}
  sleep_duration: ${sleep_time}
  
time:
  - platform: homeassistant
    id: esptime
    on_time:
      #Deep sleep at 8pm and wake up at 5am.
      - hours: 20
        then:
          #Publish battery level to the end of day sensor since it will be our last update.
          - lambda: return id(end_of_day_battery_percent).publish_state(id(batt_level).state);
          #Reset culmulative light sesnor reset
          - sensor.integration.reset: irrigation_cul_lux_hours
          - delay: 1s
          #Rest up
          - deep_sleep.enter:
              id: deep_sleep_1
              until: "05:00:00"
              time_id: esptime
              
text_sensor:
 # Reports the ESPHome Version with compile date
  - platform: version
    name: ${friendly_name} ESPHome Version
    
#Watering Mode. 
  #Watering regime allows plant to become quite dry and then get a good watering
  #as opposed to more frequent bang bang watering cycles.
  - platform: template
    id: watering_mode 
    name: "Watering Mode"
    icon: "mdi:water-pump"
    
number:

#Upper mositure target (once dry)
  - platform: template
    name: Moisture Upper Target ${friendly_name}  
    id: moisture_upper_target
    icon: mdi:arrow-collapse-up
    optimistic: true
    unit_of_measurement: '%'
    min_value: 20.0
    max_value: 50.0
    step: 1.0
    restore_value: true
    initial_value: 50.0
    
#Lower Water threshold (triggers watering regime). Need to establish good values.
  - platform: template
    name: Moisture Lower Threshold ${friendly_name}   
    id: moisture_lower_threshold  
    icon: mdi:arrow-collapse-down
    optimistic: true
    unit_of_measurement: '%'
    min_value: 1.0
    max_value: 20.0
    step: 1.0
    restore_value: true
    initial_value: 5.0
    
#Battery Saving Mode Threshold.
  - platform: template
    name: Battery Saving Threshold ${friendly_name}   
    id: battery_saving_mode_threshold
    icon: mdi:battery-alert
    optimistic: true
    unit_of_measurement: '%'
    min_value: 10.0
    max_value: 50.0
    step: 1.0
    restore_value: true
    initial_value: 30.0
    
#Low water tank threshold
  - platform: template
    name: Low tank water threshold ${friendly_name}   
    id: low_tank_water_threshold
    icon: mdi:water-remove-outline
    optimistic: true
    unit_of_measurement: '%'
    min_value: 0.0
    max_value: 20.0
    step: 1.0
    restore_value: true
    initial_value: 10.0

button: 
#Deep sleep if allowed.
  - platform: template
    name: Sleep If Allowed ${friendly_name}
    internal: ${internal_mode}
    id: sleep_if_allowed
    icon: "mdi:sleep"
    # internal: ${internal_toggle}
    on_press:
      then:
         - if:
            condition:
              - binary_sensor.is_on: stay_awake_sensor
            then:
              - logger.log: "Sleep requested but STAY AWAKE mode is on. Skipping sleep."
              - deep_sleep.prevent: deep_sleep_1
            else:
              - logger.log: "Sleep requested and ALLOWED. Going to sleep..."     
              - deep_sleep.enter:
                  id: deep_sleep_1
                  sleep_duration: ${sleep_time}
                  
#Force Deep Sleep
  - platform: template
    name:  Force Sleep ${friendly_name}
    id: force_deep_sleep
    icon: "mdi:bell-sleep"
    internal: ${internal_mode}
    on_press:
      then:
        - logger.log: "FORCE SLEEP requested...Going to sleep"     
        - deep_sleep.enter:
            id: deep_sleep_1
            sleep_duration: ${sleep_time}

#Long Sleep (part of battery saving mode)
  - platform: template
    name:  Long Sleep ${friendly_name}
    id: long_deep_sleep
    internal: ${internal_mode}
    icon: "mdi:bell-sleep"
    on_press:
      then:
        - logger.log: "LONG SLEEP requested...Going to sleep"
        #Publish battery level to the end of day sensor since it will be our last update.
        - lambda: return id(end_of_day_battery_percent).publish_state(id(batt_level).state);
        - deep_sleep.enter:
            id: deep_sleep_1
            # sleep_duration: 2min
            sleep_duration: 24h
        #Reset culmulative light sesnor reset
        - sensor.integration.reset: irrigation_cul_lux_hours

binary_sensor:
#Low tank water alert
  - platform: template
    id: tank_water_is_low
    name: Low Tank ${friendly_name}
    icon: mdi:water-remove-outline
    device_class: problem
    # entity_category: diagnostic
    #I *think* if we use an inverted logic like below then if sensor is unavailabe this should be in alert state, which is what I want. To test.
    lambda: |-
      return (id(solar_plant_moisture_level).state < id(low_tank_water_threshold).state);
    # filters:
      # - invert:
    
#Data updates alert. I've had the lux/tof playing up. Seems to be when battery < ~ 40%
  - platform: template
    id: data_update_problem
    name: Sensor Problem ${friendly_name}
    icon: mdi:database-alert-outline
    device_class: problem
    entity_category: diagnostic

#Low Battery alert    
  - platform: template
    id: battery_is_low
    name: Battery is Low ${friendly_name} 
    icon: mdi:battery-alert
    device_class: problem
    lambda: |-
      return (id(batt_level).state < id(battery_saving_mode_threshold).state);
    on_press:
      then:
        - button.press: long_deep_sleep #Long deep sleep if battery low.
        
#Pump Sensor.    
  - platform: template
    id: pump_is_on
    name: Pump is On ${friendly_name} 
    icon: mdi:water-pump
    device_class: running
    lambda: |-
      return id(pump).state;

##################################################
#Data Update Sensors. 
  #These were useful during dev. Might remove in prod.
  #All updated via other sensors.
##################################################
#Moisture
  - platform: template
    name: Moisture updated ${friendly_name}
    id: solar_plant_moisture_level_recieved
    internal: True
#Battery Level
  - platform: template
    name: Battery level updated ${friendly_name}
    id: batt_level_recieved
    internal: True
#Light
  - platform: template
    name: Lux updated ${friendly_name}
    id: irrigation_lux_recieved
    internal: True
#Water level
  - platform: template
    name: water level updated ${friendly_name}
    id: water_tank_level_recieved
    internal: True
#All
  - platform: template
    name: Data updates done ${friendly_name}
    id: all_updates_recieved
    icon: mdi:database-refresh-outline
    entity_category: diagnostic
    lambda: |-
      return
      id(solar_plant_moisture_level_recieved).state &&
      id(batt_level_recieved).state &&
      id(irrigation_lux_recieved).state &&
      id(water_tank_level_recieved).state
      ;
    on_press:
      then:
        - binary_sensor.template.publish:
            id: data_update_problem
            state: OFF
      #Log that updates are all done
        - logger.log:
            format: "All sensors updated after %.1f secs of uptime. Checking if watering is required"
            args: [ 'id(uptime_sec).state']
        - delay: 100ms
      #Water if required and allowed then go to sleep. Note watering will only happen if total system is healthy (by design).
        - if:
            condition:
              and:
                - text_sensor.state: #Check if our watering regime wants a water dose
                    id: watering_mode
                    state: 'Wetting Mode'
                - binary_sensor.is_off: battery_is_low # Not in battery saving mode.
                - binary_sensor.is_off: tank_water_is_low #Tank Water ok
                - binary_sensor.is_on: auto_water_sensor #Auto water enabled
            then:
              - logger.log: "Watering required and allowed" 
              - switch.turn_on: pump
              - delay: ${pump_run_time}
              - delay: 1s
              - button.press: sleep_if_allowed
            else:
              - logger.log: "Watering not permitted!!.." #todo log give reason.
              - button.press: sleep_if_allowed
               
##################################################
#Deep sleep control
##################################################
#A lot of my logic is remashes of:
  #https://www.wirewd.com/make/blog/esphome_sleep_modes
  #https://tatham.blog/2021/02/06/esphome-batteries-deep-sleep-and-over-the-air-updates/

#Import HA Deep sleep control
  - platform: homeassistant
    name: "Stay Awake"
    internal: false #I want to know that ESPHome knows;)
    id: "stay_awake_sensor"
    entity_id: input_boolean.keep_esps_awake_switch_ha
    icon: "mdi:sleep-off"
    entity_category: diagnostic
    on_press:
      then:
        - logger.log: "STAY AWAKE requested from HA. Preventing deep sleep"
        - deep_sleep.prevent: deep_sleep_1
    on_release:
      then:
        - logger.log: "STAY AWAKE TURNED OFF. Going to sleep..."     
        - deep_sleep.enter:
            id: deep_sleep_1
            sleep_duration: ${sleep_time}

#Bit of a trade-off as to what to have in ESPHome vs import from HA.....
#Import Auto Water control
  - platform: homeassistant
    name: Auto Water ${friendly_name}
    internal: false
    id: "auto_water_sensor"
    entity_id: input_boolean.auto_water_planty
    icon: "mdi:auto-fix"
        
script:
##################################################
#Pump auto off timer (id: pump)
################################################## 
  - id: run_pump_for_pump_run_time
    then:
      - logger.log: "Running pump for ${pump_run_time}"
      - delay: ${pump_run_time}
      - switch.turn_off: pump

switch:
##################################################
#Pump
##################################################   
  - platform: gpio
    pin: GPIO18
    id: pump
    name: ${friendly_name} Pump 
    icon: mdi:water-pump
    internal: true
    restore_mode: ALWAYS_OFF
    on_turn_on: #Auto off timer. Flood protection.
      - script.stop: run_pump_for_pump_run_time # Stop any existing timers.
      - script.execute: run_pump_for_pump_run_time # Start new timer

##################################################
#Control peripheral power (on solar power manager)
##################################################
  - platform: gpio
    id: power_peripherals 
    name: Power Peripherals ${friendly_name}     
    pin:
      number: 17
    internal: true
    restore_mode: ALWAYS_ON #Power on when waking from sleep.

globals:
#For Daily battery level change (below)
  - id: previous_value
    type: float
    restore_value: yes
    initial_value: '0.0'

sensor:
#Track end of day battery
  - platform: template
    name:  End of Day Battery ${friendly_name}
    id: end_of_day_battery_percent
    update_interval: never
    icon: "mdi:solar-power"
    unit_of_measurement: '%'
    on_value:
      then:
        #Calculate change in end of day battery and publish is to sensor
        - lambda: id(change_in_end_of_day_battery_percent).publish_state(x - id(previous_value));
        #Set previous value as current value. 
        - lambda: |-
            id(previous_value) = x;
                
#Track daily changes in battery (i.e net solar production)
  - platform: template
    # source_id: end_of_day_battery_percent
    id: change_in_end_of_day_battery_percent
    icon: mdi:trending-up
    internal: false
    name: Change in End of Day Battery ${friendly_name} 
    unit_of_measurement: '%'
    accuracy_decimals: 1

#Uptime sensor
  - platform: uptime
    id: uptime_sec
    name: Uptime Sensor ${friendly_name} 
    update_interval: 2s
    accuracy_decimals: 1
    unit_of_measurement: s


##################################################
#For counting data updates recieved for each wake cycle.
#Manually updated via publishing from other sensors.
##################################################
#Track Lux updates
  - platform: template
    name: "Count Lux Updates"
    id: count_irrigation_lux
    icon: "mdi:counter"
    unit_of_measurement: count
    entity_category: diagnostic
#Track ToF updates
  - platform: template
    name: "Count ToF Updates"
    id: count_irrigation_tof
    unit_of_measurement: count
    entity_category: diagnostic
#Track battery updates
  - platform: template
    name: "Count Batt V Updates"
    id: count_batt_voltage
    unit_of_measurement: count
    icon: "mdi:counter"
    entity_category: diagnostic
    
##########################################################################################
# Time of Flight sensor  - i2c
##########################################################################################
#Powered via 5v
  - platform: vl53l0x
    id: tof
    name:  ToF ${friendly_name}
    internal: false 
    address: 0x29
    timeout: 300ms
    update_interval: never
    entity_category: diagnostic
    #never
    # enable_pin: GPIO17 #Did not work: https://github.com/esphome/issues/issues/3644
    accuracy_decimals: 1
    unit_of_measurement: 'cm'
    filters:
    #Convert to cm
      - multiply: 100 
    #Then use moving median to smooth noise.  Sample 5 points then push.
      - median:
          window_size: 5
          send_every: 5
          send_first_at: 5
    on_raw_value:
      then:
      #Push sensor update counter.
        - lambda: id(count_irrigation_tof).publish_state(id(count_irrigation_tof).state +1);
      
#Convert the ToF distance to a water tank level (% full)
  - platform: copy
    source_id: tof
    id: water_tank_level
    internal: false
    # icon: "mdi:battery"
    name:  Water Tank Level ${friendly_name}
    unit_of_measurement: '%'
    accuracy_decimals: 1
    entity_category: ''
    filters:
      # Map from distance to % full. To calibrate.
      - calibrate_linear:
          - 3 -> 100 
          - 19.5 -> 0
      ##Overide values less than 0% and more than 100%. Round to 0.5%.
      - lambda: |
          if (x > 100) return 100; 
          else if (x < 0) return 0;
          else return ceil(x / 0.5) * 0.5;
    on_value:
      then:
       - binary_sensor.template.publish:
          id: water_tank_level_recieved
          state: ON


##########################################################################################
# bh1750 Lux/light sensor
##########################################################################################     
  - platform: bh1750
    id: irrigation_lux
    name: Lux ${friendly_name}
    address: 0x23
    update_interval: never
    filters:
    #Use moving median to smooth noise. Sample 5 points then send.
      - median:
          window_size: 5
          send_every: 5
          send_first_at: 5
    on_value:
      then:
       - binary_sensor.template.publish:
          id: irrigation_lux_recieved
          state: ON
    on_raw_value:
      then:
      #Sensor update counter.
        - lambda: id(count_irrigation_lux).publish_state(id(count_irrigation_lux).state +1);

#Measure of total light per day. Poor persons DLI https://en.wikipedia.org/wiki/Daily_light_integral
  - platform: integration
    name: "Light per day"
    id: irrigation_cul_lux_hours
    sensor: irrigation_lux
    time_unit: s
    unit_of_measurement: lux*s
    restore: true
    accuracy_decimals: 0
    # filters:
      # - multiply: 0.


#Notes:
#Voltage divider: Used 2 x 300K Ohm resistors
  - platform: adc
    id: batt_voltage
    name: Battery Voltage ${friendly_name}
    internal: true
    pin: GPIO33 #ADC1
    update_interval: never
    accuracy_decimals: 2
    attenuation: auto
    filters:
      # #The scale it back up from voltage divided value 2 x 300K > 2.1. 4.2/2.1 = 2.
      - multiply: 2
    on_raw_value:
      then:
      #Sensor update counter.
        - lambda: id(count_batt_voltage).publish_state(id(count_batt_voltage).state +1);
      
#Intermediate sensor. Might consolidate them later.
  - platform: copy
    source_id: batt_voltage
    id: batt_voltage_filtered
    icon: "mdi:battery"
    internal: false
    name:  Battery Voltage ${friendly_name}
    unit_of_measurement: V
    accuracy_decimals: 2
    filters:
    #Use moving median to smooth noise.
      - median:
          window_size: 10
          send_every: 10
          send_first_at: 10
       
#Convert the Voltage to a battery  level (%)
  - platform: copy
    source_id: batt_voltage_filtered
    id: batt_level
    internal: false 
    icon: "mdi:battery"
    name: Battery Percent ${friendly_name}
    unit_of_measurement: '%'
    accuracy_decimals: 1
    filters:
      # Map from voltage to Battery level
      - calibrate_linear:
          - 3.0 -> 0 #Set 3.0 to 0% even though it can go lower (2.4V), for life extention. There's not much capacity below this anyway.
          - 4.05 -> 100 #Set 4.05 to 100% even though it can go higher (~4.2V), for life extention.
       
      #Overide values less than 0% and more than 100%
      - lambda: |
          if (x < 0) return 0; 
          else if (x > 100) return 100;
          else return ceil(x / 5) * 5;
    on_value:
      then:
      #Publish that data is recieved
       - binary_sensor.template.publish:
          id: batt_level_recieved
          state: ON
          
#Capacitive soil moisture sensor: https://www.aliexpress.com/item/32832538686.html?spm=a2g0o.order_list.0.0.55771802WgNqEA
#Voltage of the Capacitive soil moisture sensor
  - platform: adc
    pin: GPIO34 
    name: Soil Moisture Volts ${friendly_name}
    id: soil_moisture_voltage
    internal: true 
    accuracy_decimals: 1
    update_interval: never
    # never
    attenuation: auto
    filters:
    #Use moving median to deal with noise. Sample 10, push 1.
      - median:
          window_size: 10
          send_every: 10
          send_first_at: 10

#Convert the Voltage to a moisture  level (%)
  - platform: copy
    source_id: soil_moisture_voltage
    id: solar_plant_moisture_level
    name: Soil Moisture ${friendly_name}
    internal: false     
    icon: "mdi:water-percent"
    unit_of_measurement: '%'
    accuracy_decimals: 1
    filters:
      #max and min values taken from testing with glass of water. Prob need to do in-situ tests.
      - calibrate_linear:
          - 1.66 -> 100.0
          - 2.90 -> 0.0
      #Handle/cap boundaries
      - lambda: |
          if (x < 0) return 0; 
          else if (x > 100) return 100;
          else return (x);
    on_value:
      then:
      #Data update recieved.
       - binary_sensor.template.publish:
          id: solar_plant_moisture_level_recieved
          state: ON
      #Help manage the watering mode.
       - lambda: |-
           if (x < id(moisture_lower_threshold).state) 
              { return id(watering_mode).publish_state("Wetting Mode");}
           else if (x > id(moisture_upper_target).state)
              { return id(watering_mode).publish_state("Drying Mode");}
    on_value_range:
      #Help manage the watering mode.
      - below: !lambda return id(moisture_lower_threshold).state;
        then:
          - lambda: return id(watering_mode).publish_state("Wetting Mode");      
      - above: !lambda return id(moisture_upper_target).state;
        then:
          - lambda: return id(watering_mode).publish_state("Drying Mode");  


Trying to repropagate a large jade branch cutting!

https://www.houzz.com/discussions/4766422/can-you-replant-or-propagate-a-large-jade-tree-branch

Underneath it’s just like this (it’s not the same as below but it is just a stump below soil)…! Hope it survives!

Interesting project. I just started something simular for my balcony. No plumbing and no power outlet.
I ordered some parts from Aliexpress. As for now it is enough for 8 plants, expandable to 16. I use a 8-relay board with onboard ESP, with an extra 8-relay board I can expand it to 16.
Each plant has it’s own micropump. The idea is to measure the moisture and when it’s on the minimum the according pump starts for a defined time to water it to the max level. I want to do that with some interruptions to get a more accurate measuring of the moisture level. The main hose comes from the tank, goes tot an 8-fold divider. From there there goes a hose to every pump.

I just received all the parts and programmed the ESP so it is visible in Home Assistant, so I’m not that far yet

1 Like

Keep me posted on your project please.

I’m probably going to do another one when I finally finish this one that will service two plants, so keen to see your “multi plant solution”.

The new one will replace my old “beer dispenser” irrigation system;)

That’s a really nice way to do it with a beer pump :smiley:
What type of sensor are you using for the waterlevel sensor in the tank?

The new system has a VL53L0X. I’m still working through some reliability issues. Might be connections. Stops reporting when battery voltage gets lower. But generally works well. Much smaller than an ultrasonic.

The old system doesn’t have anything. I got a float switch which I was going to use with an aqara reed switch but I never got around to installing it.

I think I was originally going to pop one of these in the bottom but the signal range didn’t seem to be good and I don’t think they’re really supposed to be immersed.

AU $23.31 48%OFF | Aqara Flood Sensor Water Immersing Sensor IP67 Waterproof Remote Alarm Work With Smart home app
https://a.aliexpress.com/_msHN9me

AU $2.18 | Thinary VL53L0X Time-of-Flight (ToF) Laser Ranging Sensor Breakout 940nm GY-VL53L0XV2 Laser Distance Module I2C IIC 3.3V/5V
https://a.aliexpress.com/_mPlpmvu

I’m not at that point yet, but I have to add one in my system as well. I’ll have a look at that one when I’m going to add one.

1 Like