Create custom uart sensor - Delta Solivia Inverter 3.0 EU G4 TR

EDIT: SOLVED !!
220924: Added Github link to complete production configuration and full details on all registers

220929: Added link to issue experienced with the RS485 module hardware I used for this implementation

I really could do with some help on this subject, as I’ve searched in vain for examples on how to create some custom uart sensors for my Delta inverter.

I’ve a Solivia gateway on the modbus already, which is quite chatty (a data package every second or so).
So I’m not able to use a modbus custom_command sensor, as my requests and the replies will drown in the gateway packages (tested that already). I’ve also an issue actually identifying the correct register commands, as all but one tested so far differs from the Solivia protocol data that can be found on the web.

I’ve looked into the gateway package response from the inverter which differs a lot from all the examples I’ve found on the net. So I had to analyse the package and have now identified the specific bytes (registers) I would like to pull out as sensors.

My package is for 0xff bytes (normally a lot fewer bytes are send). Both the request and reply is in the uart buffer, as I’m only sniffing the uart data:

02:05:01:02:60:01:85:FC:03
02:06:01:FF:60:01
45:4F:45:34:36:30:31:30:32:38:37:31:31:33:32:38:37:30:38:31:33:30:31:30:30:33:33:39:38:31:33:30
31:30:38:01:02:1A:00:00:00:00:23:34:00:00:00:00:23:34:00:00:00:00:00:00:00:00:00:00:23:34:00:00
00:00:00:00:00:00:00:00:01:00:03:96:01:9A:00:16:00:00:00:00:00:00:00:00:00:00:00:00:00:23:00:EC
13:88:03:64:FF:4E:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:08:98:07:D0:00:33
00:33:00:00:00:00:02:14:3E:3E:00:26:48:A6:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:08:F7:00:00:01:16:00:00:00:00:00:00
00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:34:A4:03

02:14:3E:3E = total yield = 34881086 = 34881,086kWh
08:F7 = daily yield = 2295W
(Haven’t located current AC power - yet :slight_smile:)

So the question is how do I make a sensor out of eg. the 32float register at position 0x86 (first position as 0x00) in the actual package data ?

ANY help getting me started on this would be much appreciated.
My programming skills are somewhat limited, so I’m quite clueless on how to proceed.

Thank you in advance
Henning

With the help from this link I finally managed to tweak that code into something useful for my little project (Great help :tada::sunglasses:). I had serious issues with the package lenght I need to store and check in the buffer (262 bytes). But finally managed to get a stable result, checking for the ETX byte at the right position as a quick and dirty ‘package integrity check’. Might be crude, but hey it works and is absolutely stable :laughing:

But i really have an issue to get multiple sensors reported correctly back to ESPHome. Simply can’t get anything besides publish_state(data.UInt16) to update the sensors. publish_state is only useful when there’s one sensor only. So I really could do with some help to get that part working as well :slightly_smiling_face:

Rgds.
Henning

Custom_sensor:


class solivia : public PollingComponent, public Sensor, public UARTDevice {
//class solivia : public Sensor, public UARTDevice {
  public:
    solivia(UARTComponent *parent) : PollingComponent(10000), UARTDevice(parent) {}
    //solivia(UARTComponent *parent) : UARTDevice(parent) {}
    Sensor *yield = new Sensor();
    Sensor *production = new Sensor();
  
  void setup() override {

  }

  std::vector<int> bytes;

  //void loop() override {  
  void update() {
    while(available() > 0) {
      bytes.push_back(read());      

      //make sure at least 8 header bytes are available to check
      if(bytes.size() < 8)       
      {
        continue;  
      }
      //ESP_LOGD("custom", "checking for init bytes");
      // Check for Delta Solivia Gateway package response.
      if(bytes[0] != 0x02 || bytes[1] != 0x06 || bytes[2] != 0x01 || bytes[3] != 0xFF || bytes[4] != 0x60 || bytes[5] != 0x01) {
        bytes.erase(bytes.begin()); //remove first byte from buffer
        //buffer will never get above 8 until the header is correct
        continue;
      }      
      
	    if (bytes.size() == 262) {

        TwoByte production_data;
        production_data.Byte[0] = bytes[105];// Seems to be DV voltage not production though
        production_data.Byte[1] = bytes[104];
        TwoByte yield_data;
        yield_data.Byte[0] = bytes[187]; // Daily yield lsb
        yield_data.Byte[1] = bytes[186]; // Daily yield msb
        char etx;
        etx = bytes[261]; // ETX byte

        // Quick and dirty check for package integrity is needed, to avoid irratic sensor value updates 
        // This effectively blocks out any false sensor updates
        // Check if ETX = 3. If not (invalid package), ditch whole package, clear buffer and continue
        if (etx != 0x03) {
          ESP_LOGI("custom", "ETX check failure - No sensor update. ETX: %i", etx);
          bytes.clear();
          continue;
        }
          //publish_state(yield_data.UInt16); //Only works with one sensor !
          //id(yield).publish_state(yield_data.UInt16); //Not updating sensor ?
          //yield->publish_state(yield.UInt16);
          yield->publish_state(yield_data.UInt16);

          //publish_state(production_data.UInt16); //Only works with one sensor !
          //production->publish_state(production.UInt16);
          id(production).publish_state(production_data.UInt16); //Not updating sensor ?

	        ESP_LOGI("custom", "ETX: %i", etx);
          ESP_LOGI("custom", "Daily yield: %i", yield_data.UInt16);
	        ESP_LOGI("custom", "Current production: %i", production_data.UInt16);
        
          bytes.clear();
      }
      else {
      }    
    }    
  }

  typedef union
  {
    unsigned char Byte[2];
    int16_t Int16;
    uint16_t UInt16;
    unsigned char UChar;
    char Char;
  }TwoByte;};

ESPHome yaml:

  name: Delta
  platform: ESP32
  board: nodemcu-32s
  includes:
    - test.h
wifi:
  ssid: "my_ssid"
  password: "my_password"
  manual_ip:
    static_ip: xxx.xxx.x.x
    gateway: xxx.xxx.x.x
    subnet: 255.255.255.0
    
  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Esp32 Fallback Hotspot"
    password: "my_password"

captive_portal:

# Enable logging
logger:
  level: VERBOSE
  #baud_rate: 0
  
# Enable Home Assistant API
api:

ota:

binary_sensor:
  - platform: status
    name: "Inverter Status"
    id: system_status
 
uart:
  id: mod_bus
  tx_pin: 17
  rx_pin: 16
  baud_rate: 19200
  parity: NONE
  stop_bits: 1
  rx_buffer_size: 1024 # Increase buffer size as package is 262 bytes in total
  #debug:
    
sensor:
- platform: custom
  lambda: |-
    auto production = new solivia(id(mod_bus));
    App.register_component(production);
     return {production};
  sensors:
    name: "Current production"
    unit_of_measurement: W
    accuracy_decimals: 0
    filters:
    - throttle: 60s

- platform: custom
  lambda: |-
    auto yield = new solivia(id(mod_bus));
    App.register_component(yield);
    return {yield};
  sensors:
    name: "Solar daily yield"
    unit_of_measurement: W
    accuracy_decimals: 0
    filters:
    - throttle: 60s
1 Like

I’ve added my final production solution to my Gihub repository:
Delta Solivia ESPHome custom component Modbus configuration

Regards
Henning

Hi Henning

Thanks for sharing your project
I have 3 Solivia G3 that I want to try it with - how can the data be split up?
Right now I’m using a python script to get it out and over in HA

BR
Mark
Denmark

Hi’ @Egelyggard

You’re welcome :slightly_smiling_face:
Well, before you proceed, it might be an idea to check if your Delta Solivia inverter replies with the same package length as mine (255 bytes). If not your registers are most likely arranged completely different than mine and you really can’t use my solution directly.

Don’t really understand what you mean about the data split up ?
I use an ESP8266 with ESPHome that make HA sensors corresponding to all the different registers I want in HA.

Regards
Henning

Hi @htvekov
Sorry if I haven’t explained myself well enough - what I mean is that when I have three inverts, I want to be able to see it for each invert.
I can’t remember what my data looks like, but then I have to see if I can change your code
BR
Mark

Ahhhh…
Sorry, didn’t read your question thoroughly enough it seems :wink:
Well, I’ll guess that your inverters most likely are at slave address #01, #02 & #03. You can check the inverters addresses directly in the display menu functions (somewhere)
My config is for an inverter at slave address #01. But that can easily be altered. The new CRC checksum when altering the address can also easily be calculated with online CRC calculators.
I would also expect that a single ESPHome node/device could handle all three inverters without issues. Haven’t really measured at the rs-485 ports, but I’ll guess you can just cascade the connection from Inverter #01 to #02 and from #02 to #03 with only one connection from the ESPHome node to the first inverter. And you don’t really need to poll data as frequently as the Solivia Gateway does (appx. once a second)

  1. Do you have a Solivia Gateway connected to the inverters ?
  2. As your inverters are the G3 model and not G4 TR like mine, I would actually not expect that neither firmware nor package length are identical.
  3. Try and send this command string to the inverter you have at address #01: [0x02, 0x05, 0x01, 0x02, 0x60, 0x01, 0x85, 0xFC, 0x03]. If response is: 02:06:01:FF:60:01 + 255 data bytes (incl. CRC bytes) + ETX byte, then you should be able to use my config directly and just tweak a bit regarding the slave addresses and the CRC.

Here’s a link for a specific G3 config (single commands):

Here’s another link for different config (also believe it’s for a G3 inverter) relying on fetching data from the package instead (like my solution). Here package length is just much smaller at 157 bytes.

You should really take a look at your python script and check out what commmands etc. are actually send today :slightly_smiling_face:

Regards
Henning

I already have Hardware set up because I use a python script to retrieve data from all 3 now - so I have control over the hardware setup and how I retrieve data and put it into HA. I would prefer to use an ESP than a Raspberry pi :slight_smile:
I can see how with your code I can call the three inverters, but as I see it, it is missing (if it were to work in my setup) that data from each inverter must be entered into each sensor, e.g. “PV1 AC power” “PV2 AC power” “PV3 AC power”

  1. no I don’t have a GW I send myself the request in python
  2. Yes they are all G3 Firmware is probably not the same and data is a little different
  3. Yes, I try it this weekend

I am well aware that I cannot use your 100% for my data format. I want to state to get it working for one inverter, then I have to make it can handle more inverters.
It is the Python that I use now that you have a link to

OK. Yes, start by checking the package length returned from one of your inverters.
If it matches either mine (255 bytes) or 157 bytes, then the package content is ‘known’ and documented and you won’t have to start from scratch :slightly_smiling_face: If the package content is completely different, then you could just use the known working single commands instead in ESPHome.

One ESPHome node should be able to handle all three inverters, if you don’t exxagerate the total amount of sensors. But the custom code would have to be rewritten to deal with three inverters and not just one.

Regards
Henning

Arrgghhh…
I should really read the entire message before replying :roll_eyes:
OK. Your data package content is ‘known’ :+1:
Rewriting the custom code and tweaking the ESPHome config should be easy for testing with just inverter#01 to begin with.
Let me know if you need any help with that :slightly_smiling_face:

Regards
Henning

I just got it set up today and put it in debug mode - here is data from my number 3 inverter so they run 157 bytes
So now I can change your code to decode the 157 byte format :slight_smile:

02:05:03:02:60:01:84:44:03
02:06:03:96:60:01
45:4F:45:34:36:30:31:30:31:39:30:31:31:33:31:39:30:31:36:31:30:34:30:30:30:32:37:33:35:31:30:34:30:31:36:02:32:1B:02:32:1B:02:32:1B:02:32:1B:00:E5:00:1B:07:D0:00:1C:00:00:00:1A:00:EE:02:4E:13:8D:00:1A:5D:5C:13:8D:00:00:5C:F8:13:8D:00:00:02:C3:02:09:00:50:00:DC:00:F5:07:80:13:79:13:9D:00:03:E0:90:00:00:69:8A:00:C0:01:33:0E:E5:00:00:08:98:00:00:00:08:00:00:00:00:00:00:00:00:00:00:00:01:01:01:01:01:01:01:01:01:01:01:01:01:01:01:01:01:01:01:01:6E:7A:03

Yep. You really don’t need to alter much to get it working with one inverter.
Revise the custom code to search for your specific inverter reply 02:06:03:96:60:01
Change the buffer size check from 262 to 157 and remember to set etx byte to 156 as well.

Remaining is just to alter number of sensors and package address for each and revise accordingly in the ESPHome yaml as well. Then you should be good to go :slightly_smiling_face::rocket:

A ‘quick and dirty’ solution to get all three inverters implemented quickly, without rewriting the custom code, would be to make 3 custom code versions altered specifically for each inverter and just include all three CC’s in ESPHome. Not pretty - but quick :wink::raising_hand_man:

Regards
Henning

Hi, I have changed the search string to 02:06:03:96:60:01 but I thought that buffer size check should be 164 and etx byte 163, that’s what I understood from your documentation.
It was also my idea to do a search for each ID and make an upload for each - yes, not pretty. :stuck_out_tongue_winking_eye:
The sun has gone down, so I can’t test it now, maybe I can get some test tomorrow. :slight_smile:

I can see it with the “buffer size check” now - I thought that “Response” had to be added to “buffer size check”

BR
Mark

Nope. The buffer size check is ‘all included’ so to speak.
the response (6 bytes) + package (255 bytes) + ETX (1 byte) = 262 bytes in total for my inverter.
And ETX byte no. is 261 as first byte is at index[0].
Sorry about the code structure etc. It’s my first go at C++
Last time i did serious coding was when i was a teenager and we all made raw Z80 machine code 24/7 almost some 40 years ago :laughing:

You should be able to send packages to the inverter 24/7, even though the package content is somewhat limitied. My G4 TR inverter sends packages every second ,24/7 to the gateway.

You don’t have to apologize because I’m not good at C++ either, :laughing: but I actually thought your code was easy to understand :+1:
I get no response from my inverters when the sun has gone down, they have gone to sleep.
I have a pellet stove called BioMax which I also want to be able to control from HA, I think I can use some of your code as a structur for it

Well, the original code sniplet was from the project I reference to in this thread.
So it’s not entirely mine, only altered… a lot :laughing:

My only real main issues with this implementation was actually the timing needed for polling and uart buffer size in order not to end up with to many incomplete or faulty packages. Hence my extra check for the ETX at the expected position. Might be crude, but it effectively has removed all faulty sensor values in production for last two months. I thought of implementing real CRC check, but I’m not experienced enough to do that - yet :slightly_smiling_face:

Looking forward to hear about your result, when the inverter wakes up

Hi Henning
I managed test a bit when I got home before the sun went down but I can’t get it to work - data is coming out, it’s just not right.
I have set it up according to this, https://forums.ni.com/ni/attachments/ni/170/1007166/1/Public%20RS485%20Protocol%201V2.pdf which is what my python is also set up after. my inverters are variant 18 and 19.
I’ll have to spend some more time on it this weekend

     if(bytes[0] != 0x02 || bytes[1] != 0x06 || bytes[2] != 0x01 || bytes[3] != 0x96 || bytes[4] != 0x60 || bytes[5] != 0x01) {
        bytes.erase(bytes.begin()); //remove first byte from buffer
        //buffer will never get above 8 until the response is a match
        continue;
      }      
      
	    if (bytes.size() == 157) {
        
        TwoByte dc_power_data;
        dc_power_data.Byte[0] = bytes[0x2C +6]; // DC Power lsb
        dc_power_data.Byte[1] = bytes[0x2B +6]; // DC Power msb
        TwoByte dc_v_data;
        dc_v_data.Byte[0] = bytes[0x30 +6]; // Solar voltage lsb
        dc_v_data.Byte[1] = bytes[0x2F +6]; // Solar voltage msb
        TwoByte dc_a_data;
        dc_a_data.Byte[0] = bytes[0x32 +6]; // Solar current lsb
        dc_a_data.Byte[1] = bytes[0x31 +6]; // Solar current msb

        TwoByte ac_a_data;
        ac_a_data.Byte[0] = bytes[0x3A + 6]; // AC current lsb
        ac_a_data.Byte[1] = bytes[0x39 + 6]; // AC current msb
        TwoByte ac_v_data;
        ac_v_data.Byte[0] = bytes[0x3C + 6]; // AC voltage lsb
        ac_v_data.Byte[1] = bytes[0x3B + 6]; // AC voltage lsb
        TwoByte freq_data;
        freq_data.Byte[0] = bytes[0x3E + 6]; // Frequency lsb
        freq_data.Byte[1] = bytes[0x3D + 6]; // Frequency msb
        TwoByte ac_power_data;
        ac_power_data.Byte[0] = bytes[0x35 +6]; // AC Power lsb
        ac_power_data.Byte[1] = bytes[0x34 +6]; // AC Power msb
        
        TwoByte iso_plus_data;
        iso_plus_data.Byte[0] = bytes[0x34 +6]; // Solar isolation resistance lsb
        iso_plus_data.Byte[1] = bytes[0x33 +6]; // Solar isolation resistance msb
        TwoByte iso_minus_data;
        iso_minus_data.Byte[0] = bytes[0x38 +6]; // Solar input MOV resistance lsb
        iso_minus_data.Byte[1] = bytes[0x37 +6]; // Solar input MOV resistance msb

        TwoByte hs_1_data;
        hs_1_data.Byte[0] = bytes[0x36 +6]; // Calculated temperature at ntc (DC side) lsb
        hs_1_data.Byte[1] = bytes[0x35 +6]; // Calculated temperature at ntc (DC side) msb
        TwoByte hs_2_data;
        hs_2_data.Byte[0] = bytes[0x42 +6]; // Calculated temperature at ntc (AC side) lsb
        hs_2_data.Byte[1] = bytes[0x41 +6]; // Calculated temperature at ntc (AC side) msb

        TwoByte d_yield_data;
        d_yield_data.Byte[0] = bytes[0x50 +6]; // Daily yield lsb
        d_yield_data.Byte[1] = bytes[0x4F +6]; // Daily yield msb
        uint32_t t_yield_data = int(
            (unsigned char)(bytes[0x86 +6]) << 24 |
            (unsigned char)(bytes[0x87 +6]) << 16 |
            (unsigned char)(bytes[0x88 +6]) << 8 |
            (unsigned char)(bytes[0x89 +6]));  // Total yield (4 bytes float)

    
        TwoByte unknown_0x64_data;
        unknown_0x64_data.Byte[0] = bytes[0x64 +6]; // Unknown lsb
        unknown_0x64_data.Byte[1] = 0; // bytes[0x64 +6]; // unknown msb
        TwoByte unknown_0x65_data;
        unknown_0x65_data.Byte[0] = bytes[0x65 +6]; // Unknown lsb
        unknown_0x65_data.Byte[1] = 0; // bytes[0x64 +6]; // unknown msb
        TwoByte unknown_0x91_data;
        unknown_0x91_data.Byte[0] = bytes[0x91 +6]; // Unknown lsb
        unknown_0x91_data.Byte[1] = 0; // bytes[0x90 +6]; // unknown msb

        char etx;
        etx = bytes[156]; // ETX byte (last byte)

Hi’ @Egelyggard

Seems to be correct as far as I can tell.
Only checked dc voltage register and I calculate the same position as you (0x2f/0x30 + 6)
What does the ESPHome log show ?
Is the ETX found at correct position ?
Enable log in ESPHome and PM me the log for at few request/responses to the inverter.
It’s hard to tell what’s wrong without logs :slightly_smiling_face:

Regards
Henning