Swapping trackers

Everything downstream of ingest (storage, rules, delivery, API) only sees schemas.LocationFix:

class LocationFix(pydantic.BaseModel):
    lat: float
    lon: float
    accuracy_m: float | None   # horizontal, meters
    timestamp: datetime        # aware UTC
    battery: float | None      # 0.0 - 1.0
    motion: str | None         # e.g. "walking", "driving", "stationary"
    source: str                # adapter name
    device: str                # stable per-phone id

So a new tracker (GPSLogger, Home Assistant, a Garmin export, …) is one adapter file plus one route.

1. Write backend/sources/<name>.py

"""<Name> adapter: <link to the app's upload format>."""

import datetime

import schemas

SOURCE = "<name>"


def parse(body: dict, default_device: str = "<name>") -> list[schemas.LocationFix]:
    """
    Convert one <Name> upload into normalized fixes.
    1. Pull lat/lon/accuracy/time/battery out of the payload.
    2. Convert time to aware UTC; battery to 0..1.
    3. Skip (don't raise on) malformed points.
    """
    ...

Rules: never raise for a single bad point (log and skip), always return UTC timestamps, and pick a stable device so geofence state and landed comparisons stay per-phone.

2. Add the route in backend/app.py

Copy ingest_overland: add <name>_token: str = "" to config.Config (env LOCATION_<NAME>_TOKEN), a _require(lambda: cfg.<name>_token, api_style=False) dependency, then fixes = <name>.parse(body) → processing.ingest(session, cfg, fixes) and return whatever the app expects as an acknowledgement (Overland needs {"result":"ok"}, OwnTracks []).

3. Test, configure, deploy

Running two trackers at once is fine: fixes are keyed by (device, timestamp); give each app a distinct device name.