How the TeslaMate Vehicles Supervisor Manages Multiple Tesla Vehicles Concurrently

The TeslaMate.Vehicles supervisor manages multiple Tesla vehicles concurrently by spawning an isolated GenStateMachine process for each car and coordinating them through a :one_for_one supervision strategy, enabling parallel operations via Task.async_stream while isolating failures to individual vehicles.

The open-source TeslaMate platform achieves fleet-scale vehicle monitoring by leveraging Elixir's actor-model concurrency. In the teslamate-org/teslamate repository, the Vehicles supervisor (TeslaMate.Vehicles) dynamically instantiates per-vehicle processes that operate independently, allowing the system to poll, stream, and log data from dozens of Teslas simultaneously without blocking or cross-contamination.

Architecture Overview

The supervision tree relies on two core components: a dynamic supervisor that manages the fleet, and individual state machines that encapsulate per-vehicle logic.

  • TeslaMate.Vehicles (lib/teslamate/vehicles.ex): A dynamic supervisor that starts child specifications for every registered car. It implements a :one_for_one restart strategy with limits of 5 restarts within 60 seconds.
  • TeslaMate.Vehicles.Vehicle (lib/teslamate/vehicles/vehicle.ex): A GenStateMachine that handles polling loops, Tesla streaming API connections, state transitions (online, asleep, charging), and fuse-based error handling.
  • TeslaMate.Vehicles.Vehicle.Summary (lib/teslamate/vehicles/vehicle/summary.ex): An immutable data structure representing the current state of a single vehicle, built from the GenStateMachine's internal %Data{} struct.

Supervisor Startup and Child Specification

When the application boots, TeslaMate.Application starts the supervisor via TeslaMate.Vehicles.start_link/1. The init/1 callback dynamically constructs child specs from the Tesla API or fallback cache.

def init(opts) do
  children =
    opts
    |> Keyword.get_lazy(:vehicles, &list_vehicles!/0)
    |> Enum.map(&{Keyword.get(opts, :vehicle, Vehicle), car: create_or_update!(&1)})
    |> Enum.uniq_by(fn {_mod, car: %Car{id: id}} -> id end)
    |> Enum.filter(fn {_mod, car: %Car{settings: settings}} -> settings.enabled end)

  Supervisor.init(children,
    strategy: :one_for_one,
    max_restarts: 5,
    max_seconds: 60
  )
end

The supervisor performs three critical filtering steps:

  1. Fetches vehicle data via list_vehicles!/0 from the Tesla API or historic logs.
  2. Deduplicates entries by car.id to prevent duplicate processes for the same vehicle.
  3. Filters by enabled status, ensuring only cars with settings.enabled == true receive dedicated processes.

The Vehicle GenStateMachine Process

Each child process is uniquely identified by the module name and car ID combination, ensuring distinct PIDs for concurrent execution.

def child_spec(arg) do
  %{
    id: :"#{__MODULE__}_#{Keyword.fetch!(arg, :car).id}",
    start: {__MODULE__, :start_link, [arg]}
  }
end

The GenStateMachine encapsulates:

  • Independent polling intervals: asleep_interval/0 and driving_interval/0 calculate sleep durations per state.
  • Per-vehicle streaming: connect_stream/1 establishes WebSocket connections to Tesla's streaming API, isolated to the process.
  • Fuse-based circuit breaking: Uses :fuse.ask/2 to prevent cascading failures when the Tesla API returns authentication errors or vehicle-not-found responses.

Because every vehicle operates in its own lightweight BEAM process, a charging Model 3 and a driving Model S maintain separate state machines without interference.

Parallel Fleet Operations

To aggregate status across the entire fleet without sequential blocking, the Vehicles.list/0 function leverages concurrent task spawning.

def list do
  Supervisor.which_children(@name)
  |> Task.async_stream(fn {_, pid, _, _} -> Vehicle.summary(pid) end,
       ordered: false,
       max_concurrency: 10,
       timeout: 5_000)
  |> Enum.map(fn {:ok, vehicle} -> vehicle end)
  |> Enum.sort_by(&{&1.car.display_priority, &1.car.id})
end

This implementation uses Task.async_stream/3 with:

  • max_concurrency: 10: Limits concurrent summary requests to prevent overwhelming the API or database.
  • ordered: false: Returns results as they complete, reducing tail latency.
  • timeout: 5_000: Fails individual requests after 5 seconds rather than blocking indefinitely.

Supervisor.which_children/1 retrieves the current PIDs of all active Vehicle processes, enabling truly parallel data retrieval across the fleet.

Fault Isolation and Recovery

The :one_for_one strategy ensures that a crash in one vehicle process—whether from a malformed API response or a streaming timeout—does not propagate to others. When a child terminates abnormally, the supervisor restarts only that specific vehicle process up to the configured restart limits.

For catastrophic failures (such as blown fuses from persistent API errors), the restart/0 function provides a hard reset mechanism:

def restart do
  with :ok <- Supervisor.stop(@name, :normal),
       :ok <- block_until_started(250) do
    :ok
  end
end

The block_until_started/1 helper polls Process.whereis/1 until the supervisor PID regenerates, ensuring the fleet reinitializes with fresh state before accepting new commands.

Practical Code Examples

Starting the Supervisor

In a typical deployment, the supervisor launches automatically via the application tree:


# In lib/teslamate/application.ex

children = [
  {TeslaMate.Vehicles, []},
  # ... other children ...

]

Supervisor.start_link(children, strategy: :one_for_one, name: TeslaMate.Supervisor)

Listing All Vehicles

Retrieve current summaries for the entire fleet:

iex> TeslaMate.Vehicles.list()
[
  %TeslaMate.Vehicles.Vehicle.Summary{
    car: %TeslaMate.Log.Car{id: 1, name: "Model 3"},
    state: :online,
    # ...

  },
  %TeslaMate.Vehicles.Vehicle.Summary{...}
]

Controlling Individual Vehicles

Programmatically suspend and resume data collection for specific cars:


# Suspend logging for car ID 1

iex> TeslaMate.Vehicles.suspend_logging(1)
:ok

# Resume logging

iex> TeslaMate.Vehicles.resume_logging(1)
:ok

Subscribing to Real-time Updates

LiveView components and Phoenix channels can subscribe to per-vehicle PubSub topics:


# Subscribe to summary updates for car ID 42

:ok = TeslaMate.Vehicles.subscribe_to_summary(42)

# In your handle_info callback:

def handle_info(%TeslaMate.Vehicles.Vehicle.Summary{} = summary, socket) do
  {:noreply, assign(socket, :summary, summary)}
end

Summary

  • Dynamic supervision: TeslaMate.Vehicles is a dynamic supervisor in lib/teslamate/vehicles.ex that spawns a GenStateMachine for each enabled vehicle at boot time.
  • Process isolation: Each Tesla runs in its own process with distinct supervision IDs (:"Elixir.TeslaMate.Vehicles.Vehicle_#{id}"), preventing cross-vehicle failures.
  • Concurrent aggregation: The list/0 function uses Task.async_stream/3 with max_concurrency: 10 to fetch summaries in parallel rather than sequentially.
  • Fault tolerance: The :one_for_one restart strategy with limits of 5 restarts per 60 seconds ensures transient API failures don't crash the entire supervisor.
  • State machine encapsulation: Per-vehicle logic including polling intervals, streaming connections, and fuse handling resides in TeslaMate.Vehicles.Vehicle.

Frequently Asked Questions

What happens if one vehicle process crashes?

The supervisor automatically restarts only the crashed child process due to the :one_for_one strategy defined in lib/teslamate/vehicles.ex. All other vehicle processes continue running uninterrupted. If the process exceeds 5 restarts within 60 seconds, the supervisor itself terminates, triggering the application to reload the vehicle list via the restart/0 function.

How does TeslaMate handle API rate limits across multiple vehicles?

Each Vehicle GenStateMachine manages its own polling schedule via independent timers (Process.send_after/3) calculated by asleep_interval/0 and driving_interval/0. The Task.async_stream in list/0 limits concurrent summary requests to 10 simultaneous calls, preventing thundering-herd scenarios when querying the Tesla API across large fleets.

Can TeslaMate scale to monitor dozens of vehicles simultaneously?

Yes. Because each vehicle runs as a lightweight BEAM process consuming minimal memory (typically kilobytes per process), the supervisor can theoretically manage hundreds of vehicles. The primary bottleneck becomes the Tesla API rate limits and database connection pooling, not the Elixir process overhead.

How do I programmatically control logging for specific vehicles?

Use the public API functions suspend_logging/1 and resume_logging/1, passing the car ID as an integer. These functions locate the specific Vehicle process by ID and send synchronous messages to pause or resume the polling loop, immediately stopping data ingestion without affecting other vehicles in the fleet.

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 →