Help with sending and receiving serial data

Hi. I am afraid I am a bit lost :frowning: I am trying to send and receive serial data using ESPHome.

Use case: I have a hot water cylinder temperature monitor with a serial data port. If I send a “R”, it returns a comma deiminated string containing the data I require.

Essentially, I would like to end up with an automation which runs every minute sending “R” to a ESP8266, and displaying the text response. I think I want to implement this: Custom UART Text Sensor — ESPHome

Is ESPHome a reasonable way to do this? This is my ESP yaml:

esphome:
  name: hwc-monitor
  includes:
    - uart_read_line_sensor.h

esp8266:
  board: d1_mini

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


# Enable Home Assistant API
api:
  encryption:
    key: "OSvqW/yDWlIwdFtaHHFBimuXhic5jmF6lTQO5new+q4="

ota:
  password: "48715da5f021aad02d4f4ce78f9e1c86"

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

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Hwc-Monitor Fallback Hotspot"
    password: "ADekUGzzM6Kf"

captive_portal:

uart:
  id: uart_bus
  tx_pin: GPIO1 
  rx_pin: GPIO3 
  baud_rate: 57600

text_sensor:
- platform: custom
  lambda: |-
    auto my_custom_sensor = new UartReadLineSensor(id(uart_bus));
    App.register_component(my_custom_sensor);
    return {my_custom_sensor};
  text_sensors:
    id: "uart_readline"    
    
switch:
  - platform: uart
    name: "Send_R"
    data: 'R'
  

And this the uart_read_line_sensor.h

#include "esphome.h"

class UartReadLineSensor : public Component, public UARTDevice, public TextSensor {
 public:
  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) {
        publish_state(buffer);
      }
    }
  }
};

I have used a wemos D1 mini connected - it is programed as above, and installed as an ‘integration’ in Home Assistant.

Problems:
What changes do I need to make to the esp yamil (or .h file) to let me create the automation to send a “R” every 60 seconds, and display the response?

Currently I dont see any sensor, etc associated with this device that I can add to the dashboard…

Thank you for any help!

I changed the Tx and Rx to GPIO1 and GPIO3.

I got the ‘switch’ set up on the HA dashboard.

When I move the switch (initially missed, now added), I successfully send ‘R’ and get the correct response seen in the ESPhome log.

So my question :slight_smile: How do I get this log data (initially un-parsed) to be displayed on the HA dashboard? And I presume I will then be able to use the data to drive an automation (once it is parsed)?

All solved :slight_smile:

Argh Andrew you should have mentioned Paladin in here somewhere LOL… I’ve been figuring this out all by myself and then stumbled upon your post :slight_smile:

Would you mind sharing your final ESPHome config YAML and any rules in HA?

1 Like

Hi Tommy - No worries… This was the solution I used: Custom UART Text Sensor — ESPHome

You will need the uart_read_line_sensor.h from the link. I then parsed the data in Node-red.

I am also updating the Paladin with the ‘real’ time, in an effort to be sure the 60 deg night top up occurs during my night rate time, but as close to 6am as possible - so I am doing a bit of a fudge factor with the time but seems to be working…

esphome:
  name: hwc-monitor
  includes:
    - uart_read_line_sensor.h

globals:
   - id: my_global_int
     type: int
     restore_value: no
     initial_value: '0'

esp8266:
  board: d1_mini

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

# Enable Home Assistant API
api:
  encryption:
    key: "xxxxxxxxxxxxxxx"


ota:
  password: "xxxxxxxxxxxxxxxxxxxxx"

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

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Hwc-Monitor Fallback Hotspot"
    password: "xxxxxxxxxxxxxx"

captive_portal:

uart:
  id: uart_bus
  tx_pin: GPIO1 #D3
  rx_pin: GPIO3 #D4
  baud_rate: 57600

text_sensor:
- platform: custom
  lambda: |-
    auto my_custom_sensor = new UartReadLineSensor(id(uart_bus));
    App.register_component(my_custom_sensor);
    return {my_custom_sensor};
  text_sensors:
    name: "HWC Data Sensor"
    id: "uart_readline"    
    
switch:
  - platform: uart
    name: "Send_R"
    data: 'R'

  - platform: uart
    name: "Send_6"
    data: '6'

  - platform: uart
    name: "Send_4"
    data: '4'    

# Example configuration entry
sensor:
  - platform: wifi_signal
    name: "HWC ESP WiFi Signal Sensor"
    update_interval: 60s

time:
- platform: homeassistant
  id: hatime
  timezone: Etc/GMT+0 


  on_time_sync:
    then:
      - if:
         condition:
           lambda: return id(my_global_int) == 0;
         then:  
              - uart.write: !lambda |-
                  char buf[20];
                  sprintf(buf, "T%lu\n", hatime->now().timestamp + 43200 + 3600 - (3 * 3600) );
                  std::string s = buf;
                  return std::vector<unsigned char>( s.begin(), s.end() );
                  id(my_global_int) += 1;
           
  on_time:
      # SUMMER: Update time every day @ 1800hrs. Need + 12hr correction (GMT), + 1hr correction DST, - 5hr correction so heating occurs at 0500 not midnight!!
      - seconds: 0
        minutes: 0
        hours: 6
        months: 9 - 2
        then:
           - logger.log: "It's 1800hrs - Time Sent!"
           - uart.write: !lambda |-
              char buf[20];
              sprintf(buf, "T%lu\n", hatime->now().timestamp + 43200 + 3600 - (5 * 3600) );
              std::string s = buf;
              return std::vector<unsigned char>( s.begin(), s.end() );

      # WINTER: Update time every day @ 1800hrs. Need + 12hr correction (GMT), + 1hr correction DST, - 3hr correction so heating occurs at 0300 not midnight!!
      - seconds: 0
        minutes: 0
        hours: 6
        months: 3 - 8
        then:
           - logger.log: "It's 1800hrs - Time Sent!"
           - uart.write: !lambda |-
              char buf[20];
              sprintf(buf, "T%lu\n", hatime->now().timestamp + 43200 + 3600 - (3 * 3600) );
              std::string s = buf;
              return std::vector<unsigned char>( s.begin(), s.end() );

Hey guys @mulcmu shared this approach recently which may be of interest.

Thanks @andypnz I’m hoping to get it all parsed in ESPHome so we will see how that goes.
Have you had any luck with controlling the Paladin via serial? I’d like to boost it between 9pm and midnight when we get free power.

Interesting on the night time stuff and clocks…
My plan was to use Home Assistant to set the max temp to 10 degrees when the sun goes down… And then set it to 73 degrees from 9pm to midnight which is when we get free power from Contact. Then set it back to 10 degrees till the sun comes up.
Setting the max to 10 degrees does appear to nicely “halt” any heating going on… And then setting it back to 73

Depending on when you use your hot water, etc I think there are several useful things which could be played with to improve efficiency. I am really surprised by the loss of temp overnight. So for example, delaying the Paladin transfer till after lunch if you don’t use the hot water till the evening (including predicted solar, etc) may be best - ie don’t heat the water until you require it… Anyway, lots of interesting stuff to play with :slight_smile:

Hi @TommySharpNZ,

Would you mind sharing your current esphome config as well please?

I’d like to crawl before I walk.

Hi @del13r no problem at all… I’ve excluded all the regular ESP home stuff and just have the Paladin specific stuff…

# Enable time component to reset at midnight
time:
  - platform: sntp
    id: my_time
uart:
  id: uart_bus
  tx_pin: 1
  rx_pin: 3
  baud_rate: 57600    
  debug:
    direction: RX
    dummy_receiver: true
    after:
      delimiter: "\r\n"
    sequence:
      - lambda: |-
          UARTDebug::log_string(direction, bytes);  //Still log the data
          float sensors_values[20] = {0};  //array to hold the converted float values
         
          //Example to convert uart text to string
          std::string str(bytes.begin(), bytes.end());
          //watch for potential problems with non printable or special characters in string
          id(paladin_rawstring).publish_state(str.c_str());
         
          if (sscanf(str.c_str(), "%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f,%f", &sensors_values[0], &sensors_values[1], &sensors_values[2], &sensors_values[3], &sensors_values[4], &sensors_values[5], &sensors_values[6], &sensors_values[7], &sensors_values[8], &sensors_values[9], &sensors_values[10], &sensors_values[11], &sensors_values[12], &sensors_values[13], &sensors_values[14], &sensors_values[15], &sensors_values[16], &sensors_values[17], &sensors_values[18], &sensors_values[19]) == 20 ) {

          // 1 = Mains Watts Now (-ve = Grid In)
          // 2 = Transfer Watts Now
          // 3 = Solar Watts Now
          // 4 = Hot Water Temp
          // 5 = Delta Temp
          // 6 = Min temp Set
          // 7 = Max Temp Set
          // 8 = Total Mains IN (all Watt Hrs below)
          // 9 = Total Mains OUT
          // 10 = Total Transfer
          // 11 = Total TopUp
          // 12 = Total Solar
          // 13 = Hours to Health Top Up
          // 14 = Inverter Active Flag
          // 15 = Charger Active Flag
          // 16 = Force Export Flag
          // 17 = Export Active
          // 18 = Import Active
          // 19 = Overflow
          // 20 = ESP Time
              // Array numbering starts at [0] for the first value
              id(paladin_mainswattsnow).publish_state(sensors_values[0]);
              id(paladin_transferwattsnow).publish_state(sensors_values[1]);
              id(paladin_solarwattsnow).publish_state(sensors_values[2]);
              id(paladin_tanktemp).publish_state(sensors_values[3]);
              id(paladin_deltatemp).publish_state(sensors_values[4]);
              id(paladin_mintempset).publish_state(sensors_values[5]);
              id(paladin_maxtempset).publish_state(sensors_values[6]);
              id(paladin_totalmainsin).publish_state(sensors_values[7]);
              id(paladin_totalmainsout).publish_state(sensors_values[8]);
              id(paladin_totaltransfer).publish_state(sensors_values[9]);
              id(paladin_totaltopup).publish_state(sensors_values[10]);
              id(paladin_totalsolar).publish_state(sensors_values[11]);
              id(paladin_hourstomht).publish_state(sensors_values[12]);
              id(paladin_inverteractive).publish_state(sensors_values[13]);
              id(paladin_chargeractive).publish_state(sensors_values[14]);
              id(paladin_forceexport).publish_state(sensors_values[15]);
              id(paladin_exportactive).publish_state(sensors_values[16]);
              id(paladin_importactive).publish_state(sensors_values[17]);
              id(paladin_overflow).publish_state(sensors_values[18]);
          }
interval:
  - interval: 10s
    then:
      - uart.write: 'A'  
sensor:
  - platform: template
    name: "Overflow"
    id: "paladin_overflow"
  - platform: template
    name: "Import Active"
    id: "paladin_importactive"
  - platform: template
    name: "Export Active"
    id: "paladin_exportactive"
  - platform: template
    name: "Force Export"
    id: "paladin_forceexport"
  - platform: template
    name: "Charger Active"
    id: "paladin_chargeractive"
  - platform: template
    name: "Inverter Active"
    id: "paladin_inverteractive"
  - platform: template
    name: "Total Solar"
    id: "paladin_totalsolar"
    icon: "mdi:transmission-tower"
    unit_of_measurement: "W"
    device_class: "power"
  - platform: template
    name: "Total Topup"
    id: "paladin_totaltopup"
    icon: "mdi:transmission-tower"
    unit_of_measurement: "W"
    device_class: "power"
  - platform: template
    name: "Total Transfer"
    id: "paladin_totaltransfer"
    icon: "mdi:transmission-tower"
    unit_of_measurement: "W"
    device_class: "power"
  - platform: template
    name: "Total Mains Out"
    id: "paladin_totalmainsout"
    icon: "mdi:transmission-tower"
    unit_of_measurement: "W"
    device_class: "power"
  - platform: template
    name: "Total Mains In"
    id: "paladin_totalmainsin"
    icon: "mdi:transmission-tower"
    unit_of_measurement: "W"
    device_class: "power"
  - platform: template
    name: "Mains Watts Now"
    id: "paladin_mainswattsnow"
    icon: "mdi:transmission-tower"
    unit_of_measurement: "W"
    device_class: "power"
  - platform: template
    name: "Transfer Watts Now"
    id: "paladin_transferwattsnow"
    icon: "mdi:transfer-right"
    unit_of_measurement: "W"
    device_class: "power"
  - platform: template
    name: "Solar Watts Now"
    id: "paladin_solarwattsnow"
    icon: "mdi:solar-power"
    unit_of_measurement: "W"
    device_class: "power"
  - platform: template
    name: "Tank Temperature"
    id: "paladin_tanktemp"
    unit_of_measurement: "°C"
    icon: "mdi:coolant-temperature"
    device_class: "temperature"
    state_class: "measurement"
    accuracy_decimals: 2
  - platform: template
    name: "Delta Temperature"
    id: "paladin_deltatemp"
    unit_of_measurement: "°C"
    icon: "mdi:coolant-temperature"
    device_class: "temperature"
    state_class: "measurement"
    accuracy_decimals: 2
  - platform: template
    name: "Min temp Set"
    id: "paladin_mintempset"
    unit_of_measurement: "°C"
    icon: "mdi:temperature-celsius"
    device_class: "temperature"
    state_class: "measurement"
    accuracy_decimals: 2
  - platform: template
    name: "Max Temp Set"
    id: "paladin_maxtempset"
    unit_of_measurement: "°C"
    icon: "mdi:temperature-celsius"
    device_class: "temperature"
    state_class: "measurement"
    accuracy_decimals: 2
  - platform: template
    name: "Hours To MHT"
    id: "paladin_hourstomht"
    unit_of_measurement: "h"
    icon: "mdi:timer-outline"
    device_class: "duration"
    accuracy_decimals: 0  
    
button:
  - platform: restart
    id: restart_button
    name: "Restart"
  - platform: template
    id: paladin_serial_3
    name: "Set Min Temp 30 Degrees"
    on_press:
      - uart.write: '3'
  - platform: template
    id: paladin_serial_6
    name: "Set Min Temp 60 Degrees"
    on_press:
      - uart.write: '6'
  - platform: template
    id: paladin_serial_7
    name: "Set Min Temp 70 Degrees"
    on_press:
      - uart.write: '7'      
  - platform: template
    id: paladin_serial_9
    name: "Set Max Temp 10 Degrees"
    on_press:
      - uart.write: '9'
  - platform: template
    id: paladin_serial_m
    name: "Set Max Temp 73 Degrees"
    on_press:
      - uart.write: 'm'
  - platform: template
    id: paladin_serial_X
    name: "Reset to Default"
    on_press:
      - uart.write: 'X'      
text_sensor:
  - platform: template
    name: "Paladin Raw String"
    id: "paladin_rawstring"
1 Like

Thanks @TommySharpNZ

Here is a post I made regarding the issues I faced and how I solved them.

1 Like

Hi!

I’ve been really helped by this thread, but struggle with how to come up with a strategy to handle the text data that end up in the text sensor.

text_sensor:
- platform: custom
  lambda: |-
    auto my_custom_sensor = new UartReadLineSensor(id(uart_bus));
    App.register_component(my_custom_sensor);
    return {my_custom_sensor};
  text_sensors:
    name: "MKS925 data"
    id: "uart_readline"

I’ve setup a bunch of switches that send various commands to a pressure sensor via RS232. The length of the response is not the same for all commands.

Ideally, I’d like to be able to pass data from an input box in Home Assistant to some of the commands, but I have not figured out how to do that yet. If anybody knows how to feed data from HA to replace teh 2e2 below it would be very much appreciated.

switch:
- platform: uart
    name: "Write set point value"
    data: '@253SP1!2e2;FF'

Below is description of what I’m struggling with:

  1. I’ve setup an automation that trigger the switch every minute so that I get a current pressure reading in the text sensor.
alias: Vacuum pump station 1 - Query pressure
description: ""
trigger:
  - platform: time_pattern
    minutes: /1
condition: []
action:
  - service: switch.turn_on
    target:
      entity_id: switch.vacuum_pump_station_1_query_pressure
    data: {}
mode: single
  1. A helper template sensor is connected to the text sensor to read out the pressure reading.
{{states.sensor.vacuum_pump_station_1_mks925_data.state.split("ACK")[1] | float}}
  1. If I send another command that has nothing to do with reading back the pressure my pressure sensor get bad data.

So to summarize, I can’t find a good strategy for separating different type of responses from the RS232 device into different sensors in Home Assistant.

Any ideas welcome!

Hi again!

I found a solution that sort of works, but I welcome any tips on a proper solution!

This is how I kludged it together.

I created a few services in ESPHome for the sensor:

services:
    - service: publish_pressure
      variables:
        pressure: float
      then:
        - lambda: 'id(id_pressure).publish_state(pressure);'

    - service: query_pressure
      then:
        uart.write: '@253PR1?;FF'

    - service: write_set_point_value
      variables:
        pressure: int
      then:
        - uart.write: !lambda  
            char buf[80];
            sprintf(buf, "@253SP1!%d;FF", pressure);
            std::string s = buf;
            return std::vector<unsigned char>( s.begin(), s.end() );

    - service: write_set_point_hysteresis_value
      variables:
        pressure: int
      then:
        - uart.write: !lambda  
            char buf[80];
            sprintf(buf, "@253SH1!%d;FF", pressure);
            std::string s = buf;
            return std::vector<unsigned char>( s.begin(), s.end() );

and a template sensor:

sensor:
  - platform: template
    name: ${devicename} pressure
    id: id_pressure
    device_class: pressure
    unit_of_measurement: mbar

From Home Assistant I created automations that trigger either periodically of when I wish to change the set point for the relays. The automation uses an input number helper to get the data:

alias: Vacuum pump station 1 - Publish pressure
description: ""
trigger:
  - platform: time_pattern
    minutes: /1
condition: []
action:
  - service: esphome.vacuum_pump_station_1_query_pressure
    data: {}
  - delay:
      hours: 0
      minutes: 0
      seconds: 0
      milliseconds: 100
  - service: esphome.vacuum_pump_station_1_publish_pressure
    data:
      pressure: >-
        {{float(states.sensor.vacuum_pump_station_1_data.state.split("ACK")[1])}}
mode: single

alias: Vacuum pump station 1 - Write set point value
description: ""
trigger:
  - platform: state
    entity_id:
      - input_number.vacuum_pump_station_1_set_point_value
condition: []
action:
  - service: esphome.vacuum_pump_station_1_write_set_point_value
    data:
      pressure: "{{states.input_number.vacuum_pump_station_1_set_point_value.state}}"
mode: single

Using automations to trigger sending data to a service seems like the right thing for changing the set pionts, but it seems to me like there is probably a smarter way to update the ESPHome sensor called “pressure”.