Sleeper85 / esphome-yambms

Yet another multi-BMS Merging Solution
GNU General Public License v3.0
33 stars 1 forks source link

Any thoughts on HV BMS translation? #3

Open clowrey opened 1 week ago

clowrey commented 1 week ago

This project is awesome! I love using ESPhome for everything and am considering trying to adapt this to work with the BYD HV battery protocol + Tesla model 3 battery protocol.

They are both CAN based and reverse engineered. They can even be on the same CAN bus together.

BYD CAN is here https://github.com/dalathegreat/Battery-Emulator/blob/main/Software/src/inverter/BYD-CAN.cpp

And the Tesla battery is here: https://github.com/dalathegreat/Battery-Emulator/blob/main/Software/src/battery/TESLA-BATTERY.cpp

The Tesla battery may seem complicated but to get it to turn on is just one CAN frame. And then to read the cell data and power is not hard either.

Probably duplicating the BYD CAN HV battery protocol will be the hardest part for me to implement - but having just found this code base makes me think it is possible in ESPhome to do it.

I have been doing allot of learning on Dala's code base and think I understand the whole thing now. But just prefer to have this in ESPhome if it can be stable etc..

What do you think? Cheers!

Sleeper85 commented 1 week ago

Hi,

If you have time to develop the necessary code, why not?

I can help you get on the right track, show you examples of CAN code but I don't have time to do a new development at the moment (I have a lot of other things on my todo list).

But so that I understand correctly, you have an HV inverter and a Tesla HV battery?

Do you want to make the Tesla battery communicate with your inverter and therefore translate the Tesla CAN protocol to CAN BYD HV?

clowrey commented 1 week ago

Thanks yes just looking for advice, I don't expect you to do it all for me :)

Yes I have Deye 50k 50kw HV inverter + Tesla model3 60kwh LFP 350V 108cell battery already working with Dala Battery Emulator. https://github.com/dalathegreat/Battery-Emulator

And it works okay, I just prefer my ESP32 firmware to be in ESPhome if possible for ease of use and updating and monitoring via home assistant etc.

Sleeper85 commented 1 week ago

The next evolution of YamBMS will be the multi-node on CAN bus so receive data from the CAN bus, process them and then send them to the inverter in a different CAN protocol. This corresponds to what you want to do but with different protocols.

The next step for me will be to prepare the current code for this new evolution so probably a new stable version 1.4.6 before starting to develop the multi-node in a new development branch.

From that moment on, I could give you more information on the procedure you will have to follow.

clowrey commented 1 week ago

Cool! I will look forward to that development then!

Have you looked into improving the underlying CAN component in ESPhome? Since it seems that it has a not so good behavior or waiting a long time if no packet response - as you have made the work around for that behavior I saw.

And I had already come across that behavior on another project I am working with to decode vehicles CAN frames to show car information on LCD running ESPhome.

I am not very good at low level programming so not the best for that task just wondering if you think it could/should be improved. Clyde Barrow (LVGL ESPhome developer) also mentioned it could be improved but not sure he will be working on that or not anytime soon.

Sleeper85 commented 1 week ago

No I did not think to work on the canbus component of esphome. The ESP32 crash problem occurs when sending frames on the CAN bus and there is no other node connected to the bus, so we can say that a CAN bus must have at least two nodes which is logical in fact.

I use the inverter response to check that the CAN bus is OK otherwise I stop transmitting frames for 120s before trying again.

clowrey commented 1 week ago

Okay makes sense. I think that is sort of a bug that it crashes when no other device on the bus but glad you have made work around :) image

I have made a little progress - this code reads the Tesla battery BMS SOC data to esphome. All the values and names are from the tesla vehicle DBC files like this one https://github.com/onyx-m2/onyx-m2-dbc/blob/main/tesla_model3.dbc

canbus:
  - platform: esp32_can
    tx_pin: GPIO5
    rx_pin: GPIO6
    can_id: 4
    bit_rate: 500kbps
    tx_queue_len: 5 # default is 5 I believe - 1 should make it drop extras?? 
    rx_queue_len: 5
    on_frame:
      - can_id: 0x292 # BMS_socStatus
        then:
          - logger.log:
              level: INFO
              format: "0x292_BMS_socStatus: 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x"
              args: [ '(uint)x[0]', '(uint)x[1]', '(uint)x[2]', '(uint)x[3]', '(uint)x[4]', '(uint)x[5]', '(uint)x[6]', '(uint)x[7]' ]

          - lambda: |-
              float BMS_socMin = (((x[1] & 0x03) << 8) | x[0]) / 10; //BMS_socMin: 0|10@1+ (0.1,0) [0|0] "%" X
              ESP_LOGI("canbus", "BMS_socMin: %.02f%%", BMS_socMin);
              id(bms_socmin).publish_state(BMS_socMin);

              float BMS_socUI = (((x[2] & 0x0F) << 6) | ((x[1] & 0xFC) >> 2)) / 10; //BMS_socUI: 10|10@1+ (0.1,0) [0|0] "%" X
              ESP_LOGI("canbus", "BMS_socUI: %.02f%%", BMS_socUI); 
              id(bms_socui).publish_state(BMS_socUI);

              float BMS_socMax = (((x[3] & 0x3F) << 4) | ((x[2] & 0xF0) >> 4)) / 10;  //BMS_socMax: 20|10@1+ (0.1,0) [0|0] "%" X
              ESP_LOGI("canbus", "BMS_socMax: %.02f%%", BMS_socMax);
              id(bms_socmax).publish_state(BMS_socMax);

              float BMS_socAvg = ((x[4] << 2) | ((x[3] & 0xC0) >> 6)) / 10; //BMS_socAvg: 30|10@1+ (0.1,0) [0|0] "%" X
              ESP_LOGI("canbus", "BMS_socAvg: %.02f%%", BMS_socAvg);
              id(bms_socavg).publish_state(BMS_socAvg);

              float BMS_initialFullPackEnergy = (((x[6] & 0x03) << 8) | x[5]) / 10; //BMS_initialFullPackEnergy: 40|10@1+ (0.1,0) [0|102.3] "kWh" X
              ESP_LOGI("canbus", "BMS_socAvg: %.02fkWh", BMS_initialFullPackEnergy);
              id(bms_initialfullpackenergy).publish_state(BMS_initialFullPackEnergy);

sensor:
  - platform: template
    name: BMS_socUI
    id: bms_socui
    accuracy_decimals: 2
    device_class: battery
    unit_of_measurement: "%"

  - platform: template
    name: BMS_socMin
    id: bms_socmin
    accuracy_decimals: 2
    device_class: battery
    unit_of_measurement: "%"

  - platform: template
    name: BMS_socMax
    id: bms_socmax
    accuracy_decimals: 2
    device_class: battery
    unit_of_measurement: "%"

  - platform: template
    name: BMS_socAvg
    id: bms_socavg
    accuracy_decimals: 2
    device_class: battery
    unit_of_measurement: "%"

  - platform: template
    name: BMS_initialFullPackEnergy
    id: bms_initialfullpackenergy
    accuracy_decimals: 2
    device_class: energy
    unit_of_measurement: "kWh"
Sleeper85 commented 1 week ago

In turn, each BMS records its information in global variables. These global variables are then processed, published to sensors and then reset to their default values. The BMS are then authorized to communicate their information again.

Currently, with LV protocols, BMS must provide the following information:

Sensor templates will need to be added for information that is not available.

Edit: 06.11.2024

Sleeper85 commented 1 week ago

The charging logic is largely based on the analysis of max_cell_v. Do you have access to this value?

How is your battery built? What type of cell? How many cells?

What charging parameters are you currently using?

clowrey commented 1 week ago

Yes all that information is available or can be estimated (like max charge / discharge can be calculated) Max cell voltage is available and is a good one to base the state on I agree.

It is 172 AH ~350V LFP 108 cell battery - cells made by CATL. They are large rectangular cells.

I have it set to 360V pack charge voltage (3.3333V cell) right now which keeps it at around 70% state of charge. But I will set it higher soon so it charges faster and get to higher SOC.

The max volage can go to 374V and still be below 3.6V peak cell voltage.

The main issue right now is that the cells will not balance so I don't need to fully charge for that reason.

They will not balance because no one has figured out how to fool the Tesla battery BMS to be in "charge" mode.

It always thinks it is "DRIVE" mode so ready for large discharge but not cell balancing currents.

It is a complicated problem since the battery BMS expect many different CAN data frames from the vehicle computer and charge port computers to be able to get into the right mode and stay there. And it does not expect any discharge when in charging mode like the inverter would cause when switching from charge to discharge quickly.

The BMS has to be in DRIVE mode for it to close the internal contactors. If its not there will be no output power. This can be bypassed of course but it is a little tricky and obviously dangerous with 350V DC and 1000s amps peak current capability. And we are still not sure if it will ever be able to get in a mode with balancing active without using its own internal AC-DC charger.

There are attempts being made by me and others to use the internal charger but no success yet that I know of.

Hopefully we will figure out a way to fix the balancing problem.. But for now I just need to limit max/min cell voltage etc.

I think I can make it work with your algorithm, or adjust it if I need. That is what I like about ESPhome - it is easy to change things :)

clowrey commented 1 week ago

I have added a few more values. And started a repo for my current logging code - I'll get all the necessary values decoded before trying to integrate it with your code. https://github.com/clowrey/esphome-tesla-bms image

Sleeper85 commented 1 week ago

Yes all that information is available or can be estimated (like max charge / discharge can be calculated) Max cell voltage is available and is a good one to base the state on I agree.

It is 172 AH ~350V LFP 108 cell battery - cells made by CATL. They are large rectangular cells.

I have it set to 360V pack charge voltage (3.3333V cell) right now which keeps it at around 70% state of charge. But I will set it higher soon so it charges faster and get to higher SOC.

The max volage can go to 374V and still be below 3.6V peak cell voltage.

The main issue right now is that the cells will not balance so I don't need to fully charge for that reason.

They will not balance because no one has figured out how to fool the Tesla battery BMS to be in "charge" mode.

It always thinks it is "DRIVE" mode so ready for large discharge but not cell balancing currents.

It is a complicated problem since the battery BMS expect many different CAN data frames from the vehicle computer and charge port computers to be able to get into the right mode and stay there. And it does not expect any discharge when in charging mode like the inverter would cause when switching from charge to discharge quickly.

The BMS has to be in DRIVE mode for it to close the internal contactors. If its not there will be no output power. This can be bypassed of course but it is a little tricky and obviously dangerous with 350V DC and 1000s amps peak current capability. And we are still not sure if it will ever be able to get in a mode with balancing active without using its own internal AC-DC charger.

There are attempts being made by me and others to use the internal charger but no success yet that I know of.

Hopefully we will figure out a way to fix the balancing problem.. But for now I just need to limit max/min cell voltage etc.

I think I can make it work with your algorithm, or adjust it if I need. That is what I like about ESPhome - it is easy to change things :)

This is very interesting.

Sleeper85 commented 1 week ago

I have added a few more values. And started a repo for my current logging code - I'll get all the necessary values decoded before trying to integrate it with your code. https://github.com/clowrey/esphome-tesla-bms image

Very good idea to do it this way.

Sleeper85 commented 1 week ago

The current code automatically calculates the right charge values ​​based on the chemistry and cell count. This can then be adapted with sliders. It should work for you.

I am currently working on adding new BMS, I am thinking of you too.

Our charging protocol is based on a cut-off voltage and current. The minimum allowed voltage in Bulk for LFP is 3.37V. The charging process also checks that the cells are equalized to declare that the charge is complete and send the soc 100% to the inverter before switching to Float.

Obviously you can modify all this for your specific case.

Charging Logic

clowrey commented 1 week ago

Yes I was super impressed with your charging logic!! One of the main reasons I want to integrate my battery with your project :)

Most people are ignoring allot of the best practices with LFP and other lithium ion batteries.

Sleeper85 commented 6 days ago

You could perhaps develop your code using packages as will be the case later with YamBMS. This then allows to create several BMS easily (even if you have only one), the entities name and id are unique.


substitutions:
  name: yourESPname

# Everything related to GPIOs is created in `board.yaml` files we then use the `id` of the `canbus` to use.
canbus:
  # CANBUS NODE 1 (to your inverter)
  - platform: esp32_can
    id: canbus_node1
    tx_pin: 38 # to CAN board CTX
    rx_pin: 39 # to CAN board CRX (with 4.7K resistor except for SN65HVD230)
    can_id: 1
    bit_rate: 500kbps
  # CANBUS NODE 2 (to your BMS)
  - platform: esp32_can
    id: canbus_node2
    tx_pin: 5
    rx_pin: 6
    can_id: 2
    bit_rate: 500kbps
    tx_queue_len: 5 # default is 5 I believe - 1 should make it drop extras?? 
    rx_queue_len: 5

packages:
  bms1: !include
    file: packages/bms/bms_tesla_canbus.yaml
    vars:
      # YamBMS ID
      yambms_id: 'yambms1' ( will be useful later )
      # BMS vars
      bms_id: '1' # must be a number
      bms_name: 'BMS 1'
      bms_update_interval: '3s' ( probably won't be useful because the update is done when the CAN frames are received )
      bms_combine_interval: '1s' ( will be useful later )
      bms_canbus_node_id: canbus_node2
      # The variables below are not useful if you can retrieve this information from your BMS
      bms_max_charge_current: '100' # A. Used to calculate maximum charge current
      bms_max_discharge_current: '100' # A. Used to calculate maximum discharge current
      bms_cell_ovpr: '3.550' # V. Used by 'Auto CCL' functions
      bms_cell_uvpr: '3.000' # V. Used by 'Auto DCL' functions and to calculate maximum discharge voltage
      bms_balance_trigger_voltage: '0.010' # V. Used by 'Auto CVL' functions

In the file bms_tesla_canbus.yaml

canbus:
  - id: !extend ${bms_canbus_node_id}
    on_frame:
      - can_id: 0x292 # BMS_socStatus
        then:
          - logger.log:
              level: INFO
              format: "0x292_BMS_socStatus: 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x 0x%02x"
              args: [ '(uint)x[0]', '(uint)x[1]', '(uint)x[2]', '(uint)x[3]', '(uint)x[4]', '(uint)x[5]', '(uint)x[6]', '(uint)x[7]' ]

          - lambda: |-
              float BMS_socMin = (((x[1] & 0x03) << 8) | x[0]) / 10; //BMS_socMin: 0|10@1+ (0.1,0) [0|0] "%" X
              ESP_LOGI("canbus", "BMS_socMin: %.02f%%", BMS_socMin);
              id(bms${bms_id}_socmin).publish_state(BMS_socMin);

              float BMS_socUI = (((x[2] & 0x0F) << 6) | ((x[1] & 0xFC) >> 2)) / 10; //BMS_socUI: 10|10@1+ (0.1,0) [0|0] "%" X
              ESP_LOGI("canbus", "BMS_socUI: %.02f%%", BMS_socUI); 
              id(bms${bms_id}_socui).publish_state(BMS_socUI);

              float BMS_socMax = (((x[3] & 0x3F) << 4) | ((x[2] & 0xF0) >> 4)) / 10;  //BMS_socMax: 20|10@1+ (0.1,0) [0|0] "%" X
              ESP_LOGI("canbus", "BMS_socMax: %.02f%%", BMS_socMax);
              id(bms${bms_id}_socmax).publish_state(BMS_socMax);

              float BMS_socAvg = ((x[4] << 2) | ((x[3] & 0xC0) >> 6)) / 10; //BMS_socAvg: 30|10@1+ (0.1,0) [0|0] "%" X
              ESP_LOGI("canbus", "BMS_socAvg: %.02f%%", BMS_socAvg);
              id(bms${bms_id}_socavg).publish_state(BMS_socAvg);

              float BMS_initialFullPackEnergy = (((x[6] & 0x03) << 8) | x[5]) / 10; //BMS_initialFullPackEnergy: 40|10@1+ (0.1,0) [0|102.3] "kWh" X
              ESP_LOGI("canbus", "BMS_socAvg: %.02fkWh", BMS_initialFullPackEnergy);
              id(bms${bms_id}_initialfullpackenergy).publish_state(BMS_initialFullPackEnergy);

sensor:
  - platform: template
    name: ${name} ${bms_name} BMS_socUI
    id: bms${bms_id}_socui
    accuracy_decimals: 2
    device_class: battery
    unit_of_measurement: "%"

  - platform: template
    name: ${name} ${bms_name} BMS_socMin
    id: bms${bms_id}_socmin
    accuracy_decimals: 2
    device_class: battery
    unit_of_measurement: "%"

  - platform: template
    name: ${name} ${bms_name} BMS_socMax
    id: bms${bms_id}_socmax
    accuracy_decimals: 2
    device_class: battery
    unit_of_measurement: "%"

  - platform: template
    name: ${name} ${bms_name} BMS_socAvg
    id: bms${bms_id}_socavg
    accuracy_decimals: 2
    device_class: battery
    unit_of_measurement: "%"

  - platform: template
    name: ${name} ${bms_name} BMS_initialFullPackEnergy
    id: bms${bms_id}_initialfullpackenergy
    accuracy_decimals: 2
    device_class: energy
    unit_of_measurement: "kWh"

YamBMS will look for the IDs below, if you use the same name it will be easier, otherwise you will have to replace them afterwards.

Edit: 08.11.2024