How can I edit a text string in a Text Template (mainly to handle escaped characters)

I am using a Text Template and LVGL to be able to enter a text string on the ESPHome webpage (or to set it with Home Assistant) and, when the string value is changed, it is then used to set a new value for an LVGL Label widget.

Along the way, I need to be able to handle escaped characters. For instance, if I change the text in the Text Template to

Warning: Laser in use

it’ll display fine on the LCD, but if I want to make a 2 line label, like:

Warning: Laser in use
Put on safety goggles
before entering

I can’t split it into different lines. If, in the YAML file, I use double quotes and use escaped characters like “\n”, they will be handled during compile time. But if I want to change the value of the text in the Label widget later, I don’t see a way to automatically do that when setting the value of the Label.

How can I take the state, or text string, from a Text Template, change occurrences of escaped characters like “\n” and change them to a newline character? I know, in C++, there’s a function replace(), so I could use std::replace, but that only replaces one character with one other.

But I haven’t used C++ in 15-20 years and I’m not sure if the files I need to do this are included during compiling or how to do this in a lambda when I get the text string value from the Text Component.

Is this possible to do? If so, how would I do it?

I’m just guessing at this, but I am wondering if this is close to what to do.

Since I can get the pointer to the string with !lambda id(test_text).state.c_str(); that tells me that id(test_text).state.c_str() does return the pointer to the string. I’ve also found std:string::replace() and one example I found of it is s.replace(s.find("$name"), sizeof("$name") - 1, "Somename"); And I know “s” is the string. I’m not sure about whether I’d be using a pointer to a string or the string, but I would think if I did this:

id(test_text).state.c_str().replace(id(test_text).state.c_str().find("\n"), sizeof("\n") - 1, "<newline character here>");

That would work - but I don’t know how to specify the newline character in the string, or if I need to do more to escape the “\n” when I use it, since I would think that would be escaped and would be read as the newline character. I thought of testing the code in a few different ways, but I’m not even sure of the basics of this to start with.

Where does the template text get its contents from? Surely you can insert actual newline characters at source.

I use the Template Text so I can enter the value from the ESPHome web page - so I can type it in and change it whenever I need to.

I don’t know if this is of some use.

I’m tired and thinking sluggishly, but, at first glance, I think this does what I need it to do. I’ll test it and report on what I get when I’m not so drowsy! Thanks!

1 Like

Yes - I couldn’t wait and, luckily, it didn’t take a lot of focus to deal with it.

However, there were a couple issues. Odd, but this lambda function is used 2 times in that file and they’re different. For some reason, when I clicked the link, even though the version you linked to was highlighted, my eyes were drawn to the one in the next component, below it. I used that, then realized there were two versions right near each other. I did have one issue. First, the code I used to test it:

          data: !lambda |-
            std::size_t pos;
            std::string str = id(esp_motor_movement_uart_command).state;
            while ((pos = str.find("\\r")) != std::string::npos)
              str.replace(pos, 2, "\r");
            std::vector<uint8_t> vec(str.begin(), str.end());
            return vec; 

I changed “ha_scobot_text_command” to refer to the Template Text I’m using, but I kept getting an error:

Narvi.yaml:205:18: error: could not convert 'vec' from 'std::vector<unsigned char>' to 'std::string' {aka 'std::__cxx11::basic_string<char>'}

So the 2nd to last line in there kept throwing errors. I finally commented it out and used str as the return value. Here it is, from my YAML file:

            text: !lambda |-
              std::size_t pos;
              std::string str = id(test_text).state;
              while ((pos = str.find("\\n")) != std::string::npos)
                str.replace(pos, 2, "\n");
              return str;

Once I did that, it ran without error and behaved as needed.

I was thinking I’d have to squeeze it all in for one line. I had completely forgot that lambdas could be multi-line, so thank you for sharing this with me, since it breaks a barrier for me and makes it much easier for me to see how to use code in my YAML files, instead of just using one line when I can do it. I haven’t written anything in C++ for at least 15 years, but really liked using it, so this opens up the chance for me to do a lot in C++ with ESPHome (which removes a lot of limits to what I can do here), as well as solving the immediate problem.

1 Like

There is also the substitute filter but that doesn’t like special characters so much.

Interesting. So it wouldn’t like dealing with control characters like newline?

So would I use the Text Sensor to read the value from the Template Text and then the Text Sensor would be able to edit the original value?

Not exactly sure. You could try.

Apparently under the hood it strips whitespace at least.

You could adjust this to try.

    filters:
      - substitute:
        - "\\r -> \r"

Have you come across filters: yet?
They are quite handy and powerful.
You can either add them on to your original template text (I think) or add them to a copy and keep both variants.

No - looks like another cool and useful tool, so thanks!

1 Like

I’m not sure if or how filters works here. I have to use a lambda to get the state of the Text Template in the first place. So if I use a filter, within the Text Template component (in the on_value section), what does the filter act on? The actual value of the state of the template?

Filters act on the value of the sensor they are placed under.

Multiple filters can be chained together which act on the output of the prior filter.

So in your case, you could:

  • Might be able to add it to your original text sensor
  • alternatively, take a copy of the original sensor using a template or copy sensor and apply the filter to that (if you want separate sensors for original and processed)

If you share a bit more config I can help more specifically.

Here’s an example for a regular sensor. Same principles apply for text sensors.
Also note how you can reference current value using ‘x’.

##########################################################################################
# Time of Flight sensor  - i2c
##########################################################################################
#Powered via 5v
  - platform: vl53l0x
    id: tof
    i2c_id: vl53l0x_i2c_bus
    # setup_priority: 300
    name:  ToF
    internal: false 
    address: 0x29
    timeout: 300ms
    update_interval: never
    entity_category: diagnostic
    # 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. 
          window_size: 10
          send_every: 10
          send_first_at: 10
    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
    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 / 5) * 5;
    on_value:
      then:
       - binary_sensor.template.publish:
          id: water_tank_level_recieved
          state: ON