How the TeslaMate Geo-Fence Feature Creates Custom Locations: A Complete Technical Guide

The TeslaMate Geo-fence feature creates custom locations by validating circular boundary parameters through Ecto changesets, persisting them to PostgreSQL inside atomic transactions, and registering them for spatial queries using the within_geofence? macro.

The TeslaMate Geo-fence feature allows users to define named circular zones around specific coordinates to track vehicle presence, monitor charging sessions, and calculate costs. Understanding how this feature creates custom locations requires examining the Elixir LiveView interface, Ecto schema validation, and PostgreSQL spatial query implementation in the teslamate-org/teslamate repository.

Geo-Fence Schema and Data Structure

Every custom location begins as a %GeoFence{} struct defined in lib/teslamate/locations/geo_fence.ex. This Ecto schema maps to the geo_fences table and stores the spatial parameters required to define a circular boundary zone.

The schema captures:

  • name: User-defined identifier for the location
  • latitude and longitude: Center point coordinates in decimal degrees
  • radius: Circular boundary distance in meters (defaults to 20)
  • billing fields: Optional cost calculation parameters for charging sessions

# lib/teslamate/locations/geo_fence.ex

%GeoFence{
  radius: 20,
  latitude: lat,
  longitude: lng
}

The Custom Location Creation Pipeline

Creating a custom location follows a three-stage pipeline involving the LiveView interface, changeset validation, and transactional persistence.

Step 1: LiveView Form Initialization

When users initiate geo-fence creation, the TeslaMateWeb.GeoFenceLive.Form module pre-populates a %GeoFence{} struct with either manually entered coordinates or the vehicle's last known position. The form assigns a default radius of 20 meters and maintains the struct in the socket assigns for state management.


# lib/teslamate_web/live/geofence_live/form.ex

%GeoFence{
  radius: 20,
  latitude: lat,
  longitude: lng
}

Step 2: Changeset Validation

Upon form submission, the system validates input through Locations.change_geofence/2 in lib/teslamate/locations.ex. This function constructs an Ecto.Changeset that enforces required fields, validates radius constraints, and sanitizes the name field before permitting database insertion.


# Validation pipeline inside the LiveView

defp validate(params, %{assigns: assigns}) do
  changeset = Locations.change_geofence(assigns.geofence, params)

  with {:ok, geofence} <- Ecto.Changeset.apply_action(changeset, :update) do
    {:ok, geofence, changeset}
  end
end

Step 3: Transactional Persistence

The Locations.create_geofence/1 function wraps database operations in Repo.transaction to ensure atomicity. It inserts the geo-fence record using GeoFence.changeset/2, then immediately registers the fence with apply_geofence/1 to activate it for system-wide spatial queries.


# lib/teslamate/locations.ex

def create_geofence(attrs) do
  Repo.transaction(fn ->
    with {:ok, geofence} <- %GeoFence{} |> GeoFence.changeset(attrs) |> Repo.insert(),
         :ok <- apply_geofence(geofence) do
      geofence
    else
      {:error, reason} -> Repo.rollback(reason)
    end
  end)
end

Spatial Querying with Custom Locations

Once persisted, custom locations function as spatial predicates through the within_geofence? macro in lib/teslamate/custom_expressions.ex. This macro generates PostgreSQL earth distance calculations using earth_box and earth_distance functions to determine if vehicle coordinates fall within a fence's radius.


# lib/teslamate/custom_expressions.ex

defmacro within_geofence?(position, geofence, :right) do
  quote do
    fragment(
      """
      earth_box(ll_to_earth(?::numeric, ?::numeric), ?) @> ll_to_earth(?::numeric, ?::numeric) AND
      earth_distance(ll_to_earth(?::numeric, ?::numeric), ll_to_earth(?::numeric, ?::numeric)) < ?
      """,
      ^unquote(geofence).latitude,
      ^unquote(geofence).longitude,
      ^unquote(geofence).radius,
      unquote(position).latitude,
      unquote(position).longitude,
      ^unquote(geofence).latitude,
      ^unquote(geofence).longitude,
      unquote(position).latitude,
      unquote(position).longitude,
      ^unquote(geofence).radius
    )
  end
end

Complete LiveView Save Handler Implementation

The following implementation from lib/teslamate_web/live/geofence_live/form.ex demonstrates the complete event handling flow when users save a new custom location:

def handle_event("save", %{"geo_fence" => params}, socket) do
  with {:ok, geofence, changeset} <- validate(params, socket),
       {:ok, socket} <- show_modal_or_save(geofence, changeset, socket) do
    {:noreply, socket}
  else
    {:error, %Ecto.Changeset{} = changeset} ->
      {:noreply, assign(socket, changeset: changeset, show_errors: true)}
  end
end

defp validate(params, %{assigns: assigns}) do
  changeset = Locations.change_geofence(assigns.geofence, params)

  with {:ok, geofence} <- Ecto.Changeset.apply_action(changeset, :update) do
    {:ok, geofence, changeset}
  end
end

defp save(%{assigns: %{action: :new, geofence: _}} = socket) do
  Locations.create_geofence(socket.assigns.changeset.params)
end

Summary

  • The Geo-fence feature creates custom locations using a circular boundary model defined by latitude, longitude, and radius parameters stored in the geo_fences table.
  • LiveView forms in lib/teslamate_web/live/geofence_live/form.ex handle UI state and coordinate pre-population, defaulting to a 20-meter radius for new locations.
  • Ecto changesets via Locations.change_geofence/2 validate all inputs before database insertion, ensuring data integrity for spatial calculations.
  • Transactional safety in Locations.create_geofence/1 guarantees that geo-fences are only activated after successful database persistence and registration.
  • The within_geofence? macro enables efficient spatial queries using PostgreSQL's earth distance functions to match vehicle positions against custom locations without loading entire tables into memory.

Frequently Asked Questions

What database does TeslaMate use for storing geo-fence custom locations?

TeslaMate uses PostgreSQL with spatial extensions to store geo-fence data. The within_geofence? macro leverages PostgreSQL-specific functions including earth_box, earth_distance, and ll_to_earth to perform efficient spherical distance calculations on the database server, minimizing application memory usage when checking vehicle positions against multiple custom locations.

Can geo-fences overlap, and how does TeslaMate handle multiple custom locations at the same coordinates?

Yes, overlapping geo-fences are permitted. The system treats each custom location as an independent record in the geo_fences table. When querying vehicle positions, the application checks against all active fences using the within_geofence? predicate, allowing a single coordinate to match multiple custom locations simultaneously for different tracking purposes such as home charging and workplace monitoring.

What is the default radius when creating a new geo-fence in TeslaMate?

The default radius is 20 meters. This value is hardcoded in the TeslaMateWeb.GeoFenceLive.Form module when initializing a new %GeoFence{} struct. Users can modify this value through the web interface before saving the custom location to adjust the detection boundary for their specific use case, such as expanding coverage for large parking structures or reducing it for precise driveway detection.

How does TeslaMate validate geo-fence data before saving?

Validation occurs through Ecto.Changeset operations in Locations.change_geofence/2 defined in lib/teslamate/locations.ex. The changeset validates required fields (name, latitude, longitude, radius), enforces radius limits, and sanitizes the name field. Invalid submissions return error tuples that the LiveView handles by re-rendering the form with validation errors displayed to the user, preventing malformed data from reaching the database.

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 →