πŸš— EV Battery Monitoring Without OBD2: HASS GPS, Weather and a Dash of Physics

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:

  1. Distance traveled (GPS person tracking)
  2. Temperature at the car’s location (weather API)
  3. 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') }}&current=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:

  1. Network trackers (Android Auto, car’s Wi-Fi) β€” most reliable when they work
  2. Trip status (input_boolean.trip_active) β€” manual or automated
  3. 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:

  • :white_check_mark: Companion App on their phone (for presence detection)
  • :white_check_mark: Person entity configured (for automations)
  • :white_check_mark: 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

  1. Drive on a cold day (-5Β°C to -10Β°C), note full-charge range
  2. Drive on a warm day (+20Β°C to +25Β°C), note full-charge range
  3. Create your S-curve between these anchor points
  4. 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_reason attribute 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

1 Like

I do not have other comments except for a couple:

  • Good job on the project
  • Good write up also
  • This is refreshing :+1: - you rarely see people doing ASCII for graphs to convey concepts these days. And I like this a lot

Also, and, another comments or takeaways: reading this gets me thinking. We tend go out and try to buy a gadget/hardware to address a problem we have. However, and evidently, it’s entirely possible that you really can use what you likely already have and cobble something together, and get a good enough results!!! Yes estimates are not accurate, but so what? Good enough to be useful, is pretty good already.

Interesting. And I get the point - a lot of it is pretty straightforward.

For me the biggest issue is how the person uses the car. Distance traveled Γ— consumption per km = energy used will vary quite a bit depending if a person is leadfoot or lightfoot! Secondly, while temperature definitely affects consumption, there is a second consideration - how much power the driver uses while driving. AC use on the car as we all know will deduct quite a few miles from any calculation, and while knowing the local temperature can give you a good guess as to how that affects AC use it’s going to vary - quite a bit - from person to person. And additional usage - sound systems, radio, cd players, phone charging - add to that.
Does your consumption equation factor in β€œaverage” A/C use based on temperature?

Other than that, seems like an interesting project, look forward to hearing more about it.

Thank you for kinds words. And I totally agree, as you might have noticed that I tried to β€œhave a new gadget” in the car and do stuff hard way. :slight_smile: And who does not like a bit of ascii drawings. :slight_smile:

Actually I tried this project β€œgadget way” about 4 years ago with ODB + android phone permanently in the car, but it did not work well as can bus of the car shutdown when power has been off for few minutes.

This is totally true, one truly needs to know the consumption of the car if any of this will work. And, this only works at the home-destination-home journeys, which in my case are 95% of the travelling.

But why in my humble opinion only observed average consumption is needed on different weather on this system?

I do know my A/C/ heatpump usage as it is included the observed consumption at the summer and winter. When hot, A/C is used and when cold -20ΒΊC the heatpump is screaming and consumption per km is high, and when it is +12C something in the between.

1 Like

Update: Companion Post Published

I’ve just published a detailed guide on building a robust car presence sensor that complements the GPS-based battery monitoring in this post:

:red_car: The β€œIs My Car Actually Home?” Problem: Layering Device Trackers, Frigate & Person Tracking

Why this matters for battery tracking:

The original GPS-based km tracking here needed reliable trip start/end detection. Single-source presence detection (WiFi, Frigate, or person tracking alone) caused:

  • False trip starts when walking to store
  • Missed trips when device tracker ghosted
  • Wrong driver attribution

The solution: Layer three unreliable sources with priority logic + 60s delays = zero false positives.

If you’re implementing the battery SoC estimation from this post, I highly recommend combining it with the robust presence sensor. They work perfectly together! :battery::zap: