TL;DR:
No OBD2 or car GPS? No problem. Track distance via person GPS, temperature from location, and math the rest. Hereβs how I rebuilt EV battery monitoring for my 2019 Hyundai Ioniq Electric using only external data sources β and if youβre already using the Home Assistant Companion App for presence detection, you get GPS tracking essentially for free.
The Problem
I wanted my EV stats in Home Assistant β battery percentage, remaining range, consumption patterns. The usual path is OBD2 with a dongle or a cloud API. Neither worked for me:
- OBD2 on the Ioniq turned out to be suboptimal for several reasons β I tried it, but wasnβt satisfied with the complexity and somewhat unpredictable behavior
- I wanted something that would work offline, forever, and could use to smart charge car on cheap electricity prices
So the question became: How do you estimate battery SoC when the car wonβt tell you?
The Core Insight
Electric vehicles are surprisingly predictable:
Distance traveled Γ consumption per km = energy used
Energy used Γ· battery capacity = state of charge
The tricky part? Consumption changes with temperature. At -10Β°C my Ioniq gets about 150 km of range. At +25Β°C? Closer to 215 km. Thatβs a 43% swing.
If I could track three things accurately enough, I could estimate SoC:
- Distance traveled (GPS person tracking)
- Temperature at the carβs location (weather API)
- Real-world consumption pattern (my own calibration data)
Building Blocks
1. GPS-Based Odometer (Using Existing Infrastructure!)
Hereβs the beautiful part: if youβre already running the Home Assistant Companion App on your phone for presence detection, you already have GPS tracking. No additional hardware, no extra setup needed.
The Companion App reports your phoneβs GPS coordinates to Home Assistant automatically. When a βtripβ is active, I simply accumulate the distance between coordinate updates:
# Simplified: actual implementation has filtering for noise
variables:
from_lat: "{{ trigger.from_state.attributes.latitude }}"
from_lon: "{{ trigger.from_state.attributes.longitude }}"
to_lat: "{{ trigger.to_state.attributes.latitude }}"
to_lon: "{{ trigger.to_state.attributes.longitude }}"
dist_km: "{{ distance(from_lat, from_lon, to_lat, to_lon) }}"
The magic: Youβre probably already tracking your phoneβs location for other automations (presence detection, zone triggers, etc.). This solution piggybacks on that existing data β no new apps, no extra battery drain, no dedicated GPS devices. It comes essentially for free.
Reality check: GPS tracking works well enough for distance. Itβs not millimeter-perfect, but over a full trip the errors average out.
2. Location-Based Temperature
Most EV range calculators use your home weather. Thatβs useless if youβre driving 50 km away where itβs 5Β°C colder. I use Open-Meteoβs free API to fetch temperature at the carβs last known GPS coordinates:
rest:
- resource_template: >-
https://api.open-meteo.com/v1/forecast?latitude={{ states('input_text.car_last_latitude') }}&longitude={{ states('input_text.car_last_longitude') }}¤t=temperature_2m,relative_humidity_2m,weather_code&timezone=auto
scan_interval: 900
timeout: 30
sensor:
- name: "Car Location Temperature"
unique_id: car_location_temperature
value_template: "{{ value_json.current.temperature_2m | float(10) }}"
unit_of_measurement: "Β°C"
device_class: temperature
Reality check: It assumes the weather at the carβs coordinates is the weather the car experiences. Good enough for driving, not for a parked car in a heated garage vs. outside.
3. Personal Consumption S-Curve
I threw out generic consumption figures and built my own model from real-world observations:
- -10Β°C: 150 km range (measured on cold winter days)
- +25Β°C: 215 km range (measured on warm summer days)
Then I fitted these anchor points to an S-curve that matches the physics of EV range loss:
Temperature (Β°C) β Expected Range (km) β Consumption (kWh/100km)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-30 112 24.1
-10 150 18.0 β anchor
0 186 14.5
10 209 12.9
20 216 12.5
25 215 12.6 β anchor
Hereβs the curve visualized in ASCII (temperature vs range):
Range
(km)
220 β€ βββββββ
200 β€ ββββββ―
180 β€ ββββ―
160 β€ ββββ―
140 β€ ββββ―
120 β€ ββββ―
100 β€ββββ―
80 β€
βββββββ¬ββββββ¬ββββββ¬ββββββ¬ββββββ¬βββββ Temp (Β°C)
-30 -15 0 15 30
Reality check: A personal S-curve beats generic lab numbers every time. This is calibrated to my driving style, my routes, my climate.
4. Intelligent βAt Homeβ Detection
Knowing if the car is home matters β I use accurate local weather when home, API weather when away. But network presence isnβt reliable (Wi-Fi sleeps, timers, etc.), so I built a three-layer fallback:
- Network trackers (Android Auto, carβs Wi-Fi) β most reliable when they work
- Trip status (
input_boolean.trip_active) β manual or automated - GPS distance from home β if trip is ON but car is <100m from home, itβs probably just sitting in the driveway
state: >-
{% if android_tracker_home or car_tracker_home %}
true
{% elif not trip_active %}
true
{% elif distance_from_home_m < 100 %}
true
{% else %}
false
{% endif %}
Reality check: Layered logic prevents stupid decisions. If Wi-Fi disappears, the system doesnβt immediately think the car teleported to Iceland.
Why This Approach Makes Sense
Zero Additional Hardware Required
Most Home Assistant users already have:
Companion App on their phone (for presence detection)
Person entity configured (for automations)
GPS location tracking enabled (for zone triggers)
This solution reuses existing infrastructure. Youβre not buying OBD2 dongles, dedicated GPS trackers, or subscribing to cloud services. Youβre just cleverly leveraging data youβre already collecting.
The βFree Lunchβ Economics
OBD2 dongle: β¬50-150 + installation hassle
Cloud API: β¬150/year subscription
GPS tracker: β¬30-100 + monthly fees
βββββββββββββββββββββββββββββββββββββββββββββββββ
This solution: β¬0 (uses existing Companion App)
If youβre already running Home Assistant with the Companion App for presence detection, the marginal cost of this EV monitoring system is literally zero.
Implementation Deep Dive
Distance Accumulation: The Core Logic
The GPS-based odometer is the heart of this system. Hereβs the full automation that tracks distance when someone is driving:
automation:
- alias: "EV - Accumulate Distance (Driver)"
description: "Track actual driving distance using person GPS coordinates"
triggers:
- platform: state
entity_id: person.driver_name
conditions:
# Only accumulate when trip is active
- condition: state
entity_id: input_boolean.car_trip_active
state: "on"
# Filter out GPS noise and teleportation
- condition: template
value_template: |
{{ dt >= 15 and dt < 5400 and dist_km > 0.05 and speed_kmh > 25 and speed_kmh < 200 }}
actions:
# Update total kilometers today
- service: input_number.set_value
target:
entity_id: input_number.car_km_today
data:
value: >-
{{ (states('input_number.car_km_today')|float(0) + dist_km)|round(3) }}
# Update kilometers since full charge
- service: input_number.set_value
target:
entity_id: input_number.car_km_since_full
data:
value: >-
{{ (states('input_number.car_km_since_full')|float(0) + dist_km)|round(1) }}
# Save current coordinates for weather lookup
- service: input_text.set_value
target:
entity_id: input_text.car_last_latitude
data:
value: "{{ to_lat }}"
- service: input_text.set_value
target:
entity_id: input_text.car_last_longitude
data:
value: "{{ to_lon }}"
- service: input_text.set_value
target:
entity_id: input_text.car_last_location_time
data:
value: "{{ now().isoformat() }}"
mode: queued
variables:
# GPS coordinates from person entity
from_lat: "{{ trigger.from_state.attributes.latitude | float(0) }}"
from_lon: "{{ trigger.from_state.attributes.longitude | float(0) }}"
to_lat: "{{ trigger.to_state.attributes.latitude | float(0) }}"
to_lon: "{{ trigger.to_state.attributes.longitude | float(0) }}"
# Calculate distance in kilometers
dist_km: "{{ distance(from_lat, from_lon, to_lat, to_lon) }}"
# Time between updates (seconds)
dt: >-
{{ (as_timestamp(trigger.to_state.last_updated) -
as_timestamp(trigger.from_state.last_updated)) | float(0) }}
# Speed in km/h (sanity check against teleportation)
speed_kmh: "{{ (dist_km / dt) * 3600 if dt > 0 else 0 }}"
Key filtering logic:
dt >= 15β ignore updates faster than 15 seconds (GPS jitter)dt < 5400β ignore gaps longer than 90 minutes (phone was off)dist_km > 0.05β ignore movement less than 50 meters (GPS drift)speed_kmh > 25β ignore slow walking speeds (carrying phone without car)speed_kmh < 200β reject unrealistic highway speeds and GPS glitches
Pro tip: If you have multiple drivers, create a copy of this automation for each person entity and use input_select.car_driver to determine which automation should be active.
Charging Detection & Auto-Reset
When the car reaches 100% charge, automatically reset the βkm since fullβ counter:
automation:
- alias: "EV - Reset Counter on Full Charge"
description: "Reset distance counter when car is fully charged"
triggers:
# Detect full charge from your charger integration
- platform: numeric_state
entity_id: sensor.car_charger_energy_session
above: 26 # 27 kWh battery, slightly under to catch near-full
# OR detect when charging stops and battery is full
- platform: state
entity_id: binary_sensor.car_charging
to: "off"
conditions:
- condition: template
value_template: >-
{{ states('input_number.car_km_since_full')|float(0) > 5 }}
actions:
- service: input_number.set_value
target:
entity_id: input_number.car_km_since_full
data:
value: 0
- service: input_datetime.set_datetime
target:
entity_id: input_datetime.car_last_full_charge
data:
datetime: "{{ now() }}"
- service: logbook.log
data:
name: "EV Charging"
message: "Full charge detected. Reset distance counter."
Smart Charging Integration
This SoC data becomes really powerful when combined with dynamic electricity pricing:
automation:
- alias: "EV - Smart Charge When Needed"
description: "Start charging only when SoC is low AND price is cheap"
triggers:
- platform: time_pattern
hours: "*"
conditions:
# Only charge when SoC is below threshold
- condition: numeric_state
entity_id: sensor.ev_battery_soc_estimate
below: 30
# Only charge during cheap hours
- condition: numeric_state
entity_id: sensor.nordpool_kwh_fi_eur_3_10_024
below: 0.05 # β¬0.05/kWh threshold
# Car is home and plugged in
- condition: state
entity_id: binary_sensor.car_home
state: "on"
actions:
- service: switch.turn_on
target:
entity_id: switch.car_charger
Reality check: Without accurate SoC estimates, you either:
- Over-charge (waste money on expensive hours)
- Under-charge (run out of battery unexpectedly)
This system gives you the confidence to charge exactly when you need to at the lowest possible price.
Putting It All Together
The Flow
Person moves β GPS updates β Distance accumulates
β
Car coordinates saved
β
Open-Meteo fetches temperature at location
β
S-curve calculates consumption
β
Energy used since last full charge
β
SoC estimate + range
Template Sensor (Simplified)
sensor:
- name: "EV Battery SoC Estimate"
state: >-
{% set battery_kwh = 27 %}
{% set km_since_full = states('input_number.km_since_full')|float %}
{% set consumption = states('input_number.consumption_kwh_100km')|float %}
{% set energy_used = (km_since_full * consumption / 100) %}
{% set soc = 100 - (energy_used / battery_kwh * 100) %}
{{ [[soc, 100]|min, 0]|max | round(1) }}
Automation: Update Consumption Based on Temperature
automation:
- alias: "Update EV Consumption Based on Temperature"
trigger:
- platform: state
entity_id: sensor.car_location_temperature
- platform: time_pattern
minutes: "/15"
action:
- variables:
temp: "{{ states('sensor.car_location_temperature')|float }}"
battery_kwh: 27
expected_range: >-
{% if temp <= -10 %}
150
{% elif temp <= 0 %}
186
{% elif temp <= 10 %}
209
{% elif temp <= 20 %}
216
{% else %}
215
{% endif %}
calculated_consumption: "{{ (battery_kwh * 100 / expected_range)|round(2) }}"
- service: input_number.set_value
target:
entity_id: input_number.consumption_kwh_100km
data:
value: "{{ calculated_consumption }}"
What Works Well
- GPS tracking works well enough for distance β not perfect, but good enough over a full trip
- Location-based temperature keeps estimates honest β especially in winter where home vs. destination can differ by 10Β°C
- A personal S-curve is more useful than generic lab numbers β tailored to actual driving patterns
- Layered βhome detectionβ avoids bad source choices β graceful degradation when one data source fails
- Reuses existing infrastructure β if you have Companion App, you have everything you need
Where It Cheats (On Purpose)
- Assumes whoβs driving and when a trip is βonβ β requires manual trip toggle or automated presence detection
- Assumes weather at coordinates matches the carβs experience β not true for underground parking or heated garages
- Assumes past behavior predicts future consumption β doesnβt account for sudden aggressive driving or traffic jams
Real-World Results
After calibration and debugging long enough:
- SoC estimates accurate within ~7% of actual values when verified
- Temperature compensation working across -25Β°C to +25Β°C range
- Reliable operation in Finnish winter (where this really matters)
Common Pitfalls & Solutions
Problem: GPS jumps around when stationary
Solution: The speed filter (25 < speed_kmh < 200) filters out both stationary GPS drift and impossible teleportation speeds.
Problem: Multiple phones in the car
Solution: Use input_select.car_driver to switch which person entity is actively tracked. Only one automation runs at a time.
Problem: Forgetting to activate trip mode
Solution: Add a zone automation that enables trip mode when you leave home:
automation:
- alias: "EV - Auto-enable Trip Mode"
triggers:
- platform: zone
entity_id: person.driver
zone: zone.home
event: leave
actions:
- service: input_boolean.turn_on
target:
entity_id: input_boolean.car_trip_active
Problem: Battery degradation over time
Solution: Adjust input_number.car_battery_capacity_kwh as your battery ages. My 2019 Ioniq started at 28 kWh, now I use 27 kWh after 100,000 km.
Performance Notes
- GPS updates: Every 15-60 seconds when moving (Home Assistant Companion App default)
- Weather API calls: Every 15 minutes (scan_interval: 900), stays well under free tier limits
- Consumption updates: Every 15 minutes or when temperature changes
- System overhead: Negligible β a few template sensors and lightweight automations
- Battery impact: None β youβre already tracking GPS for presence detection
Try It Yourself
Prerequisites
- Home Assistant with person tracking (Companion App GPS) β you probably already have this!
- Open-Meteo weather integration (free, no API key needed)
- Basic YAML configuration skills
Calibration Steps
- Drive on a cold day (-5Β°C to -10Β°C), note full-charge range
- Drive on a warm day (+20Β°C to +25Β°C), note full-charge range
- Create your S-curve between these anchor points
- Fine-tune over a few weeks based on actual vs. estimated SoC
Required Input Helpers
input_number:
car_battery_capacity_kwh:
name: "Battery Capacity"
min: 20
max: 40
step: 0.1
unit_of_measurement: "kWh"
car_consumption_kwh_100km:
name: "Current Consumption"
min: 10
max: 35
step: 0.01
unit_of_measurement: "kWh/100km"
car_km_since_full:
name: "Kilometers Since Full Charge"
min: 0
max: 300
step: 0.1
unit_of_measurement: "km"
input_text:
car_last_latitude:
name: "Car Last Latitude"
car_last_longitude:
name: "Car Last Longitude"
input_boolean:
car_trip_active:
name: "Car Trip Active"
Lessons Learned
What Iβd Do Differently
- Start with a simpler consumption model and refine gradually
- Log actual consumption data points for the curve
- Add automated trip mode activation earlier in the development
Unexpected Benefits
- No ongoing costs or subscriptions
- Works completely offline (after initial weather API setup)
- Easy to adapt to different vehicles or driving styles
- Teaches you about your actual driving patterns
- Enables smart charging strategies with dynamic pricing
- Reuses infrastructure you already have β no new hardware needed!
Related: Robust Car Presence Detection
Update (Oct 2025): Iβve published a companion guide on building a robust βcar at homeβ sensor that combines device trackers, Frigate NVR, and person tracking. This solves the trip start/end detection reliability issues mentioned in this post.
β Read: The βIs My Car Actually Home?β Problem
Key improvements:
- Zero false trip starts from walking/public transport
- 60-second delay prevents person tracking false positives
- Layered priority logic (device tracker β Frigate β person heuristics)
decision_reasonattribute for debugging
Questions? Suggestions? Tell me what youβd improve or how youβd adapt this to your system!
Tags: #electric-vehicle #ev #battery-monitoring #gps-tracking #temperature-compensation #no-obd2 #diy #share-your-projects companion-app