iconAnil Madhavapeddy, Professor of Planetary Computing

Tracking locations with OwnTracks, Life Cycle and Home Assistant / Aug 2025

I'm emerging reenergised from an epic trip to the Okavango Delta in Botswana, where we spent weeks in the wilderness gathering ground truth for TESSERA (and enjoying the wildlife!). Piecing together our locations was quite important, and so I took a cue from Ryan Gibb and deployed OwnTracks and HomeAssistant Device Tracker before I headed out there. There were four interesting tech pieces that resulted from this: a local iPhone app to determine my GPS accuracy while entirely remote; then merging my Home Assistant location database hile in the field, then reverse engineering 8 years worth of location data out of an old iOS app to backfill data, and finally deploying my own self-hosted OwnTracks on Recoil for the longer term.

How accurate are photo locations?

Device power is at a premium while in the remote wilderness, and a lot of the usual methods that iOS uses to figure out precise locations (such as Wifi SSIDs) simply don't exist there. So I need some way to figure out if our devices actually had a location lock, and if so what the error was (so I could wiggle my phone in the air until it got a lock).

Since we had utterly minimal connectivity, I managed to text Nick Ludlam back at London home base to seek his help. Nick vibed an iPhone PWA that uses heic.js to dump out all the Exif tags for a given photograph locally on my device. This solved the immediate problem while working entirely offline! You can grab its source for yourself or try it directly.

Figuring out my Home Assistant location while remote

I've used Home Assistant for many years to manage my household devices. They supply a handy iOS app which uses Location Services to keep track of where you are, in order to trigger events ("Turn on the heating when I arrive home"). When I was out in Botswana, we had a bunch of cameras with us such as Canon R5 MKII and an Olympus E-M1 MKII. These cameras don't track GPS reliably, which makes them a pain to realign with iPhone and other footage after the fact.

Idyllic and peaceful, and definitely no signal
Idyllic and peaceful, and definitely no signal

Luckily, I realised that my Home Assistant iOS app is actually recording its location locally to my device, and very occasionally syncing it back home on the rare occasions where we had signal (we discovered that by standing on top of a old termite mound and holding your phone high in the air you could occasionally get one bar of connectivity for important texts).

I retrieved my Home Assistant database from Cambridge (via Tailscale), and used hass2geo to dump out GPX format traces of our locations. Then, I vibe coded a Python script to convert the waypoint format into "trackfiles", so that they could be loaded into the Lightroom map view. After this it was relatively easy going, as Lightroom matches up the GPS traces to the camera picture times, and stamps them accordingly. It's not perfect as it doesn't interpolate between track traces, but good enough for the sort of accuracy I wanted ("in the NG32 reserve in Botswana").

In the long term though, this isn't a great solution: Home Assistant only keeps a few days of location history, and getting fine-grained GPS traces via the API requires manually dumping out the internal database.

It got cold at night on the salt plains
It got cold at night on the salt plains

Reverse engineering Life Cycle on my phone

I then looked more closely at which apps were using location on my phone, and discovered the Life Cycle app that had been there since 2018! This was an app I installed way back to categorise where I was spending my time, and then promptly forgot about it when the pandemic started. The app developers are generally privacy friendly, and it also had an option to output a CSV file of all the location history.

Unfortunately, this CSV file is incomplete, as it includes locations as names, but not their exact GPS location (which the app has internally). I opened a support request with the app developer, but they responded that they don't plan to add any new features to the app. That's fair enough, but I really wanted to get the data out!

This is how grumpy I was when I realised my GPS data was trapped in my phone
This is how grumpy I was when I realised my GPS data was trapped in my phone

The obvious solution is to extract the app database from my iPhone and reverse engineer it. While this used to be easy, the app sandboxes mean that modern iPhones are very locked down. I had to manually initiate a tethered and unencrypted backup to my Mac (as opposed to an iCloud backup), and then extract the Life Cycle app from my phone using iMazing.

$ ls 
   11440 Aug 11 15:43 .lock
     192 Aug 11 15:43 Container/
    1555 Aug 11 15:43 iTunesMetadata.plist
 6661627 Aug 11 15:43 Life Cycle.imazingapp
      96 Aug 11 15:43 Payload/
$ cd Container/Library/ && file life.db
life.db: SQLite 3.x database, user version 20, last written using SQLite
version 3043002, file counter 1, database pages 3443, cookie 0x65, schema 4,
UTF-8, version-valid-for 1

We've found the life.db, and its now in an unencrypted sqlite3 format! But what format is it actually in? I didn't have time to figure it out, but I pointed Claude Code at the database in an attempt to reverse engineer the format into something I could export. After consulting Ryan Gibb, he suggested using OwnTracks as a self-hosted location tracker.

Rather than get Claude to directly interpret the data, I directed it to examine the sqlite3 database and to emit a script that would convert it to a format that OwnTracks could use. It did this pretty accurately:

# Discovered schema with 24 tables tracking location, motion, activities
sqlite3 life.db ".tables"
sqlite3 life.db ".schema"

# Created JSON export script for primary observational data
- LocationEvent: Raw GPS coordinates (31,743 records)
- Motion: Movement classification (77,385 records)
- Activity: Behavior categorization (20,273 records)
- Visit: Stationary location periods (9,903 records)

2. Initial Upload Implementation

# Core transformation mapping
LifeCycle → OwnTracks:
- timestamp → tst (Unix epoch)
- latitude/longitude → lat/lon
- hAccuracy → acc
- altitude → alt (with validation)
- speed → vel (m/s to km/h conversion)
- WiFi context → SSID/BSSID

Features implemented:
- Duplicate prevention via state tracking
- Resume capability for interrupted uploads
- Quality filtering (>500m accuracy rejected)
- Progress monitoring

3. Enhanced Data Integration

# Added motion activities from 77K motion records
"motionactivities": [
  {"type": "automotive", "confidence": 2},
  {"type": "walking", "confidence": 0}
 ]

This implementation went well beyond what I had in mind: in addition to just translating the GPS, it also looked at the contextual data (such as the iOS motion tracking information), and also wrote a comprehensive script (available here) that collected it all together and uploaded it to my server.

Overall, the fact that I could reverse engineer a decade old app, autodetect its data structures and convert with high fidelity into a self-hosted infrastructure in about 15 minutes of agent-driven coding is a capability I find absolutely incredible. This sort of coding isn't suitable for everything, of course, but it's great for the tedious glue code which makes self-hosting often so painful.

On some occasions, we definitely did not want to be found
On some occasions, we definitely did not want to be found

Deploying OwnTracks on Recoil

I now had my past eight years of locations in one place, so it seemed a good time to switch away completely to a self-hosted location system dedicated to that purpose.

Deploying the aforementioned OwnTracks requires two things: a secure MQTT endpoint to receive events from the iOS app, and then a recorder that subscribes to it and compacts the results into location records. The recorder exposes an HTTP API that frontends can use to render map like interfaces, or it works headlessly as well for other API uses of your location.

I used this docker-compose.yml, as I couldn't find an off-the-shelf setup:

services:
  frontend:
    image: caddy:2-alpine
    depends_on:
      - mqtt
    ports:
      - "443"
      - "443/udp"
      - "80"
    volumes:
      - ./caddy/data:/data
      - ./caddy/htdocs:/htdocs
      - ./caddy/conf:/etc/caddy
    restart: always
  owntracks-frontend:
    image: owntracks/frontend
    volumes:
      - ./frontend/config.js:/usr/share/nginx/html/config/config.js
    environment:
      - SERVER_HOST=otrecorder
      - SERVER_PORT=8083
    restart: unless-stopped
  otrecorder:
    image: owntracks/recorder
    restart: unless-stopped
    environment:
      - VIRTUAL_HOST=<host>
      - TZ=Europe/London
      - OTR_USER=<user>
      - OTR_PASS=<pass>
      - OTR_HTTPHOST=0.0.0.0
      - OTR_HTTPPORT=8083
      - OTR_HOST=mqtt
      - OTR_PORT=1883
    volumes:
      - ./owntracks/config:/config
      - ./owntracks/store:/store
  mqtt:
    container_name: mqtt
    build: .
    environment:
      - TZ=Europe/London
    ports:
      - "185.33.27.72:8883:8883"
    volumes:
      - ./mosquitto/data:/mosquitto/data
      - ./mosquitto/logs:/mosquitto/logs
      - ./mosquitto/conf:/mosquitto/config
      - ./mosquitto/conf/passwd:/etc/mosquitto/passwd:ro
      - ./caddy/data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/track.recoil.org:/etc/mosquitto/crt:ro
    restart: unless-stopped

You can replace the direct environment variables with an env_file (recommended!). This compose file uses Caddy to set up LetsEncrypt certificates for the MQTT server automatically, and then you just need to drop in a caddy/conf/Caddyfile with the HTTP auth:

domainname.org {
  encode gzip
  reverse_proxy http://owntracks-frontend
  basicauth * {
   user hashed-password
  }
}

This uses normal browser auth to protect your location, and you can also run the whole thing behind Tailscale or similar to avoid having an Internet-visible port exposed too.

This harrier's piercing gaze would find us anywhere
This harrier's piercing gaze would find us anywhere

What next for locations?

I've been interested in "spatial programming" for ages, and while I was away Ryan Gibb and Josh Millar uploaded a preprint we've been working on a system for programming with physical locations called "Bifrost". We're particularly interested in the intersection between physical devices, locations and how to manage their interactions. This is a continuation of the work on Osmose and spatial naming. Stay tuned!

# 14th Aug 2025 iconnotes claude gps llms selfhosting spatial

Related News