Skip to content

Vehicle Data & VHAL

Electric vehicles and charging

The property set that only exists on an EV — battery level, charge state, port status, range — and why an estimate the driver does not trust is worse than no estimate at all.

Intermediate7 minEV · Charging · Properties

An electric vehicle exposes a set of properties a combustion car simply does not have. If you have only worked on petrol programmes, this is a whole vocabulary you have not met.

It is also the area where getting the presentation wrong causes the most customer complaints, because the numbers involved are estimates that people treat as facts.

Why EV data is different#

The core properties#

PropertyWhat it holds
EV_BATTERY_LEVELCurrent energy in the battery (Wh)
INFO_EV_BATTERY_CAPACITYNominal usable capacity (Wh) — static
RANGE_REMAININGEstimated distance still available
EV_CHARGE_STATENot charging, charging, completed, error
EV_CHARGE_PORT_OPENIs the flap open
EV_CHARGE_PORT_CONNECTEDIs a cable plugged in
EV_BATTERY_INSTANTANEOUS_CHARGE_RATECurrent power flow (positive in, negative out)
EV_CHARGE_TIME_REMAININGEstimated seconds to the target
EV_CHARGE_PERCENT_LIMITThe charge ceiling the driver set
EV_CHARGE_SWITCHStart or stop charging
EV_REGENERATIVE_BRAKING_STATECurrent regen setting
INFO_EV_CONNECTOR_TYPEWhich plug standards this car accepts — static
INFO_EV_PORT_LOCATIONWhere the port physically is — static

State of charge is not a percentage property#

There is no single "percent full" property. You compute it:

Deriving percentage from energy
val nowWh = properties.getFloatProperty(
    VehiclePropertyIds.EV_BATTERY_LEVEL, GLOBAL,
)
val capacityWh = properties.getFloatProperty(
    VehiclePropertyIds.INFO_EV_BATTERY_CAPACITY, GLOBAL,
)
 
val percent = ((nowWh / capacityWh) * 100f).coerceIn(0f, 100f)

Charging state, and the pair of properties people confuse#

Two different questions
val portOpen = properties.getBooleanProperty(
    VehiclePropertyIds.EV_CHARGE_PORT_OPEN, GLOBAL,
)
val cableIn = properties.getBooleanProperty(
    VehiclePropertyIds.EV_CHARGE_PORT_CONNECTED, GLOBAL,
)

Charge rate carries sign, and the sign is meaningful:

Direction matters
val rateW = properties.getFloatProperty(
    VehiclePropertyIds.EV_BATTERY_INSTANTANEOUS_CHARGE_RATE, GLOBAL,
)
 
when {
    rateW > 0 -> showCharging(rateW)                  // energy going in
    rateW < 0 -> showDischarging(-rateW)              // driving, or V2L
    else      -> showIdle()
}

A negative rate while stationary means the car is powering something — vehicle-to-load, running the cabin, or preconditioning.

Presenting estimates honestly#

This is where the engineering meets the complaint from the top of the page.

Degrade honestly when the estimate is unreliable
val range = properties.getProperty(
    Float::class.java, VehiclePropertyIds.RANGE_REMAINING, GLOBAL,
)
 
when (range?.status) {
    CarPropertyValue.STATUS_AVAILABLE   -> showRange(range.value)
    CarPropertyValue.STATUS_UNAVAILABLE -> showCalculating()   // not a zero
    else                                -> showPercentOnly()
}

Showing 0 km because the property was unavailable is the same silent-failure pattern from the VHAL topics, and here it makes a driver stop at a charger they did not need.

Charging is a long-running background activity#

A car charges for hours, usually unattended, with the head unit suspended.

Writing: limits and switches#

Two properties are writable, and both need care.

Setting a charge ceiling
properties.setFloatProperty(
    VehiclePropertyIds.EV_CHARGE_PERCENT_LIMIT, GLOBAL, 80f,
)

Many drivers cap at 80% to preserve battery life. The property may only accept specific values — check the config's supported values rather than assuming any percentage works.

Starting and stopping
properties.setBooleanProperty(
    VehiclePropertyIds.EV_CHARGE_SWITCH, GLOBAL, true,
)

Both need android.car.permission.CONTROL_CAR_ENERGY — signature-level, because they change vehicle behaviour.

Checking what a vehicle actually has#

Combustion vehicles do not have these properties at all, and even among EVs the set differs.

Feature detection, not assumption
val isElectric = properties.propertyList.any {
    it.propertyId == VehiclePropertyIds.EV_BATTERY_LEVEL
}
 
val canSetLimit = properties.propertyList.any {
    it.propertyId == VehiclePropertyIds.EV_CHARGE_PERCENT_LIMIT
}
On a device
adb shell dumpsys car_service --list-properties | grep -i EV_

Next#

Where the vehicle thinks it is, and what happens in a tunnel.

References & further reading

Code links target the main branch on cs.android.com. AOSP moves — if a path 404s, search the symbol instead.