{"id":37066503,"url":"https://github.com/devinslick/fmd_api","last_synced_at":"2026-01-14T07:48:30.703Z","repository":{"id":320301647,"uuid":"1081579578","full_name":"devinslick/fmd_api","owner":"devinslick","description":"A simple python client for interacting with devices connected to FMD (fmd-foss)","archived":false,"fork":false,"pushed_at":"2025-11-16T19:19:30.000Z","size":285,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-11-16T21:11:38.098Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/devinslick.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-10-23T01:34:25.000Z","updated_at":"2025-11-16T19:16:42.000Z","dependencies_parsed_at":"2025-10-23T03:36:18.277Z","dependency_job_id":"13178d8c-ac2d-4a65-a59f-78bf534510f8","html_url":"https://github.com/devinslick/fmd_api","commit_stats":null,"previous_names":["devinslick/fmd_api"],"tags_count":4,"template":false,"template_full_name":null,"purl":"pkg:github/devinslick/fmd_api","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devinslick%2Ffmd_api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devinslick%2Ffmd_api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devinslick%2Ffmd_api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devinslick%2Ffmd_api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/devinslick","download_url":"https://codeload.github.com/devinslick/fmd_api/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/devinslick%2Ffmd_api/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28413497,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-14T05:26:33.345Z","status":"ssl_error","status_checked_at":"2026-01-14T05:21:57.251Z","response_time":107,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-14T07:48:30.041Z","updated_at":"2026-01-14T07:48:30.695Z","avatar_url":"https://github.com/devinslick.png","language":"Python","readme":"# fmd_api: Python client for FMD (Find My Device)\n\n[![Tests](https://github.com/devinslick/fmd_api/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/devinslick/fmd_api/actions/workflows/test.yml)\n[![codecov](https://codecov.io/gh/devinslick/fmd_api/branch/main/graph/badge.svg?token=8WA2TKXIOW)](https://codecov.io/gh/devinslick/fmd_api)\n[![PyPI - Downloads](https://img.shields.io/pypi/dm/fmd-api)](https://pypi.org/project/fmd-api/)\n\nModern, 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.\n\n## Install\n\n- Requires Python 3.8+\n- Stable (PyPI):\n  ```bash\n  pip install fmd_api\n  ```\n\u003c!-- Pre-release via TestPyPI removed. Use stable releases from PyPI or GitHub Releases for pre-release artifacts --\u003e\n\n## Quickstart\n\n```python\nimport asyncio, json\nfrom fmd_api import FmdClient\n\nasync def main():\n  # Recommended: async context manager auto-closes session\n  async with await FmdClient.create(\"https://fmd.example.com\", \"alice\", \"secret\", drop_password=True) as client:\n    # Request a fresh GPS fix and wait a bit on your side\n    await client.request_location(\"gps\")\n\n    # Fetch most recent locations and decrypt the latest\n    blobs = await client.get_locations(num_to_get=1)\n    # decrypt_data_blob() returns raw bytes — decode then parse JSON for clarity\n    decrypted = client.decrypt_data_blob(blobs[0])\n    loc = json.loads(decrypted.decode(\"utf-8\"))\n    print(loc[\"lat\"], loc[\"lon\"], loc.get(\"accuracy\"))\n\n    # Take a picture (validated helper)\n    await client.take_picture(\"front\")\n\nasyncio.run(main())\n```\n\n### TLS and self-signed certificates\n\nFind My Device always requires HTTPS; plain HTTP is not allowed by this client. If you need to connect to a server with a self-signed certificate, you have two options:\n\n- Preferred (secure): provide a custom SSLContext that trusts your CA or certificate\n- Last resort (not for production): disable certificate validation explicitly\n\nExamples:\n\n```python\nimport ssl\nfrom fmd_api import FmdClient\n\n# 1) Custom CA bundle / pinned cert (recommended)\nctx = ssl.create_default_context()\nctx.load_verify_locations(cafile=\"/path/to/your/ca.pem\")\n\n# Via constructor\nclient = FmdClient(\"https://fmd.example.com\", ssl=ctx)\n\n# Or via factory\n# async with await FmdClient.create(\"https://fmd.example.com\", \"user\", \"pass\", ssl=ctx) as client:\n\n# 2) Disable verification (development only)\ninsecure_client = FmdClient(\"https://fmd.example.com\", ssl=False)\n```\n\nNotes:\n- HTTP (http://) is rejected. Use only HTTPS URLs.\n- Prefer a custom SSLContext over disabling verification.\n- For higher security, consider pinning the server cert in your context.\n\n\u003e Warning\n\u003e\n\u003e 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. The client enforces HTTPS and rejects `http://` URLs.\n\n#### Pinning the exact server certificate (recommended for self-signed)\n\nIf 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.\n\n```python\nimport ssl\nfrom fmd_api import FmdClient\n\n# Export your server's certificate to PEM (e.g., server-cert.pem)\nctx = ssl.create_default_context()\nctx.verify_mode = ssl.CERT_REQUIRED\nctx.check_hostname = True  # keep hostname verification when possible\nctx.load_verify_locations(cafile=\"/path/to/server-cert.pem\")\n\nclient = FmdClient(\"https://fmd.example.com\", ssl=ctx)\n# async with await FmdClient.create(\"https://fmd.example.com\", \"user\", \"pass\", ssl=ctx) as client:\n```\n\nTips:\n- If the server cert changes, pinning will fail until you update the PEM.\n- For intermediate/CA signing chains, prefer pinning a private CA instead of the leaf.\n\n## What’s in the box\n\n- `FmdClient` (primary API)\n  - Auth and key retrieval (salt → Argon2id → access token → private key retrieval and decryption)\n  - Decrypt blobs (RSA‑OAEP wrapped AES‑GCM)\n  - Fetch data: `get_locations`, `get_pictures`\n  - Export: `export_data_zip(out_path)` — client-side packaging of all locations/pictures into ZIP (mimics web UI, no server endpoint)\n  - Validated command helpers:\n    - `request_location(\"all|gps|cell|last\")`\n    - `take_picture(\"front|back\")`\n    - `set_bluetooth(enable: bool)` — True = on, False = off\n    - `set_do_not_disturb(enable: bool)` — True = on, False = off\n    - `set_ringer_mode(\"normal|vibrate|silent\")`\n\n  \u003e **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](https://gitlab.com/fmd-foss/fmd-server/-/issues/74)).\n\n  - Low‑level: `decrypt_data_blob(b64_blob)`\n\n- `Device` helper (per‑device convenience)\n  - `await device.refresh()` → hydrate cached state\n  - `await device.get_location()` → parsed last location\n  - `await device.get_picture_blobs(n)` + `await device.decode_picture(blob)`\n  - `await device.get_picture_metadata(n)` -\u003e returns only metadata dicts (if the server exposes them)\n\n  IMPORTANT (breaking change in v2.0.5): legacy compatibility wrappers were removed.\n  The following legacy methods were removed from the `Device` API: `fetch_pictures`,\n  `get_pictures`, `download_photo`, `get_picture`, `take_front_photo`, and `take_rear_photo`.\n  Update your code to use `get_picture_blobs()`, `decode_picture()`, `take_front_picture()`\n  and `take_rear_picture()` instead.\n  - Commands: `await device.play_sound()`, `await device.take_front_picture()`,\n    `await device.take_rear_picture()`, `await device.lock(message=None)`,\n    `await device.wipe(pin=\"YourSecurePIN\", confirm=True)`\n    Note: wipe requires the FMD PIN (alphanumeric ASCII, no spaces) and must be enabled in the Android app's General settings.\n    Future versions may enforce a 16+ character PIN length ([fmd-android#379](https://gitlab.com/fmd-foss/fmd-android/-/merge_requests/379)).\n\n### Example: Lock device with a message\n\n```python\nimport asyncio\nfrom fmd_api import FmdClient, Device\n\nasync def main():\n  client = await FmdClient.create(\"https://fmd.example.com\", \"alice\", \"secret\")\n  device = Device(client, \"alice\")\n  # Optional message is sanitized (quotes/newlines removed, whitespace collapsed)\n  await device.lock(message=\"Lost phone. Please call +1-555-555-1234\")\n  await client.close()\n\nasyncio.run(main())\n```\n\n### Example: Inspect pictures metadata (when available)\n\nUse `get_picture_blobs()` to fetch the raw server responses (strings or dicts). If you want a\nstrongly-typed list of picture metadata objects (where the server provides metadata as JSON\nobjects), use `get_picture_metadata()`, which filters for dict entries and returns only those.\n\n```python\nfrom fmd_api import FmdClient, Device\n\nasync def inspect_metadata():\n  client = await FmdClient.create(\"https://fmd.example.com\", \"alice\", \"secret\")\n  device = Device(client, \"alice\")\n\n  # Raw values may be strings (base64 blobs) or dicts (metadata). Keep raw when you need\n  # to decode or handle both forms yourself.\n  raw = await device.get_picture_blobs(10)\n\n  # If you want only metadata entries returned by the server, use get_picture_metadata().\n  # This returns a list of dict-like metadata objects (e.g. id/date/filename) and filters\n  # out any raw string blobs.\n  metadata = await device.get_picture_metadata(10)\n  for m in metadata:\n    print(m.get(\"id\"), m.get(\"date\"))\n\n  await client.close()\n\nasyncio.run(inspect_metadata())\n```\n\n## Testing\n\n### Functional tests\n\nRunnable scripts under `tests/functional/`:\n\n- `test_auth.py` – basic auth smoke test\n- `test_locations.py` – list and decrypt recent locations\n- `test_pictures.py` – list and download/decrypt a photo\n- `test_device.py` – device helper flows\n- `test_commands.py` – validated command wrappers (no raw strings)\n- `test_export.py` – export data to ZIP\n- `test_request_location.py` – request location and poll for results\n\nPut credentials in `tests/utils/credentials.txt` (copy from `credentials.txt.example`).\n\n### Unit tests\n\nLocated in `tests/unit/`:\n- `test_client.py` – client HTTP flows with mocked responses\n- `test_device.py` – device wrapper logic\n\nRun with pytest:\n```bash\npip install -e \".[dev]\"\npytest tests/unit/\n```\n\n## API highlights\n\n- Encryption compatible with FMD web client\n  - RSA‑3072 OAEP (SHA‑256) wrapping AES‑GCM session key\n  - AES‑GCM IV: 12 bytes; RSA packet size: 384 bytes\n- Password/key derivation with Argon2id\n- Robust HTTP JSON/text fallback and 401 re‑auth\n  - Supports password-free resume via exported auth artifacts (hash + token + private key)\n\n### Advanced: Password-Free Resume\n\nYou 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:\n\n```python\nclient = await FmdClient.create(url, fmd_id, password, drop_password=True)\nartifacts = await client.export_auth_artifacts()\n\n# Persist `artifacts` securely (contains hash, token, private key)\n\n# Later / after restart\nclient2 = await FmdClient.from_auth_artifacts(artifacts)\nlocations = await client2.get_locations(1)\n```\n\nOn 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.\n\n## Troubleshooting\n\n- \"Blob too small for decryption\": server returned empty/placeholder data. Skip and continue.\n- Pictures may be double‑encoded (encrypted blob → base64 image string). The examples show how to decode safely.\n\n## Credits\n\nThis client targets the FMD ecosystem:\n\n- https://fmd-foss.org/\n- https://gitlab.com/fmd-foss\n- Public community instance: https://server.fmd-foss.org/\n - Listed on the official FMD community page: https://fmd-foss.org/docs/fmd-server/community\n\nMIT © 2025 Devin Slick\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevinslick%2Ffmd_api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdevinslick%2Ffmd_api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdevinslick%2Ffmd_api/lists"}