163 lines
4.4 KiB
Python
163 lines
4.4 KiB
Python
|
|
"""SQLite schema + connection helpers."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import sqlite3
|
||
|
|
from pathlib import Path
|
||
|
|
|
||
|
|
DB_PATH = Path(__file__).parent / "data" / "garmin.db"
|
||
|
|
|
||
|
|
SCHEMA = """
|
||
|
|
CREATE TABLE IF NOT EXISTS activities (
|
||
|
|
activity_id INTEGER PRIMARY KEY,
|
||
|
|
start_time_local TEXT,
|
||
|
|
start_time_gmt TEXT,
|
||
|
|
activity_type TEXT,
|
||
|
|
activity_name TEXT,
|
||
|
|
distance_m REAL,
|
||
|
|
duration_s REAL,
|
||
|
|
moving_duration_s REAL,
|
||
|
|
avg_speed_mps REAL,
|
||
|
|
max_speed_mps REAL,
|
||
|
|
avg_hr REAL,
|
||
|
|
max_hr REAL,
|
||
|
|
calories REAL,
|
||
|
|
elevation_gain_m REAL,
|
||
|
|
elevation_loss_m REAL,
|
||
|
|
training_load REAL,
|
||
|
|
aerobic_te REAL,
|
||
|
|
anaerobic_te REAL,
|
||
|
|
vo2_max REAL,
|
||
|
|
raw TEXT NOT NULL,
|
||
|
|
fetched_at TEXT NOT NULL
|
||
|
|
);
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_activities_start ON activities(start_time_local);
|
||
|
|
CREATE INDEX IF NOT EXISTS idx_activities_type ON activities(activity_type);
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS activity_splits (
|
||
|
|
activity_id INTEGER NOT NULL,
|
||
|
|
split_index INTEGER NOT NULL,
|
||
|
|
distance_m REAL,
|
||
|
|
duration_s REAL,
|
||
|
|
avg_hr REAL,
|
||
|
|
avg_speed_mps REAL,
|
||
|
|
elevation_gain_m REAL,
|
||
|
|
raw TEXT NOT NULL,
|
||
|
|
PRIMARY KEY (activity_id, split_index),
|
||
|
|
FOREIGN KEY (activity_id) REFERENCES activities(activity_id)
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS daily_steps (
|
||
|
|
calendar_date TEXT PRIMARY KEY,
|
||
|
|
total_steps INTEGER,
|
||
|
|
step_goal INTEGER,
|
||
|
|
distance_m REAL,
|
||
|
|
raw TEXT NOT NULL,
|
||
|
|
fetched_at TEXT NOT NULL
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS daily_sleep (
|
||
|
|
calendar_date TEXT PRIMARY KEY,
|
||
|
|
sleep_start_gmt TEXT,
|
||
|
|
sleep_end_gmt TEXT,
|
||
|
|
deep_s REAL,
|
||
|
|
light_s REAL,
|
||
|
|
rem_s REAL,
|
||
|
|
awake_s REAL,
|
||
|
|
sleep_score REAL,
|
||
|
|
raw TEXT NOT NULL,
|
||
|
|
fetched_at TEXT NOT NULL
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS daily_stress (
|
||
|
|
calendar_date TEXT PRIMARY KEY,
|
||
|
|
avg_stress REAL,
|
||
|
|
max_stress REAL,
|
||
|
|
raw TEXT NOT NULL,
|
||
|
|
fetched_at TEXT NOT NULL
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS daily_hrv (
|
||
|
|
calendar_date TEXT PRIMARY KEY,
|
||
|
|
weekly_avg REAL,
|
||
|
|
last_night_avg REAL,
|
||
|
|
last_night_5min REAL,
|
||
|
|
status TEXT,
|
||
|
|
raw TEXT NOT NULL,
|
||
|
|
fetched_at TEXT NOT NULL
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS daily_body_battery (
|
||
|
|
calendar_date TEXT PRIMARY KEY,
|
||
|
|
charged INTEGER,
|
||
|
|
drained INTEGER,
|
||
|
|
highest INTEGER,
|
||
|
|
lowest INTEGER,
|
||
|
|
raw TEXT NOT NULL,
|
||
|
|
fetched_at TEXT NOT NULL
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS daily_intensity_minutes (
|
||
|
|
calendar_date TEXT PRIMARY KEY,
|
||
|
|
moderate_minutes INTEGER,
|
||
|
|
vigorous_minutes INTEGER,
|
||
|
|
raw TEXT NOT NULL,
|
||
|
|
fetched_at TEXT NOT NULL
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS daily_resting_hr (
|
||
|
|
calendar_date TEXT PRIMARY KEY,
|
||
|
|
resting_hr INTEGER,
|
||
|
|
raw TEXT NOT NULL,
|
||
|
|
fetched_at TEXT NOT NULL
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS activity_fit_files (
|
||
|
|
activity_id INTEGER PRIMARY KEY,
|
||
|
|
fit_path TEXT NOT NULL,
|
||
|
|
indexed_at TEXT NOT NULL
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS activity_time_in_zone (
|
||
|
|
activity_id INTEGER PRIMARY KEY,
|
||
|
|
z1_s REAL,
|
||
|
|
z2_s REAL,
|
||
|
|
z3_s REAL,
|
||
|
|
z4_s REAL,
|
||
|
|
z5_s REAL,
|
||
|
|
total_s REAL,
|
||
|
|
source TEXT,
|
||
|
|
computed_at TEXT NOT NULL,
|
||
|
|
FOREIGN KEY (activity_id) REFERENCES activities(activity_id)
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE TABLE IF NOT EXISTS sync_state (
|
||
|
|
key TEXT PRIMARY KEY,
|
||
|
|
value TEXT NOT NULL,
|
||
|
|
updated_at TEXT NOT NULL
|
||
|
|
);
|
||
|
|
"""
|
||
|
|
|
||
|
|
|
||
|
|
def connect() -> sqlite3.Connection:
|
||
|
|
DB_PATH.parent.mkdir(exist_ok=True)
|
||
|
|
conn = sqlite3.connect(DB_PATH)
|
||
|
|
conn.execute("PRAGMA journal_mode = WAL")
|
||
|
|
conn.execute("PRAGMA foreign_keys = ON")
|
||
|
|
conn.row_factory = sqlite3.Row
|
||
|
|
conn.executescript(SCHEMA)
|
||
|
|
return conn
|
||
|
|
|
||
|
|
|
||
|
|
def set_state(conn: sqlite3.Connection, key: str, value: str) -> None:
|
||
|
|
conn.execute(
|
||
|
|
"INSERT INTO sync_state(key, value, updated_at) VALUES (?, ?, datetime('now')) "
|
||
|
|
"ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=excluded.updated_at",
|
||
|
|
(key, value),
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def get_state(conn: sqlite3.Connection, key: str) -> str | None:
|
||
|
|
row = conn.execute("SELECT value FROM sync_state WHERE key = ?", (key,)).fetchone()
|
||
|
|
return row["value"] if row else None
|