#!/usr/bin/env python3 """Validate and import the Android-owned Heart Rate V1 companion.""" import argparse import json import re import sqlite3 from datetime import datetime from pathlib import Path from trainlog_sqlite import connect_database from validate_json import parse_timestamp as exact_timestamp FORMAT = "trainlog-heart-rate" VERSION = 1 MAX_BYTES = 64 * 1024 * 1024 MAX_CAPTURES = 256 MAX_SAMPLES = 200_000 MAX_RR = 64 UUID4 = r"[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}" CAPTURE_ID = re.compile(rf"^hrc_{UUID4}$") SESSION_ID = re.compile(rf"^se_{UUID4}$") SLEEP_ID = re.compile(rf"^sl_{UUID4}$") ENTRY_ID = re.compile(rf"^sxe_{UUID4}$") def fail(message: str) -> None: raise ValueError(message) def unique_object(pairs): result = {} for key, value in pairs: if key in result: fail("duplicate JSON field: " + key) result[key] = value return result def timestamp(value: object) -> tuple[str, datetime]: if not isinstance(value, str) or re.search(r"(?:Z|[+-][0-9]{2}:[0-9]{2})$", value) is None: fail("invalid heart-rate timestamp") try: parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) except ValueError as error: raise ValueError("invalid heart-rate timestamp") from error if parsed.utcoffset() is None: fail("heart-rate timestamp lacks offset") return value, parsed def validate(root: object) -> dict: if not isinstance(root, dict) or set(root) != { "format", "version", "generated_at", "captures" }: fail("invalid heart-rate envelope") if root["format"] != FORMAT or type(root["version"]) is not int or root["version"] != VERSION: fail("unsupported heart-rate format") timestamp(root["generated_at"]) captures = root["captures"] if not isinstance(captures, list) or len(captures) > MAX_CAPTURES: fail("heart-rate capture bound exceeded") seen_captures = set() for capture in captures: keys = { "capture_id", "context_kind", "context_id", "started_at", "ended_at", "sensor_name", "samples", } if not isinstance(capture, dict) or set(capture) != keys: fail("invalid heart-rate capture") capture_id = capture["capture_id"] if not isinstance(capture_id, str) or not CAPTURE_ID.fullmatch(capture_id) or capture_id in seen_captures: fail("invalid or duplicate heart-rate capture identity") seen_captures.add(capture_id) kind = capture["context_kind"] context_id = capture["context_id"] if kind in ("session", "cardio"): if not isinstance(context_id, str) or not SESSION_ID.fullmatch(context_id): fail("invalid heart-rate session context") elif kind == "sleep": if not isinstance(context_id, str) or not SLEEP_ID.fullmatch(context_id): fail("invalid heart-rate sleep context") else: fail("invalid heart-rate context kind") _, started = timestamp(capture["started_at"]) _, ended = timestamp(capture["ended_at"]) if ended < started: fail("invalid heart-rate capture interval") sensor_name = capture["sensor_name"] if sensor_name is not None and ( not isinstance(sensor_name, str) or not sensor_name.strip() or len(sensor_name.encode()) > 160 ): fail("invalid heart-rate sensor name") samples = capture["samples"] if not isinstance(samples, list) or len(samples) > MAX_SAMPLES: fail("heart-rate sample bound exceeded") for expected, sample in enumerate(samples): sample_keys = { "sequence", "observed_at", "bpm", "exercise_entry_id", "sensor_contact_detected", "energy_expended", "rr_intervals_1024", } if not isinstance(sample, dict) or set(sample) != sample_keys: fail("invalid heart-rate sample") if type(sample["sequence"]) is not int or sample["sequence"] != expected: fail("invalid heart-rate sample sequence") _, observed = timestamp(sample["observed_at"]) if observed < started or observed > ended: fail("heart-rate sample outside capture") if type(sample["bpm"]) is not int or not 0 <= sample["bpm"] <= 65535: fail("invalid heart-rate bpm") entry_id = sample["exercise_entry_id"] if entry_id is not None and ( kind not in ("session", "cardio") or not isinstance(entry_id, str) or not ENTRY_ID.fullmatch(entry_id) ): fail("invalid heart-rate exercise context") contact = sample["sensor_contact_detected"] if contact is not None and type(contact) is not bool: fail("invalid sensor contact state") energy = sample["energy_expended"] if energy is not None and (type(energy) is not int or not 0 <= energy <= 65535): fail("invalid heart-rate energy value") rr = sample["rr_intervals_1024"] if not isinstance(rr, list) or len(rr) > MAX_RR or any( type(value) is not int or not 0 <= value <= 65535 for value in rr ): fail("invalid heart-rate RR interval") return root def load(path: Path) -> dict: if path.stat().st_size > MAX_BYTES: fail("heart-rate artifact exceeds 64 MiB") return validate(json.loads(path.read_text(encoding="utf-8"), object_pairs_hook=unique_object)) def _table_exists(db: sqlite3.Connection, table: str) -> bool: return db.execute( "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (table,), ).fetchone() is not None def _corrected_capture(db: sqlite3.Connection, capture: dict) -> dict: if not _table_exists(db, "sync_causal_state"): return capture prefix = capture["capture_id"] + "|" cutoffs = [] for target_id, in db.execute( "SELECT target_id FROM sync_causal_state " "WHERE target_kind='heart_rate_tail' AND deleted=1 AND target_id LIKE ?", (prefix + "%",), ): if not target_id.startswith(prefix): continue raw = target_id[len(prefix):] timestamp(raw) value, parsed = raw, exact_timestamp(raw, "heart_rate_cutoff") cutoffs.append((parsed, value)) if not cutoffs: return capture cutoff_key, cutoff = min(cutoffs) started = exact_timestamp(capture["started_at"], "started_at") ended = exact_timestamp(capture["ended_at"], "ended_at") if cutoff_key < started or cutoff_key > ended: fail("causal heart-rate cutoff is outside capture") samples = [sample for sample in capture["samples"] if exact_timestamp(sample["observed_at"], "observed_at") <= cutoff_key] if any(sample["sequence"] != expected for expected, sample in enumerate(samples)): fail("causal heart-rate truncation would break sample sequence") corrected = dict(capture) corrected["ended_at"] = cutoff corrected["samples"] = samples return corrected def _same_capture(db: sqlite3.Connection, capture: dict) -> bool: row = db.execute( "SELECT context_kind,context_id,started_at,ended_at,sensor_name " "FROM heart_rate_captures WHERE capture_id=?", (capture["capture_id"],), ).fetchone() if row is None: return False if tuple(row) != ( capture["context_kind"], capture["context_id"], capture["started_at"], capture["ended_at"], capture["sensor_name"], ): fail("heart-rate capture identity reused with different metadata") stored_samples = db.execute( "SELECT sequence,observed_at,bpm,exercise_entry_id,sensor_contact_detected," "energy_expended FROM heart_rate_samples WHERE capture_id=? ORDER BY sequence", (capture["capture_id"],), ).fetchall() if len(stored_samples) != len(capture["samples"]): fail("heart-rate capture identity reused with different samples") rr_by_sequence = {} for sequence, rr_index, value in db.execute( "SELECT sample_sequence,rr_index,value_1024 FROM heart_rate_rr_intervals " "WHERE capture_id=? ORDER BY sample_sequence,rr_index", (capture["capture_id"],), ): values = rr_by_sequence.setdefault(sequence, []) if rr_index != len(values): fail("stored heart-rate RR sequence is not contiguous") values.append(value) for row, sample in zip(stored_samples, capture["samples"]): contact = None if sample["sensor_contact_detected"] is None else int(sample["sensor_contact_detected"]) if tuple(row) != ( sample["sequence"], sample["observed_at"], sample["bpm"], sample["exercise_entry_id"], contact, sample["energy_expended"], ): fail("heart-rate sample identity reused with different content") if rr_by_sequence.pop(sample["sequence"], []) != sample["rr_intervals_1024"]: fail("heart-rate RR identity reused with different content") if rr_by_sequence: fail("heart-rate capture has orphan RR intervals") return True def apply(db: sqlite3.Connection, root: dict) -> tuple[int, int]: applied = 0 unchanged = 0 for raw_capture in root["captures"]: # WHY: acknowledged Heart Rate V1 generations are immutable, while a # later causal correction may remove a sensor tail from history. # CONTRACT: replay compares/inserts the old snapshot only after every # durable inclusive cutoff for this capture has been applied. # INVARIANT: a corrected tail cannot be resurrected by old bytes. capture = _corrected_capture(db, raw_capture) if _same_capture(db, capture): unchanged += 1 continue db.execute( "INSERT INTO heart_rate_captures(" "capture_id,context_kind,context_id,started_at,ended_at,sensor_name,imported_at" ") VALUES(?,?,?,?,?,?,?)", ( capture["capture_id"], capture["context_kind"], capture["context_id"], capture["started_at"], capture["ended_at"], capture["sensor_name"], root["generated_at"], ), ) db.executemany( "INSERT INTO heart_rate_samples(" "capture_id,sequence,observed_at,bpm,exercise_entry_id," "sensor_contact_detected,energy_expended) VALUES(?,?,?,?,?,?,?)", [ ( capture["capture_id"], sample["sequence"], sample["observed_at"], sample["bpm"], sample["exercise_entry_id"], None if sample["sensor_contact_detected"] is None else int(sample["sensor_contact_detected"]), sample["energy_expended"], ) for sample in capture["samples"] ], ) rr_rows = [] for sample in capture["samples"]: rr_rows.extend( ( capture["capture_id"], sample["sequence"], index, value ) for index, value in enumerate(sample["rr_intervals_1024"]) ) db.executemany( "INSERT INTO heart_rate_rr_intervals(" "capture_id,sample_sequence,rr_index,value_1024) VALUES(?,?,?,?)", rr_rows, ) applied += 1 return applied, unchanged def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("input", type=Path) parser.add_argument("--database", required=True, type=Path) args = parser.parse_args() root = load(args.input) with connect_database(args.database) as db: if db.execute("PRAGMA user_version").fetchone()[0] not in (30, 31, 32, 33, 34, 35, 36, 37): fail("desktop schema v30-v37 required") db.execute("BEGIN IMMEDIATE") apply(db, root) db.commit() if __name__ == "__main__": main()