syssi / esphome-seplos-bms

ESPHome component to monitor a Seplos Battery Management System (Seplos-BMS) via UART or RS485
Apache License 2.0
57 stars 28 forks source link

Seplos v3 multi battery pack #80

Open ferelarg opened 7 months ago

ferelarg commented 7 months ago

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 commented 7 months ago

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 commented 7 months ago

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 commented 7 months ago

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 commented 7 months ago

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 commented 7 months ago

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

Privatecoder commented 7 months ago

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 commented 7 months ago

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 commented 6 months ago

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

Privatecoder commented 6 months ago

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

IMG_0069

(which is the same for my 10E version)

ferelarg commented 6 months ago

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 commented 6 months ago

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 commented 6 months ago

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 commented 6 months ago

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 commented 6 months ago

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 commented 6 months ago

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 commented 6 months ago

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

jolly12f commented 6 months ago

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 commented 6 months ago

@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 commented 6 months ago

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

klatremis commented 5 months ago

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 commented 5 months ago

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.

ferelarg commented 5 months ago

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 commented 5 months ago

@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:

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

EDIT: Nevermind, I found it in your code

ferelarg commented 5 months ago

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 commented 5 months ago

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 commented 5 months ago

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 commented 5 months ago

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 commented 4 months ago

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 commented 4 months ago

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 commented 3 months ago

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 commented 3 months ago

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