Skip to main content
Glama
README.md
![Garmin Local Archive](src/screenshots/Banner_2.jpg)

![Platform](https://img.shields.io/badge/platform-Windows-blue)
![License](https://img.shields.io/badge/license-GPLv3-green)
![No Cloud](https://img.shields.io/badge/cloud-none-brightgreen)
![Release](https://img.shields.io/github/v/release/Wewoc/Garmin_Local_Archive)

**Archive and analyze your Garmin Health data local.**

*Privacy first — inspired by European principles.*

---

## A look at the app

<a href="src/screenshots/GUI-Page_1.jpg"><img src="src/screenshots/GUI-Page_1.jpg" width="150" title="Home tab — fixed top area with connection status, archive stats by device, and Daily Actions (Sync / Mirror / Timer); dashboard viewer below."></a>
<a href="src/screenshots/GUI-Page_2.jpg"><img src="src/screenshots/GUI-Page_2.jpg" width="150" title="Files tab — in-app XLSX viewer with date, steps, resting HR, body battery, sleep duration and quality per day."></a>
<a href="src/screenshots/GUI-Page_3.jpg"><img src="src/screenshots/GUI-Page_3.jpg" width="150" title="Settings tab — two-column layout: credentials, paths, and sync config on the left; sync controls, timer, mirror, and output options on the right."></a>
<a href="src/screenshots/GUI-Page_4.jpg"><img src="src/screenshots/GUI-Page_4.jpg" width="150" title="Chat tab — chat against a local Ollama model or a Cloud LLM (Anthropic/OpenAI), with optional live tool-calling against your archive via the MCP server for questions the daily summary alone can't answer. Chat sessions can be saved and resumed."></a>
<a href="src/screenshots/GUI-Page_5.jpg"><img src="src/screenshots/GUI-Page_5.jpg" width="150" title="MCP Server tab — backend, port and headless-mode selection, Save Settings and Start MCP Server buttons."></a>

<sub>Home · Files · Settings · Chat · MCP Server — click any tab to view full size.</sub>

---

## Download

| Version | Description | Requires |
|---|---|---|
| [Garmin_Local_Archive_Standalone.zip](https://github.com/Wewoc/Garmin_Local_Archive/releases/latest) | **Recommended — no setup needed** | Nothing |
| [Garmin_Local_Archive.zip](https://github.com/Wewoc/Garmin_Local_Archive/releases/latest) | Standard version | Python 3.10+ |

No install, no terminal. Download, unzip, run.
Standard version: install dependencies first — `pip install -r requirements.txt`.

**Both versions (v1.7.2.4):** an **Update** button appears next to the
usual update notification once a new version is available — downloads,
verifies, and applies it in place. Verified against a published checksum
before anything is touched; if that check fails, nothing is changed.
There's also an opt-in "Auto-apply updates in Daily Sync" setting for
unattended updates via the scheduled daily sync. **(v1.7.2.4.1)** the
Standalone version restarts itself automatically afterward, and if the
new version fails to start, it's automatically rolled back to the
previous working version — no manual recovery needed. The standard
version applies the update the same way it always has: it closes, and
you start it again yourself.

---

## Try it first

No Garmin account yet, or just want to see it in action? A synthetic
180-day demo archive (fictional data, clearly marked as such) lets you
explore dashboards, Chat and the MCP server without connecting a real
account. See [QUICKSTART.txt](src/docs/QUICKSTART.txt), "Try it first".

---

## Project status & disclaimer

> GNU General Public License v3.0 — provided as-is, without warranty of any kind, express or implied.

- **Not an official Garmin product:** This tool is not affiliated with, endorsed, or supported by Garmin.
- **Not medical advice:** All health metrics, reference ranges, and dashboard data are for personal informational use only — not a substitute for medical advice.
- **AI and health data — handle with care:** If you use an external AI service (ChatGPT, Claude, Gemini) to interpret your data: never upload documents containing your name, date of birth, or other identifying information. Cloud AI services store what you send — linked to your account. Use a local model (Ollama) or at minimum a session without login. This includes the app's own in-app **Chat** tab — its optional Cloud backend (Anthropic/OpenAI, v1.7.2) sends your question and any archive data the model requests to that provider's API, same as pasting into their website; the Ollama backend stays fully local, always. AI responses on health topics are statistically generated — not medically validated. Treat them as a first orientation, not a conclusion.
- **Context data:** Weather data is provided by Open-Meteo and Brightsky (DWD), pollen data and air quality data by Open-Meteo — accuracy and availability are not guaranteed. Air quality data (CAMS dataset) is available from approximately 2020 onwards.
- **Early stage:** Core functionality is stable. APIs and internal structure may still change.
- **No guaranteed support:** Development happens when time and interest allow.
- **Use at your own risk:** I am not responsible for data loss or Garmin account issues.
- **Feedback welcome:** If something feels off — logic, structure, results — open an issue.

**Scope & limitations:** Local-first, personal use, no enterprise ambitions.
- Relies on python-garminconnect. Since Garmin does not offer a public API for personal use, moste non-commercial tools share this dependency. To mitigate unannounced upstream changes, GLA automatically detects and logs structural API shifts.
- Local test suites cover the full pipeline plus a separate build-output validation suite — no automated build/test CI yet; CodeQL security scanning runs via GitHub Actions on every push/PR to main
- HTML dashboards require a one-time internet connection to download Plotly (~3 MB) — cached locally after that
- Per-day checkpointing: an interrupted sync resumes from the last completed day, no full re-sync required
- Historical data quality depends on Garmin servers

This project is built for my own use. If it happens to be useful to others, feel free to use it — but evaluate it like any other unverified open-source tool.

**What this is not:**
Garmin Connect is still required — the app pulls data from there via API.

**A note on cloud folders:** the archive itself is stored as plaintext on disk — if `garmin_data/` lives inside a cloud-synced folder, that data gets uploaded automatically. See [SECURITY.md](SECURITY.md#container-security) for details and the encrypted Mirror alternative. This tool does not replace Connect, the Garmin app, or your device sync. It has no cloud component, no remote access, and no sharing features. The GUI and EXE are Windows-only.

---

## Why this exists

I wanted to ask an AI questions about my health data without sending that data to another cloud service. So I built a local alternative instead.

There's a second reason that matters more over time: Garmin silently degrades intraday data resolution. Empirical analysis of archive data (April–June 2026) shows the threshold at approximately 120–135 days, and appears to trend lower over time. Once full resolution is lost, it's gone permanently. This tool exists to capture it while it's still available.

What "intraday resolution" actually means in practice:

| Metric | API resolution | Data points / day |
|---|---|---|
| Heart Rate | ~1 minute | up to 1,440 |
| Stress | ~3 minutes | up to 480 |
| Body Battery | ~15 minutes | up to 96 |
| SpO2 | ~1 hour | up to 24 |
| Respiration | variable | variable |

After ~120 days, Garmin stops serving this data entirely. The daily summary (resting HR, average stress, etc.) remains — but the curves, the detail, the full timeline: gone. GLA captures it while it's still there.

*→ For the full story, see [MINDSET.md](src/docs/MINDSET.md).*

---

## What makes this different

This project is as much a statement as it is a tool.

This is not a data export script — it maintains a complete, consistent
local copy of your Garmin data over time. Your data stays in open formats, readable and analyzable with any tool you choose. Local AI, cloud AI, or no AI at all. **Your data, your call.**

| Feature | Garmin Connect | Cloud-AI Bridges | **Garmin Local Archive** |
| :--- | :--- | :--- | :--- |
| **Data storage** | Garmin servers (USA) | US AI servers | **Your machine** |
| **Privacy risk** | Medium | High (training data risk) | **Minimal** |
| **Access** | Online only | Requires subscription | **100% offline** |
| **History** | Erodes over time | Depends on source | **Permanent local copy** |

---

## AI-assisted development

I can't write Python. The architecture, module boundaries, and decisions are mine. Every line of code is Claude's.

*→ How this collaboration actually worked — who had which idea, where Claude was wrong — is documented in [MINDSET.md](src/docs/MINDSET.md).*

---

## How it works

The app works in two modes: **live sync** pulls recent data directly from Garmin Connect via API; **Bulk Import** loads your complete history from a Garmin GDPR export ZIP — this is the primary path for recovering years of data that the API no longer serves.

Everything is stored locally in structured formats (JSON, Excel, HTML dashboards). Once downloaded, nothing is transmitted anywhere.

---

### Bulk Import

The GDPR export from Garmin contains your complete daily history — but in our testing, no intraday data was found (no heart rate curves, no stress timelines, no body battery graphs). That resolution appears to be available only through the API, and only for recent days.

The **Bulk Import** feature fills in the rest: request your full data export from Garmin (typically ready in 20–30 minutes), point the app at the ZIP, and your complete daily history lands in the local archive — in the same format as live API data. Days already present with good quality are skipped automatically.

---

### Dashboards

The built-in dashboards cover roughly 90% of what most users are looking for — without any AI at all. For deeper analysis, your data is prepared in a format any local AI can work with directly.

| Dashboard | What it shows | Output |
|---|---|---|
| **Health Analysis** | HRV, Resting HR, SpO2, Sleep, Body Battery, Stress — daily values vs 90-day personal baseline vs age/fitness-adjusted reference ranges. Flags days outside range. | HTML, Mobile HTML, JSON + AI prompt |
| **Timeseries** | Intraday heart rate, stress, SpO2, body battery and respiration as zoomable charts across any date range. | HTML, Excel |
| **Heatmap** | Six intraday metrics (Heart Rate, Steps, Stress, Body Battery, SpO2, Respiration) as time-of-day × date grids — spot daily rhythms and irregularities at a glance. | HTML |
| **Daily Overview** | All summary fields in one flat table, one row per day. | Excel |
| **Health + Context** | Garmin health metrics alongside local weather and pollen data. | HTML, Excel |
| **Sleep Dashboard** | One row per night — segmented phase bar (Deep / Light / REM / Awake), sleep duration, score, quality badge, feedback label, HRV, Body Battery, and **7-day HRV moving average** (computed from archive, no extra API call). Color-coded numbers via continuous gradient against personal reference ranges. Inspired by [Garmin's own HRV pattern guide](https://www.garmin.com/en-US/blog/fitness/understanding-the-hrv-status-on-your-garmin-smartwatch/). | HTML, Excel |
| **Sleep & Recovery** | HRV, Body Battery, Sleep duration and phase breakdown (Deep / Light / REM / Awake) alongside weather and pollen context. Intraday detail per day. | HTML |
| **Explorer** | Free metric exploration — choose up to 4 metrics from all Garmin daily fields plus weather, pollen, and air quality on a shared time axis. Sleep phase breakdown and sleep quality log included. Built-in field descriptions and air quality interpretation guide. | HTML |
| **Custom Dashboard** | Pick any combination of Garmin daily fields and Context fields, set a date range, and build a one-off dashboard — no fixed field list, no specialist file written to disk. Field selections can be saved as named presets for reuse. Optional AES-256 encryption for the HTML output. | HTML, Excel |

<img src="src/screenshots/Create_report.jpg" width="800" alt="Garmin Local Archive — Create Report">
<br><sub>Create Reports — select dashboards and export as HTML, Excel or JSON.</sub>

<img src="src/screenshots/Dashboard.jpg" width="800" alt="Garmin Health Analysis Dashboard">
<br><sub>Analysis dashboard — daily values vs 90-day personal baseline vs age/fitness-adjusted reference ranges.</sub>

<img src="src/screenshots/sleep_dashboard.jpg" width="800" alt="Garmin Health Analysis Dashboard">
<br><sub>One row per night — segmented phase bar, duration, sleep score, quality badge, Garmin feedback text, HRV, and Body Battery. Numbers are color-coded against personal reference ranges.</sub>

<img src="src/screenshots/dashboard_mobile_landscape.jpg" width="800" alt="Garmin Health Analysis Dashboard">
<br><sub>Analysis dashboard mobile version — daily values vs 90-day personal baseline vs age/fitness-adjusted reference ranges.</sub>

**Live Tracking** (v1.6.5) — a separate, always-current view: today's progression (Body Battery, Heart Rate, Steps, Stress) plus last night's sleep summary, refreshed automatically after every sync and on demand via an "Update Live" button in the Home tab. Not part of the Create Reports selection above — it has its own trigger, by design.

---

### Chat

A native chat panel is built into the app (**Chat** tab, v1.6.6, renamed
from "Ollama-Chat" in v1.7.2) — no separate setup beyond having Ollama
itself installed and a model pulled, or a Cloud API key configured. Two
independent choices, made before Start:

- **Backend** — a local Ollama model (fully local, no setup beyond Ollama
  itself), or a Cloud LLM (Anthropic or OpenAI, v1.7.2 — requires an API
  key, configured on the **MCP Server** tab; see the AI disclaimer above
  for what that means for your data).
- **Source** — the archived daily-summary snapshot (fast, no live query,
  the original v1.6.6 behavior), or live **MCP tool-calling** (v1.7.2) —
  the model queries your archive on demand via the same MCP server external
  tools use, for questions the daily snapshot alone can't answer.

Replies stream in as they're generated for every combination except
Ollama + MCP tool-calling (Ollama's own streaming support for tool-calling
is not yet reliable upstream, so that one path waits for the complete
answer). Chat sessions auto-save and can be reloaded later via the
**Chat History** button — a session using live MCP data can always be
resumed; a session using the daily snapshot can only be resumed if that
snapshot hasn't changed since, otherwise it opens read-only. For connecting
external tools (Open WebUI, AnythingLLM, Claude Desktop) for more advanced
document/RAG workflows instead, see [`docs/README_APP.md`](src/docs/README_APP.md) and the **MCP
Server** section below.

---

### MCP Server

Garmin Local Archive exposes your archive to local LLMs via the [Model
Context Protocol](https://modelcontextprotocol.io/) — ask an LLM about your
health and context data directly, without exporting files or copy-pasting
into a chat window. Runs as its own standalone process, independent of the
main app.

Two ways to run it: a **▶️ Start MCP Server** button on the app's own
**MCP Server** tab (uses the same archive path and settings as the rest of
the app), or the standalone `mcp_server.exe` — a self-contained tool with
its own small window, usable even without the main app installed.

<img src="src/screenshots/GUI-MCP_1.jpg" width="500" alt="Garmin Local Archive — Standalone MCP Server">
<br><sub>Standalone MCP server window (<code>mcp_server.exe</code>) — runs independently of the main app, with its own archive path, backend selection, and live log.</sub>

Running Open WebUI (or any other MCP client) inside Docker? An opt-in
"Extra allowed hosts" field on the MCP Server tab (v1.7.0.2) lets you add
`host.docker.internal` — pre-filled by default — to the server's allowed-
host list, since the underlying MCP SDK otherwise rejects connections
that don't arrive as `127.0.0.1`/`localhost`.

Works with Ollama (fully local, default) or an optional cloud LLM backend —
your choice, no default push toward either. Point your MCP-compatible
client (Claude Desktop, Open WebUI, or similar) at the server and start
asking questions.

**Connect:** Streamable HTTP at `http://127.0.0.1:<port>/mcp` (default
port `8756`, configurable on the MCP Server tab) — bound to localhost
only, not reachable from other machines by default. See "Extra allowed
hosts" above for Docker clients.

**Tools exposed** (see [`docs/REFERENCE_MCP.md`](src/docs/REFERENCE_MCP.md) for full signatures):

| Tool | What it does |
|---|---|
| `query_health` | Daily health metrics (e.g. sleep, stress, heart rate, body battery) for a date or date range |
| `query_context` | Weather, pollen and air-quality data for a date or date range |
| `query_fit_activities` | FIT activity data — *planned, not yet available* (see `docs/ROADMAP.md`, "FIT Pipeline") |
| `query_raw` | Unprocessed archive data for a given domain, for deeper inspection |
| `get_archive_metadata` | Archive coverage and quality info (date ranges, missing days, data quality) |
| `list_available_fields` | Which fields the archive actually contains, per domain |
| `refresh_cache` | Rebuilds the server's local query cache on demand, e.g. right after a sync |

Example: *"How did my sleep and stress compare over the last two weeks?"*
— the model calls the tools above itself and answers from your real
archive, no manual export needed. No archive yet? See
[Try it first](#try-it-first) above.

---

## Architecture

### Token security & Login

Garmin login works via SSO — logging in with email and password on every
run triggers Captcha or MFA. The solution: log in once manually, and
Garmin returns an OAuth token that handles all subsequent runs for
approximately one year. This token is equivalent to a logged-in session
and must not sit unprotected on disk.

The token is encrypted at rest. Details on the encryption design and threat model: [SECURITY.md](SECURITY.md)

---

### Pipeline

Live sync and Bulk Import both flow through the same validation and quality pipeline before anything lands in the local archive — the diagram below shows the full picture.

> [!TIP]
> **Pipeline Architecture:** For a detailed view of the v1.3.4 data flow including the validation layer and self-healing loop, open [screenshots/flowchart_v134.html](src/screenshots/flowchart_v134.html) in your browser.

---

### System Architecture

The diagram below shows how all components relate to each other as of v1.6.x — from API ingestion and context collection through the broker layer to dashboard export. The broker layer also includes `gateway_map` (v1.6.7), a cross-domain routing layer used by the local MCP server (v1.7, see above) among other consumers — existing dashboards are unaffected and continue to query `health_map`/`context_map` directly.

![System Architecture v1.6.x](src/screenshots/data_flow.png)

---

### What is included

The project is structured into five focused layers — Garmin pipeline, Context pipeline, Data brokers, Dashboard layer, Desktop app. Each layer has a single responsibility — collect, validate, assess, broker, or render. No crossover between layers.

The diagram above shows how the layers connect. Each module is self-contained and designed to be extended — for a script-by-script reference (what each module does, owns, and how to add new ones), see [`docs/MAINTENANCE_GLOBAL.md`](src/docs/MAINTENANCE_GLOBAL.md).

The desktop app includes a **Background Timer** — once started, it automatically repairs failed/incomplete days, upgrades bulk-imported days within Garmin's intraday resolution window (~120 days), fills missing days, keeps a raw API-response backup current, retroactively adds newly supported data fields (like step count) to already-archived days, and re-fetches bulk-imported days for the handful of data fields the official Garmin export never contains at all (HRV, SpO2, Body Battery, respiration, training status, race predictions, max metrics) — regardless of how old the day is, since that gap has nothing to do with intraday resolution — with no further manual steps in between. The timer must be started manually and only runs while the app is open — it does not resume automatically after a restart.

Data is stored in two root folders:

```
garmin_data/
├── raw/        – complete API dumps (~500 KB/day) — permanent archive / basis for dashboards and analysis
├── source/     – unmodified API responses (~250 KB/day) — replay-safe intraday backup
├── summary/    – compact daily JSONs (~2 KB/day)  — basis for dashboards and analysis
└── log/        – session logs, quality register, encrypted token

context_data/
├── weather/summary/     – daily weather archive (Open-Meteo)
├── pollen/summary|raw/  – daily / hourly pollen archive (Open-Meteo Air Quality)
├── brightsky/summary|raw/    – daily / hourly weather archive (Brightsky DWD)
└── airquality/summary|raw/   – daily / hourly air quality archive (Open-Meteo Air Quality)
```

---

See [`docs/MAINTENANCE_GLOBAL.md`](src/docs/MAINTENANCE_GLOBAL.md) for full technical documentation, how to add new fields, troubleshooting, and developer notes.

---

## Testing

Sixteen test suites cover the full pipeline — no network, no API required
(the Chat tab's Cloud LLM/MCP-tool-calling tests mock every SDK/HTTP call,
same as everything else here):

```bash
python tests/test_local.py                    # Garmin pipeline
python tests/test_local_context.py            # Context pipeline (external APIs mocked)
python tests/test_dashboard.py                # Dashboard pipeline
python tests/test_broker.py                   # Broker layer (health_map / gateway_map routing, metadata_map)
python tests/test_mcp.py                      # MCP layer (mcp_map protocol translation)
python tests/test_app_logic.py                # App layer (entry points, path resolution)
pytest tests/test_qt_app.py                   # PyQt6 App layer
python tests/test_static.py                   # ruff + bandit + regression guards
python tests/test_build_output.py             # Build output validation (run after build)
pytest tests/test_cloud_llm.py                # Chat tab — Cloud LLM connector (v1.7.2)
pytest tests/test_mcp_tool_chat.py            # Chat tab — Ollama + MCP tool-calling (v1.7.2)
pytest tests/test_cloud_tool_chat.py          # Chat tab — Cloud + MCP tool-calling, streaming (v1.7.2)
pytest tests/test_chat_session_store.py       # Chat tab — session save/load/resume (v1.7.2)
pytest tests/test_cloud_credential_store.py   # Chat tab — API key storage in Windows Credential Manager (v1.7.2)
pytest tests/test_mcp_process.py              # MCP Server Start/Stop process control (v1.7.2)
python tests/test_updater.py                  # T2 + T3 self-updater (v1.7.2.4, v1.7.2.4.1)
```

`build_all.py` runs `test_local.py`, `test_local_context.py`, `test_dashboard.py`, `test_broker.py`, and `test_static.py` as pre-build gates — a failing test aborts the build before either target is built. `test_build_output.py` and `test_app_logic.py` run automatically after both builds complete, as post-build gates. `test_qt_app.py` and the six Chat-tab/MCP-process suites above are run manually via `pytest`.

GUI changes are verified manually before release. Full CI/CD with automated builds and release packaging is planned for a later version.

---

> ⚠️ **API Usage Notice:** This project uses an unofficial interface. Large-scale data retrieval (e.g., syncing long time ranges in a single run) may trigger rate limiting or temporary IP blocks by Garmin (HTTP 429).
>
> It is recommended to:
> - fetch data in smaller increments
> - include delays between requests
> - allow cool-down periods between sync sessions

---

*Built with Claude · [☕ buy me a coffee](https://ko-fi.com/wewoc)*

TDQS

A3.7/5.0

Scored across 7 tools

Disambiguation4/5

The core split is clear: query_health vs query_context vs query_fit_activities vs query_raw are separated by domain and by processed-vs-raw semantics, which the descriptions state explicitly. Minor overlap remains because query_raw also accepts a date range, field, and domain filter, so an agent could plausibly reach for it instead of the domain-specific query tools, and query_fit_activities is a placeholder that always returns 'not available'.

Naming Consistency5/5

Every tool follows a clean snake_case verb_noun pattern (query_health, query_context, query_raw, query_fit_activities, get_archive_metadata, list_available_fields, refresh_cache). The verb varies appropriately with the operation type (query/get/list/refresh) rather than being arbitrarily inconsistent.

Tool Count5/5

Seven tools is well-scoped for a local archive reader: three domain queries, a raw passthrough, metadata, field discovery, and cache refresh. Nothing feels padded, and no obvious capability is missing for the stated scope.

Completeness4/5

Coverage of the archive lifecycle is solid — field discovery, health/context/raw queries, metadata of many kinds, and a manual sync trigger address the main workflows. The one real gap is that activity/FIT data is unreachable until v1.8, so any fitness-activity question dead-ends at query_fit_activities.

Maintenance

ActivityActive
ResponsivenessResponsive