How Do I Format Lambda for text_sensor - ESPHome Sprinkler

I’ve flashed a D1 Mini with code that uses the new ESPHome Sprinkler Component to operate four sprinkler valves. My code exposes sprinkler controls, duration and multiplier settings to HA. I’ve also added a template that exposes the number of seconds remaining (time_remaining()) as a sensor in HA. Let’s call this a countdown timer. I’d like to see the sensor display minutes and seconds but I’m unable to determine how to code the template to show such.

As it stands, I’m able to show the number of remaining minutes or the number or remaining seconds. Not both! The section of code that adds the time_remaining sensor, in minutes, follows;

sensor:
  - platform: template
    name: ${valve_0_name} Duration
    id: ${valve_0_id}_duration
    unit_of_measurement: Min
    icon: mdi:timer
    accuracy_decimals: 0
    lambda: |-
       return id($devicename).time_remaining().value_or(0)/60;
    update_interval: 60s

If I want to see seconds I would use;


  - platform: template
    name: ${valve_0_name} Duration
    id: ${valve_0_id}_duration
    unit_of_measurement: Sec
    icon: mdi:timer
    accuracy_decimals: 0
    lambda: |-
       return id($devicename).time_remaining().value_or(0);
    update_interval: 1s

Any hints on how I can achieve what I’m looking for? The ability to display minutes and seconds.

On a side note: I noticed the D1 Mini outputs a “time remaining” message every second, regardless of whether a valve is in operation or not. I don’t think this is a good use of processing power or band-width and would like to see the messages passed to HA only when a valve is in operation, If you can shed light on how to deal with that as well, I’d really appreciate it.

Here is my full code - in case it’s needed.

# Based on ESPHome Sprinkler Controller - https://esphome.io/components/sprinkler.html
# Establish Substitutions
substitutions:
### Modify only the following eight lines.
    unit_id: A
    devicename_unit_id: a
    valve_0_name: Front Lawn
    valve_1_name: Front Garden
    valve_2_name: South Garden
    valve_3_name: Rear Garden
    software_version: 2022 09 14 v10
### DO NOT CHANGE ANYTHING BELOW THIS LINE ###
    valve_0_id: valve_0
    valve_1_id: valve_1
    valve_2_id: valve_2
    valve_3_id: valve_3
    esphome_name: irrigation-valve-ctrl-unit-${devicename_unit_id}
    esphome_platform: ESP8266
    esphome_board: d1_mini
    esphome_comment: Four Valve Irrigation Control (${unit_id})
    esphome_project_name: Robert.Four Valve Irrigation Control (${unit_id})
    esphome_project_version: Four Valve Irrigation Ctrl (${unit_id}), ${software_version}
    devicename: irrigation_valve_controller_unit_${devicename_unit_id}
    upper_devicename: Four Valve Irrigation Ctrl (${unit_id})
    irrigation_time_miltiplier_name: time_multiplier_${devicename_unit_id}
    upper_irrigation_time_multiplier_name: Time Multiplier (${unit_id})

#Define Project Deatils and ESP Board Type
esphome:
    name: $esphome_name
    platform: $esphome_platform
    board: $esphome_board
    comment: $esphome_comment
    project:
        name: $esphome_project_name
        version: $esphome_project_version
  

# WiFi connection, replace these with values for your WiFi.
wifi:
    ssid: !secret wifi_ssid
    password: !secret wifi_password

# Enable logging
logger:
  
# Enable over-the-air updates.
ota:
    password: !secret ota_password

# Enable Web server.
web_server:
    port: 80

# Sync time with Home Assistant.
time:
  - platform: homeassistant
    id: homeassistant_time

# Text sensors with general information.
text_sensor:
  # Expose ESPHome version as sensor.
  - platform: version
    name: ESPHome Version
  # Expose WiFi information as sensors.
  - platform: wifi_info
    ip_address:
      name: $devicename IP
    ssid:
      name: $devicename SSID
    bssid:
      name: $devicename BSSID

# Enable On-Board Status LED.
status_led:
  pin:
    # Pin D4
    number: GPIO2
    inverted: true

sensor:
  # Uptime sensor.
  - platform: uptime
    name: ${upper_devicename} Uptime

  # WiFi Signal sensor.
  - platform: wifi_signal
    name: ${upper_devicename} WiFi Signal
    update_interval: 60s

  - platform: template
    name: ${valve_0_name} Duration
    id: ${valve_0_id}_duration
    unit_of_measurement: Min
    icon: mdi:timer
    accuracy_decimals: 0
    lambda: |-
       return id($devicename).valve_run_duration_adjusted(0)/60;
    update_interval: 60s

  - platform: template
    name: ${valve_1_name} Duration
    id: ${valve_1_id}_duration
    unit_of_measurement: Min
    icon: mdi:timer
    accuracy_decimals: 0
    lambda: |-
       return id($devicename).valve_run_duration_adjusted(1)/60;
    update_interval: 60s

  - platform: template
    name: Time Remaining
    id: time_remaining
    unit_of_measurement: Sec
    icon: mdi:progress-clock
    accuracy_decimals: 2
    lambda: |-
      return id($devicename).time_remaining().value_or(0)/60;
    update_interval: 60s

# Enable Home Assistant API
api:
  encryption:
    key: !secret api_encryption
  password: !secret api_password

# Set multiplier via number - Sprinklers
number:
  - platform: template
    id: $irrigation_time_miltiplier_name
    name: $upper_irrigation_time_multiplier_name
    min_value: 0.1
    max_value: 10.0
    step: 0.1
    mode: box # Defines how the number should be displayed in the UI
    lambda: "return id($devicename).multiplier();"
    set_action:
      - sprinkler.set_multiplier:
          id: $devicename
          multiplier: !lambda 'return x;'
  - platform: template
    id: ${valve_0_id}
    name: ${valve_0_name}
    min_value: 60
    max_value: 720
    step: 1
    mode: box # Defines how the number should be displayed in the UI
    lambda: "return id($devicename).valve_run_duration(0);"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: $devicename
          valve_number: 0
          run_duration: !lambda 'return x;'
  - platform: template
    id: ${valve_1_id}
    name: ${valve_1_name}
    min_value: 60
    max_value: 720
    step: 1
    mode: box # Defines how the number should be displayed in the UI
    lambda: "return id($devicename).valve_run_duration(1);"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: $devicename
          valve_number: 1
          run_duration: !lambda 'return x;'
  - platform: template
    id: ${valve_2_id}
    name: ${valve_2_name}
    min_value: 60
    max_value: 720
    step: 1
    mode: box # Defines how the number should be displayed in the UI
    lambda: "return id($devicename).valve_run_duration(2);"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: $devicename
          valve_number: 2
          run_duration: !lambda 'return x;'
  - platform: template
    id: ${valve_3_id}
    name: ${valve_3_name}
    min_value: 60
    max_value: 720
    step: 1
    mode: box # Defines how the number should be displayed in the UI
    lambda: "return id($devicename).valve_run_duration(3);"
    set_action:
      - sprinkler.set_valve_run_duration:
          id: $devicename
          valve_number: 3
          run_duration: !lambda 'return x;'

sprinkler:
  - id: $devicename
    main_switch: Start/Pause/Resume (${unit_id})
    auto_advance_switch: Auto Advance (${unit_id})
    valve_open_delay: 2s
    valves:
      - valve_switch: ${valve_0_name}
        enable_switch: Enable ${valve_0_name}
        run_duration: 60s
        valve_switch_id: ${devicename}_0
      - valve_switch: ${valve_1_name}
        enable_switch: Enable ${valve_1_name}
        run_duration: 60s
        valve_switch_id: ${devicename}_1
      - valve_switch: ${valve_2_name}
        enable_switch: Enable ${valve_2_name}
        run_duration: 60s
        valve_switch_id: ${devicename}_2
      - valve_switch: ${valve_3_name}
        enable_switch: Enable ${valve_3_name}
        run_duration: 60s
        valve_switch_id: ${devicename}_3

button:
  - platform: restart
    id: reset${devicename_unit_id}
    name: Reset $devicename
  - platform: template
    id: sprinkler_shutdown${devicename_unit_id}
    name: Stop (${unit_id})
    on_press:
      then:
        - sprinkler.shutdown: $devicename

switch:
# Hidden switches.
# Switches that control sprinkler valve relays
  - platform: gpio
    name: Relay Board Pin IN1
    restore_mode: RESTORE_DEFAULT_OFF # Prevents GPIO pin from going high during boot
    internal: true # Prevents GPIO switch NAME from showing up in Home Assistant
    id: ${devicename}_0
    pin:
        # GPIO Pin 12
        number: D6
        inverted: true
  - platform: gpio
    name: Relay Board Pin IN2
    restore_mode: RESTORE_DEFAULT_OFF # Prevents GPIO pin from going high during boot
    internal: true # Prevents GPIO switch NAME from showing up in Home Assistant
    id: ${devicename}_1
    pin:
        # GPIO Pin 13
        number: D7
        inverted: true
  - platform: gpio
    name: Relay Board Pin IN3
    restore_mode: RESTORE_DEFAULT_OFF # Prevents GPIO pin from going high during boot
    internal: true # Prevents GPIO switch NAME from showing up in Home Assistant
    id: ${devicename}_2
    pin:
        # GPIO Pin 14
        number: D5
        inverted: true
  - platform: gpio
    name: Relay Board Pin IN4
    restore_mode: RESTORE_DEFAULT_OFF # Prevents GPIO pin from going high during boot
    internal: true # Prevents GPIO switch NAME from showing up in Home Assistant
    id: ${devicename}_3
    pin:
        # GPIO Pin 4
        number: D2
        inverted: true

Regards

You might want to expose the time remaining as text_sensor instead of a sensor. and format the string at your convenience.

I’m not familiar with text_sensors thus some research is in order. Thanks for the tip.

I’m running into issues trying to set up a text_sensor to display remaining seconds/minutes. I’ve tried several lambda configurations without luck. My latest code throws an error on compile;

Compiling /data/irrigation-valve-ctrl-unit-a/.pioenvs/irrigation-valve-ctrl-unit-a/src/main.cpp.o
/config/esphome/irrigation-valve-ctrl-unit-a.yaml: In lambda function:
/config/esphome/irrigation-valve-ctrl-unit-a.yaml:80:75: error: return-statement with a value, in function returning 'void' [-fpermissive]
   80 |           return id($devicename).time_remaining().value_or(0);
      |              ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~            ^  
*** [/data/irrigation-valve-ctrl-unit-a/.pioenvs/irrigation-valve-ctrl-unit-a/src/main.cpp.o] Error 1
========================== [FAILED] Took 7.75 seconds ==========================

Despite searching for a solution on Google, I’m unable to figure out what’s wrong.

The block of code where the trouble occurs reads;

  - platform: template
    name: Time Remaining
    icon: mdi:progress-clock
    on_value:
      then:
        lambda: |-
          return id($devicename).time_remaining().value_or(0);     

Any help in pointing me in the right direction would be appreciated.

I’m far to be a programmer, but this might work. please give it a try.

            lambda |-
              int seconds = round(id($devicename).time_remaining().value_or(0));
              seconds = seconds % (24 * 3600);
              int hours = seconds / 3600;
              seconds = seconds % 3600;
              int minutes = seconds /  60;
              seconds = seconds % 60;
              return (
                (hours ? String(hours) + "h " : "") +
                (minutes ? String(minutes) + "m " : "") +
                (String(seconds) + "s")
              ).c_str();

Thank you for jumping in to help. The above code produced two errors. The first, a colon missing after lambda, was corrected. The other seems related to format but I don’t know how to address it. The error is;

Compiling /data/irrigation-valve-ctrl-unit-a/.pioenvs/irrigation-valve-ctrl-unit-a/src/main.cpp.o
/config/esphome/irrigation-valve-ctrl-unit-a.yaml: In lambda function:
/config/esphome/irrigation-valve-ctrl-unit-a.yaml:92:14: error: could not convert 'operator+(String&&, String&&)(operator+(String&&, const T&) [with T = char [2]; <template-parameter-1-2> = void]("s")).String::c_str()' from 'const char*' to 'esphome::optional<std::__cxx11::basic_string<char> >'
   88 |       return (
      |              ~
   89 |         (hours ? String(hours) + "h " : "") +
      |         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   90 |         (minutes ? String(minutes) + "m " : "") +
      |         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   91 |         (String(seconds) + "s")
      |         ~~~~~~~~~~~~~~~~~~~~~~~
   92 |       ).c_str();
      |       ~~~~~~~^~
      |              |
      |              const char*
*** [/data/irrigation-valve-ctrl-unit-a/.pioenvs/irrigation-valve-ctrl-unit-a/src/main.cpp.o] Error 1
========================== [FAILED] Took 7.74 seconds ==========================

Thanks for assisting.

Yeah, I got the same error when I insert that code on the text_sensor. but if I update the sensor from somewhere else with id(zone_1_time_remaining).publish_state() it works perfectly.

I’m also working on a 8 zones Sprinkler Controller using this component and this might be interesting for you:

This is a section of my dashboard
Screenshot 2022-09-19 152500

I created a sensor to display the percentage of the valve run and used your idea to have the human readable time remaining next to it.

The percentage sensor also updates the text_sensor solving the issue with the compile error.

This is my sensor code:

  - platform: template
    id: zone1_progress_percentage
    name: "Zone 1 Progress Percentage"
    update_interval: 1s
    lambda: |-
      if(id(lawn_sprinkler_ctrlr).active_valve().value_or(-1) == 0)
      {
        int seconds = round(id(lawn_sprinkler_ctrlr).time_remaining().value_or(0));
        int days = seconds / (24 * 3600);
        seconds = seconds % (24 * 3600);
        int hours = seconds / 3600;
        seconds = seconds % 3600;
        int minutes = seconds /  60;
        seconds = seconds % 60;
        id(zone_1_time_remaining).publish_state((
          (days ? String(days) + "d " : "") +
          (hours ? String(hours) + "h " : "") +
         (minutes ? String(minutes) + "m " : "") +
         (String(seconds) + "s")
          ).c_str());
        return ((id(lawn_sprinkler_ctrlr).valve_run_duration_adjusted(0) - id(lawn_sprinkler_ctrlr).time_remaining().value_or(0)) * 100 / id(lawn_sprinkler_ctrlr).valve_run_duration_adjusted(0));
      }
      if (id(lawn_sprinkler_ctrlr).active_valve().has_value() == false && id(lawn_sprinkler_ctrlr_status).state == "Idle")
      {
        int seconds = round(id(lawn_sprinkler_ctrlr).valve_run_duration_adjusted(0));
        int days = seconds / (24 * 3600);
        seconds = seconds % (24 * 3600);
        int hours = seconds / 3600;
        seconds = seconds % 3600;
        int minutes = seconds /  60;
        seconds = seconds % 60;
        id(zone_1_time_remaining).publish_state((
          (days ? String(days) + "d " : "") +
          (hours ? String(hours) + "h " : "") +
         (minutes ? String(minutes) + "m " : "") +
         (String(seconds) + "s")
          ).c_str());
        return 0;
      }
      else if(id(zone1_progress_percentage).state > 1 && id(lawn_sprinkler_ctrlr_status).state != "Paused")
      {
        int seconds = 0;
        int days = seconds / (24 * 3600);
        seconds = seconds % (24 * 3600);
        int hours = seconds / 3600;
        seconds = seconds % 3600;
        int minutes = seconds /  60;
        seconds = seconds % 60;
        id(zone_1_time_remaining).publish_state((
          (days ? String(days) + "d " : "") +
          (hours ? String(hours) + "h " : "") +
         (minutes ? String(minutes) + "m " : "") +
         (String(seconds) + "s")
          ).c_str());
        return 100;
      }

This is my text_sensor:

  - platform: template
    id: zone_1_time_remaining
    name: "Zone 1 Time Remaining"

You also are going to need another text_sensor that displays the status of the controller. It is still a work in progress so it has some bugs. If you can also help improve this, it will be great.

  - platform: template
    id: lawn_sprinkler_ctrlr_status
    name: "Lawn Sprinklers Status"
    update_interval: 1s

This last text_sensor relays on this three switch:

  - platform: template
    id: lawn_sprinkler_ctrlr_run
    name: "Sprinkler Controller Run"
    optimistic: true
    on_turn_on:
      - text_sensor.template.publish:
          id: lawn_sprinkler_ctrlr_status
          state: "Running"
      - sprinkler.resume_or_start_full_cycle: lawn_sprinkler_ctrlr
      - switch.turn_off: lawn_sprinkler_ctrlr_pause
    
  - platform: template
    id: lawn_sprinkler_ctrlr_pause
    name: "Sprinkler Controller Pause"
    optimistic: true
    turn_on_action:
      
      - delay: 500ms
      - lambda: |-
          if(id(lawn_sprinkler_ctrlr_status).state != "Running")
          {
            id(lawn_sprinkler_ctrlr_pause).turn_off();
          }
      - lambda: |-
          if(id(lawn_sprinkler_ctrlr_status).state == "Running")
          {
            id(lawn_sprinkler_ctrlr_status).publish_state("Paused");
            id(lawn_sprinkler_ctrlr).pause();
          }

    on_turn_off:
      lambda: |-
        if(id(lawn_sprinkler_ctrlr_status).state == "Paused")
        {
          id(lawn_sprinkler_ctrlr_status).publish_state("Running");
          id(lawn_sprinkler_ctrlr).resume();
        }


  - platform: template
    id: lawn_sprinkler_ctrlr_stop
    name: "Sprinkler Controller Stop"
    turn_on_action:
      - text_sensor.template.publish:
          id: lawn_sprinkler_ctrlr_status
          state: "Idle"
      - sprinkler.resume: lawn_sprinkler_ctrlr
      - delay: 25ms
      - sprinkler.shutdown: lawn_sprinkler_ctrlr
      - switch.turn_off: lawn_sprinkler_ctrlr_pause
      - switch.turn_off: lawn_sprinkler_ctrlr_run

I hope this can be helpfull in your proyect.

@SebaVT, thank you for confirming the error. and how to resolve it. I like the direction you’re heading. especially the dashboard layout. It’s exactly what I’m looking for. I’ll adopt your approach to make things easier for both of us as I move. That means rewriting some code, adapting most of your naming conventions. I don’t have much time to work on these changes over the next few days so it may be Friday before I post an update.

Thanks again!

I should use $devicename as you did.

I find substitutions work very well. If you change to using $devicename (and other substitutions) you’ll find it easier to update code. Updating or changing the name in a substitution list is easier than searching for and changing multiple instances of a name in ones code.

Forgot to mention; like yourself I’m heading towards an eight zone sprinkler controller. I’m currently coding two four zone units. That’s why I’m using substitutions named ‘unit_id: A’ and ‘devicename_unit_id: a’. I’ll I need to do is copy my code and change these values to ‘B’ and ‘b’ respectively to create a second controller.

@SebaVT , I found time to modify my code, adding some of your thoughts to mine. After adding yours I noticed that “Lawn Sprinkler Status” was not updated when the device is started/restarted. To overcome this shortfall, I’ve added the following code under the esphome: section;

esphome:
    on_boot:
      priority: 800
      then:
        - text_sensor.template.publish:
            id: lawn_sprinkler_ctrlr_status
            state: "Idle" 

This addition also corrected the “Zone 1 Time Remaining” value showing as unknown when the device starts.

On another note: Is there really a need to add time remaining and percentage complete for each sprinkler? Doing so would result in 16 additional sensors on an 8 sprinkler controller. Since only one sprinkler can be active at any give point is time, one set of statistics should be suitable. To keep the number of sensors and length code at minimum, I’m going to add only one set of sensors at this point in time. What are your thoughts?

@rcblackwell I actually had that piece of code before posting it, I just forgot to share it here. The only difference is I set it with priority: -100 which sets that status after the controller is 100% bootup.

I also added this on the on_boot section:

      - sprinkler.set_multiplier:
          id: lawn_sprinkler_ctrlr
          multiplier: !lambda "return id(multiplier);"
      - sprinkler.set_valve_run_duration:
          id: lawn_sprinkler_ctrlr
          valve_number: 0
          run_duration: !lambda "return id(z1_duration)* 60;"

This is to load the global variables that storage the multiplier and the valve_run_duration in case the controller reboots.

globals:
  - id: multiplier
    type: int
    restore_value: true
    initial_value: '1'

  - id: z1_duration
    type: int
    restore_value: true
    initial_value: '15'

Blockquote
On another note : Is there really a need to add time remaining and percentage complete for each sprinkler? Doing so would result in 16 additional sensors on an 8 sprinkler controller. Since only one sprinkler can be active at any give point is time, one set of statistics should be suitable. To keep the number of sensors and length code at minimum, I’m going to add only one set of sensors at this point in time. What are your thoughts?

I’m planning to create an Irrigation routine that depends on the precipitation rates for each zone, rainfall and moisture sensors. The idea is to water the lawn as much efficiently as possible, and for that I will need each zone to have their own set of sensors to calculate the statistics.

I already compiled with all the 8 zones and the ESP32 still has a lot of flash and RAM available.