How TeslaMate Charge Cost Tracking Calculates Energy Expenses Per Session

TeslaMate calculates energy expenses per charging session by executing a SQL UPDATE that combines geofence pricing settings (session fees and per-unit rates) with actual consumption metrics, supporting both per-kWh and per-minute billing models.

Charge cost tracking in the teslamate-org/teslamate repository turns raw charging telemetry into precise monetary values using a server-side SQL calculation. The Elixir-based application persists pricing rules within geofence configurations and applies them to charging sessions through an efficient batch update mechanism. Understanding how TeslaMate calculates these costs per session requires examining the interplay between schema definitions, pricing configurations, and the SQL logic that unifies them.

The Three-Component Architecture

Geofence Pricing Schema

Located in [lib/teslamate/locations/geo_fence.ex](https://github.com/teslamate-org/teslamate/blob/main/lib/teslamate/locations/geo_fence.ex), the GeoFence schema stores the monetary parameters that drive expense calculations. Each geofence record contains session_fee (a flat charge per session), cost_per_unit (the variable rate), and billing_type (a string of either "per_kwh" or "per_minute").

Charging Process Schema

The ChargingProcess schema in [lib/teslamate/log/charging_process.ex](https://github.com/teslamate-org/teslamate/blob/main/lib/teslamate/log/charging_process.ex) defines the target field: cost (a :decimal type). This schema also tracks the input metrics charge_energy_used, charge_energy_added, and duration_min, which serve as the variables for the expense formula.

SQL Calculation Logic

The actual computation occurs in [lib/teslamate/locations.ex](https://github.com/teslamate-org/teslamate/blob/main/lib/teslamate/locations.ex#L31-L49) within the calculate_charge_costs/1 function. Instead of iterating through records in Elixir, this function issues a single SQL UPDATE statement that computes costs for all unprocessed sessions belonging to a specific geofence.

How Energy Expenses Are Computed

The SQL UPDATE employs a CASE expression to handle three pricing scenarios:

  1. Unconfigured pricing: When both session_fee and cost_per_unit are NULL, the cost remains NULL.

  2. Per-kWh billing: For billing_type = 'per_kwh', the calculation uses GREATEST(charge_energy_used, charge_energy_added) multiplied by the per-unit rate, then adds any flat session fee.

  3. Per-minute billing: For billing_type = 'per_minute', the system multiplies duration_min by the per-unit rate and adds the session fee.

The underlying SQL structure resembles:

CASE
  WHEN g.session_fee IS NULL AND g.cost_per_unit IS NULL THEN NULL
  WHEN g.billing_type = 'per_kwh' THEN
    COALESCE(g.session_fee, 0) +
    COALESCE(g.cost_per_unit * GREATEST(c.charge_energy_used,
                                        c.charge_energy_added), 0)
  WHEN g.billing_type = 'per_minute' THEN
    COALESCE(g.session_fee, 0) +
    COALESCE(g.cost_per_unit * c.duration_min, 0)
END

This approach ensures that energy expenses per session reflect the higher of the two energy readings (useful when the vehicle's internal meter resets mid-charge) and safely handles NULL values via COALESCE.

Practical Implementation Examples

To programmatically trigger the cost calculation for a specific geofence:

alias TeslaMate.Locations

# Load a geofence with configured pricing

geofence = Locations.get_geofence!(42)

# Execute the batch cost calculation

:ok = Locations.calculate_charge_costs(geofence)

Inspecting the calculated result:

alias TeslaMate.Log.ChargingProcess
alias TeslaMate.Repo

process = Repo.get!(ChargingProcess, 101)

# The calculated expense is stored as a Decimal

process.cost

# => #Decimal<12.40>

The SQL UPDATE executed atomically processes all pending rows:


# Conceptual representation of the Ecto query executed:

"""
UPDATE charging_processes AS cp
SET cost = (
  SELECT CASE
    WHEN g.session_fee IS NULL AND g.cost_per_unit IS NULL THEN NULL
    WHEN g.billing_type = 'per_kwh' THEN
      COALESCE(g.session_fee, 0) +
      COALESCE(g.cost_per_unit * GREATEST(c.charge_energy_used,
                                          c.charge_energy_added), 0)
    WHEN g.billing_type = 'per_minute' THEN
      COALESCE(g.session_fee, 0) +
      COALESCE(g.cost_per_unit * c.duration_min, 0)
  END
  FROM charging_processes c
  JOIN geofences g ON g.id = c.geofence_id
  WHERE c.id = cp.id
)
WHERE cp.geofence_id = $1 AND cp.cost IS NULL;
"""

Summary

  • Geofence configuration determines pricing via session_fee, cost_per_unit, and billing_type fields in the GeoFence schema.
  • Session storage occurs in the ChargingProcess schema, which defines the cost column along with energy and duration metrics.
  • Batch calculation happens in TeslaMate.Locations.calculate_charge_costs/1, which runs a SQL UPDATE to populate the cost field for all sessions lacking a value.
  • Energy safety is ensured by using GREATEST(charge_energy_used, charge_energy_added) to capture the maximum energy consumed.
  • Mixed pricing is supported by adding flat fees to variable rates through COALESCE wrapping.

Frequently Asked Questions

How does TeslaMate handle energy data when the vehicle meter resets during charging?

The SQL calculation uses GREATEST(charge_energy_used, charge_energy_added) to compare the two metrics and select the larger value. This ensures that even if the vehicle's internal meter resets mid-session, the calculation accounts for the total energy transferred.

Can I configure both a flat session fee and a per-kWh rate for the same location?

Yes. When both values are present in the geofence configuration, the COALESCE(g.session_fee, 0) expression adds the flat fee to the result of cost_per_unit multiplied by the energy or duration. This allows hybrid pricing models common at public charging stations.

Why do some charging sessions show NULL for cost instead of zero?

If a geofence lacks both a session_fee and cost_per_unit, or if the session occurred at a location without an associated geofence, the CASE statement returns NULL. This indicates that pricing was never configured for that session rather than a free charge. You can manually enter costs through the live view defined in [lib/teslamate_web/live/charge_live/cost.ex](https://github.com/teslamate-org/teslamate/blob/main/lib/teslamate_web/live/charge_live/cost.ex).

Does updating a geofence's pricing retroactively change historical session costs?

No. The calculate_charge_costs/1 function filters with WHERE cp.cost IS NULL, meaning it only calculates expenses for sessions that haven't been priced yet. Existing cost values remain static to preserve historical accuracy. To recalculate old sessions, you would need to NULLify the cost column for those specific records and re-trigger the function.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →