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 GlobalSettings for application defaults and CarSettings for vehicle-specific values.
  • Database Separation: Global settings reside in the settings table (singleton row), while per-car settings live in car_settings with 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 nil or undefined.
  • Validation Independence: Each schema maintains its own changeset/2 validation logic, ensuring data integrity without cross-dependencies.
  • Implementation Files: Core logic exists in lib/teslamate/settings/global_settings.ex and lib/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:

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 →