Send Template Text Sensor state to HA only at change

Delta filter is available for sensors to block publishing values if there was no change, but is there such option or trick/ workaround possible for Template Text Sensor?

I would like to only publish the values once they have changed.

1 Like

sensor.template.publish Action

You can also publish a state to a template sensor from elsewhere in your YAML file with the sensor.template.publish action.

I was thinking about this, but dropped the idea, because I couldnā€™t figure a way to trigger sensor.template.publish action only when the state changes.

Itā€™s not even clear to me that in which sensor I should fire this? In the sensor from which I want to publish the new state or in the sensor into which I want the state to be published?

I have the following text template sensor what updates and publishes its state every 5s in line with the update_interval. I would like to get the states published only when the state changes. I understand you propose to have another sensor and use sensor.template.publish to update it, but can you help me how?

text_sensor:
- platform: template
  id: drip_progress
  name: Drip Progress %
  update_interval: 5s
  icon: mdi:progress-clock
  lambda: !lambda |-
    auto ctrl = id(drip);
    auto active = ctrl->active_valve();
    if (!active.has_value()) {
      return std::string("--");
    }
    auto duration_adjusted = ctrl->valve_run_duration_adjusted(active.value());
      return std::to_string((uint32_t)(100.0 * (duration_adjusted - time_remaining.value())) / duration_adjusted);

The state of what? Can you clarify what state you wish to monitor on your lambda? Is it ā€œdripā€? Where does this come from? What is it? Is it a sensor, binary_sensor or another template? I do not have visibility on the base state you wish to use to trigger an automation. Automation can be triggered on most things; but you your lambda is completely unfamiliar to me.

The base states of this text template sensor are coming from ESPHomeā€™s sprinkler component.
These are active_valve(), valve_run_duration_adjusted(active.value(), time_remaining_active_valve(), etc.

The text template sensor calculates the progress percentages based on the values getting from these ESPHome native functions.

What I would like to avoid is that the template sensor publish the string ā€œā€“ā€ every 5 seconds if there is no active sprinkler valve.

I think your question is about the sprinkler component. I suspect I would write a lambda and/ or template to monitor change in state in those components and then trigger publish OR just running your lambda directly after state change. I would have to read how the sprinkler component works. If you donā€™t get an answer I would repost your question with correct title.

Reading the sprinkler component there is a section on states including running lambda to determine a state and therefore run an automation. Worth reading original on ESPHome and asking on a sprinkler discussion

Understanding the Sprinkler Controllerā€™s StateĀ¶

A number of people have asked about how to determine the sprinkler controllerā€™s state. This section aims to outline how to use the sprinkler controllerā€™s API to determine what it is doing, generally with the goal of indicating this on some form of display hardware. Note that this discussion largely revolves around C++ code (as is used in ESPHome lambdas).

Many of the methods used to determine the sprinkler controllerā€™s state return a type of value known as an optional. If you are curious, a general reference for the optionaltype may be found here, but what is important for now is:

  • The optional type may or may not contain a value
    • The method has_value() is used to determine if a value is present. For example: id(lawn_sprinkler_ctrlr).active_valve().has_value()
    • The method value() is used to determine the value, if it is determined that a value is present. For example: auto running_valve =id(lawn_sprinkler_ctrlr).active_valve().value()
  • The optional type can contain a value of any C++ type (bool, int, float, etc.) (In C++ terms, it is a template.)

The examples that follow illustrate use of the the sprinkler controllerā€™s methods within adisplay lambda. The examples are intended to illustrate a pattern and (for sake of brevity) are not complete; at very least youā€™ll need to fill out the display componentā€™s specific configuration details before you can use them.

With these points in mind, letā€™s discuss some of the methods which indicate the state of the sprinkler controller. Weā€™ll approach this from the angle of ā€œhow do Iā€¦ā€

How Do Iā€¦

  • **ā€¦determine if the sprinkler controller is running?**Use the method optional<size_t> active_valve() to check if there is an active valve. If the optional returned has_value(), the sprinkler controller is running and you may use the value() method to check which specific valve is active.Example:

display: - platform: ā€¦ # ā€¦display configurationā€¦ lambda: |- if (id(lawn_sprinkler_ctrlr).active_valve().has_value()) { // the controller is running, get the active valve into running_valve and print it auto running_valve = id(lawn_sprinkler_ctrlr).active_valve().value(); it.printf(0, 0, ā€œValve %u activeā€, running_valve); } else { // the controller is NOT running it.print(0, 0, ā€œIdleā€); }

Hi Julian,

I appreciate your help and time looking into this.
I am almost always doing my homework, before asking, just like I did it in this case.
I studied the documentation and was digging through the forum as well.

With full respect, this was not a sprinkler component question. I was asking if it was somehow possible to use some trick within a text template sensor to only return a new state if the text/string/value/state, whatever we call it has changed compared to the previous value.

For a template sensor, this is easily possible with the delta filter. Consequently one workaround to my issue can be that I setup a template sensor, monitoring the base states, using delta filter on it to filter out values with no change, then run a publish automation to a text template sensor (because I want to do some manipulation of the value).

However, all that would be much simpler, cleaner and easier if there was a native filter in ESPHome for text template sensor to store the last value passed through this filter and only passes incoming values through if incoming value is different from the previously passed one (again, similarly as delta sensor, but for text/ strings).

Why? What does it matter?

My intention is actually decrease the polling time of template sensors internally within ESPHome to every 1 seconds.

It may not causing issues, but feeling clumsy and unnecessary higher footprint/ burden on all participants of this messaging (ESPHome, HA, network, etc.) dealing with all these unnecessary messages. I mean not one particular device, but what if dozens or hundreds of devices are doing the same.

I am absolutely not sure how much impact this is, but again, feels like a lot of waste. Is this really doesnā€™t matter at all even in case of high number of devices?

I am pretty sure that esphome only publishes a sensor when the value changes.

Well, we are taking about a template text sensor what is running in every X seconds according to the update_interval what has been set.

Please see the below logs of ESPHome, when the update_interval is set to 1s.
Are you saying that ESPHome is not actually sending those states across the network to HA if the state is the same as the previous? I donā€™t think this is the case.

[13:12:26][D][api.connection:961]: Home Assistant 2023.5.2 (192.168.9.200): Connected successfully
[13:12:26][D][time:044]: Synchronized time: 2023-05-09 13:12:26
[13:12:26][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:27][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:28][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:29][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:30][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:31][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:32][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:33][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:34][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:35][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:36][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:37][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:38][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:39][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:40][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:41][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:42][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:43][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:44][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:45][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:46][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:47][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:48][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:49][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:50][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:51][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:52][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'
[13:12:53][D][text_sensor:064]: 'Drip Zone Time Remaining': Sending state '0s'

You should see my wind meter in a gale! Goes crazy with pulses!

If you force it to 1s it will report it every 1s.

Ummm, Nick I think youā€™ve lost traction here.
How do you think one should set the update_interval to only publishes values on value changes in case of a template sensor what is polling data from native ESPHome functions?

Anyhow, I figured a workaround (actually more) in the meantimeā€¦ I am just saying it would be really nice if ESPHome would have a filter for text template sensor for such cases.

Normally an Automation. I understand you may not want to do that.

Might be an idea to post your workaround for anybody who is also looking for help; and for all our knowledge

What you want to do makes perfect sense to me and Iā€™m also not aware of a ā€œdelta equivalentā€ which would be my go to for a numerical sensor.

I found on Discord Ssieb suggested adding this as a filter.

filters:
    - lambda: |-
        static std::string last;
        if (x == last)
          return {};
        last = x;
        return x;

Edit: Added filters node for clarity.

3 Likes

The workarounds are specific to my case and are actually involving different, more complex implementation with more bulky code. One of this is what you and I are already discussed: using automation and two sensors. One template sensor that that is monitoring the base states (using sprinkler component native functions) and I can use the delta filter there and I publish data to my text template sensor from there. Another one is that I learned that adding ā€˜durationā€™ device class to a sensor will be formatted as hh:mm:ss in HA, so I could even use this (and not publishing the data to the text template sensor), but I would loose the possibility to format the time more human readable (e.g. 1h 10m 10s).

However, finally we seems to have a promising approach from @Mahko_Mahko. This is great! Many thanks for this - I will do some testing tomorrow.

1 Like

I am just thinking do we still need another text template sensor to publish the data to and do the filtering with this lambda there or can we do within the original lambda whereas there are multiple if conditions? If yes, would you know how to add this to the below sensor?

text_sensor:
- platform: template
  id: test_sensor
  name: Test Sensor
  update_interval: 1s
  icon: mdi:progress-clock
  lambda: !lambda |-
    auto ctrl = id(sprinkler_ctrl);
    auto active = ctrl->active_valve();
    if (!active.has_value()) {
      return std::string("--");
    }
    auto time_remaining = ctrl->time_remaining_active_valve();
    if (!time_remaining.has_value()) {
      return std::string("00");
    }
    int seconds = round(id(sprinkler_ctrl).time_remaining_active_valve().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;
      return {
        ((days ? String(days) + "d " : "") +
        (hours ? String(hours) + "h " : "") +
        (minutes ? String(minutes) + "m " : "") +
        (String(seconds) + "s")
        ).c_str()};

I think you have options. I think the best solution might just be up to your preference. Iā€™m not sure of performance differences or best practice. I think possibly it all gets optimised during compiles anyway. Dunno much about that though.

Note how the filter: is a different node to the lambda:

During development (and often permanently) , I usually use ā€œcopy sensorsā€ (option 2). I mark the ā€œintermediateā€ ones as internal and mark the one I want to send to HA as external. I find it easier to manage and debug like this.

But sometimes I merge from option 2 to option 1 once everything is running well (less config). But often donā€™t bother.

I quite like keeping the filters seperate as I find it easier to follow/track the logic (especially when I look at it later).

#Option 1: Filter on original sensor

text_sensor:
  - platform: template
    id: test_sensor
    name: Test Sensor
    update_interval: 1s
    icon: mdi:progress-clock
    lambda: !lambda |-
      auto ctrl = id(sprinkler_ctrl);
      auto active = ctrl->active_valve();
      if (!active.has_value()) {
        return std::string("--");
      }
      auto time_remaining = ctrl->time_remaining_active_valve();
      if (!time_remaining.has_value()) {
        return std::string("00");
      }
      int seconds = round(id(sprinkler_ctrl).time_remaining_active_valve().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;
        return {
          ((days ? String(days) + "d " : "") +
          (hours ? String(hours) + "h " : "") +
          (minutes ? String(minutes) + "m " : "") +
          (String(seconds) + "s")
          ).c_str()};
    filters: ##########New
     - lambda: |-
          static std::string last;
          if (x == last)
            return {};
          last = x;
          return x;
        
#Option 2: Filter on "copy sensor"         
text_sensor:
  - platform: template
    id: test_sensor
    name: Test Sensor
    internal: true ##########New
    update_interval: 1s
    icon: mdi:progress-clock
    lambda: !lambda |-
      auto ctrl = id(sprinkler_ctrl);
      auto active = ctrl->active_valve();
      if (!active.has_value()) {
        return std::string("--");
      }
      auto time_remaining = ctrl->time_remaining_active_valve();
      if (!time_remaining.has_value()) {
        return std::string("00");
      }
      int seconds = round(id(sprinkler_ctrl).time_remaining_active_valve().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;
        return {
          ((days ? String(days) + "d " : "") +
          (hours ? String(hours) + "h " : "") +
          (minutes ? String(minutes) + "m " : "") +
          (String(seconds) + "s")
          ).c_str()};
        
  - platform: copy
    source_id: test_sensor
    name: "Test Sensor For HA"
    internal: false
    filters: 
     - lambda: |-
          static std::string last;
          if (x == last)
            return {};
          last = x;
          return x;
        
#Option3: Merge it into your main lambda. Not sure how to do that from top of my hea

Credit really goes to @ssieb on this solution since I found out on a search from one of his Discord responses for the same query.

3 Likes