Skip to content
This repository was archived by the owner on Apr 30, 2026. It is now read-only.

Commit ce4a2c8

Browse files
a-dealclaude
andcommitted
Add sleep time backfill script for timezone bug fix
Re-pulls N days of Garmin history using fixed utcfromtimestamp extraction. INSERT OR REPLACE overwrites bad sleep_start/sleep_end values. Supports --dry-run to preview suspicious rows before pulling. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 35a472d commit ce4a2c8

1 file changed

Lines changed: 106 additions & 0 deletions

File tree

scripts/backfill_sleep_times.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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

Comments
 (0)