Skip to content

Instantly share code, notes, and snippets.

@miaucl
Last active April 3, 2025 13:59
Show Gist options
  • Save miaucl/1fdc65705bdb6166e32805d434637e2a to your computer and use it in GitHub Desktop.
Save miaucl/1fdc65705bdb6166e32805d434637e2a to your computer and use it in GitHub Desktop.
Cube Battery SOC Station powered by ESPHome

Cube Battery SOC Station powered by ESPHome

Bike

https://www.cube.eu/de-de/cube-trike-family-hybrid-750-swampgrey-n-reflex/789520

Problems

  • Battery from Bosch, required to be removed for charging
  • Charger does not allow smart control and always charges to 100%

Goal

  • Control when to charge
  • Control target SOC (ex. 80%) to enhance battery lifetime
  • Cut power to charger when done for safety reasons

Hardware

  • D1Mini ESP32
  • Push Button
  • Switch
  • Luminosity sensor
  • Smart power socket

Tools

  • ESPHome
  • Home Assistant
  • 3D Printer

Summary

The charging station needs to have following features, detect battery presence, read current SOC and toggle battery saving mode. Using ESPHome, we can connect a push button to detect the battery, a simple switch to choose the mode and a carefully placed luminosity sensor wih a resistance dividor to get the number of LEDs turned on. There are 5 LEDs which indicate the rough SOC in 20% steps. Using the analog input of the ESP32, we convert the raw signal, combined with the battery presence, to a SOC and expose it as sensors to Home Assistant. In Home Assistant, we implement 2 scripts to turn on the power socket with the charger when a battery is detected and turn off the charger when the battery reached our desired SOC.

Photos

Battery

Connector and LEDs for current battery charge.

IMG_3512 IMG_3511

Charging Station

IMG_3514 IMG_3515

See here for 3D printing.

https://www.thingiverse.com/thing:6999548/files

Home Assistant

Available sensors exposed from ESPHome.

Bildschirmfoto 2025-04-03 um 15 19 04

Typical charging session to 100%.

Bildschirmfoto 2025-04-03 um 15 18 39
alias: Cube SOC done charging
triggers:
- type: turned_on
device_id: 137b6ca5456e78a0320f59f660f7181d
entity_id: 93b53df07e8017ee35c00f4327f40b05
domain: binary_sensor
trigger: device
for:
hours: 0
minutes: 0
seconds: 10
conditions: []
actions:
- type: turn_off
device_id: 348d7d46f72e10dde140eb8c3d38ed5b
entity_id: a8bd86f0cbaf0f62d835fe849a2322cb
domain: light
- type: turn_off
device_id: 348d7d46f72e10dde140eb8c3d38ed5b
entity_id: d2c59626a91f45fb8bb42c5658f926c2
domain: switch
mode: single
---
alias: Cube SOC ready for charging
triggers:
- type: present
device_id: 137b6ca5456e78a0320f59f660f7181d
entity_id: e8809995f9df4b5b4b5b64caa0d80202
domain: binary_sensor
trigger: device
for:
hours: 0
minutes: 0
seconds: 10
conditions: []
actions:
- type: turn_on
device_id: 348d7d46f72e10dde140eb8c3d38ed5b
entity_id: a8bd86f0cbaf0f62d835fe849a2322cb
domain: light
flash: long
- type: turn_on
device_id: 348d7d46f72e10dde140eb8c3d38ed5b
entity_id: d2c59626a91f45fb8bb42c5658f926c2
domain: switch
- delay:
hours: 0
minutes: 0
seconds: 20
milliseconds: 0
- type: turn_on
device_id: 348d7d46f72e10dde140eb8c3d38ed5b
entity_id: a8bd86f0cbaf0f62d835fe849a2322cb
domain: light
mode: single
esphome:
name: cube-battery-soc
friendly_name: Cube Battery SOC
esp8266:
board: d1_mini
# Enable logging
logger:
level: INFO
# Enable Home Assistant API
api:
encryption:
key: "<redacted>"
ota:
- platform: esphome
password: "<redacted>"
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "Cube-Battery-Soc"
password: "<redacted>"
captive_portal:
# Depends on the build and most probably require adjustments
globals:
- id: threshold_1_bar
type: float
restore_value: no
initial_value: '0.08'
- id: threshold_2_bar
type: float
restore_value: no
initial_value: '0.2'
- id: threshold_3_bar
type: float
restore_value: no
initial_value: '0.4'
- id: threshold_4_bar
type: float
restore_value: no
initial_value: '0.8'
- id: threshold_5_bar
type: float
restore_value: no
initial_value: '0.925'
sensor:
- platform: adc
pin: A0
id: raw
name: "RAW"
update_interval: 100ms # 10Hz (every 100ms)
on_value:
- lambda: |-
ESP_LOGD("Raw Sensor", "Value changed to: %f", x);
filters:
- sliding_window_moving_average:
window_size: 40
send_every: 10
send_first_at: 10
- skip_initial: 4
- platform: template
id: level
name: "Level"
device_class: battery
unit_of_measurement: "%"
update_interval: 1s
lambda: |-
if (!id(battery_presence).state) return {};
else if (id(raw).state > id(threshold_5_bar)) return 100.0;
else if (id(raw).state > id(threshold_4_bar)) return 80.0;
else if (id(raw).state > id(threshold_3_bar)) return 60.0;
else if (id(raw).state > id(threshold_2_bar)) return 40.0;
else if (id(raw).state > id(threshold_1_bar)) return 20.0;
return 0.0;
on_value:
- lambda: |-
ESP_LOGD("Level", "Battery level changed to: %f\%", x);
binary_sensor:
- platform: template
id: charged
name: "Charged"
lambda: |-
return id(battery_presence).state && ((id(level).state == 80.0 && id(lifetime_optimisation).state) || (id(level).state == 100.0 && !id(lifetime_optimisation).state));
filters:
- delayed_on: 3s
on_state:
- lambda: |-
ESP_LOGI("Battery charge", "Battery is %s", x ? "charged" : "not charged");
- platform: template
id: charging
name: "Charging"
device_class: battery_charging
lambda: |-
return id(battery_presence).state && id(raw).state > id(threshold_1_bar);
on_state:
- lambda: |-
ESP_LOGI("Battery charging", "Battery is %s", x ? "charging" : "not charging");
- platform: gpio
pin:
number: D5
mode: INPUT_PULLUP
id: lifetime_optimisation # Target for SOC: ON=80%, OFF=100%
name: "Lifetime optimisation"
on_state:
- lambda: |-
ESP_LOGI("SOC mode", "Charge until %s", x ? "80\%" : "100\%");
- platform: gpio
pin:
number: D6
mode: INPUT_PULLUP
name: "Presence"
id: battery_presence
device_class: presence
filters:
- invert:
on_state:
- lambda: |-
ESP_LOGI("Presence", "Battery is %s", x ? "present" : "not present");
switch:
- platform: gpio
id: add_gnd_0
pin: D0
restore_mode: ALWAYS_OFF
- platform: gpio
id: add_gnd_1
pin: D7
restore_mode: ALWAYS_OFF
- platform: gpio
id: add_gnd_2
pin: D8
restore_mode: ALWAYS_OFF
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment