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.
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.
backend/app.pyCopy 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 []).
backend/tests/test_sources.py; run cd backend && venv/bin/python -m pytest tests.openssl rand -hex 24), add it to secrets/location.env, add the file to scripts/deploy.sh's
copy list, run scripts/deploy.sh.https://location.evanrthomas.com/ingest/<name>?token=….Running two trackers at once is fine: fixes are keyed by (device, timestamp); give each app a distinct device name.