How State Change Logging Is Implemented in TeslaMate: Elixir Source Code Analysis

TeslaMate tracks vehicle online/offline/asleep transitions by persisting each status as a State record in PostgreSQL, where TeslaMate.Log.start_state/2 atomically closes the previous state and opens a new one whenever the vehicle process detects a status change.

State change logging is a core feature of the teslamate-org/teslamate repository that enables precise tracking of when Tesla vehicles are online, offline, or asleep. The implementation combines Ecto schemas with strict database constraints and a finite state machine to ensure every transition is captured with microsecond accuracy.

The State Schema and Database Constraints

Every vehicle state is modeled by the TeslaMate.Log.State schema defined in lib/teslamate/log/state.ex. This Ecto schema maps to the states table and defines three critical fields:

  • state – An atom representing the vehicle status (:online, :offline, or :asleep)
  • start_date – A DateTime UTC timestamp marking when the state began
  • end_date – A nullable timestamp indicating when the state concluded (NULL for the currently active state)

The database enforces data integrity through two specific constraints. The states_car_id__end_date_IS_NULL_index unique index guarantees only one open state per vehicle by preventing duplicate NULL end_date values for the same car_id. Additionally, the positive_duration constraint ensures that end_date must always be greater than start_date, eliminating zero-length or negative-duration states.

Core Logging Logic in TeslaMate.Log

The lib/teslamate/log.ex module serves as the primary API for state persistence, exposing start_state/2 for transitions and complete_current_state/1 for graceful shutdowns.

Starting a New State with start_state/2

Located at lines 58-78 in lib/teslamate/log.ex, the start_state/2 function handles the atomic handoff between vehicle states:

def start_state(%Car{} = car, state, opts \\ []) when not is_nil(state) do
  now = Keyword.get(opts, :date) || DateTime.utc_now()

  case get_current_state(car) do
    %State{state: ^state} = s -> {:ok, s}
    %State{} = s ->
      Repo.transaction(fn ->
        with {:ok, _} <- s |> State.changeset(%{end_date: now}) |> Repo.update(),
             {:ok, new_state} <- create_state(car, %{state: state, start_date: now}) do
          new_state
        else
          {:error, reason} -> Repo.rollback(reason)
        end
      end)
    nil -> create_state(car, %{state: state, start_date: now})
  end
end

The function follows a three-step logic:

  1. Query the current state using get_current_state/1.
  2. If the requested state matches the active one, return the existing record to avoid duplicate entries.
  3. If the state differs, execute a database transaction that updates the existing state's end_date to the current timestamp and inserts a new State row with the updated status and start_date.

Completing States on Shutdown with complete_current_state/1

When the TeslaMate application shuts down or a vehicle process terminates, complete_current_state/1 (lines 103-128 in lib/teslamate/log.ex) ensures no states remain open indefinitely:

def complete_current_state(%Car{id: id} = car) do
  case get_current_state(car) do
    %State{start_date: date} = state ->
      query = from s in State, where: s.car_id == ^id and s.start_date > ^date,
                order_by: [asc: s.start_date], limit: 1
      end_date = case Repo.one(query) do
        %State{start_date: d} -> d
        nil -> DateTime.add(date, 1, :second)
      end
      state |> State.changeset(%{end_date: end_date}) |> Repo.update()
    nil -> :ok
  end
end

This function searches for the next chronological state to use as the end_date. If no subsequent state exists, it defaults to one second after the start date, ensuring the positive_duration constraint is satisfied.

Integration with the Vehicle State Machine

The vehicle process in lib/teslamate/vehicles/vehicle.ex orchestrates state changes through its finite state machine (FSM). When the vehicle transitions between statuses, the process invokes Log.start_state/2 and caches the timestamp in its internal Data struct as last_state_change.

When a vehicle goes offline, the FSM executes:

def handle_event(:info, {:ok, %Vehicle{state: "offline"}} = result, state, data) do
  {:ok, %Log.State{start_date: last_state_change}} =
    call(data.deps.log, :start_state, [data.car, :offline, date_opts(vehicle)])

  {:next_state, {:offline, asleep_interval()}, %{data | last_state_change: last_state_change}, …}
end

Conversely, when returning online:

def handle_event(:internal, {:update, {:online, vehicle}} = evt, :start, data) do
  {:ok, %Log.State{start_date: last_state_change}} =
    call(data.deps.log, :start_state, [car, :online, date_opts(vehicle)])
  # … continue processing

end

The last_state_change field stored in the Data struct drives dashboard summaries and determines when the logger should suspend polling to conserve API rate limits.

Summary

  • Database integrity is enforced by unique indexes and check constraints in lib/teslamate/log/state.ex that prevent overlapping states and negative durations.
  • Atomic transitions are handled by start_state/2 in lib/teslamate/log.ex, which wraps state closure and creation in a database transaction.
  • Graceful shutdowns use complete_current_state/1 to finalize any open states by inferring the end date from subsequent records or defaulting to a one-second duration.
  • FSM integration in lib/teslamate/vehicles/vehicle.ex triggers logging on every status change and maintains the last_state_change timestamp for operational decisions.

Frequently Asked Questions

How does TeslaMate prevent duplicate state entries?

TeslaMate prevents duplicates through the idempotency logic in start_state/2. Before creating a new record, the function compares the requested state against the current active state returned by get_current_state/1. If they match, the existing record is returned unchanged; only mismatched states trigger a database update and insertion.

What happens if a vehicle changes states rapidly?

Rapid state changes are handled safely due to the database transaction wrapping the close-and-create operations in start_state/2. The transaction ensures that the previous state's end_date and the new state's start_date are committed atomically, preventing race conditions or partial writes that could violate the positive_duration constraint.

How does state change logging support TeslaMate's analytics?

The states table provides the temporal boundaries necessary for calculating charging efficiency, drive durations, and energy consumption per trip. By knowing exactly when a vehicle was online versus asleep, TeslaMate can correlate high-frequency telemetry data with specific operational periods and exclude offline intervals from efficiency calculations.

Where is the source of truth for the current vehicle state?

The database serves as the ultimate source of truth via the get_current_state/1 function, which queries for the single row where end_date IS NULL (enforced by the states_car_id__end_date_IS_NULL_index). The vehicle process maintains a local cache in the last_state_change field of its Data struct for performance, but this is periodically reconciled with the database.

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 →