ESPHome Modbus and non-sequential low/high registers

I’m working on reading data from a Sol-Ark inverter over the modbus connection. Things are going pretty smoothly, but I’ve hit a snag where a single 32bit value is spread over two registers that are not sequential.

Here’s a snippet of the PDF describing the registers:

Take 74 – if you configure it like this:

  - platform: modbus_controller
    modbus_controller_id: modbus_client_sa
    name: "Total Batt Discharge Power"
    address: 74
    unit_of_measurement: "kwh"
    register_type: holding
    value_type: U_DWORD_R
    filters:
    - multiply: 0.1

The U_DWORD_R instructs the modbus sensor to properly read “unsigned 32 bit integer from 2 registers low word first” which is how I read the docs.

However, “Total Grid Buy Power” is the problem. It’s another UINT32, but it’s on register 78 and 80 – register 79 is Grid Frequency.

How can I get this oddly split value into ESPHome and back to HA?

There are configs for pulling values from odd offsets, or for modbus implementations that store values in odd manners (like offsets, etc). Nothing I found in the docs, however, deals with values which are stored in non-sequential registers and cobbling it back together.

I imagine there’s a lambda that could be done which reads from 78 through 80 and then combines the values back to the single 32bit int, but that’s kinda beyond my skill level at the moment; aside from seeing the potential in doing that.

It’s so idiotic that I doubt esphome has option to read that. The good point is that you have time to practice with lambdas until you reach 6553.5 kWh on reg 78.
Modbus controller component has example to unite two registers with lamda code, for 32bit float though.

Inverter software issue. Or just table?

Very much feels like at one point they only supported up to that six thousand’ish kwh and someone came along and said “that’s too small, we use a bunch!” and so they had to make it 32bit, but people were already using 78, so they just “extended it” somewhere else. Backwards compatible!

That…is a good point, lol.

If you are not a bit manipulator guy, you could use a template sensor to combine low and high word. I think you could calculate it : LW + HW*65536

I used an online compiler and some ai code helpers to cobble together this C++ code to demonstrate what I suspect I should see:


#include <iostream>
#include <iomanip>
#include <cstdint>
#include <vector>

int main()
{
    
    // dummy object to emulate the item and data objects I would expect
    class Itemer {
    public:
        int offset;
    };
    Itemer* item = new Itemer;
    item->offset = 0;
    
    // The value, bajillions of kwh:
    //      2294967294 => hex 88CA6BFE
    // register 78 (low word) would be 0x6B 0xFE
    // register 80 (high word) would be 0x88 0xCA
    // so the data should be:
    //
    //      data[item->offset + 0] = 0x6B;
    //      data[item->offset + 1] = 0xFE;
    //      data[item->offset + 2] = N/A; // ignore, is the grid frequency
    //      data[item->offset + 3] = N/A; // ignore, is the grid frequency
    //      data[item->offset + 4] = 0x88;
    //      data[item->offset + 5] = 0xCa;

    std::vector<unsigned char> data = { 0x6B, 0xFE, 0xAA, 0xBB, 0x88, 0xCA };
    
    // to get that back into a regular uint32_t, we bitmath the values as such:
    // high word in 4/5, ignore 2/3, low in 0/1

    uint32_t result = data[item->offset + 4] << 24
                    | data[item->offset + 5] << 16
                    | data[item->offset + 0] << 8
                    | data[item->offset + 1];
    
    std::cout << "back together: " << result << std::endl;

    return 0;
}

Given that, I think the solution looks like this:

- platform: modbus_controller
  modbus_controller_id: modbus_client_sa
  name: "Total Grid Buy Power"
  address: 78
  unit_of_measurement: "kwh"
  register_type: holding
  value_type: "U_DWORD"
  register_count: 3
  lambda: |-
    union {
        float f;
        uint32_t i;
    } return_data;
    return_data.i = data[item->offset + 4] << 24
                  | data[item->offset + 5] << 16
                  | data[item->offset + 0] << 8
                  | data[item->offset + 1];
    return return_data.f;
  filters:
    - multiply: 0.1

It’ll be a bit before I can test/verify it, however, so it’s kinda guesswork at this point, but at least sorta educated guesswork :slight_smile:

1 Like

Put it in comparison with my previous post approach.
And post the results when time is due. :+1:

I took a spare esp32 and created a dummy modbus server which has this absurd “int split over non-contiguous registers” issue – this code does the trick when reading it back:

  - platform: modbus_controller
    modbus_controller_id: modbus_client_sa
    name: "Total Grid Buy Power"
    address: 78
    unit_of_measurement: "kwh"
    register_type: holding
    register_count: 3
    accuracy_decimals: 1
    lambda: |-
      uint32_t return_data = data[item->offset + 4] << 24
                           | data[item->offset + 5] << 16
                           | data[item->offset + 0] << 8
                           | data[item->offset + 1];
      return return_data;
    filters:
      - multiply: 0.1

That whole union thing…did not do what I had expected it to do, nor what the modbus examples implied it would do. Returning an int, despite the docs saying you have to return a float, works just fine. One must suspect the docs are…perhaps not totally accurate.

I’ll stick with this method so that, from the esp device, the data is accurate.

(Granted, I originally started with a really big number, and converting that to a float, which then got rounded in fun “Computers Suck at Floating Point Math” ways was a super cool rabbit hole to fall in…I’ll just leave that for future me problem when I hit the absolutely absurd KWH numbers.)

Yep, 32bit float conversion is much more complicated.
To put together low-word and high-word UINT is simple, basically you have two counters from 0-65535. When low-word counter arrives to max, it adds 1 to high-word counter and roll over to 0.