How the TeslaMate Vehicles Module Manages Multiple Tesla Vehicles Simultaneously
The TeslaMate Vehicles module manages multiple Tesla vehicles simultaneously by spawning an isolated GenStateMachine process for each car under a single OTP Supervisor, enabling parallel monitoring, fault isolation, and independent state tracking across the entire fleet.
TeslaMate is a self-hosted data logger for Tesla vehicles built with Elixir and Erlang/OTP. Its ability to track driving statistics, charging sessions, and battery health for multiple cars concurrently relies on a process-oriented architecture where the TeslaMate.Vehicles module acts as a supervision root. This design ensures that each vehicle operates as an independent entity, preventing failures in one car's API connection from affecting the monitoring of others.
Supervisor Architecture in TeslaMate.Vehicles
At the core of the fleet management system lies the TeslaMate.Vehicles supervisor. When the application starts, this module queries the Tesla API to discover all available vehicles and dynamically constructs a supervision tree.
The init/1 function in lib/teslamate/vehicles.ex fetches the vehicle list via list_vehicles!/0 (which calls TeslaMate.Api.list_vehicles/0), removes duplicates with Enum.uniq_by/2, filters out disabled cars, and generates a child specification for each remaining vehicle:
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(& &1.car.id)
|> Enum.filter(& &1.car.settings.enabled)
Supervisor.init(children,
strategy: :one_for_one,
max_restarts: 5,
max_seconds: 60
)
end
The supervisor employs a :one_for_one restart strategy. If a single vehicle process crashes due to a network timeout or API error, only that specific process restarts, leaving the other vehicle processes unaffected. Each child process is created using Vehicle.child_spec/1, which registers the process with a unique name based on the car's database ID.
The Vehicle State Machine (TeslaMate.Vehicles.Vehicle)
Each Tesla vehicle runs as an independent GenStateMachine within lib/teslamate/vehicles/vehicle.ex. This module encapsulates all per-vehicle logic, including API polling, streaming API connections, state transitions, and data logging.
The state machine maintains distinct states such as {:asleep, interval}, {:online, _}, {:driving, status, drive}, and {:charging, proc}. Upon initialization, the process:
- Registers API error fuses for circuit breaker protection
- Subscribes to settings changes via
TeslaMate.Settings - Schedules an immediate data fetch using
{:next_event, :internal, :fetch}
When the streaming API is enabled in CarSettings.use_streaming_api, the process spawns a dedicated stream connection via connect_stream/1. Stream messages ({:stream, %Stream.Data{}}) drive real-time state transitions, such as detecting when a drive starts or charging begins, while traditional polling continues as a fallback.
Fleet Coordination and Parallel Execution
The Vehicles module manages multiple Tesla vehicles simultaneously through complete process isolation. Each vehicle maintains its own internal %Data{} struct containing the car record, last API response, stream PID, and current state. No shared mutable state exists between vehicles.
To aggregate data across the fleet, the supervisor provides the list/0 function, which concurrently queries all vehicle processes:
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 to request summaries from all vehicles in parallel. If one vehicle is offline or experiencing high latency, the five-second timeout ensures the call returns without blocking the entire fleet query.
Real-Time Communication via PubSub
Each vehicle process broadcasts updates through Phoenix PubSub to decouple data producers from consumers. The handle_event/4 callback in lib/teslamate/vehicles/vehicle.ex emits two primary topics:
- Summary updates – Compact state representations sent to
summary_topic(car_id)for the web interface - Fetch status – Indicators of whether an API request is in progress
def handle_event(:internal, {:broadcast_summary, state, data}, ...) do
call(data.deps.pubsub, :broadcast,
[TeslaMate.PubSub, summary_topic(data.car.id), payload])
end
The TeslaMate.Mqtt.PubSub.VehicleSubscriber module subscribes to these topics and forwards vehicle data to MQTT brokers, enabling integration with home automation systems without modifying the core vehicle processes.
Practical Fleet Management API
The TeslaMate.Vehicles module exposes a unified API for interacting with the entire fleet or individual cars:
# Retrieve all vehicle summaries ordered by display priority
summaries = TeslaMate.Vehicles.list()
# Subscribe the current process to live updates for a specific vehicle
TeslaMate.Vehicles.subscribe_to_summary(3)
# Temporarily pause data logging for a specific car
:ok = TeslaMate.Vehicles.suspend_logging(3)
# Resume logging
:ok = TeslaMate.Vehicles.resume_logging(3)
These functions delegate to the respective GenStateMachine processes via helper functions in lib/teslamate/vehicles.ex, ensuring consistent behavior whether managing one vehicle or twenty.
Summary
- Process-per-vehicle architecture: Each Tesla runs as an isolated GenStateMachine under the
TeslaMate.Vehiclessupervisor, ensuring fault tolerance via the:one_for_onestrategy. - Dynamic fleet discovery: The supervisor queries
TeslaMate.Api.list_vehicles/0at startup and creates child specs usingVehicle.child_spec/1, filtering duplicates and disabled entries. - Parallel data collection: Fleet-wide queries use
Task.async_streamto concurrently fetch vehicle summaries without blocking. - State machine encapsulation: All per-vehicle logic (polling, streaming, logging) lives in
lib/teslamate/vehicles/vehicle.ex, with states like{:driving, status, drive}tracking real-time conditions. - Decoupled communication: Vehicles broadcast updates via PubSub topics (
summary_topic/1), consumed by the web UI and MQTT subscribers independently.
Frequently Asked Questions
How does TeslaMate prevent one vehicle's API failure from affecting the entire fleet?
Because each vehicle operates as an independent OS process under the :one_for_one supervision strategy, a crash or timeout in one vehicle's GenStateMachine triggers only a single process restart. The supervisor does not restart sibling processes, ensuring that API issues with one Tesla do not interrupt data collection for others.
What happens when a new Tesla is added to the account after TeslaMate has started?
The current implementation loads the vehicle list once at startup via list_vehicles!/0. To add a new vehicle, the TeslaMate application requires a restart so the supervisor can re-execute init/1 and generate a new child spec for the additional car. The system caches vehicle data in the database, allowing it to fall back to stored records if the Tesla API is unreachable during startup.
Can TeslaMate monitor vehicles with different API tokens simultaneously?
Yes. While the standard configuration uses a single global API token, the architecture supports dependency injection through the opts parameter in start_link/1. Each Vehicle process receives its dependencies (including API credentials) via the car struct and data.deps map, theoretically allowing different tokens per vehicle if the supervisor initialization is customized.
How does the streaming API coexist with traditional polling?
The GenStateMachine manages both data sources concurrently. When use_streaming_api is enabled, the process spawns a stream connection that sends messages directly to the vehicle process mailbox. These messages trigger immediate state transitions (e.g., from online to driving). Simultaneously, the process maintains scheduled polling intervals as a fallback mechanism to ensure data continuity if the streaming connection drops.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →