|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Backfill sleep_start/sleep_end in wearable_daily. |
| 3 | +
|
| 4 | +The Garmin timezone bug (fixed in 64d5843) stored sleep times 7 hours |
| 5 | +early due to double-converting local timestamps. This script re-pulls |
| 6 | +90 days of Garmin data using the fixed extraction code, which overwrites |
| 7 | +the bad values via INSERT OR REPLACE. |
| 8 | +
|
| 9 | +Usage: |
| 10 | + python3 scripts/backfill_sleep_times.py [--days N] [--user USER_ID] [--dry-run] |
| 11 | +""" |
| 12 | + |
| 13 | +import argparse |
| 14 | +import sys |
| 15 | +from pathlib import Path |
| 16 | + |
| 17 | +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) |
| 18 | + |
| 19 | + |
| 20 | +def main(): |
| 21 | + parser = argparse.ArgumentParser(description="Backfill sleep times from Garmin") |
| 22 | + parser.add_argument("--days", type=int, default=90, help="Days of history to re-pull") |
| 23 | + parser.add_argument("--user", default="andrew", help="User ID") |
| 24 | + parser.add_argument("--dry-run", action="store_true", help="Show what would change without pulling") |
| 25 | + args = parser.parse_args() |
| 26 | + |
| 27 | + from engine.gateway.db import get_db, init_db |
| 28 | + init_db() |
| 29 | + db = get_db() |
| 30 | + |
| 31 | + # Show current bad data |
| 32 | + person_row = db.execute( |
| 33 | + "SELECT id FROM person WHERE health_engine_user_id = ?", (args.user,) |
| 34 | + ).fetchone() |
| 35 | + if not person_row: |
| 36 | + print(f"No person found for user_id={args.user}") |
| 37 | + sys.exit(1) |
| 38 | + |
| 39 | + person_id = person_row["id"] |
| 40 | + rows = db.execute( |
| 41 | + """SELECT date, sleep_start, sleep_end FROM wearable_daily |
| 42 | + WHERE person_id = ? AND source = 'garmin' AND sleep_start IS NOT NULL |
| 43 | + ORDER BY date DESC LIMIT ?""", |
| 44 | + (person_id, args.days), |
| 45 | + ).fetchall() |
| 46 | + |
| 47 | + print(f"Current sleep times for {args.user} ({len(rows)} rows with sleep data):") |
| 48 | + suspicious = 0 |
| 49 | + for r in rows: |
| 50 | + start = r["sleep_start"] |
| 51 | + # Sleep start between 06:00-18:00 is suspicious (should be evening) |
| 52 | + h = int(start.split(":")[0]) if start else 0 |
| 53 | + flag = " <-- SUSPICIOUS" if 6 <= h <= 18 else "" |
| 54 | + if flag: |
| 55 | + suspicious += 1 |
| 56 | + print(f" {r['date']} start={start} end={r['sleep_end']}{flag}") |
| 57 | + |
| 58 | + print(f"\n{suspicious}/{len(rows)} rows look wrong (sleep_start during daytime)") |
| 59 | + |
| 60 | + if args.dry_run: |
| 61 | + print("\n--dry-run: skipping re-pull") |
| 62 | + return |
| 63 | + |
| 64 | + if suspicious == 0: |
| 65 | + print("No suspicious rows found. Nothing to backfill.") |
| 66 | + return |
| 67 | + |
| 68 | + print(f"\nRe-pulling {args.days} days of Garmin history with fixed timezone code...") |
| 69 | + from engine.integrations.garmin import GarminClient |
| 70 | + from engine.gateway.token_store import TokenStore |
| 71 | + |
| 72 | + ts = TokenStore() |
| 73 | + if not ts.has_token("garmin", args.user): |
| 74 | + print(f"No Garmin tokens for {args.user}") |
| 75 | + sys.exit(1) |
| 76 | + |
| 77 | + token_dir = str(ts.garmin_token_dir(args.user)) |
| 78 | + client = GarminClient(token_dir=token_dir, token_store=ts, user_id=args.user) |
| 79 | + client.connect() |
| 80 | + series = client.pull_daily_series(days=args.days, person_id=person_id) |
| 81 | + ts.sync_garmin_tokens(args.user) |
| 82 | + |
| 83 | + print(f"\nPulled {len(series)} days. Verifying fix...") |
| 84 | + |
| 85 | + # Show updated data |
| 86 | + rows_after = db.execute( |
| 87 | + """SELECT date, sleep_start, sleep_end FROM wearable_daily |
| 88 | + WHERE person_id = ? AND source = 'garmin' AND sleep_start IS NOT NULL |
| 89 | + ORDER BY date DESC LIMIT 10""", |
| 90 | + (person_id,), |
| 91 | + ).fetchall() |
| 92 | + |
| 93 | + fixed = 0 |
| 94 | + for r in rows_after: |
| 95 | + start = r["sleep_start"] |
| 96 | + h = int(start.split(":")[0]) if start else 0 |
| 97 | + status = "OK" if not (6 <= h <= 18) else "STILL BAD" |
| 98 | + if status == "OK": |
| 99 | + fixed += 1 |
| 100 | + print(f" {r['date']} start={start} end={r['sleep_end']} [{status}]") |
| 101 | + |
| 102 | + print(f"\n{fixed}/{len(rows_after)} recent rows now have correct sleep times.") |
| 103 | + |
| 104 | + |
| 105 | +if __name__ == "__main__": |
| 106 | + main() |
0 commit comments