Custom UART Text Sensor split text into multiple sensors

I am working on a project to send serial data to HA and I needed to be able to split a string into 3 sensors. Many examples on how to put a single line into a text sensor, but nothing on splitting text.

I figured it out with some trial and error but figured I would post this here for the next time I go looking on how to do this :slight_smile:

uart_read_line_sensor.h

#include "esphome.h"

class UartReadLineSensor : public Component, public UARTDevice, public TextSensor {
 public:
  TextSensor *playtime = new TextSensor();
  TextSensor *switchhits = new TextSensor();
  TextSensor *score = new TextSensor();
  UartReadLineSensor(UARTComponent *parent) : UARTDevice(parent) {}

  void setup() override {
    // nothing to do here
  }

  int readline(int readch, char *buffer, int len)
  {
    static int pos = 0;
    int rpos;

    if (readch > 0) {
      switch (readch) {
        case '\n': // Ignore new-lines
          break;
        case '\r': // Return on CR
          rpos = pos;
          pos = 0;  // Reset position index ready for next time
          return rpos;
        default:
          if (pos < len-1) {
            buffer[pos++] = readch;
            buffer[pos] = 0;
          }
      }
    }
    // No end of line has been found, so return -1.
    return -1;
  }

  void loop() override {
    const int max_line_length = 80;
    static char buffer[max_line_length];
    while (available()) {
      if(readline(read(), buffer, max_line_length) > 0) {
        playtime->publish_state(strtok(buffer, ":"));
        switchhits->publish_state(strtok(NULL, ":"));
        score->publish_state(strtok(NULL, ":"));
      }
    }
  }
};

Here is the ESP home config:

substitutions:
  device_name: pinball-logger
  friendly_name: pinball logger
  
esphome:
  name: ${device_name}
  platform: ESP8266
  board: d1_mini
  includes:
    - uart_read_line_sensor.h

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_password
  fast_connect: true


logger:
  level: VERBOSE #makes uart stream available in esphome logstream
  baud_rate: 0 #disable logging over uart

api:
  
  
ota:

uart:
  id: uart_bus
  tx_pin: D0
  rx_pin: D1
  baud_rate: 9600

text_sensor:
- platform: custom
  lambda: |-
    auto my_custom_sensor = new UartReadLineSensor(id(uart_bus));
    App.register_component(my_custom_sensor);
    return {my_custom_sensor->playtime, my_custom_sensor->switchhits, my_custom_sensor->score};
  text_sensors:
  - name: "playtime"
  - name: "switchhits"
  - name: "score"
4 Likes

Thank you for taking the time to document this here! Really useful.

1 Like

If you have a combination of different sensor types in your serial data, for example, a text_sensor and a (float) sensor, this is a bit more complicated. I solved it by making use of some kind of global variable to pass the data to the other sensor type. Like below:

    #include "esphome.h"
    
    namespace MyTextData {
      TextSensor *my_text_sensor = new TextSensor();
    }
    
    class MyTextSensor : public TextSensor {
      public:
        TextSensor *my_text_sensor = MyTextData::my_text_sensor;
    };
    
    class MySensor : public Sensor {
      public: Sensor *my_sensor = new Sensor();
      ...
      my_sensor->publish_state(...);
      MyTextData::my_text_sensor->publish_state(...);
    }

And then in the yaml file you can register the different components:

    text_sensor:
      - platform: custom
        lambda: |-
          auto textsensor = new MyTextSensor();
          App.register_component(textsensor);
          return {textsensor->my_text_sensor};
        text_sensors:
          - name: my_text_sensor
    
    sensor:
      - platform: custom
        lambda: |-
          auto sensor = new MySensor();
          App.register_component(sensor);
          return {sensor->my_sensor};
        sensors:
          - name: my_sensor

By the way, it’s simpler to do
id(my_sensor).publish_state(...)
id(my_text_sensor).publish_state(...)
from within your c++ instead of using the globals but it is a nice trick and can be really useful.

1 Like

Thank you Phillip, I will give that a try!

image

Hey im new to esphome. im able to get values from uart. i have configuerd ardunio to send data to esphome. but i need to split my values. because im sending two values like “0.00V 0.00V” this is come from valtage sensore. im monitoing two battories now. so need to to split this value to two value like “0.00V” and “0.00V”. anybody can help me ?

I’m onto doing something similar to what @euchre did in the OP above, but my case is slightly different:
I have three-character status messages coming in, with one of them having some extra payload separated with spaces. I would like to populate the normal/ordinary “publish_state” return value in both cases (extra payload or not), whilst only updating the extra text sensors when there is payload data available. (otherwise they should remain static)
Am I making sense? :slight_smile:
This is how far I have understood it, but C++ is not my “native” language, and I’m really stuggeling to understand what kind of syntactical errors I have made here. :pensive:

...trunkated. This part of the include file ...
    int ChRead;
    while (available()) {
      ChRead = readline(read(), buffer, max_line_length);
      if(ChRead = 3) publish_state(buffer);
      if(ChRead > 3) {
        publish_state(strtok(buffer, " "));
        localtime->publish_state(strtok(NULL, " "));
        watercl->publish_state(strtok(NULL, " "));
      }
    }

And this is the part in YAML:

text_sensor:
- platform: custom
  lambda: |-
    auto my_custom_sensor = new UartReadLineSensor(id(uart_bus));
    App.register_component(my_custom_sensor);
    return {my_custom_sensor, my_custom_sensor->localtime, my_custom_sensor->watercl};
  text_sensors:
    - id: "uart_readline"
      on_value:
        then:
          - lambda: |-
              if (x == "CCS") id(UV_cycle).publish_state(true);
              if (x == "CCE") id(UV_cycle).publish_state(false);
             
              if (x == "UVE") id(UVLampSensor).publish_state(true);
              if (x == "UVD") id(UVLampSensor).publish_state(false);
             
              if (x == "PME") id(WaterPumpMotorSensor).publish_state(true);
              if (x == "PMD") id(WaterPumpMotorSensor).publish_state(false);
             
              if (x == "TPA") id(FootSwitchSensor).publish_state(true);
              if (x == "TPR") id(FootSwitchSensor).publish_state(false);
    - name: "localtime"
    - name: "watercl"

Sorry for the noob question; I’ve just got the feeling that I’m not too far off from what I want to accomplish :smirk: