Skip to main content

fmd_api: Python client for FMD (Find My Device)

Tests codecov PyPI - Downloads

Modern, async Python client for the open‑source FMD (Find My Device) server. It handles authentication, key management, encrypted data decryption, location/picture retrieval, and common device commands with safe, validated helpers.

Supports both FMD Server API v1 and API v2 / Protocol v2 (fmd-server >= 0.17.0). The client negotiates the account's protocol version automatically — v1 accounts and older servers keep working unchanged, while v2 accounts use the new Argon2id + HKDF + AES-GCM key schedule (end-to-end encryption with per-item data keys). New v2 accounts can be registered from the client via register_v2().

Install

  • Requires Python 3.8+
  • Stable (PyPI):
    pip install fmd_api
    

Quickstart

import asyncio, json
from fmd_api import FmdClient

async def main():
  # Recommended: async context manager auto-closes session
  async with await FmdClient.create("https://fmd.example.com", "alice", "secret", drop_password=True) as client:
    # Request a fresh GPS fix and wait a bit on your side
    await client.request_location("gps")

    # Fetch most recent locations and decrypt the latest
    blobs = await client.get_locations(num_to_get=1)
    # decrypt_data_blob() returns raw bytes — decode then parse JSON for clarity
    decrypted = client.decrypt_data_blob(blobs[0])
    loc = json.loads(decrypted.decode("utf-8"))
    print(loc["lat"], loc["lon"], loc.get("accuracy"))

    # Take a picture (validated helper)
    await client.take_picture("front")

asyncio.run(main())

TLS and self-signed certificates

HTTPS is strongly recommended for all connections to FMD server. HTTP is permitted for local development or trusted private networks, but should not be used in production. If you need to connect to a server with a self-signed certificate, you have two options:

  • Preferred (secure): provide a custom SSLContext that trusts your CA or certificate
  • Last resort (not for production): disable certificate validation explicitly

Examples:

import ssl
from fmd_api import FmdClient

# 1) Custom CA bundle / pinned cert (recommended)
ctx = ssl.create_default_context()
ctx.load_verify_locations(cafile="/path/to/your/ca.pem")

# Via constructor
client = FmdClient("https://fmd.example.com", ssl=ctx)

# Or via factory
# async with await FmdClient.create("https://fmd.example.com", "user", "pass", ssl=ctx) as client:

# 2) Disable verification (development only)
insecure_client = FmdClient("https://fmd.example.com", ssl=False)

Notes:

  • HTTPS is strongly recommended. Use HTTP only on trusted local networks or for development.
  • Prefer a custom SSLContext over disabling verification.
  • For higher security, consider pinning the server cert in your context.

Warning

Passing ssl=False disables TLS certificate validation and should only be used in development. For production, use a custom ssl.SSLContext that trusts your CA/certificate or pin the server certificate. Using http:// URLs sends credentials and data in plaintext — only use HTTP on trusted local networks or for development purposes.

If you're using a self-signed certificate and want to pin to that exact cert, load the server's PEM (or DER) directly into an SSLContext. This ensures only that certificate (or its CA) is trusted.

import ssl
from fmd_api import FmdClient

# Export your server's certificate to PEM (e.g., server-cert.pem)
ctx = ssl.create_default_context()
ctx.verify_mode = ssl.CERT_REQUIRED
ctx.check_hostname = True  # keep hostname verification when possible
ctx.load_verify_locations(cafile="/path/to/server-cert.pem")

client = FmdClient("https://fmd.example.com", ssl=ctx)
# async with await FmdClient.create("https://fmd.example.com", "user", "pass", ssl=ctx) as client:

Tips:

  • If the server cert changes, pinning will fail until you update the PEM.
  • For intermediate/CA signing chains, prefer pinning a private CA instead of the leaf.

What’s in the box

  • FmdClient (primary API)

    • Auth and key retrieval (salt → Argon2id → access token → private key retrieval and decryption)
    • Decrypt blobs (RSA‑OAEP wrapped AES‑GCM)
    • Fetch data: get_locations, get_pictures
    • Export: export_data_zip(out_path) — client-side packaging of all locations/pictures into ZIP (mimics web UI, no server endpoint)
    • Validated command helpers:
      • request_location("all|gps|cell|last")
      • take_picture("front|back")
      • set_bluetooth(enable: bool) — True = on, False = off
      • set_do_not_disturb(enable: bool) — True = on, False = off
      • set_ringer_mode("normal|vibrate|silent")

    Note: Device statistics functionality (get_device_stats()) has been temporarily removed and will be restored when the FMD server supports it (see fmd-server#74).

    • Low‑level: decrypt_data_blob(b64_blob)
  • Device helper (per‑device convenience)

    • await device.refresh() → hydrate cached state
    • await device.get_location() → parsed last location
    • await device.get_picture_blobs(n) + await device.decode_picture(blob)
    • await device.get_picture_metadata(n) -> returns only metadata dicts (if the server exposes them)

    IMPORTANT (breaking change in v2.0.5): legacy compatibility wrappers were removed. The following legacy methods were removed from the Device API: fetch_pictures, get_pictures, download_photo, get_picture, take_front_photo, and take_rear_photo. Update your code to use get_picture_blobs(), decode_picture(), take_front_picture() and take_rear_picture() instead.

    • Commands: await device.play_sound(), await device.take_front_picture(), await device.take_rear_picture(), await device.lock(message=None), await device.wipe(pin="YourSecurePIN", confirm=True) Note: wipe requires the FMD PIN (alphanumeric ASCII, no spaces) and must be enabled in the Android app's General settings. Future versions may enforce a 16+ character PIN length (fmd-android#379).

Example: Lock device with a message

import asyncio
from fmd_api import FmdClient, Device

async def main():
  client = await FmdClient.create("https://fmd.example.com", "alice", "secret")
  device = Device(client, "alice")
  # Optional message is sanitized (quotes/newlines removed, whitespace collapsed)
  await device.lock(message="Lost phone. Please call +1-555-555-1234")
  await client.close()

asyncio.run(main())

Example: Inspect pictures metadata (when available)

Use get_picture_blobs() to fetch the raw server responses (strings or dicts). If you want a strongly-typed list of picture metadata objects (where the server provides metadata as JSON objects), use get_picture_metadata(), which filters for dict entries and returns only those.

from fmd_api import FmdClient, Device

async def inspect_metadata():
  client = await FmdClient.create("https://fmd.example.com", "alice", "secret")
  device = Device(client, "alice")

  # Raw values may be strings (base64 blobs) or dicts (metadata). Keep raw when you need
  # to decode or handle both forms yourself.
  raw = await device.get_picture_blobs(10)

  # If you want only metadata entries returned by the server, use get_picture_metadata().
  # This returns a list of dict-like metadata objects (e.g. id/date/filename) and filters
  # out any raw string blobs.
  metadata = await device.get_picture_metadata(10)
  for m in metadata:
    print(m.get("id"), m.get("date"))

  await client.close()

asyncio.run(inspect_metadata())

Testing

Functional tests

Runnable scripts under tests/functional/:

  • test_auth.py – basic auth smoke test
  • test_locations.py – list and decrypt recent locations
  • test_pictures.py – list and download/decrypt a photo
  • test_device.py – device helper flows
  • test_commands.py – validated command wrappers (no raw strings)
  • test_export.py – export data to ZIP
  • test_request_location.py – request location and poll for results

Put credentials in tests/utils/credentials.txt (copy from credentials.txt.example).

Unit tests

Located in tests/unit/:

  • test_client.py – client HTTP flows with mocked responses
  • test_device.py – device wrapper logic

Run with pytest:

pip install -e ".[dev]"
pytest tests/unit/

API highlights

  • Encryption compatible with FMD web client
    • RSA‑3072 OAEP (SHA‑256) wrapping AES‑GCM session key
    • AES‑GCM IV: 12 bytes; RSA packet size: 384 bytes
  • Password/key derivation with Argon2id
  • Robust HTTP JSON/text fallback and 401 re‑auth
    • Supports password-free resume via exported auth artifacts (hash + token + private key)

Advanced: Password-Free Resume

You can onboard once with a raw password, optionally discard it immediately using drop_password=True, export authentication artifacts, and later resume without storing the raw secret:

client = await FmdClient.create(url, fmd_id, password, drop_password=True)
artifacts = await client.export_auth_artifacts()

# Persist `artifacts` securely (contains hash, token, private key)

# Later / after restart
client2 = await FmdClient.from_auth_artifacts(artifacts)
locations = await client2.get_locations(1)

On a 401, the client will transparently reauthenticate using the stored Argon2id password_hash if available. When drop_password=True, the raw password is never retained after initial onboarding.

Troubleshooting

  • "Blob too small for decryption": server returned empty/placeholder data. Skip and continue.
  • Pictures may be double‑encoded (encrypted blob → base64 image string). The examples show how to decode safely.

Credits

This client targets the FMD ecosystem:

MIT © 2025 Devin Slick

Metadata

Release files for fmd-api 3.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for fmd-api 3.1.0
File Size Uploaded
fmd_api-3.1.0.tar.gz 35.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for fmd-api 3.1.0
File Interpreter ABI Platform
fmd_api-3.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 68.7 kB

Release files / fmd_api-3.1.0.tar.gz

Download URL fmd_api-3.1.0.tar.gz
Size 35.2 kB
Tags Source
SHA-256 checksum
How to use checksums
30be00180623de7c1a5bbf0654e6fbdd5e1e38bafc2bb4875d4de2f72adf99c2
BLAKE2b-256 checksum
How to use checksums
1e55e7bf864fabc6102585a18e8e9f86795306f9051d1e65a4acb95e83aa8edf
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release files / fmd_api-3.1.0-py3-none-any.whl

Download URL fmd_api-3.1.0-py3-none-any.whl
Size 33.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
407f8636dd6591bd3127244d67501e7373df5b8d62d5180d54103c309013eea1
BLAKE2b-256 checksum
How to use checksums
46ee3ce663ebc4545d57bfe3d1204becf74dcef36e5ed4f270cb653101bb5a60
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 1, 2026.

Transparency log

Release history Release notifications | RSS feed

3.2.0

2 release files

3.1.4

2 release files

3.1.3

2 release files

3.1.2

2 release files

3.1.1

2 release files

This release

3.1.0 This release

2 release files

3.0.1

2 release files

3.0.0

2 release files

2.0.10

2 release files

2.0.9

2 release files

2.0.8

2 release files

2.0.7

2 release files

2.0.6

2 release files

2.0.5

2 release files

2.0.4

2 release files

2.0.3

2 release files

2.0.2

2 release files

2.0.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page