Repository navigation
Ops Theft Tracking
When a theft alarm fires (lifted, left the dock without a command, left the garden) MowDeck starts tracking the mower: a position every 10 seconds over whatever link the mower has (the cloud over 4G for a Mammotion), for at most 24 hours. Design and decisions: ADR 0058.
- A notification "Ortung gestartet" (critical, also during quiet hours). Tapping it opens
/mowers/<id>/tracking. - On that page: a big map with the track since the alarm on a street map or aerial photo (Base map), the last position and its age, the coordinates as text with a copy button and links to Apple Maps, Google Maps and OpenStreetMap, Link teilen, Bericht (PDF) and Ortung beenden. Ending asks for the reason: Fehlalarm or Ortung beenden. The mower page has a shield button that turns red while tracking runs.
- Further notifications are rare on purpose: a move of 50 m or more (at most every 5 minutes), "Kontakt verloren" once per outage (the last position stays on the map, "stale since"), and the end (by hand or after the maximum time).
Starting needs can_control on the mower, viewing the track, the links and the report needs can_view.
Start tracking by hand on the tracking page ("Ortung starten"): it takes the same path as an alarm (lease to the gateway, notifications, track, share link, report) without lifting the mower or leaving the garden. End it with "Fehlalarm". A geofence test works too: drive the mower out of its mapped area by hand with the margin in mind (ADR 0027), which triggers the real alarm and the automatic start.
"Link teilen" creates a secret address https://<host>/track#<token>. Whoever has it sees, without a login, a live map with the position, the track since the alarm, the times, and the mower's name and model; nothing else (no account, no other mowers, no serial number, no controls). It expires after 48 hours at the latest and can be revoked in the tracking view at any time; the list shows created, expiry and number of views. The token is 256 bits, only its SHA-256 is stored, and it sits in the URL fragment so it never appears in logs or in the Referer. The page and API answer with noindex and no-referrer headers and are rate limited per client. Treat the link like a key: send it to the police or a trusted person, not to a public chat.
A stolen mower can be carried hundreds of kilometres away, so the tracking map needs a real map of wherever it is, not the garden map. Telemetry serves up to two background layers, switchable on the map ("Karte" / "Luftbild"), for the owner's view and for the share link. The map always fits the whole track, wherever it lies.
| Layer | Setting | Coverage |
|---|---|---|
| Karte (street map), recommended source OpenStreetMap |
BASEMAP_URL=https://tile.openstreetmap.org/{z}/{x}/{y}.png, BASEMAP_ATTRIBUTION=© OpenStreetMap contributors, BASEMAP_MAX_ZOOM=19
|
worldwide, street level up to zoom 19 |
| Karte, alternative BKG TopPlusOpen (Bundesamt für Kartographie und Geodäsie, open data dl-de/by-2-0, no key) |
BASEMAP_URL=https://sgx.geodatenzentrum.de/wmts_topplus_open/tile/1.0.0/web/default/WEBMERCATOR/{z}/{y}/{x}.png, BASEMAP_MAX_ZOOM=18, credit © BKG (2026), dl-de/by-2-0, Datenquellen: sgx.geodatenzentrum.de/web_public/gdz/datenquellen/Datenquellen_TopPlusOpen.html
|
world up to zoom 12; Europe up to about zoom 16 to 17 (checked: Madrid, Warsaw, Istanbul, Moscow); Germany up to zoom 18. Outside Europe deeper zooms are empty tiles (checked: New York, Tokyo from zoom 13) |
| Luftbild (aerial photo) | the aerial photo source of the mower map (aerial-photo.md, AERIAL_*) |
that source's area only; the Bavarian orthophoto covers Bavaria only: set AERIAL_COVERAGE=47.27,8.97,50.57,13.84
|
The street map is off by default (no request to any third party until the operator sets it). The credit line is always shown while a layer is visible; an OpenStreetMap credit links to https://www.openstreetmap.org/copyright.
Coverage and fallback. BASEMAP_COVERAGE and AERIAL_COVERAGE (south,west,north,east in degrees, empty: the world) tell Telemetry where a source has data. The tracking status says per layer whether it covers the last position (covers_last); the map then shows the chosen layer only where it has data and otherwise switches to the next one (for example from Luftbild to Karte once the mower leaves Bavaria) with a hint, or to a grid with a hint when no layer covers the place. A layer whose tiles all fail to load is skipped the same way. Tiles outside a coverage box are refused before any upstream request.
Coordinates as text. Under the map both pages show the last position as decimal latitude and longitude with six places (about 10 cm), the time of the fix and its age, a copy button, and links "In Apple Karten öffnen", "In Google Maps öffnen" and "In OpenStreetMap öffnen" (plus the Android maps app via geo:). They are built from the coordinates only, so a share token never appears in them. This works even where no map covers the place; give these coordinates to the police. The vendor reports no accuracy figure; without RTK the position can be several metres off.
How tiles are fetched. The browser asks MowDeck for each tile (GET /api/v1/telemetry/mowers/{id}/tracking/map/{layer}/{z}/{x}/{y} for the owner, POST /api/v1/telemetry/public/tracking/tile with the token in the body for the share link). Telemetry serves only tiles that overlap the session's positions plus MOWDECK_TELEMETRY_TRACKING_MAP_MARGIN_METERS (2000) and lie in the layer's zoom range and coverage, and keeps them in the aerial cache volume (256 MB). So the browser never contacts the provider, the content security policy stays as it is, and a share link reveals nothing beyond the track it already shows. The provider sees the server's address and the tiles around the track.
OpenStreetMap tile usage policy (https://operations.osmfoundation.org/policies/tiles/). Telemetry, which is the only client of the tile server:
- identifies itself on every upstream request:
User-Agent: MowDeck/<version> (+https://github.com/lukislp/mowdeck), and sends noReferer(none is forged either); - keeps every tile as long as the server's
Cache-Control: max-ageorExpiresallows and does not ask again before that; without either header it keeps a tile forAERIAL_CACHE_TTL_DAYS(30), never less than 7 days;no-store/no-cacheanswers are not kept; - fetches only tiles a viewer is looking at: no prefetching, no bulk download, no tiles outside the track's box;
- limits new tiles per account (
MOWDECK_TELEMETRY_AERIAL_MISSES_PER_MINUTE, 240) with four requests in flight at most, and the share page per client (600 tiles per minute); - shows the attribution with a link to the copyright page.
Keep it that way for a family-sized installation; an operator with heavy use needs its own tile server or a commercial provider.
Mower model, serial number or device name (from the connection options, Mammotion device_name), the MowDeck name, alarm kind and time, the track (position table and a drawing on a blank metre grid; no map tiles are used) and up to four photos of the mower camera (one is taken at the start and one a minute later if the first failed; more by POST .../tracking/photo). Blank fields for name, address, contact, police report number and insurance. German or English (?lang=).
| Variable | Default | Meaning |
|---|---|---|
MOWDECK_TELEMETRY_TRACKING_INTERVAL_SECONDS |
10 |
Position report interval (the adapter never goes below 5) |
MOWDECK_TELEMETRY_TRACKING_MAX_HOURS |
24 |
A session ends by itself after this long |
MOWDECK_TELEMETRY_TRACKING_AUTO_START |
true |
Start on a theft alarm |
MOWDECK_TELEMETRY_TRACKING_STALE_AFTER_SECONDS |
120 |
No report for this long marks the session stale |
MOWDECK_TELEMETRY_TRACKING_MOVE_METERS / _MOVE_NOTICE_SECONDS
|
50 / 300
|
Distance and minimum spacing of "moved" notices |
MOWDECK_TELEMETRY_TRACKING_LINK_TTL_HOURS / _MAX_LINKS
|
48 / 10
|
Share link lifetime and active links per session |
MOWDECK_TELEMETRY_TRACKING_RETENTION_DAYS |
30 |
Ended sessions, links and photos are deleted this long after the end |
MOWDECK_TELEMETRY_TRACKING_PHOTOS |
true |
Take camera photos for the report |
MOWDECK_TELEMETRY_TRACKING_PUBLIC_RATE_PER_MINUTE / _PUBLIC_FAILURES_PER_MINUTE
|
60 / 20
|
Rate limits of the public page per client |
MOWDECK_TELEMETRY_TRACKING_TICK_SECONDS / _LEASE_SECONDS
|
15 / 60
|
Background tick and gateway lease |
MOWDECK_TELEMETRY_BASEMAP_URL / _ATTRIBUTION
|
empty | Street map under the track (see Base map); empty: off |
MOWDECK_TELEMETRY_BASEMAP_MIN_ZOOM / _MAX_ZOOM
|
8 / 19
|
Zoom range of the street map |
MOWDECK_TELEMETRY_BASEMAP_COVERAGE / MOWDECK_TELEMETRY_AERIAL_COVERAGE
|
empty (the world) | Box a source covers, south,west,north,east; outside it the map switches layers |
MOWDECK_TELEMETRY_TRACKING_MAP_MARGIN_METERS |
2000 |
Base map tiles are served this far around the session's positions |
MOWDECK_TELEMETRY_TRACKING_PUBLIC_TILES_PER_MINUTE |
600 |
Base map tiles per minute and client on the share page |
- The vendor cloud sees one report request per interval per tracked mower; the adapter backs off (up to 120 s) when the mower stays silent. If Telemetry stops, the gateway returns to its normal rhythm within a minute (the lease runs out).
- The public page needs the web app's nginx (
/trackis served withno-store,noindexandno-referrer) and Traefik routing of/api/v1/telemetryas for every Telemetry route. - Metrics:
mowdeck_device_gateway_tracking_leases_total{outcome}. - Privacy: ended sessions are deleted after 30 days, or at once with
DELETE /api/v1/telemetry/mowers/{id}/tracking/sessions/{session}; the positions stay in the mower's track under its own retention.
Mirrored from docs/ops/theft-tracking.md at commit 11a94afbcace55924c9675ef400e93c7785b4ac5. Edit the file in the repository, not this wiki page.
Mirrored from the docs/ directory at commit 11a94afbcace55924c9675ef400e93c7785b4ac5. Edit the files in the repository, not this wiki.
MowDeck documentation
- Home
- Phases
- Architecture
-
Decisions (ADR)
- Architecture decisions
- ADR 0001: OpenFGA instead of self-built authorization
- ADR 0002: Self-built Identity service in FastAPI instead of Keycloak or Zitadel
- ADR 0003: SvelteKit for the web app
- ADR 0004: Monorepo with a uv workspace
- ADR 0005: AGPL-3.0 for services and apps, Apache-2.0 for libraries
- ADR 0006: PostgreSQL for all persistence, no SQLite
- ADR 0007: NATS JetStream as event bus
- ADR 0008: Traefik as gateway
- ADR 0009: Command and event envelopes, subject naming and de-duplication
- ADR 0010: Python 3.13 for all services and libraries
- ADR 0011: Separate GPL-3.0 package for the PyMammotion adapter
- ADR 0012: Mower control interface and safety rules
- ADR 0013: Device-Gateway registry, sharding and per-mower concurrency
- ADR 0014: Authorization model, tuple ownership and the mowdeck-authz client
- ADR 0015: Tokens, sessions and key rotation
- ADR 0016: Fleet commands and authorization per command kind
- ADR 0017: Telemetry storage and retention
- ADR 0018: Realtime WebSocket protocol
- ADR 0019: Mammotion adapter transports, manual drive and secrets
- ADR 0020: Web app architecture
- ADR 0021: Browser authentication flow
- ADR 0022: Notifications: rules, recipients and delivery
- ADR 0023: Map rendering and projection
- ADR 0024: Scheduler: recurrence, rain pause and command issuing
- ADR 0025: Home Assistant integration as a pure API client
- ADR 0026: Manual drive channel over the Realtime WebSocket
- ADR 0027: Health history and theft alarm
- ADR 0028: Entitlements: plans as data and enforcement
- ADR 0029: Helm chart and release pipeline
- ADR 0030: Mowing patterns as plug-ins and the Planner
- ADR 0031: Billing with a Stripe adapter
- ADR 0032: Live camera over Agora, joined directly by the browser
- ADR 0033: Vision-assisted recovery with human approval
- ADR 0034: Linked cloud accounts and device discovery
- ADR 0035: Server-side camera frames and automatic AI analysis on errors
- ADR 0036: Native app shells with Capacitor, loading the web app from the server
- ADR 0037: Camera panorama calibration in the browser, with the AI supplying points only
- ADR 0038: Dual link per mower and firmware updates
- ADR 0039: Spot mowing by MowDeck-driven paths
- ADR 0040: Link heartbeat for online status
- ADR 0041: Spot runs from the dock: undock, transit and return
- ADR 0042: Drive calibration
- ADR 0043: Turn styles
- ADR 0044: Drive view with camera
- ADR 0045: Camera relay
- ADR 0046: Device settings as capabilities
- ADR 0047: The RTK base station as a Fleet device
- ADR 0048: Maintenance commands
- ADR 0049: Manual driving over the Mammotion cloud
- ADR 0050: Passkeys and the app login hand-off
- ADR 0051: Sign in with GitHub
- ADR 0052: Sign in with Apple
- ADR 0053: Retention and downsampling of the position track
- ADR 0054: Aerial photo under the mower map
- ADR 0055: Deploy maintenance window
- ADR 0056: Camera photo in push notifications for errors and theft alarms
- ADR 0057: Step-up re-authentication for critical actions
- ADR 0058: Theft tracking mode
- ADR 0059: iOS widgets and the mowing Live Activity
- ADR 0060: Apple Watch app
- ADR 0061: Mowing angle rotation
- ADR 0062: Setup tour for new users
- ADR 0063: Matter server (mowers in Apple Home and Google Home)
- ADR 0064: Personal access tokens and the public API documentation
- Development
-
Operations
- Operating the Compose stack
- Operating MowDeck on Kubernetes (Helm)
- Building the Bluetooth bridge (ESPHome on a XIAO ESP32-S3)
- Releases
- Billing operations
- Telemetry: retention and position downsampling
- iPhone and Android apps
- Passkeys
- Sign in with GitHub
- Aerial photo under the map
- Sign in with Apple
- Theft tracking mode
- Matter server
- Source code offer (AGPL-3.0 section 13)
- Spikes
- Guides
- API reference
- Original plan (German)