MODBUS help needed - Ampinvt solar controller to Home Assistant via ESPhome

I’ve emailed the manufacturer for some help but I’ll share the challenge here too.

The string being returned by an 0xA3 query is 01:A3:01:00:0D:00:02:B2:05:15:00:F6:00:A8:00:00:00:20:00:00:3E

I know the first 3 bytes are address, command type and control code. The last is the checksum. That leaves 17 bytes but in the doc, it states that there will be a 37 bytes response. and I’m only getting 21 bytes unless I am missing something. At first, I thought that maybe I wasn’t reading all the output but the checksum is correct for the 20 bytes returned.

Am I missing something here?

Edit:
I noticed a few mistakes in the doc so I’m not at all confident that it is up-to-date, correct etc. Take the ‘Remarks’ section of byte 6/7 (PV Voltage): “Take 1 decimal place; 0x0C43=1219 means PV voltage is 121.9V”

I have no clue at all how they are deriving 1219 from a hex value of 0x0C43. The rest map out OK using INT16 but there are clearly errors in the doc.

I also don’t seem to be able to make sense of the values in terms of breaking out the bits for bytes 3, 4 and 5 or even figuring out the high and low bytes for the remainder.

1 Like
Take 1decimal places,such as:
0x0C43=1219,means PV voltage is 
121.9V

Strange byte swap or docs mistake

1219 = 0x04C3


Edit:

Docs typo.

This is ok:

5372 = 0x14FC


edit2

About binary

So I was doing the right thing in breaking down the hex to binary to get the bit settings. Still didn’t make any sense but last night I think I found out why. This might make you smile…

Support is in China, I’m in MST zone so I don’t get responses until in the evening. They are largely pretty helpful after a rocky start. I send them the command from the master and the response from the controller and point out that it isn’t 37 bytes but 21 bytes. I ask them to match up the response string to their document. Clearly this proves a challenge for the L1 guys.

I get am email that simply asks “Is it OK if we send you the communication protocol in Chinese?”… Well, I guess so…I’ll figure it out. I run it through Google Translate and behold, I get a pretty perfect translation.

The thing is, the command codes in this (second) document state that it is 0xB3 (not 0xA3) for the real time data so I give that a try and get a perfect 37 byte response that makes more sense of the face of it.

I’m currently stuck trying to get an output of the ESP32 log as detailed in this thread. That is proving even more challenging than modbus or Chinese documentation translations.

You can use interval: and uart.write.

Once you have response in log,

adapt the other topic solution:

The main difference, you need active requests.

# Example configuration entry
interval:
  - interval: 1min
    then:
      - switch.toggle: relay_1

It seems that the 0xB3 command code is the correct one and the bytes map directly to the protocol sheet. Now I can take a closer look at the work done by you guys and figure out how to interpret those into HA sensors.

The logging issue I mentioned was due to me being unaware that Esphome had switched tracks back at the start of 2022 and I was still on version 2022.3.1. I’ve updated now and logging is working so I have been able to gather a lot of the modbus responses to get a good picture.

I can see the conversion of negative temperature readings is going to be a challenge. I understand how it works, that the hex needs to be converted to binary and the first digit being one denotes negative but how to implement that in one calculation for positive and negative will be the hurdle I think.

I’m a little more informed than I was earlier but…that’s all relative. :slight_smile:

I’ve been through the solutions posted by @assembly and @htvekov and for the data that is coming in, I understand maybe 75% of what is going on with the processing. I see that the additional *.h file is handling the uart read and breaking out the bytes into individual data that are then sent to the sensor in the .yaml file. I’ve taken a ‘first pass’ at adapting the files created for my own data but my developer skills and knowledge leave much to be desired, especially in a language like C+.

Much like @assembly posted after his “Yahoo!” moment, my data coming from the controller to the ESP32 log file looks like this:

[18:40:01][D][uart_debug:114]: <<< 01:B3:01:00:00:00:02:41:04:C3:00:5D:00:39:00:00:10:20:00:00:00:00:00:22:00:00:05:6C:00:00:00:00:00:00:00:00:18

Mapped out, it looks like this:
Byte 1 > 37
MPPT Address - 1 byte (01)
Command Code - 1 byte (B3)
Control Code - 1 byte (01)
Operating Status - 1 byte (00) [uses 8 bits for status]
Charging Status - 1 byte (00) [uses 8 bits for status]
Control Status - 1 byte (00) [uses 8 bits for status]
PV Voltage - 2 bytes (02 & 41) **
Battery Voltage - 2 bytes (04 & C3) **
Charge Current - 2 bytes (00 & 5D) **
MPPT Temp - 2 bytes (00 & 39) **
Not used - 1 byte (00)
Not used - 1 byte (00)
Battery Temp - 2 bytes (10 & 20) ** !This will be tricky due to negative temps
Not used - 1 byte (00)
Not used - 1 byte (00)
Daily power - 4 bytes (00 & 00 & 00 & 22)
Total Power - 4 bytes (00 & 00 & 05 & 6C)
Not used - 1 byte (00)
Not used - 1 byte (00)
Not used - 1 byte (00)
Not used - 1 byte (00)
Not used - 1 byte (00)
Not used - 1 byte (00)
Not used - 1 byte (00)
Not used - 1 byte (00)
Checksum - 1 byte (18)

There is no timestamp in the data but I assume (read, hope) that I can add that in yaml code later. The ** denote the data that I want to focus on getting into HA first.

The adaptation of @htvekov solivia.h code is as follows:

#include "esphome.h"

class ampinvt : public PollingComponent, public Sensor, public UARTDevice {
  public:
    ampinvt(UARTComponent *parent) : PollingComponent(400), UARTDevice(parent) {}

   // 37 bytes total - 25 bytes used, 12 bytes unused
//    Sensor *time_stamp = new Sensor(); // from logging?
//    Sensor *operating_status = new Sensor(); // binary (1 byte) - future implementation/low priority
//    Sensor *charging_status = new Sensor(); // binary (1 byte) - future implementation/low priority
//    Sensor *control_status = new Sensor(); // binary (1 byte) - future implementation/low priority
    Sensor *pv_voltage = new Sensor(); // 2 byte
    Sensor *battery_voltage = new Sensor(); // 2 byte
    Sensor *charge_current = new Sensor(); // 2 byte
    Sensor *mppt_temperature = new Sensor(); // 2 byte
    Sensor *battery_temperature = new Sensor(); //2 byte
//    Sensor *today_yield = new Sensor(); //4 byte - future implementation/low priority
//    Sensor *generation_total = new Sensor(); //4 byte - future implementation/low priority

  
  void setup() override {

  }

  std::vector<int> bytes;
  int count = 15;

  //void loop() override {

  void update() {
    while(available() > 0) {
      bytes.push_back(read());      
      //make sure at least 8 header bytes are available for check
      if(bytes.size() < 8)       
      {
        continue;  
      }
      if(bytes[0] != 0x01 || bytes[1] != 0xB3 || bytes[2] != 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() == 37) {

//        OneByte operating_status;
//        operating_status.Byte[0] = bytes[0x4B +6]; // Operating Status (binary)
//        OneByte command_code;
//        command_code.Byte[0] = bytes[0x4B +6]; // Command Code (binary)
//        OneByte control_status;
//        control_status.Byte[0] = bytes[0x4B +6]; // Control Status (binary)
        TwoByte pv_voltage;
        pv_voltage.Byte[0] = bytes[0x4B +6]; // PV Voltage high byte
        pv_voltage.Byte[1] = bytes[0x4A +6]; // PV Voltage low byte
        TwoByte battery_voltage;
        battery_voltage.Byte[0] = bytes[0x4D +6]; // Battery Voltage high byte
        battery_voltage.Byte[1] = bytes[0x4C +6]; // Battery Voltage high byte
        TwoByte charge_current;
        charge_currrent.Byte[0] = bytes[0x4F +6]; // Charge Current high byte
        charge_current.Byte[1] = bytes[0x4E +6]; // Charge Current low byte
        TwoByte mptt_temperature;
        mptt_temperature.Byte[0] = bytes[0x5D + 6]; // MPPT Controller Temp high byte (Temp should always be positive)
        mptt_temperature.Byte[1] = bytes[0x5C + 6]; // MPPT Controller Temp low byte
        TwoByte battery_temperature;
        battery_temperature.Byte[0] = bytes[0x5F + 6]; // Battery Temp high byte (Temp could be positive or negative)
        battery_temperature.Byte[1] = bytes[0x5E + 6]; // Battery Temp low byte
//        FourByte today_yeild;
//        today_yeild.Byte[0] = bytes[0x61 + 6]; // Daily Power Generation Yield
//        today_yeild.Byte[1] = bytes[0x60 + 6]; // Daily Power Generation Yield
//        today_yeild.Byte[2] = bytes[0x60 + 6]; // Daily Power Generation Yield
//        today_yeild.Byte[3] = bytes[0x60 + 6]; // Daily Power Generation Yield
//        FourByte generation_total;
//        generation_total.Byte[0] = bytes[0x63 +6]; // Total Power Generation Yield
//        generation_total.Byte[1] = bytes[0x62 +6]; // Total Power Generation Yield
//        generation_total.Byte[2] = bytes[0x62 +6]; // Total Power Generation Yield
//        generation_total.Byte[3] = bytes[0x62 +6]; // Total Power Generation Yield

//        TwoByte d_yield_data;
//        d_yield_data.Byte[0] = bytes[0xB5 +6]; // Daily yield lsb
//        d_yield_data.Byte[1] = bytes[0xB4 +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)

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

        // Quick and dirty check for package integrity is done, in order to avoid irratic sensor value updates 
        // This effectively blocks out any erroneous sensor updates due to rx package corruption
        // 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;
//        }

//          operating_status->publish_state(operating_status.UInt16);
//          charging_status->publish_state(charging_status.UInt16);
//          control_status->publish_state(control_status.UInt16);
          pv_voltage->publish_state(pv_voltage.UInt16);
          battery_voltage->publish_state(battery_voltage.UInt16);
          charge_current->publish_state(charge_current.UInt16);
          mptt_temperature->publish_state(mptt_temperature.UInt16);
          battery_temperature->publish_state(battery_temperature.UInt16);
//          today_yeild->publish_state(today_yeild.UInt16);
//          generation_total->publish_state(generation_total.Int16);

//	        ESP_LOGI("custom", "ETX check OK: %i - ESPHome sensors updated", etx);
        
          bytes.clear();
      }
      else {
      }    
    }    
  }

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

I have commented out the stuff that isn’t vital for a first stage implementation and I can circle back to that later when I understand more about what is going on. The code coming in from the Solivia is in a different format to the code that I have coming back; rather than individual registers, I have a string dump of hex values so I’m not sure how to adapt the byte locations used in the original file but I utilised the approach from @assembly as that made more sense.

And that leaves the 25% of code there that I don’t understand. If you guys could look it over and make suggestions, I’d be deeply grateful. Thanks so much for your help so far.

Looks good so far!
However the registers don’t seem to match.

For example:

0x4A + 6 would be 80, which can’t be because you have only 37 bytes to begin with.

The bytes for battery voltage are probably:

battery_voltage.Byte[0] = Bytes[4]
battery_voltage.Byte[1] = Bytes[3]

The link to your protocol is no longer working so I’m just guessing.

Ah yes, I actually figured that out from your C+ file. I didn’t understand how the byte references from Henning worked but I did understand that you were referencing byte positions.

Strange that the link is no longer working. Reposted again here:

Original modbus comms file with incorrect 0xA3 command code

Second modbus comms file translated from Chinese with correct 0xB3 command code

Ok so battery voltage should be:

battery_voltage.Byte[0] = Bytes[9]
battery_voltage.Byte[1] = Bytes[8]

And so on.

Personally I think it’s a bit easier to understand in decimal than in hex. But it’s the same thing.

Dammit! I was hoping to figure this out but I’m all out of ideas.

The files validate OK but when I try to install, I get some errors. I’ve eliminated most of them but these last ones (or last one as they are all the same, repeated) are beyond me. Here is my ampinvt.h file:

#include "esphome.h"

class ampinvtcomponent : public PollingComponent, public Sensor, public UARTDevice {
  public:
    ampinvtcomponent(UARTComponent *parent) : PollingComponent(600), UARTDevice(parent) {}

  //37 bytes total - 25 bytes used, 12 bytes unused
  Sensor *pv_voltage          = new Sensor(); // 2 byte
  Sensor *battery_voltage     = new Sensor(); // 2 byte
  Sensor *charge_current      = new Sensor(); // 2 byte
  Sensor *mppt_temperature    = new Sensor(); // 2 byte
  Sensor *battery_temperature = new Sensor(); // 2 byte
  
  void setup() override {
  }

  std::vector<int> bytes;

  void update() {
    while(available() > 0) {
      bytes.push_back(read());      
      if(bytes.size() < 37){
        continue;  
      }
    
      else {
      }
	    if(bytes.size() == 37) {

        TwoByte pv_voltage;
        pv_voltage_value.Byte[0] = bytes[6]; // PV Voltage high byte
        pv_voltage_value.Byte[1] = bytes[7]; // PV Voltage low byte
        pv_voltage->publish_state(pv_voltage_value.UInt16);
        
        TwoByte battery_voltage;
        battery_voltage_value.Byte[0] = bytes[8]; // Battery Voltage high byte
        battery_voltage_value.Byte[1] = bytes[9]; // Battery Voltage low byte
        id(battery_voltage).publish_state(battery_voltage_value.UInt16);
        
        TwoByte charge_current;
        charge_currrent_value.Byte[0] = bytes[10]; // Charge Current high byte
        charge_current_value.Byte[1] = bytes[11]; // Charge Current low byte
        id(charge_current).publish_state(charge_current_value.UInt16);
        
        TwoByte mptt_temperature;
        mptt_temperature_value.Byte[0] = bytes[12]; // MPPT Controller Temp high byte (Temp should always be positive)
        mptt_temperature_value.Byte[1] = bytes[13]; // MPPT Controller Temp low byte
        id(mptt_temperature).publish_state(mptt_temperature_value.UInt16);
        
        TwoByte battery_temperature;
        battery_temperature_value.Byte[0] = bytes[16]; // Battery Temp high byte (Temp could be positive or negative)
        battery_temperature_value.Byte[1] = bytes[17]; // Battery Temp low byte
        id(battery_temperature).publish_state(battery_temperature_value.UInt16);
       
        bytes.clear();
      }
      else {
      }    
    }    
  }

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

And here are the errors when I try to install:

In file included from src/main.cpp:46:0:
src/ampinvt.h: In member function 'virtual void ampinvtcomponent::update()':
src/ampinvt.h:31:9: error: 'pv_voltage_value' was not declared in this scope
         pv_voltage_value.Byte[0] = bytes[6]; // PV Voltage high byte
         ^
src/ampinvt.h:33:19: error: base operand of '->' has non-pointer type 'ampinvtcomponent::TwoByte'
         pv_voltage->publish_state(pv_voltage_value.UInt16);
                   ^
src/ampinvt.h:36:9: error: 'battery_voltage_value' was not declared in this scope
         battery_voltage_value.Byte[0] = bytes[8]; // Battery Voltage high byte
         ^
src/ampinvt.h:38:29: error: 'union ampinvtcomponent::TwoByte' has no member named 'publish_state'
         id(battery_voltage).publish_state(battery_voltage_value.UInt16);
                             ^
src/ampinvt.h:41:9: error: 'charge_currrent_value' was not declared in this scope
         charge_currrent_value.Byte[0] = bytes[10]; // Charge Current high byte
         ^
src/ampinvt.h:42:9: error: 'charge_current_value' was not declared in this scope
         charge_current_value.Byte[1] = bytes[11]; // Charge Current low byte
         ^
src/ampinvt.h:43:28: error: 'union ampinvtcomponent::TwoByte' has no member named 'publish_state'
         id(charge_current).publish_state(charge_current_value.UInt16);
                            ^
src/ampinvt.h:46:9: error: 'mptt_temperature_value' was not declared in this scope
         mptt_temperature_value.Byte[0] = bytes[12]; // MPPT Controller Temp high byte (Temp should always be positive)
         ^
src/ampinvt.h:48:30: error: 'union ampinvtcomponent::TwoByte' has no member named 'publish_state'
         id(mptt_temperature).publish_state(mptt_temperature_value.UInt16);
                              ^
src/ampinvt.h:51:9: error: 'battery_temperature_value' was not declared in this scope
         battery_temperature_value.Byte[0] = bytes[16]; // Battery Temp high byte (Temp could be positive or negative)
         ^
src/ampinvt.h:53:33: error: 'union ampinvtcomponent::TwoByte' has no member named 'publish_state'
         id(battery_temperature).publish_state(battery_temperature_value.UInt16);
                                 ^
*** [/data/esp32-solar-monitor/.pioenvs/esp32-solar-monitor/src/main.cpp.o] Error 1
========================== [FAILED] Took 4.36 seconds ==========================

Both the .h files I’m referencing use a slightly different syntax so I have deliberately used the version from Henning in the first item (pv_voltage) and the version from @assembly for the rest, just to see if one worked where others did not.

I suspect that I an in the realm of inheritance issues but have no idea how to fix it. Any ideas?

Edit:
Ignore the typo in ‘charge_currrent’, I fixed that since posting.

Edit #2:
Nothing like making a post on a forum 2 minutes before realising that you have multiple typos in your code and some missing _value entries. Just one more to solve I think.

Yeah. Was just about to type when your edit2 came in. That’s definitely your problem. Either add “value” to all variables or leave it away for all variables.

I just added the “value” to distinguish them from the actual esphome sensor declarations, but there shouldn’t be any conflict even if you name them the same.

Also for pv voltage replace “->” with “.”

Great, you got it working :muscle::slightly_smiling_face:

Well, The response from my Solivia inverter is somewhat different than yours.
First I check for a 6 bytes response match (header), as my inverters response starts with the query command response, in total 6 bytes, followed by the actual data including CRC (255 bytes) + ETX (1 byte) = 262 bytes in total.

Your inverter just returns data without any prefixes or trailing data.
So you should alter the uart buffer check for correct amount of bytes from 262 to 37 in the line:
if (bytes.size() == 262)

In my case the actual inverter data byte starts at byte 0x06h, hence my code adding 6 to all the indexes. Simply to make it more readable. You should delete this completely and reference directly to the index byte number from your list, as your inverter data start at byte 0x00h

1 Like

Well, I’m to slow it seems :laughing:Didn’t read the entire thread to the end before answering.

One note though.
I have a semi ‘CRC check’ on the prefixed 6 bytes + my ETX at fixed positions.
This ensures, to some extend, that data is actually valid and not just some random bytes used to update all the sensor values.
You have to rely on only three prefixed bytes and no ETX at a fixed byte position. So if you experience issues with the uart data quality (random spikes on the sensors) it might be worth the hazzle to actually implement the correct CRC check on the last byte. If not a match, scratch all bytes before you update the sensors.

I did get it working! I’m pretty ecstatic at this point and couldn’t have got close to this point without the help from you guys and the work you did before. I still have some work to do on formatting but given the snowstorm outside right now, these temperatures look pretty appealing. I also appear to have solved an energy crisis with an ESP32 looking at the voltage readings. LOL!

3 Likes

Ohh, you’ve abandoned your check on the initial three bytes now :open_mouth::open_mouth:
You’ll have no idea about what data actually will update your sensors, if the uart inbound queue for some reason gets out of sync / otherwise f*cked up with the request.

Well done !! :clap::clap:

Even though the temperatures are somewhat appealing, you better add some filters on your esphome sensors like this (Divides value with 10)

- name: "Solar DC current"
    device_class: energy
    unit_of_measurement: A
    accuracy_decimals: 1
    filters:
    - throttle: 60s
    - multiply: 0.1
1 Like

I’m a ‘see one, do one, teach one’ kind of learner so having your code to look at has been extremely helpful. Of course, the drawback is that I am way behind on the knowledge needed to understand most of it. As I worked through it to both understand it and fit it to my scenario, I just stripped out anything that I recognised as specific to your application so that I could get to a working baseline.

My plan from here is to carry on and start adding that stuff back in so I’m far from complete. Once I have the easy formatting for voltages and current sorted (I think I may regret that statement) I’ll move on to the challenge of temperatures. I know the 4 byte hex needs to first be converted to binary and the first character of the binary denotes whether the temperature is positive or negative with the following 15 characters being the temperature. Once that is done, I’ll look to add the bit values for the operating status etc and then circle back to the data checks.

Good Job!!
I think it’s totally amazing that we are adding all kinds of inverters to HA based on the same code principle :+1:

Question on byte counts using either method…

Does the byte count used in the .h file begin at 0 or 1? Here is my data string:

01:B3:01:00:07:00:02:16:04:9F:00:33:00:0E:00:00:10:22:00:00:00:00:00:0E:00:00:05:8C:00:00:00:00:00:00:00:00:89

The first 3 bytes are always constant and not used. (01:B3:01). Are these at position 0, 1, 2 or at 1, 2, 3?

I note Henning used a different format in hex which seemed to use a location and an offset. I’m playing around with the variatnts but not quite getting to where I should be. I have a spreadsheet set up with the hex to dec values so I know what I’m aiming for.

I also spend sh*tload of hours getting usefull data from my Solivia inverter.
As the Solivia datasheet at that time wasn’t public, I had to check and ‘play’ with all the values to find the correct bytes for each sensor. That took some hours… :laughing:

According to documentation both you PV voltage and temp readings are just factor 10 ?? So all needed here is a multiply filter.

image