Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Seplos v3 multi battery pack #80

Open
ferelarg opened this issue Dec 28, 2023 · 33 comments
Open

Seplos v3 multi battery pack #80

ferelarg opened this issue Dec 28, 2023 · 33 comments

Comments

@ferelarg
Copy link

I have been doing some research on the seplos rs485 bus, and I have programmed a python script to "listen" the data from the complete battery pack (when there is more than one) and publish it to a MQTT server (with auto discovery for home assistant).
When there is only one battery, modbus can be used directly, but when there are several batteries the main one acts as "Master" and there can only be one master, so there is no other option but to listen to what the master asks and decode it.
The code (a draft version) is in: https://github.com/ferelarg/Seplos3MQTT

@Privatecoder
Copy link

Privatecoder commented Dec 28, 2023

you could access the slaves via RS485 directly from the free master's or the last slaves-RS485 port and use a second RS485 connection on the CAN-port of the master using a splitter (different BAUD rate of 9600) while also using CAN for Battery<->Inverter communication as CAN and RS485 are using separate pins on that port.

But the idea to fetch the slaves data from the master using a single connection is also nice ;)

@ferelarg
Copy link
Author

Sorry if I expressed myself wrong, exactly, I connect to any free RS485 port, the one of the master or the one of the last battery.
My first intention was to use CAN (the connection to the inverter) but it does not transmit the cell voltages, which was a data that I was interested in incorporating in Home assistant.

@Privatecoder
Copy link

Privatecoder commented Dec 28, 2023

Maybe you got me wrong too: You can use the CAN-Port using a splitter (https://www.amazon.de/dp/B00D3KIQXC/) to connect the CAN-part to the inverter and read the battery-stuff via RS485 simultaneously – including voltages and being able to use the infamous BatteryMonitor supplied by Seplos to connect to it and change its settings etc – via RS485 through the CAN-port (through this you can only read the master, not the slaves, which is why a second regular RS485 connection is necessary in this setup)

Seplos

@ferelarg
Copy link
Author

I haven't tried that... interesting...
I find the implementation of not being able to modify the parameters of all the batteries centrally very crappy... but well, I understand that it is not common to have to change them on a regular basis. But I have 6 Seplos Mason 280 and it's a pain.
Could I change the configuration of each battery using that other port (the one that shares the can connector)?

@ferelarg
Copy link
Author

I was trying to connect to that port today and it was impossible.
Does it have a different pinout for the RS485?

@Privatecoder
Copy link

I did not check the title – mine is not v3 but the previous version 10E. Have you tried with the baud-rate set to 9600? This works on the 10E BMS.

@ferelarg
Copy link
Author

Yes, I tried, but at least with the pinout I'm using: Can on 4-5 and rs485 on 7-8 it doesn't work.

@jamietb
Copy link

jamietb commented Jan 3, 2024

I achieve this using pin 1 = B and pin 2 = A - maybe try that.

@Privatecoder
Copy link

Privatecoder commented Jan 3, 2024

This is from the v3.0 manual for the CAN-Port

IMG_0069

(which is the same for my 10E version)

@ferelarg
Copy link
Author

ferelarg commented Jan 3, 2024

Ok, I will try tomorrow.
And can you change the ID of each slave on that port? because if so you could set the script as master and control and configure all the batteries centrally..

@Privatecoder
Copy link

No, you can only access the master this way. You can access all slaves via their ID‘s with the second RS485 connection to the regular RS485 Ports on the master or the last slave.

@ferelarg
Copy link
Author

ferelarg commented Jan 3, 2024

Yes, that's what my script already does, but if you can access the master and can connect in parallel to the others, you should be able to access the others.. In the end it's another RS485 port, you can daisy chain them all.

@jolly12f
Copy link

jolly12f commented Jan 9, 2024

Hi, I've been trying to read the two batteries via Modbus for months but without getting results, with a single battery these problems didn't exist. If anyone has any news, please let me know!

@ferelarg
Copy link
Author

ferelarg commented Jan 9, 2024

When you have more than one battery, the main battery is set to "Master" mode and in a RS485 network there can only be one master.
That is why you can read when the battery is alone.

What my script does is to "listen" to what the master asks, and get the data.

@jolly12f
Copy link

jolly12f commented Jan 9, 2024

With your system were you able to read the values ​​of the two batteries? I already use mqtt with another broker, is it possible to use it for both?

@ferelarg
Copy link
Author

ferelarg commented Jan 9, 2024

Sure, and you can still use the Seplos app at the same time.

@jolly12f
Copy link

I already use mqtt for another service can I use this together? how can you also use seplos app if port 485 is busy for mqtt?

@syssi
Copy link
Owner

syssi commented Jan 10, 2024

@jolly12f A single MQTT broker is required and can be used by tons of different devices simultaneously. Are you asking about the windows application or the Android app?

@jolly12f
Copy link

I use Windows and unfortunately I don't know where to start :(

@klatremis
Copy link

I have been doing some research on the seplos rs485 bus, and I have programmed a python script to "listen" the data from the complete battery pack (when there is more than one) and publish it to a MQTT server (with auto discovery for home assistant). When there is only one battery, modbus can be used directly, but when there are several batteries the main one acts as "Master" and there can only be one master, so there is no other option but to listen to what the master asks and decode it. The code (a draft version) is in: https://github.com/ferelarg/Seplos3MQTT

Great job! How do we get a ESP32 version of your script for Multi Seplos 3.0? :D

@ferelarg
Copy link
Author

Basically you can use the same methodology: "listen" to the serial port and decode the messages.
I have done a small proof of concept using EspHome and it works fine.
I have not gone any further because in my installation it is more logical to use a rs485 adapter directly.

1 similar comment
@ferelarg
Copy link
Author

Basically you can use the same methodology: "listen" to the serial port and decode the messages.
I have done a small proof of concept using EspHome and it works fine.
I have not gone any further because in my installation it is more logical to use a rs485 adapter directly.

@Privatecoder
Copy link

Privatecoder commented Feb 22, 2024

@ferelarg an offtopic question: What does the 3.0 BMS send between packs when you just listen (and don't send a telemetry request)? the v16/10e only provides:

  • lowest/highest cell
  • cells temp 0+1
  • dis-/charge current
  • total pack-voltage
  • residual capacity
  • battery capacity
  • soc
  • port-voltage

and some info I wasn't able to decode yet, probably alarm status..

EDIT: Nevermind, I found it in your code

@ferelarg
Copy link
Author

It sends all the data that the application displays, in fact I started to suspect that it worked that way when it "discovered" the packets in no specific order.
The battery that works as Master asks for the register packs, it is just a matter of understanding what it asks for and decoding it.
It practically queries the information of all the data of each battery pack. Including alarms, balancing states, etc.

To do this, I first detected all the log packets queried by the Master with a python sniffer.
Then I made a script that detects them by calculating the size of the response and the type of record it contains... later it's just a matter of counting and converting bytes.

What I like most about this solution is that it is totally passive. It can be used at the same time as the native windows application, connect to any free rs485 port and does not interfere at all in the normal operation of the system.

@ferelarg
Copy link
Author

EDIT: Nevermind, I found it in your code

No, in my code I only use those registers, but it sends many more... if you look at the protocol manual that is in the "docs" folder you will see that the Master asks for everything (except the serial number and some other minor things).

@Privatecoder
Copy link

Privatecoder commented Feb 22, 2024

I also wrote a script for the v16/10e which requests telemetry and telesignalization frames and decodes them. they include more data than the frames, requested by the master.. however I am struggling to decode some of the data as there is no documentation about these "intra-pack-communication" frames and the offsets / type of data being used..

And I do agree, that listening is better than adding more request to the existing communication. Thats why I want to implement an option to chose between passive/listening and active/requesting.

@jolly12f
Copy link

Fondamentalmente puoi usare la stessa metodologia: "ascoltare" la porta seriale e decodificare i messaggi. Ho fatto una piccola prova di concetto utilizzando EspHome e funziona bene. Non sono andato oltre perché nella mia installazione è più logico utilizzare direttamente un adattatore RS485.

it would be nice to know how to use on esphome too.

@clowrey
Copy link

clowrey commented Feb 28, 2024

Basically you can use the same methodology: "listen" to the serial port and decode the messages. I have done a small proof of concept using EspHome and it works fine. I have not gone any further because in my installation it is more logical to use a rs485 adapter directly.

Could you share your proof of concept ESPhome YAML ? I am hoping to be able to do what you have done but in ESPhome - for easier remote connection and to have less dependence on HA scripts. I also wanted individual cell voltages for all batteries in a multi pack bank.

@ferelarg
Copy link
Author

Basically you can use the same methodology: "listen" to the serial port and decode the messages. I have done a small proof of concept using EspHome and it works fine. I have not gone any further because in my installation it is more logical to use a rs485 adapter directly.

Could you share your proof of concept ESPhome YAML ? I am hoping to be able to do what you have done but in ESPhome - for easier remote connection and to have less dependence on HA scripts. I also wanted individual cell voltages for all batteries in a multi pack bank.

For a quick proof of concept in ESPHome I have slightly modified the following script:
https://github.com/htvekov/solivia_esphome
so that the solivia.h looked like this:

// *****************************************************************
// *          ESPHome Custom Component Modbus sniffer for             *
// *          Seplosv3 based on Delta Solvia Inverter 3.0 EU G4 TR      *
// *****************************************************************

#include "esphome.h"

class solivia : public PollingComponent, public Sensor, public UARTDevice {
  public:
    solivia(UARTComponent *parent) : PollingComponent(400), UARTDevice(parent) {}
    Sensor *pack_voltage = new Sensor();
    Sensor *current = new Sensor();
    Sensor *remaining_apacity = new Sensor();
    Sensor *total_capacity = new Sensor();
    Sensor *total_discharge_capacity = new Sensor();
    Sensor *soc = new Sensor();
    Sensor *soh = new Sensor();
    Sensor *cycle = new Sensor();
    Sensor *average_cell_voltage = new Sensor();
    Sensor *average_cell_temp = new Sensor();
    Sensor *max_cell_voltage = new Sensor();
    Sensor *min_cell_voltage = new Sensor();
    Sensor *max_cell_temp = new Sensor();
    Sensor *min_cell_temp = new Sensor();
    Sensor *maxdiscurt = new Sensor();
    Sensor *maxchgcurt = new Sensor();
  
  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() < 5)       
      {
        continue;  
      }
      // Check for Delta Solivia Gateway package response.
      if(bytes[0] != 0x01 || bytes[1] != 0x04 || bytes[2] != 0x24) {
        bytes.erase(bytes.begin()); //remove first byte from buffer
        //buffer will never get above 8 until the response is a match
        continue;
      }      
      //ESP_LOGD("custom", "Checking for inverter package: %i", bytes.size());
      
	    if (bytes.size() == 36+5) {
        
        // Some 15 packages are recieved appx. every 10 seconds
        // With this counter we will only update ESPHome sensor appx. every 10 seconds
        // NB. Remove this counter if you're not using a Solivia Gateway. 
    
        TwoByte pack_voltage_data;
        pack_voltage_data.Byte[0] = bytes[1 +3]; // DC voltage lsb
        pack_voltage_data.Byte[1] = bytes[0 +3]; // DC voltage msb

        TwoByte current_data;
        current_data.Byte[0] = bytes[3 +3]; // DC voltage lsb
        current_data.Byte[1] = bytes[2 +3]; // DC voltage msb

        TwoByte remaining_apacity_data;
        remaining_apacity_data.Byte[0] = bytes[5 +3]; // DC voltage lsb
        remaining_apacity_data.Byte[1] = bytes[4 +3]; // DC voltage msb

        TwoByte total_capacity_data;
        total_capacity_data.Byte[0] = bytes[7 +3]; // DC voltage lsb
        total_capacity_data.Byte[1] = bytes[6 +3]; // DC voltage msb

        TwoByte total_discharge_capacity_data;
        total_discharge_capacity_data.Byte[0] = bytes[9 +3]; // DC voltage lsb
        total_discharge_capacity_data.Byte[1] = bytes[8 +3]; // DC voltage msb

        TwoByte soc_data;
        soc_data.Byte[0] = bytes[11 +3]; // DC voltage lsb
        soc_data.Byte[1] = bytes[10 +3]; // DC voltage msb

        TwoByte soh_data;
        soh_data.Byte[0] = bytes[13 +3]; // DC voltage lsb
        soh_data.Byte[1] = bytes[12 +3]; // DC voltage msb

        TwoByte cycle_data;
        cycle_data.Byte[0] = bytes[15 +3]; // DC voltage lsb
        cycle_data.Byte[1] = bytes[14 +3]; // DC voltage msb

        TwoByte average_cell_voltage_data;
        average_cell_voltage_data.Byte[0] = bytes[17 +3]; // DC voltage lsb
        average_cell_voltage_data.Byte[1] = bytes[16 +3]; // DC voltage msb

        TwoByte average_cell_temp_data;
        average_cell_temp_data.Byte[0] = bytes[19 +3]; // DC voltage lsb
        average_cell_temp_data.Byte[1] = bytes[18 +3]; // DC voltage msb

        TwoByte max_cell_voltage_data;
        max_cell_voltage_data.Byte[0] = bytes[21 +3]; // DC voltage lsb
        max_cell_voltage_data.Byte[1] = bytes[20 +3]; // DC voltage msb

        TwoByte min_cell_voltage_data;
        min_cell_voltage_data.Byte[0] = bytes[23 +3]; // DC voltage lsb
        min_cell_voltage_data.Byte[1] = bytes[22 +3]; // DC voltage msb

        TwoByte max_cell_temp_data;
        max_cell_temp_data.Byte[0] = bytes[25 +3]; // DC voltage lsb
        max_cell_temp_data.Byte[1] = bytes[24 +3]; // DC voltage msb

        TwoByte min_cell_temp_data;
        min_cell_temp_data.Byte[0] = bytes[27 +3]; // DC voltage lsb
        min_cell_temp_data.Byte[1] = bytes[26 +3]; // DC voltage msb

        TwoByte maxdiscurt_data;
        maxdiscurt_data.Byte[0] = bytes[31 +3]; // DC voltage lsb
        maxdiscurt_data.Byte[1] = bytes[30 +3]; // DC voltage msb

        TwoByte maxchgcurt_data;
        maxchgcurt_data.Byte[0] = bytes[33 +3]; // DC voltage lsb
        maxchgcurt_data.Byte[1] = bytes[32 +3]; // DC voltage ms

        pack_voltage->publish_state(pack_voltage_data.UInt16);
        current->publish_state(current_data.Int16);
        remaining_apacity->publish_state(remaining_apacity_data.UInt16);
        total_capacity->publish_state(total_capacity_data.UInt16);
        total_discharge_capacity->publish_state(total_discharge_capacity_data.UInt16);
        soc->publish_state(soc_data.UInt16);
        soh->publish_state(soh_data.UInt16);
        cycle->publish_state(cycle_data.UInt16);
        average_cell_voltage->publish_state(average_cell_voltage_data.UInt16);
        average_cell_temp->publish_state(average_cell_temp_data.UInt16/10 - 273.15);
        max_cell_voltage->publish_state(max_cell_voltage_data.UInt16);
        min_cell_voltage->publish_state(min_cell_voltage_data.UInt16);
        max_cell_temp->publish_state(max_cell_temp_data.UInt16/10 - 273.15);
        min_cell_temp->publish_state(min_cell_temp_data.UInt16/10 - 273.15);
        maxdiscurt->publish_state(maxdiscurt_data.UInt16);
        maxchgcurt->publish_state(maxchgcurt_data.UInt16);

        bytes.clear();
      }
      else {
      }    
    }    
  }

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

and the yaml:

substitutions:
  name: seplos-bms
  battery_bank0: "${name} bank 0"
  battery_bank1: "${name} bank 1"
  battery_bank2: "${name} bank 2"
  device_description: "Monitor a Seplos BMS via RS485"
  tx_pin: RX
  rx_pin: TX


esphome:
  name: sniffer-modbus
  friendly_name: Sniffer Modbus
  includes:
    - ./config/solivia.h

esp32:
  board: esp32dev
  framework:
    type: arduino



# Enable Home Assistant API
api:


ota:


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

  # Enable fallback hotspot (captive portal) in case wifi connection fails
  ap:
    ssid: "Sniffer-Modbus Fallback Hotspot"

captive_portal:
    
# Enable logging
logger:
  baud_rate: 0

# Enable Web server
web_server:
  port: 80

uart:
  - id: mod_bus
    baud_rate: 19200
    tx_pin: ${tx_pin}
    rx_pin: ${rx_pin}
    rx_buffer_size: 1024



sensor:
  - platform: custom
    lambda: |-
      auto delta = new solivia(id(mod_bus));
      App.register_component(delta);
      return {delta->pack_voltage,delta->current,delta->remaining_apacity,delta->total_capacity,delta->total_discharge_capacity,delta->soc,delta->soh,delta->cycle,delta->average_cell_voltage,delta->average_cell_temp,delta->max_cell_voltage,delta->min_cell_voltage,delta->max_cell_temp,delta->min_cell_temp,delta->maxdiscurt,delta->maxchgcurt};


    sensors:
    - name: "Pack Voltage"
      unit_of_measurement: V
      device_class: voltage
      accuracy_decimals: 2
      filters:
        - multiply: 0.01
    
    - name: "Current"
      id: current
      device_class: current
      unit_of_measurement: A
      accuracy_decimals: 2
      filters:
        - multiply: 0.01

    - name: "remaining Capacity"
      unit_of_measurement: Ah
      accuracy_decimals: 2
      filters:
        - multiply: 0.001

    - name: "Total Capacity"
      unit_of_measurement: Ah
      accuracy_decimals: 2
      filters:
        - multiply: 0.001

    - name: "Total Discharge Capacity"
      unit_of_measurement: Ah
      filters:
        - multiply: 10

    - name: "SOC"
      filters:
        - multiply: 0.1
      accuracy_decimals: 1
      unit_of_measurement: '%'

    - name: "SOH"
      filters:
        - multiply: 0.1
      accuracy_decimals: 1
      unit_of_measurement: '%'

    - name: "average_cell_voltage"
      unit_of_measurement: V
      device_class: voltage
      accuracy_decimals: 3
      filters:
        - multiply: 0.0001
        
    - name: "average_cell_temp"
      unit_of_measurement: V
      device_class: temperature
      accuracy_decimals: 1

    - name: "max_cell_voltage"
      unit_of_measurement: V
      device_class: voltage
      accuracy_decimals: 2
      filters:
        - multiply: 0.0001

    - name: "min_cell_voltage"
      unit_of_measurement: V
      device_class: voltage
      accuracy_decimals: 2
      filters:
        - multiply: 0.0001

    - name: "max_cell_temp"
      unit_of_measurement: V
      device_class: temperature
      accuracy_decimals: 1

    - name: "min_cell_temp"
      unit_of_measurement: V
      device_class: temperature
      accuracy_decimals: 1

    - name: "maxdiscurt"
      unit_of_measurement: A
      device_class: current
      accuracy_decimals: 2
      filters:

    - name: "maxchgcurt"
      unit_of_measurement: A
      device_class: current
      accuracy_decimals: 2
      filters:
        
        

@jolly12f
Copy link

I managed to see something with esphome following your comment, would you just give me some help to see the individual cells even just an example for just one then I'll take care of the rest

@SeByDocKy
Copy link

Very interesting topic .... thx....

@DpunktS
Copy link

DpunktS commented Oct 2, 2024

@ferelarg könntest du deine YAML mit den Zellspannungen vervollständigen? Aktuell stehe ich nämlich vor dem Problem das ich meine Akkus nicht mehr auslesen kann seitdem ich meinen Speicher erweitert habe. Deine Lösung mit Seplos3MQTT kommt für mich leider nicht in frage weil ich mir die Werte über ESPHome in den IOBroker hole.

@DpunktS
Copy link

DpunktS commented Oct 7, 2024

Hier ist eine funktionierende Lösung: #112

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants