How TeslaMate Handles Global vs Per-Car Configuration in the Settings Module
TeslaMate uses two distinct Ecto schemas—GlobalSettings for application-wide defaults and CarSettings for vehicle-specific overrides—to separate global configuration from per-car preferences using a fallback resolution pattern.
TeslaMate, the open-source Tesla data logger built with Elixir and Phoenix, manages application configuration through a sophisticated dual-layer settings architecture. Understanding how the Settings module handles global vs per-car configuration is essential for customizing individual vehicle behavior while maintaining consistent application defaults across your fleet. The implementation stores these values in separate database tables and resolves them through a hierarchical lookup mechanism.
Understanding the Dual-Layer Architecture
The TeslaMate codebase maintains configuration in two isolated scopes that operate independently yet complement each other during runtime resolution.
Global Settings (GlobalSettings)
The TeslaMate.Settings.GlobalSettings module, defined in lib/teslamate/settings/global_settings.ex, manages application-wide defaults that apply universally unless overridden. This schema maps to the settings table and stores values such as :unit_of_length, :unit_of_temperature, :unit_of_pressure, :preferred_range, :base_url, :grafana_url, :language, and :theme_mode.
The system treats this table as a singleton—only the first row returned by Repo.one(GlobalSettings) is considered the active global configuration. The schema's changeset/2 function casts permitted attributes, validates required fields, trims URL values, and runs validate_url/2 to ensure endpoint validity. An @supported_languages map restricts language codes to predefined options using validate_inclusion/3.
Per-Car Settings (CarSettings)
Vehicle-specific configurations reside in TeslaMate.Settings.CarSettings, located in lib/teslamate/settings/car_settings.ex. This schema connects to the car_settings table and contains fields like :suspend_min, :suspend_after_idle_min, :req_not_unlocked, :free_supercharging, :use_streaming_api, :enabled, and :lfp_battery.
Each car maintains a one-to-one relationship with its settings record through has_one :car, Car, foreign_key: :settings_id. The changeset/2 function in this module casts all fields defined in @all_fields and enforces their presence, providing strict validation for car-specific operational parameters.
Database Schema and Migrations
The physical database layer reflects this logical separation through distinct migration files. The create_settings.exs migration establishes the global settings table, while car_settings.exs creates the per-car car_settings table. These migrations align exactly with the Ecto schema definitions, ensuring type consistency and constraint enforcement at the database level.
Configuration Resolution and Fallback Logic
TeslaMate resolves configuration values through a three-step hierarchy that prioritizes specificity over generality.
When the application needs a configuration value, it first loads the singleton global record using Repo.one(GlobalSettings). For vehicle-specific operations, the code fetches the car record and preloads its associated settings via Repo.preload(:settings). The resolution logic then checks the per-car value first; if the field is nil or missing, the system falls back to the corresponding global default.
# Fetch the global configuration singleton
global = Repo.one(TeslaMate.Settings.GlobalSettings)
# Load a specific car with its settings association
car = Repo.get!(TeslaMate.Log.Car, car_id) |> Repo.preload(:settings)
# Resolve a setting with fallback logic
suspend_min =
case car.settings.suspend_min do
nil -> global.suspend_min # fallback to global default
val -> val # use per-car override
end
Updates to global configuration flow through GlobalSettings.changeset/2 and persist via Repo.update/1, while per-car modifications use CarSettings.changeset/2. Both schemas maintain independent validation pipelines, ensuring data integrity within their respective scopes.
Working with Settings in Practice
Updating Global Settings
To modify application-wide defaults, fetch the singleton record and apply changes through the changeset pipeline:
attrs = %{
unit_of_length: :km,
unit_of_temperature: :C,
unit_of_pressure: :bar,
preferred_range: :ideal,
language: "en",
theme_mode: :system,
base_url: "https://example.com",
grafana_url: "https://grafana.example.com"
}
global = Repo.one!(TeslaMate.Settings.GlobalSettings)
changeset = TeslaMate.Settings.GlobalSettings.changeset(global, attrs)
{:ok, _} = Repo.update(changeset)
Updating Per-Car Settings
Vehicle-specific overrides require preloading the settings association before modification:
car = Repo.get!(TeslaMate.Log.Car, car_id) |> Repo.preload(:settings)
attrs = %{
suspend_min: 15,
suspend_after_idle_min: 10,
req_not_unlocked: true,
free_supercharging: false,
use_streaming_api: true,
enabled: true,
lfp_battery: false
}
changeset = TeslaMate.Settings.CarSettings.changeset(car.settings, attrs)
{:ok, _} = Repo.update(changeset)
Reading Settings with Fallback
Encapsulate the resolution logic in a helper function to maintain consistent fallback behavior throughout the application:
defp get_setting(car, key) do
case Map.get(car.settings, key) do
nil -> Map.get(global(), key) # global/0 returns the singleton
value -> value
end
end
Summary
- Dual Schema Design: TeslaMate separates concerns using
GlobalSettingsfor application defaults andCarSettingsfor vehicle-specific values. - Database Separation: Global settings reside in the
settingstable (singleton row), while per-car settings live incar_settingswith a one-to-one relationship to the cars table. - Fallback Resolution: The system checks per-car values first and falls back to global settings when specific values are
nilor undefined. - Validation Independence: Each schema maintains its own
changeset/2validation logic, ensuring data integrity without cross-dependencies. - Implementation Files: Core logic exists in
lib/teslamate/settings/global_settings.exandlib/teslamate/settings/car_settings.ex, supported by corresponding migration files.
Frequently Asked Questions
What happens if a per-car setting value is null?
When a field in the CarSettings record is nil, TeslaMate automatically falls back to the corresponding value in the GlobalSettings singleton. This ensures that cars without explicit overrides still inherit sensible application-wide defaults defined by the administrator.
How does TeslaMate validate settings updates?
Both schemas implement changeset/2 functions that cast permitted fields and enforce validation rules. The GlobalSettings changeset validates URL formats and supported language codes, while CarSettings requires all defined fields in @all_fields to be present, preventing partial updates that could leave vehicles in undefined states.
Can multiple cars share the same per-car settings record?
No. The architecture enforces a strict one-to-one relationship between a car and its settings record through the has_one :car, Car, foreign_key: :settings_id association. Each vehicle maintains its own isolated row in the car_settings table, ensuring that modifications to one vehicle's configuration never affect another.
Where are the database migrations for these settings located?
The migration files reside in the standard Phoenix migrations directory. The global settings table is created by create_settings.exs, while the per-car settings table is established by car_settings.exs. These migrations define the column types and constraints that align with the GlobalSettings and CarSettings schema definitions.
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 →