OwnTracks Location Endpoint

Project to set up a small HTTP endpoint for receiving OwnTracks location updates from John's phone.

Current situation

Resolved 2026-06-29. POST /loc is live at https://www.critchley.biz/loc with HTTP Basic Auth. Phone is sending updates and they are being stored in SQLite on gravlax.

Payload shape

{"_type":"location","topic":"owntracks/john/<device-id>","lat":51.522545,"lon":-2.509061,"tst":1782649022,"acc":3,"vac":3,"alt":72,"batt":85,"conn":"m","vel":26,"tid":"00"}

The phone screenshot exposed precise co-ordinates and a device/topic identifier, so avoid publishing it unredacted.

Required behaviour

Web server / proxy issue

Resolved — WSGIScriptAlias /loc added to the SSL vhost. No reverse-proxy to a separate service; runs under the existing mod_wsgi/Apache process.

Security

Smoke test

# With auth (expect 204) — use fake UUID in topic for test entries
curl -i -X POST 'https://www.critchley.biz/loc' \
  -u 'owntracks:<password>' \
  -H 'Content-Type: application/json' \
  --data '{"_type":"location","lat":51.5,"lon":-2.5,"tst":1782649022,"topic":"owntracks/test/00000000-0000-0000-0000-000000000000","acc":5,"batt":80}'

# Without auth (expect 401)
curl -i -X POST 'https://www.critchley.biz/loc' \
  -H 'Content-Type: application/json' \
  --data '{"_type":"location","lat":51.5,"lon":-2.5,"tst":1782649022}'

With credentials returns 204 No Content. Without credentials: 401 Unauthorized. Bad JSON or missing fields: 400 Bad Request.

Open questions

Deployment (gravlax, 2026-06-29)

Apache config snippet added to www.critchley.biz-ssl.conf

# OwnTracks location receiver
WSGIScriptAlias /loc /usr/local/www/wsgi-scripts/owntracks_loc.py
<Location /loc>
    AuthType Basic
    AuthName "OwnTracks"
    AuthUserFile /etc/apache2/.htpasswd-owntracks
    Require valid-user
</Location>

Querying stored locations

import sqlite3, datetime
conn = sqlite3.connect('/var/lib/owntracks/locations.db')
for r in conn.execute('SELECT received_at, topic, lat, lon, acc, batt FROM locations ORDER BY received_at DESC LIMIT 10'):
    ts = datetime.datetime.utcfromtimestamp(r[0]).strftime('%Y-%m-%d %H:%M:%S UTC')
    print(ts, r[1], r[2], r[3], 'acc='+str(r[4]), 'batt='+str(r[5]))

Run as: sudo python3 <script> on gravlax (DB is owned by www-data).

Location API (/location)

Read-only JSON API added 2026-06-30. Mounted at https://www.critchley.biz/location via WSGIScriptAlias /location in the SSL vhost. Source: ~/OwnTracks/owntracks_api.py on kelp, installed to /usr/local/www/wsgi-scripts/owntracks_api.py on gravlax.

Usage

# Last 100 fixes for john's phone (default UUID filter)
GET /location?who=john

# Last N fixes
GET /location?who=john&n=20

# Specific device UUID
GET /location?who=john&device=53AECFB9-BA35-44B8-BB70-9A35D2B32500

# All devices including test entries
GET /location?who=john&device=all

# Time-bounded (n limit lifted when both given)
GET /location?who=john&start=1782649000&end=1782759000

# No who / wrong who -> 403
GET /location

Client scripts (on kelp, ~/OwnTracks/)

Tabular dump of the SQLite database. Runs on gravlax (needs sudo for DB access).

sudo python3 ~/dump_locations.py          # last 20 (default)
sudo python3 ~/dump_locations.py -n 50   # last 50
sudo python3 ~/dump_locations.py --db /path/to/other.db

owntracks_speed.py

Fetches today's fixes from the /location API and computes speed between consecutive points using the Haversine formula. One line per fix.

python3 ~/OwnTracks/owntracks_speed.py              # today, mph, text
python3 ~/OwnTracks/owntracks_speed.py --kph         # km/h
python3 ~/OwnTracks/owntracks_speed.py --ms          # m/s
python3 ~/OwnTracks/owntracks_speed.py --sixel       # speed-over-time graph (sixel)
python3 ~/OwnTracks/owntracks_speed.py --all         # all records, not just today
python3 ~/OwnTracks/owntracks_speed.py --start EPOCH --end EPOCH

User/device config

/etc/owntracks/config.json on gravlax. Maps who tokens (API access tokens) to device UUIDs. Read on every request — edit and save, no restart needed.

{
  "users": {
    "john": "53AECFB9-BA35-44B8-BB70-9A35D2B32500"
  }
}

To add a person: add their who token and UUID to the config. The receiver (owntracks_loc.py) also uses this file to resolve a topic UUID back to a name.

owntracks_kml.py

Exports location data as KML (Google Earth/Maps). Includes a LineString track and individual timestamped placemarks with accuracy and battery.

python3 ~/OwnTracks/owntracks_kml.py                         # today to stdout
python3 ~/OwnTracks/owntracks_kml.py --output ~/tmp/today.kml
python3 ~/OwnTracks/owntracks_kml.py --all --output ~/tmp/all.kml
python3 ~/OwnTracks/owntracks_kml.py --start EPOCH --end EPOCH --output ~/tmp/range.kml

Location inference guidance

Route landmark recovery notes

Todo

PostgreSQL / geospatial store (2026-07-13)

See using-gps-data for cross-project conventions (fresh-fix checks, gazetteer comparisons, PostGIS query patterns) that apply here too.

Data is dual-written to PostgreSQL (PostGIS) alongside SQLite. See location-db for schema, architecture, and the pg_query MCP tool.

Visualisations

Home → David Lloyd ride, 2026-07-17 — bar chart of cycling speed (bar height, km/h) vs bearing (hue, constant lightness/saturation) built from a pg_query speed/bearing window function over the ride.

version 5  ·  created 2026-06-29  ·  updated 2026-08-02  ·  tags project, infrastructure, location, owntracks