🌿 ESPlanty | Self-watering Solar Powered Plant | No plumbing & no powerpoints | # Irrigation , # Deep Sleep , # Battery

Thanks, I’ll keep that in mind

Can you share your button code from ha?

Which button do you mean?

I thought you were trying to add a button to the dashboard to stop deepsleep? I’m trying to figure out how to prevent my sensor node from going to sleep too do OTA updates when needed.

1 Like

yeah my main way is based on this approach. You toggle on a HA control and next time it wakes and connects, it will stay awake.

The HA config is similar to this (think I’ve changed the name).

input_boolean:
  keep_esps_awake_switch_ha:
    name: "Keep ESPs Awake"
    initial: false
    icon: mdi:sleep-off

image

1 Like

I’ll maintain my master config here. May not maintain it well though.

#Todo: 
  #Determine night time battery loss.
  #More testing insitu
  #HA alerts for low water, low battery, data updates not working.
  #Check pump runs.
  #Monitor review wet/dry/battery thresholds.
  #Make debugging sensors internal.

substitutions:
  devicename: irrigation-balcony-t7mini32v15
  friendly_name: Planty 
  device_description: Planty | Bedroom Balcony Irrigation
  
  sleep_time: 5min
  auto_wake_time: 20s
  pump_run_time: 40s
  internal_mode: "true"
  long_deep_sleep_duration: 24h
  
  bh1750_i2c_scl_blue: GPIO27
  bh1750_i2c_sda_green: GPIO25
  
  vl53l0x_i2c_scl_blue: GPIO23
  vl53l0x_i2c_sda_green: GPIO05  
  
  power_peripheral_pin: GPIO26

  pump_switch_pin: GPIO18
  batt_voltage_pin: GPIO33
  soil_moisture_pin: GPIO34


# G                        # RST                            #1 tx         # G
# NC                       # VP                             #3 rx         #@ 27 bh1750 blue scl  ${bh1750_i2c_scl_blue}
# VN                       #@ 26 ${power_peripheral_pin}    #22           #@ 25 bh1750 green sda ${bh1750_i2c_sda_green}
# 35                       #@ 18 ${pump_switch_pin}         #21           #@ 32
#@ 33 ${batt_voltage_pin}  #@ 19                            #27           # TDI
# 34 ${soil_moisture_pin}  #@ 23 ${vl53l0x_i2c_scl_blue}    #25           #@ 4
# TMS                      #@ 5  ${vl53l0x_i2c_sda_green}   #G            # 0
# NC                       # 3v3                            #5v           # 2
# SD2                      # TCK                            #TDO          # SD1
# CMD                      # SD3                            #SDO          # CLK


esp32:
  board: ttgo-t7-v14-mini32 #pinout: https://ae01.alicdn.com/kf/Ha204b20d14d243faa0c1a8760de1b187r.jpg
esphome:
  name: $devicename
  comment: ${device_description}
  on_boot:    
    # #Try this if sensors don't boot. https://community.home-assistant.io/t/add-sensor-delay-upon-power-on/158567/6?u=mahko_mahko
    # https://community.home-assistant.io/t/i2c-bus-scan-after-deep-sleep-recovery/524925/7?u=mahko_mahko
    - priority: 900
      then:
      - lambda: |-
          Wire.begin();
          delay(500);
    - priority: -100
      then:
        #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     
        
        #Auto sensor updates are turned off and manually requested on boot. These are multi-sampled and then aggregated. 
        #The ESP then goes back to sleep when they're done (unless told to stay awake).
          #Request sensor updates        
        - logger.log: "....Starting sensor updates"
        - repeat:
            count: 5 #Update cycles
            then:
              - component.update: batt_voltage #Battery level
              - delay: 100ms
              - component.update: soil_moisture_voltage #Moisture level
              - delay: 100ms
              - component.update: tof #Water tank level
              - delay: 100ms
              - component.update: irrigation_lux #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: #Includes deep sleep
    priority: -100
    then:
      - script.wait: run_pump_for_pump_run_time #Wait until pump run is done.
      - 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 #Probably not required....
    #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.123
      gateway: 192.168.1.1
      subnet: 255.255.255.0

api:
ota:

logger:
  level: VERBOSE
  baud_rate: 0
  
i2c:
  - id: bh1750_i2c_bus
    sda: GPIO25 #data > green wire
    scl: GPIO27 #clock > blue wire
    scan: true
  - id: vl53l0x_i2c_bus
    sda: ${vl53l0x_i2c_sda_green} #data > green wire
    scl: ${vl53l0x_i2c_scl_blue} #clock > blue wire
    scan: true
    

deep_sleep: 
#Auto deep sleep shouldn't actually get used if data updates are happening properly (should sleep sooner).
#But if something is wrong with data updates this will kick-in to protect battery discharge.
  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 6am.
      - 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 sensor reset
          - sensor.integration.reset: irrigation_cul_lux_hours
          - delay: 1s
          #Rest up
          - deep_sleep.enter:
              id: deep_sleep_1
              until: "06:00:00"
              time_id: esptime
              
text_sensor:
 # Reports the ESPHome Version with compile date
  - platform: version
    name: ${friendly_name} ESPHome Version
    
select:
#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. 
#TODO: More testing. 
  - platform: template
    name: "Watering Mode"
    id: watering_mode
    optimistic: true
    restore_value: true
    options:
      - Wetting Mode
      - Drying Mode
    initial_option: Drying Mode
    
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: 40.0
    max_value: 90.0
    step: 5.0
    restore_value: true
    initial_value: 80.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: 5.0
    max_value: 60.0
    step: 5.0
    restore_value: true
    initial_value: 50.0
    
#Battery Saving Mode Threshold. Trigger longer sleep cycles if below this and hope for sunlight.
  - 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: 80.0
    step: 5.0
    restore_value: true
    initial_value: 20.0
    
#Low water tank threshold. Don't water if below this.
  - 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: 5.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"
    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: ${long_deep_sleep_duration}
        #Reset culmulative light sesnor reset
        - sensor.integration.reset: irrigation_cul_lux_hours

binary_sensor:
#Low tank water alert
  - platform: template
    id: tank_water_level_is_ok
    name: Tank Level Ok ${friendly_name}
    icon: mdi:water-remove-outline
    device_class: moisture
    lambda: |-
      if (id(water_tank_level).state > id(low_tank_water_threshold).state) 
          {return true;}
        else if (id(water_tank_level).state <= id(low_tank_water_threshold).state) 
          {return false;}
        else
          return {};

    
#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_level_is_low
    name: Battery Level Is Low ${friendly_name} 
    icon: mdi:battery-alert
    lambda: |-
      if (id(batt_level).state < id(battery_saving_mode_threshold).state) 
          {return true;}
        else if (id(batt_level).state >= id(battery_saving_mode_threshold).state) 
          {return false;}
        else
          return {};
    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: ${internal_mode}
#Battery Level
  - platform: template
    name: Battery level updated ${friendly_name}
    id: batt_level_recieved
    internal: ${internal_mode}
#Light
  - platform: template
    name: Lux updated ${friendly_name}
    id: irrigation_lux_recieved
    internal: ${internal_mode}
#Water level
  - platform: template
    name: water level updated ${friendly_name}
    id: water_tank_level_recieved
    internal: ${internal_mode}
#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:
                #Check if our watering regime wants a water dose
                - lambda: return (id(watering_mode).state == "Wetting Mode");
                - binary_sensor.is_off: battery_level_is_low # Not in battery saving mode.
                - binary_sensor.is_on: tank_water_level_is_ok #Tank Water is ok
                - binary_sensor.is_on: auto_water_sensor #Auto water is 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 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 everyone is talking to each other;)
    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}


#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: 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: ${pump_switch_pin}
    id: pump
    name: ${friendly_name} Pump 
    icon: mdi:water-pump
    internal: false
    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: ${power_peripheral_pin}
    internal: true
    restore_mode: ALWAYS_ON #Power on when waking from sleep.

globals:
#For daily battery level change (below). i.e net energy production or loss.
#Globals survive deep sleep
  - 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: '%'
    accuracy_decimals: 3
    on_value:
      then:
        #Calculate change in end of day battery and publish 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
    name: Change in End of Day Battery ${friendly_name}   
    id: change_in_end_of_day_battery_percent
    icon: mdi:trending-up
    internal: false
    unit_of_measurement: '%'
    accuracy_decimals: 3

#Uptime sensor
  - platform: uptime
    id: uptime_sec
    name: Uptime Sensor ${friendly_name} 
    update_interval: 2s
    accuracy_decimals: 0
    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
    i2c_id: vl53l0x_i2c_bus
    # setup_priority: 300
    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:
      - multiply: 100 #Convert to cm
      - median: #Moving median to smooth noise.  Sample 5 points then push.
          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
    i2c_id: bh1750_i2c_bus
      # - id: bh1750_i2c_bus
    # sda: GPIO25 #data > green wire
    # scl: GPIO27 #clock > blue wire
    # scan: true
  # - id: vl53l0x_i2c_bus
    name: Lux ${friendly_name}
    address: 0x23
    update_interval: never
    filters:
      - median:  #Use moving median to smooth noise. Sample 5 points then send.
          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

#Notes:
#Voltage divider: Used 2 x 300K Ohm resistors
  - platform: adc
    id: batt_voltage
    name: Battery Voltage ${friendly_name}
    internal: true
    pin: ${batt_voltage_pin} #ADC1
    update_interval: never
    accuracy_decimals: 2
    attenuation: auto
    filters:
      # #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:
      - median: #Use moving median to smooth noise.
          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: 0
    filters:
      # Map from voltage to Battery level
      - calibrate_linear:
          - 3.1 -> 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.1 -> 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
    name: Soil Moisture Volts ${friendly_name} 
    id: soil_moisture_voltage    
    pin: ${soil_moisture_pin}
    internal: false 
    accuracy_decimals: 1
    update_interval: never
    # never
    attenuation: auto
    filters:
      - median:     #Use moving median to deal with noise. Sample 10, push 1.
          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.
      #TODO: Monitor/test
       - 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");  

This post is for content relating to it’s operation.

I’ll start with a high level description. It essentially works very similar to other irrigation and deep sleep projects projects around:

  1. Wake up from deep sleep periodically and see if the plant needs water. If so, water, if not, back to sleep.
  2. Don’t water the plant if battery is low, instead, go back to sleep for a long time and hope you get recharged on next wake.
  3. Don’t water the plant if the tank level is low.
  4. The watering regime might be described as “incremental drip-feed towards an upper target, followed by drying towards a lower threshold”. This lets the plant dry out a bit, then “gives it a good water, but slowly rather than all at once”… I still need to monitor that this is working ok.
  5. The light sensor is handy for checking how much light both the plant and the solar panel is getting.

Hi @Mahko_Mahko , how are you measuring the battery voltage? do you have a schematic?

Basically I built my first voltage divider.

There’s some details over here for it actually. But it’ll try to remember to tidy it up and add something here for it. It was harder to find simple info for it than expected. My final solution changed slightly to what’s in this link.

I moved to using 2 x 300kOhm resistors. It’s a simpler set-up. There’s also a few comments in my config about the resistors used and scaling/conversions.

Battery Monitoring

Summary:

  1. I made my first voltage divider. You need two resistors. I landed on 2 x 300kΩ
  2. Wire them like below. I think it’s right. I wanted to keep the diagram simple and didn’t want to draw up my full wiring yet. You just add extra wires for power and ground as you need for your other devices

Longer version and resources:

  1. Article: ESP8266 battery level meter | ezContents blog 37,
  2. Thread: ESPHome Battery Level Sensor 48,
  3. Some help/checks from @ssieb over on Discord ,

From what I could understand the sum of the resistors should be high and the divided voltage should be < 2.4V
image

Which led me to my 2 x 300kΩ selection. I also like this “symmetric” design. Doesn’t matter which way you wire it.

Battery sensor snippet

#Notes:
#Voltage divider: Used 2 x 300K Ohm resistors
  - platform: adc
    id: batt_voltage
    name: Battery Voltage
    internal: true
    pin: ${batt_voltage_pin} #ADC1
    update_interval: never
    accuracy_decimals: 2
    attenuation: auto
    filters:
      # #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
    unit_of_measurement: V
    accuracy_decimals: 2
    filters:
      - median: #Use moving median to smooth noise.
          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
    unit_of_measurement: '%'
    accuracy_decimals: 0
    filters:
      # Map from voltage to Battery level
      - calibrate_linear:
          - 3.1 -> 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.1 -> 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

@Mahko_Mahko, I’m trying to learn from your code, but I can’t really make it work. I’m focusing on the deep sleep logic and I’m trying to understand how it works.

as you can see my esp is ignoring the button to prevent deep sleep and the fact to sleep during the night, it constantly follows the cycle defined by the variables, in my case 30min sleep, 30min run

  run_duration: 30min
  sleep_duration: 30min

image

can you help me to understand what I’m missing?

binary_sensor:
  - platform: homeassistant
    id: prevent_deep_sleep
    entity_id: input_boolean.prevent_deep_sleep
    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_control
    on_release:
      then:
        - logger.log: "STAY AWAKE TURNED OFF. Going to sleep..."     
        - deep_sleep.enter:
            id: deep_sleep_control
            sleep_duration: ${sleep_duration}


time:
  - platform: homeassistant
    id: homeassistant_time
    on_time:
      #Deep sleep at 8pm and wake up at 6am.
      - hours: 20
        then:
          #Rest up
          - deep_sleep.enter:
              id: deep_sleep_control
              until: "06:00:00"
              time_id: homeassistant_time

deep_sleep:
  id: deep_sleep_control
  run_duration: ${run_duration}
  sleep_duration: ${sleep_duration}
  
esphome:
  on_boot:
    - priority: -100
      then:
        - wait_until:
            condition:
              api.connected:
        - logger.log: "API connected"
        - wait_until:
            condition:
              time.has_time:
        - logger.log: "Time has a value from Home Assistant"
        - if:
            condition:
              binary_sensor.is_on: prevent_deep_sleep
            then:
              - logger.log: "Prevent deep sleep"
              - deep_sleep.prevent: deep_sleep_control
            else:
              - logger.log: "Allow deep sleep"
              - deep_sleep.allow: deep_sleep_control
      
  on_shutdown:
    priority: -100
    then:
      - if:
          condition:
            binary_sensor.is_on: prevent_deep_sleep
          then:
            - logger.log: "Prevent deep sleep"
            - deep_sleep.prevent: deep_sleep_control
          else:
            - logger.log: "Allow deep sleep"
            - deep_sleep.allow: deep_sleep_control

I had to work through some buggyness too at a few points.

Do you have any sensors with a high update_interval (say 1sec). Or are you managing the updates manually?
Try pulling them back to say 5sec for debugging.

Can you see the home assistant sensor being imported in the ESPHOME logs? Try making this NOT internal and check if it comes back re-imported into HA ok. So that it is clear everyone is hearing each other.

I also seem to recall sometimes the input Boolean only seemed to start working once you had toggled it from HA once the esp was online (once). So maybe fiddle with that.

In summary, I’m not sure. Mine is working great but I recall having to work through weird issues.

did you get any success? Have the same issue

@Mar1us, almost
I’m now able to prevent deep sleep with the button, but when It enters in deep sleep on sunset it never resume :frowning: still study/test/fail/learn…


# Time configuration
time:
  - platform: homeassistant
    id: homeassistant_time

# Sun configuration
sun:
  latitude:  9999999999999  #replace with yours
  longitude: 9999999999999 #replace with yours
  id: sun_component
  on_sunset:
    then:
      - logger.log: "Sunset occurred, entering deep sleep mode until next sunrise"
      - deep_sleep.enter:
          id: deep_sleep_control
          sleep_duration:  !lambda "return id(sleep_duration_calc).state * 60;"

# Deep sleep configuration
deep_sleep:
  id: deep_sleep_control
  #run_duration: 5min # Run for 5 minutes before entering deep sleep mode
  #sleep_duration: 5min # Sleep for 5 minutes before waking up again

binary_sensor:
  # Get the state of the prevent_deep_sleep input_boolean from Home Assistant to control whether or not to enter deep sleep mode
  - platform: homeassistant
    id: prevent_deep_sleep
    entity_id: input_boolean.prevent_deep_sleep
    icon: "mdi:sleep-off"
    entity_category: diagnostic

sensor:
  # Get the sleep_duration value from Home Assistant to control how long to sleep for in deep sleep mode
  - platform: homeassistant
    id: sleep_duration
    entity_id: input_number.deep_sleep_sleep_duration
  # Get the delay_duration value from Home Assistant to control how long (in seconds) to wait before proceeding
  - platform: homeassistant
    id: delay_duration
    entity_id: input_number.deep_sleep_script_delay_duration
##################
  - platform: template
    name: "Sleep Duration to next Sunrise"
    id: sleep_duration_calc
    unit_of_measurement: "min"
    lambda: |-
      if (id(tomorrow_sunrise_string).has_state() && id(today_sunset_string).has_state()) {
        struct tm tomorrow_sunrise_tm;
        strptime(id(tomorrow_sunrise_string).state.c_str(), "%Y-%m-%d %H:%M:%S", &tomorrow_sunrise_tm);
        time_t tomorrow_sunrise_time = mktime(&tomorrow_sunrise_tm);
        struct tm today_sunset_tm;
        strptime(id(today_sunset_string).state.c_str(), "%Y-%m-%d %H:%M:%S", &today_sunset_tm);
        time_t today_sunset_time = mktime(&today_sunset_tm);
        double sleep_duration_seconds = difftime(tomorrow_sunrise_time, today_sunset_time);
        return sleep_duration_seconds / 60.0;
      }
      return {};  
##################
text_sensor:
  - platform: homeassistant
    id: today_sunrise_string
    entity_id: sensor.suninfo_sunrise
    attribute: today
    internal: true
  - platform: homeassistant
    id: today_sunset_string
    entity_id: sensor.suninfo_sunset
    attribute: today
    internal: true
  - platform: homeassistant
    id: tomorrow_sunrise_string
    entity_id: sensor.suninfo_sunrise
    attribute: tomorrow
    internal: true
  - platform: homeassistant
    id: tomorrow_sunset_string
    entity_id: sensor.suninfo_sunset
    attribute: tomorrow
    internal: true
  - platform: template
    name: "Today Sunset"
    id: today_sunset
    lambda: |-
      if (id(today_sunset_string).has_state()) {
        struct tm t;
        strptime(id(today_sunset_string).state.c_str(), "%Y-%m-%d %H:%M:%S", &t);
        char buf[30];
        strftime(buf, sizeof(buf), "%B %d, %Y at %I:%M %p", &t);
        return std::string(buf);
      }
      return {};
  - platform: template
    name: "Tomorrow Sunrise"
    id: tomorrow_sunrise
    lambda: |-
      if (id(tomorrow_sunrise_string).has_state()) {
        struct tm t;
        strptime(id(tomorrow_sunrise_string).state.c_str(), "%Y-%m-%d %H:%M:%S", &t);
        char buf[9];
        strftime(buf, sizeof(buf), "%H:%M:%S", &t);
        return std::string(buf);
      }
      return {};                    
esphome:
  on_boot:
    priority: -10
    then:
      - logger.log:
          format: "Waiting for API to connect..."
      # Wait until API is connected and time is synchronized before proceeding
      - wait_until:
          api.connected:
      - logger.log:
          format: "API connected. Waiting for time to synchronize..."
      - wait_until:
          time.has_time:
      - logger.log:
          format: "Time synchronized. Checking prevent_deep_sleep binary sensor value..."
      # Check if the prevent_deep_sleep binary sensor value is on or off before proceeding
      - if:
          condition:
            lambda: 'return id(prevent_deep_sleep).state;'
          then:
            - logger.log:
                format: "prevent_deep_sleep binary sensor value is on"
          else:
            - logger.log:
                format: "prevent_deep_sleep binary sensor value is off"
      # Log that API is connected, time is synchronized, and prevent_deep_sleep binary sensor value is checked
      - logger.log:
          format: "API connected, time synchronized, and prevent_deep_sleep binary sensor value checked."
      - wait_until:
          lambda: "return id(today_sunset).has_state() && id(tomorrow_sunrise).has_state();"
      - wait_until:
          lambda: "return id(prevent_deep_sleep).has_state() && id(sleep_duration).has_state() && id(delay_duration).has_state();"
      - script.execute: consider_deep_sleep

script:
  - id: consider_deep_sleep
    mode: queued
    then:
      - logger.log:
          format: "Executing consider_deep_sleep script"
      - delay: 10s
      - if:
          condition:
            binary_sensor.is_on: prevent_deep_sleep
          then:
            - logger.log:
                format: "Skipping sleep, per prevent_deep_sleep"
            - deep_sleep.prevent: deep_sleep_control
          else:
            - logger.log:
                format: "Entering deep sleep mode"
            - if:
                condition:
                    - sun.is_above_horizon:
                        id: sun_component # Sun is above horizon.
                then:
                  - logger.log:
                      format: "Entering deep sleep until sunrise"
                  - deep_sleep.enter: 
                      id: deep_sleep_control
                      #sleep_duration: 2min
                      sleep_duration:  !lambda "return id(sleep_duration_calc).state * 60;"
                else:
                  - logger.log:
                      format: "Entering deep sleep normal"
                  - deep_sleep.enter: 
                      id: deep_sleep_control
                      #sleep_duration: 2min                    
                      sleep_duration:  !lambda "return id(delay_duration).state;"
      - script.execute: consider_deep_sleep  

@Mar1us thank you, because after your message I provided more love to the code; it seems that now it works

  • The button to suspend deep sleep works
  • The input to control the deep sleep duration and the delay duration are working
  • The sunset condition seems ok (testing now)

essentially this is the behavior:

if the prevent deep sleep is on, the esp remains up
if the prevent deep sleep is off, it goes in deep sleep for the amount of minutes defined by the input sleep duration, then the script is delayed for the amount of time defined by the input number script delat duration. This is the normal condition, when the sun is below the horizontal then the esp goes in deep sleep until the next sunrise. (in order to calculate the sunst/sunrise duration I used the SUN2 component)

here the full code, testing it right now.
any feedback/comments is really apprecated

# Substitutions
# Define variables to be used throughout the configuration
substitutions:
  device_name: test
  friendly_name: "test"
  device_platform: espressif32
  device_board: nodemcu-32s
  device_ip: X.X.X.X

# Packages
# Include common configurations from external files
packages:
  wifi: !include common/device_wifi.yaml
  device_base: !include common/device_base_ESP32.yaml
  home_assistant_api: !include common/device_api.yaml
  sensor_wifi_ip_address: !include common/sensor_wifi_ip_address.yaml

# Enable logging
logger:
  level: VERBOSE

# API
api:

# Time configuration
time:
  - platform: homeassistant
    id: homeassistant_time

# Sun configuration
sun:
  latitude: 999999
  longitude: 99999
  id: sun_component
  on_sunset:
    then:
      - logger.log: "Sunset occurred, entering deep sleep mode until next sunrise"
      - logger.log:
          format: "Sleep duration until next sunrise: %f minutes"
          args: [ "id(sleep_duration_calc).state" ]
      - deep_sleep.enter:
          id: deep_sleep_control
          sleep_duration: !lambda "return id(sleep_duration_calc).state * 60000;"
# Deep sleep configuration
deep_sleep:
  id: deep_sleep_control

binary_sensor:
  # Get the state of the prevent_deep_sleep input_boolean from Home Assistant to control whether or not to enter deep sleep mode
  - platform: homeassistant
    id: prevent_deep_sleep
    entity_id: input_boolean.prevent_deep_sleep
    icon: "mdi:sleep-off"
    entity_category: diagnostic

sensor:
  # Get the sleep_duration value from Home Assistant to control how long to sleep for in deep sleep mode
  - platform: homeassistant
    id: sleep_duration
    entity_id: input_number.deep_sleep_sleep_duration
  # Get the delay_duration value from Home Assistant to control how long (in seconds) to wait before proceeding
  - platform: homeassistant
    id: delay_duration
    entity_id: input_number.deep_sleep_script_delay_duration    
##################
  - platform: template
    name: "Sleep Duration to next Sunrise"
    id: sleep_duration_calc
    unit_of_measurement: "min"
    lambda: |-
      if (id(tomorrow_sunrise_string).has_state() && id(today_sunset_string).has_state()) {
        struct tm tomorrow_sunrise_tm;
        strptime(id(tomorrow_sunrise_string).state.c_str(), "%Y-%m-%d %H:%M:%S", &tomorrow_sunrise_tm);
        time_t tomorrow_sunrise_time = mktime(&tomorrow_sunrise_tm);
        struct tm today_sunset_tm;
        strptime(id(today_sunset_string).state.c_str(), "%Y-%m-%d %H:%M:%S", &today_sunset_tm);
        time_t today_sunset_time = mktime(&today_sunset_tm);
        double sleep_duration_seconds = difftime(tomorrow_sunrise_time, today_sunset_time);
        return sleep_duration_seconds / 60.0;
      }
      return {};  
##################
text_sensor:
  - platform: homeassistant
    id: today_sunrise_string
    entity_id: sensor.suninfo_sunrise
    attribute: today
    internal: true
  - platform: homeassistant
    id: today_sunset_string
    entity_id: sensor.suninfo_sunset
    attribute: today
    internal: true
  - platform: homeassistant
    id: tomorrow_sunrise_string
    entity_id: sensor.suninfo_sunrise
    attribute: tomorrow
    internal: true
  - platform: homeassistant
    id: tomorrow_sunset_string
    entity_id: sensor.suninfo_sunset
    attribute: tomorrow
    internal: true
  - platform: template
    name: "Today Sunset"
    id: today_sunset
    lambda: |-
      if (id(today_sunset_string).has_state()) {
        struct tm t;
        strptime(id(today_sunset_string).state.c_str(), "%Y-%m-%d %H:%M:%S", &t);
        char buf[30];
        strftime(buf, sizeof(buf), "%B %d, %Y at %I:%M %p", &t);
        return std::string(buf);
      }
      return {};
  - platform: template
    name: "Tomorrow Sunrise"
    id: tomorrow_sunrise
    lambda: |-
      if (id(tomorrow_sunrise_string).has_state()) {
        struct tm t;
        strptime(id(tomorrow_sunrise_string).state.c_str(), "%Y-%m-%d %H:%M:%S", &t);
        char buf[9];
        strftime(buf, sizeof(buf), "%H:%M:%S", &t);
        return std::string(buf);
      }
      return {};                    
esphome:
  on_boot:
    priority: -10
    then:
      - logger.log:
          format: "Waiting for API to connect..."
      # Wait until API is connected and time is synchronized before proceeding
      - wait_until:
          api.connected:
      - logger.log:
          format: "API connected. Waiting for time to synchronize..."
      - wait_until:
          time.has_time:
      - logger.log:
          format: "Time synchronized. Checking prevent_deep_sleep binary sensor value..."
      # Check if the prevent_deep_sleep binary sensor value is on or off before proceeding
      - if:
          condition:
            lambda: 'return id(prevent_deep_sleep).state;'
          then:
            - logger.log:
                format: "prevent_deep_sleep binary sensor value is on"
          else:
            - logger.log:
                format: "prevent_deep_sleep binary sensor value is off"
      # Log that API is connected, time is synchronized, and prevent_deep_sleep binary sensor value is checked
      - logger.log:
          format: "API connected, time synchronized, and prevent_deep_sleep binary sensor value checked."
      - wait_until:
          lambda: "return id(today_sunset).has_state() && id(tomorrow_sunrise).has_state();"
      - wait_until:
          lambda: "return id(prevent_deep_sleep).has_state() && id(sleep_duration).has_state() && id(delay_duration).has_state();"

      - script.execute: consider_deep_sleep

script:
  - id: consider_deep_sleep
    mode: queued
    then:
      - logger.log:
          format: "Executing consider_deep_sleep script"
      - logger.log:
          format: "Delaying for %d minutes"
          args: [ "(int) id(delay_duration).state" ]
      - delay: !lambda "return id(delay_duration).state * 60000;"
      - logger.log:
          format: "After the delay"
      - if:
          condition:
            binary_sensor.is_on: prevent_deep_sleep
          then:
            - logger.log:
                format: "Skipping sleep, per prevent_deep_sleep"
          else:
            - logger.log:
                format: "Entering deep sleep mode"
            - if:
                condition:
                    - sun.is_above_horizon:
                        id: sun_component # Sun is above horizon.
                then:
                  - logger.log:
                      format: "Entering deep sleep normal"
                  - logger.log:
                      format: "Sleep for %d minutes"
                      args: [ "(int) id(sleep_duration).state" ]
                  - deep_sleep.enter: 
                      id: deep_sleep_control
                      sleep_duration:  !lambda "return id(sleep_duration).state * 60000;"
                else:
                  - logger.log:
                      format: "Entering deep sleep until sunrise"
                  - logger.log:
                      format: "Sleep duration until next sunrise: %f minutes"
                      args: [ "id(sleep_duration_calc).state" ]
                  - deep_sleep.enter: 
                      id: deep_sleep_control
                      sleep_duration:  !lambda "return id(sleep_duration_calc).state * 60000;"
      - script.execute: consider_deep_sleep
2 Likes

thank you so much, that helped me a lot

Hi Folks, I would like to have your opinion here
Deep sleep cycle it fine to reduce the power consumption, but the ESP must remain on during the watering cycle.

how you calculated the battery capacity/pump consumption/solar panel?
I’m looking around to select the hw

I think Andreas Spiess has done good videos on this (at least the solar and battery parts). You might need to search a bit on his channel.

For the pump you’d prob just measure current with a multimeter?