If you charge an e-bike (or any battery-powered device) through a Shelly plug with power monitoring, here’s a setup that goes beyond a simple on/off relay: it estimates the battery’s State of Charge from energy consumption, shows a live time-remaining estimate, automatically cuts power once the target % (or 100%) is reached, and self-calibrates its own accuracy over time.
Hardware
Any Shelly plug with power monitoring (Plug S, Plus Plug S, Pro, etc.) wired to the charger. That’s it, no extra sensors needed, everything else is template sensors and automations inside Home Assistant.
How it works
A template sensor estimates SOC using the % you set as the starting point, plus the energy consumed since charging began, divided by a calibration factor (Wh needed per 1% of battery). Four automations manage the session lifecycle: detecting charge start (power rises above a threshold for a few seconds), detecting the target % being reached (switches the plug off immediately), detecting real charge end/idle (power drops and stays low for a few minutes, in case the battery’s own BMS stops charging before your target), and, the interesting part, auto-calibration.
Every time you confirm or correct the “real final %” (because you actually checked the bike’s own display), the automation compares your correction against its own estimate and nudges the Wh-per-percent calibration factor with a weighted average (70% old value / 30% newly observed value), clamped to a sane range. A small counter tracks how many calibration passes have run, so you can see the estimate’s accuracy improve the more you use it.
The dashboard card (Mushroom cards) shows time remaining, live power/energy, starting/target/real % sliders, and a status tile whose icon changes color: grey battery when off, red while actively charging, green once the target or 100% is reached.
Dashboard
Blueprint
I packaged the four automations into a single blueprint (using trigger IDs + choose, so it’s one automation instead of four). You’ll still need to create the helper entities (a few input_number/input_boolean/counter helpers) and the “calculated SOC” template sensor yourself, see the comments in the blueprint for what’s expected. I adapted this from a working setup I run daily, but haven’t independently tested the blueprint packaging itself, so feedback welcome if you hit snags adapting it to your entities.
blueprint:
name: E-Bike Charging Monitor & Self-Calibrating SOC (Shelly PM)
description: >
Tracks a charging session using a Shelly power-monitoring plug, estimates
battery State of Charge from energy consumption, cuts power at the
target % (or near 100%), and self-calibrates the Wh-per-percent ratio
every time you correct the real final charge level. Requires a set of
input_number/input_boolean/counter helpers and a "calculated SOC"
template sensor (see forum post for setup notes).
domain: automation
input:
power_sensor:
name: Charger power sensor
selector:
entity:
domain: sensor
device_class: power
energy_sensor:
name: Charger energy sensor (kWh)
selector:
entity:
domain: sensor
device_class: energy
charger_switch:
name: Charger switch
selector:
entity:
domain: switch
calculated_soc_sensor:
name: Calculated SOC template sensor (you create this, see notes)
selector:
entity:
domain: sensor
start_soc_helper:
name: "Helper: starting % (input_number)"
selector:
entity:
domain: input_number
target_soc_helper:
name: "Helper: target % (input_number)"
selector:
entity:
domain: input_number
real_final_soc_helper:
name: "Helper: real final % (input_number)"
selector:
entity:
domain: input_number
wh_per_percent_helper:
name: "Helper: Wh-per-percent calibration (input_number)"
selector:
entity:
domain: input_number
session_start_soc_helper:
name: "Helper: session start % (input_number, internal)"
selector:
entity:
domain: input_number
session_start_energy_helper:
name: "Helper: session start energy (input_number, internal)"
selector:
entity:
domain: input_number
charging_active_helper:
name: "Helper: charging-active (input_boolean)"
selector:
entity:
domain: input_boolean
calibration_counter:
name: "Helper: calibration counter (optional, counter)"
default: ""
selector:
entity:
domain: counter
start_power_threshold:
name: Power threshold to detect charging start (W)
default: 15
selector:
number: { min: 1, max: 200, unit_of_measurement: W }
end_power_threshold:
name: Power threshold to detect charging end (W)
default: 5
selector:
number: { min: 1, max: 100, unit_of_measurement: W }
notify_start_actions:
name: Actions to run when charging starts (e.g. notifications)
default: []
selector:
action: {}
notify_target_actions:
name: Actions to run when target % is reached
default: []
selector:
action: {}
notify_end_actions:
name: Actions to run when charging ends
default: []
selector:
action: {}
triggers:
- id: start
trigger: numeric_state
entity_id: !input power_sensor
above: !input start_power_threshold
for: { seconds: 10 }
- id: target
trigger: numeric_state
entity_id: !input calculated_soc_sensor
above: !input target_soc_helper
- id: end
trigger: numeric_state
entity_id: !input power_sensor
below: !input end_power_threshold
for: { minutes: 5 }
- id: calibrate
trigger: state
entity_id: !input real_final_soc_helper
conditions: []
actions:
- variables:
power_sensor: !input power_sensor
energy_sensor: !input energy_sensor
charger_switch: !input charger_switch
calculated_soc_sensor: !input calculated_soc_sensor
start_soc_helper: !input start_soc_helper
target_soc_helper: !input target_soc_helper
real_final_soc_helper: !input real_final_soc_helper
wh_per_percent_helper: !input wh_per_percent_helper
session_start_soc_helper: !input session_start_soc_helper
session_start_energy_helper: !input session_start_energy_helper
charging_active_helper: !input charging_active_helper
calibration_counter: !input calibration_counter
- choose:
- conditions:
- condition: trigger
id: start
- condition: state
entity_id: !input charging_active_helper
state: "off"
sequence:
- action: input_number.set_value
target: { entity_id: !input session_start_energy_helper }
data: { value: "{{ states(energy_sensor) | float(0) }}" }
- action: input_number.set_value
target: { entity_id: !input session_start_soc_helper }
data: { value: "{{ states(start_soc_helper) | float(0) }}" }
- action: input_boolean.turn_on
target: { entity_id: !input charging_active_helper }
- choose: []
default: !input notify_start_actions
- conditions:
- condition: trigger
id: target
- condition: state
entity_id: !input charging_active_helper
state: "on"
sequence:
- action: switch.turn_off
target: { entity_id: !input charger_switch }
- choose: []
default: !input notify_target_actions
- conditions:
- condition: trigger
id: end
- condition: state
entity_id: !input charging_active_helper
state: "on"
sequence:
- variables:
final_soc: "{{ states(calculated_soc_sensor) | float(0) }}"
- action: input_number.set_value
target: { entity_id: !input start_soc_helper }
data: { value: "{{ final_soc | round(0) }}" }
- action: switch.turn_off
target: { entity_id: !input charger_switch }
- action: input_boolean.turn_off
target: { entity_id: !input charging_active_helper }
- action: input_number.set_value
target: { entity_id: !input real_final_soc_helper }
data: { value: "{{ (100 if final_soc >= 97 else final_soc) | round(0) }}" }
- choose: []
default: !input notify_end_actions
- conditions:
- condition: trigger
id: calibrate
- condition: state
entity_id: !input charging_active_helper
state: "off"
sequence:
- variables:
real_final: "{{ states(real_final_soc_helper) | float(0) }}"
start_before: "{{ states(session_start_soc_helper) | float(0) }}"
wh_now: "{{ states(energy_sensor) | float(0) * 1000 }}"
wh_start: "{{ states(session_start_energy_helper) | float(0) * 1000 }}"
consumed: "{{ wh_now - wh_start }}"
real_gain: "{{ real_final - start_before }}"
old_factor: "{{ states(wh_per_percent_helper) | float(9.1) }}"
- if:
- condition: template
value_template: "{{ real_gain > 1 and consumed > 0 }}"
then:
- variables:
observed_factor: "{{ consumed / real_gain }}"
new_factor_clamped: "{{ min(max((old_factor*0.7 + observed_factor*0.3), 5), 15) | round(2) }}"
- action: input_number.set_value
target: { entity_id: !input wh_per_percent_helper }
data: { value: "{{ new_factor_clamped }}" }
- action: input_number.set_value
target: { entity_id: !input start_soc_helper }
data: { value: "{{ real_final | round(0) }}" }
- if:
- condition: template
value_template: "{{ calibration_counter not in ['', none] }}"
then:
- action: counter.increment
target: { entity_id: !input calibration_counter }
mode: single
Happy to answer questions about adapting this to other plugs or batteries. Let me know if you try it out!
