Eco-worthy 100ah iot battery integration

I picked up one of the latest Eco-Worthy batteries, a 100 ah battery that has cold disconnect (all be it at -7C rather than 0) and wifi / bluetooth app integration:


12V 100Ah LiFePO4 Lithium Battery with Bluetooth and WiFi
(I needed a new caravan battery, and the price looked competitive to others on the market)
This is my mini-review (after about 1 week) plus some (rough and ready) code I’ve put together to feed into home assistant, along side other data like from the caravan truma water heater, victron solar controller)
The battery
In terms of its function as a battery, it seems to work fine - it arrived with about 40% charge, and once installed I was easily able to get it fully charged. I’ve not properly stress tested it, but with some basic load on it (all the interior lights etc) it seemed to be working fine.
The Smart Features
This is where I feel it falls down a little
Installing their provided app is straightforward, and no immediately obvious translation issues.
The smart features are provided by a small module they call BW02, which is powered by the batteries rs485 rj45 port. I’ve not definitively identified the pin out on the rj45 port
I first added it via bluetooth. No ecoworthy account required.

  1. Power on coms module on battery
  2. Plug in BW02 module
  3. Click + in app and add device
  4. select device

It then gives an instant readout on capacity, load, runtime, individual cell voltages etc

For wifi connectivity, you then need (while connected via bluetooth) to go into the settings and set the ssid and key - all standard stuff

I didn’t scan for where it sent to, but based on further experiments, I believe it sends the data in plain to hq. You need to have an account to then associate the battery with your account, and then in theory, while battery and wifi last, it will report in

Downsides
The biggest one I’ve found is the coms module going into standby - this is why I don’t feel I can recommend it for long term use.
If the battery is discharging, it stays on
If the battery is charging, it stays on
If its idle / standby, after about 4 mins, it turns off
Even if you have the app open
Even if you’re in the middle of seeing if there is a new firmware update and flashing it (Yes I did briefly brick it, but thankfully they document how to update the firmware via windows and a micro usb port!)
Idle / standby can be due to it being fully charged, and the solar / mains charger being in “float” and meeting any other demands
It can also be that the solar controller is making the same amount of power as would be drawn off the battery.
The manual states 15 mins, and not if the app is active, but in my experience, it is not this way
BW02 mini tear down
The device mac address identifies it as BouffaloLab
I’ve only taken the rear cover screws off, but the SoC that it runs on looks to be HiLink HLK-B35


I can’t track the wires from the data cable to pins within its circuit board, and left the gunk alone!
Packet structure
This can be done using wireshark + android debug and bluetooth, or by using the
ip / port setting in the app and send via wifi to an open port, such as a netcat server
For the ip / port method, you will then loose the ability for it to report back into the wifi part of the ecoworthy app Dont be like me - make sure if you sniff out where it is going to first if you want to be able to go back!

The data part of the packets starts 0xA1 or 0xA2. When working in esphome and using wifi and stream sever, the MAC address was also included.
As far as I can see, the general status is in 0xA1, with battery total and available ah, voltage, current, overall battery health. 0xA2 contains cell specific data - I’ve only picked out the individual cell voltages and the number of cells so far:
0xA1
Position
0-5 MAC address
6 Packet Type
22-23 Available capacity
24-25 Total capacity (0x64 =100)
26-27 Battery voltage (centivolts)
28-29 Battery load (centi-amps) signed int so 0x0001 = 1 = 0.01A 0x8000 = -1 = -0.01A
0xA2
Position
0-5 MAC address
6 Packet Type
21 Number of cells (reads 4)
22-23 Cell 1 voltage (mv)
24-25 Cell 2 voltage (mv)
26-27 Cell 3 voltage (mv)
28-29 Cell 4 voltage (mv)
30-85 (Unused but could be further cell voltages if larger battery)
86-87 Number of cells (0 indexed - reads 3)

Linking to ESPHome
I see 2 ways of linking it - via ble or via a tcp port.
To get the raw data, either way works - no encryption. The main contents of the packets either way start 0xA1 or 0xA2.

To do it via tcp port, your esphome device will need a static IP. I used github://tube0013/esphome-stream-server-v2 to provide tcp → uart service
Below is my config (minus some of the other bits for mqtt, victron etc.
I’m not much of a c++ expert - I get by! I didn’t feel up to making a whole external module yet, so have done it with lambda for now! I followed the [How to UART read without custom component] ([How to] UART read without custom component) guide to put this together

esphome:
  name: ecoworth
  friendly_name: Ecoworthy-Example

external_components:
  - source: github://tube0013/esphome-stream-server-v2

esp32:
  board: esp32dev
  framework:
    type: esp-idf
    version: recommended
    
ota:
  password: "<CHANGEME!>"

wifi:
  networks:
  - ssid: !secret wifi_ssid
    password: !secret wifi_password
  domain: .lan
  
uart:
  - id: tcp_uart_bus
    baud_rate: 115200
    tx_pin: 01
    rx_pin: 03
    debug:
      direction: BOTH
      dummy_receiver: false
      sequence:
      - lambda: |-
          UARTDebug::log_string(direction, bytes);
          std::string str(bytes.begin(), bytes.end());
          
          //watch for potential problems with non printable or special characters in string
          id(rawString).publish_state(str.c_str());
          /* check if packet format a1 for main bat info 
          eg: \x28\xBB\xED\x01\xCF\xB7\xA1\x00\x00\x00\x65\x00\x00\x00\x00\x00\x18\x01\x03\x44\x00\x18\x00\x5C\x00\x64\x05\x34\xFF\xCF\x00\x03\x27\x10\x00\x01\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x02\x00\x00\xFF\xFF\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xF3\x16 */
          if (str[6] == '\xA1') {
              std::string batload(str[28], str[29]);
              // int signedInt = (int) myUnsigned;
              id(bat_usable_ah).publish_state((float)str[23]); 
              id(bat_max_ah).publish_state((float)str[25]); 
              id(bat_voltage)->publish_state((float) encode_uint16(str[26], str[27]) / 100);
              id(bat_health)->publish_state((float) encode_uint16(str[32], str[33]) / 100);
              if (str[28] >= '\x80') {
                id(bat_load)->publish_state((float) (encode_uint16(str[28], str[29])-65537)/100); 
              }
              else {
                id(bat_load)->publish_state((float) encode_uint16(str[28], str[29]) /100);
              }
          }
stream_server:
   uart_id: tcp_uart_bus
   port: 6638 
sensor:
  - platform: template
    name: "ecoworthy remaining ah"
    id: "bat_usable_ah"
    accuracy_decimals: 0
    unit_of_measurement: 'ah'
    device_class: energy_storage
  - platform: template
    name: "ecoworthy total ah"
    id: "bat_max_ah"
    accuracy_decimals: 0
    unit_of_measurement: 'ah'
    device_class: energy_storage
  - platform: template
    name: "ecoworthy battery voltage"
    id: "bat_voltage"
    unit_of_measurement: 'V'
    accuracy_decimals: 2
    device_class: voltage
  - platform: template
    name: "ecoworthy health"
    id: "bat_health"
    unit_of_measurement: '%'
    accuracy_decimals: 2
  - platform: template
    name: "ecoworthy load"
    id: "bat_load"
    unit_of_measurement: 'A'
    accuracy_decimals: 4
    device_class: current
text_sensor:
  - platform: template
    name: "ecoworthy load (raw)"
    id: "raw_bat_load