added models, model-swap, ...
This commit is contained in:
@@ -1,253 +1,41 @@
|
||||
# opencode setup
|
||||
# Garmin Data Fetcher
|
||||
|
||||
Canonical OpenCode config + Phoenix bridge plugin for the localgenai
|
||||
stack. `install.sh` deploys it to `~/.config/opencode/` on a Mac.
|
||||
|
||||
## What's wired up
|
||||
|
||||
- **Local models**: two providers, manually switched via `/model`.
|
||||
- `framework/qwen3-coder:30b` — Qwen3-Coder 30B-A3B via Ollama, the
|
||||
daily-driver coding model. 128K context, 11434.
|
||||
- `framework-vllm/kimi-linear` — Kimi-Linear 48B-A3B via vLLM, the
|
||||
long-context play (hybrid KDA/MLA, MoE 3B active). 32K context for
|
||||
now (ramps further in P3 of the kimi-linear roadmap), 8000.
|
||||
**Tools disabled** (`tool_call: false`) — Kimi-Linear is a research
|
||||
architecture release and isn't strongly tool-trained; the model
|
||||
knows the Kimi-K2 tool tokens but emits non-structured output when
|
||||
given an MCP toolbox. Use it for chat / long-context reasoning;
|
||||
switch to `framework/qwen3-coder:30b` for agentic work.
|
||||
- **Playwright MCP** ([@playwright/mcp](https://github.com/microsoft/playwright-mcp)) —
|
||||
browser automation. The model can navigate pages, click, fill forms,
|
||||
read DOM snapshots. Closes the agentic-browsing gap.
|
||||
- **SearXNG MCP** ([mcp-searxng](https://github.com/ihor-sokoliuk/mcp-searxng)) —
|
||||
web search via your self-hosted instance at <https://searxng.n0n.io>.
|
||||
No external API keys, no rate-limit roulette.
|
||||
- **Serena MCP** ([oraios/serena](https://github.com/oraios/serena)) —
|
||||
LSP-backed semantic code navigation (find symbol, references, rename,
|
||||
insert before/after). Cuts the tokens a local 70B-class model burns on
|
||||
grep-style flailing by roughly an order of magnitude. Uses a **custom
|
||||
trimmed context** (`serena-ide-trim.yml`) that exposes only the 8
|
||||
unique-LSP-value tools — JetBrains tools, line-level edits redundant
|
||||
with opencode's `Edit`, Serena's own memory tools (basic-memory MCP is
|
||||
canonical), and onboarding/meta noise are all excluded. Down from 46
|
||||
raw → 41 ide-context-filtered → **8 active**. Scoped to the cwd via
|
||||
`--project-from-cwd`.
|
||||
- **basic-memory MCP** ([basicmachines-co/basic-memory](https://github.com/basicmachines-co/basic-memory)) —
|
||||
Markdown-backed persistent memory across sessions. Storage lives in
|
||||
`~/Documents/obsidian/AI-memory/` (symlinked from `~/basic-memory`),
|
||||
so notes are browsable in Obsidian's graph and search. Replaces
|
||||
Claude Code's auto-memory write-back, which opencode lacks natively.
|
||||
- **sequential-thinking MCP** ([modelcontextprotocol/servers/sequentialthinking](https://github.com/modelcontextprotocol/servers/tree/main/src/sequentialthinking)) —
|
||||
externalizes chain-of-thought as tool calls. Helps weaker local
|
||||
models stay on-plan over multi-step work; near-zero cost when not
|
||||
actively used.
|
||||
- **github MCP** ([github/github-mcp-server](https://github.com/github/github-mcp-server)) —
|
||||
GitHub repo / issue / PR / code-search access. Launched with
|
||||
`--read-only` and a narrowed `--toolsets repos,issues,pull_requests,code_security`
|
||||
allowlist. With a **classic** PAT (`ghp_…`), GitHub's auto-scope-filtering
|
||||
(Jan 2026) trims tools further by hiding ones whose scopes the token
|
||||
lacks — saves ~23k tokens of tool-list overhead, meaningful for a 70B's
|
||||
effective context. Requires `GITHUB_PERSONAL_ACCESS_TOKEN` to be exported
|
||||
in your shell env (not in opencode.json). Drop `--read-only` from
|
||||
`opencode.json` once you trust the model's tool calls.
|
||||
|
||||
**Note**: This MCP is disabled since the user is utilizing a self-hosted Gitea instance instead of GitHub.
|
||||
- **task-master MCP** ([eyaltoledano/claude-task-master](https://github.com/eyaltoledano/claude-task-master)) —
|
||||
Workflow / task-gate MCP. File-based: each project gets a
|
||||
`.taskmaster/` dir with tasks, complexity, and config — no DB, no
|
||||
external service. `OLLAMA_BASE_URL` is pre-set in `opencode.json` so
|
||||
task-master's AI features (parse-prd, expand-task) route through your
|
||||
framework Ollama. The npm-global install also provides a `task-master`
|
||||
CLI (`task-master init` to scaffold per-project). Replaces the
|
||||
workflow-gate role originally proposed for Archon, without Supabase.
|
||||
- **Phoenix bridge plugin** (`.opencode/plugin/phoenix-bridge.js`) —
|
||||
exports OpenTelemetry spans for every LLM call, tool call, and
|
||||
subagent invocation to the Phoenix container running on the Framework
|
||||
Desktop. Per-prompt waterfall / flamegraph viz at
|
||||
<http://framework:6006>.
|
||||
A Python script to download per-second activity data (heart rate, GPS, cadence, etc.) from Garmin Connect and extract it from FIT files for analysis.
|
||||
|
||||
## Setup
|
||||
|
||||
```sh
|
||||
./install.sh
|
||||
1. Create virtual environment:
|
||||
```bash
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
```
|
||||
|
||||
Idempotent — re-run after editing `opencode.json` or pulling changes to
|
||||
the plugin. Each step checks before doing work. Specifically:
|
||||
|
||||
1. Verifies Homebrew is present (won't install it for you)
|
||||
2. `brew install node uv jq sst/tap/opencode` (skips if already at latest)
|
||||
3. Pre-caches Playwright's chromium so the first MCP call is instant
|
||||
4. `uv tool install serena-agent@latest --prerelease=allow` so opencode
|
||||
can launch Serena as a plain `serena` binary on PATH (faster than
|
||||
re-resolving via `uvx` on every session)
|
||||
5. Creates `~/Documents/obsidian/AI-memory/` and symlinks `~/basic-memory`
|
||||
to it, so basic-memory MCP writes into the Obsidian vault by default
|
||||
6. `brew install github-mcp-server` and warns if `GITHUB_PERSONAL_ACCESS_TOKEN`
|
||||
isn't set in your shell — the MCP needs it to authenticate
|
||||
7. `npm install -g task-master-ai` (workflow MCP, also exposes the
|
||||
`task-master` CLI for `task-master init` per project)
|
||||
8. `npm install` in `.opencode/plugin/` for the Phoenix bridge OTel deps
|
||||
9. Generates `~/.config/opencode/opencode.json` from the repo's
|
||||
`opencode.json`, rewriting relative plugin paths to absolute so
|
||||
OpenCode loads the plugin regardless of which directory it's launched
|
||||
from
|
||||
|
||||
Step 9 is the reason the deployed config isn't a plain symlink. The
|
||||
repo's `opencode.json` uses a relative plugin path (`./...`) so it stays
|
||||
valid in place; the deployed copy is generated with that path resolved
|
||||
to an absolute one. Edits to the repo's `opencode.json` need a re-run
|
||||
of `./install.sh` to take effect.
|
||||
|
||||
## Verify
|
||||
|
||||
```sh
|
||||
# Local model reachable
|
||||
curl -s http://framework:11434/v1/models | jq '.data[].id'
|
||||
|
||||
# SearXNG instance answers JSON
|
||||
curl -s 'https://searxng.n0n.io/search?q=test&format=json' | jq '.results | length'
|
||||
2. Install dependencies:
|
||||
```bash
|
||||
pip install garminconnect garmin_fit_sdk
|
||||
```
|
||||
|
||||
Then in opencode:
|
||||
|
||||
```
|
||||
opencode
|
||||
> /mcp # should list playwright, searxng, serena, basic-memory,
|
||||
# sequential-thinking, github, task-master as connected
|
||||
> search the web for "qwen3-coder benchmarks"
|
||||
> open https://example.com and tell me the H1
|
||||
> use serena to find the definition of `parse_request`
|
||||
> remember: this project ships its memory into the Obsidian vault
|
||||
> /sequentialthinking think through the trade-offs of X vs Y
|
||||
> list my recent github PRs across all repos
|
||||
> task-master init # then ask the model to plan tasks for this project
|
||||
3. Set environment variables:
|
||||
```bash
|
||||
export GARMIN_EMAIL=your_email@example.com
|
||||
export GARMIN_PASSWORD=your_password
|
||||
```
|
||||
|
||||
For parallel agents, plain tmux + git worktree is enough at the 70B's
|
||||
~2-pane concurrency ceiling. A two-line zsh helper covers the
|
||||
"new isolated worktree → split tmux pane → start opencode" loop:
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
work() {
|
||||
local name="${1:?usage: work <branch-name>}"
|
||||
local wt="../$(basename "$PWD")-$name"
|
||||
git worktree add "$wt" -b "$name" && tmux split-window -h -c "$wt" "opencode"
|
||||
}
|
||||
unwork() { local wt="$PWD"; cd .. && git worktree remove --force "$wt"; }
|
||||
Run the script to download and extract data:
|
||||
```bash
|
||||
python scripts/fetch_garmin_data.py
|
||||
```
|
||||
|
||||
Serena's first invocation in a project may take a few seconds — it
|
||||
indexes the workspace via the language server. basic-memory's first
|
||||
write creates the project layout under `~/Documents/obsidian/AI-memory/`
|
||||
which Obsidian will pick up on its next vault scan.
|
||||
The script will:
|
||||
- Authenticate with Garmin Connect
|
||||
- Download the most recent activity's FIT file
|
||||
- Extract per-second record data (heart rate, GPS, cadence, etc.)
|
||||
- Save the data as JSON for analysis
|
||||
|
||||
## Phoenix tracing
|
||||
## Output
|
||||
|
||||
The plugin at `.opencode/plugin/phoenix-bridge.js` boots an OpenTelemetry
|
||||
SDK on OpenCode startup and ships every span to Phoenix on the Framework
|
||||
Desktop. With `experimental.openTelemetry: true` (already set in
|
||||
`opencode.json`), OpenCode emits Vercel AI SDK spans that Phoenix renders
|
||||
as a per-turn waterfall: user prompt → main agent's `ai.streamText` →
|
||||
each tool call (built-in + MCP) with token counts and latencies inline.
|
||||
|
||||
The plugin uses `@opentelemetry/exporter-trace-otlp-proto` (not `-http`)
|
||||
because Phoenix's OTLP receiver only speaks protobuf — the JSON variant
|
||||
returns 415.
|
||||
|
||||
Spans go to Phoenix only. Earlier versions of this plugin dual-exported
|
||||
to OpenLIT as well, but OpenLIT's container doesn't currently host an
|
||||
OTLP receiver — the failing exporter cascaded into OpenCode's tool-call
|
||||
parsing pipeline and broke tool use. Re-enable once `openlit.yml` adds
|
||||
an `otel-collector` sidecar.
|
||||
|
||||
Defaults can be overridden via env vars (set before launching opencode):
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `PHOENIX_OTLP_ENDPOINT` | `http://framework:6006/v1/traces` | Phoenix HTTP target |
|
||||
| `PHOENIX_SERVICE_NAME` | `opencode` | Phoenix project name |
|
||||
| `PHOENIX_OTEL_DEBUG` | unset | `1` to surface OTel internal logs |
|
||||
|
||||
### Verifying
|
||||
|
||||
```sh
|
||||
: > /tmp/phoenix-bridge.log # truncate prior runs
|
||||
opencode # any directory; CWD doesn't matter
|
||||
tail -f /tmp/phoenix-bridge.log
|
||||
```
|
||||
|
||||
Healthy startup looks like:
|
||||
```
|
||||
plugin function entered
|
||||
endpoint=http://framework:6006/v1/traces serviceName=opencode
|
||||
OTel imports resolved
|
||||
sdk.start() returned
|
||||
tracer obtained
|
||||
boot span emitted (will flush within ~5s)
|
||||
```
|
||||
|
||||
Then open <http://framework:6006/projects> — an `opencode` project should
|
||||
appear with at least one `phoenix-bridge.boot` span. Send a prompt in
|
||||
OpenCode and real LLM-call traces follow.
|
||||
|
||||
If the plugin's deps aren't installed, OpenCode logs a warning and the
|
||||
plugin no-ops — the rest of OpenCode still works fine.
|
||||
|
||||
### Known limitations
|
||||
|
||||
- **Subagent nesting is best-effort.** The plugin opens a parent span
|
||||
per session and tries to stitch child sessions (Task-tool subagents)
|
||||
under their parent, but Vercel AI SDK spans live in their own OTel
|
||||
trace context. Until [sst/opencode#6142](https://github.com/sst/opencode/issues/6142)
|
||||
exposes `sessionID` in the `chat.system.transform` hook, child-session
|
||||
spans may show as separate traces in Phoenix.
|
||||
- **Console output from plugins is swallowed by OpenCode's TUI.** That's
|
||||
why init progress goes to `/tmp/phoenix-bridge.log` rather than stdout.
|
||||
|
||||
## Notes
|
||||
|
||||
- **SearXNG JSON output** must be enabled on the instance for the MCP
|
||||
server to work. If `format=json` returns HTML or 403, edit
|
||||
`settings.yml` on the SearXNG box: `search.formats: [html, json]`,
|
||||
restart.
|
||||
- **Playwright first-run** downloads ~200 MB of browser binaries into
|
||||
`~/Library/Caches/ms-playwright/`. Subsequent runs are instant.
|
||||
- **Tool-calling reliability** with Qwen3-Coder is decent but not
|
||||
Claude-grade. If a tool call hangs or returns malformed JSON, the
|
||||
model is the culprit, not the MCP. Worth trying the same prompt
|
||||
against a hosted Claude or GPT-5 to confirm before debugging the
|
||||
server.
|
||||
- **Adding more MCP servers**: drop another entry under the `mcp` key
|
||||
using the same `type/command/enabled` shape. The
|
||||
[official MCP registry](https://registry.modelcontextprotocol.io/)
|
||||
and [Awesome MCP Servers](https://mcpservers.org/) catalog options.
|
||||
- **Tool-list bloat is real on a local 70B.** Every tool description
|
||||
costs context. Five MCP servers exposing ~10 tools each puts the
|
||||
active-tool list around 50 — manageable, but adding two more
|
||||
full-spectrum servers (e.g. GitHub MCP at ~70 tools without scope
|
||||
filtering, plus Context7) starts crowding effective context. Prefer
|
||||
servers with toolset filtering or per-agent allow-lists in opencode.
|
||||
- **basic-memory storage path.** The symlink `~/basic-memory` →
|
||||
`~/Documents/obsidian/AI-memory` is created by `install.sh` only if
|
||||
`~/basic-memory` doesn't already exist. If you'd previously run
|
||||
basic-memory before this setup, move that directory's contents into
|
||||
`AI-memory/` first, then delete `~/basic-memory` and re-run
|
||||
`install.sh`.
|
||||
- **Serena PATH gotcha.** `uv tool install` puts `serena` in
|
||||
`~/.local/bin/`. If your shell rc doesn't export that, `opencode`
|
||||
won't find the binary. The script warns; fix is one line in
|
||||
`~/.zshrc`: `export PATH="$HOME/.local/bin:$PATH"`.
|
||||
- **Serena tool trim** (`serena-ide-trim.yml`). The custom context
|
||||
excludes 28 tools beyond what the built-in `ide` context already
|
||||
filters. To re-expose any of them, edit
|
||||
[`serena-ide-trim.yml`](serena-ide-trim.yml) and remove the entry
|
||||
from `excluded_tools`, then re-run `./install.sh`. The path injection
|
||||
(`./serena-ide-trim.yml` → absolute) is handled by install.sh's jq
|
||||
pass at deploy time.
|
||||
- **GitHub PAT.** Use a **classic** PAT (`ghp_…`) — auto-scope-filtering
|
||||
only kicks in for classic tokens, not fine-grained ones. Without
|
||||
it, the GitHub MCP exposes its full ~70-tool surface, which costs
|
||||
~23k tokens of context the local 70B can ill afford. Generate at
|
||||
<https://github.com/settings/tokens> with the scopes you actually
|
||||
want exposed.
|
||||
Data is saved in `garmin_data/` as:
|
||||
- `{activity_id}.fit` - Original FIT file
|
||||
- `{activity_id}_data.json` - Extracted per-second records
|
||||
@@ -9,7 +9,7 @@
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "Framework Desktop (Strix Halo) — Ollama",
|
||||
"options": {
|
||||
"baseURL": "http://framework:11434/v1"
|
||||
"baseURL": "http://10.0.0.70:11434/v1"
|
||||
},
|
||||
"models": {
|
||||
"qwen3-coder:30b": {
|
||||
@@ -25,7 +25,7 @@
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "Framework Desktop (Strix Halo) — vLLM",
|
||||
"options": {
|
||||
"baseURL": "http://framework:8000/v1",
|
||||
"baseURL": "http://10.0.0.70:8000/v1",
|
||||
"apiKey": "dummy"
|
||||
},
|
||||
"models": {
|
||||
@@ -43,7 +43,7 @@
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "Framework Desktop (Strix Halo) — llama.cpp (long-task)",
|
||||
"options": {
|
||||
"baseURL": "http://framework:8081/v1",
|
||||
"baseURL": "http://10.0.0.70:8081/v1",
|
||||
"apiKey": "dummy"
|
||||
},
|
||||
"models": {
|
||||
@@ -77,7 +77,7 @@
|
||||
"command": ["uvx", "kagimcp"],
|
||||
"enabled": true,
|
||||
"environment": {
|
||||
"KAGI_API_KEY": "${KAGI_API_KEY}"
|
||||
"KAGI_API_KEY": "gD6BmNHpHL2hLHYX0MnHhWMrmCjjs0dNIp4azxSTO0g.J64hRRR4NHIKcnEjcwyR4YV-6vuf622GsadLn8u4das"
|
||||
}
|
||||
},
|
||||
"serena": {
|
||||
@@ -115,7 +115,7 @@
|
||||
"command": ["npx", "-y", "task-master-ai"],
|
||||
"enabled": true,
|
||||
"environment": {
|
||||
"OLLAMA_BASE_URL": "http://framework:11434/v1"
|
||||
"OLLAMA_BASE_URL": "http://10.0.0.70:11434/v1"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
198
opencode/scripts/fetch_garmin_data.py
Normal file
198
opencode/scripts/fetch_garmin_data.py
Normal file
@@ -0,0 +1,198 @@
|
||||
import os
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
from garminconnect import Garmin
|
||||
from garmin_fit_sdk import Decoder
|
||||
|
||||
def get_db_path():
|
||||
"""Return path to SQLite database."""
|
||||
return os.path.join('garmin_data', 'garmin.db')
|
||||
|
||||
def init_database(db_path):
|
||||
"""Initialize SQLite database with required tables."""
|
||||
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
||||
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Create activities table
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS activities (
|
||||
id INTEGER PRIMARY KEY,
|
||||
start_time TEXT,
|
||||
end_time TEXT,
|
||||
distance REAL,
|
||||
duration INTEGER,
|
||||
activity_type TEXT,
|
||||
avg_heart_rate INTEGER,
|
||||
max_heart_rate INTEGER,
|
||||
avg_speed REAL,
|
||||
max_speed REAL,
|
||||
calories INTEGER,
|
||||
climb INTEGER,
|
||||
UNIQUE(id)
|
||||
)
|
||||
''')
|
||||
|
||||
# Create records table
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS records (
|
||||
id INTEGER PRIMARY KEY,
|
||||
activity_id INTEGER,
|
||||
timestamp TEXT,
|
||||
heart_rate INTEGER,
|
||||
cadence INTEGER,
|
||||
speed REAL,
|
||||
altitude REAL,
|
||||
latitude REAL,
|
||||
longitude REAL,
|
||||
power INTEGER,
|
||||
distance REAL,
|
||||
FOREIGN KEY (activity_id) REFERENCES activities (id)
|
||||
)
|
||||
''')
|
||||
|
||||
# Create indexes for better query performance
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_records_activity ON records(activity_id)')
|
||||
cursor.execute('CREATE INDEX IF NOT EXISTS idx_records_time ON records(timestamp)')
|
||||
|
||||
conn.commit()
|
||||
|
||||
def get_garmin_client(email, password):
|
||||
"""Authenticate with Garmin Connect."""
|
||||
try:
|
||||
client = Garmin(email, password)
|
||||
client.login()
|
||||
return client
|
||||
except Exception as e:
|
||||
print(f"Error authenticating: {e}")
|
||||
return None
|
||||
|
||||
def download_activities(client):
|
||||
"""Download activity list."""
|
||||
try:
|
||||
return client.get_activities(0, 1) # Get most recent activity
|
||||
except Exception as e:
|
||||
print(f"Error downloading activity list: {e}")
|
||||
return None
|
||||
|
||||
def download_fit_file(client, activity_id, output_dir):
|
||||
"""Download FIT file for activity."""
|
||||
try:
|
||||
fit_data = client.download_activity(activity_id, dl_fmt=client.ActivityDownloadFormat.ORIGINAL)
|
||||
fit_path = os.path.join(output_dir, f"{activity_id}.fit")
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
with open(fit_path, 'wb') as f:
|
||||
f.write(fit_data)
|
||||
return fit_path
|
||||
except Exception as e:
|
||||
print(f"Error downloading FIT file: {e}")
|
||||
return None
|
||||
|
||||
def extract_fit_data(fit_file_path):
|
||||
"""Extract data from FIT file."""
|
||||
try:
|
||||
decoder = Decoder()
|
||||
messages, errors = decoder.read_fit_file(fit_file_path)
|
||||
return messages, errors
|
||||
except Exception as e:
|
||||
print(f"Error decoding FIT file: {e}")
|
||||
return None, None
|
||||
|
||||
def save_to_database(db_path, messages):
|
||||
"""Save extracted data to SQLite database."""
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Extract activity metadata
|
||||
activity_message = next((m for m in messages if m['type'] == 'session'), None)
|
||||
if not activity_message:
|
||||
print("No session message found")
|
||||
return
|
||||
|
||||
# Insert activity
|
||||
cursor.execute('''
|
||||
INSERT OR IGNORE INTO activities
|
||||
(id, start_time, end_time, distance, duration, activity_type,
|
||||
avg_heart_rate, max_heart_rate, avg_speed, max_speed,
|
||||
calories, climb)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (
|
||||
activity_message['message']['start_time'],
|
||||
activity_message['message']['end_time'],
|
||||
activity_message['message']['total_distance'],
|
||||
activity_message['message']['total_elapsed_time'],
|
||||
activity_message['message']['sport'],
|
||||
activity_message['message']['avg_heart_rate'],
|
||||
activity_message['message']['max_heart_rate'],
|
||||
activity_message['message']['avg_speed'],
|
||||
activity_message['message']['max_speed'],
|
||||
activity_message['message']['total_calories'],
|
||||
activity_message['message']['total_ascent']
|
||||
))
|
||||
|
||||
# Insert records
|
||||
record_messages = [m for m in messages if m['type'] == 'record']
|
||||
for message in record_messages:
|
||||
record = message['message']
|
||||
cursor.execute('''
|
||||
INSERT INTO records
|
||||
(activity_id, timestamp, heart_rate, cadence, speed,
|
||||
altitude, latitude, longitude, power, distance)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (
|
||||
activity_message['message']['start_time'],
|
||||
record.get('timestamp'),
|
||||
record.get('heart_rate'),
|
||||
record.get('cadence'),
|
||||
record.get('speed'),
|
||||
record.get('altitude'),
|
||||
record.get('position_lat'),
|
||||
record.get('position_long'),
|
||||
record.get('power'),
|
||||
record.get('distance')
|
||||
))
|
||||
|
||||
conn.commit()
|
||||
print(f"Saved {len(record_messages)} records to database")
|
||||
|
||||
def main():
|
||||
# Authentication credentials
|
||||
email = os.getenv('GARMIN_EMAIL')
|
||||
password = os.getenv('GARMIN_PASSWORD')
|
||||
|
||||
if not email or not password:
|
||||
print("Please set GARMIN_EMAIL and GARMIN_PASSWORD environment variables")
|
||||
return
|
||||
|
||||
# Initialize database
|
||||
db_path = get_db_path()
|
||||
init_database(db_path)
|
||||
|
||||
# Initialize client
|
||||
client = get_garmin_client(email, password)
|
||||
if not client:
|
||||
return
|
||||
|
||||
# Get activities
|
||||
activities = download_activities(client)
|
||||
if not activities:
|
||||
return
|
||||
|
||||
activity_id = activities[0]['activityId']
|
||||
|
||||
# Download FIT file
|
||||
fit_path = download_fit_file(client, activity_id, 'garmin_data')
|
||||
if not fit_path:
|
||||
return
|
||||
|
||||
# Extract data
|
||||
messages, errors = extract_fit_data(fit_path)
|
||||
if errors:
|
||||
print(f"FIT file errors: {errors}")
|
||||
|
||||
# Save to database
|
||||
save_to_database(db_path, messages)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user