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

> Discover how TeslaMate implements state change logging. Learn about the `State` record in PostgreSQL and the `start_state/2` function for tracking vehicle status transitions.

- Repository: [TeslaMate/teslamate](https://github.com/teslamate-org/teslamate)
- Tags: internals
- Published: 2026-06-16

---

**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`](https://github.com/teslamate-org/teslamate/blob/main/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`](https://github.com/teslamate-org/teslamate/blob/main/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`](https://github.com/teslamate-org/teslamate/blob/main/lib/teslamate/log.ex), the `start_state/2` function handles the atomic handoff between vehicle states:

```elixir
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`](https://github.com/teslamate-org/teslamate/blob/main/lib/teslamate/log.ex)) ensures no states remain open indefinitely:

```elixir
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`](https://github.com/teslamate-org/teslamate/blob/main/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:

```elixir
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:

```elixir
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`](https://github.com/teslamate-org/teslamate/blob/main/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`](https://github.com/teslamate-org/teslamate/blob/main/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`](https://github.com/teslamate-org/teslamate/blob/main/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.