Integration von LiFePO4-BMS and Smoke Alarm in HA

Ich habe neulich meine herkömmliche Autobatterie (Blei-Säure) durch eine KEPWORTH 12V 100Ah LiFePO4 mit integriertem 100A-Bluetooth-BMS ersetzt.
Über das integrierte JBD (Jiabaida) Bluetooth-BMS werden in Echtzeit die Daten wie Spannung, Strom, Kapazität (SOC) und Zellspannungen geliefert.

1. Integration einer LiFePO4-BMS-Batterie in Home Assistant (ESP32)

12V 100Ah LiFePO4 mit integriertes Bluetooth-BMS
12V 100Ah LiFePO4 mit integriertem Bluetooth-BMS

Die Überwachung und Konfiguration erfolgt mobil über die Xiaoxiang BMS App (verfügbar im App Store/Play Store).
Für das Pairing ist normalerweise kein Passwort erforderlich, und falls doch ein Passwort benötigt wird, lautet es üblicherweise 000000 oder 123456.
Nun gibt es verschiedene JBD-BMS-Modelle und es gibt auch mehrere Methoden, um JBD-BMS-Daten mit einem ESP32 auszulesen.
Meiner Meinung nach eignet sich die bewährte Library von esphome-jbd-bms-Software perfekt zum Auslesen der BMS-Daten dieser Batterie.

Der Vorteil dieser Software liegt in der automatischen Erkennung des BMS über die MAC-Adresse. (darauf achten, dass keine andere App die Bluetooth-Verbindung blockiert).
Ich verwende einen ESP32-S3 Zero Mikrocontroller, weil er über integriertes Bluetooth verfügt und als Bridge für die Kommunikation mit dem BMS über Bluetooth dienen kann.

Jump to the Code zum Auslesen von BMS-Daten


2. Integration eines RF-Rauchmelders in ESPHome

Da ich ohnehin schon am Basteln war, habe ich gleich noch einen Rauchmelder mit demselben ESP-Chip integriert.

Dafür habe ich meinen noch gut funktionierenden 433-MHz-Funkrauchmelder aus meinem alten System verwendet.
Um es mit ESPHome zu verbinden, entfernte ich den originalen RF-Transmitter und las das Alarmsignal direkt von der Platine ab.
Da das Signal eine Spannung von etwa 9V aufwies, habe ich es über einen Spannungsteiler auf 3,3V reduziert und an einem ADC-Pin (Analog-Digital-Wandler) gemessen.

433-MHz-Funkrauchmelder
433-MHz-Funkrauchmelder

Der Einsatz des ADC-Pins anstelle eines digitalen GPIOs erlaubt es mir, Schwellenwerte genau zu definieren.
So kann ich sicherstellen, dass der Alarm erst ausgelöst wird, wenn die Spannung stabil über einem bestimmten Wert liegt, was die Zuverlässigkeit im Vergleich zu einer rein digitalen Abfrage erhöht.

In Home Assistant wird dieser Wert als Binärsensor ausgegeben, der im Alarmfall so konfiguriert ist, dass er aktiv bleibt, bis er manuell zurückgesetzt wird.

Die Stromversorgung des Rauchmelders erfolgte ursprünglich über eine 9V-Blockbatterie.
Diese habe ich durch zwei 18650-Lithium-Ionen-Akkus (2S-Konfiguration) ersetzt.
Um die benötigte Betriebsspannung für den ESP-Chip bereitzustellen, wird die 8.4V Akkuspannung über ein Mini560 DC-DC Step-Down-Modul effizient auf 3,3V geregelt.

Für die Akkuladeregelung nutze ich ein 2S 15W USB-C Lademodul mit Boost-Funktion. Dieses erhöht die 5V vom USB-Eingang auf die 8,4V, die für 2S erforderlich sind.
Dadurch kann der Rauchmelder wieder aufgeladen werden und hat eine deutlich längere Lebensdauer.

ESPHome-Code für Smoke Alarm

Dieser Code wurde für den ESP32-S3 Zero entwickelt, ist jedoch grundsätzlich mit allen Modellen der ESP32-Serie kompatibel.

Hinweis: Die Pin-Belegungen für die ADC-Eingänge (Analog-Digital-Wandler) und die Status-LED müssen für den jeweils verwendeten Mikrocontroller individuell im Code angepasst werden.

Folgende Daten werden durch das Modul erfasst und verarbeitet:

  • Batteriespannung: Überwachung des aktuellen Ladestands der 2S-18650-Akkus.
  • Akkutemperatur: Überwachung der 18650-Zellen mittels eines NTC-Thermistors, um die Sicherheit beim Laden und Entladen zu gewährleisten.
  • Alarmsignal-Erkennung: Das Ausgangssignal des Rauchmelders wird kontinuierlich gemessen. Sobald ein definierter Schwellenwert überschritten wird, wird in Home Assistant ein binärer Schalter (Binary Sensor) aktiviert.
sensor:
  - platform: adc
    pin: GPIO03 # ADC1_2_
    id: adc_smoke_sensor
    internal: true
    attenuation: auto
    update_interval: 1s
    # Spannungsteilerfaktor: 1 / (33k / (100k + 33k)) = 4.03
    filters:
      - multiply: 4.09
      - calibrate_linear:
          datapoints:
            - 0.0 - 0.0
            - 8.4 - 8.4
    on_value:
        then:
          - if:
              condition:
                lambda: 'return id(adc_smoke_sensor).state gt 5;'
              then:
                - switch.turn_on: alarm_enabled

  - platform: adc
    pin: GPIO01 # ADC1_1_0
    name: "battary Voltage"
    id: bty_voltage
    unit_of_measurement: "V"
    accuracy_decimals: 2
    update_interval: 30s
    attenuation: auto
    icon: mdi:car-battery
    # Spannungsteilerfaktor: 1 / (33k / (100k + 33k)) = 4.03
    filters:
      - multiply: 4.09
      - calibrate_linear:
          datapoints:
            - 0.0 - 0.0
            - 8.4 - 8.4

  - platform: ntc
    sensor: resistance_sensor
    name: "Battery Temperature"
    unit_of_measurement: "°C"
    accuracy_decimals: 1
    icon: "mdi:thermometer"
    # Calibration using datasheet B-constant
    calibration:
      b_constant: 3950
      reference_temperature: 25°C
      reference_resistance: 5kOhm # ntc

  - platform: resistance
    id: resistance_sensor
    sensor: source_sensor
    configuration: DOWNSTREAM
    resistor: 5.6kOhm # to 3.3v
    reference_voltage: 3.3V

  - platform: adc
    id: source_sensor
    pin: GPIO13     # ADC2_2
    update_interval: 60s
    attenuation: auto

light:
  - platform: esp32_rmt_led_strip
    id: status_led
    pin: GPIO21 
    chipset: WS2812
    max_refresh_rate: 15ms
    num_leds: 1
    rgb_order: GRB
    restore_mode: ALWAYS_OFF
    rmt_symbols: 192
    default_transition_length: 0ms
    effects:
      - pulse:
          name: "Fast Pulse"
          transition_length: 100ms
          update_interval: 100ms
          min_brightness: 50%
          max_brightness: 100%

 


BMS Karte für Home Assistant Dashboard

Hier der YAML Code zur BMS Karte. Denkt daran, die Entität gegen eure zu ersetzen.

BMS Battery BMS Data Card
BMS Battery BMS Data Card
type: custom:flex-horseshoe-card
entities:
  - entity: sensor.bms_bluetooth_proxy_state_of_charge
    decimals: 0
    unit: "%"
    name: BMS Battery
    tap_action:
      action: more-info
  - entity: sensor.bms_bluetooth_proxy_battery_temperature
    decimals: 1
    unit: °C
    name: Temperature
    icon: mdi:thermometer-alert
    tap_action:
      action: more-info
  - entity: sensor.bms_bluetooth_proxy_total_voltage
    decimals: 2
    icon: mdi:car-battery
    unit: V
    tap_action:
      action: more-info
  - entity: sensor.bms_bluetooth_proxy_current
    name: Current
    decimals: 3
    unit: A
    icon: mdi:current-dc
  - entity: sensor.bms_bluetooth_proxy_max_cell_voltage
    name: highest cell
    decimals: 3
    unit: V
    tap_action:
      action: more-info
  - entity: sensor.bms_bluetooth_proxy_min_cell_voltage
    name: lowest cell
    decimals: 3
    unit: V
  - entity: binary_sensor.bms_bluetooth_proxy_charging
    name: charge
    icon: mdi:power
    tap_action:
      action: more-info
  - entity: binary_sensor.bms_bluetooth_proxy_discharging
    name: discharge
    icon: mdi:power
    tap_action:
      action: more-info
  - entity: binary_sensor.bms_bluetooth_proxy_balancing
    name: balancer
    icon: mdi:chart-gantt
    tap_action:
      action: more-info
  - entity: sensor.bms_bluetooth_proxy_capacity_remaining
    decimals: 2
    unit: Ah
    name: capacity
    icon: mdi:lightning-bolt
  - entity: binary_sensor.bms_bluetooth_proxy_charging
    decimals: 2
    name: charging
    icon: mdi:battery-sync-outline
  - entity: sensor.bms_bluetooth_proxy_charging_cycles
    decimals: 0
    name: cycles
    icon: mdi:cached
  - entity: sensor.bms_bluetooth_proxy_charging_power
    name: charging_power
    decimals: 2
    tap_action:
      action: more-info
  - entity: sensor.bms_bluetooth_proxy_discharging_power
    name: discharging_power
    decimals: 2
    tap_action:
      action: more-info
layout:
  icons:
    - id: 1
      entity_index: 1
      xpos: 2
      ypos: 5
      icon_size: 1.2
      color: "#2798F5"
    - id: 2
      entity_index: 2
      xpos: 77
      ypos: 5
      icon_size: 1.2
      color: "#2798F5"
    - id: 3
      entity_index: 3
      xpos: 25
      ypos: 29
      icon_size: 1.2
      color: "#2798F5"
    - id: 8
      entity_index: 8
      xpos: 15
      ypos: 72
      align: start
      icon_size: 1
      color: "#2798F5"
    - id: 9
      entity_index: 9
      xpos: 14
      ypos: 80
      align: start
      icon_size: 1.1
      color: "#2798F5"
    - id: 10
      entity_index: 10
      animation_id: 3
      xpos: 14
      ypos: 87
      align: start
      icon_size: 1.1
      color: "#2798F5"
    - id: 11
      entity_index: 11
      xpos: 56
      ypos: 87
      align: start
      icon_size: 1.2
      color: "#2798F5"
  hlines:
    - id: 0
      xpos: 50
      ypos: 34
      length: 65
      styles:
        - stroke-width: 3;
        - opacity: 0.4;
        - stroke-linecap: round;
  vlines:
    - id: 0
      xpos: 50
      ypos: 52
      length: 27
      styles:
        - opacity: 0.2;
        - stroke-linecap: round;
  states:
    - id: 0
      entity_index: 0
      xpos: 50
      ypos: 20
      styles:
        - font-size: 1.5em;
        - opacity: 0.9;
    - id: 1
      entity_index: 1
      xpos: 5
      ypos: 6
      styles:
        - text-anchor: start;
    - id: 2
      entity_index: 2
      xpos: 100
      ypos: 6
      styles:
        - text-anchor: end;
    - id: 3
      entity_index: 3
      xpos: 75
      ypos: 30
      styles:
        - text-anchor: end;
        - opacity: 0.9;
    - id: 4
      entity_index: 4
      xpos: 47
      ypos: 44
      styles:
        - text-anchor: end;
        - opacity: 0.9;
    - id: 5
      entity_index: 5
      xpos: 54
      ypos: 44
      styles:
        - text-anchor: start;
        - opacity: 0.9;
    - id: 6
      entity_index: 6
      xpos: 43
      ypos: 60
      styles:
        - text-anchor: end;
        - opacity: 0;
    - id: 12
      entity_index: 12
      xpos: 32
      ypos: 58
      styles:
        - text-anchor: start;
        - opacity: 0.9;
        - font-size: 0.7em;
    - id: 7
      entity_index: 7
      xpos: 60
      ypos: 60
      styles:
        - text-anchor: end;
        - opacity: 0;
    - id: 13
      entity_index: 13
      xpos: 55
      ypos: 58
      styles:
        - text-anchor: start;
        - opacity: 0.9;
        - font-size: 0.7em;
    - id: 8
      entity_index: 8
      xpos: 51
      ypos: 73
      styles:
        - text-anchor: start;
        - font-size: 0.7em;
    - id: 9
      entity_index: 9
      xpos: 50
      ypos: 80
      styles:
        - font-size: 0.7em;
        - text-anchor: start;
    - id: 10
      entity_index: 10
      xpos: 51
      ypos: 88
      styles:
        - font-size: 0.7em;
        - text-anchor: start;
    - id: 11
      entity_index: 11
      xpos: 72
      ypos: 87.5
      styles:
        - font-size: 0.7em;
        - text-anchor: start;
        - opacity: 0.7;
  names:
    - id: 0
      entity_index: 0
      xpos: 50
      ypos: 100
      styles:
        - font-size: 0.8em;
        - opacity: 0;
    - id: 3
      entity_index: 3
      xpos: 30
      ypos: 29.5
      styles:
        - text-anchor: start;
        - font-size: 0.8em;
        - opacity: 0.7;
    - id: 4
      entity_index: 4
      xpos: 46
      ypos: 49
      styles:
        - font-size: 0.5em;
        - text-anchor: end;
        - opacity: 0.7;
    - id: 5
      entity_index: 5
      xpos: 54
      ypos: 49
      styles:
        - font-size: 0.5em;
        - text-anchor: start;
        - opacity: 0.7;
    - id: 6
      entity_index: 6
      xpos: 33
      ypos: 62
      styles:
        - text-anchor: start;
        - font-size: 0.5em;
        - opacity: 0.7;
    - id: 7
      entity_index: 7
      xpos: 54
      ypos: 62
      styles:
        - text-anchor: start;
        - font-size: 0.4em;
        - opacity: 0.7;
    - id: 8
      entity_index: 8
      xpos: 29
      ypos: 73
      styles:
        - text-anchor: start;
        - font-size: 0.5em;
        - opacity: 0.7;
    - id: 9
      entity_index: 9
      xpos: 29
      ypos: 80
      styles:
        - text-anchor: start;
        - font-size: 0.5em;
        - opacity: 0.7;
    - id: 10
      entity_index: 10
      xpos: 29
      ypos: 88
      styles:
        - text-anchor: start;
        - font-size: 0.5em;
        - opacity: 0.7;
    - id: 11
      entity_index: 11
      xpos: 70
      ypos: 88
      styles:
        - text-anchor: start;
        - font-size: 0.6em;
        - opacity: 0;
animations:
  entity.6:
    - state: "on"
      icons:
        - animation_id: 0
          styles:
            - color: orange;
            - opacity: 0.9;
    - state: "off"
      icons:
        - animation_id: 0
          styles:
            - fill: var(--primary-text-color);
  entity.7:
    - state: "off"
      icons:
        - animation_id: 1
          styles:
            - fill: var(--primary-text-color);
    - state: "on"
      icons:
        - animation_id: 1
          styles:
            - color: orange;
            - opacity: 0.9;
  entity.10:
    - state: "on"
      icons:
        - animation_id: 3
          styles:
            - color: orange;
            - opacity: 0.9;
    - state: "off"
      icons:
        - animation_id: 3
          styles:
            - fill: var(--primary-text-color);
show:
  horseshoe_style: colorstopgradient
horseshoe_state:
  color: "#db4437"
  width: 6
horseshoe_scale:
  min: 0
  max: 110
  color: "#43a047"
  width: 2
color_stops:
  "65": red
  "75": yellow
  "85": green
card_mod:
  style: |
    ha-card {
        height: 340px !important;
        Box-shadow: none !important;
        background-color: transparent !important;
        --ha-card-background: var(--card-background-color);
        color: var(--primary-color);
      }

Code zum Auslesen von BMS-Daten

Der folgende Code basiert auf dem Beispiel esp32-ble-example.yaml aus dem syssi-Repository (GitHub: esphome-jbd-bms).
Ich habe das ursprüngliche Skript angepasst und alle für mein Projekt nicht relevanten Zeilen entfernt, um den Code schlank und übersichtlich zu halten.

Hinweis: Da meine 12V 100Ah LiFePO4-Batterie intern aus 4 Zellen besteht und das BMS keine Daten zur einzelnen Zelltemperatur übermittelt, wurden die entsprechenden Sensor-Entitäten aus dem Code entfernt.

Um den Energieverbrauch (WiFi & System) des ESP32 zu optimieren, wurden außerdem die folgenden Anpassungen an der YAML-Konfiguration vorgenommen:

MaßnahmeVorteil
Fast ConnectSchnellerer Verbindungsaufbau zum WLAN.
Power Save Mode (LIGHT)Reduziert den Stromverbrauch im Leerlauf.
Static IPSpart Zeit und Traffic beim DHCP-Handshake.

# *******************************************************************************
# ESP32 S3 Mini Zero based 
# ESPHome code to monitor and control a JBD-BMS via Bluetooth
# Aus Originalskript esp32-ble-example.yaml - syssi (GitHub: esphome-jbd-bms) Library
# ******************************************************************************

substitutions:
  name: jbd-bms-ble
  device_description: "Monitor and control a Xiaoxiang Battery Management System (JBD-BMS) via BLE"
  external_components_source: GitHub://syssi/esphome-jbd-bms@main
  mac_address: A5:C2:39:1A:E0:52
  log_level: "DEBUG" # "INFO" # "DEBUG"
  friendly_name: 'jbd-bms-ble and garage_smoke'

esphome:
  name: ${name}
  friendly_name: ${friendly_name}
  comment: ${device_description}
  project:
    name: "syssi.esphome-jbd-bms"
    version: 2.4.0

esp32:
  board: esp32-s3-devkitc-1
  framework:
    type: esp-idf


external_components:
  - source: ${external_components_source}
    refresh: 0s

packages:
  # Individual time estimation packages from remote repository
  remaining_charging_time:
    url: https://github.com/syssi/esphome-jbd-bms
    files:
      - path: packages/remaining_charging_time.yaml
        vars:
          sensor_id: bms0_remaining_charging_time
          sensor_name: "remaining charging time"
          current_sensor_id: bms0_average_current
          capacity_remaining_id: bms0_capacity_remaining
          nominal_capacity_id: bms0_nominal_capacity
  remaining_discharging_time:
    url: https://github.com/syssi/esphome-jbd-bms
    files:
      - path: packages/remaining_discharging_time.yaml
        vars:
          sensor_id: bms0_remaining_discharging_time
          sensor_name: "remaining discharging time"
          current_sensor_id: bms0_average_current
          capacity_remaining_id: bms0_capacity_remaining

wifi:
  power_save_mode: LIGHT # HIGH
  fast_connect: true
  ssid: !secret iot_wifi_ssid
  password: !secret iot_wifi_password
  # Optional manual IP
  manual_ip:
    static_ip: 192.168.178.xxx
    gateway: 192.168.178.1
    subnet: 255.255.255.0

logger:
  level: ${log_level}

ota:
  platform: esphome
  on_begin:
    then:
      - switch.turn_off: ble_client_switch0
      - logger.log: "BLE connection suspended for OTA update"

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

esp32_ble_tracker:
  scan_parameters:
    active: false

ble_client:
  - id: client0
    mac_address: ${mac_address}

jbd_bms_ble:
  - id: bms0
    ble_client_id: client0

binary_sensor:
  - platform: jbd_bms_ble
    jbd_bms_ble_id: bms0
    balancing:
      name: "balancing"
    charging:
      name: "charging"
    discharging:
      name: "discharging"
    online_status:
      name: "online status"


sensor:
  - platform: jbd_bms_ble
    jbd_bms_ble_id: bms0
    battery_strings:
      name: "battery strings"
    current:
      name: "current"
      id: bms0_current
    power:
      name: "power"
    charging_power:
      name: "charging power"
    discharging_power:
      name: "discharging power"
    state_of_charge:
      name: "state of charge"
    nominal_capacity:
      name: "nominal capacity"
      id: bms0_nominal_capacity
    charging_cycles:
      name: "charging cycles"
    capacity_remaining:
      name: "capacity remaining"
      id: bms0_capacity_remaining
    battery_cycle_capacity:
      name: "battery cycle capacity"
    total_voltage:
      name: "total voltage"
    average_cell_voltage:
      name: "average cell voltage"
    delta_cell_voltage:
      name: "delta cell voltage"
    min_cell_voltage:
      name: "min cell voltage"
    max_cell_voltage:
      name: "max cell voltage"
    min_voltage_cell:
      name: "min voltage cell"
    max_voltage_cell:
      name: "max voltage cell"
    cell_voltage_1:
      name: "cell voltage 1"
    cell_voltage_2:
      name: "cell voltage 2"
    cell_voltage_3:
      name: "cell voltage 3"
    cell_voltage_4:
      name: "cell voltage 4"
    software_version:
      name: "software version"

  # Average current sensor with exponential filtering
  - platform: copy
    source_id: bms0_current
    name: "average current"
    id: bms0_average_current
    filters:
      - exponential_moving_average:
          alpha: 0.1
          send_every: 1
  # WiFi Signal sensor.
  - platform: wifi_signal
    name: WiFi Signal
    update_interval: 30s
    unit_of_measurement: dB
    accuracy_decimals: 0
    force_update: false
    icon: mdi:wifi
    
select:
  - platform: jbd_bms_ble
    jbd_bms_ble_id: bms0
    read_eeprom_register:
      name: "read eeprom register"
      id: read_eeprom_register0
      optionsmap:
        0xAA: "Error Counts"

switch:
  - platform: ble_client
    ble_client_id: client0
    name: "enable bluetooth connection"
    id: ble_client_switch0

  - platform: jbd_bms_ble
    jbd_bms_ble_id: bms0
    charging:
      name: "charging"
    discharging:
      name: "discharging"

text_sensor:
  - platform: jbd_bms_ble
    jbd_bms_ble_id: bms0
    errors:
      name: "errors"
    operation_status:
      name: "operation status"
    device_model:
      name: "device model"
  - platform: wifi_info
    ip_address:
      name: IP Address
    mac_address:
      name: Mac Address