LiTime Solar MPPT Charge Controller data via BLE (ESPHome)

Hey all! I reverse-engineered the BLE commands and responses for core data for my LiTime MPPT solar charge controller:

I hope this helps someone; if you have questions, concerns, or suggestions, feel free to open an issue in the repo. Thanks!

3 Likes

This is just great! Amazing job Mark, never thought it would be integrable with home assistant.
I created a ESPhome shunt that is connected to the MPTT, now i can combine the code of both sensors.

Awesome! I’m curious to see where to take it :slightly_smiling_face:

I have this working and thanks for your work, but i was wondering about the text sensor in the litime_solar_mppt.yaml, have you managed to get this working or is there a work around, as this will not compile when i try to set it up giving a few errors ? those addition would be great in my setup.

# ESPHome Configuration for BT-LTMPPT4860 Solar Charge Controller

# specify values to be substitued into common components. todo: change these as desired.
substitutions:
  location: solar
  locationName: Battery Bay
  device: litime-solar
  deviceUnderscore: litime_solar
  deviceName: LiTime Solar
  deviceDescription: LiTime Solar BLE pull
  liTimeMacAddress: AA:BB:CC:DD:EE:FF # todo: replace with the MAC for your device

# Enable Home Assistant API
api:
  encryption:
    key: !secret encryption_key

preferences:
  flash_write_interval: 5min

logger:
  level: DEBUG
  baud_rate: 0 # disable logging over UART (maybe comment this out for initial upload)

ota:
  password: !secret ota_password
  platform: esphome

wifi:
  ssid: !secret wifi_ssid
  password: !secret wifi_psk
  power_save_mode: light
  fast_connect: true    # don't bother scanning. When doing a full scan, can get watchdog timeouts, and we know who we're connecting to anyway
  use_address: !secret litime_solar_ip
  manual_ip:
    static_ip: !secret litime_solar_ip
    gateway: 192.168.1.1 # todo: replace with your gateway IP if yours is different
    subnet: 255.255.255.0


#region Buttons

button:
  - platform: restart
    name: $location $deviceName Restart 
    id: ${location}_${deviceUnderscore}_restart

#endregion

esphome:
  includes: 
    - litime_solar_mppt.h
  name: ${location}-${device}
  comment: $deviceDescription

esp32:
  board: nodemcu-32s
  framework:
    type: arduino

esp32_ble_tracker:
  scan_parameters:
    # We currently use the defaults to ensure Bluetooth
    # can co-exist with WiFi In the future we may be able to
    # enable the built-in coexistence logic in ESP-IDF
    active: true

# Using ble_client to get notifications from the device
sensor:
  - platform: wifi_signal
    name: $location $deviceName WiFi Signal
    id: ${location}_${deviceUnderscore}_wifi_signal
    entity_category: diagnostic
    update_interval: 60s

  - platform: uptime
    name: $location $deviceName Uptime
    id: ${location}_${deviceUnderscore}_uptime
    entity_category: diagnostic
    accuracy_decimals: 0
    unit_of_measurement: min
    update_interval: 60s
    filters:
      - lambda: return x / 60;

  - platform: template
    name: "LiTime Battery Voltage"
    unit_of_measurement: "V"
    accuracy_decimals: 1
    id: battery_voltage
    device_class: voltage
        
  - platform: template
    name: "LiTime Battery Current"
    unit_of_measurement: "A"
    accuracy_decimals: 2
    id: battery_current
    device_class: current
    
  - platform: template
    name: "LiTime Battery Power"
    unit_of_measurement: "W"
    accuracy_decimals: 0
    id: battery_power
    device_class: "power"
    
  - platform: template
    name: "LiTime Controller Temperature"
    unit_of_measurement: "°C"
    accuracy_decimals: 0
    id: controller_temp
    device_class: "temperature"

  - platform: template
    name: "LiTime Load Voltage"
    unit_of_measurement: "V"
    accuracy_decimals: 1
    id: load_voltage
    device_class: voltage
    
  - platform: template
    name: "LiTime Load Current"
    unit_of_measurement: "A"
    accuracy_decimals: 2
    id: load_current
    device_class: current
    
  - platform: template
    name: "LiTime Load Power"
    unit_of_measurement: "W"
    accuracy_decimals: 0
    id: load_power
    device_class: "power"
    
  - platform: template
    name: "LiTime PV Input Voltage"
    unit_of_measurement: "V"
    accuracy_decimals: 1
    id: pv_voltage
    device_class: voltage
    
  - platform: template
    name: "LiTime Max Charging Power Today"
    unit_of_measurement: "W"
    accuracy_decimals: 0
    id: max_charge_power
    device_class: "power"

  - platform: template
    name: "LiTime Power Generation Today"
    unit_of_measurement: "Wh"
    accuracy_decimals: 0
    id: energy_today
    device_class: "energy"
    state_class: total_increasing
    
  - platform: template
    name: "LiTime Running Days"
    unit_of_measurement: "days"
    accuracy_decimals: 0
    id: running_days
    
  - platform: template
    name: "LiTime Total Power Generation"
    unit_of_measurement: "Wh"
    accuracy_decimals: 0
    id: total_energy
    device_class: "energy"
    state_class: total_increasing
    
  - platform: template
    name: "LiTime Battery Level"
    unit_of_measurement: "%"
    accuracy_decimals: 0
    update_interval: 60s
    device_class: "battery"
    lambda: |-
      if (id(battery_voltage).has_state()) {
        float voltage = id(battery_voltage).state;
        // Adjust these values based on your battery type
        float min_voltage = 11.0;
        float max_voltage = 14.4;
        return std::min(100.0f, std::max(0.0f, (voltage - min_voltage) / (max_voltage - min_voltage) * 100.0f));
      } else {
        return 0.0f;
      }

# text_sensor:
#   - platform: template
#     name: "LiTime Controller Status"
#     id: controller_status
#     lambda: |-
#       switch (id(mppt_data).controller_mode) {
#         case 0: return {"Standby"};
#         case 1: return {"Float"};
#         case 2: return {"Boost"};
#         case 3: return {"Equalization"};
#         case 4: return {"Absorption"};
#         default: return {"Unknown"};
#       }
      
# Custom component to handle BLE notifications
  - platform: ble_client
    ble_client_id: ltmppt_client
    id: ble_sensor
    internal: true
    service_uuid: "ffe0"
    characteristic_uuid: "ffe1"
    type: characteristic
    notify: true
    update_interval: never
    # on_notify: 
    #   then:
    #     - lambda: |-
    #         ESP_LOGW("ble_client.notify", "x: %.2f", x);

    lambda: |-
      // Display the hex string for debugging.
      std::vector<uint8_t> data = x;
      String hex_string = "";
      for (size_t i = 0; i < x.size(); ++i) {
        char hex_buffer[3];
        snprintf(hex_buffer, sizeof(hex_buffer), "%02X", data[i]);
        hex_string += hex_buffer;
        if (i % 2 == 1 && i != data.size() - 1) {
          hex_string += " ";  // Insert a space every two bytes
        }
      }
      uint8_t arr_size = x.size();
      ESP_LOGD("LOG_TAG", "Hexadecimal string: %s", hex_string.c_str());

      HandleResponseData(x);
      return 0.0; // this sensor isn't actually used other than to hook into raw value and publish to template sensors

# Add a switch to control the DC load
switch:
  - platform: template
    name: "LiTime DC Load"
    id: dc_load
    optimistic: true
    restore_mode: RESTORE_DEFAULT_ON
    turn_on_action:
      - ble_client.ble_write:
          id: ltmppt_client
          service_uuid: "FFE0"
          characteristic_uuid: "FFE1"
          value: !lambda |-
            ESP_LOGI("mppt", "Sending load ON command");

            // Command to turn on load: 010601200001483c
            vector<uint8_t> request = {0x01, 0x06, 0x01, 0x20, 0x00, 0x01, 0x48, 0x3c};
            return request;
    turn_off_action:
      - ble_client.ble_write:
          id: ltmppt_client
          service_uuid: "FFE0"
          characteristic_uuid: "FFE1"
          value: !lambda |-
            ESP_LOGI("mppt", "Sending load OFF command");

            // Command to turn off load: 01060120000089fc
            vector<uint8_t> request = {0x01, 0x06, 0x01, 0x20, 0x00, 0x00, 0x89, 0xfc};
            return request;

# Define the BLE client for the LTMPPT4860
ble_client:
  - mac_address: ${liTimeMacAddress}
    id: ltmppt_client
    auto_connect: true 
    on_connect:
      then:
        - logger.log: 
            format: "BLE client connected to LiTime MPPT"
            level: DEBUG
    on_disconnect:
      then:
        - logger.log: 
            format: "BLE client disconnected from LiTime MPPT"
            level: DEBUG

# Run this when we connect to the device
interval:
  - interval: 10s
    then:
      - ble_client.ble_write:
          id: ltmppt_client
          service_uuid: "ffe0"
          characteristic_uuid: "ffe1"

          value: [0x01, 0x03, 0x01, 0x01, 0x00, 0x13, 0x54, 0x3B]
          # example output: 0103 2600 6400 8E04 2E00 9719 FF00 0000 0000 0003 1803 6805 0400 0000 0400 0000 0300 0018 2A00 0000 00E1 65

I haven’t looked at it since I first wrote the integration, and I don’t know when I’ll have a chance. But if you are about to get it working, please feel free to submit a pull request to the project.

Thanks!

1 Like

Thank you for the quick reply and i will do.