AHDX: Apple Health exporter with SQLite + Grafana dashboard (MIT)
This commit is contained in:
+116
@@ -0,0 +1,116 @@
|
||||
-- Schema for one Apple Health database.
|
||||
--
|
||||
-- Apple's export.xml is flat: a big pile of <Record> rows plus some <Workout>
|
||||
-- and <ActivitySummary> rows. We keep that shape instead of splitting the
|
||||
-- hundreds of health "types" into their own tables, because Apple adds new
|
||||
-- types with every iOS release and we don't want a migration each time.
|
||||
--
|
||||
-- Records have no ID from Apple, so we build our own uniqueness key and use
|
||||
-- INSERT OR IGNORE on import. That way re-scanning the same export is a no-op
|
||||
-- and a newer export only adds the rows we haven't seen.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
type TEXT NOT NULL, -- e.g. HKQuantityTypeIdentifierHeartRate
|
||||
source_name TEXT,
|
||||
source_version TEXT,
|
||||
device TEXT,
|
||||
unit TEXT,
|
||||
value TEXT, -- raw value straight from the file
|
||||
value_num REAL, -- the same value as a number, or NULL if it isn't one
|
||||
start_date TEXT,
|
||||
end_date TEXT,
|
||||
creation_date TEXT
|
||||
);
|
||||
|
||||
-- The natural key. COALESCE keeps NULLs from slipping past the uniqueness check
|
||||
-- (in SQLite two NULLs are treated as distinct, which would let duplicates in).
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_records ON records (
|
||||
type,
|
||||
COALESCE(start_date, ''),
|
||||
COALESCE(end_date, ''),
|
||||
COALESCE(value, ''),
|
||||
COALESCE(source_name, '')
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_records_type_start ON records (type, start_date);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS workouts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
activity_type TEXT,
|
||||
duration REAL,
|
||||
duration_unit TEXT,
|
||||
total_distance REAL,
|
||||
distance_unit TEXT,
|
||||
total_energy REAL,
|
||||
energy_unit TEXT,
|
||||
start_date TEXT,
|
||||
end_date TEXT,
|
||||
source_name TEXT
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_workouts ON workouts (
|
||||
COALESCE(activity_type, ''),
|
||||
COALESCE(start_date, ''),
|
||||
COALESCE(end_date, ''),
|
||||
COALESCE(source_name, '')
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS activity_summary (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
date TEXT UNIQUE, -- one summary per day
|
||||
active_energy REAL,
|
||||
active_energy_goal REAL,
|
||||
move_time REAL,
|
||||
move_time_goal REAL,
|
||||
exercise_time REAL,
|
||||
exercise_time_goal REAL,
|
||||
stand_hours REAL,
|
||||
stand_hours_goal REAL
|
||||
);
|
||||
|
||||
-- GPS tracks from outdoor workouts (the workout-routes/*.gpx files in the zip).
|
||||
CREATE TABLE IF NOT EXISTS routes (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
filename TEXT UNIQUE,
|
||||
start_date TEXT,
|
||||
point_count INTEGER
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS route_points (
|
||||
route_id INTEGER NOT NULL REFERENCES routes(id),
|
||||
lat REAL,
|
||||
lon REAL,
|
||||
ele REAL,
|
||||
t TEXT
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_route_points ON route_points(route_id);
|
||||
|
||||
-- ECG readings (the electrocardiograms/*.csv files). Each is a short waveform
|
||||
-- plus some header lines; we keep the metadata we can read and all the samples.
|
||||
CREATE TABLE IF NOT EXISTS ecg (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
filename TEXT UNIQUE,
|
||||
recorded_date TEXT,
|
||||
classification TEXT,
|
||||
sample_rate REAL,
|
||||
duration_s REAL,
|
||||
unit TEXT
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS ecg_samples (
|
||||
ecg_id INTEGER NOT NULL REFERENCES ecg(id),
|
||||
idx INTEGER,
|
||||
uv REAL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_ecg_samples ON ecg_samples(ecg_id);
|
||||
|
||||
-- One row, updated while an import runs so the web page can show progress.
|
||||
CREATE TABLE IF NOT EXISTS import_status (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
state TEXT, -- idle | running | done | error
|
||||
records_seen INTEGER DEFAULT 0, -- rows read from the file
|
||||
records_new INTEGER DEFAULT 0, -- rows that were actually new
|
||||
current_type TEXT,
|
||||
source TEXT, -- what triggered it: upload, inbox filename, or ingest
|
||||
message TEXT,
|
||||
started_at TEXT,
|
||||
finished_at TEXT
|
||||
);
|
||||
INSERT OR IGNORE INTO import_status (id, state) VALUES (1, 'idle');
|
||||
Reference in New Issue
Block a user