How to read/translate serial data from solar hot water diverter : Paladin

I have a “Paladin” solar hot water diverter which diverts excess solar production into my hot water cylinder. It has a serial interface which I am trying to use to get the values like solar divert watts, hot water cylinder temp etc…

In basic language I’ve been told if a send “R” to the Paladin it will respond with;

Mains Watts Now (-ve = Grid In)
Transfer Watts Now
Hot Water Temp
Solar Watts Now
Inverter Active Flag
Charger Active Flag
Force Export Flag

I have a basic ESPHome configuration up and running that gives me a button to send the “R” character.

I have very little experience with serial, ascii, hex and all that so I’m hoping someone can help me work with the following data that I get back.

[15:37:34][D][uart_debug:114]: <<< FE:00:FC:00:00:00:F8:00:80:00:00:00:FD:00:C0:00:00:00:FE:00:00:00:00:00:FC:00:C0:00:00:FF:00:FC:00:C0:00:00:FF:00:FE:00:00:00:FF:00:FE:00:00:00:FF:00:FE:00:00:00:FE:00:F8:00:00:00:D8:00:FE:00:00:00:C0:00:FE:00:00:00:FB:00:FE:00:00:00:FD:00:80:00:00:FF:00:FC:00:C0:00:00:FE:00:FF:00:FC:00:00:FF:00:FE:00:FA:00:00:00:FF:00:00:00:FF:00:C8:00:00:00:EC:00:C0:00:00:00:D2:00:FF:00:00:00:FE:00:D0:00:00:00:FE:00:E4:00:00:FF:00:FE:00:F8:00:00:00:FF:00:FA:00

I’ve been in contact with the manufacturer and they have given me some example C++ code that might hopefully give someone with the experience the information needed to help me out?

===================================
declare a global String PalData

Then you need to bring in the data from the serial stream byte by byte as a char and accumulate into PalData.
LastPalData holds the previous record - so declare that as global also.

void checkPalData()
{
  uint8_t ch;
  while (Serial.available())
  {
    ch = Serial.read();
    if (ch >= 9 && ch <= 128)
    {
      if (ch == '\n')
      {
        PalDataDone = true;
        LastPalData = PalData;
        break;
      }
      else
      {
        PalData += (char)ch;
      }
    }
  }
}

PalDataDone is a Boolean flag so I only hit this routine when needed.
You can then do this…

void getPalData()  // Matches Pal6 string off serial
{
  Pal6.MainsWatts = returnValue(0).toInt();
  Pal6.TransWatts = returnValue(1).toInt();
  Pal6.SolarWatts = returnValue(2).toInt();
  Pal6.HotWater = returnValue(3).toFloat();
  Pal6.DeltaT = returnValue(4).toFloat();
  Pal6.MinTemp = returnValue(5).toInt();
  if (Pal6.MinTemp == 0) Pal6.MinTemp = 1;
  P6V.mint = Pal6.MinTemp;
  Pal6.MaxTemp = returnValue(6).toInt();
  Pal6.MainsWattsIn = returnValue(7).toInt();                             // There are 720 of these per hour and each gia unit = 1W per hour
  Pal6.MainsWattsOut = returnValue(😎.toInt();
  Pal6.TransWattsOut = returnValue(9).toInt();
  Pal6.TransWattsIn = returnValue(10).toInt();
  Pal6.SolarWattsOut = returnValue(11).toInt();
  Pal6.HoursToMHT = returnValue(12).toInt();
  Pal6.GWActive = returnValue(13).toInt();
  Inverter.Active = Pal6.GWActive;
  Pal6.GWCharging = returnValue(14).toInt();
  Charger.Active = Pal6.GWCharging;
  Pal6.SolarExport = returnValue(15).toInt();
  Solar.Active = Pal6.SolarExport;                                      //Export Solar ?
  Pal6.ExportActive = returnValue(16).toInt();
  Pal6.ImportActive = returnValue(17).toInt();
  Pal6.OverFlow = returnValue(18).toInt();
  espTime = returnValue(19);
  unsigned long palLongTime = makeLong(espTime);
  int diff = palLongTime - now();

  if (abs(diff) > 120)
  {
    if (LastTimeHack < now())
    {
      pme("Clock Diff ", diff);
      //    getInternetTime();
      sendPaladinT();
      LastTimeHack = now() + 120;                  // wait 60 seconds at least between time hacks
    }
  }

Using the returnValue(num) routine

String returnValue(int index)                                 // Return value @pos from comma delimited string
{
  int found = 0;
  int strIndex[] = { 0, -1 };
  int maxIndex = PalData.length() - 1;

  for (int i = 0; i <= maxIndex && found <= index; i++)
  {
    if (PalData.charAt(i) == ',' || i == maxIndex)
    {
      found++;
      strIndex[0] = strIndex[1] + 1;
      strIndex[1] = (i == maxIndex) ? i + 1 : i;
    }
  }
  return (found > index) ? PalData.substring(strIndex[0], strIndex[1]) : "";
}

Here was a guide to use the UART debug to process the response data. Your sample data provided doesn’t seem to match up with the c code from the manufacturer. The logic looks for , and \n but there aren’t any of those characters 0x2C and 0x0A in the data. Could you post some more sample data from logs with both RX and TX?

@mulcmu Thanks for the reply!
I have set the sequence to be…

UARTDebug::log_string(direction, bytes);

This changes slightly what the incoming data looks like but it’s still seems like “noise” to me…

\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x

This is what I currently have set in ESPHome…


uart:
  tx_pin: 1
  rx_pin: 3
  baud_rate: 9600    
  debug:
    direction: RX
    dummy_receiver: true
    after:
      delimiter: "\n"
    sequence:
      - lambda: |-
          UARTDebug::log_string(direction, bytes);  //Still log the data

I agree, looks like “noise”. Is it an ESP8266? If so the logger data could be using the same serial port. Set baud_rate to zero for logger to disable sending log data out over UART.

I’d also check the hardware connections. Are you using a level shifter to get the esp 3.3V TTL signal converted to RS232 signals (Assuming the Paladin is using RS232 levels)?

# Enable logging via web interface only
logger:
  baud_rate: 0 

Baud rate for the logger is already set to zero.
I tried this with an ESP8266 but could never get any data showing up… Tried with a level shifter but it didn’t make any difference.
Then tried with an ESP32 CAM board that I had spare and that’s where I’m at right now… Not level shifter in place though.
Manufacture implies that it is RS232 with this comment… “With an RS232 connection in place, you can do all sorts of interesting things

Worth noting that my understanding of a level shift is to just convert 3.3v to 5v, not entirely sure how this relates to converting TTL to RS232…

The RS232 protocol has different voltage levels. The 0 values are positive voltages between 3 and 15 volts and the 1 values are negative voltages between -15 and -3 volts. So for the ESP to connect to an RS232 device you need a TTL to RS232 adapter.

Doh! Okay… I will have to source one and see if that helps :slight_smile:
Is it possible that something like this is the correct converter?

That one looks fine. I’d check with the Paladin support to confirm the connections.

Okay I must have had some kind of physical connection issues because this evening I went through the process if using a level shifter in between the ESP32 and the Paladin and that didn’t work… Took it out and now I’m getting some serial data that looks a lot better!

[D][uart_debug:158]: <<< “-1290.19,0.00,0.00,71.63,0.00,30,78,1278.20, NAN,0.00, NAN,0.00,168,0,0,0,0,0,0,53\r\n”

So that’s a great result! Next step is to read up and figure out how to take that comma delimited string and sent it to Home Assistant as various entities.

If you use the method linked in post 2 above, you will need to create a sensor in the yaml config for each value you want to transmit back to HA and be sure to give it an id:

Then you could expand the sscanf() from the example to cover the 18 different float values returned.

    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());

          if (sscanf(str.c_str(), "%f,%f,%f,%f ..........", &sensors_values[0], &sensors_values[1],  ...............) == 18 ) {

              id(temp1).publish_state(sensors_value[0]); 
              id(temp2).publish_state(sensors_values[15]);
              ....................
          }

Thanks @mulcmu I’ve tried copy/paste and tinkered with your code but I don’t think I fully understand exactly what the sscanf() part truly does… This is what I have but my paladin_tanktemp sensor never receives a value and is always “unknown”…

The paladin returns 20 values in the CSV string so I’m also not sure what your reference to 18 was?

          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]) == 19 ) {

              // 0 = Grid In, 1 = Transfer, 2 = Solar, 3 = Tank Temp
              id(paladin_tanktemp).publish_state(sensors_values[3]); 
          }

Okay, I wonder if it was just that “If” condition in your example code had == 18 which I changed to ==20 and now I am getting values into my sensor!!!

1 Like

18 was a mistake, I must have miscounted. Glad to see it working!!!

The scan() function was used to read user input from the keyboard (scan keyboard button matrix). sscanf() is a library function that gets user input from a string and expects it to be in a particular format. The function returns the number of formatted elements it was able to extract from the provided string. So the if logic is a crude check to make sure the string that was passed, returned the expected number of elements. So your fix to ==20 was necessary with 20 %f floats being processed by the sscanf(). If a string is received that was too short or had unexpected characters a value other than 20 will be returned and the logic to update the sensor values will be skipped so the prior data will be retained.

Using the sscanf() with 20 values is not very concise but gets the job done without implementing lots of extra logic to process the text in the lambda.

@mulcmu
I have a similar situation and hoping someone can point me in the right direction. Your Ge laundry UART project looks like a promising place to start. It’s one of the only projects I’ve seen that consumes HEX from a UART…

Im monitoring a UART for messages coming from a Pellet stove display that return values all in HEX. I’ve gotten quite far in and have mapped all the data and messages, but Im stuck at how to get decyphered values from ESPHome into home assistant, as I think the best place to process the data is in ESPHome, not home assistant.

Without hijacking this thread, I’ve created a discussion for this part of my project in my GitHub. Any help appreciated!

Thanks @TommySharpNZ & @mulcmu for your YAML and hints.

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

Thanks everyone for your code it was very helpful,

I am getting some errors in the log

15:23:42][W][component:232]: Component uart took a long time for an operation (110 ms).
[15:23:42][W][component:233]: Components should block for at most 30 ms.

Below is how I have it configured in ESPHome.

uart:
  tx_pin: GPIO16
  rx_pin: GPIO17
  baud_rate: 57600
  stop_bits: 1
  parity: none
  debug:
    direction: BOTH
    dummy_receiver: true
    after:
      delimiter: "\r\n"
    sequence:
      - lambda: |-
          UARTDebug::log_string(direction, bytes);

          float sensors_values[20] = {0};
          std::string str(bytes.begin(), bytes.end());

          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 ) {

            id(paladin_grid).publish_state(sensors_values[0]);
            id(paladin_trans).publish_state(sensors_values[1]);
            id(paladin_solar).publish_state(sensors_values[2]);
            id(paladin_hot_water_temp).publish_state(sensors_values[3]);
          }

sensor:
  - platform: template
    name: "Paladin Grid"
    id: "paladin_grid"
  - platform: template
    name: "Paladin Transferd"
    id: "paladin_trans"
  - platform: template
    name: "Paladin Solar"
    id: "paladin_solar"
  - platform: template
    name: "Paladin Tank Temp"
    id: "paladin_hot_water_temp"

interval:
  - interval: 10s
    then:
      - uart.write: 'A'

I am not sure if this is actually going to cause any issues or not?